Вебинар: Использование статических анализаторов кода при разработке безопасного ПО - 19.12
Для проверки качества программного обеспечения приходится применять много разных инструментов. В частности, к ним относятся инструменты статического и динамического анализа. В данной статье мы попробуем разобраться, почему одной методологии, будь то статический или динамический анализ, может оказаться недостаточно для проведения комплексного анализа программ и почему лучше совместно использовать эти два подхода.
Наша команда много пишет о пользе статического анализа и о том, какую выгоду может принести его использование в вашем проекте. Мы любим искать ошибки в различных проектах с открытым исходным кодом с помощью нашего инструмента, тем самым популяризируя методологию статического анализа кода. В свою очередь эта методология помогает улучшать качество программ, делает их более надёжными и уменьшает количество потенциальных уязвимостей. Наверное, каждый, кто непосредственно работает с кодом, получает внутреннее удовлетворение от исправления ошибок. Но даже в том случае, если вы не получаете выброс эндорфинов от того, что вам удалось найти (и исправить) очередной баг, то, скорее всего, вам нравится мысль о том, что у вас получилось сократить денежные затраты на разработку путём того, что статический анализатор помог более продуктивно использовать время программистов. Подробнее о том, какую пользу в денежном эквиваленте может принести использование инструмента статического анализа, можно узнать из данной статьи. Приблизительные расчёты показаны на примере использования анализатора PVS-Studio, но подобное можно проэкстраполировать и для других статических анализаторов, имеющихся на рынке.
Из написанного выше можно сделать вывод, что смысл статического анализа в том, чтобы как можно раньше находить ошибки в исходном коде программ, тем самым уменьшая денежные затраты на их исправление. Но для чего тогда нужен динамический анализ, и почему использование только одного из этих двух подходов может оказаться недостаточным? Давайте несколько формализуем понятия статического и динамического анализа и дадим им более чёткие определения, попутно стараясь ответить на поставленные выше вопросы.
Статический анализ кода — это процесс выявления ошибок и недочетов в исходном коде программ. Для его выполнения не нужно запускать программу, весь анализ будет выполнен на имеющейся кодовой базе. Самая ближайшая аналогия, которую можно провести со статическим анализом кода, это так называемый процесс code review, только автоматизированный (выполняемый программой-роботом).
К основным преимуществам статического анализа можно отнести:
К объективным недостаткам статического анализа кода относятся следующие факторы:
Отметим, что использование статического анализа кода не ограничивается только выявлением ошибок в программе. Например, используя инструменты статического анализа, можно получать рекомендации по оформлению кода. Некоторые статические анализаторы позволяют проверять, соответствует ли исходный код принятому в компании стандарту оформления кода. Имеется в виду контроль количества отступов в различных конструкциях, использование пробелов/символов табуляции и так далее. Помимо этого, статический анализ можно использовать для подсчёта метрик. Метрика программного обеспечения — это мера, позволяющая получить численное значение некоторого свойства программного обеспечения или его спецификаций. Если вас интересует, каким ещё образом можно использовать статический анализатор кода, вы можете обратиться к этой статье.
Динамический анализ кода – это способ анализа программы непосредственно при её выполнении. Отсюда следует, что из исходного кода в обязательном порядке должен быть получен исполняемый файл, то есть нельзя таким способом проанализировать код, содержащий ошибки компиляции или сборки. Динамический анализ выполняется с помощью набора данных, которые подаются на вход исследуемой программе. Поэтому эффективность анализа напрямую зависит от качества и количества входных данных для тестирования. Именно от них зависит полнота покрытия кода, которая будет получена по результатам тестирования.
Используя динамическое тестирование, можно получить следующие метрики и предупреждения:
К основным преимуществам динамического анализа кода относят:
Перечислим недостатки, которые присущи динамическому анализу кода:
Динамическое тестирование наиболее важно в тех областях, где главным критерием является надежность программы, время отклика или потребляемые ресурсы. Это может быть, например, система реального времени, управляющая ответственным участком производства, или сервер базы данных. В таких областях любая допущенная ошибка может оказаться критической.
Возвращаясь к вопросу о том, почему использование только одного варианта анализа может оказаться недостаточным, давайте приведём пару довольно тривиальных примеров ошибок в коде, с которыми справляется один подход к анализу программ, но который не по зубам другому, и наоборот.
Это пример кода из проекта Clang:
MapTy PerPtrTopDown;
MapTy PerPtrBottomUp;
void clearBottomUpPointers() {
PerPtrTopDown.clear();
}
void clearTopDownPointers() {
PerPtrTopDown.clear();
}
Здесь статический анализ укажет на то, что тела двух функций абсолютно идентичны. Конечно, нельзя с абсолютной уверенностью говорить, что если тела функций одинаковы, то это ошибка. Однако существует вероятность, что это был результат копипаста, совмещённый с невнимательностью разработчика, что уже и приведёт к непредвиденному поведению программы. В данном случае, внутри метода clearBottomUpPointers должен был быть осуществлён вызов PerPtrBottomUp.clear. В приведённом примере динамический анализ кода не сможет увидеть ничего подозрительного, ведь с его точки зрения это абсолютно рабочий код.
Другой пример. Представим, что имеется следующий код:
size_t index = 0;
....
if (scanf("%zu", &index) == 1)
{
....
DoSomething(arr[index]);
}
В приведённом выше коде может произойти выход за границу массива arr в случае, если пользователем программы будет введено значение, превышающее максимально допустимый индекс массива arr. На первый взгляд можно предположить, что статическому анализатору будет не по зубам обнаружение подобных ошибок. Ведь узнать, какое число введёт пользователь, можно только при фактическом выполнении программы. Однако современные статические анализаторы реализуют внутри себя достаточно сложную логику, в том числе опирающуюся и на механизм аннотирования. Аннотации предоставляют различную информацию об аргументах, возвращаемом значении и внутренних особенностях функций, которые не могут быть выяснены в автоматическом режиме. Программист путём аннотирования известных и широко используемых функций даёт понять анализатору, чего можно ожидать от того или иного вызова функции. Таким образом статические анализаторы могут мыслить в терминах "небезопасных входных данных" (tainted data) и отслеживать, может ли полученное значение привести к ошибке.
Из приведённого выше примера кода анализатор может понять, что переменная index получила своё значение из проаннотированной функции scanf. Основываясь на том, что значение переменной index может получиться большим чем размер массива arr, анализатор выдаст предупреждение. Оно будет сообщать о том, что перед обращением к значению массива arr по индексу index, эту переменную следует предварительно проверить. Например, в приведённом ниже коде перед обращением к значению массива по индексу, производится соответствующая проверка переменной index. Анализатор это понимает и не выдаёт предупреждение.
size_t index = 0;
....
if (scanf("%zu", &index) == 1)
{
....
if (index < arraySize)
DoSomething(arr[index]);
}
Подобное диагностическое правило, предупреждающее о том, что данные, полученные извне, были использованы без предварительной проверки, уже реализовано в статическом анализаторе PVS-Studio и имеет номер V1010.
При правильном наборе входных данных, динамические анализаторы так же могут обнаружить описанную выше проблему. Некоторое множество ошибок всё-таки может находиться как динамическим, так и статическим анализатором, но существуют и такие ошибки, которые поддаются детектированию только одним подходом.
Взглянем на пример следующего кода:
void OutstandingIssue(int number)
{
int array[10];
unsigned nCount = MathLibrary::MathFunctions::Abs(number);
memset(array, 0, nCount * sizeof(int));
}
Abs это некий статический метод из используемой нами библиотеки MathLibrary, доступа к исходному коду которой у нас нет. Если в этот метод закралась ошибка, и при определённом значении number может вернуться число, превышающее размер массива arr, то в функции memset произойдёт выход за границу массива. Как статический анализатор может понять, что метод Abs может вернуть число, которое может превышать размер массива? Аннотирование незнакомого метода Abs из никому неизвестной библиотеки MathLibrary произведено не было - всех методов не проаннотируешь. Выдавать предупреждение на все места, где статический анализатор не уверен во входных данных - это дорога в один конец с огромным количеством ложных срабатываний (подробнее о философии статического анализатора PVS-Studio можно прочитать в этой статье). В свою очередь, динамический анализатор (при правильном наборе входных данных) смог бы легко указать на то, что в данной программе есть ошибка при работе с памятью.
Целью данной статьи не является сравнение техник статического и динамического анализа. Нет одной технологии, которая бы позволяла выявлять ошибки всех типов. Один вид анализа не способен полностью заменить другой. Для повышения качества требуется использовать инструменты разного типа, чтобы они дополняли друг друга. Надеюсь, приведённые выше примеры ошибок только подтверждают это.
Не хочется казаться чрезмерно предвзятым и как-то особенно выделять технику статического анализа, но в последнее время именно о ней всё больше говорят и, что более важно, внедряют в свои CI процессы многие компании. Статический анализ выступает как один из этапов так называемого барьера или ворот качества (quality gates) к построению надёжного и качественного программного обеспечения. Рекомендую обратить внимание на интересную лекцию по этой теме тут. Нам кажется, что статический анализ через пару лет станет стандартной практикой при разработке программ, такой же, как когда-то стало юнит-тестирование.
В заключение ещё раз отмечу, что динамический и статический анализ - это просто две разные методологии, которые дополняют друг друга. В конечном счёте смысл использования всех этих техник сводится к поднятию качества программного продукта и сокращению затраченных денежных средств при его разработке.
Дополнительные ссылки:
0