Применение статического анализа при разработке программ. Анализ унаследованного кода, когда исходный код утрачен: делать или не делать? Методы статического анализа

У каждого из команды ][ - свои предпочтения по части софта и утилит для
пентеста. Посовещавшись, мы выяснили: выбор так разнится, что можно составить
настоящий джентльменский набор из проверенных программ. На том и решили. Чтобы
не делать сборную солянку, весь список разбит на темы. Сегодня мы разберем
статические анализаторы кода
для поиска уязвимостей в приложениях, когда на
руках – их исходники.

Наличие исходных кодов программы существенно упрощает поиск уязвимостей.
Вместо того чтобы вслепую манипулировать различными параметрами, которые
передаются приложению, куда проще посмотреть в сорцах, каким образом она их
обрабатывает. Скажем, если данные от пользователя передаются без проверок и
преобразований, доходят до SQL-запроса – имеем уязвимость типа SQL injection.
Если они добираются до вывода в HTML-код – получаем классический XSS. От
статического сканера требуется четко обнаруживать такие ситуации, но, к
сожалению, выполнить это не всегда так просто как кажется.

Современные компиляторы

Может показаться забавным, но одними из самых эффективных анализаторов
кода
являются сами компиляторы. Конечно, предназначены они совсем для
другого, но в качестве бонуса каждый из них предлагает неплохой верификатор
исходников, способный обнаружить большое количество ошибок. Почему же он не
спасает? Изначально настройки такой верификации кода выставлены достаточно
лояльно: в результате, чтобы не смущать программиста, компилятор начинает
ругаться только в случае самых серьезных косяков. А вот и зря - если поставить
уровень предупреждений повыше, вполне реально откопать немало сомнительных мест
в коде. Выглядит это примерно следующим образом. Скажем, в коде есть отсутствие
проверки на длину строки перед копированием ее в буфер. Сканер находит функцию,
копирующую строку (или ее фрагмент) в буфер фиксированного размера без
предварительной проверки ее длины. Он прослеживает траекторию передачи
аргументов: от входных данных до уязвимой функции и смотрит: возможно ли
подобрать такую длину строки, которая бы вызывала переполнение в уязвимой
функции и не отсекалась бы предшествующими ей проверками. В случае если такой
проверки нет, находим практически 100% переполнение буфера. Главная сложность в
использовании для проверки компилятора - заставить его "проглотить" чужой код.
Если ты хоть раз пытался скомпилировать приложение из исходников, то знаешь,
насколько сложно удовлетворить все зависимости, особенно в больших проектах. Но
результат стоит того! Тем более, помимо компилятора в мощные IDE встроены и
некоторые другие средства для анализа кода . К примеру, на следующий
участок кода в Visual Studio будет выдано предупреждение об использовании в
цикле функции _alloca, что может быстро переполнить стек:

char *b;
do {
b = (char*)_alloca(9)
} while(1)

В этом заслуга статического анализатора PREfast. Подобно FxCop,
предназначенной для анализа управляемого кода, PREfast изначально
распространялся в виде отдельной утилиты и лишь позже стал частью Visual Studio.

RATS - Rough Auditing Tool for Security

Сайт: www.securesoftware.com
Лицензия: GNU GPL
Платформа: Unix, Windows
Языки: С++, PHP, Python, Ruby

Ошибка ошибке - рознь. Часть тех огрех, которые допускают программисты,
некритична и грозит только нестабильностью программы. Другие, напротив,
позволяют инжектировать шелл-код и выполнять произвольные команды на удаленном
сервере. Особый риск в коде представляют команды, позволяющие выполнить buffer
overflow и другие похожие типы атак. Таких команд очень много, в случае с C/C++
это функции для работы со строками (xstrcpy(), strcat(), gets(), sprintf(),
printf(), snprintf(), syslog()), системные команды (access(), chown(), chgrp(),
chmod(), tmpfile(), tmpnam(), tempnam(), mktemp()), а также команды системных
вызовов (exec(), system(), popen()). Вручную исследовать весь код (особенно,
если он состоит из нескольких тысяч строк) довольно утомительно. А значит, можно
без труда проглядеть передачу какой-нибудь функции непроверенных параметров.
Значительно облегчить задачу могут специальные средства для аудита, в том числе,
известная утилита RATS (Rough Auditing Tool for Security ) от
известной компании Fortify. Она не только успешно справится с обработкой кода,
написанного на C/C++, но сможет обработать еще и скрипты на Perl, PHP и Python.
В базе утилиты находится внушающая подборка с детальным описанием проблемных
мест в коде. С помощью анализатора она обработает скормленный ей сорец и
попытается выявить баги, после чего выдаст информацию о найденных недочетах.
RATS
работает через командную строку, как под Windows, так и *nix-системами.

Yasca

Сайт: www.yasca.org
Лицензия: Open Source
Платформа: Unix, Windows
Языки: С++, Java, .NET, ASP, Perl, PHP, Python и другие.

Yasca так же, как и RATS не нуждается в установке, при этом имеет не
только консольный интерфейс, но и простенький GUI. Разработчики рекомендуют
запускать утилиту через консоль - мол, так возможностей больше. Забавно, что
движок Yasca написан на PHP 5.2.5, причем интерпретатор (в самом урезанном
варианте) лежит в одной из подпапок архива с программой. Вся программа логически
состоит из фронт-енда, набора сканирующих плагинов, генератора отчета и
собственно движка, который заставляет все шестеренки вращаться вместе. Плагины
свалены в директорию plugins - туда же нужно устанавливать и дополнительные
аддоны. Важный момент! Трое из стандартных плагинов, которые входят в состав
Yasca
, имеют неприятные зависимости. JLint, который сканирует Java"овские
.class-файлы, требует наличия jlint.exe в директории resource/utility. Второй
плагин - antiC, используемый для анализа сорцов Java и C/C++, требует antic.exe
в той же директории. А для работы PMD, который обрабатывает Java-код, необходима
установленная в системе Java JRE 1.4 или выше. Проверить правильность установки
можно, набрав команду "yasca ./resources/test/". Как выглядит сканирование?
Обработав скормленные программе сорцы, Yasca выдает результат в виде
специального отчета. Например, один из стандартных плагинов GREP позволяет с
помощью паттернов, описанных в.grep файлах, указать уязвимые конструкции и
легко выявлять целый ряд уязвимостей. Набор таких паттернов уже включен в
программу: для поиска слабого шифрования, авторизации по "пароль равен логину",
возможные SQL-инъекции и много чего еще. Когда же в отчете захочется увидеть
более детальную информации, не поленись установить дополнительные плагины. Чего
стоит одно то, что с их помощью можно дополнительно просканировать код на на
.NET (VB.NET, C#, ASP.NET), PHP, ColdFusion, COBOL, HTML, JavaScript, CSS,
Visual Basic, ASP, Python, Perl.

Cppcheck

Сайт:
Лицензия: Open Source
Платформа: Unix, Windows
Языки: С++

Разработчики Cppcheck решили не разбрасываться по мелочам, а потому
отлавливают только строго определенные категории багов и только в коде на С++.
Не жди, что программа продублирует предупреждения компилятора - он обойдется без
суфлера. Поэтому не поленись поставить для компилятора максимальный уровень
предупреждений, а с помощью Cppcheck проверь наличие утечек памяти, нарушений
операций allocation-deallocation, различных переполнений буфера, использования
устаревших функций и многого другого. Важная деталь: разработчики Cppcheck
постарались свести количество ложных срабатываний к минимуму. Поэтому, если
прога фиксирует ошибку, можно с большой вероятностью сказать: "Она действительно
есть!" Запустить анализ можно как из-под консоли, так и с помощью приятного
GUI-интерфейса, написанного на Qt и работающего под любой платформой.

graudit

Сайт:
www.justanotherhacker.com/projects/graudit.html
Лицензия: Open Source
Платформа: Unix, Windows
Языки: C++, PHP, Python, Perl

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

graudit /path/to/scan

Наградой за старание будет цветастый отчет о потенциально эксплуатируемых
местах в коде. Надо сказать, что, помимо самого скрипта (а это всего 100 строчек
кода на Bash), ценность представляют сигнатурные базы, в которых собраны
регекспы и названия потенциально уязвимых функций в разных языках. По умолчанию
включены базы для Python, Perl, PHP, C++ - можно взять файлы из папки signatures
и использовать в своих собственных разработках.

SWAAT

Сайт: www.owasp.org
Лицензия: Open Source
Платформа: Unix, Windows
Языки: Java, JSP, ASP .Net, PHP

Если в graudit для задания сигнатуры уязвимости используются текстовые файлы,
то в SWAAT – более прогрессивный подход с помощью XML-файлов. Вот так
выглядит типичная сигнатура:

vuln match - регулярное выражение для поиска;
type - указывает на тип уязвимости:
severity - обозначает уровень риска (high, medium или low)
alt - альтернативный вариант кода для решения проблемы

SWAAT считывает базу сигнатур и с ее помощью пытается найти проблемные
участки кода в исходниках на Java, JSP, ASP .Net, и PHP. База постоянно
пополняется и помимо списка "опасных" функций, сюда включены типичные ошибки в
использовании форматирования строк и составлении SQL-запросов. Примечательно,
что прога написана на C#, однако отлично работает и под никсами, благодаря
проекту Mono - открытой реализации платформы.Net.

PHP Bug Scanner

Сайт:
raz0r.name/releases/php-bug-scanner
Лицензия: Freeware
Платформа: Windows
Языки: PHP

Если тебе нужно провести статический анализ PHP-приложения, рекомендую
попробовать PHP Bug Scanner , которую написал наш автор - raz0r. Работа
проги основана на сканировании различных функций и переменных в PHP-скриптах,
которые могут быть задействованы при проведении веб-атак. Описание таких
ситуаций оформляется в виде так называемых пресетов, причем в программу уже
включены 7 специальных прессетов, сгруппированных по категориям:

  • code execution;
  • command execution;
  • directory traversal;
  • globals overwrite;
  • include;
  • SQL-injection;
  • miscellaneous.

Забавно, что прога написана на
PHP/WinBinder и скомпилирована
bamcompile , поэтому выглядит так же, как и обычное Windows-приложение. Через
удобный интерфейс пентестер может включить или отключь анализ кода на наличие
тех или иных уязвимостей.

Pixy

Сайт:
pixybox.seclab.tuwien.ac.at
Лицензия: Freeware
Платформа: Unix, Windows
Языки: PHP

В основе работы инструмента - сканирование исходного кода и построение графов
потоков данных. По такому графу прослеживается путь данных, которые поступают
извне программы - от пользователя, из базы данных, от какого-нибудь внешнего
плагина и т.п. Таким образом строится список уязвимых точек (или входов) в
приложениях. С помощью паттернов, описывающих уязвимость, Pixy проверяет такие
точки и позволяет определить XSS- и SQL-уязвимости. Причем сами графы, которые
строятся во время анализа, можно посмотреть в папке graphs (например,
xss_file.php_1_dep.dot) - это очень полезно для того чтобы понять, почему именно
тот или иной участок кода считается Pixy-уязвимым. Вообще, сама разработка
крайне познавательна и демонстрирует, как работают продвинутые утилиты для
статического анализа кода. На страничке

документации разработчик доходчиво рассказывает о разных этапах работы
программы, объясняет логику и алгоритм того, как должен анализироваться прогой
тот или иной фрагмент кода. Сама программа написана на Java и распространяется в
открытых исходниках, а на домашней страничке есть даже простенький онлайн-сервис
для проверки кода на XSS-уязвимости.

Ounce 6

Сайт: www.ouncelabs.com/products
Лицензия: Shareware
Платформа: Windows

Увы, существующие бесплатные решения пока на голову ниже, чем коммерческие
аналоги. Достаточно изучить качество и детальность отчета, который составляет
Ounce 6
– и понять, почему. В основе программы лежит специальный
анализирующий движок Ounce Core, который проверяет код на соответствие правилам
и политикам, составленными командой профессиональных пентестеров,
аккумулировавших опыт известных security-компаний, хакерского комьюнити, а также
стандартов безопасности. Программа определяет самые разные уязвимости в коде: от
переполнения буфера до SQL-инъекций. При желании Ounce несложно интегрируется с
популярными IDE, чтобы реализовать автоматическую проверку кода во время сборки
каждого нового билда разрабатываемого приложения. Кстати говоря,
компанию-разработчика - Ounce Labs - летом этого года приобрела сама IBM. Так
что продукт, скорее всего, продолжит развитие уже как часть одного из
коммерческих приложений IBM.

Klocwork Insight

Сайт: www.klocwork.com
Лицензия: Shareware
Платформа: Windows
Языки: C++, Java, C#

Долгое время этот, опять же, коммерческий продукт реализовал статическое
сканирование кода только для C, C+ и Java. Но, как только вышли Visual Studio
2008 и.NET Framework 3.5, разработчики заявили о поддержке C#. Я прогнал
программу на двух своих вспомогательных проектах, которые на скорую руку написал
на "шарпе" и программа выявила 7 критических уязвимостей. Хорошо, что они
написаны исключительно для внутреннего использования:). Klocwork Insight
изначально настроен, прежде всего, на работу в связке с интегрированными средами
разработки. Интеграция с теми же Visual Studio или Eclipse выполнена чрезвычайно
удачно – начинаешь всерьез задумываться, что такая функциональность должна быть
реализована в них по умолчанию:). Если не брать в расчет проблемы с логикой
работы приложения и проблемы с быстродействием, то Klocwork Insight
отлично справляется с поиском переполнения буфера, отсутствия фильтрации
пользовательского кода, возможности SQL/Path/Cross-site инъекций, слабого
шифрования и т.п. Еще одна интересная опция – построение дерева выполнения
приложения, позволяющего быстро вникнуть в общий принцип работы приложения и
отдельно проследить, например, за обработкой какого-либо пользовательского
ввода. А для быстрого конструирования правил для проверки кода предлагается даже
специальный инструмент - Klocwork Checker Studio .

