Проблемы 64-битного кода на примерах

470 views
414 views

Published on

Статья представляет собой рассмотрение примеров реальных проблем в Си++ коде, проявляющихся при разработке 64-битных решений.

Published in: Technology, Business
0 Comments
0 Likes
Statistics
Notes
  • Be the first to comment

  • Be the first to like this

No Downloads
Views
Total views
470
On SlideShare
0
From Embeds
0
Number of Embeds
7
Actions
Shares
0
Downloads
0
Comments
0
Likes
0
Embeds 0
No embeds

No notes for slide

Проблемы 64-битного кода на примерах

  1. 1. Проблемы 64-битного кода напримерахАвтор: Евгений РыжковДата: 23.07.2007АннотацияСтатья представляет собой рассмотрение примеров реальных проблем в Си++ коде,проявляющихся при разработке 64-битных решений.Данная статья содержит различные примеры 64-битных ошибок. Однако с момента ее написания,мы узнали значительно больше примеров и типов ошибок, которые не описаны в этой статье. Мыпредлагаем вам познакомиться со статьей "Коллекция примеров 64-битных ошибок в реальныхпрограммах", в которой наиболее полно описаны известные нам дефекты в 64-битныхпрограммах. Также рекомендуем изучить "Уроки разработки 64-битных приложений на языкеСи/Си++", где описана методика создания корректного 64-битного кода и методы поиска всехвидов дефектов с использованием анализатора кода Viva64.ВведениеПри переносе 32-битного программного обеспечения на 64-битные системы в коде приложений,написанных на языке Си++, могут проявляться отсутствующие ранее ошибки. Причина этогокроется в изменении базовых типов данных (а точнее, отношений между ними) на новойаппаратной платформе. "Но ведь Си++ - высокоуровневый язык!", - воскликнете Вы и будете,конечно же, правы. Однако все высокоуровневые абстракции этого языка реализуются через всете же низкоуровневые типы данных.Справочная документация для программистов, конечно же, содержит описание этих ошибок.Однако даже такие авторитетные источники информации, как MSDN, зачастую пытаютсяограничиться лишь общими фразами, например: • Типы int и long остались 32-битными на 64-битных версиях Windows; • Типы size_t, time_t, и ptrdiff_t стали 64-битными на 64-битных версиях Windows.Однако, что это означает для программиста, и в какие потенциальные проблемы может вылиться,зачастую не сообщается.Между тем статей, содержащих конкретные примеры ошибок в коде приложений для 64-битныхверсий Windows, совсем мало. Настоящая статья и призвана восполнить этот пробел.Немного терминологии. Под memsize-типом будем понимать любой тип данных, который меняетсвой размер при изменении разрядности архитектуры с 32 бит на 64 бита. Примеры: size_t,ptrdiff_t, DWORD_PTR, LONG_PTR и другие.Сразу же оговоримся, что в данной статье лишь кратко приводятся примеры ошибок. Объяснениеих причин можно найти в статье "20 ловушек переноса Си++ кода на 64-битную платформу"http://www.viva64.com/art-1-2-599168895.html
  2. 2. Исходный код примера с ошибкамиНе будем томить программистов, желающих поскорее приступить к изучению примеров ошибок,и приведем полный исходный код такой программы. После исходного кода, каждая ошибка будетрассмотрена отдельно.Для демонстрации ошибок необходимо скомпилировать и запустить данный код в 64-битномрежиме.Вы можете найти исходный код приложения в дистрибутиве Viva64 под названием PortSample.Для этого скачайте и установите Viva64 (www.viva64.com/download.php), а затем установите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;
  3. 3. 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;
  4. 4. 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);
  5. 5. } 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;
  6. 6. 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;
  7. 7. 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; }
  8. 8. 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);
  9. 9. 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;
  10. 10. 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();
  11. 11. V106(); V107(); V108(); V109(); V110(); V111(); V112(); V113(); V114(); V201(); V202(); V203(); V301(); return 0;}Теперь, когда Вы увидели весь код, давайте рассмотрим все функции, содержащие ошибки. Когдамы говорим, что функции содержат ошибки, мы имеем в виду следующее. Представленный кодпрекрасно компилируется и работает в 32-битном режиме. Однако, после компиляции для 64-битного режима, работа программы становится некорректной, вплоть до падений.Неявное приведение к типу memsizevoid V101(){ unsigned imageWidth = 1000; unsigned imageHeght = 1000; unsigned bytePerPixel = 3; unsigned maxFrameCountInBuffer; if (IsX64Platform()) { maxFrameCountInBuffer = 2000; } else { maxFrameCountInBuffer = 100; }
  12. 12. 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 гигабайта, то переполнение произойдет и результат будет неверным.Использование не memsize-типа для арифметики с указателямиvoid V102(){ int domainWidth; int domainHeght; int domainDepth; if (IsX64Platform()) { domainWidth = 2000; domainHeght = 2000;
  13. 13. 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гигабайт.Неявное приведение memsize-типа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));
  14. 14. for (size_t i = 0; i != n; ++i) buffer[i] = 0; free(buffer);}В следующем фрагменте кода присутствует совершенно очевидная ошибка: unsigned arraySize = n * sizeof(INT_PTR);Это неявное приведение к unsigned-типу переменной большей разрядности (на 64-битнойплатформе).Неявное приведение к memsize-типу в арифметическом выражении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)
  15. 15. проблема в том, что переменная i типа unsigned сравнивается с переменной n типа size_t, послечего происходит ее увеличение. Однако из-за того, что unsigned никогда не превышает 4 гигабайт,то переменная i никогда не будет больше этого значения. Что мы имеем в результате?Бесконечный цикл, так как условие i != n будет выполняться всегда.Неявное приведение к memsize типу в операции ?: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));
  16. 16. else memset(buffer, 0, SafeMul(5, 1024, 1024)); free(buffer);}В строке: buffer = malloc(5 * unit);программист рассчитывал получить на 64-битной системе буфер из 5 гигабайт. Однако здесьпроизойдет ошибка. Почему? Функция malloc() принимает аргумент memsize-типа и 5 гигабайт -вполне подходящее число. Однако при умножении (5 * unit) произойдет переполнение, т.к.переменная unit имеет тип unsigned. В результате получится вовсе не 5 гигабайт.Неявное приведение аргумента функции memsize-типа к 32-битному типу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);
  17. 17. }В строке, с вызовом функции 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 будет невозможно.
  18. 18. Приведение к memsize-типу при использовании возвращаемогозначения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.
  19. 19. Приведение memsize-типа при использовании возвращаемогозначения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;
  20. 20. Отличие лишь в том, что здесь выполняется приведение memsize типа к типу int. В результатеразмер буфера (из примера) никогда не сможет быть вычислен в случае, если он больше 2гигабайт.Вызов функции с переменным числом аргументов с memsize-параметром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-битном режимеприведет к неправильному выводу.Неявное приведение memsize-типа к double и наоборотvoid V113(){ size_t a = size_t(-1); double b = a; --a; --b; size_t c = b;
  21. 21. 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) {
  22. 22. ::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-битные платформы.

×