20 пасток перенесення Сі + + – коду на 64-бітну платформу (исходники), Різне, Програмування, статті

Розглянуто програмні помилки, що проявляють себе при перенесенні Сі + + – коду з 32-бітових платформ на 64-бітові платформи. Наведені приклади некоректного коду і способи його виправлення. Перераховано методики і засоби аналізу коду, що дозволяють діагностувати обговорювані помилки.




 
1. Відключені попередження
2. Використання функцій з перемінним кількістю аргументів
3. Магічні константи
4. Зберігання в double цілочисельних значень
5. Операції зсуву
6. Упаковка покажчиків
7. Memsize типи в об’єднаннях
8. Зміна типу масиву
9. Віртуальні функції з аргументами типу memsize
10. Серіалізация і обмін даними
11. Бітові поля
12. Адресна арифметика з вказівниками
13. Індексація масивів
14. Змішання використання простих цілочисельних типів і memsize типів
15. Неявні приведення типів при використанні функцій
16. Перевантажені функції
17. Вирівнювання даних
18. Винятки.
19. Використання застарілих функцій і зумовлених констант.
20. Явні приведення типів
Діагностика помилок
Юніт-тестування
Переглянути джерело
Вбудовані засоби компіляторів
Статичні аналізатори
Висновок
Ресурси
 

Вашій увазі пропонується стаття, присвячена переносу програмного коду 32-біних додатків на 64-бітові системи. Стаття складена для програмістів, що використовують Сі + +, але може бути корисна всім, хто стикається з переносом додатків на інші платформи.


Потрібно чітко розуміти, що новий клас помилок, що виникає при написанні 64-бітних програм, не просто ще кілька нових некоректних конструкцій, серед тисяч інших. Це неминучі складнощі, з якими зіткнуться розробники будь розвивається програми. Дана стаття допоможе бути готовим до цих труднощів і покаже шляхи їх подолання.


Будь-яка нова технологія (як в програмуванні, так і в інших областях) несе в собі крім переваг, деякі обмеження або навіть проблеми використання цієї технології. Точно така ж ситуація відбувається в галузі розробки 64-бітного програмного забезпечення. Ми всі знаємо про те, що 64-бітове програмне забезпечення – це наступний етап розвитку інформаційних технологій. Проте не багато програмістів поки реально зіткнулися з нюансами цієї області, а саме розробки 64-бітних програм.


Ми не будемо затримуватися на перевагах, які відкриває перед програмістами перехід на 64-бітову архітектуру. Даної тематики присвячена велика кількість публікацій, і читачеві не важко їх знайти.


Метою ж цієї статті є детальний огляд тих проблем, з якими може зіткнутися розробник 64-бітних програм. У статті Ви познайомитеся:



Наведена інформація дозволить Вам:



У статті наводиться багато прикладів, які для кращого розуміння варто спробувати в середовищі програмування. Знайомлячись з ними, Ви отримаєте щось більше суми окремих частин. Ви відкриєте двері в світ 64-бітових систем.


Для полегшення розуміння подальшого тексту на початку згадаємо деякі типи, з якими ми можемо зіткнутися (див. таблиця N1).





























Назва типу Розмір-ність типу в бітах (32-бітна система) Розмір-ність типу в бітах (64-бітна система) Опис
ptrdiff_t 32 64 Знаковий цілочисельний тип, що утворюється при відніманні двох покажчиків. Використовується для зберігання розмірів. Іноді використовується як результату функції, що повертає розмір або -1 при виникненні помилки.
size_t 32 64 Беззнаковий цілочисельний тип. Результат оператора sizeof (). Служить для зберігання розміру або кількості об’єктів.
intptr_t uintptr_t SIZE_T SSIZE_T INT_PTR DWORD_PTR і так далі 32 64 Цілочисельні типи, здатні зберігати в собі значення покажчика.
time_t 32 64 Час в секундах.
Таблиця N1. Опис деяких цілочисельних типів.

В тексті буде використовуватися термін “memsize” тип. Під memsize типом ми будемо розуміти будь-який простий цілочисельний тип, здатний зберігати в собі покажчик і міняє свою розмірність при зміні розрядності платформи з 32-біт на 64-біта. Приклади memsize типів: size_t, ptrdiff_t, все покажчики, intptr_t, INT_PTR, DWORD_PTR.


Кілька слів слід приділити моделям даних, що визначають співвідношення розмірів фундаментальних типів для різних систем. У таблиці N2 наведені моделі даних, які можуть бути нам цікаві.




















































  ILP32 LP64 LLP64 ILP64
char 8 8 8 8
short 16 16 16 16
int 32 32 32 64
long 32 64 32 64
long long 64 64 64 64
size_t 32 64 64 64
pointer 32 64 64 64
Таблиця N2. Моделі 32-розрядних і 64-розрядних даних

За замовчуванням у статті буде вважатися, що перенесення програм здійснюється з системи має модель даних ILP32 на системи з моделлю даних LP64 або LLP64.


І останнє, 64-бітна модель в Linux (LP64) і Windows (LLP64) має відмінність тільки в розмірності типу long. Оскільки це їх єдина відмінність, то для узагальнення викладу ми будемо уникати використання типів long, unsigned long, і будемо використовувати типи ptrdiff_t, size_t.


