Содержание к диссертации
Введение
Глава 1. Поиск ошибок в исходном коде 10
1.1. Использование статического анализатора в жизненном цикле разработки программ 10
1.2. Ошибки в исходном коде 12
1.3. Корректность анализа 15
1.4. Синтаксические анализаторы 16
1.5. Обзор существующих семантических анализов 17
1.6. Анализатор Svace 27
Глава 2. Анализируемый язык 32
2.1. Промежуточное представление Svace 32
2.2. Описание языка svaceO 32
2.3. Структурная операционная семантика языка 36
2.4. Функция на языке svaceO 40
Глава 3. Внутрипроцедурный анализ 42
3.1. Используемый подход 42
3.2. Идентификаторы значений и ссылки 44
3.3. Передаточные функции 50
3.4. Анализ циклов 53
3.5. Корректность анализа 55
3.6. Сильные и слабые обновления 65
3.7. Слияние состояний 67
3.8. Пример анализа 68
3.9. Массивы, объединения, приведения типов 70
Глава 4. Детекторы 73
4.1. Атрибуты 73
4.2. Критерий выдачи з
4.3. События 78
4.4. Вспомогательные атрибуты 80
4.5. Разыменование нулевого указателя 80
4.6. Обратный анализ 83
4.7. Сопоставление атрибутов ссылкам 85
4.8. Чувствительность к путям 87
Глава 5. Межпроцедурный анализ 92
5.1. Резюме функции 92
5.2. Создание резюме функции 93
5.3. Трансляция резюме в контекст вызова 97
5.4. Отложенное слияние для параметров функций 100
5.5. Трассы атрибутов 102
5.6. Параметризованные атрибуты 103
5.7. Удаление резюме 104
5.8. Анализ конструкторов и деструкторов 105
Глава 6. Реализация 108
6.1. Инструмент Svace 108
6.2. Трансляция Си и Си++-кода в LLVM 110
6.3. Вспомогательные алгоритмы 112
6.4. Создание резюме 115
6.5. Оценка времени анализа 115
6.6. Оценка выданных предупреждений 117
Заключение 122
Список сокращений и условных обозначений 124
Список литературы
- Корректность анализа
- Структурная операционная семантика языка
- Анализ циклов
- Разыменование нулевого указателя
Введение к работе
Актуальность темы исследования.
Дефекты и ошибки в программном обеспечении (ПО) в большой степени влияют на качество ПО. Ошибки времени выполнения программы снижают надёжность программы, приводя к сбоям в работе, отказам в обслуживании. Некоторые ошибки - уязвимости защиты - приводят к возможности выполнения произвольного кода злоумышленником. Дефекты исходного кода программы, такие, как, например, мёртвый или избыточный код, могут не проявляться во время её работы, но тем не менее снижают качество исходного кода.
Сложность поиска ошибок сильно выросла из-за взрывного роста размеров ПО. Количество строк кода в современных дистрибутивах ОС Tizen и Android достигает десятков миллионов, а в дистрибутиве ОС Debian GNU/Linux - миллиарда. Многие ошибки остаются не найденными в течение многих лет после выпуска ПО. Из-за этого, а также из-за доступности программ через компьютерные сети растет ущерб от проявления ошибок и от их эксплуатации злоумышленником. Популярность библиотек с открытым исходным кодом приводит к возможности эксплуатации единственной ошибки на миллионах инсталляций самых разных программ.
В настоящее время известно множество методов поиска ошибок: экспертный аудит кода, ручное тестирование, дедуктивная верификация, динамический анализ (мониторинг выполнения программы, фаззинг и др.). Все эти методы имеют различные области применимости - не исключают, а дополняют друг друга. Одним из признанных методов поиска, широко используемым в индустрии, является статический анализ исходного кода программ.
Статический анализ выдает список предупреждений о местах потенциальных ошибок в исходном коде программы. Выданное анализом предупрежде-ние называется истинным, если в используемой анализом модели поведения программы действительно может возникнуть искомая ошибочная ситуация, и ложным в противном случае. Преимуществом статического анализа является одновременный анализ многих путей исполнения программы и поиск ошибок на редко выполняющихся путях, которые плохо покрываются при общесистемном тестировании или динамическом анализе. Среди недостатков нужно отметить наличие ложных предупреждений, от которых невозможно полностью
избавиться. Ложные предупреждения связаны с тем, что точное вычисление необходимых свойств программы при статическом анализе является алгоритмически неразрешимой задачей.
Для промышленного применения статического анализатора нужно выполнить следующие требования:
масштабируемость: анализ больших программных систем (миллионы строк кода) должен длиться не более 4-6 часов (ночная сборка);
качество: высокий уровень истинных предупреждений (50-70%) вместе с пропуском небольшого количества ошибок;
понятность: объяснение причин, по которым найденная ситуация является ошибкой. Объяснение включает в себя подробное текстовое описание ошибки, указание места проявления ошибки, описание цепочки событий, приведших к ошибке, а также дополнительную информацию, которая будет полезной пользователю для понимания ошибки;
полнота: поддержка популярных языков программирования, поддержка реализации детекторов для поиска известных типов ошибок разной критичности;
расширяемость: простое добавление анализа новых типов ошибок, в том числе специфичных для конкретной программы.
Ключевой сложностью при разработке статического анализа является поиск компромисса между уровнем истинных срабатываний и количеством пропущенных ошибок при условии масштабируемости анализа для заданного размера программ. С одной стороны, попытка выдачи предупреждений о всех потенциальных местах ошибок приводит к потоку ложных срабатываний. С другой стороны, максимальная консервативность анализа, проявляющаяся в срабатывании анализа только для ситуаций, в которых есть практически полная уверенность в ошибке, приводит к пропуску значительной доли реальных ошибок. Следовательно, необходимо применять нестрогий анализ (unsound analysis), который может пропускать некоторые ошибочные ситуации, регулируя точность алгоритмов анализа с помощью набора эвристик.
Используемые эвристики необходимо регулярно пересматривать при повышении требований к масштабируемости анализа, потому что с ростом размера анализируемой программы увеличивается вероятность обнаружения ранее не встреченных программных конструкций; неправильная работа эвристик для этих конструкций может серьезно ухудшить качество анализа. Кроме того, нужно повышать точность используемых алгоритмов анализа потока данных и управления за счёт большей точности алгоритмов анализа указателей, анализа циклов, учёта контекста вызова функции и влияния отдельных путей выполнения. Таким способом разрабатываются ведущие мировые промышленные анализаторы (инструменты Coverity CMC, Klocwork К10, HP Fortify, инструмент Svace Института системного программирования РАН).
Статический анализатор Svace ищет ошибки в исходном коде программ, написанных на языках Си, СиН—Ь, Java и Си^. Предыдущие версии анализатора демонстрировали промышленное качество анализа при обработке программ из сотен тысяч строк исходного кода. Однако при переходе к анализу ПО размером в миллионы строк кода уровень истинных срабатываний для многих типов ошибок упал до 15-20%. Актуальной является задача разработки алгоритмов анализа потока данных и управления программы, обеспечивающих высокое качество статического анализа при поиске ошибок в сверхбольших программах, и их реализация в инструменте Svace.
Цели и задачи диссертационной работы: разработка алгоритмов внут-рипроцедурного анализа потока данных и управления, алгоритмов межпроцедурного контекстно- и потоково- чувствительного анализа, предназначенных для поиска дефектов в исходном коде программ, написанных на языках Си и СиН—Ь, и их реализация в инструменте Svace. Разработанные алгоритмы должны обеспечивать качество анализа, соответствующее современным требованиям, выдвигаемым к промышленным статическим анализаторам, и масштабируемость анализа для программ в миллионы строк исходного кода.
Для достижения поставленных целей были сформулированы и решены следующие задачи:
Разработка внутрипроцедурных алгоритмов анализа, позволяющих на основе вычисленной ими информации о потоке данных и управления программы выполнять поиск широкого класса дефектов; доказательство кор-
ректности алгоритмов.
Разработка межпроцедурных алгоритмов анализа на основе предложенных внутрипроцедурных алгоритмов, обеспечивающих контекстную и потоковую чувствительность анализа.
Разработка детекторов для часто встречающихся критических ошибок и дефектов исходного кода, использующих предложенные алгоритмы анализа, в том числе критерия выдачи предупреждений на основе вычисленной детектором информации.
Реализация разработанных алгоритмов в инструменте статического анализа Svace.
Научная новизна. В работе получены следующие основные результаты, обладающие научной новизной:
Разработан алгоритм внутрипроцедурного анализа функции, интегрирующий анализ указателей и нумерацию значений и обеспечивающий возможность поиска широкого класса дефектов. Доказана корректность разработанного алгоритма.
Разработан алгоритм межпроцедурного контекстно- и потоково- чувствительного анализа функций, основанный на алгоритме создания резюме функции, которое обобщает результаты внутрипроцедурного анализа функции, и применения созданного резюме при обработке вызова функций.
Разработанные алгоритмы реализованы в статическом анализаторе Svace для программ на языках Си и СиН—\-.
Теоретическая и практическая значимость. Предложены алгоритмы внутрипроцедурного анализа функции для задачи поиска дефектов. На основе внутрипроцедурного анализа функции предложен алгоритм масштабируемого межпроцедурного контекстно-чувствительного анализа.
Все предложенные алгоритмы были реализованы в статическом анализаторе Svace. Кроме того, на основе предложенных алгоритмов реализованы алгоритмы детекторов конкретных типов дефектов - разыменования нулевого указателя, отсутствия освобождения ресурсов, выхода за границы массивов,
неинициализированных переменных. Экспериментальные результаты анализа больших программных систем подтвердили масштабируемость реализованных алгоритмов при высоком качестве анализа - более 60% истинных срабатываний критических детекторов и в разы возросшее количество предупреждений по сравнению с предыдущей версией анализатора. Разработанный анализатор продемонстрировал масштабируемость и качество анализа на уровне лучших мировых аналогов и внедрён для промышленного использования в компании Samsung.
Положения, выносимые на защиту:
алгоритм внутрипроцедурного анализа функции, интегрирующий анализ указателей и нумерацию значений и обеспечивающий возможность поиска дефектов разыменования нулевых указателей, выхода за границы массивов, использования неинициализированных переменных, отсутствия освобождения ресурсов (в том числе освобождения памяти), повторной блокировки семафора, несогласованность конструкторов и деструкторов СиН—\-классов, ошибок использования библиотечных функций;
алгоритм создания резюме функции, обобщающий результаты внутрипроцедурного анализа функции и обеспечивающий потоковую чувствительность межпроцедурного анализа;
алгоритм анализа инструкций вызова функций, обеспечивающий масштабируемый контекстно-чувствительный межпроцедурный анализ;
инструмент статического анализа, реализующий разработанные алгоритмы для программ на языках Си и СиН—\-.
Апробация результатов. Основные результаты диссертации докладывались на следующих конференциях:
-
IEEE Seventh International Conference on Software Testing, Verification and Validation (ICST) (Cleveland, Ohio, USA, 2014);
-
XXI Международная научная конференция студентов, аспирантов и молодых учёных «Ломоносов-2014» (Москва, Россия, 2014);
3. Открытая конференция по компиляторным технологиям (Москва, Россия, 2015).
Публикации. Материалы диссертации опубликованы в 9 печатных работах, из них 6 работ опубликованы в изданиях, входящих в перечень рецензируемых научных изданий ВАК РФ -], 3 статьи в сборниках трудов конференций [-]. В статье ] личный вклад автора заключается в описании возможностей расширения анализа на примере разработанных детекторов. В работе ] личный вклад состоит в описании ядра анализатора, межпроцедурного анализа и взаимодействия ядра и детекторов. Совместная работа ] посвящена описанию инструмента Svace, личный вклад автора состоит в описании подсистемы анализа исходного кода на языках Си и СиН—\-. Статья ] является переводом статьи []. В статье ] личный вклад автора заключается в описании ядра анализатора Svace и детекторов разыменования нулевых указателей.
Личный вклад автора. Все представленные в диссертации результаты получены лично автором.
Структура и объем диссертации. Диссертация состоит из введения, 6 глав, заключения, библиографии и 2 приложений. Общий объем диссертации 137 страниц, из них 131 страница текста. Библиография включает 72 наименования.
Корректность анализа
Каждая функция анализируется независимо от других, не используется какая-либо информация о контексте вызова функции. Информация распространяется только от вызываемой функции к вызывающей.
Все описываемые далее анализаторы построены на основе использования резюме. Основные отличия конкретных анализаторов заключаются в способе проведения внутрипроцедурного анализа, алгоритме создания и применения резюме и в способе обхода графа вызовов (работа с рекурсивными функциями). При создании резюме необходимо выбрать правильный баланс между точностью и количеством сохраняемой информации. Чем больше информации будет сохранено, тем более точный анализ можно построить. Но одновременно размер структур данных для хранения резюме во многом определяет скорость анализа. При анализе программ требуется хранить резюме для многих функций, поэтому слишком большой размер резюме сделает невозможным их одновременное хранение в оперативной памяти. Сохранение же на диск замедлит анализ и также зависит от размера резюме.
Анализ на основе резюме имеет ряд особенностей, обеспечивших его популярность. Анализ имеет естественную контекстную чувствительность, т. к. каждое резюме применяется для конкретного контекста вызова. Такой анализ имеет высокий потенциал для распараллеливания программы, что актуально, т. к. в настоящее время даже бюджетные машины имеют несколько процессорных ядер. Распараллеливание основано на том, что многие функции могут анализироваться независимо в соответствии с графом вызовов. Причём распараллеливание осуществляется на уровне целой функции, то есть нет необходимости использовать специальные многопоточные алгоритмы для анализа отдельной функции, а также не требуется передавать информацию между такими функциями. Кроме этого, каждая функция анализируется ограниченное количество раз (один раз для ациклических графов вызовов), благодаря чему анализ хорошо масштабируется.
Консервативный анализ находит приближенное решение задачи поиска дефектов, включающее в себя все ошибки определённого типа, которые могут произойти при выполнении программы. Т. е. такой анализатор не пропускает реальные дефекты, но может выдавать ложные предупреждения.
Анализатор, описанный в [19], основан на использовании графа чтения-записи2 [20, 21]. Граф чтения-записи является абстракцией для анализа указателей и позволяет изменять баланс между точностью и стоимостью анализа. В графе различаются чтение и запись в память. На основе графа можно создавать компактные резюме для функции.
В анализаторе все циклы заменяются на функции с хвостовой рекурсией, в результате чего все функции становятся ациклическими. В отличие от большинства анализаторов, описываемых в этом разделе, здесь каждая функция может анализироваться более одного раза. В анализаторе реализовано множество детекторов, включая поиск утечек памяти и ресурсов, разыменования нулевых указателей, переполнения массива.
Отличительной чертой анализатора является возможность изменять точность всего анализа. Базовым анализом является анализ, осведомлённый о потоке управления3. Такой анализ производится для линейной последовательности инструкций, полученной топологическим упорядочиванием инструкций графа потока управления для ациклических функций. Анализ будет различать, когда одна инструкция доминирует над другой. Но анализ условных выражений производится не точно - вначале идут инструкции одной ветки условного выражения, после чего - инструкции другой ветки. Точность анализа можно увеличивать в различных направлениях. Например, добавлять чувствительность
В статье [19] приводится результаты оценки инструмента для 41 проекта. Размер большинства проектов не превышает 25 тысяч строк кода. Самый большой проект, balsa, имеет размер 110 тысяч строк кода. Время анализа для большинства проектов измерялось секундами, и для balsa составило 43 секунды. К сожалению, время анализа не включает в себя время на чтение исходных файлов, их разбор и построение промежуточного представления. Осуществлялся поиск разных видов ошибок, в том числе разыменований нулевых указателей, переполнения массивов, утечки памяти и ресурсов. Каждое предупреждение выдавалось, только если оно было дополнительно подтверждено с помощью символьного выполнения.
Всего инструмент выдал 221 предупреждение, из них 38 оценили как истинные. Т. е. уровень истинных срабатываний 17%. Можно сделать вывод, что использование консервативного анализа не является приемлемым из-за низкого процента истинных срабатываний. Кроме этого, нет гарантии, что даже такой уровень истинных срабатываний удастся сохранить при переходе к анализу проектов больших размеров.
Существуют инструменты, гарантирующие поиск в программе всех дефектов определённого типа. Если в программе есть дефект, то инструмент обязан выдать предупреждение о дефекте. Если инструмент не выдаёт предупреждение, то гарантируется, что в проанализированном проекте нет дефектов определённого типа. К таким инструментам относятся Astree [22, 23], Polyspace Verifier [24].
В построении подобных инструментов достигнут существенный прогресс. Инструменты выдают немного ложных срабатываний, а для некоторых проектов вообще не выдаётся ложных срабатываний. Но в настоящий момент использование таких анализаторов ограничено проектами средних размеров. Также существенно ограничиваются конструкции, которые допустимо использовать в программе. В частности, для анализа с помощью Astree в программе не должно быть рекурсивных вызовов процедур, динамического выделения памяти, обрат 21 ных goto (безусловных переходов на метки, определённые ранее), параллельного выполнения, сложных структур данных [25].
Верификаторы кода используют в программном обеспечении, где стоимость ошибки чрезмерно высока. Для таких программ важна гарантия отсутствия ошибок. Но для большинства программ этого не требуется. Если в проекте есть дефект, то это не является поводом откладывать выпуск продукта.
Структурная операционная семантика языка
Рассматривается задача анализа только для некоторых состояний на входе в процедуру языка svaceO. Разрешёнными состояниями будут такие состояния, где множество всех достижимых значений, начиная с параметров процедуры, не является алиасами.
Будем использовать абстрактное состояние (АС) для того, чтобы выражать множество конкретных состояний, являющихся его конкретизацией.
Задача анализа заключается в создании пар: абстрактное состояние и множество анализируемых путей, где множество путей - это конечное непустое множество путей на графе потока управления, следующих из точки входа функции в некоторую точку (одну и ту же для всех элементов этого множества).
Результат анализа будет корректным, если для любого пути исполнения, начинающегося для разрешённого состояния из точки входа, и принадлежащего множеству анализируемых путей, его конкретное состояние в текущей точке должно принадлежать конкретизации абстрактного состояния.
В работе [37] было показано, что использование эвристик об отсутствии алиасов среди входных параметров является приемлемым для поиска дефектов в исходном коде. Это предположение выполняется для большинства функций. А нарушение предположения не ведёт к росту количества выдаваемых ложных пРеДУпреждений.
Конкретные состояния определены в разделе 2.3. Обозначим П : RU М — С - множество конкретных состояний. Обозначим Ф - множество абстрактных состояний. Абстрактные состояния будут определяться функциями VCLIR, VCLIA и pt, которые будут описаны в разделах 3.2.2, 3.2.3.
Для анализа построим граф потока управления функции на языке svaceO. В качестве вершин будем использовать инструкции, а не базовые блоки. Выполняется обход вершин графа потока управления. С каждым ребром графа ассоциируется абстрактное состояние анализа. Анализ продвигает состояние по графу потока управления. Если вершина имеет несколько входящих рёбер, то выполняется операция объединения состояний (раздел 3.7). Ациклические графы обходятся в топологическом порядке.
Анализ циклов рассматривается в разделе 3.4. Для каждого цикла производится несколько итераций обхода тела цикла. Для рёбер, выходящих из цикла, сохраняются все абстрактные состояния, соответствующие отдельным итерациям, и в конце анализа цикла производится объединение этих абстрактных состояний.
Опишем причины, по которым для анализа функции не используются такие подходы, как символьное выполнение программы и итеративный анализ потока данных. При использовании символьного выполнения даже в функции без циклов количество возможных путей растёт экспоненциально относительно числа вершин в графе потока управления. Поэтому в инструментах, использующих символьное выполнение (например, PREfix [27]), количество путей ограничивается некоторой константой, и используются эвристики для выбора путей. В результате даже простой код в функциях, содержащих множество путей, может быть не проанализирован.
Анализ потока данных [63] позволяет проанализировать все пути внутри функции для всех возможных входных данных. Отказ от анализа потока данных был сделан по следующим причинам:
Текущая версия Svace использует три итерации обхода цикла. Для анализа потока данных в большинстве случаев трёх итераций недостаточно, особенно если используются решётки с большой высотой. Большее количество итераций замедлит анализ. Для сходимости анализа потока данных используемые решётки должны иметь небольшую высоту. В используемом алгоритме нет требования к высоте решётки, а также к её конечности. Если производить анализ потока данных над определениями переменных, то в передаточных функций необходимо будет при прохождении через определение переменной обновлять все ссылки на эти определения. Обход всех ссылок займёт значительное количество времени. Для более эффективного удаления ссылок необходимо будет запоминать все возможные ссылки, что усложняет анализ. Описываемый анализ основан на идентификаторах значений, имеющих свойство мобильности - после создания идентификаторов значений свойства программы можно выражать в виде функций над идентификаторами значений. Нет необходимости изменять ссылки на идентификаторы значений после прохождения через определения переменных, а также в других случаях.
Для того, чтобы определить, что фиксированная точка достигнута, необходимо сравнить текущее состояние анализа с предыдущим, на что может потребоваться значительное количество времени. Дополнительно необходимо запоминать предыдущее состояние.
Анализ циклов
Рассмотрим ссылку а в точке слияния путей. Пусть v\ = val\(a) - идентификатор значения ссылки на одном пути, a v = val (a) - идентификатор значения на другом. Пусть v% = joinval(v 1, 2) идентификатор значения на выходном ребре. Будет вызван оператор объединения для всех атрибутов, ассоциированных с V\ и г 2- Если г і является предзначением, то ничего неизвестно про его свойства до начала выполнения функции. Поэтому свойства атрибутов будут описываться верхним элементом решётки Т. В этом случае для любого атрибута результатом слияния будет Т U Attr(v2) = Т, т. е. произойдёт потеря информации. В случае внутрипроцедурного анализа такой анализ приемлем, т. к. рассматриваются все контексты вызова функции. Проблема возникает в том случае, когда результат, описываемый идентификатором г з, записывается в память, видимую на стороне вызова, где значения предзначений будут известны.
На рис. 5.3 показан пример кода, где происходит потеря информации. Атрибут Valuelnterval используется для анализа интервала целочисленных значений (раздел 4.4). При анализе функции set ничего не известно про значения р. Поэтому значение Valuelnterval для р в начале функции будет [—оо; +оо]. В точке слияния путей (1) новое значение для р будет [—оо; +оо] U [11; 11] = [—оо; +оо], т. е. информация о том, что в функции переменной р может быть присвоено значение 11, будет потеряна. В результате с идентификатором значения для переменной х в точке (2) будет ассоциировано значение интервала [—оо; +оо].
Для более точного анализа предложен механизм отложенного слияния. Если дополнительно запоминать слияния, произошедшие с предзначениями, то можно вызывать оператор объединения для атрибутов для фактических параметров и значений, используемых внутри функции. Благодаря этому улучшается степень контекстной чувствительности алгоритма. 101 void set(int p, int flag) { void use(int a) { if (flag) int x = 10; p = 11; set (&x, a); //(1) //(2)... } } Рис. 5.3. Мотивация для отложенного слияния идентификаторов значений Будем использовать непересекающиеся множества JV и SV, определённые в разделе 3.2.2. Множество JV используется для обозначения составных идентификаторов значения, созданных с помощью функции joinval. Множество SV обозначает простые идентификаторы значений.
Выделим подмножества из множества Vol: PV для обозначения предзна-чений. Будем использовать функцию parts, которая для каждого идентификатора значения возвращает множество идентификаторов значений, из которых он был создан: J {v} если v Є SV] parts{v) = у part{v\) U part(v2) если v = joinval{v\,V2). Для механизма отложенного слияния потребуется две функции: delay и ival. Функция ival используется для создания идентификатора значения, обозначающего свойства всех внутренних значений.
Пусть trans обозначает карту трансляции идентификаторов значения из резюме в сопоставленные идентификаторы значения вызывающей функции. В момент трансляции резюме в контекст вызова запомненные предзначения заменяются на соответствующие идентификаторы значения на стороне вызова: delay (у) = {trans(w)\w Є delay(v)}. Объединение атрибутов производится для атрибутов соответствующих элементов контекста вызова delay (у) и атрибутов резюме для ival(v).
Это позволяет более точно производить объединение атрибутов для отдельных детекторов. Рассмотрим механизм на примере рис. 5.3. При использовании отложенного слияния для функции set будет создан идентификатор значения со ссылками на предзначение vrjp для р и г»ц, при этом Valuelnterval(vdp) = [—00; +00], Valuelnterval(vn) = [11; 11]. В контексте вызова функции set Valuelnterval(vx) = [10; 10], новое значение для х будет г = joinval(vx,Vn). Значение Valuelnterval для г будет вычислено как Valuelntervaluse(r) = Valuelntervaluse(vx) U V aluelnterval set(vi\) = [10; 10] U [11; 11] = [10; 11]. При этом, для операции слияния используются атрибуты как контекста функции use, так и резюме для функции set. Для данного примера, где неизвестны возможные значения параметра а, значение интервала для переменной ж, равное [10; 11], будет наиболее точным. Удаление 10 или 11 из интервала делает анализ некорректным.
От статического анализатора требуется не только найти ошибку, но и объяснить, почему найденная ситуация является ошибкой. Для этой цели используются трассы атрибутов. Трасса атрибута является односвязным списком, в котором каждый элемент хранит точку в программе, где менялось значение атрибута, и поясняющее сообщение. Детектор принимает решение о выдаче предупреждения на основе состояния атрибутов. Для этих атрибутов детектор дополнительно показывает трассу, т. е. точки в программе, где атрибут распространялся.
Трассы полезны и для внутрипроцедурных детекторов, но особенно важны для поиска межпроцедурных дефектов. На рис. 5.4 показан пример предупреждения разыменования нулевого указателя, найденный для проекта Android 5.0.2 (external/linuxools-perf/perf-3.12.0/tools/perf/builtin-script.c:914).
Для улучшения контекстной чувствительности могут использоваться параметризованные атрибуты: такие атрибуты, которые ссылаются на идентификаторы значений. С помощью этих атрибутов сохраняется информация о взаимосвязи идентификаторов значений. Во время создания резюме и его трансляции в контекст вызова ссылки на формальные параметры в атрибутах будут заменены на фактические параметры в соответствии с картой трансляции trans.
Простейший параметризованный атрибут содержит только одну ссылку. Обозначим простой атрибут как Рагат. Между идентификаторами значения устанавливается взаимосвязь, если идентификатор значения v\ имеет значение атрибута, равное г 2, т. е. Param{v\) = г 2- Семантика взаимосвязи зависит от конкретного детектора. Более сложные параметризованные атрибуты могут содержать множество ссылок.
Параметризованные атрибуты удобно использовать для обработки кодов ошибок функций. Многие функции в случае неудачного завершения возвращают некоторый код ошибки. Если возвращаемое значение функции равно этому коду ошибки, то некоторое действие не было произведено функцией.
Например, функция pthread_mutex_lock стандартной библиотеки Си изменяет состояние семафора на заблокированное. В случае удачного завершения функция возвращает нулевое значение. Ненулевое значение обозначает код ошибки, в этом случае семафор не был заблокирован. На рис. 5.5 показан типичный пример использования функций блокировки семафора. Если результат res вызова функции pthread_mutex_lock не равен нулю, то семафор mutex не был заблокирован. Поэтому в точке, помеченной на рисунке как ( ), семафор находится в разблокированном состоянии. Анализ, не учитывающий кодов возврата, будет считать, что в точке ( ) семафор заблокирован. Это приведёт к некорректному анализу, в частности, может быть выдано ложное срабатывание об отсутствии вызова pthread_mutex_unlock для всех путей внутри функции use.
Для решения проблемы введём атрибут ErrLock, который будем ассоциировать с переменной для кода возврата из функции. Значение атрибута будет обозначать блокируемый семафор. Для кода из примера выше ErrLock(vres) = vmutex- В точке сравнения результата с нулём анализ переведёт семафор в разблокированное состояние.
Разыменование нулевого указателя
К Си-проектам было отнесено ПО, написанное преимущественно на языке Си. Табл. А.1 содержит названия проектов, размер их исходного кода, размер сгенерированного биткода LLVM и время анализа. Размер биткода приведён, т. к. скорость анализа во многом зависит от размера внутреннего представления, которое генерируется из биткода LLVM. Размер сгенерированного биткода не пропорционален размеру Си-файлов и зависит от используемых конструкций языка (особенно макросов).
Здесь и далее при приведении данных о размере исходного кода для определения количества строк кода использовалась утилита sloccount [72]. Подсчи-тывался только размер файлов, компиляция которых при сборке исходной программы была перехвачена Svace (некоторые временные файлы, например, генерируемые скриптами configure служебные файлы для определения свойств машины пользователя, не анализируются). Округление производилось до тысяч строк кода отбрасыванием дробной части.
Как видно из результатов, анализ даже таких огромных проектов, как ОС Android 5.0.2, занимает менее 5 часов, т. е. анализ можно использовать во время ночной сборки. Средняя скорость анализа для огромных проектов ниже, чем скорость для анализа Си-проектов, но выше, чем скорость анализа СиН—Ь-проектов. На основе приведённых данных можно сделать вывод, что время анализа растёт линейно с ростом размера проекта. Во многом скорость анализа определяется соотношением Си и Си++-кода. Проект Android 5.0.2 содержит значительную часть СиН—Ь-кода, и его анализ производится медленнее, чем анализ Tizen 2.3, где доля СиЬ—Ь-кода ниже.
В табл. А.6 содержатся результаты замеров скорости анализа Tizen 2.3 при использовании разного количества потоков выполнения. Использование 16 потоков вместо двух ускоряет анализ почти в 4 раза (3,89). Использование 32 потоков вместо 16 даёт дополнительный прирост производительности в 7%. Анализ производился на сервере server4-32-264 с 16 ядрами процессора, реализующего технологию HyperThreading.
В разделе приводится оценка количества выданных предупреждений для 32 проектов размером в сотни тысяч строк кода и 5 проектов размером в миллионы строк кода. Методика оценивания приведена в 6.6.1.
Для оценки количества истинных срабатываний использовалась следующая методика. Оценка производилась независимо по разным группам предупреждений.
В одну группу предупреждений включались срабатывания, выданные похожими детекторами. Для каждой группы производилась случайная выборка предупреждений. Применялся следующий алгоритм: 1. для каждого предупреждения вычислялся хэш на основе имени файла, номера строки и типа предупреждения; 2. все предупреждения упорядочивались по хэшу; 3. выбирались первые N элементов из получившегося упорядоченного списка, где N - требуемый размер выборки. Если несколько предупреждений имеют одинаковый хэш, то все предупреждения включались в выборку; 4. все элементы выборки оценивались вручную на истинные, ложные и непонятные; 5. вычислялся процент истинных срабатываний как , где Т - количество истинных.
Описанная выше методика имеет следующие преимущества: не обязательно оценивать все предупреждения, достаточно оценить только часть; постоянный хэш для предупреждения удобно использовать, если анализатор меняется. В этом случае при повторном запуске изменённого инструмента большинство уже размеченных предупреждений снова попадёт в выборку. При сравнении двух версий инструмента общие предупреждения размечаются только один раз.
Оценка результатов, выданных для операционной системы Tizen 2.3, показана в табл. Б.1. Оценка результатов, выданных для операционной системы Android 5.0.2, показана в табл. Б.2. Для всех предупреждений с количеством больше 50 оценивалась случайная выборка, содержащая минимум 50 предупреждений. Для большинства детекторов более 70% срабатываний являются истинными.
Наиболее плохой результат у детекторов поиска утечек памяти (58%). Основная причина ложных срабатываний была связана с недостаточно точным моделированием ситуаций, когда функция возвращает структуру данных, одно из полей которой описывает код ошибки. Если значение поля равно некоторому выделенному значению, то внутри функции произошла ошибка, и память не была выделена.