Як виявити переповнення 32-бітних змінної в довгих циклах в 64-бітної програмі

Одна з проблем, з якою стикаються розробники 64-бітних додатків, це переповнення 32-бітових змінних у дуже довгих циклах. З цим завданням добре справляється аналізатор коду PVS-Studio (набір діагностик Viva64). На тему переповнення змінних в циклах є ряд питань на сайті StackOverflow.com. Але оскільки мої відповіді можуть вважати виключно рекламними, а не як корисну інформацію, я вирішив описати можливості PVS-Studio в статті.

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

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

Отже, проблема. В 64-бітної програмі відбувається переповнення цілочисельних 32-бітних типів. Мова йде про таких типах int, unsigned, long (якщо це Win64). Необхідно якось виявити такі небезпечні місця. Це може зробити аналізатор PVS-Studio, про що ми і поговоримо.

Розглянемо різні варіанти переповнення змінних, пов'язаних з довгими циклами.

Перша ситуація. Описана на сайті StackOverflow тут: "How can elusive 64-bit portability issues be detected?". Є код наступного виду:
int n;
size_t pos, npos;
/* ... initialization ... */
while((pos = find(ch, start)) != npos)
{
/* ... advance start position ... */
n++; // this will overflow if the loop iterates too many times
}

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

В 64-бітної програмі довжина рядка може бути більше INT_MAX і відповідно змінна n може переповнитися. Це призведе до невизначеного поведінки програми. Не треба думати, що переповнення просто перетворить число 2147483647 в -2147483648. Це саме невизначений поведінку і передбачити наслідки неможливо. Для тих, хто не вірить, що переповнення знаковою змінної призводить до несподіваних змін у роботі програми, пропоную познайомитися з моєю статтею "Undefined behavior ближче, ніж ви думаєте".

Отже, потрібно виявити, що змінна n може переповнитися. Немає нічого простіше. Запускаємо PVS-Studio і отримуємо попередження:

V127 An overflow of the 32-bit 'n' variable is possible inside a long cycle which utilizes a memsize-type loop counter. mfcapplication2dlg.cpp 190

Якщо змінити тип змінної n size_t, то помилка, а відповідно і повідомлення аналізатора, зникне.

Там же наводиться ще один приклад коду, який потрібно виявити:
int i = 0;
for (iter = c.begin(); iter != c.end(); iter++, i++)
{
/* ... */
}

Запускаємо PVS-Studio і знову отримуємо попередження V127:

V127 An overflow of the 32-bit 'i' variable is possible inside a long cycle which utilizes a memsize-type loop counter. mfcapplication2dlg.cpp 201

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

Як бачимо, ці помилки можна виявляти за допомогою статичного аналізатора коду PVS-Studio. І це єдиний спосіб впоратися з великим проектом. Так само треба відзначити, що PVS-Studio надає зручний інтерфейс для роботи з великою кількістю діагностичних повідомлень. Ви можете інтерактивно фільтрувати повідомлення, позначати їх як помилкові і так далі. Однак опис можливостей PVS-Studio виходить за рамки цієї статті. Для тих, хто зацікавився інструментом пропоную ознайомитися з наступними матеріалами:Зазначу також, що ми маємо досвід портування великого проекту в 9 млн. рядків коду на 64-бітну платформу. І PVS-Studio відмінно показав себе в роботі над цим проектом.

Перейдемо до наступної теми на сайті StackOverflow: "Can Klocwork (or other tools) be aware of types, typedefs and #define directives?".

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

Це завдання дещо відрізняється від попередньої. Але такі цикли дійсно слід шукати. Адже з допомогою змінної int неможливо обробляти величезні масиви і так далі.

Підійшов чоловік до вирішення завдання неправильно. У цьому він не винен. Він просто не знає про існування PVS-Studio. Зараз ви зрозумієте чому я так кажу.

Отже, він планує шукати:
for (int i = 0; i < 10; i++)
// ...

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

Правити всі цикли підряд, замінюючи int, наприклад intptr_t теж поганий варіант. Це дуже багато роботи і змін у коді.

Аналізатор PVS-Studio може допомогти. Наведений вище цикл він не знайде. Тому, що його не треба шукати. У ньому просто немає місця для помилок. Цикл виконує 10 ітерацій. І ніякого переповнення в ньому бути не може. Так що нічого програмісту витрачати час на цю ділянку коду.

Зате аналізатор вкаже ось на такі цикли:
void Foo(std::vector<float> &v)
{
for (int i = 0; i < v.size(); i++)
v[i] = 1.0;
}

Аналізатор відразу видасть 2 попередження. Перше попереджає про те, що у виразі 32-бітний тип порівнюється з memsize-типом:

V104 Implicit conversion of 'i' to memsize type in an arithmetic expression: i < v.size() mfcapplication2dlg.cpp 210

І дійсно, тип змінної i не підходить для організації довгих циклів.

Друге попередження каже, що дивно в якості індексу використовувати 32-бітну змінну. Якщо масив великий, то код є помилковим.

V108 Incorrect index type: v[not a memsize-type]. Use memsize type instead. mfcapplication2dlg.cpp 211

Коректний код повинен виглядати так:
void Foo(std::vector<float> &v)
{
for (std::vector<float>::size_type i = 0; i < v.size(); i++)
v[i] = 1.0;
}

Код став довгим і некрасивим, тому з'являється спокуса використовувати ключове слово auto, але цього робити не можна — змінений таким чином код знову некоректний:
for (auto i = 0; i < v.size(); i++)
v[i] = 1.0;

Так як константа 0 має тип int, то й мінлива i буде мати тип int. І ми повернулися до того, з чого почали. До речі, раз зайшла мова про нові можливості стандарту мови С++, пропоную поглянути на статтю "C++11 і 64-бітові помилки".

Думаю, можна піти на компроміс і написати не ідеальний, але правильний код:
for (size_t i = 0; i < v.size(); i++)
v[i] = 1.0;

ПриміткаЗвичайно, ще більш правильним буде використовувати ітератори або алгоритм fill(). Але ми говоримо про пошуках переповнення 32-бітових змінних в старих програмах. Тому я і не розглядаю такі варіанти виправлення коду. Це вже зовсім інша тема.

Хочу підкреслити, що аналізатор досить розумний і намагається даремно не турбувати програміста. Наприклад, він не буде видавати попередження, якщо побачить, що обробляється маленький масив:
void Foo(int n)
{
float A[100];
for (int i = 0; i < n; i++)
A[i] = 1.0;
}

Висновок

Аналізатор PVS-Studio є лідером з пошуку 64-бітових помилок. Спочатку, він якраз і створювався для допомоги програмістам в їх портування програм на 64-бітні системи. В той час він ще називався Viva64. Це вже потім, він перетворився в аналізатор загального призначення, але існували 64-бітні діагностики нікуди не зникли і всі також готові вам допомогти.

Завантажити демонстраційну версію можна тут.

Детальніше розробки 64-бітних програм.

Джерело: Хабрахабр
  • avatar
  • 0

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.