Приступимо до розгляду типових помилок, що виникають при перенесенні програм на 64-бітову архітектуру.


1. Відключені попередження


У всіх книгах присвячених розробці якісного коду рекомендується виставляти рівень попереджень, що видаються компілятором на якомога вищий рівень. Але на практиці зустрічається ситуації, коли для певних частин проекту виставлений менший рівень діагностики або взагалі вимкнений. Звичайно це дуже старий код, який продовжує підтримуватися, але не модифікується. Програмісти, що працюють в проекті, звикли, що цей код працює, і закривають очі на його якість. Тут і криється небезпека пропустити серйозні попередження компілятора, при перенесенні програм на нову 64-бітну систему.


При перенесенні програми слід обов’язково включити попередження для всього проекту, що допомагають перевірити код на сумісність і уважно проаналізувати їх. Це може істотно заощадити час при налагодженні проекту на новій архітектурі.


Якщо цього не зробити, то самі дурні й прості помилки будуть проявляти себе у всьому своєму різноманітті. Ось найпростіший приклад переповнення, який виникне в 64-бітної програмі, якщо повністю ігнорувати попередження.





unsigned char *array[50];
unsigned char size = sizeof(array);
32-bit system: sizeof(array) = 200
64-bit system: sizeof(array) = 400

2. Використання функцій з перемінним кількістю аргументів


Класичним прикладом є некоректне використання функцій printf, scanf і їх різновидів:





1) const char *invalidFormat = “%u”;
size_t value = SIZE_MAX;
printf(invalidFormat, value);




2) char buf[9];
sprintf(buf, “%p”, pointer);

В першому випадку не враховується, що тип size_t не еквівалентний типу unsigned на 64-бітної платформі. Це призведе до висновку на друк некоректного результату, в разі якщо value> UINT_MAX.


У другому випадку, автор коду не врахував, що розмір покажчика в майбутньому може стати більше 32 біт. В результаті на 64-бітної архітектури даний код призведе до переповнення буферу.


Неправильне використання функцій з перемененним кількістю параметрів є поширеною помилкою на всіх архітектурах, а не тільки 64-бітних. Це пов’язано з принциповою небезпекою використання даних конструкцій мови Сі + +. Загальноприйнятою практикою є відмова від них і використання безпечних методик програмування. Ми настійно рекомендуємо модифіковані код і використовувати безпечні методи. Наприклад, можна замінити printf на cout, а sprintf на boost :: format або std :: stringstream.


Якщо Ви змушені підтримувати код, який використовує функції типу sscanf, то в форматі керуючих рядків можна використовувати спеціальні макроси, що розкриваються в необхідні модифікатори для різних систем. Приклад:





// PR_SIZET on Win64 = “I”
// PR_SIZET on Win32 = “”
// PR_SIZET on Linux64 = “l”
// …
size_t u;
scanf(“%” PR_SIZET “u”, &u);

3. Магічні константи


У неякісному коді часто зустрічаються магічні числові константи, наявність яких небезпечно саме по собі. При міграції коду на 64-бітну платформу ці константи можуть зробити код непрацездатним, якщо беруть участь в операціях обчислення адреси, розміру об’єктів або в бітових операціях.


У таблиці N3 перераховані основні магічні константи, які можуть впливати на працездатність програми на новій платформі.






















Значення Опис
4 Кількість байт в типі
32 Кількість біт у типі
0x7fffffff Максимальне значення 32-бітової знакової змінної. Маска для обнулення старшого біта в 32-бітному типі.
0x80000000 Мінімальне значення 32-бітової знакової змінної. Маска для виділення старшого біта в 32-бітному типі.
0xffffffff Максимальне значення 32-бітової змінної. Альтернативна запис -1 як ознака помилки.
Таблиця N3. Основні магічні значення, небезпечні при перенесенні додатків з 32-бітної на 64-бітну платформу.

Слід уважно вивчити код на наявність магічних констант і замінити їх безпечними константами і виразами. Для цього можна використовувати оператор sizeof (), спеціальні значення з , і так далі.


Наведемо кілька помилок, пов’язаних з використанням магічних констант. Найпоширенішою є запис у вигляді числових значень розмірів типів.





1) size_t ArraySize = N * 4;
intptr_t *Array = (intptr_t *)malloc(ArraySize);




2) size_t values[ARRAY_SIZE];
memset(values, ARRAY_SIZE * 4, 0);




3) size_t n, newexp;
n = n >> (32 – newexp);

У всіх випадках, припускаємо, що розмір використовуваних типів завжди дорівнює 4 байта. Виправлення коду полягає у використанні оператор sizeof ().





1) size_t ArraySize = N * sizeof(intptr_t);
intptr_t *Array = (intptr_t *)malloc(ArraySize);




2) size_t values[ARRAY_SIZE];
memset(values, ARRAY_SIZE * sizeof(size_t), 0);

або





   memset(values, sizeof(values), 0); //preferred alternative




3) size_t n, newexp;
n = n >> (CHAR_BIT * sizeof(n) – newexp);

Іноді може знадобитися специфічна константа. Як приклад ми візьмемо значення size_t, де всі біти крім 4 молодших повинні бути заповнені одиницями. У 32-бітної програмі ця константа може бути оголошена таким чином.





// constant “1111..110000”
const size_t M = 0xFFFFFFF0u;