Coverity Prevent Static Analysis

Сайт: www.coverity.com/products
Лицензия: Shareware
Платформа: Windows
Языки: C++, Java, C#

Один из самых известных статических анализаторов кода на C/C++, Java и C#.
Если верить его создателям, – решение используется более чем 100.000
разработчиков по всему миру. Продуманные механизмы позволяют автоматизировать
поиск утечек памяти, неотловленных исключений, проблем с быстродействием и,
конечно же, уязвимостей в безопасности. Продукт поддерживает разные платформы,
компиляторы (gcc, Microsoft Visual C++ и многие другие), а также интегрируется с
различными средами разработки, прежде всего Eclipse и Visual Studio. В основе
обхода кода используются не тупые алгоритмы обхода от начала до конца, а что-то
вроде отладчика, анализирующего, как программа поведет в себя в различных
ситуациях после встречи ветвления. Таким образом достигается 100% покрытия кода.
Столь сложный подход потребовался в том числе, чтобы всецело анализировать
многопоточные приложения, специально оптимизированные для работы на многоядерных
процессорах. Coverity Integrity Center позволяет находить такие ошибки
как состояние гонки (ошибка проектирования многозадачной системы, при которой
работа системы зависит от того, в каком порядке выполняются части кода), тупики
и многое другое. Зачем это нужно реверсерам? Спроси об этом разработчиков 0day
сплоитов для Firefox и IE:).

OWASP Code Crawler

Сайт: www.owasp.org
Лицензия: GNU GPL
Платформа: Windows
Языки: Java, C#, VB

Создатель этой тулзы Алессио Марциали - автор двух книжек по ASP.NET,
авторитетный кодер высоконагруженных приложений для финансового сектора, а также
пентестер. В 2007 году он опубликовал информацию о критических уязвимостях в 27
правительственных сайтах Италии. Его детище – OWASP Code Crawler
предназначенное для статического анализа кода.NET и J2EE/JAVA, открыто доступно
в инете, а в конце года автор обещается выпустить новую версию программы с
намного большей функциональностью. Но самое-то главное реализовано уже сейчас –
анализ исходников на C#, Visual Basic и Java. Файлы для проверки выбираются
через GUI-интерфейс, а сканирование запускается автоматически. Для каждого
проблемного участка кода выводится описание уязвимости в разделе Threat
Description. Правда, поле OWASP Guidelines , вероятно, указывающее пути
решения проблемы, увы, пока не доступно. Зато можно воспользоваться
экспериментальной особенностью сканирования кода на удаленной машине, доступной
во вкладке Remote Scan. Автор обещает серьезно прокачать эту возможность и, в
том числе, агрегировать исходники приложения для анализа прямо из системы
контроля версий.

