Статья представляет собой рассмотрение примеров реальных проблем в Си++ коде, проявляющихся при разработке 64-битных решений.
Данная статья содержит различные примеры 64-битных ошибок. Однако с момента ее написания, мы узнали значительно больше примеров и типов ошибок, которые не описаны в этой статье. Мы предлагаем вам познакомиться со статьей "Коллекция примеров 64-битных ошибок в реальных программах", в которой наиболее полно описаны известные нам дефекты в 64-битных программах. Также рекомендуем изучить "Уроки разработки 64-битных приложений на языке Си/Си++", где описана методика создания корректного 64-битного кода и методы поиска всех видов дефектов с использованием анализатора кода Viva64.
При переносе 32-битного программного обеспечения на 64-битные системы в коде приложений, написанных на языке Си++, могут проявляться отсутствующие ранее ошибки. Причина этого кроется в изменении базовых типов данных (а точнее, отношений между ними) на новой аппаратной платформе. "Но ведь Си++ - высокоуровневый язык!", - воскликнете Вы и будете, конечно же, правы. Однако все высокоуровневые абстракции этого языка реализуются через все те же низкоуровневые типы данных.
Справочная документация для программистов, конечно же, содержит описание этих ошибок. Однако даже такие авторитетные источники информации, как MSDN, зачастую пытаются ограничиться лишь общими фразами, например:
Однако, что это означает для программиста, и в какие потенциальные проблемы может вылиться, зачастую не сообщается.
Между тем статей, содержащих конкретные примеры ошибок в коде приложений для 64-битных версий Windows, совсем мало. Настоящая статья и призвана восполнить этот пробел.
Немного терминологии. Под memsize-типом будем понимать любой тип данных, который меняет свой размер при изменении разрядности архитектуры с 32 бит на 64 бита. Примеры: size_t, ptrdiff_t, DWORD_PTR, LONG_PTR и другие.
Сразу же оговоримся, что в данной статье лишь кратко приводятся примеры ошибок. Объяснение их причин можно найти в статье "20 ловушек переноса Си++ кода на 64-битную платформу".
Не будем томить программистов, желающих поскорее приступить к изучению примеров ошибок, и приведем полный исходный код такой программы. После исходного кода, каждая ошибка будет рассмотрена отдельно.
Для демонстрации ошибок необходимо скомпилировать и запустить данный код в 64-битном режиме.
Вы можете найти исходный код приложения в дистрибутиве Viva64 под названием PortSample. Для этого скачайте и установите Viva64, а затем установите PortSample из программной группы Viva64.
bool IsX64Platform() {
return sizeof(size_t) == 8;
}
template <typename A, typename B>
inline size_t SafeMul(A a, B b) {
return static_cast<size_t>(a) * static_cast<size_t>(b);
}
template <typename A, typename B, typename C>
inline size_t SafeMul(A a, B b, C c) {
return static_cast<size_t>(a) * static_cast<size_t>(b) *
static_cast<size_t>(c);
}
template <typename A, typename B, typename C, typename D>
inline size_t SafeMul(A a, B b, C c, D d) {
return static_cast<size_t>(a) * static_cast<size_t>(b) *
static_cast<size_t>(c) * static_cast<size_t>(d);
}
void V101()
{
unsigned imageWidth = 1000;
unsigned imageHeght = 1000;
unsigned bytePerPixel = 3;
unsigned maxFrameCountInBuffer;
if (IsX64Platform()) {
maxFrameCountInBuffer = 2000;
} else {
maxFrameCountInBuffer = 100;
}
size_t bufferSize = imageWidth * imageHeght *
bytePerPixel * maxFrameCountInBuffer;
BYTE *buffer = static_cast<BYTE *>(malloc(bufferSize));
BYTE *ptr = buffer;
for (unsigned frame = 0; frame != maxFrameCountInBuffer; ++frame)
for (unsigned width = 0; width != imageWidth; ++width)
for (unsigned height = 0; height != imageHeght; ++height) {
*ptr++ = 0xFF;
*ptr++ = 0xFF;
*ptr++ = 0x00;
}
free (buffer);
}
void V102()
{
int domainWidth;
int domainHeght;
int domainDepth;
if (IsX64Platform()) {
domainWidth = 2000;
domainHeght = 2000;
domainDepth = 2000;
} else {
domainWidth = 500;
domainHeght = 500;
domainDepth = 500;
}
char *buffer =
new char [size_t(domainWidth) * size_t(domainHeght) *
size_t(domainDepth)];
char *current = buffer;
char *end = buffer;
end += domainWidth * domainHeght * domainDepth;
while (current != end)
*current++ = 1;
delete [] buffer;
}
void V103()
{
size_t Megabyte = 1048576;
size_t Gigabyte = 1073741824;
size_t n = IsX64Platform() ? Gigabyte : Megabyte;
unsigned arraySize = n * sizeof(INT_PTR);
INT_PTR *buffer = (INT_PTR *)malloc(size_t(arraySize));
for (size_t i = 0; i != n; ++i)
buffer[i] = 0;
free(buffer);
}
void V104()
{
volatile size_t n;
if (IsX64Platform()) {
n = SafeMul(5, 1024, 1024, 1024);
} else {
n = SafeMul(5, 1024, 1024);
}
char *buffer = new char [n];
volatile size_t index = 0;
volatile unsigned i;
for (i = 0; i != n; ++i)
buffer[index++] = 1;
delete [] buffer;
}
void V105()
{
bool flag = true;
unsigned a = unsigned(-1);
if ((flag ? a : sizeof(float)) != size_t(-1)) {
throw CString("x64 portability issues");
}
}
void V106()
{
void *buffer;
const unsigned Megabyte = 1024 * 1024;
const unsigned Gigabyte = 1024 * 1024 * 1024;
unsigned unit;
if (IsX64Platform())
unit = Gigabyte;
else
unit = Megabyte;
buffer = malloc(5 * unit);
if (IsX64Platform())
memset(buffer, 0, SafeMul(5, 1024, 1024, 1024));
else
memset(buffer, 0, SafeMul(5, 1024, 1024));
free(buffer);
}
void V107_FillFunction(char *array, unsigned arraySize) {
for (unsigned i = 0; i != arraySize; ++i)
array[i] = 1;
}
void V107()
{
size_t n;
if (IsX64Platform()) {
n = SafeMul(5, 1024, 1024, 1024);
} else {
n = SafeMul(5, 1024, 1024);
}
char *array = (char *)malloc(n * sizeof(char));
memset(array, 0, n * sizeof(char));
V107_FillFunction(array, n);
for (size_t i = 0; i != n; ++i)
if (array[i] != 1)
throw CString("x64 portability issues");
free(array);
}
void V108()
{
size_t n;
if (IsX64Platform()) {
n = SafeMul(5, 1024, 1024, 1024);
} else {
n = SafeMul(5, 1024, 1024);
}
char *array = (char *)malloc(n * sizeof(char));
memset(array, 0, n * sizeof(char));
volatile int index = 0;
for (size_t i = 0; i != n; ++i) {
array[index++] = 1;
if (array[i] != 1)
throw CString("x64 portability issues");
}
free(array);
}
ptrdiff_t UnsafeCalcIndex(int x, int y, int width) {
volatile int result = x + y * width;
return result;
}
void V109()
{
int domainWidth;
int domainHeght;
if (IsX64Platform()) {
domainWidth = 50000;
domainHeght = 50000;
} else {
domainWidth = 5000;
domainHeght = 5000;
}
char *array = (char *)malloc(SafeMul(domainWidth, domainHeght));
for (int x = 0; x != domainWidth; ++x)
for (int y = 0; y != domainHeght; ++y) {
array[UnsafeCalcIndex(x, y, domainWidth)] = 55;
}
free(array);
}
int UnsafeStrLen(const char *text) {
const char *ptr = text;
while (*ptr != 0)
++ptr;
return ptr - text;
}
void V110()
{
size_t n;
CString trueSize;
if (IsX64Platform()) {
n = SafeMul(3, 1024, 1024, 1024);
trueSize = _T("3221225472");
} else {
n = SafeMul(3, 1024, 1024);
trueSize = _T("3145728");
}
char *str = (char *)malloc(n * sizeof(char));
memset(str, 'V', n * sizeof(char));
str[n - 1] = 0;
int len = UnsafeStrLen(str);
CString falseSize;
falseSize.Format(_T("%i"), len + 1);
free(str);
if (falseSize != trueSize)
throw CString(_T("x64 portability issues"));
}
void V111()
{
char invalidStr[100], validStr[100];
const char *invalidFormat = "%u";
const char *validFormat = "%Iu";
size_t a = SIZE_MAX;
sprintf_s(invalidStr, sizeof(invalidStr),invalidFormat, a);
sprintf_s(validStr, sizeof(validStr), validFormat, a);
if (strcmp(invalidStr, validStr) != 0)
throw CString(_T("x64 portability issues"));
}
void V113()
{
size_t a = size_t(-1);
double b = a;
--a;
--b;
size_t c = b;
if (a != c)
throw CString(_T("x64 portability issues"));
}
void V114()
{
unsigned intPtr[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
size_t *sizetPtr = (size_t *)(intPtr);
size_t sum = 0;
for (size_t i = 0; i != 10; ++i)
sum += sizetPtr[i];
if (sum != 45)
throw CString(_T("x64 portability issues"));
}
void V301()
{
class CWinAppTest {
public:
virtual void WinHelp(DWORD_PTR, UINT) {
::AfxMessageBox(_T("Cannot activate WinHelp"));
}
};
class CPortSampleApp : public CWinAppTest {
public:
virtual void WinHelp(DWORD, UINT) {
::AfxMessageBox(_T("WinHelp activated"));
}
};
CWinAppTest *Application = new CPortSampleApp();
Application->WinHelp(NULL, 0);
delete Application;
}
int _tmain(int argc, TCHAR* argv[])
{
V101();
V102();
V103();
V104();
V105();
V106();
V107();
V108();
V109();
V110();
V111();
V112();
V113();
V114();
V201();
V202();
V203();
V301();
return 0;
}
Теперь, когда Вы увидели весь код, давайте рассмотрим все функции, содержащие ошибки. Когда мы говорим, что функции содержат ошибки, мы имеем в виду следующее. Представленный код прекрасно компилируется и работает в 32-битном режиме. Однако, после компиляции для 64-битного режима, работа программы становится некорректной, вплоть до падений.
void V101()
{
unsigned imageWidth = 1000;
unsigned imageHeght = 1000;
unsigned bytePerPixel = 3;
unsigned maxFrameCountInBuffer;
if (IsX64Platform()) {
maxFrameCountInBuffer = 2000;
} else {
maxFrameCountInBuffer = 100;
}
size_t bufferSize = imageWidth * imageHeght *
bytePerPixel * maxFrameCountInBuffer;
BYTE *buffer = static_cast<BYTE *>(malloc(bufferSize));
BYTE *ptr = buffer;
for (unsigned frame = 0; frame != maxFrameCountInBuffer; ++frame)
for (unsigned width = 0; width != imageWidth; ++width)
for (unsigned height = 0; height != imageHeght; ++height) {
*ptr++ = 0xFF;
*ptr++ = 0xFF;
*ptr++ = 0x00;
}
free (buffer);
}
Проблема скрыта в следующей строке:
size_t bufferSize = imageWidth * imageHeght *
bytePerPixel * maxFrameCountInBuffer;
Все переменные, участвующие в умножении, имеют тип unsigned, который и в 32-х и 64-битном режиме имеет размер 32 бита. Однако результат умножения записывается в переменную типа size_t, который в 32-битном режиме имеет размер, совпадающий с размером типа unsigned, а в 64-битном - не совпадающий. Но компилятор прекрасно выполняет расширение результирующего типа до unsigned. Казалось бы, проблемы нет? Есть! Если в результате умножения результат превысит 4 гигабайта, то переполнение произойдет и результат будет неверным.
void V102()
{
int domainWidth;
int domainHeght;
int domainDepth;
if (IsX64Platform()) {
domainWidth = 2000;
domainHeght = 2000;
domainDepth = 2000;
} else {
domainWidth = 500;
domainHeght = 500;
domainDepth = 500;
}
char *buffer =
new char [size_t(domainWidth) * size_t(domainHeght) *
size_t(domainDepth)];
char *current = buffer;
char *end = buffer;
end += domainWidth * domainHeght * domainDepth;
while (current != end)
*current++ = 1;
delete [] buffer;
}
Проблема в данном коде заключается в арифметике с указателями, точнее в использовании для этой арифметики не memsize-типов:
end += domainWidth * domainHeght * domainDepth;
Ошибка в том, что на 64-битной платформе указатель end никогда не получит приращение более 4 гигабайт.
void V103()
{
size_t Megabyte = 1048576;
size_t Gigabyte = 1073741824;
size_t n = IsX64Platform() ? Gigabyte : Megabyte;
unsigned arraySize = n * sizeof(INT_PTR);
INT_PTR *buffer = (INT_PTR *)malloc(size_t(arraySize));
for (size_t i = 0; i != n; ++i)
buffer[i] = 0;
free(buffer);
}
В следующем фрагменте кода присутствует совершенно очевидная ошибка:
unsigned arraySize = n * sizeof(INT_PTR);
Это неявное приведение к unsigned-типу переменной большей разрядности (на 64-битной платформе).
void V104()
{
volatile size_t n;
if (IsX64Platform()) {
n = SafeMul(5, 1024, 1024, 1024);
} else {
n = SafeMul(5, 1024, 1024);
}
char *buffer = new char [n];
volatile size_t index = 0;
volatile unsigned i;
for (i = 0; i != n; ++i)
buffer[index++] = 1;
delete [] buffer;
}
Операции вроде сравнения двух переменных, как ни странно, также могут стать источником проблем.
В следующей строке:
for (i = 0; i != n; ++i)
проблема в том, что переменная i типа unsigned сравнивается с переменной n типа size_t, после чего происходит ее увеличение. Однако из-за того, что unsigned никогда не превышает 4 гигабайт, то переменная i никогда не будет больше этого значения. Что мы имеем в результате? Бесконечный цикл, так как условие i != n будет выполняться всегда.
void V105()
{
bool flag = true;
unsigned a = unsigned(-1);
if ((flag ? a : sizeof(float)) != size_t(-1)) {
throw CString("x64 portability issues");
}
}
Этот пример очень похож на предыдущий. Проблема в следующей строке:
if ((flag ? a : sizeof(float)) != size_t(-1)) {
Здесь переменная a имеет тип unsigned, который при сравнении с size_t может дать некорректный результат. Почему? Да потому, что unsigned(-1) - это не тоже самое, что size_t (-1). Неявное приведение аргумента функции к memsize-типу.
void V106()
{
void *buffer;
const unsigned Megabyte = 1024 * 1024;
const unsigned Gigabyte = 1024 * 1024 * 1024;
unsigned unit;
if (IsX64Platform())
unit = Gigabyte;
else
unit = Megabyte;
buffer = malloc(5 * unit);
if (IsX64Platform())
memset(buffer, 0, SafeMul(5, 1024, 1024, 1024));
else
memset(buffer, 0, SafeMul(5, 1024, 1024));
free(buffer);
}
В строке:
buffer = malloc(5 * unit);
программист рассчитывал получить на 64-битной системе буфер из 5 гигабайт. Однако здесь произойдет ошибка. Почему? Функция malloc() принимает аргумент memsize-типа и 5 гигабайт - вполне подходящее число. Однако при умножении (5 * unit) произойдет переполнение, т.к. переменная unit имеет тип unsigned. В результате получится вовсе не 5 гигабайт.
void V107_FillFunction(char *array, unsigned arraySize) {
for (unsigned i = 0; i != arraySize; ++i)
array[i] = 1;
}
void V107()
{
size_t n;
if (IsX64Platform()) {
n = SafeMul(5, 1024, 1024, 1024);
} else {
n = SafeMul(5, 1024, 1024);
}
char *array = (char *)malloc(n * sizeof(char));
memset(array, 0, n * sizeof(char));
V107_FillFunction(array, n);
for (size_t i = 0; i != n; ++i)
if (array[i] != 1)
throw CString("x64 portability issues");
free(array);
}
В строке, с вызовом функции
V107_FillFunction(array, n);
происходит приведение типа переменной n к unsigned. Это означает усечение значения переменной, в результате чего может быть заполнен не весь массив.
void V108()
{
size_t n;
if (IsX64Platform()) {
n = SafeMul(5, 1024, 1024, 1024);
} else {
n = SafeMul(5, 1024, 1024);
}
char *array = (char *)malloc(n * sizeof(char));
memset(array, 0, n * sizeof(char));
volatile int index = 0;
for (size_t i = 0; i != n; ++i) {
array[index++] = 1;
if (array[i] != 1)
throw CString("x64 portability issues");
}
free(array);
}
Если для индексации массива использовать не memsize-тип, то может произойти ошибка вида:
array[index++] = 1;
Проблема состоит в том, что в случае если в массиве присутствует более 4 гигабайт элементов, то обратится к ним с помощью переменной типа unsigned будет невозможно.
ptrdiff_t UnsafeCalcIndex(int x, int y, int width) {
volatile int result = x + y * width;
return result;
}
void V109()
{
int domainWidth;
int domainHeght;
if (IsX64Platform()) {
domainWidth = 50000;
domainHeght = 50000;
} else {
domainWidth = 5000;
domainHeght = 5000;
}
char *array = (char *)malloc(SafeMul(domainWidth, domainHeght));
for (int x = 0; x != domainWidth; ++x)
for (int y = 0; y != domainHeght; ++y) {
array[UnsafeCalcIndex(x, y, domainWidth)] = 55;
}
free(array);
}
Удивительно, но в данном примере ошибка содержится в строке:
return result;
Переменная result имеет тип int, который неявно будет расширен до ptrdiff_t. Однако функция UnsafeCalcIndex() никогда не сможет вернуть индекс элемента, следующего за 2 гигабайтами. Конечно, правильнее сказать, что ошибка в неудачно выбранном типе переменной result. Эта переменная в данном случае должна иметь тип ptrdiff_t.
int UnsafeStrLen(const char *text) {
const char *ptr = text;
while (*ptr != 0)
++ptr;
return ptr - text;
}
void V110()
{
size_t n;
CString trueSize;
if (IsX64Platform()) {
n = SafeMul(3, 1024, 1024, 1024);
trueSize = _T("3221225472");
} else {
n = SafeMul(3, 1024, 1024);
trueSize = _T("3145728");
}
char *str = (char *)malloc(n * sizeof(char));
memset(str, 'V', n * sizeof(char));
str[n - 1] = 0;
int len = UnsafeStrLen(str);
CString falseSize;
falseSize.Format(_T("%i"), len + 1);
if (falseSize != trueSize)
throw CString(_T("x64 portability issues"));
}
Ситуация повторяет предыдущий пример, ошибка опять в строке возврата значения:
return ptr - text;
Отличие лишь в том, что здесь выполняется приведение memsize типа к типу int. В результате размер буфера (из примера) никогда не сможет быть вычислен в случае, если он больше 2 гигабайт.
void V111()
{
char invalidStr[100], validStr[100];
const char *invalidFormat = "%u";
const char *validFormat = "%Iu";
size_t a = SIZE_MAX;
sprintf_s(invalidStr, sizeof(invalidStr),invalidFormat, a);
sprintf_s(validStr, sizeof(validStr), validFormat, a);
if (strcmp(invalidStr, validStr) != 0)
throw CString(_T("x64 portability issues"));
}
Функции с переменным числом аргументов очень часто используются для форматирования и ввода/вывода текстовых строк. Некорректное задание строки формата может привести к неправильной работе:
const char *invalidFormat = "%u";
sprintf_s(invalidStr, sizeof(invalidStr),invalidFormat, a);
Строка формата в данном примере рассчитана на 32-битный режим работы и в 64-битном режиме приведет к неправильному выводу.
void V113()
{
size_t a = size_t(-1);
double b = a;
--a;
--b;
size_t c = b;
if (a != c)
throw CString(_T("x64 portability issues"));
}
В представленном примере ошибки содержатся в строках:
double b = a;
и
size_t c = b;
Такое присваивание на 64-битных системах некорректно, так как может вызвать потерю точности.
void V114()
{
unsigned intPtr[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
size_t *sizetPtr = (size_t *)(intPtr);
size_t sum = 0;
for (size_t i = 0; i != 10; ++i)
sum += sizetPtr[i];
if (sum != 45)
throw CString(_T("x64 portability issues"));
}
Язык Си++, являясь низкоуровневым языком, позволяет работать с памятью на уровне указателей. Явные приведения типов при использовании указателей являются опасными в любом случае, однако приведение memsize типов как показано в примере, вдвойне опаснее:
size_t *sizetPtr = (size_t *)(intPtr);
Все дело в разности размеров типов size_t и unsigned.
void V301()
{
class CWinAppTest {
public:
virtual void WinHelp(DWORD_PTR, UINT) {
::AfxMessageBox(_T("Cannot activate WinHelp"));
}
};
class CPortSampleApp : public CWinAppTest {
public:
virtual void WinHelp(DWORD, UINT) {
::AfxMessageBox(_T("WinHelp activated"));
}
};
CWinAppTest *Application = new CPortSampleApp();
Application->WinHelp(NULL, 0);
delete Application;
}
Одна из самых забавных ошибок в Си++ приложениях, которая может проявиться на 64-битных системах, связана с виртуальными функциями. Обратите внимание на параметры виртуальных функций в примере выше. На 32-битной системе DWORD_PTR и DWORD совпадают, и получается перекрытая виртуальная функция, а на 64-битной - это две разных функции! В результате вызов функции WinHelp() из примера приведет к появлению сообщения "Cannot activate WinHelp".
Итак, мы перечислили основные ошибки в коде, которые проявляются при переносе приложений на 64-битные системы? Вы скажете, что многие из них надуманы? Кому, например, может понадобиться буфер в 5 гигабайт на Windows-системе? Может быть, в 2007 году, это еще и не столь актуально, хотя многие ресурсоемкие приложения уже могут использовать такой объем памяти. Однако посмотрим, будет ли востребована эта статья уже через пару лет. Кто знает, может быть именно Вы будете долго отлаживать ошибку, возникающую при выделении нескольких гигабайт памяти.
Евгений Рыжков, один из создателей статического анализатора кода Viva64 (www.viva64.com), предназначенного для упрощения переноса приложений на 64-битные платформы. Исследует проблемы миграции 32-битных программных систем на 64-битные платформы.