Це некоректний код у разі 64-бітної системи. Такі помилки дуже неприємні, так як запис магічних констант може бути здійснена різними способами і їх пошук достатньо трудомісткий. На жаль, немає жодних шляхів, окрім як знайти і виправити цей код, використовуючи # ifdef або спеціальний макрос.





#ifdef _WIN64
#define CONST3264(a) (a##i64)
#else
#define CONST3264(a) (a)
#endif
const size_t M = ~CONST3264(0xFu);

Іноді як коду помилки або іншого спеціального маркера використовують значення “-1”, записуючи його як “0xffffffff”. На 64-бітної платформі записане вираз некоректно і слід явно використовувати значення -1. Приклад некоректного коду, який використовує значення 0xffffffff як ознака помилки.





#define INVALID_RESULT (0xFFFFFFFFu)
size_t MyStrLen(const char *str) {
if (str == NULL)
return INVALID_RESULT;

return n;
}
size_t len = MyStrLen(str);
if (len == (size_t)(-1))
ShowError();

На всякий випадок уточнимо ваше розуміння, чому дорівнює значення “(size_t) (-1)” на 64-бітної платформі. Можна помилитися, назвавши значення 0x00000000FFFFFFFFu. Згідно з правилами мови Сі + + спочатку значення -1 перетвориться в знаковий еквівалент більшого типу, а потім в беззнаковое значення:





int a = -1;           // 0xFFFFFFFFi32
ptrdiff_t b = a; // 0xFFFFFFFFFFFFFFFi64
size_t c = size_t(b); // 0xFFFFFFFFFFFFFFFui64

Таким чином “(size_t) (-1)” на 64-бітної архітектури представляється значенням 0xFFFFFFFFFFFFFFFui64, яке є максимально великим значенням для 64-бітного типу size_t.


Повернемося до помилки з INVALID_RESULT. Використання константи 0xFFFFFFFFu призводить до невиконання умови “len == (size_t) (-1)” в 64-бітної програмі. Найкраще рішення полягає в зміні коду так, щоб спеціальних маркерних значень не було потрібно. Якщо з якоїсь причини ви не можете від них відмовитися або вважаєте недоцільним суттєві правки коду, то просто використовуйте чесне значення -1.





#define INVALID_RESULT (size_t(-1))


4. Зберігання в double цілочисельних значень


Тип double, як правило, має розмір 64-біта, і сумісний зі стандартом IEEE-754 на 32-бітних і 64-бітних системах. Деякі програмісти використовують тип double для зберігання і роботи з цілочисельними типами.





size_t a = size_t(-1);
double b = a;
–a;
–b;
size_t c = b; // x86: a == c
// x64: a != c

Даний приклад ще можна намагатися виправдовувати на 32-бітної системі, так як тип double має 52 значущих біт і здатний без втрати зберігати 32-бітове ціле значення. Але при спробі зберегти double 64-бітове ціле число точне значення може бути втрачено (див. малюнок 1).


Можливо, наближене значення цілком можна застосувати у вашій програмі, але на всяк випадок хочеться вас попередити про потенційні ефекти на новій архітектурі. І в жодному разі не рекомендується змішувати целочисленную арифметику і арифметику з плаваючою крапкою.


5. Операції зсуву


Операції зсуву при неуважному використанні, можуть принести багато неприємностей при переході від 32-бітної до 64-бітної системи. Почнемо з прикладу функції, виставляє у змінній типу memsize, зазначений вами біт в 1.





ptrdiff_t SetBitN(ptrdiff_t value, unsigned bitNum) {
ptrdiff_t mask = 1 << bitNum;
return value / mask;
}

Наведений код працездатний на 32-бітної архітектури і дозволяє виставляти біти з номерами від 0 дл 31. Після перенесення програми на 64-бітну платформу виникне необхідність виставляти біти від 0 до 63. Як ви думаєте, яке значення поверне наступний виклик функції SetBitN (0, 32)? Якщо Ви думаєте, що 0x100000000, то автори раді, що не дарма підготували цю статтю. Ви отримаєте 0.


Для виправлення коду необхідно зробити константу “1” того ж типу, що й мінлива mask.





ptrdiff_t mask = ptrdiff_t(1) << bitNum;

або





ptrdiff_t mask = CONST3264(1) << bitNum;

Ще одне питання. Чому буде дорівнює результат виклику невиправленої функції SetBitN (0, 31)? Правильна відповідь 0xffffffff80000000. Результатом виразу 1 << 31 є негативне число -2147483648. Це число представляється в 64-бітної цілої змінної як 0xffffffff80000000. Слід пам'ятати і враховувати ефекти зсуву значень різних типів. Для кращого розуміння та наочності викладеної інформації в таблиці N4 наведено ряд цікавих виразів зі зрушеннями на 64-бітної системи.




























Вираз Результат (Dec) Результат (Hex)
ptrdiff_t Result; Result = 1 << 31; -2147483648 0xffffffff80000000
Result = ptrdiff_t(1) << 31; 2147483648 0x0000000080000000
Result = 1U << 31; 2147483648 0x0000000080000000
Result = 1 << 32; 0 0x0000000000000000
Result = ptrdiff_t(1) << 32; 4294967296 0x0000000100000000
Таблиця N4. Вирази зі зрушеннями та результати на 64-бітної системи.

