>
>
>
Как обнаружить переполнение 32-битных п…

Андрей Карпов
Статей: 673

Как обнаружить переполнение 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, о чем мы и поговорим.

Рассмотрим различные варианта переполнения переменных, связанных с длинными циклами.

Первая ситуация. Описана на сайте Stack Overflow здесь: "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

В теме на Stack Overflow также поднимается вопрос, что делать если кодовая база огромна и как найти все подобные ошибки.

Как видим, эти ошибки можно обнаруживать с помощью статического анализатора кода PVS-Studio. И это единственный способ совладать с большим проектом. Так же надо отметить, что PVS-Studio предоставляет удобный интерфейс для работы с большим количеством диагностических сообщений. Вы можете интерактивно фильтровать сообщения, помечать их как ложные и так далее. Однако описание возможностей PVS-Studio выходит за рамки этой заметки. Для тех, кто заинтересовался инструментом предлагаю познакомиться со следующим материалами:

Отмечу также, что мы имеем опыт портирования большого проекта в 9 млн. строк кода на 64-битную платформу. И PVS-Studio отлично показал себя в работе над этим проектом.

Перейдем к следующей теме на сайте Stack Overflow: "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-битных программ.