WARNING

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

С оригиналом статьи можно познакомится, воспользовавшись Wayback Machine - Internet Archive: Static Code Analysis .

Поскольку все статьи на нашем сайте представлены на русском и английском языке, то мы выполнили перевод статьи Static Code Analysis на русский язык. А заодно решили опубликовать её на Хабре. Здесь уже публиковался пересказ этой статьи . Но уверен, многим будет интересно прочитать именно перевод.

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

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

Я всегда старался писать хороший код. По своей натуре я похож на ремесленника, которым движет желание непрерывно что-то улучшать. Я прочел груды книг со скучными названиями глав типа «Стратегии, стандарты и планы качества», а работа в Armadillo Aerospace открыла мне дорогу в совершенно иной, отличный от предшествующего опыта мир разработки ПО с повышенными требованиями к безопасности.

Более десяти лет назад, когда мы занимались разработкой Quake 3, я купил лицензию на PC-Lint и пытался применять его в работе: привлекала идея автоматического обнаружения дефектов в коде. Однако необходимость запуска из командной строки и просмотра длинных списков диагностических сообщений отбили у меня охоту пользоваться этим инструментом, и я вскоре отказался от него.

С тех пор и количество программистов, и размер кодовой базы выросли на порядок, а акцент в программировании сместился с языка C на C++. Все это подготовило намного более благодатную почву для программных ошибок. Несколько лет назад, прочитав подборку научных статей о современном статическом анализе кода, я решил проверить, как изменилось положение дел в этой области за последние десять лет с тех пор, как я попробовал работать с PC-Lint.

На тот момент код у нас компилировался на 4-ом уровне предупреждений, при этом выключенными мы оставляли лишь несколько узкоспециальных диагностик. С таким подходом - заведомо рассматривать каждое предупреждение, как ошибку - программисты были вынуждены неукоснительно придерживаться данной политики. И хотя в нашем коде можно было отыскать несколько пыльных уголков, в которых с годами скопился всякий «мусор», в целом он был довольно современным. Мы считали, что у нас вполне неплохая кодовая база.

Coverity

Началось все с того, что я связался с Coverity и подписался на пробную диагностику нашего кода их инструментом. Это серьезная программа, стоимость лицензии зависит от общего количества строк кода, и мы остановились на цене, выраженной пятизначным числом. Показывая нам результаты анализа, эксперты из Coverity отметили, что наша база оказалась одной из самых чистых в своей «весовой категории» из всех, что им доводилось видеть (возможно, они говорят это всем клиентам, чтобы приободрить их), однако отчет, который они нам передали, содержал около сотни проблемных мест. Такой подход сильно отличался от моего предыдущего опыта работы с PC-Lint. Соотношение сигнал/шум в данном случае оказался чрезвычайно высок: большинство из выданных Coverity предупреждений действительно указывали на явно некорректные участки кода, которые могли иметь серьезные последствия.

Этот случай буквально открыл мне глаза на статический анализ, но высокая цена всего удовольствия некоторое время удерживала от покупки инструмента. Мы подумали, что в оставшемся до релиза коде у нас будет не так много ошибок.

Microsoft /analyze

Не исключено, что я, в конце концов, решился бы купить Coverity, но пока я размышлял над этим, Microsoft пресекли мои сомнения, реализовав новую функцию /analyze в 360 SDK. /Analyze прежде был доступен в качестве компонента топовой, безумно дорогой версии Visual Studio, а потом вдруг достался бесплатно каждому разработчику под xbox 360. Я так понимаю, о качестве игр на 360-й платформе Microsoft печется больше, чем о качестве ПО под Windows. :-)

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

Я потихоньку приступил к правке кода - прежде всего, занялся своим собственным, затем системным, и, наконец, игровым. Работать приходилось урывками в свободное время, так что весь процесс затянулся на пару месяцев. Однако эта задержка имела и свой побочный полезный эффект: мы убедились, что /analyze действительно отлавливает важные дефекты. Дело в том, что одновременно с моими правками наши разработчики устроили большую многодневную охоту за багами, и выяснилось, что каждый раз они нападали на след какой-нибудь ошибки, уже помеченной /analyze, но еще не исправленной мной. Помимо этого, были и другие, менее драматичные, случаи, когда отладка приводила нас к коду, уже помеченному /analyze. Все это были настоящие ошибки.

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

Однажды мы в каком-то проекте случайно выключили статический анализ. Прошло несколько месяцев, и когда я заметил это и снова включил его, инструмент выдал кучу новых предупреждений об ошибках, внесенных в код за это время. Подобным же образом программисты, работающие только под PC или PS3, вносят в репозиторий код с ошибками и пребывают в неведении, пока не получат письмо с отчетом о «неудачной 360-сборке». Эти примеры наглядно демонстрируют, что в процессе своей повседневной деятельности разработчики раз за разом совершают ошибки определенных видов, и /analyze надежно уберегал нас от большей их части.

PVS-Studio

Поскольку мы могли использовать /analyze только на 360-коде, большой объем нашей кодовой базы по-прежнему оставался не покрытым статическим анализом - это касалось кода под платформы PC и PS3, а также всех программ, работающих только на PC.

Следующим инструментом, с которым я познакомился, был PVS-Studio . Он легко интегрируется в Visual Studio и предлагает удобный демо-режим (попробуйте сами!). В сравнении с /analyze PVS-Studio ужасно медлителен, но он сумел выловить некоторое количество новых критических багов, причем даже в том коде, который был уже полностью вычищен с точки зрения /analyze. Помимо очевидных ошибок PVS-Studio отлавливает множество других дефектов, которые представляют собой ошибочные программистские клише, пусть и кажущиеся на первый взгляд нормальным кодом. Из-за этого практически неизбежен некоторый процент ложных срабатываний, но, черт возьми, в нашем коде такие шаблоны нашлись, и мы их поправили.

На сайте PVS-Studio можно найти большое количество замечательных статей об инструменте, и многие из них содержат примеры из реальных open-source проектов, иллюстрирующие конкретно те виды ошибок, о которых идет речь в статье. Я думал, не вставить ли сюда несколько показательных диагностических сообщений, выдаваемых PVS-Studio, но на сайте уже появились намного более интересные примеры. Так что посетите страничку и посмотрите сами. И да - когда будете читать эти примеры, не надо ухмыляться и говорить, что вы бы так никогда не написали.

PC-Lint

В конце концов, я вернулся к варианту с использованием PC-Lint в связке с Visual Lint для интеграции в среду разработки. В соответствии с легендарной традицией мира Unix инструмент можно настроить на выполнение практически любой задачи, однако интерфейс его не очень дружественен и его нельзя просто так «взять и запустить». Я приобрел набор из пяти лицензий, но его освоение оказалось настолько трудоемким, что, насколько я знаю, все остальные разработчики от него в итоге отказались. Гибкость действительно имеет свои преимущества - так, например, мне удалось настроить его для проверки всего нашего кода под платформу PS3, хотя это и отняло у меня немало времени и усилий.

И снова в том коде, который был уже чист с точки зрения /analyze и PVS-Studio, нашлись новые важные ошибки. Я честно старался вычистить его так, чтобы и lint не ругался, но не удалось. Я поправил весь системный код, но сдался, когда увидел, сколько предупреждений он выдал на игровой код. Я рассортировал ошибки по классам и занялся наиболее критичными из них, игнорируя массу других, относящихся больше к стилистических недоработкам или потенциальным проблемам.

Я полагаю, что попытка исправить громадный объем кода по максимуму с точки зрения PC-Lint заведомо обречена на провал. Я написал некоторое количество кода с нуля в тех местах, где послушно старался избавиться от каждого назойливого «линтовского» комментария, но для большинства опытных C/C++-программистов такой подход к работе над ошибками - уже чересчур. Мне до сих пор приходится возиться с настройками PC-Lint, чтобы подобрать наиболее подходящий набор предупреждений и выжать из инструмента максимум пользы.

Выводы

