вторник, 16 сентября 2014 г.

Statiс code analysis 1

В качестве предисловия.
Некоторое время назад я готовил / проводил семинар в рамках сертификации компании на соответствие стандарту PA DSS (http://en.wikipedia.org/wiki/PA-DSS). Одним из требований, предъявленных аудитором, было использование при разработке ПО автоматизированных инструментов анализа программного кода. Конкретной темой моего семинара был статический анализ и его возможности.
Теперь я хочу поделиться собранным материалом.


СТАТИЧЕСКИЙ АНАЛИЗ КОДА: ЧТО ЭТО ТАКОЕ?
В основном, определения и термины, от общих к частным. Это позволит в дальнейшем лучше понимать суть материала.

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

Динамический анализ кода (англ. dynamic code analysis) – анализ, проводимый при помощи выполнения программ на реальном или виртуальном процессоре. Утилиты динамического анализа могут требовать загрузки специальных библиотек, перекомпиляцию программного кода. Некоторые утилиты могут инструментировать исполняемый код в процессе исполнения или перед ним. Для большей эффективности динамического анализа требуется подача тестируемой программе достаточного количества входных данных, чтобы получить более полное покрытие кода. Также требуется позаботиться о минимизации воздействия инструментирования на исполнение тестируемой программы (включая временные характеристики).

В области программирования под инструментированием понимают возможность отслеживания или установления количественных параметров уровня производительности программного продукта, а также возможность диагностировать ошибки и записывать информацию для отслеживания причин их возникновения. Измерение в виде инструкций кода обычно используется для отслеживания работы определённого компонента системы (например, инструкции, выводящие логи на экран). Когда приложение содержит инструментальный код, им можно управлять при помощи специальных инструментов-утилит. Измерение необходимо для оценки производительности приложения. Методы измерений делятся на два основных типа: измерения на основе исходного кода и измерения на основе двоичного кода. В области программирования измерение означает возможность измерить приложение с точки зрения следующих параметров:
  • трассировка кода – получение информационных сообщений о выполнении приложения на всём протяжении его работы;
  • отладка программы и (структурированная) обработка исключений – отслеживание и исправление ошибок программистов в приложении ещё на стадии его разработки;
  • счётчики производительности – компоненты, позволяющие отслеживать уровень производительности приложения;
  • регистраторы событий – компоненты, позволяющие получать уведомления и отслеживать ключевые события при выполнении приложения;
  • отслеживание выполнения технологии, управляемые сервисы и опыт по собиранию, объединению, анализу и представлению уровней использования приложения, шаблонов и методик использования;
  • профилирование – набор методик отслеживания производительности кода.
Основные задачи динамического анализа: обнаружение ошибок памяти, выделение и освобождение памяти, memleaks, ситуации гонки потоков и взаимоблокировок, стэки вызовов и пр.

Статический анализ кода (англ. static code analysis) анализ ПО, производимый без реального выполнения исследуемых программ. В большинстве случаев анализ производится над какой-либо версией исходного кода, хотя иногда анализу подвергается какой-нибудь вид объектного кода (например, P-код).

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

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

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



В ЧЁМ ПОЛЬЗА?
Очевидно, чтобы начать использовать что-то новое, оно должно быть полезным. Далее – немного о профите от SCA.

Чем раньше ошибка в коде приложения будет обнаружена, тем дешевле стоит её исправление. Отсюда следует вывод, что наиболее дёшево и просто ошибка может быть устранена в процессе написания кода. А ещё лучше, если ошибка вовсе не будет написана.
Вот только захотел сделать ошибку, так сразу хлоп себя по рукам и код написан уже правильно. Но так как-то не получается. Подход «надо писать без ошибок» всё равно не работает.
Даже высококвалифицированный программист, который никуда не торопится, совершает ошибки, начиная от простейших опечаток и заканчивая логическими ошибками в алгоритмах. Здесь срабатывает закон больших чисел. Вот вроде бы в каждом конкретном операторе «if» сделать ошибку невозможно. Но на 200, 500, 1000 примеров одна ошибка да и найдётся.
Общая мысль: как бы ни были опытны разработчики, ошибки всё равно появляются в коде. Эти ошибки невозможно прекратить делать. Но со многими из них можно успешно бороться на гораздо более раннем этапе, чем это делается обычно.

Обычно самым первым уровнем обороны от ошибок является создание юнит-тестов на вновь написанный код. Иногда тесты пишутся ещё до кода, который они будут проверять (test driven development). Однако у юнит-тестов есть свой ряд недостатков, которые я не буду подробно рассматривать, т. к. они и так известны программистам. Не всегда легко создать юнит-тест для функции, которая требует большой предварительной подготовки данных. Юнит-тесты становятся обузой, если требования к проекту быстро меняются. Тесты отнимают много времени на написание и сопровождение. Тестами не всегда просто покрыть все ветвления. А ещё вы можете получить в подарок монолитный проект, в котором юнит-тестов просто не существует и не планировалось. Не отрицая огромной пользы юнит-тестов, я считаю, что хотя это хороший оборонительный рубеж, его можно и стоит существенно укрепить.

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

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


ЧТО МОЖНО ОБНАРУЖИТЬ?
Конечно, SCA не является панацеей и лекарством от всех бед в разработке ПО. Немного о том, какого рода дефекты можно обнаружить при статическом анализе.
  • Явные случаи неопределённого поведения: неинициализированные переменные, обращение к null-указателям и т. д.
    int x;
    int y = x + 2; // variable "x" might not have been initialized
  • Нарушение схемы использования ресурса. Например, для каждого open нужен close. И если файловая переменная теряется раньше, чем файл закрывается, анализатор может сообщить об ошибке.
  • Типичные сценарии, приводящие к недокументированному поведению. Стандартная библиотека языка Си известна огромным количеством неудачных технических решений. Некоторые функции, например, gets, в принципе небезопасны. sprintf и strcopy  безопасны лишь при определённых условиях. И, например, статический анализатор может обратить внимание программиста на такой код:
void doSomething (const char* x) {
    char s[40];
    springf(s.  "[%s]", x); // sprintf в локальный буфер, возможно переполнение
    ...
}
  • Сценарии, мешающие кроссплатформенности.
Object *p = getObject();
int pNum =reinterpretCast<int>(p); // на x86-32 верно, на x64 часть указателя будет потеряна, нужен size_t
  • Прочие ошибки по невнимательности. Например, многие функции из стандартных библиотек не имеют побочного эффекта, и вызов их как процедур не имеет смысла. Так что ошибочным будет код.
std::string s;
s.empty(); //вероятно, вы хотели s.clear()?
  • Ошибки, возникающие при копировании кода. Да-да, многие (все) разработчики иногда копируют частично повторяющиеся фрагменты не пишут с нуля, а размножают и исправляют. Получаются ошибки наподобие:
dest.x = src.x + dx;
dest.y = src.y + dx; // ошибка, надо dy!
  • Неизменный параметр, передаваемый в функцию. Иногда подобные «брошенные» параметры – признаки изменившихся требований к программе: когда-то параметр был задействован, но сейчас он уже не нужен. В таком случае разработчик может вообще избавиться от этого параметра и от связанной с ним логики.
void doSomething (int n, boolean flag) { // flag всегда равен true
    if (flag) {
        // какая-то логика
    } else {
        // код есть, но не задействован
    }
}

doSomething (n, true);
...
doSomethig (10, true);
...
doSomethig(x.size(), true);

Нужно отметить, что большинство современных компиляторов выводят сообщения о том, что код, будучи формально правильным, скорее всего, содержит ошибку. Это тоже простейший статический анализ. Но у компилятора есть много других немаловажных характеристик – скорость работы, качество машинного кода, удобство… Да и «зашумлять» консоль ложными тревогами – не самое удачное решение. Поэтому компиляторы проверяют код лишь на простейшие, очевидные ошибки. А для более серьёзного исследования кода есть специализированное ПО, запускаемое не постоянно, а лишь время от времени.


КАК ЭТО ДЕЛАЕТСЯ?
Доверяй, но понимай. Вопрос в том, как же выполняется статический анализ.

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

Комментариев нет:

Отправить комментарий