6. Упаковка покажчиків


Велика кількість помилок при мігрування на 64-бітові системи пов’язані зі зміною розміру покажчика по відношенню до розміру звичайних цілих. В середовищі з моделлю даних ILP32 звичайні цілі і покажчики мають однаковий розмір. На жаль 32-бітний код повсюдно спирається на це припущення. Покажчики часто приводяться до int, unsigned int та іншим невідповідним типам для виконання адресних розрахунків.


Слід чітко пам’ятати, що для цілочисельного подання покажчиків слід використовувати тільки memsize типи. Перевага, мабуть, слід віддавати типу uintptr_t, так як він краще висловлює наміри і робить код більш стерпним, оберігаючи його від змін в майбутньому.


Розглянемо два невеликих прикладу.





1) char *p;
p = (char *) ((int)p & PAGEOFFSET);




2) DWORD tmp = (DWORD)malloc(ArraySize);

int *ptr = (int *)tmp;

Обидва прикладу не враховують, що розмір покажчика може відрізнятися від 32-біт. Використовується явне приведення типу, відкидає старші біти в покажчику, що є явною помилкою на 64-бітної системи. Виправлені варіанти, які використовують для упаковки покажчиків цілочисельні memsize типи (intptr_t і DWORD_PTR), наведені нижче:





1) char *p;
p = (char *) ((intptr_t)p & PAGEOFFSET);




2) DWORD_PTR tmp = (DWORD_PTR)malloc(ArraySize);

int *ptr = (int *)tmp;

Небезпека двох розглянутих прикладів в тому, що збій в програмі може бути виявлений через дуже великий проміжок часу. Програма може абсолютно коректно працювати з невеликим об’ємом даних на 64-бітної системі, поки оброблювані адреси знаходяться в просторі перших чотирьох гігабайт пам’яті. А потім при запуску програми на великих виробничих задачах відбудеться виділення пам’яті за межами цій галузі. Розглянутий в прикладах код, обробляючи покажчик на об’єкт поза цій галузі, призведе до невизначеного поведінки програми.


Наступний наведений код не буде ховатися та проявить себе при першому виконанні.





void GetBufferAddr(void **retPtr) {

// Access violation on 64-bit system
*retPtr = p;
}
unsigned bufAddress;
GetBufferAddr((void **)&bufAddress);

Виправлення також полягає у виборі типу, здатного вміщати в себе покажчик.





uintptr_t bufAddress;
GetBufferAddr((void **)&bufAddress); //OK

Бувають ситуації, коли упаковка покажчика в 32-бітний тип просто необхідна. В основному такі ситуації виникають при необхідності роботи зі старими API функціями. Для таких випадків слід вдатися до спеціальних функцій, таких як LongToIntPtr, PtrToUlong і так далі.


Наприкінці хочеться зауважити, що поганим стилем буде упаковка покажчика в типи завжди рівні 64-бітам. Показаний далі код знову доведеться виправляти з приходом 128-бітних систем.





PVOID p;
// Bad style. The 128-bit time will come.
__int64 n = __int64(p);
p = PVOID(n);

7. Memsize типи в об’єднаннях


Особливістю об’єднання є те, що для всіх елементів членів об’єднання виділяється одна і та ж область пам’яті, тобто вони перекриваються. Хоча доступ до цієї області пам’яті можливий з використанням будь-якого з елементів, елемент для цієї мети повинен вибиратися так, щоб отриманий результат не був безглуздим.


Слід уважно поставитися до об’єднань, що мають в своєму складі покажчики та інші члени типу 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. Слід використовувати тип, який буде відповідати розміром вказівником.





union PtrNumUnion {
char *m_p;
size_t m_n; //type fixed
} u;

Інше часте використання об’єднання полягає в поданні одного члена, набором інших більш дрібних. Наприклад, нам може знадобитися розбити значення типу size_t на байти для реалізації табличного алгоритму підрахунку кількості нульових бітів у байті.





union SizetToBytesUnion {
size_t value;
struct {
unsigned char b0, b1, b2, b3;
} bytes;
} u;

SizetToBytesUnion u;
u.value = value;
size_t zeroBitsN = TranslateTable[u.bytes.b0] +
TranslateTable[u.bytes.b1] +
TranslateTable[u.bytes.b2] +
TranslateTable[u.bytes.b3];


Тут допущена принципова алгоритмічна помилка, яка полягає у припущенні, що тип size_t складається з 4 байт. Можливість автоматичного пошуку алгоритмічних помилок поки навряд чи можлива, але ми можемо здійснити пошук всіх об’єднань і перевірити наявність в них memsize типів. Знайшовши таке об’єднання, ми можемо виявити алгоритмічну помилку і переписати код наступним чином.





union SizetToBytesUnion {
size_t value;
unsigned char bytes[sizeof(value)];
} u;

SizetToBytesUnion u;
u.value = value;
size_t zeroBitsN = 0;
for (size_t i = 0; i != sizeof(bytes); ++i)
zeroBitsN += TranslateTable[bytes[i]];


8. Зміна типу масиву


Іноді в програмах необхідно (або просто зручно) подати елементи масиву у вигляді елементів іншого типу. Небезпечне і безпечне приведення типів представлено в наступному коді.