Я немало узнал, пройдя через все это. Боюсь, что кое-какие из моих выводов будут с трудом восприняты людьми, которым не приходилось лично разбирать сотни сообщений об ошибках в сжатые сроки и всякий раз чувствовать дурноту, приступая к их правке, и стандартной реакцией на мои слова будет «ну у нас-то все в порядке» или «все не так плохо».

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

Автоматизация необходима. Когда видишь сообщения о чудовищных сбоях в автоматических системах, невозможно не испытать эдакое злорадство, однако на каждую ошибку в автоматизации приходится легион ошибок человеческих. Призывы к тому, чтобы «писать более качественный код», благие намерения о проведении большего числа сеансов инспекции кода, парного программирования и так далее просто-напросто не действуют, особенно когда в проект вовлечены десятки людей и работать приходится в дикой спешке. Громадная ценность статического анализа заключается в возможности при каждом запуске находить хотя бы небольшие порции ошибок, доступных этой методике.

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

Инструменты статического анализа вынуждены работать «с одной рукой, связанной за спиной»: им приходится делать выводы на основе разбора языков, которые вовсе не обязательно предоставляют информацию для таких выводов, и в целом делать очень осторожные предположения. Поэтому вы должны помогать своему анализатору, насколько возможно - отдавать предпочтение индексации перед арифметикой с указателями, держать граф вызовов в едином исходном файле, использовать явные аннотации и т.п. Все, что может показаться статическому анализатору неочевидным, почти наверняка собьет с толку и ваших коллег-программистов. Характерное «хакерское» отвращение к языкам со строгой статической типизацией («bondage and discipline languages») на деле оказывается недальновидным: потребности крупных, долгоживущих проектов, в разработку которых вовлечены большие команды программистов, кардинально отличаются от мелких и быстрых задач, выполняемых для себя.

Нулевые указатели - это самая насущная проблема в языке C/C++, по крайней у нас. Возможность двойственного использования единого значения в качестве как флага, так и адреса приводит к невероятному числу критических ошибок. Поэтому всегда, когда для этого есть возможность, в C++ следует отдавать предпочтение ссылкам, а не указателям. Хотя ссылка «на самом деле» есть ни что иное как тот же указатель, она связана неявным обязательством о невозможности равенства нулю. Выполняйте проверки указателей на ноль, когда они превращаются в ссылки - это позволит вам впоследствии забыть о данной проблеме. В сфере игростроения существует множество глубоко укоренившихся программистских шаблонов, несущих потенциальную опасность, но я не знаю способа, как полностью и безболезненно перейти от проверок на ноль к ссылкам.

Второй по важности проблемой в нашей кодовой базе были ошибки с printf-функциями. Она дополнительно усугублялась тем, что передача idStr вместо idStr::c_str() практически каждый раз заканчивалась падением программы. Однако, когда мы стали использовать аннотации /analyze для функций с переменным количеством аргументов, чтобы поверки типов выполнялись корректно, проблема была решена раз и навсегда. В полезных предупреждениях анализатора мы встречали десятки таких дефектов, которые могли привести к падению, случись какому-нибудь ошибочному условию запустить соответствующую ветку кода - это, между прочим, говорит еще и том, как мал был процент покрытия нашего кода тестами.

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

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

Если вас не напугали до глубины души все те дополнительные трудности, которые несет в себе параллельное программирование, вы, похоже, просто не вникли в этот вопрос как следует.

Невозможно провести достоверные контрольные испытания при разработке ПО, но наш успех от использования анализа кода был настолько отчетливым, что я могу позволить себе просто заявить: не использовать анализ кода - безответственно! Автоматические консольные логи о падениях содержат объективные данные, которые ясно показывают, что Rage, даже будучи по многим показателям первопроходцем, оказался намного стабильнее и здоровее, чем большинство самых современных игр. Запуск Rage на PC, к сожалению, провалился - готов поспорить, что AMD не используют статический анализ при разработке своих графических драйверов.

Вот вам готовый рецепт: если в вашей версии Visual Studio есть встроенный /analyze, включите его и попробуйте поработать так. Если бы меня попросили выбрать из множества инструментов один, я бы остановился именно на этом решении от Microsoft. Всем остальным, кто работает в Visual Studio, я советую хотя бы попробовать PVS-Studio в демо-режиме. Если вы разрабатываете коммерческое ПО, приобретение инструментов статического анализа будет одним из лучших способов вложения средств.

И напоследок комментарий из твиттера.

При статическом анализе (static analysis) можно обнаружить много разнообразных дефектов и слабых мест исходного кода даже до того, как код будет готов для запуска. С другой стороны, динамический анализ (runtime analysis), или анализ во время выполнения, происходит на работающем программном обеспечении и обнаруживает проблемы по мере их возникновения, обычно используя сложные инструментальные средства. Кто-то может возразить, что одна форма анализа предваряет другую, но разработчики могут комбинировать оба способа для ускорения процессов разработки и тестирования, а также для повышения качества выдаваемого продукта.

В данной статье вначале рассматривается метод статического анализа. С его помощью можно предотвратить проблемы до их проникновения в состав основного программного кода и гарантировать, что новый код соответствует стандарту. Используя разные техники анализа, например, проверку абстрактного синтаксического дерева (abstract syntax tree, AST) и анализ кодовых путей, инструменты статического анализа могут выявить скрытые уязвимости, логические ошибки, дефекты реализации и другие проблемы. Это может происходить как на этапе разработки на каждом рабочем месте, так и во время компоновки системы. Далее в статье исследуется метод динамического анализа, который можно использовать на этапе разработки модулей и системной интеграции и который позволяет выявить проблемы, пропущенные при статическом анализе. При динамическом анализе не только обнаруживаются ошибки, связанные с указателями и другими некорректностями, но и есть возможность также оптимизировать использование циклов ЦПУ, ОЗУ, флеш-памяти и других ресурсов.

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

Объединяя лучшее из двух миров

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

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

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

Поскольку во время статического анализа делается попытка предсказать поведение программы, основанное на модели исходного кода, то иногда обнаруживается "ошибка", которой фактически не существует – это так называемое "ложное срабатывание" (false positive). Многие современные средства статического анализа реализуют улучшенную технику, чтобы избежать подобной проблемы и выполнить исключительно точный анализ.

Статический анализ: аргументы "за" Статический анализ: аргументы "против"
Используется уже на ранних этапах жизненного цикла программного обеспечения, прежде чем код готов для исполнения и до начала тестирования.

Могут анализироваться существующие базы кодов, которые уже прошли тестирование.

Средства могут быть интегрированы в среду разработки в качестве части компонента, используемого при "ночных сборках" (nightly builds) и как часть инструментального набора рабочего места разработчика.

Низкие стоимостные затраты: нет необходимости создавать тестовые программы или фиктивные модули (stubs); разработчики могут запускать свои собственные виды анализа.

Могут обнаруживаться программные ошибки и уязвимости, которые не обязательно приводят к отказу программы или воздействуют на поведение программы во время ее реального исполнения.

Ненулевая вероятность "ложного срабатывания".

Таблица 1 - Аргументы "за" и "против" статического анализа.

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

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

Динамический анализ: аргументы "за" Динамический анализ: аргументы "против"
Редко возникают "ложные срабатывания" – высокая продуктивность по нахождению ошибок

Для отслеживания причины ошибки может быть произведена полная трассировка стека и среды исполнения.

Захватываются ошибки в контексте работающей системы, как в реальных условиях, так и в режиме имитационного моделирования.

Происходит вмешательство в поведение системы в реальном времени; степень вмешательства зависит количества используемых инструментальных вставок. Это не всегда приводит к возникновению проблем, но об этом нужно помнить при работе с критическим ко времени кодом.

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

Таблица 2 - Аргументы "за" и "против" динамического анализа.

Раннее обнаружение ошибок для уменьшения затрат на разработку

Чем раньше обнаруживается программная ошибка, тем быстрее и дешевле ее можно исправить. Поэтому инструменты статического и динамического анализа представляют реальную ценность, обеспечивая поиск ошибок на ранних этапах жизненного цикла программного обеспечения. Различные исследования промышленных продуктов показывают, что коррекция проблемы на этапе тестирования системы (для подтверждения качества ее работы, QA) или уже после поставки системы оказывается на несколько порядков более дорогостоящей, чем устранение тех же проблем на этапе разработки программного обеспечения. Многие организации имеют свои оценочные показатели относительно затрат на устранение дефектов. На рис. 1 приведены данные по обсуждаемой проблеме, взятые из часто цитируемой книги Каперса Джонса (Capers Jones) "Applied Software Measurement" ("Прикладные измерения программного обеспечения") .

