>
>
>
Красивая 64-битная ошибка на языке Си

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

Красивая 64-битная ошибка на языке Си

В языке Си можно использовать функции без их предварительного объявления. Обращаю внимание, что речь идет именно о языке Си, а не Си++. Конечно, данная возможность опасна. Рассмотрим связанный с ней интересный пример 64-битной ошибки.

Ниже приведен корректный код, который выделяет и использует три массива размером 1 гигабайт каждый:

#include <stdlib.h>
void test()
{
  const size_t Gbyte = 1024 * 1024 * 1024;
  size_t i;
  char *Pointers[3];
  // Allocate
  for (i = 0; i != 3; ++i)
    Pointers[i] = (char *)malloc(Gbyte);
  // Use
  for (i = 0; i != 3; ++i)
    Pointers[i][0] = 1;
  // Free
  for (i = 0; i != 3; ++i)
    free(Pointers[i]);
}

Данный код корректно выделит память, запишет в первый элемент каждого массива по единице и освободит занятую память. Код совершенно корректно работает на 64-битной системе.

Теперь удалим или закомментируем строчку "#include <stdlib.h>". Код по-прежнему будет собираться, но при запуске программы произойдет ее аварийное завершение. Поскольку заголовочный файл "stdlib.h" не подключен, компилятор языка Си считает, что функция malloc вернет тип int. Первые два выделения памяти, скорее всего, пройдут успешно. При третьем обращении функция malloc вернет адрес массива за пределами первых 2-х гигабайт. Поскольку компилятор считает, что результат работы функции имеет тип int, он неверно интерпретирует результат и сохраняет в массиве Pointers некорректное значение указателя.

Для лучшего пояснения приведу ассемблерный код, генерируемый компилятором Visual C++ для 64-битной Debug версии. Вначале рассмотрим корректный код, который будет сгенерирован, когда присутствует объявление функции malloc (подключен файл "stdlib.h"):

Pointers[i] = (char *)malloc(Gbyte);
mov   rcx,qword ptr [Gbyte]
call  qword ptr [__imp_malloc (14000A518h)]
mov    rcx,qword ptr [i]
mov    qword ptr Pointers[rcx*8],rax

Теперь рассмотрим вариант некорректного кода, когда отсутствует объявление функции malloc:

Pointers[i] = (char *)malloc(Gbyte);
mov    rcx,qword ptr [Gbyte]
call   malloc (1400011A6h)
cdqe
mov    rcx,qword ptr [i]
mov    qword ptr Pointers[rcx*8],rax

Обратите внимание на наличие инструкции CDQE (Convert doubleword to quadword). Компилятор посчитал, что результат содержится в регистре eax и расширил его до 64-битного значения, чтобы записать в массив Pointers. Соответственно старшие биты регистра rax будут потеряны. Если даже адрес выделенной памяти лежит в пределах первых четырех гигабайт, в случае, когда старший бит регистра eax равен 1 мы все равно получим некорректный результат. Например, адрес 0x81000000 превратится в 0xFFFFFFFF81000000.

К счастью данный вид ошибки легко обнаружить. Например, компилятор Visual C++ выдает два предупреждения, свидетельствующих о потенциальной проблеме:

warning C4013: 'malloc' undefined; assuming extern returning int

warning C4312: 'type cast' : conversion from 'int' to 'char *' of greater size

А анализатор PVS-Studio 3.40 выдает предупреждение "error V201: Explicit type conversion. Type casting to memsize.".