int array[4] = { 1, 2, 3, 4 };
enum ENumbers { ZERO, ONE, TWO, THREE, FOUR };
//safe cast (for MSVC2005)
ENumbers *enumPtr = (ENumbers *)(array);
cout << enumPtr[1] << ” “;
//unsafe cast
size_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 (див. малюнок 3). В деяких випадках саме така поведінка і буває потрібно, але зазвичай це помилка.


Виправлення описаної ситуації полягає у відмові від небезпечних привидів типів шляхом модернізації програми. Іншим варіантом є створення нового масиву і копіювання в нього значень з вихідного масиву.


9. Віртуальні функції з аргументами типу memsize


Якщо у вас в програмі є великі ієрархії успадкування класів з віртуальними функціями, то існує ймовірність використання через неуважність аргументів різних типів, але які фактично співпадають на 32-бітної системи. Наприклад, в базовому класі Ви використовуєте як аргумент віртуальної функції тип size_t, а в спадкоємця тип unsigned. Відповідно на 64-бітної системі цей код буде некоректне.


Але така помилка не обов’язково криється в складних ієрархіях спадкування, і ось один з прикладів.





сlass CWinApp {

virtual void WinHelp(DWORD_PTR dwData, UINT nCmd);
};
class CSampleApp : public CWinApp {

virtual void WinHelp(DWORD dwData, UINT nCmd);
};

Простежимо життєвий цикл розробки деякого програми. Нехай спочатку воно розроблялося під Microsoft Visual C + + 6.0. коли, функція WinHelp в класі CWinApp мала наступний прототип:





virtual void WinHelp(DWORD dwData, UINT nCmd = HELP_CONTEXT);

Абсолютно вірно було здійснити перекриття віртуальної функції в класі CSampleApp, як показано в прикладі. Потім проект був перенесений в Microsoft Visual C + + 2005, де прототип функції в класі CWinApp зазнав змін, які полягають у зміні типу DWORD на тип DWORD_PTR. На 32-бітної системі програма продовжить зовсім коректно працювати, так як тут типи DWORD і DWORD_PTR збігаються. Неприємності проявлять себе при компіляції даного коду під 64-бітну платформу. Вийдуть дві функції з однаковими іменами, але з різними параметрами, в результаті чого перестане викликатися користувальницький код.


Виправлення полягає в використанні однакових типів у відповідних віртуальних функціях.





сlass CSampleApp: public CWinApp {

virtual void WinHelp(DWORD_PTR dwData, UINT nCmd);
};

10. Серіалізация і обмін даними


Важливим пунктом при перенесенні програмного рішення на нову платформу є наступність до існуючих протоколів обміну даними. Необхідно забезпечити читання існуючих форматів проектів, здійснювати обмін даними між 32-бітними і 64-бітними процесами і так далі.


В основному помилки даного роду полягають в сериализации memsize типів та операціях обміну даними з їх використанням.





1) size_t PixelCount;
fread(&PixelCount, sizeof(PixelCount), 1, inFile);




2) __int32 value_1;
SSIZE_T value_2;
inputStream >> value_1 >> value_2;




3) time_t time;
PackToBuffer(MemoryBuf, &time, sizeof(time));

У всіх наведених прикладах є помилки двох видів: використання типів непостійною розмірності в бінарних інтерфейсах та ігнорування порядку байт.


Використання типів непостійною розмірності. Неприпустимо використання типів, які змінюють свій розмір залежно від середовища розробки, в бінарних інтерфейсах обміну даними. У мові Сі + + всі типи не мають чіткого розміру і, отже, їх все неможливо використовувати для цих цілей. Тому творці засобів розробки і самі програмісти, створюють типи даних, що мають строгий розмір, такі як __ int8, __ int16, INT32, word64 і так далі.


Використання подібних типів забезпечує переносимість даних між програмами на різних платформах, хоча і вимагає додаткових зусиль. Три показаних прикладу написані неакуратно, що і дасть про взнаки при зміні розрядності деяких типів даних з 32-біт до 64-біт. Враховуючи необхідність підтримки старих форматів даних, виправлення може виглядати наступним чином.





1) size_t PixelCount;
__uint32 tmp;
fread(&tmp, sizeof(tmp), 1, inFile);
PixelCount = static_cast(tmp);




2) __int32 value_1;
__int32 value_2;
inputStream >> value_1 >> value_2;




3) time_t time;
__uint32 tmp = static_cast<__uint32>(time);
PackToBuffer(MemoryBuf, &tmp, sizeof(tmp));

Але наведений варіант виправлення може бути не найкращим. При переході на 64-бітну систему програма може обробляти більшу кількість даних, і використання в даних 32-бітних типів може стати суттєвою перешкодою. В такому випадку можна залишити старий код для сумісності зі старим форматом даних, виправивши некоректні типи. І реалізувати новий бінарний формат даних, вже з урахуванням допущених помилок. Ще одним варіантом може стати відмова від бінарних форматів і перехід на текстовий формат або інші формати, що надаються різними бібліотеками.


Ігнорування порядку байт (byte order). Навіть після внесення виправлень стосуються розмірів типу, ви можете зіткнутися з несумісністю бінарних форматів. Причина – інше уявлення даних. Найбільш часто це пов’язано з іншого послідовністю байт.