Рис. 1 - По мере продвижения проекта стоимость устранения дефектов программного обеспечения может экспоненциально возрастать. Инструменты статического и динамического анализа помогают предотвратить эти затраты благодаря обнаружению программных ошибок на ранних этапах жизненного цикла программного обеспечения.

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

Статический анализ используется в практике программных разработок почти столь же долго, как существует сама разработка программного обеспечения. В своем первоначальном виде анализ сводился к контролю соответствия стандартам стиля программирования (lint). Разработчики пользовались этим непосредственно на своем рабочем месте. Когда дело доходило до обнаружения программных ошибок, то в ранних инструментах статического анализа основное внимание обращалось на то, что лежит на поверхности: стиль программирования и часто возникающие синтаксические ошибки. Например, даже самые простейшие инструменты статического анализа смогут обнаружить ошибку такого рода:

Int foo(int x, int* ptr) { if(x & 1); { *ptr = x; return; } ... }

Здесь ошибочное употребление лишней точки с запятой приводит к потенциально катастрофическим результатам: указатель входного параметра функции переопределяется при неожидаемых условиях. Указатель переопределяется всегда, вне зависимости от проверяемого условия.

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

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

Для выполнения таких изощренных форм анализа в инструментах статического анализа имеют дело с двумя основными типами проверок кода:

  • Проверка абстрактного синтаксического дерева - для проверки базового синтаксиса и структуры кода.
  • Анализ кодовых путей - для выполнения более полного анализа, который зависит от понимания состояния программных объектов данных в конкретной точке на пути исполнения кода.

Абстрактные синтаксические деревья

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

Очень просто построить программу контроля, которая проверяет соблюдение стандартов относительно соглашения по присваиванию имен и ограничений по функциональным вызовам, например, контроль небезопасных библиотек. Цель выполнения проверок по AST обычно состоит в получении каких-либо логических выводов на основании кода как такового без использования знаний о поведении кода в процессе исполнения.

Многие инструменты предлагают проверки на основе AST для множества языков, включая инструменты с открытым исходным кодом, например, PMD для Java. Некоторые инструменты используют грамматику Х-путей или производную от Х-путей грамматику, чтобы определять условия, которые представляют интерес для программ контроля. Другие инструменты предоставляют расширенные механизмы, дающие возможность пользователям создавать свои собственные программы проверки на основе AST. Такой тип проверки относительно просто проводить, и многие организации создают новые программы проверки такого типа, чтобы проверить соблюдение корпоративных стандартов написания кода или рекомендованных по отрасли лучших методов организации работ.

Анализ кодовых путей

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

If(x & 1) ptr = NULL; *ptr = 1;

Поверхностное рассмотрение фрагмента приводит к очевидному выводу, что переменная ptr может принимать значение NULL, если переменная x – нечетна, и это условие при разыменовании неизбежно приведет к обращению к нулевой странице. Тем не менее, при создании программы проверки на основе AST найти такую программную ошибку весьма проблематично. Рассмотрим дерево AST (в упрощенном виде для ясности), которое было бы создано для приведенного выше фрагмента кода:

Statement Block If-statement Check-Expression Binary-operator & x 1 True-Branch Expression-statement Assignment-operator = ptr 0 Expression-statement Assignment-operator = Dereference-pointer - ptr 1 В подобных случаях никакой поиск по дереву или простое перечисление узлов не смогут обнаружить в разумно обобщенной форме предпринимаемую попытку (по меньшей мере, иногда недопустимую) разыменования указателя ptr. Соответственно, инструмент анализа не может просто проводить поиск по синтаксической модели. Необходимо также анализировать жизненный цикл объектов данных по мере их появления и использования внутри управляющей логики в процессе исполнения.

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

  • Не был ли вновь созданный объект освобожден прежде, чем из области видимости были удалены все обращающиеся к нему ссылки?
  • Проводилась ли для некоторого объекта данных проверка разрешенного диапазона значений, прежде чем объект передается функции ОС?
  • Проверялась ли строка символов на наличие в ней специальных символов перед передачей этой строки в качестве SQL-запроса?
  • Не приведет ли операция копирования к переполнению буфера?
  • Безопасно ли в данное время вызывать эту функцию?

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

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

Последовательность действий при выполнении статического анализа

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

Уникальный в своем роде продукт Klocwork Insight позволяет проводить анализ кода, создаваемого на рабочем месте конкретного разработчика, при этом избегать проблем, связанных с неточностью диагностики, что обычно свойственно инструментам подобного рода. Компания Klocwork предоставляет возможность проведения связного анализа кода на рабочем месте (Connected Desktop Analysis), когда выполняется анализ кода разработчика с учетом понимания всех системных зависимостей. Это приводит к проведению локального анализа, который является в такой же мере точным и мощным, как и централизованный системный анализ, но все это выполняется до полной сборки кода.

С точки зрения перспектив относительно последовательности действий при анализе, данная способность позволяет разработчику провести точный и высококачественный статический анализ на самом раннем этапе жизненного цикла разработки. Инструмент Klockwork Insight выдает в интегрированную среду разработчика (IDE) или в командную строку сообщения по всем проблемам по мере написания разработчиком кода и периодического выполнения им операций компиляции/компоновки. Выдача таких сообщений и отчетов происходит до выполнения динамического анализа и до того, как все разработчики сведут свои коды воедино.

Рис. 2 - Последовательность выполнения статического анализа.

Технология динамического анализа

Для обнаружения программных ошибок в инструментах динамического анализа часто производится вставка небольших фрагментов кода либо непосредственно в исходный текст программы (вставка в исходный код), либо в исполняемый код (вставка в объектный код). В таких кодовых сегментах выполняется "санитарная проверка" состояния программы и выдается отчет об ошибках, если обнаруживается что-то некорректное или неработоспособное. В таких инструментах могут быть задействованы и другие функции, например, отслеживание распределения памяти и ее использования во времени.

Технология динамического анализа включает в себя:

  • Размещение вставок в исходный код на этапе препроцессорной обработки – в исходный текст приложения до начала компиляции вставляется специальный фрагмент кода для обнаружения ошибок. При таком подходе не требуется детального знания среды исполнения, в результате чего такой метод пользуется популярностью среди инструментов тестирования и анализа встраиваемых систем. В качестве примера такого инструмента можно привести продукт IBM Rational Test RealTime.
  • Размещение вставок в объектный код – для такого инструмента динамического анализа необходимо обладать достаточными знаниями относительно среды исполнения, чтобы иметь возможность вставлять код непосредственно в исполняемые файлы и библиотеки. При этом подходе не нужно иметь доступа к исходному тексту программы или делать перекомпоновку приложения. В качестве примера такого инструмента можно привести продукт IBM Rational Purify.
  • Вставка кода во время компиляции – разработчик использует специальные ключи (опции) компилятора для внедрения в исходный код. Используется способность компилятора обнаруживать ошибки. Например, в компиляторе GNU C/C++ 4.x используется технология Mudflap для выявления проблем, касающихся операций с указателями.
  • Специализированные библиотеки этапа исполнения – для обнаружения ошибок в передаваемых параметрах разработчик использует отладочные версии системных библиотек. Дурной славой пользуются функции типа strcpy() из-за возможности появления нулевых или ошибочных указателей во время исполнения. При использовании отладочных версий библиотек такие "плохие" параметры обнаруживаются. Данная технология не требует перекомпоновки приложения и в меньшей степени влияет на производительность, чем полноценное использование вставок в исходный/объектный код. Данная технология используется в инструменте анализа ОЗУ в QNX® Momentics® IDE.

В данной статье мы рассмотрим технологии, используемые в инструментах разработчика QNX Momentics, особенно обращая внимание на GCC Mudflap и специализированные библиотеки этапа исполнения.

GNU C/C++ Mudflap: внедрение в исходный код на этапе компиляции

