|
|
|||
![]() PVS-Studio, статический анализатор кода для 64-битного и параллельного программирования на Си/Си++
|
|||
![]() ![]() ![]() ![]() ![]()
28.06.2010
Почему в PVS-Studio в окне Error List иногда не показывается номер строки, в которой обнаружена проблема? Иногда анализатор кода PVS-Studio якобы обнаруживает проблему в коде, выдает сообщение, указывает имя файла, но не показывает номер проблемной строки как показано на рисунке.»
07.06.2010
Общение разработчиков с пользователями программ При разработке программных продуктов разработчикам очень важно иметь обратную связь с пользователями их программ.» ![]()
22.07.2010
Использование PVS-Studio вместе с системами continuous integration В статье показаны приемы организации работы анализатора кода PVS-Studio вместе с системами непрерывной интеграции (continuous integration).»
06.07.2010
Сравнение возможностей PVS-Studio и Visual Studio 2010 по выявлению дефектов в 64-битных программах В статье сравниваются три механизма анализа кода с точки зрения выявления 64-битных ошибок: компилятор Visual C++2010, компонент Code Analysis for C/C++ входящий в состав Visual Studio 2010 Premium/Ultimate и анализатор Viva64 входящий в состав PVS-Studio 3.60. »
29.06.2010
Коллекция примеров 64-битных ошибок в реальных программах
Статья представляет собой наиболее полную коллекцию примеров 64-битных ошибок на языках Си и Си++. » ![]() |
64-битные уроки![]() Урок 17. Паттерн 9. Смешанная арифметикаНадеемся, вы уже успели отдохнуть от 13 урока и теперь сможете рассмотреть еще один важный паттерн ошибок, связанный с арифметическими выражениями, в которых участвуют типы различной размерности. Смешанное использование 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" никогда не выполнится, что и приводит к бесконечному циклу. Примечание. Заметим, что при определенных настройках компилятора данный пример может успешно отработать. Из-за этого иногда, кажется, что код корректен, и возникает путаница. В одном из последующих уроков мы расскажем о фантомных ошибках, проявляющих себя только временами. Если вам интересно уже сейчас понять это странное поведение кода, то предлагаем ознакомиться со статьей "64-битный конь, который умеет считать". Для исправления кода необходимо использовать в выражениях только memsize-типы. В данном примере можно заменить тип переменной Index с unsigned на size_t. Другая часто встречающаяся ошибка - запись выражений следующего вида: int x, y, z; ptrdiff_t SizeValue = x * y * z; Ранее уже рассматривались подобные примеры, когда при вычислении значений с использованием не memsize-типов происходило арифметическое переполнение. И конечный результат был некорректен. Поиск и исправление приведенного кода осложняется тем, что компиляторы, как правило, не выдают на него никаких предупреждений. С точки зрения языка Си++ это совершенно корректная конструкция. Происходит умножение нескольких переменных типа int, после чего результат неявно расширяется до типа ptrdiff_t и происходит присваивание. Приведем небольшой код, показывающий опасность неаккуратных выражений со смешанными типами (результаты получены с использованием Microsoft Visual C++ 2005, 64-битный режим компиляции): int x = 100000; int y = 100000; int z = 100000; ptrdiff_t size = 1; // Result: ptrdiff_t v1 = x * y * z; // -1530494976 ptrdiff_t v2 = ptrdiff_t (x) * y * z; // 1000000000000000 ptrdiff_t v3 = x * y * ptrdiff_t (z); // 141006540800000 ptrdiff_t v4 = size * x * y * z; // 1000000000000000 ptrdiff_t v5 = x * y * z * size; // -1530494976 ptrdiff_t v6 = size * (x * y * z); // -1530494976 ptrdiff_t v7 = size * (x * y) * z; // 141006540800000 ptrdiff_t v8 = ((size * x) * y) * z; // 1000000000000000 ptrdiff_t v9 = size * (x * (y * z)); // -1530494976 Необходимо чтобы все операнды в подобных выражениях были приведены в процессе вычисления к типу большей разрядности. Помните, что выражение вида ptrdiff_t v2 = ptrdiff_t (x) + y * z; вовсе не гарантирует правильный результат. Оно гарантирует только то, что выражение " ptrdiff_t (x) + y * z" будет иметь тип ptrdiff_t. Следовательно, если результатом выражения должен являться memsize-тип, то в выражении должны участвовать только memsize-типы. Или элементы, приведенные к memsize-типам. Правильный вариант: ptrdiff_t v2 = ptrdiff_t (x) + ptrdiff_t (y) * ptrdiff_t (z); // OK! Впрочем не всегда необходимо приводить все аргументы к memsize-типу. Если выражение состоит из одинаковых операторов, то достаточно привести к memsize-типу только первый аргумент. Рассмотрим пример: int c(); int d(); int a, b; ptrdiff_t v2 = ptrdiff_t (a) * b * c() * d(); Порядок вычисления выражения с операторами одинакового приоритета не определен. Точнее, компилятор волен вычислять подвыражения, например вызов функций c() и d() в том порядке, который он считает более эффективным, даже если подвыражения вызывают побочные эффекты. Порядок возникновения побочных эффектов не определен. Но поскольку операция умножения относится к лево-ассоциативным операторам, то вычисление будет происходить следующим образом: ptrdiff_t v2 = ((ptrdiff_t (a) * b) * c()) * d(); В результате каждый из операндов перед умножением будет преобразовываться к типу ptrdiff_t и мы получим корректный результат. Примечание. Если у вас есть целочисленные вычисления, для которых крайне важен контроль над переполнениями, то мы предлагаем обратить внимание на класс 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\n");
else
printf ("val_1 is not greater than val_2\n");
//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". На рисунке 1 и 2 схематично отображены происходящие преобразования. Рисунок 1 - Преобразования, происходящие в 32-битном коде Рисунок 2 - Преобразования, происходящие в 64-битном коде Если вам необходимо вернуть прежнее поведение кода - следует изменить тип переменной 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\n");
else
printf ("val_1 is not greater than val_2\n");
Правильнее вообще не сравнивать знаковые и беззнаковые типы, но это выходит за рамки обсуждаемой темы. Мы рассмотрели только простые выражения. Но описываемые проблемы могут проявиться и при использовании других конструкций языка Си++: 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);
}
Или чуть более просто: extern int Width, Height, Depth;
size_t GetIndex(int x, int y, int z) {
return (size_t)(x) +
(size_t)(y) * Width +
(size_t)(z) * Widt) * Height;
}
В следующем примере, у нас вновь смешивается memsize-тип (указатель) и 32-битный тип 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); Этот код не напоминает вам пример с некорректной арифметикой указателей, рассмотренный в 13-том уроке? Да, здесь происходит то же самое. Некорректный результат возникает при неявном расширении фактического аргумента, имеющего значение 0xFFFFFFFF и тип unsigned, до типа ptrdiff_t. ДиагностикаОшибки, возникающие на 64-битных системах при смешанном использование простых целочисленных типов и memsize-типов, представлены большим количеством синтаксических конструкций языка Си++. Для диагностики этих ошибок используется целый ряд диагностических сообщений. Анализатор PVS-Studio предупреждает о потенциально возможных ошибках, используя следующие сообщения: V101, V103, V104, V105, V106, V107, V109, V110, V121. Вернемся к ранее рассмотренному примеру: int c(); int d(); int a, b; ptrdiff_t x = ptrdiff_t(a) * b * c() * d(); Хотя само выражение перемножает аргументы, расширяя их тип до ptrdiff_t, ошибка может содержаться в вычислении самих этих аргументов. Поэтому анализатор все равно предупреждает о смешивании типов: "V104: Implicit type conversion to memsize type in an arithmetic expression". Также инструмент PVS-Studio позволяет найти потенциально опасные выражения, которые скрываются за явным приведением типов. Для этого можно включить в настройках анализатора предупреждения V201 и V202. По умолчанию анализатор не выдает предупреждения связанные с приведением типа, в случае, когда приведение осуществляется явно. Пример: TCHAR *begin, *end; unsigned size = static_cast<unsigned>(end - begin); Выявить подобный некорректный код и позволяют сообщения V201 и V202. При этом анализатор не обратит внимания на безопасные с точки зрения 64-битного кода приведения типов: const int *constPtr; int *ptr = const_cast<int>(constPtr); float f = float(constPtr[0]); char ch = static_cast<char>(sizeof(double)); Авторы курса: Андрей Карпов (karpov@viva64.com), Евгений Рыжков (evg@viva64.com). Правообладателем курса "Уроки разработки 64-битных приложений на языке Си/Си++" является ООО "Системы программной верификации". Компания занимается разработкой программного обеспечения в области анализа исходного кода программ. Сайт компании: http://www.viva64.com. Контактная информация: e-mail: support@viva64.com, 300027, г. Тула, а/я 1800. | ||