Порядок байт – метод запису байтів мультибайтних чисел (див. також малюнок 4). Порядок від молодшого до старшого (англ. little-endian) – запис починається з молодшого і закінчується старшим. Цей порядок записи прийнятий в пам’яті персональних комп’ютерів з x86-процесорами. Порядок від старшого до молодшого (англ. big-endian) – запис починається зі старшого і закінчується молодшим. Цей порядок є стандартним для протоколів TCP / IP. Тому, порядок байтів від старшого до молодшого часто називають мережевим порядком байтів (англ. network byte order). Цей порядок байт використовується процесорами Motorola 68000, SPARC.


Розробляючи бінарний інтерфейс або формат даних, слід пам’ятати про послідовність байт. А якщо 64-бітна система, на яку Ви переносите 32-бітове додаток, має іншу послідовність байт, то ви просто будете змушені врахувати це в своєму коді. Для перетворення між мережевим порядком байт (big-endian) та порядком байт (little-endian), можна використовувати функції htonl (), htons (), bswap_64, і так далі.


11. Бітові поля


Якщо ви використовуєте бітові поля, то ви повинні враховувати, що використання memsize типів спричинить зміна розмірів структур і вирівнювання. Наприклад, наведена далі структура матиме розмір 4 байта на 32-бітної системі і 8 байт на 64-бітної системи.





struct MyStruct {
size_t r : 5;
};

Але на цьому ваша уважність до бітовим полям обмежуватися не повинна. Розглянемо тонкий приклад.





struct BitFieldStruct {
unsigned short a:15;
unsigned short b:13;
};
BitFieldStruct obj;
obj.a = 0x4000;
size_t addr = obj.a << 17; //Sign Extension
printf(“addr 0x%Ixn”, addr);
//Output on 32-bit system: 0x80000000
//Output on 64-bit system: 0xffffffff80000000

Зверніть увагу, що якщо приклад скомпілювати для 64-бітної системи, то у виразі “addr = obj.a << 17;" присутній знакова розширення, незважаючи на те, що обидві змінні addr і obj.a є беззнаковими. Це знакове розширення обумовлено правилами приведення типів, які застосовуються таким чином (див. також малюнок 5):


Член структури obj.a перетвориться з бітового поля типу unsigned short в int. Ми отримуємо тип int, а не unsigned int через те, що 15-бітове поле поміщається в 32-бітове знакове ціле.


Вираз “obj.a << 17" має тип int, але воно перетвориться в ptrdiff_t і потім в size_t, перед тим як буде присвоєно змінної addr. Знакова розширення відбувається в момент вчинення перетворення з int в ptrdiff_t.


Так що будьте уважні при роботі з бітовими полями. Для уникнення описаного ефекту в нашому прикладі, нам досить явно привести obj.a до типу size_t.






size_t addr = size_t(obj.a) << 17;
printf(“addr 0x%Ixn”, addr);
//Output on 32-bit system: 0x80000000
//Output on 64-bit system: 0x80000000

12. Адресна арифметика з вказівниками


Приклад перший.





unsigned short a16, b16, c16;
char *pointer;

pointer += a16 * b16 * c16;

Даний приклад коректно працює з покажчиками, якщо значення виразу “a16 * b16 * c16” не перевищує UINT_MAX (4Gb). Такий код міг завжди коректно працювати на 32-бітній платформі, так як програма ніколи не виділяла масивів великих розмірів. На 64-бітної архітектури розмір масиву перевищив UINT_MAX елементів. Припустимо, ми хочемо зрушити значення покажчика на 6.000.000.000 байт, і з цього змінні a16, b16 і c16 мають значення 3000, 2000 і 1000 відповідно. При обчисленні виразу “a16 * b16 * c16” всі змінні, згідно з правилами мови Сі + +, будуть приведені до типу int, а вже потім буде проведено їх множення. В ході виконання множення, відбудеться переповнення. Некоректний результат виразу буде розширений до типу ptrdiff_t і відбудеться некоректне обчислення покажчика.


Слід старанно уникати можливих переповнень в арифметиці з покажчиками. Для цього краще всього використовувати memsize типи або явне приведення типів у виразах, де присутні покажчики. Використовуючи явне приведення типів, мо можемо переписати наш код наступним чином.





short a16, b16, c16;
char *pointer;

pointer += static_cast(a16) *
static_cast(b16) *
static_cast(c16);

Якщо Ви думаєте, що пригоди чекають неакуратні програми тільки на великих обсягах даних, то ми змушені вас засмутити. Розглянемо цікавий код для роботи з масивом, що містить всього 5 елементів. Другий приклад працездатний в 32-бітному варіанті і не працездатний в 64-бітному.





int A = -2;
unsigned B = 1;
int array[5] = { 1, 2, 3, 4, 5 };
int *ptr = array + 3;
ptr = ptr + (A + B); //Invalid pointer value on 64-bit platform
printf(“%in”, *ptr); //Access violation on 64-bit platform

Давайте прослідкуємо, як відбувається обчислення виразу “ptr + (a + b)”:



Потім відбувається обчислення виразу “ptr + 0xFFFFFFFFu”, але що з цього вийде, залежить від розміру покажчика на даній архітектурі. Якщо складання буде відбуватися в 32-бітної програмі, то даний вираз буде еквівалентно “ptr – 1” і ми успішно роздрукуємо число 3.


