>
>
>
64 бита для Си++ программистов: от /Wp6…

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

64 бита для Си++ программистов: от /Wp64 к Viva64

Развитие рынка 64-битных решений поставило новые задачи в области их верификации и тестирования. В статье говорится об одном из таких инструментов - Viva64. Это lint-подобный статический анализатор Си/Си++ кода, предназначенный специально для выявления ошибок, связанных с особенностями 64-битных платформ. Освещены предпосылки для создания данного анализатора и отражена его связь с режимом "Detect 64-Bit Portability Issues" в Си++ компиляторе Visual Studio 2005.

Инструмент Viva64 стал частью продукта PVS-Studio и более отдельно не распространяется. Все возможности поиска специфических ошибок, связанных с разработкой 64-битных приложений, а также переносом кода с 32-битной на 64-битную платформу теперь доступны в рамках анализатора PVS-Studio.

Один из наиболее частых вопросов, который мне задавали разработчики программ на языке Си++, звучал так: "Зачем нужен анализатор Viva64, если в Visual C++ 2005 существует встроенное средство диагностики кода, переносимого на 64-битную архитектуру?". Данная диагностика включается с использованием ключа компилятора /Wp64 и носит название "Detect 64-Bit Portability Issues". Из ответов в форуме и родилась эта статья. В ней кратко рассказывается о том, что послужило предпосылкой к созданию статического анализатора Viva64 и в чем заключается его отличие от ряда других средств верификации и повышения качества кода.

Ключ компилятора /Wp64 (Detect 64-Bit Portability Issues), безусловно, хорошее средство обнаружения ошибок, связанных с переносом приложения на 64-битную систему. Он способен указать на многие места в коде, которые приведут к некорректной работе. Но тут кроется тонкость. Многие широко распространенные конструкции языка Си++ потенциально опасны с точки зрения 64-бит, но компилятор не может выдавать на них предупреждения, так как в большинстве случаев они совершенно корректны. Далее в примерах этот аспект будет раскрыт подробнее. Анализатор Viva64 производит более глубокий и подробный анализ, выявляя потенциально опасный код, и выдает соответствующую диагностику. Он не является аналогом или заменителем /Wp64. Он является его расширением и дополнением!

До выпуска анализатора Viva64 я принимал участие в переносе достаточно большого приложения на 64-битную платформу, и выглядело это приблизительно так. Первая пара дней ушла на то, чтобы просто скомпилировать проект на новой архитектуре. Потом еще неделя на то, чтобы выправить все опасные места (точнее, казалось, что все), которые диагностировались с помощью ключей /W4 и /Wp64. В результате, через полторы недели было получен 64-битный вариант программы. Весь исходный код, за исключением сторонних библиотек, собирался с включенными опциями /W4 и /Wp64 без единого предупреждения. Стоит также заметить, что поскольку тот проект разрабатывался для нескольких платформ, то, например, под Linux он собирался компилятором gcc без предупреждений с ключом -Wall. Наша команда была довольна, и считала, что перенос почти завершен. Приложение делало вид, что работало. Мы приступили к тестированию.

И началось... Ошибки стали обнаруживаться в самых неожиданных местах. В результате, были потрачены более двух месяцев на отладку и исправление всех найденных ошибок. Правки давались большим трудом. Оказалось, что специализированных программ для поиска данного класса ошибок просто нет. Существующие lint-подобные анализаторы кода помогли очень слабо и требовали значительных усилий в настройке.

Вы, наверное, спросите "А юнит-тесты?" Они должны были сузить круг поиска таких ошибок. К сожалению, проект существует много лет, и на ранних этапах использование юнит-тестов не практиковалось. В результате код покрыт ими весьма фрагментарно. Но, к сожалению, недостаток юнит-тестов для нашей ситуации проявился вот в чем. Тесты не покрывали ситуации обработки более 4 гигабайт данных. Это объяснимо, так как ранее такая обработка была просто невозможна. Да и сейчас использование подобных тестов весьма затруднительно. Реализация юнит-тестов для столь больших массивов данных приводит к огромным временным затратам на их выполнение. Но как раз ради больших массивов и был начат перенос на 64-битную платформу.

Ничего не оставалось, кроме как тестировать и тестировать, анализировать код глазами и править. Все это в дальнейшем и привело к идее разработки специализированного статического анализатора, заточенного специально на поиск ошибок, возникающих при переносе Си++ кода на 64-битную платформу. Теперь рассмотрим некоторые ситуации, которые позволяет диагностировать анализатор Viva64.

Пример первый. Мой любимый. Изменение поведения виртуальных функций. А проявиться он может очень просто. Вдруг у Вас на 64-битной платформе перестает работать справочная система в MFC приложении. Вот код, демонстрирующий проблему:

class CWinApp { 
  virtual void WinHelp(DWORD_PTR dwData, UINT nCmd); 
};
class CMyApp : public CWinApp { 
  // Don't called in x64 mode
  virtual void WinHelp(DWORD dwData, UINT nCmd); 
};

