Коллекция примеров 64-битныхошибок в реальных программахАвтор: Андрей КарповДата: 29.06.2010АннотацияСтатья представляет с...
};struct STRUCT_2{    int x;};...STRUCT_1 Abcd;STRUCT_2 Qwer;memset(&Abcd, 0, sizeof(Abcd));memset(&Qwer, 0, sizeof(Abcd))...
Рисунок 1 – Схематичное пояснение примера переполнения буфераПодобную ошибку может быть сложно выявить, если при этом порт...
переменная length имеет тип int. Однако нас интересует другая ошибка, поэтому будем считать,что программа работает с небол...
while(curr_pos - buffer < length && *curr_pos != n)  curr_pos++;Пример 3. Некорректные #ifdefЧасто в программах с длинной ...
Рисунок 3 – Два варианта - это слишком малоПолагаться на вариант #else в подобных ситуациях опасно. Лучше явно рассмотреть...
cout << "This is Win32" << endl;#elif defined _WIN16 // Win16 code cout << "This is Win16" << endl;#else static_assert(fal...
Рисунок 4 – Проверяются все возможные пути компиляцииПример 4. Путаница с int и int*В старых программах, особенно на Си, н...
*x = &GlobalInt;}void SetValue(int *x){    GlobalInt = *x;}...int XX;GetValue((int **)&XX);SetValue((int *)XX);В данном пр...
Рисунок 5 – Порча памяти рядом с переменной XXПриведенный код писался или новичком, или в спешке. Причем явные приведения ...
Программиста, некогда написавшего этот код, не в чем упрекнуть. В ходе разработки, лет 5-10назад, программист, опираясь на...
предупреждение было проигнорировано, так как казалось, что ничего плохого при работе скороткими строками произойти не може...
Рисунок 7 – Схематичное пояснение ошибки обрезания значенияИсправление элементарно, достаточно прислушаться к предупрежден...
#include <stdlib.h>void test(){  const size_t Gbyte = 1024 * 1024 * 1024;  size_t i;  char *Pointers[3];    // Allocate   ...
mov       rcx,qword ptr [i]mov       qword ptr Pointers[rcx*8],raxОбратите внимание на наличие инструкции CDQE (Convert do...
if (((unsigned)num_items >= MAX_ALLOCATION) ||          ((unsigned)size >= MAX_ALLOCATION) ||          ((long long)size * ...
...  virtual void WinHelp(DWORD dwData, UINT nCmd);};И все было прекрасно до тех пор, пока не появились 64-битные системы....
Рисунок 9 – Ошибка, связанная с виртуальными функциямиПример 10. Магические числа в качестве параметровМагические числа, с...
Продемонстрируем пример работы с функцией CreateFileMapping, встретившийся в одной из CAD-систем:HANDLE hFileMapping = Cre...
В данном случае в 64-битной системе выделяется больше памяти, чем затем заполняетсянулевыми значениями (см. рисунок 11) . ...
Рисунок 11 – Заполнение только части массиваКорректный вариант:size_t count = 500;size_t *values = new size_t[count];memse...
позволяет это сделать. Если передается более четырех целочисленных параметров, в стекерезервируется соответствующее дополн...
Рисунок 12 – Переполнение буфера при работе с функцией sprintfВарианты исправления данного кода различны. Рациональнее все...
// Будет распечатано неверное значениеprintf(invalidFormat, value);В других случаях ошибка в строке форматирования будет к...
size_t c = b; // x86: a == c                    // x64: a != cДанный пример еще можно пытаться оправдывать на 32-битной си...
Причина, по которой в Win64 программе A + B != A - (-B), показана на рисунке 15.Рисунок 15 – Win64: A + B != A - (-B)Ошибк...
Рисунок 16 – Выход за границы массива
Исправление заключается в использовании memsize-типов и аккуратной работе со знаковыми ибеззнаковыми типами:LONG p1[100];L...
float Region::GetCell(int x, int y, int z) const {    return array[static_cast<ptrdiff_t>(x) + y * Width +           z * W...
enum ENumbers { ZERO, ONE, TWO, THREE, FOUR };//safe cast (for MSVC)ENumbers *enumPtr = (ENumbers *)(array);cout << enumPt...
Рисунок 17 – Представление элементов массивов в памятиПримечание. Тип enum в компиляторе Visual C++ по умолчанию совпадает...
работать, и может неожиданно отказать только спустя большой промежуток времени (см. рисунок18).Рисунок 18 – Помещение указ...
Пример 21. Memsize-типы в объединенияхКогда возникает необходимость работать с указателем как с целым числом, иногда удобн...
{ ... }Это цикл никогда не прекратится, если значение Count > UINT_MAX. Предположим, что на 32-битных системах этот код ра...
Рисунок 20 – Механизм возникновения вечного циклаПример 23. Работа с битами и операция NOTРабота с битовыми операциями тре...
if (a != c)  cout << "Error" << endl;Ошибка заключается в том, что маска, заданная выражением "~(b - 1)", имеет тип ULONG....
Рисунок 21 – Ошибка из-за обнуления старших битИсправленный вариант кода может выглядеть следующим образом:UINT_PTR c = a ...
Приведенный код работоспособен на 32-битной архитектуре и позволяет выставлять бит сномерами от 0 до 31 в единицу. После п...
ptrdiff_t mask = static_cast<ptrdiff_t>(1) << bitNum;Заметим также, что неисправленный код приведет еще к одной интересной...
Рисунок 24 - Вычисление выражения в 32-битном кодеОбратим внимание, что при вычислении выражения "obj.a << 17" происходит ...
}Result:type is unsigned 16-bittype is signed 32-bitТеперь посмотрим, к чему приводит наличие знакового расширения в 64-би...
существующих форматов проектов, осуществлять обмен данными между 32-битными и 64-битными процессами и так далее.В основном...
Рисунок 26 - Порядок байт в 64-битном типе на little-endian и big-endian системахРазрабатывая бинарный интерфейс или форма...
Рисунок 27 – Размеры типы и границы их выравнивания (значения точны для Win32/Win64, номогут варьироваться в "Unix-мире" и...
Когда меняются размеры полей в структуре и из-за этого меняется сам размер структуры этопонятно и привычно. Здесь другая с...
Рисунок 28 – Схематическое изображение структур и правил выравнивания типовПример 28. Выравнивания типов и почему нельзя п...
Рисунок 29 – Расположение данных в памяти в 32-битной и 64-битной системеКорректный расчет размера должен выглядеть следую...
Рисунок 30 – Выбор перегруженной функции в 32-битной и 64-битной системеПример проблемы:class MyStack {...public:  void Pu...
битной среде. Подсистема WoW64 очень хорошо справляется со своей задачей, изолируя 32-битное приложение, и практически все...
Коллекция примеров 64-битных ошибок в реальных программах
Upcoming SlideShare
Loading in...5
×

Коллекция примеров 64-битных ошибок в реальных программах

621

Published on

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

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
621
On Slideshare
0
From Embeds
0
Number of Embeds
0
Actions
Shares
0
Downloads
3
Comments
0
Likes
0
Embeds 0
No embeds

No notes for slide

Transcript of "Коллекция примеров 64-битных ошибок в реальных программах"

  1. 1. Коллекция примеров 64-битныхошибок в реальных программахАвтор: Андрей КарповДата: 29.06.2010АннотацияСтатья представляет собой наиболее полную коллекцию примеров 64-битных ошибок на языкахСи и Си++. Статья ориентирована на разработчиков Windows-приложений, использующих VisualC++, но будет полезна и более широкой аудитории.ВведениеНаша компания ООО "Системы программной верификации" занимается разработкойспециализированного статического анализатора Viva64 выявляющего 64-битные ошибки в кодеприложений на языке Си/Си++. В ходе этой работы наша коллекция примеров 64-битныхдефектов постоянно пополняется, и мы решили собрать в этой статье наиболее интересные нанаш взгляд ошибки. В статье приводятся примеры как взятые непосредственно из кода реальныхприложений, так и составленные синтетически на основе реального кода, так как в нем онислишком "растянуты".Статья только демонстрирует различные виды 64-битных ошибок и не описывает методов ихобнаружения и профилактики. Вы можете подробно познакомиться с методами диагностики иисправления дефектов в 64-битных программах, обратившись к следующим ресурсам: 1. Курс по разработке 64-битных приложений на языке Си/Си++ [1]; 2. Что такое size_t и ptrdiff_t [2]; 3. 20 ловушек переноса Си++ - кода на 64-битную платформу [3]; 4. Учебное пособие по PVS-Studio [4]; 5. 64-битный конь, который умеет считать [5].Также вы можете познакомиться с демонстрационной версией инструмента PVS-Studio, в составкоторой входит статический анализатор кода Viva64, выявляющий практически все описанные встатье ошибки. Демонстрационная версия доступна для скачивания по адресу:http://www.viva64.com/ru/pvs-studio/download/.Пример 1. Переполнение буфераstruct STRUCT_1{ int *a;
  2. 2. };struct STRUCT_2{ int x;};...STRUCT_1 Abcd;STRUCT_2 Qwer;memset(&Abcd, 0, sizeof(Abcd));memset(&Qwer, 0, sizeof(Abcd));В программе объявлены два объекта типа STRUCT_1 и STRUCT_2, которые перед началомиспользования необходимо очистить (инициализировать все поля нулями). Реализуяинициализацию, программист решил скопировать похожу строчку и заменил в ней "&Abcd" на"&Qwer". Но при этом он забыл заменить "sizeof(Abcd)" на "sizeof(Qwer)".По удачному стечениюобстоятельств размер структур STRUCT_1 и STRUCT_2 совпадал в 32-битной системе и кодкорректно работал долгое время.При переносе кода на 64-битную систему размер структуры Abcd увеличился и как следствиевозникла ошибка переполнения буфера (см. рисунок 1).
  3. 3. Рисунок 1 – Схематичное пояснение примера переполнения буфераПодобную ошибку может быть сложно выявить, если при этом портятся данные, используемыегораздо позднее.Пример 2. Лишние приведения типовchar *buffer;char *curr_pos;int length;...while( (*(curr_pos++) != 0x0a) && ((UINT)curr_pos - (UINT)buffer < (UINT)length) );Код плох, но это реальный код. Его задача состоит в поиске конца строки, обозначенногосимволом 0x0A. Код не будет работать со строками длиннее INT_MAX символов, так как
  4. 4. переменная length имеет тип int. Однако нас интересует другая ошибка, поэтому будем считать,что программа работает с небольшим буфером и использование типа int корректно.Проблема в том, что в 64-битной системе указатели buffer и curr_pos могут лежать за пределамипервых 4 гигабайт адресного пространства. В этом случае явное приведение указателей к типуUINT отбросит значащие биты, и работа алгоритма будет нарушена (см. рисунок 2).Рисунок 2 – Некорректны вычисления при поиске терминального символаОшибка неприятна тем, что код долгое время может корректно работать, пока память под буфербудет выделяться в младших четырех гигабайтах адресного пространства. Исправление ошибкизаключается в удалении совершенно ненужных явных приведений типов:
  5. 5. while(curr_pos - buffer < length && *curr_pos != n) curr_pos++;Пример 3. Некорректные #ifdefЧасто в программах с длинной историей можно встретить участки кода, обернутые в конструкции#ifdef - -#else - #endif. При переносе программ на новую архитектуру, некорректно написанныеусловия могут привести к компиляции не тех фрагментов кода, как это планировалосьразработчиками в прошлом (см. рисунок 3). Пример:#ifdef _WIN32 // Win32 code cout << "This is Win32" << endl;#else // Win16 code cout << "This is Win16" << endl;#endif//Альтернативный некорректный вариант:#ifdef _WIN16 // Win16 code cout << "This is Win16" << endl;#else // Win32 code cout << "This is Win32" << endl;#endif
  6. 6. Рисунок 3 – Два варианта - это слишком малоПолагаться на вариант #else в подобных ситуациях опасно. Лучше явно рассмотреть поведениедля каждого случая (см. рисунок 4), а в ветку #else поместить сообщение об ошибке компиляции:#if defined _M_X64 // Win64 code (Intel 64) cout << "This is Win64" << endl;#elif defined _WIN32 // Win32 code
  7. 7. cout << "This is Win32" << endl;#elif defined _WIN16 // Win16 code cout << "This is Win16" << endl;#else static_assert(false, "Неизвестная платформа");#endif
  8. 8. Рисунок 4 – Проверяются все возможные пути компиляцииПример 4. Путаница с int и int*В старых программах, особенно на Си, не редки фрагменты кода, где указатель хранят в типе int.Однако иногда это делается не умышленно, а скорее по невнимательности. Рассмотрим пример,содержащий путаницу, возникшую с использованием типа int и указателем на тип int:int GlobalInt = 1;void GetValue(int **x){
  9. 9. *x = &GlobalInt;}void SetValue(int *x){ GlobalInt = *x;}...int XX;GetValue((int **)&XX);SetValue((int *)XX);В данном примере переменная XX используется в качестве буфера для хранения указателя. Этоткод будет корректно работать в тех 32-битных системах, где размер указателя совпадает сразмером типа int. В 64-битном системе этот код некорректен и вызовGetValue((int **)&XX);приведет к порче 4 байт памяти рядом с переменной XX (см. рисунок 5).
  10. 10. Рисунок 5 – Порча памяти рядом с переменной XXПриведенный код писался или новичком, или в спешке. Причем явные приведения типасвидетельствует, что компилятор до последнего сопротивлялся, намекая разработчику чтоуказатель и int, это разные сущности. Однако победила грубая сила.Исправление ошибки элементарно и заключается в выборе правильного типа для переменной XX.При этом перестает быть необходимым явное приведение типа:int *XX;GetValue(&XX);SetValue(XX);Пример 5. Использование устаревших функцийРяд API-функций, хотя и оставлен для совместимости, представляет собой опасность приразработке 64-битных приложений. Классическим примером является использование такихфункций как SetWindowLong и GetWindowLong. В программах можно встретить код, подобныйследующему:SetWindowLong(window, 0, (LONG)this);...Win32Window* this_window = (Win32Window*)GetWindowLong(window, 0);
  11. 11. Программиста, некогда написавшего этот код, не в чем упрекнуть. В ходе разработки, лет 5-10назад, программист, опираясь на свой опыт и MSDN, составил код совершенно корректный с точкизрения 32-битвной системы Windows. Прототип этих функций выглядит следующим образом:LONG WINAPI SetWindowLong(HWND hWnd, int nIndex, LONG dwNewLong);LONG WINAPI GetWindowLong(HWND hWnd, int nIndex);То, что указатель явно приводится к типу LONG также оправдано, поскольку размер указателя итипа LONG совпадают в Win32 системах. Но думаю понятно, что при перекомпиляции программыв 64-битном варианте, данные приведения типа могут послужить причиной падения или невернойработы приложения.Неприятность ошибки заключается в ее нерегулярном или даже крайне редком проявлении.Произойдет ошибка или нет, зависит от того, в какой области памяти создан объект, на которыйуказывает указатель "this". Если объект создается в младших 4 гигабайтах адресного пространства,то 64-битная программа может корректно функционировать. Ошибка неожиданно можетпроявить себя через большой промежуток времени, когда из-за выделения памяти, объектыначнут создаваться за пределами первых четырех гигабайт.В 64-битной системе использовать функции SetWindowLong/GetWindowLong можно только в томслучае, если программа действительно сохраняет некие значения типа LONG, int, bool и подобныеим. Если необходимо работать с указателями, то следует использовать расширенные вариантыфункций: SetWindowLongPtr/GetWindowLongPtr. Хотя, пожалуй, следует порекомендовать влюбом случае использовать новые функции, чтобы не спровоцировать в будущем новых ошибок.Примеры с функциями SetWindowLong и GetWindowLong являются классическими и приводятсяпрактически во всех статьях посвященных разработке 64-битных приложений. Однако следуетучесть, что этими функциями дело не ограничивается. Обратите внимания на: SetClassLong,GetClassLong, GetFileSize, EnumProcessModules, GlobalMemoryStatus (см. рисунок 6).Рисунок 6 – Таблица с именами некоторых устаревших и современных функцийПример 6. Обрезание значений при неявном приведении типовНеявное приведение типа size_t к типу unsigned и аналогичные приведения хорошодиагностируются предупреждениями компилятора. Однако в больших программах, подобныепредупреждения легко могут затеряться. Рассмотрим пример схожий с реальным кодом, где
  12. 12. предупреждение было проигнорировано, так как казалось, что ничего плохого при работе скороткими строками произойти не может.bool Find(const ArrayOfStrings &arrStr){ ArrayOfStrings::const_iterator it; for (it = arrStr.begin(); it != arrStr.end(); ++it) { unsigned n = it->find("ABC"); // Truncation if (n != string::npos) return true; } return false;};Приведенная функция ищет текст "ABC" в массиве строк и возвращает true, в случае если хотя быодна строка содержит последовательность "ABC". При компиляции 64-битной версии кода, этафункция всегда будет возвращать true.Константа "string::npos" в 64-битной системе имеет значение 0xFFFFFFFFFFFFFFFF типа size_t. Припомещение этого значения в переменную "n" типа unsigned, происходит его обрезание до0xFFFFFFFF. В результате условие " n != string::npos" всегда истинно, так как 0xFFFFFFFFFFFFFFFF неравно 0xFFFFFFFF (см. рисунок 7).
  13. 13. Рисунок 7 – Схематичное пояснение ошибки обрезания значенияИсправление элементарно, достаточно прислушаться к предупреждениям компилятора:for (auto it = arrStr.begin(); it != arrStr.end(); ++it){ auto n = it->find("ABC"); if (n != string::npos) return true;}return false;Пример 7. Необъявленные функции в СиНесмотря на годы, программы или части программ, написанные на языке Си, остаются живее всехживых. Код этих программ гораздо более предрасположен к 64-битным ошибкам из-за менеестрогих правил контроля типов в языке Си.В языке Си можно использовать функции без их предварительного объявления. Проанализируемсвязанный с этим интересный пример 64-битной ошибки. Для начала рассмотрим корректныйвариант кода, в котором происходит выделение и использование трех массивов размером погигабайту каждый:
  14. 14. #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
  15. 15. 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.Пример 8. Останки динозавров в больших и старых программахБольшие старые программные системы, развивающиеся десятилетиями, изобилуютразнообразнейшими атавизмами и просто участками кода, написанными с использованиемпопулярных парадигм и стилей разнообразных лет. В таких системах можно наблюдать эволюциюразвития языков программирования, когда наиболее старые части написаны в стиле языка Си, а внаиболее свежих можно встретить сложные шаблоны в стиле Александреску.Рисунок 8 – Раскопки динозавраЕсть атавизмы связанные и с 64-битностью. Вернее атавизмы, препятствующие работесовременного 64-битного кода. Рассмотрим пример:// beyond this, assume a programming error#define MAX_ALLOCATION 0xc0000000void *malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size){ void *ptr; ...
  16. 16. if (((unsigned)num_items >= MAX_ALLOCATION) || ((unsigned)size >= MAX_ALLOCATION) || ((long long)size * num_items >= (long long) MAX_ALLOCATION)) { fprintf(stderr, "*** malloc_zone_calloc[%d]: arguments too large: %d,%dn", getpid(), (unsigned)num_items, (unsigned)size); return NULL; } ptr = zone->calloc(zone, num_items, size); ... return ptr;}Во-первых, код функции содержит проверку на допустимые размеры выделяемой памяти,являющиеся странными для 64-битной системы. А во-вторых, выдаваемое диагностическоесообщение будет некорректно, поскольку если мы попросим выделить память под 4 400 000 000элементов, из-за явного приведения типа к unsigned, нам будет выдано странное сообщение оневозможности выделения памяти всего лишь для 105 032 704 элементов.Пример 9. Виртуальные функцииОдним из красивых примеров 64-битных ошибок является использование неверных типоваргументов в объявлениях виртуальных функций. Причем обычно это не чья-то неаккуратность, апросто "несчастный случай", где нет виноватых, но есть ошибка. Рассмотрим следующуюситуацию.С незапамятных времен в библиотеке MFC есть класс CWinApp, в котором имеется функцияWinHelp:class CWinApp { ... virtual void WinHelp(DWORD dwData, UINT nCmd);};Для показа собственной справки в пользовательском приложении необходимо было эту функциюперекрыть:class CSampleApp : public CWinApp {
  17. 17. ... virtual void WinHelp(DWORD dwData, UINT nCmd);};И все было прекрасно до тех пор, пока не появились 64-битные системы. Разработчикам MFCпришлось поменять интерфейс функции WinHelp (и некоторых других функций) так:class CWinApp { ... virtual void WinHelp(DWORD_PTR dwData, UINT nCmd);};В 32-битном режиме типы DWORD_PTR и DWORD совпадали, а вот в 64-битном нет. Естественноразработчики пользовательского приложения также должны сменить тип на DWORD_PTR, ночтобы это сделать, про это необходимо в начале узнать. В результате в 64-битной программевозникает ошибка, так как функция WinHelp в пользовательском классе не вызывается (см.рисунок 9).
  18. 18. Рисунок 9 – Ошибка, связанная с виртуальными функциямиПример 10. Магические числа в качестве параметровМагические числа, содержащиеся в теле программ, являются плохим стилем и провоцируютвозникновение ошибок. В качестве примера магических чисел можно привести 1024 и 768, жесткообозначающие размер разрешения экрана. В рамках этой статьи нам интересны те магическиечисла, которые могут привести к проблемам в 64-битном приложении. Наиболеераспространенные числа, опасные для 64-битных программ, представлены в таблице на рисунке10.Рисунок 10 – Магические числа опасные для 64-битных программ
  19. 19. Продемонстрируем пример работы с функцией CreateFileMapping, встретившийся в одной из CAD-систем:HANDLE hFileMapping = CreateFileMapping( (HANDLE) 0xFFFFFFFF, NULL, PAGE_READWRITE, dwMaximumSizeHigh, dwMaximumSizeLow, name);Вместо корректной зарезервированной константы INVALID_HANDLE_VALUE используется число0xFFFFFFFF. Это некорректно в Win64 программе, где константа INVALID_HANDLE_VALUEпринимает значение 0xFFFFFFFFFFFFFFFF. Правильным вариантом вызова функции будет:HANDLE hFileMapping = CreateFileMapping( INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, dwMaximumSizeHigh, dwMaximumSizeLow, name);Примечание. Некоторые считают, что значение 0xFFFFFFFF при расширении до указателяпревращается в 0xFFFFFFFFFFFFFFFF. Это не так. Согласно правилам языка Си/Си++ значение0xFFFFFFFF имеет тип "unsigned int", так как не может быть представлено типом "int".Соответственно, расширяясь до 64-битного типа, значение 0xFFFFFFFFu превращается в0x00000000FFFFFFFFu. А вот если написать так (size_t)(-1), то мы получим ожидаемое0xFFFFFFFFFFFFFFFF. Здесь "int" вначале расширяется до "ptrdiff_t", а затем превращается в"size_t".Пример 11. Магические константы, обозначающие размерДругой частой ошибкой является использование магических чисел для задания размера объекта.Рассмотрим пример выделения и обнуления буфера:size_t count = 500;size_t *values = new size_t[count];// Будет заполнена только часть буфераmemset(values, 0, count * 4);
  20. 20. В данном случае в 64-битной системе выделяется больше памяти, чем затем заполняетсянулевыми значениями (см. рисунок 11) . Ошибка заключается в предположении, что размер типаsize_t всегда равен четырем байтам.
  21. 21. Рисунок 11 – Заполнение только части массиваКорректный вариант:size_t count = 500;size_t *values = new size_t[count];memset(values, 0, count * sizeof(values[0]));Схожие ошибки можно встретить при вычислении размеров выделяемой памяти илисериализации данных.Пример 12. Переполнение стекаВо многих случаях 64-битная программа потребляет больше памяти и стека. Выделение большегоколичества памяти в куче опасности не представляет, так как этого вида памяти 64-битнойпрограмме доступно во много раз больше, чем 32-битной. А вот увеличение используемойстековой памяти может привести к его неожиданному переполнению (stack overflow).Механизм использования стека отличается в различных операционных системах и компиляторах.Мы рассмотрим особенность использования стека в коде Win64 приложений, построенныхкомпилятором Visual C++.При разработке соглашений по вызовам (calling conventions) в Win64 системах решили положитьконец существованию различных вариантов вызова функций. В Win32 существовал целый рядсоглашений о вызове: stdcall, cdecl, fastcall, thiscall и так далее. В Win64 только одно "родное"соглашение по вызовам. Модификаторы подобные __cdecl компилятором игнорируются.Соглашение по вызовам на платформе x86-64 похоже на соглашение fastcall, существующее в x86.В x64-соглашении первые четыре целочисленных аргумента (слева направо) передаются в 64-битных регистрах, выбранных специально для этой цели:RCX: 1-й целочисленный аргументRDX: 2-й целочисленный аргументR8: 3-й целочисленный аргументR9: 4-й целочисленный аргументОстальные целочисленные аргументы передаются через стек. Указатель "this" считаетсяцелочисленным аргументом, поэтому он всегда помещается в регистр RCX. Если передаютсязначения с плавающей точкой, то первые четыре из них передаются в регистрах XMM0-XMM3, апоследующие - через стек.Хотя аргументы могут быть переданы в регистрах, компилятор все равно резервирует для нихместо в стеке, уменьшая значение регистра RSP (указателя стека). Как минимум, каждая функциядолжна резервировать в стеке 32 байта (четыре 64-битных значения, соответствующие регистрамRCX, RDX, R8, R9). Это пространство в стеке позволяет легко сохранить содержимое переданных вфункцию регистров в стеке. От вызываемой функции не требуется сбрасывать в стек входныепараметры, переданные через регистры, но резервирование места в стеке при необходимости
  22. 22. позволяет это сделать. Если передается более четырех целочисленных параметров, в стекерезервируется соответствующее дополнительное пространство.Описанная особенность приводит к существенному возрастанию скорости поглощения стека.Даже если функция не имеет параметров, то от стека все равно будет "откушено" 32 байта,которые затем никак не используются. Смысл использования такого неэкономного механизмасвязан в унификации и упрощение отладки.Обратим внимание еще на один момент. Указатель стека RSP должен перед очередным вызовомфункции быть выровнен по границе 16 байт. Таким образом, суммарный размер используемогостека при вызове в 64-битном коде функции без параметров составляет 48 байт: 8 (адресвозврата) + 8 (выравнивание) + 32 (резерв для аргументов).Неужели все так плохо? Нет. Не следует забывать, что большее количество регистров имеющихсяв распоряжении 64-битного компилятора, позволяют построить более эффективный код и нерезервировать в стеке память под некоторые локальные переменные функций. Таким образом, вряде случаев 64-битный вариант функции использует меньше стека, чем 32-битный вариант.Более подробно этот вопрос и различные примеры рассматриваются в статье "Причины, покоторым 64-битные программы требуют больше стековой памяти".Предсказать, будет потреблять 64-битная программа больше стека или меньше невозможно. Всилу того, что Win64-программа может использовать в 2-3 раза больше стековой памяти,необходимо подстраховаться и изменить настройку проекта, отвечающую за размеррезервируемого стека. Выберите в настройках проекта параметр Stack Reserve Size (ключ/STACK:reserve) и увеличьте размер резервируемого стека в три раза. По умолчанию этот размерсоставляет 1 мегабайт.Пример 13. Функция с переменным количеством аргументов ипереполнение буфераХотя использование функций с переменным количеством аргументов, таких как printf, scanfсчитается в Си++ плохим стилем, они по прежнему широко используются. Эти функции создаютмножество проблем при переносе приложений на другие системы, в том числе и на 64-битныесистемы. Рассмотрим пример:int x;char buf[9];sprintf(buf, "%p", &x);Автор кода не учел, что размер указателя в будущем может составить более 32 бит. В результатена 64-битной архитектуре данный код приведет к переполнению буфера (см. рисунок 12). Этуошибку вполне можно отнести к использованию магического числа 9, но в реальномприложении переполнение буфера может возникнуть и без магических чисел.
  23. 23. Рисунок 12 – Переполнение буфера при работе с функцией sprintfВарианты исправления данного кода различны. Рациональнее всего провести рефакторинг кода сцелью избавиться от использования опасных функций. Например, можно заменить printf на cout, аsprintf на boost::format или std::stringstream.Примечание. Эту рекомендацию часто критикуют разработчики под Linux, аргументируя тем,что gcc проверяет соответствие строки форматирования фактическим параметрам,передаваемым, например, в функцию printf. И, следовательно, использование printf безопасно.Однако они забывают, что строка форматирования может передаваться из другой частипрограммы, загружаться из ресурсов. Другими словами, в реальной программе строкаформатирования редко присутствует в явном виде в коде, и, соответственно, компилятор неможет ее проверить. Если же разработчик использует Visual Studio 2005/2008/2010, то он несможет получить предупреждение на код вида "void *p = 0; printf("%x", p);" даже используяключи /W4 и /Wall.Пример 14. Функция с переменным количеством аргументов иневерный форматЧасто в программах можно встретить некорректные строки форматирования при работе сфункцией printf и другими схожими функциями. Из-за этого будут выведены неверные значения,что хотя и не приведет к аварийному завершению программы, но, конечно же, является ошибкой:const char *invalidFormat = "%u";size_t value = SIZE_MAX;
  24. 24. // Будет распечатано неверное значениеprintf(invalidFormat, value);В других случаях ошибка в строке форматирования будет критична. Рассмотрим пример,основанный на реализации подсистемы UNDO/REDO в одной из программ:// Здесь указатели сохранялись в виде строкиint *p1, *p2;....char str[128];sprintf(str, "%X %X", p1, p2);// В другой функции данная строка// обрабатывалась следующим образом:void foo(char *str){ int *p1, *p2; sscanf(str, "%X %X", &p1, &p2); // Результат - некорректное значение указателей p1 и p2. ...}Формат "%X" не предназначен для работы с указателями и как следствие подобный коднекорректен сточки зрения 64-битных систем. В 32-битных системах он вполне работоспособен,хотя и не красив.Пример 15. Хранение в double целочисленных значенийНам не приходилось самим встречать подобную ошибку. Вероятно, это ошибка редка, но вполнереальна.Тип double, имеет размер 64-бита, и совместим со стандартом IEEE-754 на 32-битных и 64-битныхсистемах. Некоторые программисты используют тип double для хранения и работы сцелочисленными типами:size_t a = size_t(-1);double b = a;--a;--b;
  25. 25. size_t c = b; // x86: a == c // x64: a != cДанный пример еще можно пытаться оправдывать на 32-битной системе, так как тип double имеет52 значащих бита и способен без потерь хранить 32-битное целое значение. Но при попыткесохранить в double 64-битное целое число точное значение может быть потеряно (см. рисунок 13).Рисунок 13 – Количество значащих битов в типах size_t и doubleПример 16. Адресная арифметика. A + B != A - (-B)Адресная арифметика (address arithmetic) - это способ вычисления адреса какого-либо объектапри помощи арифметических операций над указателями, а также использование указателей воперациях сравнения. Адресную арифметику также называют арифметикой над указателями(pointer arithmetic).Большой процент 64-битных ошибок связан именно с адресной арифметикой. Часто ошибкивозникают в тех выражениях, где совместно используются указатели и 32-битные переменные.Рассмотрим первую из ошибок данного типа:char *A = "123456789";unsigned B = 1;char *X = A + B;char *Y = A - (-B);if (X != Y) cout << "Error" << endl;Причина, по которой в Win32 программе A + B == A - (-B), показана на рисунке 14.Рисунок 14 – Win32: A + B == A - (-B)
  26. 26. Причина, по которой в Win64 программе A + B != A - (-B), показана на рисунке 15.Рисунок 15 – Win64: A + B != A - (-B)Ошибка будет устранена, если использовать подходящий memsize-тип. В данном случаеиспользуется тип ptrdfiff_t:char *A = "123456789";ptrdiff_t B = 1;char *X = A + B;char *Y = A - (-B);Пример 17. Адресная арифметика. Знаковые и беззнаковые типы.Рассмотрим еще один вариант ошибки, связанный с использованием знаковых и беззнаковыхтипов. В этот раз ошибка приведет не к неверному сравнению, а сразу к падению приложения.LONG p1[100];ULONG x = 5;LONG y = -1;LONG *p2 = p1 + 50;p2 = p2 + x * y;*p2 = 1; // Access violationВыражение "x * y" имеет значение 0xFFFFFFFB и имеет тип unsigned. Данный код, собранный в32-битном варианте работоспособен, так как сложение указателя с 0xFFFFFFFB эквивалентно егоуменьшению на 5. В 64-битной указатель после прибавления 0xFFFFFFFB начнет указывать далекоза пределы массива p1 (смотри рисунок 16).
  27. 27. Рисунок 16 – Выход за границы массива
  28. 28. Исправление заключается в использовании memsize-типов и аккуратной работе со знаковыми ибеззнаковыми типами:LONG p1[100];LONG_PTR x = 5;LONG_PTR y = -1;LONG *p2 = p1 + 50;p2 = p2 + x * y;*p2 = 1; // OKПример 18. Адресная арифметика. Переполнения.class Region { float *array; int Width, Height, Depth; float Region::GetCell(int x, int y, int z) const; ...};float Region::GetCell(int x, int y, int z) const { return array[x + y * Width + z * Width * Height];}Код взят из реальной программы математического моделирования, в которой важным ресурсомявляется объем оперативной памяти, и возможность на 64-битной архитектуре использоватьболее 4 гигабайт памяти существенно увеличивает вычислительные возможности. В программахданного класса для экономии памяти часто используют одномерные массивы, осуществляя работус ними как с трехмерными массивами. Для этого существуют функции, аналогичные GetCell,обеспечивающие доступ к необходимым элементам.Приведенный код корректно работает с указателями, если значение выражения " x + y * Width + z* Width * Height" не превышает INT_MAX (2147483647). В противном случае произойдетпереполнение, что приведет к неопределенному поведению в программы.Такой код мог всегда корректно работать на 32-битной платформе. В рамках 32-битнойархитектуры программе недоступен объем памяти для создания массива подобного размеров. На64-битной архитектуре это ограничение снято, и размер массива легко может превысить INT_MAXэлементов.Программисты часто допускают ошибку, пытаясь исправить код следующим образом:
  29. 29. float Region::GetCell(int x, int y, int z) const { return array[static_cast<ptrdiff_t>(x) + y * Width + z * Width * Height];}Они знают, что по правилам языка Си++ выражение для вычисления индекса будет иметь типptrdiff_t и надеются за счет этого избежать переполнения. Но переполнение может произойтивнутри подвыражения "y * Width" или "z * Width * Height", так как для их вычисления по-прежнему используется тип int.Если вы хотите исправить код, не изменяя типов переменных, участвующих в выражении, то выможете явно привести каждое подвыражение к типу ptrdiff_t:float Region::GetCell(int x, int y, int z) const { return array[ptrdiff_t(x) + ptrdiff_t(y) * Width + ptrdiff_t(z) * Width * Height];}Другое, более верное решение - изменить типы переменных:typedef ptrdiff_t TCoord;class Region { float *array; TCoord Width, Height, Depth; float Region::GetCell(TCoord x, TCoord y, TCoord z) const; ...};float Region::GetCell(TCoord x, TCoord y, TCoord z) const { return array[x + y * Width + z * Width * Height];}Пример 19. Изменение типа массиваИногда в программах для удобства изменяют тип массива при его обработке. Опасное ибезопасное приведение типов представлено в следующем коде:int array[4] = { 1, 2, 3, 4 };
  30. 30. enum ENumbers { ZERO, ONE, TWO, THREE, FOUR };//safe cast (for MSVC)ENumbers *enumPtr = (ENumbers *)(array);cout << enumPtr[1] << " ";//unsafe castsize_t *sizetPtr = (size_t *)(array);cout << sizetPtr[1] << endl;//Output on 32-bit system: 2 2//Output on 64-bit system: 2 17179869187Как видите, результат вывода программы отличается в 32-битном и 64-битном варианте. На 32-битной системе доступ к элементам массива осуществляется корректно, так как размеры типовsize_t и int совпадают, и мы видим вывод "2 2".На 64-битной системе мы получили в выводе "2 17179869187", так как именно значение17179869187 находится в 1-ом элементе массива sizetPtr (см. рисунок 17). В некоторых случаяхименно такое поведение и бывает нужно, но обычно это является ошибкой.
  31. 31. Рисунок 17 – Представление элементов массивов в памятиПримечание. Тип enum в компиляторе Visual C++ по умолчанию совпадает размером с типом int,то есть является 32-битным типом. Использование enum другого размера возможно только спомощью расширения, считающимся нестандартным в Visual C++. Поэтому приведенныйпример корректен в Visual C++, но с точки зрения других компиляторов приведение указателяна элементы int к указателю на элементы enum может быть также некорректным.Пример 20. Упаковка указателя в 32-битный типИногда в программах указатели сохраняют в целочисленных типах. Обычно для этогоиспользуется такой тип, как int. Это, пожалуй, одна из самых распространенных 64-битныхошибок.char *ptr = ...;int n = (int) ptr;...ptr = (char *) n;В 64-битной программе это некорректно, поскольку тип int остался 32-битным и не может хранитьв себе 64-битный указатель. Часто это не удается заметить сразу. Благодаря стечениюобстоятельств при тестировании указатель может всегда ссылаться на объекты, расположенные вмладших 4 гигабайтах адресного пространства. В этом случае 64-битная программа будет удачно
  32. 32. работать, и может неожиданно отказать только спустя большой промежуток времени (см. рисунок18).Рисунок 18 – Помещение указателя в переменную типа intЕсли все же необходимо поместить указатель в переменную целочисленного типа, то следуетиспользовать такие типы как intptr_t, uintptr_t, ptrdiff_t и size_t.
  33. 33. Пример 21. Memsize-типы в объединенияхКогда возникает необходимость работать с указателем как с целым числом, иногда удобновоспользоваться объединением, как показано в примере, и работать с числовым представлениемтипа без использования явных приведений:union PtrNumUnion { char *m_p; unsigned m_n;} u;u.m_p = str;u.m_n += delta;Данный код корректен на 32-битных системах и некорректен на 64-битных. Изменяя член m_n на64-битной системе, мы работаем только с частью указателя m_p (смотри рисунок 19).Рисунок 19 – Представление объединения в памяти на 32-битной и 64-битной системе.Следует использовать тип, который будет соответствовать размеру указателя:union PtrNumUnion { char *m_p; uintptr_t m_n; //type fixed} u;Пример 22. Вечный циклСмешанное использование 32-битных и 64-битных типов неожиданно может привести квозникновению вечных циклов. Рассмотрим синтетический пример, иллюстрирующий целыйкласс подобных дефектов:size_t Count = BigValue;for (unsigned Index = 0; Index != Count; Index++)
  34. 34. { ... }Это цикл никогда не прекратится, если значение Count > UINT_MAX. Предположим, что на 32-битных системах этот код работал с количеством итераций менее значения UINT_MAX. Но 64-битный вариант программы может обрабатывать больше данных, и ему может потребоватьсябольшее количество итераций. Поскольку значения переменной Index лежат в диапазоне[0..UINT_MAX], то условие "Index != Count" никогда не выполнится, что и приводит к бесконечномуциклу (см. рисунок 20).
  35. 35. Рисунок 20 – Механизм возникновения вечного циклаПример 23. Работа с битами и операция NOTРабота с битовыми операциями требует особой аккуратности от программиста при разработкекроссплатформенных приложений, в которых типы данных могут иметь различные размеры.Поскольку перенос программы на 64-битную платформу также ведет к изменению размерностинекоторых типов, то высока вероятность возникновения ошибок в участках кода, работающих сотдельными битами. Чаще всего это происходит из-за смешенной работы с 32-битными и 64-битными типами данных. Рассмотрим ошибку, возникшую в коде из-за некорректногоприменения операции NOT:UINT_PTR a = ~UINT_PTR(0);ULONG b = 0x10;UINT_PTR c = a & ~(b - 1);c = c | 0xFu;
  36. 36. if (a != c) cout << "Error" << endl;Ошибка заключается в том, что маска, заданная выражением "~(b - 1)", имеет тип ULONG. Этоприводит к обнулению старших разрядов переменной "a", ходя должны были обнулиться толькомладшие четыре бита (см. рисунок 21).
  37. 37. Рисунок 21 – Ошибка из-за обнуления старших битИсправленный вариант кода может выглядеть следующим образом:UINT_PTR c = a & ~(UINT_PTR(b) - 1);Приведенный пример крайне прост, но хорошо демонстрирует класс ошибок, которые могутвозникать при активной работе с битовыми операциями.Пример 24. Работа с битами, сдвигиptrdiff_t SetBitN(ptrdiff_t value, unsigned bitNum) { ptrdiff_t mask = 1 << bitNum; return value | mask;}
  38. 38. Приведенный код работоспособен на 32-битной архитектуре и позволяет выставлять бит сномерами от 0 до 31 в единицу. После переноса программы на 64-битную платформу возникаетнеобходимость выставлять биты с номерами от 0 до 63. Однако данный код неспособенвыставить старшие биты, с номерами 32-63. Обратите внимание, что числовой литерал "1" имееттип int, и при сдвиге на 32 позиции произойдет переполнение, как показано на рисунке 22.Получим мы в результате 0 (рисунок 22-B) или 1 (рисунок 22-C) - зависит от реализациикомпилятора.Рисунок 22 – a) корректная установка 31-ого бита в 32-битном коде (биты считаются от 0); b,c) -Ошибка установки 32-ого бита на 64-битной системе (два варианта поведения, зависящих откомпилятора)Для исправления кода необходимо сделать константу "1" того же типа, что и переменная mask:
  39. 39. ptrdiff_t mask = static_cast<ptrdiff_t>(1) << bitNum;Заметим также, что неисправленный код приведет еще к одной интересной ошибке. Привыставлении 31 бита на 64-битной системе результатом работы функции будет значение0xffffffff80000000 (см. рисунок 23). Результатом выражения 1 << 31 является отрицательное число -2147483648. Это число представляется в 64-битной целой переменной как 0xffffffff80000000.Рисунок 23 – Ошибка установки 31-ого бита на 64-битной системеПример 25. Работа с битами и знаковое расширениеПриведенная далее ошибка редка, но, к сожалению, достаточно сложна в понимании. Поэтомуостановимся на ней чуть подробнее.struct BitFieldStruct { unsigned short a:15; unsigned short b:13;};BitFieldStruct obj;obj.a = 0x4000;size_t x = obj.a << 17; //Sign Extensionprintf("x 0x%Ixn", x);//Output on 32-bit system: 0x80000000//Output on 64-bit system: 0xffffffff80000000В 32-битной среде порядок вычисления выражения будет выглядеть, как показано на рисунке 24.
  40. 40. Рисунок 24 - Вычисление выражения в 32-битном кодеОбратим внимание, что при вычислении выражения "obj.a << 17" происходит знаковоерасширение типа unsigned short до типа int. Более наглядно, это может продемонстрироватьследующий код:#include <stdio.h>template <typename T> void PrintType(T){ printf("type is %s %d-bitn", (T)-1 < 0 ? "signed" : "unsigned", sizeof(T)*8);}struct BitFieldStruct { unsigned short a:15; unsigned short b:13;};int main(void){ BitFieldStruct bf; PrintType( bf.a ); PrintType( bf.a << 2); return 0;
  41. 41. }Result:type is unsigned 16-bittype is signed 32-bitТеперь посмотрим, к чему приводит наличие знакового расширения в 64-битном коде.Последовательность вычисления выражения показана на рисунке 25.Рисунок 25 - Вычисление выражения в 64-битном кодеЧлен структуры obj.a преобразуется из битового поля типа unsigned short в int. Выражение "obj.a<< 17" имеет тип int, но оно преобразуется в ptrdiff_t и затем в size_t, перед тем как будетприсвоено переменной addr. В результате мы получим число значение 0xffffffff80000000, вместоожидаемого значения 0x0000000080000000.Будьте внимательны при работе с битовыми полями. Для предотвращения описанной ситуации внашем примере достаточно явно привести obj.a к типу size_t....size_t x = static_cast<size_t>(obj.a) << 17; // OKprintf("x 0x%Ixn", x);//Output on 32-bit system: 0x80000000//Output on 64-bit system: 0x80000000Пример 26. Сериализация и обмен даннымиВажным элементом переноса программного решения на новую платформу являетсяпреемственность к существующим протоколам обмена данными. Необходимо обеспечить чтение
  42. 42. существующих форматов проектов, осуществлять обмен данными между 32-битными и 64-битными процессами и так далее.В основном, ошибки данного рода заключаются в сериализации memsize-типов и операцияхобмена данными с их использованием:size_t PixelsCount;fread(&PixelsCount, sizeof(PixelsCount), 1, inFile);Недопустимо использование типов, которые меняют свой размер в зависимости от средыразработки, в бинарных интерфейсах обмена данными. В языке Си++ большинство типов неимеют четкого размера и, следовательно, их все невозможно использовать для этих целей.Поэтому создатели средств разработки и сами программисты создают типы данных, имеющиестрогий размер, такие как __int8, __int16, INT32, word64 и так далее.Даже после внесения исправлений, касающихся размеров типа, вы можете столкнуться снесовместимостью бинарных форматов. Причина кроется в ином представлении данных.Наиболее часто это связано с другой последовательностью байт.Порядок байт - метод записи байтов многобайтовых чисел (см. рисунок 26). Порядок от младшегок старшему (англ. little-endian) - запись начинается с младшего и заканчивается старшим. Этотпорядок записи принят в памяти персональных компьютеров с x86 и x86-64-процессорами.Порядок от старшего к младшему (англ. big-endian) - запись начинается со старшего изаканчивается младшим. Этот порядок является стандартным для протоколов TCP/IP. Поэтому,порядок байтов от старшего к младшему часто называют сетевым порядком байтов (англ. networkbyte order). Этот порядок байт используется процессорами Motorola 68000, SPARC.Кстати, некоторые процессоры могут работать и в порядке от младшего к старшему, и наоборот. Ких числу относится, например IA-64.
  43. 43. Рисунок 26 - Порядок байт в 64-битном типе на little-endian и big-endian системахРазрабатывая бинарный интерфейс или формат данных, следует помнить о последовательностибайт. А если 64-битная система, на которую Вы переносите 32-битное приложение, имеет инуюпоследовательность байт, то вы просто будете вынуждены учесть это в своем коде. Дляпреобразования между сетевым порядком байт (big-endian) и порядком байт (little-endian), можноиспользовать функции htonl(), htons(), bswap_64, и так далее.Пример 27. Изменение выравнивания типовПомимо изменения размеров некоторых типов данных, ошибки могут возникать и из-заизменения правил их выравнивания в 64-битной системе (см. рисунок 27).
  44. 44. Рисунок 27 – Размеры типы и границы их выравнивания (значения точны для Win32/Win64, номогут варьироваться в "Unix-мире" и приводятся просто в качестве примера)Рассмотрим пример описания проблемы, найденного в одном из форумов:Столкнулся сегодня с одной проблемой в Linux. Есть структура данных, состоящая изнескольких полей: 64-битный double, потом 8 unsigned char и один 32-битный int. Итогополучается 20 байт (8 + 8*1 + 4). Под 32-битными системами sizeof равен 20 и всё работаетнормально. А под 64-битным Linuxом sizeof возвращает 24. Т.е. идёт выравнивание по границе64 бит.Далее в форуме идут рассуждения о совместимости данных и просьба совета, как упаковатьданные в структуре. Но не это сейчас интересно. Интереснее то, что здесь наблюдается очереднойтип ошибки, который может возникнуть при переносе приложений на 64-битную систему.
  45. 45. Когда меняются размеры полей в структуре и из-за этого меняется сам размер структуры этопонятно и привычно. Здесь другая ситуация. Размер полей остался прежний, но из-за иных правилвыравнивания размер структуры все равно изменится (см. рисунок 28). Такое поведение можетпривести к разнообразным ошибкам, например в несовместимости форматов сохраняемыхданных.
  46. 46. Рисунок 28 – Схематическое изображение структур и правил выравнивания типовПример 28. Выравнивания типов и почему нельзя писать sizeof(x) +sizeof(y)Иногда программисты используют структуры, в конце которых расположен массив переменногоразмера. Эта структура и выделение памяти для нее может выглядеть следующим образом:struct MyPointersArray { DWORD m_n; PVOID m_arr[1];} object;...malloc( sizeof(DWORD) + 5 * sizeof(PVOID) );...Этот код будет корректно работать в 32-битном варианте, но его 64-битный вариант даст сбой.При выделении памяти, необходимой для хранения объекта типа MyPointersArray, содержащего 5указателей, необходимо учесть, что начало массива m_arr будет выровнено по границе 8 байт.Расположение данных в памяти на разных системах (Win32/Win64) показано на рисунке 29.
  47. 47. Рисунок 29 – Расположение данных в памяти в 32-битной и 64-битной системеКорректный расчет размера должен выглядеть следующим образом:struct MyPointersArray { DWORD m_n; PVOID m_arr[1];} object;...malloc( FIELD_OFFSET(struct MyPointersArray, m_arr) + 5 * sizeof(PVOID) );...В приведенном коде мы узнаем смещение последнего члена структуры и суммируем этосмещение с его размером. Смещение члена структуры или класса можно узнать с использованиеммакроса offsetof или FIELD_OFFSET. Всегда используйте эти макросы для получения смещения вструктуре, не опираясь на свои предположения о размерах типов и правилах их выравнивания.Пример 29. Перегруженные функцииПри перекомпиляции программы может начать выбираться другая перегруженная функция (см.рисунок 30).
  48. 48. Рисунок 30 – Выбор перегруженной функции в 32-битной и 64-битной системеПример проблемы:class MyStack {...public: void Push(__int32 &); void Push(__int64 &); void Pop(__int32 &); void Pop(__int64 &);} stack;ptrdiff_t value_1;stack.Push(value_1);...int value_2;stack.Pop(value_2);Неаккуратный программист помещал и затем выбирал из стека значения различных типов(ptrdiff_t и int). На 32-битной системе их размеры совпадали, все замечательно работало. Когда в64-битной программе изменился размер типа ptrdiff_t, то в стек стало попадать больше байт, чемзатем извлекаться.Пример 30. Ошибки в 32-битных модулях, работающих в WoW64Последний пример посвящен ошибкам в 32-битных программах, которые возникают при ихвыполнении в 64-битной среде. В состав 64-битных программных комплексов еще долго будутвходить 32-битные модули, а следовательно необходимо обеспечить их корректную работу в 64-
  49. 49. битной среде. Подсистема WoW64 очень хорошо справляется со своей задачей, изолируя 32-битное приложение, и практически все 32-битные приложения функционирует корректно. Однакоиногда ошибки все же встречаются и в основном они связаны с механизмом перенаправленияпри работе с файлами и реестром Windows.Например, в комплексе, состоящим из 32-битных и 64-битных модулей, при их взаимодействииследует учитывать, что они используют различные представления реестра. Таким образом, водной из программ следующая строчка в 32-битном модуле стала неработоспособной:lRet = RegOpenKeyEx(HKEY_LOCAL_MACHINE, "SOFTWAREODBCODBC.INIODBC Data Sources", 0, KEY_QUERY_VALUE, &hKey);Чтобы подружить эту программу с другими 64-битными частями, необходимо вписать ключKEY_WOW64_64KEY:lRet = RegOpenKeyEx(HKEY_LOCAL_MACHINE, "SOFTWAREODBCODBC.INIODBC Data Sources", 0, KEY_QUERY_VALUE | KEY_WOW64_64KEY, &hKey);ЗаключениеНаилучший результат при поиске описанных в статье ошибок дает методика статического анализакода. В качестве примера инструмента осуществляющего такой анализ, можно назватьразрабатываемый нами инструмент Viva64, входящий в состав PVS-Studio.Методы статического поиска дефектов позволяют обнаруживать дефекты по исходному кодупрограммы. При этом поведение программы оценивается на всех путях исполненияодновременно. За счет этого, возможно обнаружение дефектов, проявляющихся лишь нанеобычных путях исполнения при редких входных данных. Эта особенность позволяет дополнитьметоды тестирования, повышая надежность программ. Системы статического анализа могутиспользоваться при проведении аудита исходного кода, для систематического устранениядефектов в существующих программах, и встраиваться в цикл разработки, автоматическиобнаруживая дефекты в создаваемом коде.Библиографический список 1. Андрей Карпов, Евгений Рыжков. Уроки разработки 64-битных приложений на языке Си/Си++. http://www.viva64.com/ru/articles/x64-lessons/ 2. Андрей Карпов. Что такое size_t и ptrdiff_t. http://www.viva64.com/art-1-1-72510946.html 3. Андрей Карпов, Евгений Рыжков. 20 ловушек переноса Си++ - кода на 64-битную платформу. http://www.viva64.com/art-1-1-1958348565.html 4. Евгений Рыжков. Учебное пособие по PVS-Studio. http://www.viva64.com/art-4-1- 1796251700.html 5. Андрей Карпов. 64-битный конь, который умеет считать. http://www.viva64.com/art-1-1- 1064884779.html

×