У 64-бітної програмі до покажчика чесним чином додасться значення 0xFFFFFFFFu, в результаті чого покажчик виявиться далеко за межами масиву. І при доступі до елементу з даного вказівником нас чекають неприємності.


Для запобігання показаної ситуації, як і в першому випадку, рекомендуємо використовувати в арифметиці з покажчиками тільки memsize типи. Два варіанти виправлення коду:





ptr = ptr + (ptrdiff_t(A) + ptrdiff_t(B));




ptrdiff_t A = -2;
size_t B = 1;

ptr = ptr + (A + B);

Ви можете заперечити і запропонувати наступний варіант виправлення:





int A = -2;
int B = 1;

ptr = ptr + (A + B);

Так, такий код буде працювати, але він поганий по ряду причин:



13. Індексація масивів


Цей різновид помилок виділена для кращої структуризації викладу, так як індексація в масивах з використанням квадратних дужок це всього лише інша запис адресної арифметики, розглянутої до цього.


У програмуванні на мові Сі, а потім і Сі + + склалася практика використовувати в конструкціях наступного виду змінні типу int / unsigned:





unsigned Index = 0;
while (MyBigNumberField[Index] != id)
Index++;

Але час іде і все змінюється. І ось тепер прийшов час сказати – більше так не робіть! Використовуйте для індексації (великих) масивів тільки memsize типи.


Наведений код не зможе обробити в 64-бітної програмі масив, що містить більше UINT_MAX елементів. Після доступу до елементу з індексом UINT_MAX відбудеться переповнення змінної Index і ми отримаємо вічний цикл.


Щоб остаточно переконати Вас у необхідності використання тільки memsize типів для індексації та у виразах адресної арифметики, наведемо останній приклад.





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, що забезпечують доступ до необхідних елементів. Але наведений код буде коректно працювати тільки з масивами, що містять менш INT_MAX елементів. Причина – використання 32-бітних типів int для обчислення індексу елемента.


Програмісти часто припускаються помилки, намагаючись виправити код наступним чином:





float Region::GetCell(int x, int y, int z) const {
return array[static_cast(x) + y * Width +
z * Width * Height];
}

Вони знають, що за правилами мови Сі + + вираз для обчислення індексу буде мати тип ptrdiff_t і сподіваються за рахунок цього уникнути переповнення. Але переповнення може статися всередині подвираженія “y * Width” або “z * Width * Height”, так як для їх обчислення і раніше використовується тип int.


Якщо Ви хочете виправити код, не змінюючи типів змінних, що беруть участь у вираженні, то Ви можете явно привести кожну змінну до memsize типу:





float Region::GetCell(int x, int y, int z) const {
return array[ptrdiff_t(x) +
ptrdiff_t(y) * ptrdiff_t(Width) +
ptrdiff_t(z) * ptrdiff_t(Width) *
ptrdiff_t(Height)];
}

Інше рішення – змінити типи змінних на memsize тип:





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];
}

14. Змішання використання простих цілочисельних типів і memsize типів


Змішане використання memsize і не memsize типів у виразах може призводити до некоректних результатів на 64-бітних системах і бути пов’язано зі зміною діапазону вхідних значень. Розглянемо ряд прикладів.





size_t Count = BigValue;
for (unsigned Index = 0; Index != Count; ++Index)
{ … }

Це приклад вічного циклу, якщо Count> UINT_MAX. Припустимо, що на 32-бітних системах цей код працював з діапазоном менш UINT_MAX ітерацій. Але 64-бітний варіант програми може обробляти більше даних і йому може знадобитися більша кількість ітерацій. Оскільки значення змінної Index лежать в діапазоні [0 .. UINT_MAX], то умова “Index! = Count” ніколи не виконається, що і призводить до нескінченного циклу.


Інша часта помилка – запис виразів наступного вигляду:





int x, y, z;
intptr_t SizeValue = x * y * z;

Раніше вже розглядалися подібні приклади, коли при обчисленні значень з використанням не memsize типів відбувалося арифметичне переповнення. І кінцевий результат був некоректний. Пошук і виправлення наведеного коду ускладнюється тим, що компілятори, як правило, не видають на нього ніяких попереджень. З точки зору мови Сі + + це зовсім коректна конструкція. Відбувається множення декількох змінних типу int, після чого результат неявно розширюється до типу intptr_t і відбувається присвоювання.


Наведемо невеликий код, що показує небезпеку неакуратних виразів зі змішанням типами (результати отримані з використанням Microsoft Visual C + + 2005, 64-бітний режим компіляції).





int x = 100000;
int y = 100000;
int z = 100000;
intptr_t size = 1; // Result:
intptr_t v1 = x * y * z; // -1530494976
intptr_t v2 = intptr_t(x) * y * z; // 1000000000000000
intptr_t v3 = x * y * intptr_t(z); // 141006540800000
intptr_t v4 = size * x * y * z; // 1000000000000000
intptr_t v5 = x * y * z * size; // -1530494976
intptr_t v6 = size * (x * y * z); // -1530494976
intptr_t v7 = size * (x * y) * z; // 141006540800000
intptr_t v8 = ((size * x) * y) * z; // 1000000000000000
intptr_t v9 = size * (x * (y * z)); // -1530494976