В инструментальном средстве Mudflap, присутствующем в версии 4.х компилятора GNU C/C++ (GCC), используется внедрение в исходный код во время компиляции. При этом во время исполнения происходит проверка конструкций, потенциально несущих в себе возможность появления ошибок. Основное внимание в средстве Mudflap обращается на операции с указателями, поскольку они являются источником многих ошибок на этапе выполнения для программ, написанных на языках C и C++.

С подключением средства Mudflap в работе компилятора GCC появляется еще один проход, когда вставляется проверочный код для операций с указателями. Вставляемый код обычно выполняет проверку на достоверность значений передаваемых указателей. Неправильные значения указателей приведут к выдаче компилятором GCC сообщений на стандартное устройство выдачи ошибок консоли (stderr). Средство Mudflap для контроля за указателями не просто проверяет указатели на нулевое значение: в его базе данных хранятся адреса памяти для действительных объектов и свойства объектов, например, местоположение в исходном коде, метка даты/времени, обратная трассировка стека при выделении и освобождении памяти. Такая база данных позволяет быстро получить необходимые данные при анализе операций доступа к памяти в исходном коде программы.

В библиотечных функциях, подобных strcpy(), не выполняется проверка передаваемых параметров. Такие функции не проверяются и средством Mudflap. Тем не менее, в Mudflap можно создать символьную оболочку (symbol wrapper) для статически подключаемых библиотек или вставку для динамических библиотек. При такой технологии между приложением и библиотекой создается дополнительный слой, что дает возможность провести проверку достоверности параметров и выдать сообщение о проявлении отклонений. В средстве Mudflap используется эвристический алгоритм, основанный на знании границ памяти, используемых приложением (динамическая память, стек, сегменты кода и данных и т.д.), чтобы определить правильность значений возвращаемых указателей.

Используя ключи командной строки компилятора GCC, разработчик может подключить возможности Mudflap для вставки кодовых фрагментов и управления поведением, например, управление нарушениями (границ, значений), проведение дополнительных проверок и настроек, подключение эвристических методов и самодиагностики. Например, комбинация ключей -fmudflap устанавливает конфигурацию Mudflap по умолчанию. Сообщения компилятора о выявленных средством Mudflap нарушениях выдаются на выходную консоль (stderr) или в командную строку. В подробном выводе предоставляется информация о нарушении и о задействованных при этом переменных, функциях, а также о местонахождении кода. Эта информация может автоматически импортироваться в среду IDE, где происходит ее визуализация, и выполняется трассировка стека. Пользуясь этими данными, разработчик может быстро перейти к соответствующему месту исходного кода программы.

На рис. 3 показан пример представления ошибки в среде IDE вместе с соответствующей информацией по обратной трассировке. Обратная трассировка работает как связующее звено с исходным кодом, что позволяет разработчику быстро диагностировать причину проблемы.

