Магические константы и функция malloc()

Андрей Карпов
Статей: 370



Вновь хочется вернуться к вопросу использования магических констант в коде. Можно сколько угодно говорить, что для вычисления корректного размера выделяемой памяти необходимо использовать оператор sizeof(). Но эти знания и корректное написание нового кода не помогут в диагностировании уже существующей ошибки в дебрях старого кода в больших проектах.

Приведем классический пример ошибки:

size_t nCount = 10;
int **poinerArray = (int **)malloc(nCount * 4);

Код некорректен, но в рамках одной 32-битной системы он будет стабильно работать. Ошибка может проявить себя при адаптации программы к другой программной/аппаратной среде. Важность обнаружения подобного кода стала по настоящему актуальна в связи с массовым переносом программного обеспечения на 64-битные системы. Изменение размеров ряда базовых типов делает код, аналогичный выше приведенному, крайне опасным.

На приведенный выше код статический анализатор Viva64, входящий в состав PVS-Studio, выдаст предупреждение об использование магической константы "4″ и ошибка будет обнаружена в ходе просмотра диагностических сообщений. Но код может быть более запутан:

#define N_COUNT 100
#define POINTER_SIZE 4
#define NSIZE (N_COUNT * POINTER_SIZE)
int **pArray = (int **)malloc(NSIZE);

Ошибку в подобном коде, написанном в стиле языка Си с использованием #define, диагностировать сложнее. Хотя в коде присутствует константа 4, заданная через макрос, в анализаторе Viva64 сознательно заблокирован вывод предупреждения на подобные конструкции. Анализатор игнорирует магические константы, задаваемые макросами (#define) по двум причинам. Во-первых, если программист задает константы через макросы, то, скорее всего, он знает, что делает и очень высока вероятность ложного срабатывания. Во-вторых, если реагировать на опасные с точки зрения 64-битности константы (4, 8, 32 и т.д.), то возникнет лавина ложных срабатываний связанная с использованием Windows API. В качестве примера приведем безобиднейший код:

MessageBox("Are you sure ?",
           "Some question",
           MB_YESNO | MB_ICONQUESTION);

Если анализировать магические числа, скрывающиеся за макросами MB_YESNO и MB_ICONQUESTION, то на приведенную строчку необходимо выдать два предупреждения об использовании магических констант 4 и 32. Естественно это будет неприемлемо высоким уровнем ложных срабатываний.

Можно при анализе функции malloc() выводить информацию о всех опасных магических константах, независимо, макрос это или нет. Но и этого будет недостаточно для следующего случая:

int **pArray = (int **)malloc(400);

Если пойти дальше и считать любое число, используемое в выражении для функции malloc() опасным, то это тоже повлечет к ложным срабатываниям на корректном коде:

int **pArray = (int **)malloc(400 * sizeof(int *));

Проанализировав ситуацию, мы решили реализовать новое правило, для верификации выражений, результат которых передается в функцию malloc(). На данный момент правило сформулировано следующим образом:

Опасным следует считать использование числовых литералов в выражении, передаваемом в функцию malloc().

Исключения:

1) В выражении присутствует оператор sizeof()

2) Все числовые литералы делятся на 4 c остатком

Благодаря данному правилу мы сможем предупредить об ошибке в следующем коде:

1) Первый пример:

void *p = malloc(nCount * 4);

2) Второй пример:

#define N_COUNT 100
#define POINTER_SIZE 4
#define NSIZE (N_COUNT * POINTER_SIZE)
int **pArray = (int **)malloc(NSIZE);

А также не выдать ложное предупреждение на код вида:

1) Первый пример:

void *p = malloc(sizeof(double) * 4);

2) Второй пример:

#define N_COUNT 100
#define POINTER_SIZE sizeof(int *)
#define NSIZE (N_COUNT * POINTER_SIZE)
int **pArray = (int **)malloc(NSIZE);

Скорее всего, это новое диагностическое правило появится в следующей версии PVS-Studio 3.30.

Рассмотрим теперь другую ситуацию, также связанную с функцией malloc() и неверным предположением о выравнивании данных. Это не совсем связано с магическими числами, но проблема очень родственная. Рассмотрим пример кода:

struct MyBigStruct {
  unsigned m_numberOfPointers;
  void *m_Pointers[1];
};
unsigned n = 10000;
void *ptr = malloc(sizeof(unsigned) +
                   n * sizeof(void *));

Хотя в данном коде не используется магических чисел, размер типов определяется через sizeof(), код все равно не корректен. В нем неучтено изменение способа выравнивания данных, различного для 32-битных и 64-битных систем. Корректным будет следующий код:

void *ptr = malloc(
  offsetof(MyBigStruct, m_Pointers) +
  n * sizeof(void *));

Чтобы предупредить пользователя о возможной допущенной ошибке мы планируем реализовать еще одно правило:

Опасным следует считать использование более одного оператора sizeof() в выражении, передаваемом в функцию malloc. Возможно, при вычислении размера структуры не учитывается изменение выравнивания.

В ряде случаев такое правило будет давать ложные срабатывания, но подобные места в любом случае стоит тщательно проверить.

Описанные выше опасные выражения с участием магических констант актуальны не только для функции malloc(), но и для класса таких функций как fread, fwrite и так далее. Но эти функции требуют отдельного изучения, и к их анализу мы приступим позже, когда отработаем диагностику, связанную с malloc().



Найдите ошибки в своем C, C++, C# и Java коде

Предлагаем попробовать проверить код вашего проекта с помощью анализатора кода PVS-Studio. Одна найденная в нём ошибка скажет вам о пользе методологии статического анализа кода больше, чем десяток статей.

goto PVS-Studio;

Андрей Карпов
Статей: 370


Найденные ошибки

Проверено проектов
344
Собрано ошибок
12 970

А ты совершаешь ошибки в коде?

Проверь с помощью
PVS-Studio

Статический анализ
кода для C, C++, C#
и Java

goto PVS-Studio;