Урок 7. Проблемы выявления 64-битных ошибок

23.01.2012

Существуют различные подходы к выявлению ошибок в программном коде. Рассмотрим основные методологии и их эффективность в выявлении 64-битных ошибок.

Обзор кода

Самым старым, проверенным и надежным подходом к поиску дефектов является совместный обзор кода (англ. code review). Эта методика основана на совместном чтении кода с выполнением ряда правил и рекомендаций, хорошо описанных в книге Стива Макконнелла "Совершенный код" (Steve McConnell, "Code Complete"). К сожалению, эта практика неприменима для крупномасштабной проверки современных программных систем в силу их большого объема.

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

Статический анализ кода

На помощь разработчикам, которые осознают необходимость регулярного просмотра кода, но не имеют достаточного количества времени, приходят средства статического анализа кода. Их основной задачей является сокращение объема кода, требующего внимания человека, и тем самым сокращение времени его просмотра. К статическим анализаторам кода относится достаточно большой класс программ, реализованных для различных языков программирования и имеющих разнообразный набор функций, от простейшего контроля выравнивания кода, до сложного анализа потенциально опасных мест. Преимуществом статического анализа является его хорошая масштабируемость. С его помощью можно в разумные сроки проанализировать проект любого объема. А систематическое использование анализаторов позволяет выявлять многие ошибки еще на этапе написания кода.

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

Метод белого ящика

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

Читателю должно быть очевидно, что полная отладка приложения для выявления 64-битных ошибок также нереальна, как и полный обзор программного кода.

Дополнительно стоит заметить, что при отладке 64-битных приложений, обрабатывающих большие массивы данных, способ пошаговой отладки может стать неприменимым. Отладка таких приложений может занимать намного больше времени. Стоит заранее обдумать возможность использования систем протоколирования ("логирования") для отладки приложений или предусмотреть иные методы.

Метод черного ящика (юнит-тесты)

Намного лучше себя зарекомендовал метод черного ящика. К этому типу тестирования можно отнести юнит-тестирование (unit tests). Основная идея метода заключается в написании набора тестов для отдельных модулей и функций, проверяющего все основные режимы их работы. Ряд источников относят юнит-тестирование к методу белого ящика, поскольку оно основывается на знании устройства программы. Мы придерживаемся позиции, что тестируемые функции и модули следует рассматривать как черные ящики, так как юнит-тесты не должны учитывать внутреннее устройство функции. Обоснованием этому может служить такая методология, когда тесты разрабатываются до начала написания самих функций, что способствует повышению контроля их функциональности с точки зрения спецификации.

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

C использованием юнит-тестов для поиска 64-битных ошибок связан ряд неприятных моментов. Стремясь сократить время выполнения тестов, при их разработке стараются использовать небольшой объем вычислений и объем обрабатываемых данных. Например, разрабатывая тест на функцию поиска элемента в массиве, не имеет большого значения, будет она обрабатывать 100 элементов или 10 000 000. Сотни элементов будет достаточно, а вот по сравнению с обработкой 10 000 000 элементов скорость выполнения теста может быть существенно выше. Но если вы хотите разработать полноценные тесты, чтобы проверить эту функцию на 64-битной системе, вам потребуется обработать более 4 миллиардов элементов! Вам кажется, что если функция работает на 100 элементах, она будет работать и на миллиардах? Нет. Приведем пример.

bool FooFind(char *Array, char Value,
             size_t Size)
{
  for (unsigned i = 0; i != Size; ++i)
    if (i % 5 == 0 && Array[i] == Value)
      return true;
  return false;
}
#ifdef _WIN64
  const size_t BufSize = 5368709120ui64;
#else
  const size_t BufSize = 5242880;
#endif
int _tmain(int, _TCHAR *) {
  char *Array =
    (char *)calloc(BufSize, sizeof(char));
  if (Array == NULL)
    std::cout << "Error allocate memory" << std::endl;
  if (FooFind(Array, 33, BufSize))
    std::cout << "Find" << std::endl;
  free(Array);
}

Ошибка кроется в использовании типа unsigned для счетчика. В результате на 64-битной системе при обработке большого массива происходит переполнение счетчика и возникает вечный цикл.

Примечание. Есть вероятность, что при определенных настройках компилятора данный пример не продемонстрирует ошибку. Чтобы понять это странное событие, предлагаем ознакомиться со статьей "64-битный конь, который умеет считать".

Как видно из примера, если ваша программа на 64-битной системе начнет обрабатывать больший объем данных, то не стоит рассчитывать на старые наборы юнит-тестов. Следует их обязательно расширить с учетом обработки больших объемов данных.

К сожалению, написать новые тесты недостаточно. Здесь мы сталкиваемся с проблемой скорости выполнения модифицированного набора тестов, охватывающего обработку больших объемов данных. Первым следствием станет то, что такие тесты нельзя будет добавить в набор тестов, запускаемых разработчиком в ходе разработки. С внесением их в ночные тесты тоже могут возникнуть сложности. Суммарное время выполнения всех тестов может вырасти на порядок или два, а то и более. В результате время выполнения теста может превысить 24 часа. Следует помнить об этом и подойти к доработке тестов для 64-битной версии программы со всей серьезностью.

Ручное тестирование

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

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

Авторы курса: Андрей Карпов (karpov@viva64.com), Евгений Рыжков (evg@viva64.com).

Правообладателем курса "Уроки разработки 64-битных приложений на языке Си/Си++" является ООО "Системы программной верификации". Компания занимается разработкой программного обеспечения в области анализа исходного кода программ. Сайт компании: http://www.viva64.com.

Контактная информация: e-mail: support@viva64.com, 300027, г. Тула, а/я 1800.