При использовании средства Mudflap может возрасти время на компоновку, и может уменьшиться производительность во время исполнения. Данные, представленные в статье “Mudflap: Pointer Use Checking for C/C++” ("Средство Mudflap: проверка использования указателей для языков C/C++”) говорят о том, что с подключением Mudflap время компоновки возрастает в 3…5 раз, а программа начинает работать медленнее в 1.25 … 5 раз. Совершенно ясно, что разработчики критических по времени исполнения приложений должны пользоваться эти средством с осторожностью. Тем не менее Mudflap является мощным средством для выявления подверженных ошибкам и потенциально фатальных кодовых конструкций. Компания QNX планирует использовать средство Mudflap в будущих версиях своих инструментов динамического анализа.

Рис. 3 - Использование информации обратной трассировки, отображаемой в среде QNX Momentics IDE для поиска исходного кода, приведшего к ошибке.

Отладочные версии библиотек этапа исполнения

Наряду с использованием специальных отладочных вставок в библиотеки этапа исполнения, которые приводят к значительным дополнительным расходам памяти и времени на этапах компоновки и исполнения, разработчики могут использовать предварительно подготовленные (pre-instrumented) библиотеки этапа исполнения. В таких библиотеках вокруг функциональных вызовов добавляется определенный код, цель которого состоит в проверке достоверности входных параметров. Например, рассмотрим старую знакомую – функцию копирования строк:

strcpy(a,b);

В ней участвуют два параметра, оба являющиеся указателями на тип char : один для исходной строки (b ), а другой для строки-результата (a ). Несмотря на такую простоту, эта функция может являться источником множества ошибок:

  • если значение указателя a равно нулю или является недействительным, то копирование по этому адресу назначения приведет к ошибке запрета доступа к памяти;
  • если значение указателя b равно нулю или является недействительным, то чтение информации с этого адреса приведет к ошибке запрета доступа к памяти;
  • если в конце строки b пропущен завершающий строку символ "0", то в строку назначения будет скопировано большее число символов, чем ожидается;
  • если размер строки b больше размера памяти, выделенного под строку a , то по указанному адресу будет записано больше байт, чем предполагалось (типичный сценарий переполнения буфера).

В отладочной версии библиотеки производится проверка значений параметров ‘a ’ и ‘b ’. Проверяются также длины строк, чтобы убедиться в их совместимости. Если обнаруживается недействительный параметр, то выдается соответствующее аварийное сообщение. В среде QNX Momentics данные сообщения об ошибках импортируются из целевой системы и выводятся на экран. В среде QNX Momentics используется также технология слежениями за случаями выделения и освобождения памяти, что дает возможность выполнять глубокий анализ использования ОЗУ.

Отладочная версия библиотеки будет работать с любым приложением, в котором используются ее функции; не нужно вносить никаких дополнительных изменений в код. Более того, разработчик может добавить библиотеку во время запуска приложения. Тогда библиотека заменит соответствующие части полной стандартной библиотеки, устраняя необходимость использовать отладочную версию полной библиотеки. В среде QNX Momentics IDE разработчик может добавить такую библиотеку при запуске программы как элемент нормального интерактивного сеанса отладки. На рис. 4 показан пример того, как в среде QNX Momentics происходит обнаружение и выдача сообщений об ошибках работы с памятью.

В отладочных версиях библиотек предоставляется проверенный "неагрессивный" метод обнаружения ошибок при вызовах библиотечных функций. Такая технология идеальна для анализа ОЗУ и для других методов анализа, которые зависят от согласованных пар вызовов, например, malloc() и free(). Другими словами, данная технология может обнаруживать ошибки на этапе исполнения только для кодов с библиотечными вызовами. При этом не обнаруживаются многие типичные ошибки, такие как встроенный код разыменования ссылки (inline pointer dereferences) или некорректные арифметические операции с указателями. Обычно при отладке осуществляется контроль только некоторого подмножества системных вызовов. Подробнее с этим можно познакомиться в статье .

Рис. 4 - Анализ ОЗУ происходит путем расстановки ловушек в области вызовов API, связанных с обращением к памяти.

Последовательность действий при динамическом анализе

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

Как показано на рис. 5, динамический анализ не только позволяет обнаружить ошибки, но и помогает обратить внимание разработчика на подробности расходования памяти, циклов ЦПУ, дискового пространства и других ресурсов. Процесс анализа состоит из нескольких шагов, и хороший инструмент для динамического анализа предоставляет надежную поддержку каждого шага:

  1. Наблюдение – прежде всего, происходит захват ошибок среды выполнения, обнаружение мест утечки памяти и отображение всех результатов в среде IDE.
  2. Корректировка – далее разработчик имеет возможность провести трассировку каждой ошибки назад до нарушающей работу строки исходного текста. При хорошей интеграции в среду IDE каждая ошибка будет отображаться на экране. Разработчику нужно просто щелкнуть мышью на строке ошибки, и откроется фрагмент исходного кода со строкой, нарушающей работу. Во многих случаях разработчик может быстро устранить проблему, используя доступную трассировку стека и дополнительные инструменты работы с исходным кодом в среде IDE (средства просмотра вызовов функций, средства трассировки вызовов и т.д.).
  3. Профилирование – устранив обнаруженные ошибки и утечки памяти, разработчик может проанализировать использование ресурсов во времени, включая пиковые ситуации, среднюю загрузку и избыточное расходование ресурсов. В идеальном случае инструмент анализа выдаст визуальное представление долговременного использования ресурсов, позволяя немедленно идентифицировать всплески в распределении памяти и иные аномалии.
  4. Оптимизация – используя информацию этапа профилирования, разработчик может теперь провести "тонкий" анализ использования ресурсов программой. Среди прочего, подобная оптимизация может минимизировать случаи пикового использования ресурсов и их избыточное расходование, включая работу с ОЗУ и использование времени ЦПУ.

Рис. 5 - Типовая последовательность действий при динамическом анализе

Комбинирование последовательности действий различных видов анализа в среде разработки

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

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

Вот какой можно предложить пример совместного использования двух видов инструментов:

  1. В начале рабочего дня разработчик просматривает отчет о результатах ночной компоновки. В данный отчет включаются как собственно ошибки компоновки, так и результаты статического анализа, проведенного во время компоновки.
  2. В отчете по статическому анализу приводятся обнаруженные дефекты наряду с информацией, которая поможет в их устранении, в том числе ссылки на исходный код. Используя среду IDE, разработчик может пометить каждую ситуацию либо как действительно найденную ошибку, либо как "ложное срабатывание". После этого производится исправление фактически присутствующих ошибок.
  3. Разработчик локально, внутри среды IDE, сохраняет внесенные изменения вместе с любыми новыми фрагментами кода. Разработчик не передает эти изменения обратно в систему управления исходными текстами до тех пор, пока изменения не будут проанализированы и протестированы.
  4. Разработчик анализирует и корректирует новый код, используя инструмент статического анализа на локальном рабочем месте. Для того чтобы быть уверенным в качественном обнаружении ошибок и отсутствии "ложных срабатываний", в анализе используется расширенная информация на уровне системы. Эта информация берется из данных выполненного в ночное время процесса компоновки/анализа.
  5. После анализа и "чистки" любого нового кода разработчик встраивает код в локальный тестовый образ или исполняемый файл.
  6. Используя инструменты динамического анализа, разработчик запускает тесты для проверки внесенных изменений.
  7. С помощью среды IDE разработчик может быстро выявить и исправить ошибки, о которых сообщается через инструменты динамического анализа. Код считается окончательным и готовым к использованию, когда он прошел через статический анализ, блочное тестирование и динамический анализ.
  8. Разработчик передает изменения в систему управления исходными текстами; после этого измененный код участвует в процессе последующей ночной компоновки.

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

Роль архитектуры ОСРВ

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

Например, в микроядерной ОСРВ типа QNX Neutrino все приложения, драйверы устройств, файловые системы и сетевые стеки располагаются за пределами ядра в отдельных адресных пространствах. В результате все они оказываются изолированными от ядра и друг от друга. Такой подход обеспечивает наивысшую степень локализации сбоев: отказ одного из компонентов не приводит к краху системы в целом. Более того, оказывается, что можно легко локализовать ошибку, связанную с ОЗУ, или иную логическую ошибку с точностью до компонента, который эту ошибку вызвал.

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

Рис. 6 - В микроядерной ОС сбои в ОЗУ для драйверов, стеков протоколов и других службах не приведут к нарушению работы других процессов или ядра. Более того, ОС может мгновенно обнаружить неразрешенную попытку доступа к памяти и указать, со стороны какого кода предпринята эта попытка.

По сравнению с обычным ядром ОС микроядру свойственно необычайно малое среднее время восстановления после сбоя (Mean Time to Repair, MTTR). Рассмотрим, что происходит при сбое в работе драйвера устройства: ОС может завершить работу драйвера, восстановить используемые драйвером ресурсы и перезапустить драйвер. Обычно на это уходит несколько миллисекунд. В обычной монолитной операционной системе устройство должно быть перезагружено – этот процесс может занять от нескольких секунд до нескольких минут.

Заключительные замечания

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

Между тем, инструменты динамического анализа поддерживают этапы интеграции и тестирования, сообщая в среду разработки об ошибках (или потенциальных проблемах), возникающих при исполнении программы. Эти инструменты предоставляют также полную информацию по обратной трассировке до места возникновения ошибки. Используя эту информацию, разработчики могут выполнить "посмертную" отладку таинственного отказа программы или краха системы за значительно меньший интервал времени. При динамическом анализе через трассировку стека и переменных могут быть выявлены основополагающие причины проблемы – эта лучше, чем повсеместно использовать операторы “if (ptr != NULL)” для предотвращения и обхода аварийных ситуаций.

Использование раннего обнаружения, лучшего и полного тестового покрытия кода совместно с коррекцией ошибок помогает разработчикам создавать программное обеспечение лучшего качества и в более короткие сроки.

Библиография

  • Eigler, Frank Ch., “Mudflap: Pointer Use Checking for C/C++”, Proceedings of the GCC Developers Summit 2003, pg. 57-70. http://www.linux.org.uk/~ajh/gcc/gccsummit-2003-proceedings.pdf
  • “Heap Analysis: Making Memory Errors a Thing of the Past”, QNX Neutrino RTOS Programmer’s Guide. http://pegasus.ott.qnx.com/download/download/16853/neutrino_prog.pdf

О компании QNX Software Systems

QNX Software Systems является одной из дочерних компаний Harman International и ведущим глобальным поставщиком инновационных технологий для встраиваемых систем, включая связующее ПО, инструментальные средства разработки и операционные системы. ОСРВ QNX® Neutrino®, комплект разработчика QNX Momentics® и связующее ПО QNX Aviage®, основанные на модульной архитектуре, образуют самый надежный и масштабируемый программный комплекс для создания высокопроизводительных встраиваемых систем. Глобальные компании-лидеры, такие как Cisco, Daimler, General Electric, Lockheed Martin и Siemens, широко используют технологии QNX в сетевых маршрутизаторах, медицинской аппаратуре, телематических блоках автомобилей, системах безопасности и защиты, в промышленных роботах и других приложениях для ответственных и критически важных задач. Головной офис компании находится в г. Оттава (Канада), а дистрибьюторы продукции расположены в более чем 100 странах по всему миру.

О компании Klocwork

Продукты компании Klocwork предназначены для автоматизированного анализа статического кода, выявления и предотвращения дефектов программного обеспечения и проблем безопасности. Наша продукция предоставляет коллективам разработчиков инструментарий для выявления коренных причин недостатков качества и безопасности программного обеспечения, для отслеживания и предотвращения этих недостатков на протяжении всего процесса разработки. Запатентованная технология компании Klocwork была создана в 1996 г. и обеспечила высокий коэффициент окупаемости инвестиций (ROI) для более чем 80 клиентов, многие из которых входят в рейтинг крупнейших 500 компаний журнала Fortune и предлагают среды разработки программного обеспечения, пользующиеся наибольшим спросом в мире. Klocwork является частной компанией и имеет офисы в Берлингтоне, Сан Хосе, Чикаго, Далласе (США) и Оттаве (Канада).

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

Обзор кода (code review) – один из самых старых и надежных методов выявления дефектов. Он заключается в совместном внимательном чтении исходного кода и высказывании рекомендаций по его улучшению. В процессе чтения кода выявляются ошибки или участи кода, которые могут стать ошибочными в будущем. Также считается, что автор кода во время обзора не должен давать объяснений, как работает та или иная часть программы. Алгоритм работы должен быть понятен непосредственно из текста программы и комментариев. Если это условие не выполняется, то код должен быть доработан.

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

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

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

Задачи, решаемые программами статического анализа кода можно разделить на 3 категории:

  1. Выявление ошибок в программах. Подробнее про это будет рассказано ниже.
  2. Рекомендации по оформлению кода. Некоторые статические анализаторы позволяют проверять, соответствует ли исходный код, принятому в компании стандарту оформления кода. Имеется в виду контроль количества отступов в различных конструкциях, использование пробелов/символов табуляции и так далее.
  3. Подсчет метрик . Метрика программного обеспечения - это мера, позволяющая получить численное значение некоторого свойства программного обеспечения или его спецификаций. Существует большое количество разнообразных метрик, которые можно подсчитать, используя те ли иные инструменты.

Другие преимущества статического анализа кода:

  1. Полное покрытие кода. Статические анализаторы проверяют даже те фрагменты кода, которые получают управление крайне редко. Такие участки кода, как правило, не удается протестировать другими методами. Это позволяет находить дефекты в обработчиках редких ситуаций, в обработчиках ошибок или в системе логирования.
  2. Статический анализ не зависит от используемого компилятора и среды, в которой будет выполняться скомпилированная программа. Это позволяет находить скрытые ошибки, которые могут проявить себя только через несколько лет. Например, это ошибки неопределенного поведения . Такие ошибки могут проявить себя при смене версии компилятора или при использовании других ключей для оптимизации кода. Другой интересный пример скрытых ошибок приводится в статье "Перезаписывать память - зачем? ".
  3. Можно легко и быстро обнаруживать опечатки и последствия использования Copy-Paste . Как правило, нахождение этих ошибок другими способами является кране неэффективной тратой времени и усилий. Обидно после часа отладки обнаружить, что ошибка заключается в выражении вида "strcmp(A, A)". Обсуждая типовые ошибки, про такие ляпы, как правило, не вспоминают. Но на практике на их выявление тратится существенное время.

Недостатки статического анализа кода

  1. Статический анализ, как правило, слаб в диагностике утечек памяти и параллельных ошибок. Чтобы выявлять подобные ошибки, фактически необходимо виртуально выполнить часть программы. Это крайне сложно реализовать. Также подобные алгоритмы требуют очень много памяти и процессорного времени. Как правило, статические анализаторы ограничиваются диагностикой простых случаев. Более эффективным способом выявления утечек памяти и параллельных ошибок является использование инструментов динамического анализа.
  2. Программа статического анализа предупреждает о подозрительных местах. Это значит, что на самом деле код, может быть совершенно корректен. Это называется ложно-позитивными срабатываниями. Понять, указывает анализатор на ошибку или выдал ложное срабатывание, может только программист. Необходимость просматривать ложные срабатывания отнимает рабочее время и ослабляет внимание к тем участкам кода, где в действительности содержатся ошибки.

Ошибки, обнаруживаемые статическими анализаторами весьма разнообразны. Вот, например, список диагностик , которые реализованы в инструменте PVS-Studio. Некоторые анализаторы специализируются на определенной области или типах дефектов. Другие, поддерживают определенные стандарты кодирование, например MISRA-C:1998, MISRA-C:2004, Sutter-Alexandrescu Rules, Meyers-Klaus Rules и так далее.

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

Анализ бинарного кода, то есть кода, который выполняется непосредственно машиной, – нетривиальная задача. В большинстве случаев, если надо проанализировать бинарный код, его восстанавливают сначала посредством дизассемблирования, а потом декомпиляции в некоторое высокоуровневое представление, а дальше уже анализируют то, что получилось.

Здесь надо сказать, что код, который восстановили, по текстовому представлению имеет мало общего с тем кодом, который был изначально написан программистом и скомпилирован в исполняемый файл. Восстановить точно бинарный файл, полученный от компилируемых языков программирования типа C/C++, Fortran, нельзя, так как это алгоритмически неформализованная задача. В процессе преобразования исходного кода, который написал программист, в программу, которую выполняет машина, компилятор выполняет необратимые преобразования.

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

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

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

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

Задача дизассемблирования обычно решается в полуавтоматическом режиме, то есть специалист делает восстановление вручную при помощи интерактивных инструментов, например, интерактивным дизассемблером IdaPro , radare или другим инструментом. Дальше также в полуавтоматическом режиме выполняется декомпиляция. В качестве инструментального средства декомпиляции в помощь специалисту используют HexRays , SmartDecompiler или другой декомпилятор, который подходит для решения данной задачи декомпиляции.

Восстановление исходного текстового представления программы из byte-кода можно сделать достаточно точным. Для интерпретируемых языков типа Java или языков семейства.NET, трансляция которых выполняется в byte-код, задача декомпиляции решается по-другому. Этот вопрос мы в данной статье не рассматриваем.

Итак, анализировать бинарные программы можно посредством декомпиляции. Обычно такой анализ выполняют, чтобы разобраться в поведении программы с целью ее замены или модификации.

Из практики работы с унаследованными программами

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

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

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

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

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

Во-первых, найденные уязвимости надо уметь не только находить, но и объяснять. Если уязвимость была найдена в программе на языке высокого уровня, аналитик или инструментальное средство анализа кода показывают в ней, какие фрагменты кода содержат те или иные недостатки, наличие которых стало причиной появления уязвимости. Что делать, если исходного кода нет? Как показать, какой код стал причиной появления уязвимости?

Декомпилятор восстанавливает код, который «замусорен» артефактами восстановления, и делать отображение выявленной уязвимости на такой код бесполезно, все равно ничего не понятно. Более того, восстановленный код плохо структурирован и поэтому плохо поддается инструментальным средствам анализа кода. Объяснять уязвимость в терминах бинарной программы тоже сложно, ведь тот, для кого делается объяснение, должен хорошо разбираться в бинарном представлении программ.

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

Несмотря на все особенности и сложности проведения статического анализа бинарных программ по требованиям ИБ, ситуаций, когда такой анализ выполнять нужно, много. Если исходного кода по каким-то причинам нет, а бинарная программа выполняет функционал, критичный по требованиям ИБ, ее надо проверять. Если уязвимости обнаружены, такое приложение надо оправлять на доработку, если это возможно, либо делать для него дополнительную «оболочку», которая позволит контролировать движение чувствительной информации.

Когда уязвимость спряталась в бинарном файле

Если код, который выполняет программа, имеет высокий уровень критичности, даже при наличии исходного текста программы на языке высокого уровня, полезно сделать аудит бинарного файла. Это поможет исключить особенности, которые может привнести компилятор, выполняя оптимизирующие преобразования. Так, в сентябре 2017 года широко обсуждали оптимизационное преобразование, выполненное компилятором Clang. Его результатом стал вызов функции , которая никогда не должна вызываться.

#include typedef int (*Function)(); static Function Do; static int EraseAll() { return system("rm -rf /"); } void NeverCalled() { Do = EraseAll; } int main() { return Do(); }

В результате оптимизационных преобразований компилятором будет получен вот такой ассемблерный код. Пример был скомпилирован под ОС Linux X86 c флагом -O2.

Text .globl NeverCalled .align 16, 0x90 .type NeverCalled,@function NeverCalled: # @NeverCalled retl .Lfunc_end0: .size NeverCalled, .Lfunc_end0-NeverCalled .globl main .align 16, 0x90 .type main,@function main: # @main subl $12, %esp movl $.L.str, (%esp) calll system addl $12, %esp retl .Lfunc_end1: .size main, .Lfunc_end1-main .type .L.str,@object # @.str .section .rodata.str1.1,"aMS",@progbits,1 .L.str: .asciz "rm -rf /" .size .L.str, 9

В исходном коде есть undefined behavior . Функция NeverCalled() вызывается из-за оптимизационных преобразований, которые выполняет компилятор. В процессе оптимизации он скорее всего выполняет анализ аллиасов , и в результате функция Do() получает адрес функции NeverCalled(). А так как в методе main() вызывается функция Do(), которая не определена, что и есть неопределенное стандартом поведение (undefined behavior), получается такой результат: вызывается функция EraseAll(), которая выполняет команду «rm -rf /».

Следующий пример: в результате оптимизационных преобразований компилятора мы лишились проверки указателя на NULL перед его разыменованием.

#include void Checker(int *P) { int deadVar = *P; if (P == 0) return; *P = 8; }

Так как в строке 3 выполняется разыменование указателя, компилятор предполагает, что указатель ненулевой. Дальше строка 4 была удалена в результате выполнения оптимизации «удаление недостижимого кода» , так как сравнение считается избыточным, а после и строка 3 была удалена компилятором в результате оптимизации «удаление мертвого кода» (dead code elimination). Остается только строка 5. Ассемблерный код, полученный в результате компиляции gcc 7.3 под ОС Linux x86 с флагом -O2, приведен ниже.

Text .p2align 4,15 .globl _Z7CheckerPi .type _Z7CheckerPi, @function _Z7CheckerPi: movl 4(%esp), %eax movl $8, (%eax) ret

Приведенные выше примеры работы оптимизации компилятора – результат наличия в коде undefined behavior UB. Однако это вполне нормальный код, который большинство программистов примут за безопасный. Сегодня программисты уделяют время исключению неопределенного поведения в программе, тогда как еще 10 лет назад не обращали на это внимания. В результате унаследованный код может содержать уязвимости, связанные с наличием UB.

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

Loading...Loading...