Hatred's Log Place

DON'T PANIC!

Sep 18, 2013 - 2 minute read - programming c++

Забавный баг

Есть некая система (embedded), у которой вся память поделена на пулы различного размера и способы выделения память (фиксированный размер, переменный размер). Есть механизм для выделения памяти из этих пулов (на основе размера аллокации подбирает подходящий пул и делает от туда выделение) и возвращение её туда, примем его таким:

void *pool_alloc(size_t size); // выделение памяти из пула
void *pool_calloc(size_t size); // выделение памяти из пула и зануление её
void pool_free(void *ptr); // возврат (освобождение) памяти в пул

есть так же определённые операторы new/new[]/delete/delete[] для использования этого механизма в C++ коде (код сильно упрощён):

void* operator new (size_t size)
{
    return pool_alloc(size);
}

void* operator new[] (size_t size)
{
    return pool_alloc(size);
}

void operator delete (void *ptr)
{
    pool_free(ptr);
}

void operator delete[] (void *ptr)
{
    pool_free(ptr);
}

На этом вводная часть закончена. Переходим к сути.

Существовала испокон веков некая большая структура (в терминологии C++ - POD тип), и код, использующий её был повсеместно на чистом C. Назовём эту структуру так:

struct BigStruct 
{
    // a lot of fields
};

Соответственно память под эту структуру выделялась и освобождалась так:

...
big_struct_ptr = (struct BigStruct*)pool_calloc(sizeof(struct BigStruct)); // память сразу занулялась
...
pool_free(big_struct_ptr);
...

Время шло, код постепенно “переписывался” на C++. В кавычках, потому как это “переписывание” сводилось к изменению расширения файла на .cpp и исправлению очевидных ошибок и предупреждений (тут, с одной стороны, используется -Werror как опция для GCC, с другой стороны, предупреждение включены далеко не все). И вот, в один прекрасный момент, весь код, который использует эту структуру, уже компилируется C++ компилятором. Как результат, при очередной фиче или багофиксе в структуре появляется поле типа

std::vector<Foo> some_field;

То есть, одним мановением руки, структура перестаёт быть POD-типом, но… Механизм выделения и освобождения памяти для неё не меняется!

Теперь сделайте паузу и пофантазируйте, к чему это приводит.

На практике, это решение жило около двух лет (на момент написания статьи) и никак себя не проявляло. А приводило оно к постепенной утечке памяти: для std::vector не вызывался ни конструктор ни деструктор. Отсутствие вызова конструктора проходило незаметно, в этом конкретном примере “спасало” то, что память занулялась и у класса нет vtable. А вот отсутствие вызова деструктора приводило к тому, что все аллокации, сделанные внутри вектора, не освобождались. Маскировало эту проблему то, что аллокации были очень маленькие (около 4 байт), и их было немного за весь период нормальной работы аппарата от момента включения до выключения.

Выявило нагрузочное тестирование, где один алгоритм прогонялся атипичное, для нормальной работы, количество раз (помогло логирование вызовов new при помощи gcc’шной __builtin_return_address(), objdump -d и базовые знания ассемблера).