Необхідно щоб всі операнди в подібних виразах були заздалегідь наведено до типу більшої розрядності. Пам’ятайте, що вираз вигляду





intptr_t v2 = intptr_t(x) * y * z;

зовсім не гарантує правильний результат. Воно гарантує тільки, що вираз “intptr_t (x) * y * z” матиме тип intptr_t. Правильний результат, показаний цим виразом у прикладі – везіння, обумовлене конкретної версією компілятора і фазою місяця.


Порядок обчислення виразу з операторами однакового пріоритету не визначений. Точніше, компілятор вільний обчислювати подвираженія в тому порядку, який він вважає більш ефективним, навіть якщо подвираженія викликають побічні ефекти. Порядок виникнення побічних ефектів не визначений. Вирази, що включають в себе комутативні і асоціативні операції (*, +, &, /, ^), можуть бути реорганізовані довільним чином, навіть при наявності дужок. Для завдання визначеного порядку обчислення вираження необхідно використовувати явну тимчасову змінну.


По цьому, якщо результатом вираження повинен бути memsize тип, то у виразі повинні брати участь тільки memsize типи. Або елементи, приведені до memsize типам. Правильний варіант:





intptr_t v2 = intptr_t(x) * intptr_t(y) * intptr_t(z); // OK!





ПРИМІТКА
 

Примітка. Якщо у Вас багато цілочисельних обчислень і контроль над переповнення для вас є важливим завданням, то ми пропонуємо звернути Вашу увагу на клас SafeInt, реалізацію та опис якого можна знайти в MSDN.


Змішання використання типів може виявлятися й у зміні програмної логіки.





ptrdiff_t val_1 = -1;
unsigned int val_2 = 1;
if (val_1 > val_2)
printf (“val_1 is greater than val_2n”);
else
printf (“val_1 is not greater than val_2n”);
//Output on 32-bit system: “val_1 is greater than val_2”
//Output on 64-bit system: “val_1 is not greater than val_2”

На 32-бітної системі мінлива val_1 згідно з правилами мови Сі + + розширювалася до типу unsigned int і ставала значенням 0xFFFFFFFFu. В результаті умова “0xFFFFFFFFu> 1” виконувалося. На 64-бітної системі навпаки розширюється мінлива val_2 до типу ptrdiff_t. В цьому випадку вже перевіряється вираз “-1> 1”. На малюнку 6 схематично відображено відбуваються перетворення.


Якщо вам необхідно повернути колишнє поведінка коду вам слід змінити тип змінної val_2.





ptrdiff_t val_1 = -1;
size_t val_2 = 1;
if (val_1 > val_2)
printf (“val_1 is greater than val_2n”);
else
printf (“val_1 is not greater than val_2n”);

15. Неявні приведення типів при використанні функцій


Розглядаючи попередній клас помилок, пов’язаний із змішуванням простих цілочисельних типів і memsize типів, ми розглядали тільки прості вирази. Але схожі проблеми можуть проявитися і при використанні інших конструкцій мови Сі + +.





extern int Width, Height, Depth;
size_t GetIndex(int x, int y, int z) {
return x + y * Width + z * Width * Height;
}

MyArray[GetIndex(x, y, z)] = 0.0f;

У разі роботи з великими масивами (більше INT_MAX елементів) даний код буде вести себе некоректно, і ми буде адресуватися не до тих елементів масиву MyArray, до яких розраховуємо. Не дивлячись на те, що ми повертаємо значення типу size_t, вираз “x + y * Width + z * Width * Height” обчислюється з використанням типу int. Ми думаємо, Ви вже здогадалися, що виправлений код буде виглядати наступним чином:





extern int Width, Height, Depth;
size_t GetIndex(int x, int y, int z) {
return (size_t)(x) +
(size_t)(y) * (size_t)(Width) +
(size_t)(z) * (size_t)(Width) * (size_t)(Height);
}

У наступному прикладі, у нас знову змішується memsize тип (покажчик) і простий тип unsigned.





extern char *begin, *end;
unsigned GetSize() {
return end – begin;
}

Результат виразу “end – begin” має тип ptrdiff_t. Оскільки функція повертає тип unsigned, то відбувається неявне приведення типу, при якому старші біти результату губляться. Таким чином, якщо покажчики begin і end, посилаються на початок і кінець масиву, за розміром більшого UINT_MAX (4Gb), то функція поверне некоректне значення.


І ще один приклад, але на цей раз розглянемо не повертається значення, а формальний аргумент функції.





void foo(ptrdiff_t delta);
int i = -2;
unsigned k = 1;
foo(i + k);

Вам цей код не нагадує приклад некоректної арифметикою з покажчиками, розглянутий раніше? Так, тут відбувається те ж саме. Некоректний результат виникає при неявному розширенні фактичного аргументу, має значення 0xFFFFFFFF і тип unsigned до типу ptrdiff_t.


 


16. Перевантажені функції


При перенесенні 32-бітних програм на 64-бітну платформу може спостерігатися зміна логіки її роботи, пов’язане з використанням перевантажених функцій. Якщо функція перекрита для 32-бітних і 64-бітових значень, то звернення до неї з аргументом типу memsize транслюватиметься в різні виклики на різних системах. Цей прийом може бути корисний, як наприклад, у наведеному коді:





static size_t GetBitCount(const unsigned __int32 &) {

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*