20 пасток перенесення коду Сі + + на 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_tu intptr_t SIZE_TS SIZE_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).




Рисунок 1. Кількість значущих бітів в типах size_t і double.

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


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” має тип int і при зсуві на 32 позиції відбудеться переповнення, як показано на малюнку 2.





Рисунок 2. Обчислення виразу “ptrdiff_t mask = 1 << bitNum".


Для виправлення коду необхідно зробити константу “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). В деяких випадках саме така поведінка і буває потрібно, але зазвичай це є помилкою.




Рисунок 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<size_t>(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.




Рисунок 4. Порядок байт в 64-бітному типі на little-endian і big-endian системах.

Розробляючи бінарний інтерфейс або формат даних, слід пам’ятати про послідовність байт. А якщо 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%Ix
“, 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.







Рисунок 5. Обчислення виразу на різних системах.

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



size_t addr = size_t(obj.a) << 17;
printf(“addr 0x%Ix
“, 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<ptrdiff_t>(a16) *
static_cast<ptrdiff_t>(b16) *
static_cast<ptrdiff_t>(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(“%i
“, *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<ptrdiff_t>(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_2
“);
else
printf (“val_1 is not greater than val_2
“);
//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 схематично відображено відбуваються перетворення.







Малюнок 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_2
“);
else
printf (“val_1 is not greater than val_2
“);

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 &) {
return 32;
}
static size_t GetBitCount(const unsigned __int64 &) {
return 64;
}
size_t a;
size_t bitCount = GetBitCount(a);

Але така зміна логіки зберігає в собі небезпеку. Уявіть собі програму, де для якихось цілей використовується клас для організації стека. Особливість цього класу в тому, що він дозволяє зберігати значення різних типів:


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


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


17. Вирівнювання даних


Процесори працюють ефективніше, коли мають справу з правильно вирівняними даними. Як правило, 32-бітний елемент даних повинен бути вирівняний по межі, кратної 4 байт, а 64-бітний елемент – по межі 8 байт. Спроба працювати з не вирівняними даними на процесорах IA-64 (Itanium), як показано в наступному прикладі, призведе до виникнення виключення:


#pragma pack (1) // Also set by key /Zp in MSVC
struct AlignSample {
unsigned size;
void *pointer;
} object;
void foo(void *p) {
object.pointer = p; // Alignment fault
}

Якщо Ви змушені працювати з не вирівняними даними на Itanium, то слід явно вказати це компілятору. Наприклад, скористатися спеціальним макросом UNALIGNED:


#pragma pack (1) // Also set by key /Zp in MSVC
struct AlignSample {
unsigned size;
void *pointer;
} object;
void foo(void *p) {
*(UNALIGNED void *)&object.pointer = p; //Very slow
}

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


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


Розглянемо ще один приклад коду, не враховує вирівнювання даних:


struct MyPointersArray {
DWORD m_n;
PVOID m_arr[1];
} object;

malloc( sizeof(DWORD) + 5 * sizeof(PVOID) );


Якщо ми хочемо виділити обсяг пам’яті, необхідний для зберігання об’єкта типу MyPointersArray, що містить 5 покажчиків, то ми повинні врахувати, що початок масиву m_arr буде вирівняно по межі 8 байт. Розташування даних у пам’яті на різних системах (Win32/Win64) показано на малюнку 7.



Малюнок 7. Вирівнювання даних у пам’яті на системах Win32 і Win64


Коректний розрахунок розміру повинен виглядати наступним чином:


struct MyPointersArray {
DWORD m_n;
PVOID m_arr[1];
} object;

malloc( FIELD_OFFSET(struct MyPointersArray, m_arr) +
5 * sizeof(PVOID) );


У наведеному коді ми дізнаємося зсув останнього члена структури і підсумовуємо це зміщення з його розміром. Зсув члена структури або класу можна дізнатися з використанням макросу offsetof або FIELD_OFFSET.


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


struct TFoo {
DWORD_PTR whatever;
int value;
} object;
int *valuePtr =
(int *)((size_t)(&object) + offsetof(TFoo, value)); // OK

18. Винятки.


Генерування та обробка винятків за участю цілочисельних типів не є гарною практикою програмування на мові Сі + +. Для цих цілей слід використовувати більш інформативні типи, наприклад класи, похідні від класів std :: exception. Але іноді все-таки доводиться працювати з менш якісним кодом, таким як показано нижче:


char *ptr1;
char *ptr2;
try {
try {
throw ptr2 – ptr1;
}
catch (int) {
std::cout << “catch 1: on x86” << std::endl;
}
}
catch (ptrdiff_t) {
std::cout << “catch 2: on x64” << std::endl;
}

Слід ретельно уникати генерування або обробку винятків з використанням memsize-типів, так як це загрожує зміною логіки роботи програми. Виправлення даного коду може полягати в заміні “Catch (int)” на “catch (ptrdiff_t)”. А більш правильним рішенням буде використання спеціального класу для передачі інформації про виниклу помилку.


19. Використання застарілих функцій і зумовлених констант.


Розробляючи 64-бітове додаток, пам’ятайте про зміни середовища, в якому воно тепер буде виконуватися. Частина функцій стануть застарілими, їх буде необхідно змінити на оновлені варіанти. Прикладом такої функції в ОС Windows буде GetWindowLong. Зверніть увагу на константи, що відносяться до взаємодії з середовищем, в якій виконується програма. У Windows підозрілими будуть рядки, які містять “System32” або “Program Files”.


20. Явні приведення типів


Будьте обережні з явними привидами типів. Вони можуть змінити логіку виконання програми при зміні розрядності типів або спровокувати втрату значущих бітів. Привести типові приклади помилок, пов’язаних з явним приведенням типів складно, тому що вони дуже різноманітні і специфічні для різних програм. З деякими з помилками, пов’язаними з явним приведенням типів, Ви вже познайомилися раніше.


Діагностика помилок


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


Юніт-тестування


Юніт-тестування давно завоювало заслужену повагу серед програмістів. Юніт-тести допоможуть перевірити коректність програми після перенесення на нову платформу. Але тут є одна тонкість, про яку Ви повинні пам’ятати.


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


Тому, модифікуючи набори тестів, пам’ятайте про компроміс між швидкістю виконання ЮНІТЕСТ і повнотою перевірок. На щастя, переконатися в працездатності Ваших додатків можуть допомогти інші методики.


Переглянути джерело


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


Але у цього підходу до аналізу вихідного коду є один суттєвий недолік. Він вимагає дуже великої кількості часу, через що практично непридатний на великих проектах.


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


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


Вбудовані засоби компіляторів


Частина завдань з пошуком дефектного коду дозволяють вирішувати компілятори. У них часто бувають вбудовані різні механізми для діагностики розглянутих нами помилок. Наприклад, в Microsoft Visual C + + 2005 Вам можуть бути корисні наступні ключі: / Wp64, / Wall, а в SunStudio C + + ключ-xport64.


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


Статичні аналізатори


Статичні аналізатори – прекрасний засіб підвищення якості та надійності програмного коду. Основна складність, пов’язана з використанням статичних аналізаторів полягає в тому, що вони генерують досить багато помилкових повідомлень про потенційні помилки. Програмісти, будучи по натурі ледачими, використовують цей аргумент, щоб так чи інакше не займатися виправленням знайдених помилок. У Microsoft ця проблема вирішується безумовним внесенням виявлених помилок в bug tracking систему. Тим самим у програміста не залишається вибору між виправленням коду і спробами уникнути цього.


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


Статичні аналізатори можуть з успіхом використовуватися для діагностики багатьох з розглянутих у статті класів помилок.


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



  1. Gimpel Software PC-Lint (http://www.gimpel.com). Даний аналізатор має широкий списком підтримуваних платформ і є статичним аналізатором загального призначення. Він дозволяє виявляти помилки при перенесення програм на архітектуру з моделлю даних LP64. Перевагою є можливість побудови жорсткого контролю над перетвореннями типів. До недоліків можна віднести відсутність середовища, але це можна виправити, використовуючи сторонню оболонку Riverblade Visual Lint.
  2. Parasoft C + + test (http://www.parasoft.com/). Інший відомий статичний аналізатор загального призначення. Також існує під велику кількість апаратних і програмних платформ. Має вбудовану середу, істотно полегшує роботу і настройку правил аналізу. Як і PC-Lint він розрахований на модель даних LP64.
  3. Viva64 (http://www.viva64.com). На відміну від інших аналізаторів розрахований на модель даних Windows (LLP64). Інтегрується в середовище розробки Visual Studio 2005. Призначений тільки для діагностики проблем, пов’язаних з перенесенням програм на 64-бітові системи, що істотно спрощує його настройку.

Висновок


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


Ресурси



  1. Chandra Shekar. Extend your application”s reach from 32-bit to 64-bit environments. enterprisenetworksandservers.com/monthly/art.php?2670
  2. Converting 32-bit Applications Into 64-bit Applications: Things to Consider. developers.sun.com/sunstudio/articles/ILP32toLP64Issues.html
  3. Andrew Josey. Data Size Neutrality and 64-bit Support. www.unix.org/whitepapers/64bit.html
  4. Harsha S. Adiga. Porting Linux applications to 64-bit systems. www.ibm.com/developerworks/library/l-port64.html
  5. Transitioning C and C++ programs to the 64-bit data model. http://devresource.

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


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

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

    Ваш отзыв

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

    *

    *