![]() PVS-Studio, статический анализатор кода для 64-битного и параллельного программирования на Си/Си++
|
|||||||||||||
![]() ![]() ![]() ![]() ![]()
11.03.2010
Параллельные заметки N4 - продолжаем знакомиться с конструкциями OpenMP Продолжим знакомство с технологией OpenMP и рассмотрим некоторые функции и новые директивы.»
02.03.2010
Параллельные заметки №3 - базовые конструкции OpenMP Мы начинаем знакомить вас непосредственно с использованием технологии OpenMP и рассмотрим в этой заметке некоторые базовые конструкции.»
28.02.2010
Как стандарт C++0x поможет в борьбе с 64-битными ошибками Программисты видят в стандарте C++0x возможность использовать лямбда-функции и прочие мало понятные для меня сущности :).» ![]()
10.12.2009
Вопросы и ответы по PVS-Studio (PVS-Studio FAQ) В документе собраны некоторые вопросы и ответы по анализатору кода PVS-Studio компании ООО "СиПроВер".»
09.12.2009
Вопросы и ответы по библиотеке VivaCore (VivaCore FAQ) В документе собраны некоторые вопросы и ответы по библиотеке анализа Си/Си++ кода VivaCore компании ООО "СиПроВер".»
23.11.2009
PVS-Studio: использование функции "Mark as False Alarm"
В статье приведены описание и пример использования новой функции PVS-Studio 3.40 "Mark as False Alarm" ("Пометить как ложное срабатывание").» ![]() |
64-битные уроки![]() Урок 13. Паттерн 5. Адресная арифметика
Номер тринадцатого урока для рассказа об ошибках связанных с адресной арифметикой выбран специально. Ошибки, связанные с арифметикой над указателями в 64-битных системах являются наиболее коварными и хорошо, если число 13 заставит быть внимательнее. Суть паттерна: чтобы избежать ошибок в 64-битном коде используйте для адресной арифметики только memsize-типы. Рассмотрим код:
Данный пример корректно работает с указателями, если значение выражения "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-типы или явное приведение типов в выражениях, где присутствуют указатели. Используя явное приведение типов, мы можем переписать код следующим образом:
Если вы думаете, что злоключения ждут неаккуратные программы только на больших объемах данных, то мы вынуждены вас огорчить. Рассмотрим интересный код для работы с массивом, содержащим всего 5 элементов. Этот пример работоспособен в 32-битном варианте и не работоспособен в 64-битном:
Давайте проследим, как происходит вычисление выражения "ptr + (A + B)":
Что из этого выйдет, будет зависеть от размера указателя на данной архитектуре. Если сложение будет происходить в 32-битной программе, то данное выражение будет эквивалентно "ptr - 1", и мы успешно распечатаем число "3". В 64-битной программе к указателю честным образом прибавится значение 0xFFFFFFFFu, в результате чего указатель окажется далеко за пределами массива. И при доступе к элементу по данному указателю нас ждут неприятности. Для предотвращения показанной ситуации, как и в первом случае, рекомендуем использовать в арифметике с указателями только memsize-типы. Два варианта исправления кода:
Вы можете возразить и предложить следующий вариант исправления:
Да, такой код будет работать, но он плох по ряду причин:
Индексация массивовДанная разновидность ошибок выделена для лучшей структуризации изложения, так как индексация в массивах с использованием квадратных скобок - это всего лишь иная запись адресной арифметики, рассмотренной выше. В программах обрабатывающих большие объемы данных могут встретиться ошибки связанные с индексацией больших массивов или возникнуть вечные циклы. Следующий пример содержит сразу 2 ошибки:
Первая ошибка заключается в том, что если размер обрабатываемых данных превысит 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-типов для индексации и в выражениях адресной арифметики, приведем последний пример.
Данный код взят из реальной программы математического моделирования, в которой важным ресурсом является объем оперативной памяти, и возможность на 64-битной архитектуре использовать более 4 гигабайт памяти существенно увеличивает вычислительные возможности. В программах данного класса для экономии памяти часто используют одномерные массивы, осуществляя работу с ними как с трехмерными массивами. Для этого существуют функции, аналогичные GetCell, обеспечивающие доступ к необходимым элементам. Но приведенный код будет корректно работать только с массивами, содержащими менее INT_MAX элементов. Причина - использование 32-битных типов int для вычисления индекса элемента. Программисты часто допускают ошибку, пытаясь исправить код следующим образом:
Они знают, что по правилам языка Си++ выражение для вычисления индекса будет иметь тип ptrdiff_t и надеются за счет этого избежать переполнения. Но переполнение может произойти внутри подвыражения "y * Width" или "z * Width * Height", так как для их вычисления по-прежнему используется тип int. Если вы хотите исправить код, не изменяя типов переменных, участвующих в выражении, то вы можете явно привести каждую переменную к memsize-типу:
Другое, более верное решение - изменить типы переменных на memsize-тип:
ДиагностикаОшибки адресной арифметики хорошо диагностируются инструментом PVS-Studio. Анализатор предупреждает об потенциально опасных выражениях с помощью диагностических сообщений V102 и V108. По возможности анализатор пытается понять, когда использование не memsize-типа в адресной арифметике безопасно и не выдавать в этом месте предупреждения. В результате поведение анализатора иногда может показаться странным. В таких случаях мы просим не спешить и постараться разобраться. Рассмотрим следующий код:
Данный код исправно работает в 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. | ||||||||||||