Во времена Visual C++ 6 прототип виртуальной функции WinHelp имел в качестве первого аргумента тип DWORD. И вполне логично, что перекрывая эту функцию в то время, Вы тоже использовали тип DWORD. Затем прототип функции в заголовочных файлах Visual C++ изменился, и первый аргумент стал иметь тип DWORD_PTR. На 32-битной платформе все будет продолжать работать по-прежнему. А вот в 64-битном режиме - нет! Это будут просто две разные функции и все. Никто не виноват, а одну ошибку мы уже нашли.

Если у Вас существуют классы со сложной иерархией наследования и виртуальными функциями, то подобные ошибки вполне могут там затаиться. Соответственно, Viva64 находит и диагностирует данный вид ошибок. Компилятор с ключом /Wp64 молчит, так как с точки зрения языка все корректно.

Пример второй. Вечные циклы:

size_t n = bigValue;
for (unsigned i = 0; i != n; ++i) { ... }

Вот пример классического вечного цикла, если переменная bigValue превысит значение UINT_MAX. Компилятор даже с /Wp64 вынужден молчать, так как это широко распространенная операция сравнения двух переменных различной разрядности в битах. Код совершенно корректен при bigValue<=UINT_MAX. Но, создавая 64-битное приложение, мы часто подразумеваем обработку большого количества элементов. Тогда найти и проанализировать такие операции сравнения просто необходимо. Именно это и делает анализатор Viva64. Он помечает все операции сравнения 32-битных типов, и типов, которые на 64-битной платформе становятся 64-битными.

Пример третий. Некорректное явное приведение типа.

Часто ошибки обрезания 64-битных типов до 32-битных значений прячутся за явными приведениями типов. Такие места могут существовать в коде по различным причинам. Здесь компилятор также не имеет права выдавать предупреждения. Одно и то же явное приведение типа можно записать множеством способов:

size_t a;
int b = (int)a;
int b = (int)(a);
int b = int(a);
int b = static_cast<int>(a);

Поиск явных приведений типов может стать весьма трудоемкой задачей. Тем более, что нужно искать не все явные приведения, а только опасные, с точки зрения переноса программы на новую платформу.

Здесь вновь может помочь анализатор Viva64, запущенный в соответствующем режиме поиска.

Пример четвертый. Некорректная индексация массивов:

size_t n = bigValue;
unsigned index = 0;
for (size_t i = 0; i != n; ++i)
  array[index++] = 10;

К сожалению, частой практикой программирования, является использование для индексации массивов типов int и unsigned. Больше никогда так не делайте! Только ptrdiff_t и size_t! Если Вы будете работать с массивами, содержащими более UINT_MAX элементов, как в приведенном ранее примере, то работа алгоритма будет некорректна.

/Wp64, к сожалению, вновь не может помочь. Если компилятор начнет предупреждать об индексации по 32-битному типу, то забракует приличную часть Вашего совершенно корректного кода. Ведь ошибка возникнет только при работе с огромными массивами, которых в вашей программе может и не быть. Но, если у вас такие массивы есть, то найти подобные ошибки весьма сложная задача.

Анализатор Viva64 позволяет Вам просмотреть все обращения в программе к массиву по 32-битным индексам и провести правки, если это необходимо. При этом он достаточно интеллектуален, чтобы не показывать конструкции вида:

enum NUM { ZERO, ONE, TWO };
array[0] = array[ONE];

Хочется предложить Вашему вниманию еще один способ ощутить достоинства Viva64. Представьте, что у Вас есть старый "грязный" код стороннего разработчика. Он компилируется у Вас с отключенными предупреждениями, так как править его нет никакого желания или смысла. Теперь представьте, что Вам необходимо перенести этот код на 64-битную платформу. Оставить выключенными предупреждения, означает получить неработающий 64-битный код. Включить - погрузиться на недели или месяцы в их просмотр. Эта реальная, но весьма печальная ситуация. Используя Viva64, Вы можете просмотреть ТОЛЬКО ТЕ участки кода, которые потенциально опасны в контексте поддержки 64 бит, не отвлекаясь на второстепенные предупреждения. Это может существенно сэкономить Ваше время.

Естественно, это далеко не все, на что способен анализатор. Но я надеюсь, что дал Вам общее представление о нем. Уверен, что он сможет сэкономить кому-то несколько миллионов нервных клеток, позволив выпустить 64-битный продукт вовремя, а не как компания из примера, с опозданием в два месяца.

Анализатор является отличным дополнением к другим средствам верификации и повышения качества приложения. Таким как, например, /Wp64 в Visual Studio, статический анализатор PC-Lint от Gimpel Software или BoundsChecker от Compuware. Все эти и многие другие инструменты могут существенно облегчить жизнь программисту и ускорить разработку программного обеспечения. И надеюсь, что Viva64 внесет в это свой существенный вклад.

Удачи в освоении 64-битных систем!