|
|
|||
![]() PVS-Studio, статический анализатор кода для 64-битного и параллельного программирования на Си/Си++
|
|||
![]() ![]() ![]() ![]() ![]()
02.09.2010
Щупаем новый Intel Parallel Studio XE 2011 beta Вот, наконец, добрался попробовать Си++ компилятор, входящий в состав Intel Parallel Studio XE 2011 beta.»
30.08.2010
Пять дней на исправление ошибки в два символа, или миф о всемогущих технологиях при разработке программ В этом блоге нередко можно почитать о том, как тот или иной программный инструмент, или технология разработки программ помогает делать меньше ошибок, быстрее их находить, легче исправлять.»
30.08.2010
Д'Артаньян и интернет, или работа над проблемой битых ссылок Господа, хватит уже рассматривать ссылки исключительно в контексте их количества, купли/продажи и считать PR сайта, где они расположены.» ![]()
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-битные уроки![]() Урок 13. Паттерн 5. Адресная арифметикаПримечание. Мы специально выбрали номер "тринадцать" для этого урока, поскольку ошибки, связанные с адресной арифметикой в 64-битных системах, являются наиболее коварными. Надеемся, число 13 заставит вас быть внимательнее. Суть паттерна: чтобы избежать ошибок в 64-битном коде используйте для адресной арифметики только memsize-типы. Рассмотрим код: unsigned short a16, b16, c16; char *pointer; ... pointer += a16 * b16 * c16; Данный пример корректно работает с указателями, если значение выражения "a16 * b16 * c16" не превышает INT_MAX (2147483647). Такой код мог всегда корректно работать на 32-битной платформе. В рамках 32-битной архитектуры программе недоступен объем памяти для создания массива подобного размеров. На 64-битной архитектуре это ограничение снято, и размер массива легко может превысить INT_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\n", *ptr); //Access violation on 64-bit platform
Давайте проследим, как происходит вычисление выражения "ptr + (A + B)":
Что из этого выйдет, будет зависеть от размера указателя на данной архитектуре. Если сложение будет происходить в 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); Да, такой код будет работать, но он плох по ряду причин:
Индексация массивовДанная разновидность ошибок выделена для лучшей структуризации изложения, так как индексация в массивах с использованием квадратных скобок - это всего лишь иная запись адресной арифметики, рассмотренной выше. В программах, обрабатывающих большие объемы данных, могут встретиться ошибки связанные с индексацией больших массивов, или возникнуть вечные циклы. Следующий пример содержит сразу 2 ошибки: const size_t size = ...;
char *array = ...;
char *end = array + size;
for (unsigned i = 0; i != size; ++i)
{
const int one = 1;
end[-i - one] = 0;
}
Первая ошибка заключается в том, что если размер обрабатываемых данных превысит 4 гигабайта (0xFFFFFFFF), то возможно возникновение вечного цикла, поскольку переменная 'i' имеет тип 'unsigned' и никогда не достигнет значения больше чем 0xFFFFFFFF. Мы специально пишем, что возникновение возможно, но не обязательно оно произойдет. Это зависит от того, какой код построит компилятор. Например, в отладочном (debug) режиме вечный цикл будет присутствовать, а в release-коде зацикливание исчезнет, так компилятор примет решение оптимизировать код, используя для счетчика 64-битный регистр, и цикл будет корректным. Все это добавляет путаницы, и код, который работал вчера, неожиданно может перестать работать на следующий день. Вторая ошибка связана с проходом по массиву от конца к началу, для чего используются отрицательные значения индексов. Приведенный код работоспособен в 32-битном режиме, но при его запуске на 64-битной машине на первой же итерации цикла произойдет доступ за границы массива, и программа аварийно завершится. Рассмотрим причину такого поведения. Хотя то, что написано ниже, аналогично примеру с "ptr = ptr + (A + B);", это повторение делается сознательно. Важно показать, что опасность скрывается даже в простых конструкциях и может выглядеть по-разному. Согласно правилам языка Си++ на 32-битной системе выражение "-i - one" будет вычисляться следующим образом (на первом шаге i = 0):
На 32-битной системе обращение к массиву по индексу 0xFFFFFFFFu эквивалентно использованию индекса -1. То есть end[0xFFFFFFFFu] является аналогом end[-1]. В результате происходит корректная обработка элемента массива. В 64-битной системе в последнем пункте картина будет иной. Произойдет расширение типа unsigned до знакового ptrdiff_t и индекс массива будет равен 0x00000000FFFFFFFFi64. В результате произойдет выход за рамки массива. Для исправления кода необходимо использовать такие типы, как ptrdiff_t и size_t. Чтобы окончательно убедить вас в необходимости использования только 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];
}
ДиагностикаОшибки адресной арифметики хорошо диагностируются инструментом PVS-Studio. Анализатор предупреждает о потенциально опасных выражениях с помощью диагностических сообщений V102 и V108. По возможности анализатор пытается понять, когда использование не memsize-типа в адресной арифметике безопасно и не выдавать в этом месте предупреждения. В результате, поведение анализатора иногда может показаться странным. В таких случаях мы просим не спешить и постараться разобраться. Рассмотрим следующий код: char Arr[] = { '0', '1', '2', '3', '4' };
char *p = Arr + 2;
cout << p[0u + 1] << endl;
cout << p[0u - 1] << endl; //V108
Данный код исправно работает в 32-битном режиме и печатает на экране числа 3 и 1. При проверке этого кода мы получим предупреждение только на одну строку с выражением "p[0u - 1]". И это совершенно верно! Если вы скомпилируете и запустите данный пример в 64-битном режиме, то увидите, как на экране будет распечатано значение 3, после чего произойдет аварийное завершение программы. Если вы уверены в корректности индексации, то вы можете изменить соответствующую настройку анализатора на вкладке настроек Settings: General или использовать фильтры. Также можно использовать явное приведение типов. Авторы курса: Андрей Карпов (karpov@viva64.com), Евгений Рыжков (evg@viva64.com). Правообладателем курса "Уроки разработки 64-битных приложений на языке Си/Си++" является ООО "Системы программной верификации". Компания занимается разработкой программного обеспечения в области анализа исходного кода программ. Сайт компании: http://www.viva64.com. Контактная информация: e-mail: support@viva64.com, 300027, г. Тула, а/я 1800. | ||