1. Понятия анализа и верификации ПО. Классификация методов анализа ПО. Непротиворечивость, полнота и точность анализа. Проблемы анализа ПО: теорема Тьюринга, теорема Райса.
Анализ ПО — совокупность методов, позволяющих судить о ПО в отрыве от программиста, то есть устанавливать различные свойства ПО как объекта:
- Является ли программа корректной?
- Соответствует ли она спецификации?
- Можно ли ее оптимизировать?
- Насколько быстро она работает?
- Завершается ли она когда-нибудь?
- Есть ли в ней мертвый код?
- ...
Верификация (по версии Википедии) - формальное доказательство соответствия или несоответствия формального предмета верификации его формальному описанию.
Верификация - это статический метод анализа ПО, применяющийся с целью точного определения каких-либо свойств.
Методы анализа ПО делятся на:
- Статические (не требуют запуска кода для анализа, статический анализ и верификация - именно статические методы);
- Динамические (работают на основе результатов запуска кода);
- Гибридные (совмещают оба подхода - например, автоматическая генерация тестов и последующее их выполнение).
Непротиворечивость - (???) свойство формальной системы, заключающееся в невыводимости из неё противоречия. Противоречивая система имеет ряд правил, которые напрямую или косвенно (через ряд дополнительных логических действий) начинают противоречить друг другу. Противоречивость свидетельствует о том, что анализируемая система несовершенна.
Метод верификации характеризуется двумя свойствами:
- soundness: если вы доказали какое-то свойство, значит оно действительно было;
- completeness: если свойство имеет место, ваш метод может его доказать.
Статический анализ тоже обладает двумя свойствами:
- precision (точность): доля истинных результатов среди всех полученных;
- recall (полнота): доля обнаруженных результатов среди всех истинных.
Можно неформально сказать, что
- soundness = 100% precision;
- completeness = 100% recall;
- верификация — частный случай статического анализа.
- Алан Тьюринг(1936 г): Невозможно написать алгоритм, который говорит, завершается Тьюринг-полная программа или нет;
- Генри Райс (1952 г): Невозможно написать алгоритм, который определяет любое нетривиальное свойство Тьюринг-полной программы.
Нетривальное свойство - это такое свойство, которое есть у одних программ и его нет у других. Получается, что какие-либо интересные свойства программы мы получить/узнать не можем.
Есть несколько вариантов решения проблемы:
- Заняться Тьюринг-неполной моделью (Model checking);
- Дополнить недостающую информацию вручную (Дедуктивная и автоматизированная верификации);
- Аппроксимация (сверху/снизу - статический анализ).
2. Основные подходы к статическому анализу ПО. Сигнатурный поиск. Понятие абстрактного синтаксического дерева, другие модели ПО.
- Сигнатурный поиск;
- Анализ на основе типов;
- Абстрактная интерпретация;
- Символьное исполнение;
- Анализ потока данных;
- ...
Сигнатурный поиск - отличный метод анализа, но очень простой и неинтересный, хотя и полезный.
Хороший сигнатурный анализатор — это 10000+ сигнатур. Сигнатуры — примеры написания некорректного кода, с которыми сравнивается анализируемый код, сигнализирующие о том, что и в каких местах может пойти не так.
Самая простая модель — абстрактное синтаксическое дерево (AST). Это конечное помеченное ориентированное дерево, в котором внутренние вершины сопоставлены (помечены) с операторами языка программирования, а листья — с соответствующими операндами.
Откуда берется AST?
- В результате парсинга программы получается дерево разбора;
- Далее из него удаляются все лишние элементы, например, комментарии;
- Получаем AST.
Анализ дерева может происходить при помощи различных паттернов:
- ООП: visitors (описываем поведение в зависимости от типа посещённой вершины);
- ФП: pattern matching.
AST можно дополнять всякими данными про семантику:
- Графы потока данных/управления;
- SSA (static single assignment — каждой переменной значение может быть присвоено всего один раз);
- Графы зависимостей;
- ...
3. Анализ ПО на основе типов. Связь классических систем типов и анализа ПО. Системы типов как простейший вид анализа ПО. Полнота и точность анализа на основе систем типов. Понятие анализа на основе ограничений, типичная структура такого анализа.
Анализ ПО на основе типов, связь классических систем типов и анализа ПО, системы типов как простейший вид анализа ПО
Анализ типов — вид статического анализа у языков, в которых присутствует система статической типизации программы. Анализ типов позволяет выявить свойства программ, связанных с переменными и операциями над ними относительно их типа.
Анализ типов позволяет гарантировать, что:
- сравнивать можно только выражения одного типа;
- условия в if должны быть булевыми;
- только функции можно вызывать;
- только указатели можно разыменовывать;
- ...
Иными словами, анализ типов позволяет заранее выявить часть ошибок, связанных с некорректным обращением с переменной её типа.
Говоря про анализ типов, подразумевают одну из следующих задач:
- Type checking (проверка типов)
- Type interference (вывод типов)
Вывод типов обычно вторичен, и если язык поддерживает систему проверки типов, то вывод типов реализовать относительно просто.
Как и другие анализаторы нетривиальных свойств, достичь абсолютных показателей полноты и точности невозможно, поэтому используется аппроксимация с ориентацией на полноту: все программы, которые компилятор считает корректными относительно системы типов является корректными по факту.
Однако существует множество корректных программ не проходящих проверку типов (slack).
Type soundness — гарантия того, что программа, прошедшая проверку типов на этапе компиляции, не упадет с ошибкой типов во время ее выполнения.
Однако системы типов во многих языках не всегда могут гарантировать это свойство:
- Массивы в Java;
- void*, union в C;
- Reinterpret cast в C++;
- ...
Проверка типов является анализом на основе ограничений/уравнений. Из исходного кода составляется модель, из которой можно составить систему уравнений. Далее данную систему решает решатель.
4. Способы решения задачи присвоения типов. Задача унификации. Алгоритмы решения задачи унификации. Рекурсивные типы и регулярная унификация.
Используя AST анализируемой программы и набор уравнений-правил анализа, можно составить и решить систему уравнений.
Выше приведен предмет правил, по которому каждое выражение программы можно преобразовать в равенство.
На выходе мы хотим получить систему равенств, в котором каждому выражению программы соответствует тип.
Для того чтобы автоматизировать процесс решения, используется формализм решателей унификаторы:
Есть набор равенств вида:
Решение — подбор методом подстановки набора значений таких переменных, при которых верны равенства:
-
$\alpha = f(d(b), b)$ ; -
$\beta = d(b)$ ; -
$γ = b$ .
Примером алгоритма унифицирующего солвера является линейный алгоритм решения задачи унификации (Патерсон и Вегман, 1978 г.). Алгоритм заключается в многократном выполнении последовательности шагов:
- delete: удаляем уравнения вида t = t
- decompose: c(a, b, c) = c(x, y, z) => a = x; b = y; c = z
- conflict: c(x0, .., xN) = d(y0, .., yN) => ошибка
- swap: c(x0, .., xN) = z => z = c(x0, .., xN)
- eliminate: x = t если x не содержится в t, но содержится в других уравнениях => подставляем t вместо x во всех остальных уравнениях
- check: t = f(x0, .., xN) и t содержится в x0,..,xN => ошибка (для рекурсивных типов есть специальная нотация, об этом ниже).
Последовательность повторяется до тех пор, пока не сойдутся типы для всех выражений
Сложность O(N); рекурсивные уравнения запрещены.
Регулярная унификация (Хьюет, 1976 г.) позволяет использовать регулярные рекурсивные термы (те, которые можно записать
через
Для обозначения рекурсивных типов (типов, значение которых зависит от них же) существует особая μ-нотация:
p = alloc; *p = p;⟦p⟧ = &α; ⟦p⟧ = &⟦p⟧;⟦p⟧ = α where α = &α->⟦p⟧ = µα.&α
Алгоритм основан на системе Union-Find (Галлер и Фишер, 1964 г) – система непересекающихся данных, которая при наличии набора элементов позволяет разбить их на непересекающиеся подмножества равных друг другу элементов.
Сложность: O(N ∗ A(N)), где A(N) это обратная функция Аккермана
5. Понятие графа потока управления и моделей на его основе. Понятие чувствительности к потоку управления в анализе ПО. Виды потокочувствительных анализов. Абстрактный домен и абстрактное состояние. Потокочувствительный анализ знаков.
Граф потока управления (CFG) – модель программы, которая представляет все возможные пути ее выполнения; состоит из базовых блоков и переходов между ними.
Анализ может быть:
- Потоко-нечувствительным:
- Типы выражений обычно не зависят от потока выполнения или потока данных программы;
- Уравнения анализа строятся по исходному коду программы (AST).
- Потоко-чувствительным:
- Результат зависит от порядка выполнения операций;
- Уравнения анализа строятся по графу потока управления (CFG).
Абстрактный домен – абстрактная информации, которая описывает конкретные свойства программы в определенный момент выполнения CFG.
- Абстрактная информация для переменной программы
- Может отличаться в разных точках программы
Для работы с абстрактным доменом используются:
eval- абстрактные вычисления;op- абстрактный оператор;JOIN(n)- оператор объединения информации из предшествующих/последующих вершин.
Потокочувствительный анализ знаков – анализ на основе CFG и решётки знаков, заключающийся в поиске наименьшей неподвижной точки (конкретнее описано в последующих вопросах).
6. Введение в теорию решёток. Понятие частично упорядоченного множества. Верхние и нижние грани. Примеры решёток, применимых в анализе ПО. Анализ знака на основе теории решёток.
Для анализа очень здорово, если домен является решеткой.
Решетка — это частично упорядоченное множество (poset), для любых двух элементов которого есть точная верхняя и точная нижняя грани.
У решетки есть максимальный (T) и минимальный (⊥) элементы, а также высота (длина максимально возможного пути от ⊥ до T).
В полностью упорядоченном множестве связи между значениями характеризуются как меньше/больше.
Частично упорядоченное множество (poset) - это множество S, для которого задано частичное отношение порядка
- рефлексивным (
$\forall x \in S : x \subseteq x$ ); - транзитивным (
$\forall x, y, z \in S : x \subseteq y \land y \subseteq z \rightarrow x \subseteq z$ ); - анти-симметричным (
$\forall x, y \in S : x \subseteq y \land y \subseteq x \rightarrow x = y$ );
Верхние и нижние грани:
Иными словами,
С нижней/точной нижней гранью аналогично.
- Булеан (решетка над множеством всех подмножеств S)
- Перевёрнутый булеан
- Плоская решетка
- Решётка обображений
Анализ знака на основе теории решеток:
Иными словами:
- При объявлении переменной её знак неопределён;
- При присваивании переменной какого-либо значения её знак меняется на знак присваиваемого выражения.
Информацию о присвоении знаков нужно объединить (JOIN) с информацией о типах из предшествующих вершин.
7. Теорема о наименьшей неподвижной точке. Понятие монотонного фреймворка. Алгоритмы поиска неподвижной точки. May- и must- анализ.
Обычно в результате анализа мы получаем набор переменных
Эти ограничения можно собрать в одну функцию, тогда они примут вид:
В итоге наша задача при решении - найти наименьший (наиболее точный)
Сама теорема о неподвижной точке звучит так: Для любой монотонной функции
Функция
Если совсем упрощать: "при уточнении входных данных результат не должен становиться менее точным".
Функция нескольких аргументов является монотонной, если она монотонна по каждому аргументу.
Проверить функцию на монотонность (в контексте анализа знаков функция - каждый используемый оператор) можно путём перебора всех таблиц:
Сам монотонный фреймворк представляет собой:
- Набор вершин CFG программы;
- Решётку конечной длины L (представление интересующего нас домена);
- Набор переменных для каждой вершины CFG;
- Набор ограничений для различных видов узлов CFG.
Используя всё вышеперечисленное, можно извлечь ограничения из CFG и решить их, используя алгоритм поиска неподвижной точки.
Для начального
Для начального
В round-robin
Можно использовать структуру CFG, и при расчете каждого его узла учитывать информацию всех предыдущих.
Для этого составляется worklist (
- Расчитывая
$y = f_i(x_1,\dots, x_n)$ ; - Если
$x_i != y$ , то$x_i = y$ и добавляем потомков текущей вершины в$WL$ .
Можно и дальше улучшить алгоритм, особым образом приоретизируя вершины или анализируя иные зависимости вершин друг от друга.
На условной решётке ход работы алгоритма выглядит примерно следующим образом:
В моментах, когда нам необходим ответ, который точно будет включать в себя истинный, используется May-анализ, он же переаппроксимация:
- Решётка обходится снизу вверх, от частных результатов к общим;
- Например, анализ знаков, поиск живых переменных (live variables), обнаружение зависимостей значения текущей переменной (reaching definitions).
И, напротив, Must-анализ, он же недоаппроксимация, используется в обратных случаях:
- На практике рассматривают решётку не с
$\sqsubseteq$ , а с$\sqsupseteq$ ; - Используется при поиске предрасчитанных выражений (availible expressions) и анализе занятости (very busy expressions).
Анализ живости (live variables) служит для поиска живых переменных в различных точках программы, а "мёртвые" переменные можно просто игнорировать во многих задачах анализа/оптимизации, что уменьшает фактическую размерность задач.
Так как живость переменной в какой-либо момент выполнения программы - нетривиальное свойство, его необходимо аппроксимировать.
Будем искать в первую очередь те переменные, которые ТОЧНО мертвы (May-анализ).
В качестве решётки будем использовать булеан от всех переменных программы – таким образом, самым безопасным для нас ответом будет живость всех переменных в рассматриваемой точке программы.
В качестве точек программы будем рассматривать состояния программы ДО вершин CFG с использованием следующих правил:
где
Иными словами:
- При инициализации переменной удаляем её из списка живых;
- При назначении переменной какого-либо значения удаляем её из списка живых, затем добавляем в список все использованные в расчёте переменные;
- При любых других выражениях добавляем в список живых все используемые переменные.
Анализ готовых выражений (Available expressions) служит для поиска рассчитанных ранее выражений (уже рассчитанные выражения можно не пересчитывать повторно).
Так как "готовность" выражения в какой-либо момент выполнения программы - нетривиальное свойство, его необходимо аппроксимировать.
Будем искать в первую очередь те переменные, которые уже точно считались (Must-анализ).
В качестве решётки будем использовать перевёрнутый булеан от всех переменных программы - таким образом, самым безопасным для нас ответом будет отсутствие расчитанных переменных в рассматриваемой точке программы.
В качестве точек программы будем рассматривать состояния программы после вершин CFG с использованием следующих правил:
Иными словами:
- При назначении переменной какого-либо значения удаляем все выражения, в которых эта переменная использовалась;
- При наличии любых других нетривиальных выражений добавляем их в список.
Анализ занятости (Very busy expressions) служит для поиска выражений, которые были гарантированно использованы до интересующей нас точки в программе.
Так как "готовность" выражения в какой-либо момент выполнения программы - нетривиальное свойство, его необходимо аппроксимировать.
Будем искать в первую очередь те переменные, которые уже точно использовались (Must-анализ).
В качестве решётки будем использовать перевёрнутый булеан от всех переменных программы - таким образом, самым безопасным для нас ответом будет отсутствие использованных до рассматриваемой точки программы выражений.
В качестве точек программы будем рассматривать состояния программы ДО вершин CFG с использованием следующих правил:
Иными словами:
- При назначении переменной какого-либо значения удаляем все выражения, в которых эта переменная использовалась, затем добавляем все используемые при расчёте присваиваемого значения переменные;
- При наличии любых других нетривиальных выражений добавляем их в список.
Анализ достижимых значений (Reaching definitions) служит для поиска тех присваиваний/объявлений, которые отвечают за значения переменных в текущей точке программы.
Так как достижимость выражений в какой-либо момент выполнения программы - нетривиальное свойство, его необходимо аппроксимировать.
Будем искать наиболее полный набор определений, пусть среди них и будут лишние(Must-анализ).
В качестве решётки будем использовать булеан от всех присваиваний в программе - таким образом, самым безопасным для нас ответом будут все присваивания.
В качестве точек программы будем рассматривать состояния программы после вершин CFG с использованием следующих правил:
Иными словами:
- При объявлении переменной включаем её в список;
- При присвоении значения переменной удаляем из списка предыдущее объявление/присвоение ей значения и вносим новое;
- Для всех остальных значений оставляем список без изменений.
Распространение констант (Constant propagation) служит для предрасчёта тех значений, которые точно являются константами.
В простейшем виде используется плоская решётка с перечислением всех возможных констант, однако, такая реализация может быть расширена решёткой для знаков.
При дальнейшем расширении (например, для возможности предрасчёта выражений
10. Интервальный анализ. Тривиальная интервальная решётка. Проблемы реализации анализов на основе бесконечных решёток. Widening в применении к решёткам и ограничения, связанные с его применением.
Интервальный анализ - анализ над интервалами значений. Используется во многих полезных вещах:
- Распространение констант;
- Поиск выходов за границы массива;
- Упрощение выражений;
- т.д.
Простейшая интервальная решётка - булеан от всех возможных интервалов в
Основная проблема тривиальной решётки - то, что она бесконечная. Помимо того, что это несёт за собой проблемы с временем работы, все алгоритмы по поиску неподвижной точки не работают (у решётки бесконечная высота). Решение "в лоб", с ограничением размера целых чисел, на практике работает плохо, поэтому необходимо иным образом формировать решётку, "загрубляя" её.
Widening - способ представления бесконечной интервальной решётки в конечном виде путём загрубления:
Иными словами:
-
$\omega$ - некая "загрубляющая" функция, возвращающая конечную решётку, такую, что её стационарная точка - переаппроксимация стационарной точки исходной решётки; -
$\omega (L)$ - конечная, "загрублённая" решётка от бесконечной решётки$L$ .
Один из примеров реализации - загрубление интервалов до ближайших интервалов над
Однако, результаты после использования widening могут получиться слишком неточными из-за переаппроксимации – их можно и нужно уточнить, и для этого используется narrowing.
11. Narrowing в применении к решёткам и ограничения, связанные с его применением. Методы объединения widening и narrowing. Модификации алгоритма решения задачи поиска неподвижной точки с применением widening и narrowing.
Narrowing - Способ уточнения результатов анализа путём применения функции
Иными словами:
- "Спускаемся" по решётке вниз, не выходя за истинную стационарную точку;
- Можем всё же попасть прямо в стационарную точку, можем остаться там же, можем очутиться где-то посередине.
Однако, у narrowing тоже имеется ряд проблем:
- Результат, скорее всего, всё ещё будет переаппроксимацией, хоть и более точной, чем сразу после widening;
- Если исходное множество констант программы
$B$ подобрано плохо, то narrowing будет работать очень долго.
Классический вариант - сначала использовать только widening, затем - только narrowing.
Однако, можно пробовать применять информацию о структуре программы, и, в соответствии с ней, чередовать widening и narrowing - результат, скорее всего, будет точнее и быстрее.
Можно ввести общий widening-оператор:
Используя тот факт, что для любой возрастающей последовательности
Можно ввести общий narrowing-оператор:
Используя тот факт, что для любой нисходящей последовательности
Объединим общий widening и общий narrowing - для
- Если
$l_2 \sqsubseteq l1$ , то используем narrowing ($l_1 \delta l_2$ ); - Иначе используем widening (
$l_1 \nabla l_2$ ).
Подобное совместное использование ускоряет сходимость, увеличивает точность решения, но не работает со стандартными решателями монотонных фреймворков.
12. Понятие чувствительности к пути исполнения. Зависимости по данным и по управлению. Предикаты путей. Интервальный анализ, чувствительный к путям исполнения, проблемы, связанные с ним. Выбор множества предикатов путей в общем случае. Метод уточнения абстракции на основе контрпримеров (CEGAR).
Чувствительность к путям исполнения – это зависимость результатов анализа от пути выполнения программы, то есть от логических условий при ветвлениях.
Анализ может быть:
- Чувствительным к зависимостям по данным (рассмотренные ранее анализы – важны взаимодействия данных);
- Чувствительным к путям исполнения (важны переходы и логические условия в них).
Один из вариантов решения проблемы без полной перестройки исходного графа (всё ещё хочется работать с какой-либо версией
CFG) является добавление оператора assume в пути при ветвлениях.
Assume очень похож на assert:
Assert- это проверка определённого условия в месте расположения оператора (и падения в случае невыполнения)...- ...
Assumeв свою очередь - это утверждение о корректности задаваемого логического выражения.
В итоге при ветвлениях просто происходит добавление предиката assume с соответствующим логическим условием в нужную
ветвь ветвления, например, для if (x == 5):
- В ветвь с выполняющимся условием добавляется
assume(x == 5); - В ветвь с невыполняющимся условием добавляется
assume(!(x == 5)).
При внедрении assume в интервальный анализ нужно добавить правила обработки для операций сравнения:
-
$eq([a, b], [c, d]) = [a, b] \sqcap [c, d]$ ; -
$gt([a, b], [c, d]) = [a, b] \sqcap [c, +\infty]$ ; -
$lt([a, b], [c, d]) = [a, b] \sqcap [−\infty, d]$ ; -
$⟦assume(P_1 \& \& P_2)⟧ = ⟦assume(P_1)⟧ \sqcap ⟦assume(P_2)⟧$ ; -
$⟦assume(P_1 || P_2)⟧ = ⟦assume(P_1)⟧ \sqcup ⟦assume(P_2)⟧$ ; - ...
С использованием assume анализ всё равно может некорректно обрабатывать некоторые случаи после слияния ветвей:
В данном случае анализ игнорирует flag, из-за чего считает, что в момент закрытия файла он может находиться в любом из
двух состояний. Значит, нужно добавить её в анализ.
Сделать это можно при помощи решётки состояний для каждого контекста – таким образом можно выразить зависимость
состояния файла (open/close) от значения переменной flag (0/1):
В общем случае, множество предикатов путей – это комбинация из условий переходов и их отрицаний. Таким образом, полная
комбинация имеет размер
В итоге:
- Размер решётки растёт экспоненциально;
- Алгоритмы не очень хорошо к такому адаптированы;
- Вообще, можно сказать, что это – NP-полная задача.
Можно решить проблему выбора множества предикатов при помощи метода уточнения абстракции на основе контрпримеров (CEGAR aka Counterexample-guided abstraction refinement):
- Запустить анализ с какой-то абстракцией;
- Если нет проблем - закончили;
- Если есть проблемы - генерируем абстрактный контрпример (путь, на котором происходит нарушение);
- Ищем реальный контрпример (реальные значения с таким же путём);
- Если нашли контрпример - закончили;
- Используем контрпример для уточнения абстракции;
- Возвращаемся к шагу 1, но с уточнённой абстракцией.
Искать конкретный контрпример можно, например, с использованием SMT-солверов. Для улучшения абстракции нужно вытащить неверные факты из контрпримера и добавить переменные, от которых они зависят.
Также для упрощения задачи можно:
- Запустить вспомогательные анализы (поиск мёртвого кода, распространение констант);
- Разбить переменные на кластеры;
- Нормализовать предикаты:
x > n + k=>x > n + k&&n < x - k&&k < x - n
13. Понятие межпроцедурности в анализе ПО. Пессимистичный межпроцедурный анализ и проблемы, связанные с ним. Полная подстановка тела функции и проблемы, связанные с ней. Понятие полного графа потока управления для программы и анализ на его основе.
Межпроцедурный анализ - анализ всей программы с учётом всех вызовов. Также есть внутрипроцедурный анализ, который анализирует только тело одной функции.
Один из вариантов перехода к межпроцедурному анализу - "пессимистичный" анализ:
- Анализируем функции отдельно;
- Для каждой функции выбирается самый пессимистичный вариант.
Из-за того, что выбирается самый пессимистичный вариант, снижается полнота (recall). Имеются проблемы с рекурсией.
Альтернативный вариант - полностью подставлять тело другой функции при вызове. Такой подход грозит большими бедами с производительностью и не решает проблемы с рекурсией.
Можно использовать полный граф управления:
- Находим граф управления для каждой функции;
- Объединяем их в один большой граф (он же - полный граф).
Необходимо не забыть про:
- Передачу параметров в функцию;
- Возврат значений из функции;
- Вернуть состояние локальных переменных.
Однако, при вызове функции из нескольких других есть возможность уйти в другую функцию после выполнения, так как в подобной реализации отсутствует контекстная чувствительность.
14. Понятие контекстной чувствительности. Метод ограниченного клонирования процедур, его ограничения и параметризация. Методы улучшения контекстной чувствительности.
Контекстная чувствительность - чувствительность анализа функции к тому, в каком месте она была вызвана.
Контекстная чувствительность должна решать проблемы вызова функции из нескольких различных мест, типа такой:
Для решения проблемы контекстной чувствительности можно клонировать CFG вызываемой функции в каждое место вставки, однако, это вызовет те же проблемы, что и полная подстановка тела функции.
Можно клонировать логически - использовать решётку Contexts -> Lift(States):
- Contexts - все контексты, из которых вызывается функция;
- Lift(States) используется для того, чтобы описать недостижимые состояния через низ решётки.
Вообще, Contexts – это множество списков элементов из множества контекстов вызова функции, а длину контекста в случае, например, рекурсии.
Можно менять – таким образом, можно балансировать между размером множества контекста и качеством анализа.
Так как сам подход очень похож на Path sensitivity, для его решения можно пользоваться теми же методами, e.g. CEGAR'ом. Более того, для улучшения качества анализа можно увеличивать размер множества контекстов.
15. Метод контекстной чувствительности на основе входных данных, его анализ в сравнении с методом ограниченного клонирования процедур. Понятие аппроксимации функции в программе. Проблема внешних функций и варианты её решения.
До этого описывались попытки поставить функцию в зависимость от пути исполнения, хотя куда логичнее поставить её в зависимость он подаваемых ей на вход данных. Такой подход называется методом контекстной чувствительности на основе входных данных.
В наиболее общем случае:
- Contexts = States;
- Используем решётку States -> Lift(States).
То есть моделируем не вызовы, а само вычисление.
Для упрощения работы можно аппроксимировать функцию:
- Моделировать функцию программы как функцию из входных аргументов в состояние всех переменных;
- Если брать только последнюю вершину графа функции, то можно вообще ограничиться только входными аргументами и возвращемым в этой вершине результатом.
Внешние функции aka внешние зависимости - это вызовы функций, не описанных непосредственно в программе:
- Стандартные библиотеки языков;
- Сторонние библиотеки;
- Системные вызовы;
- ...
Их можно описать:
- Функциями-заглушками:
- Полными;
- Частичными;
- Контрактами:
- Как в верификации - описывают предусловия и постусловия для каждой функции;
- Хорошо подходят для анализов на основе логики;
- Автоматами:
- Что-то среднее между контрактом и функцией;
- Используют собственные языки;
- принципиально заточены под конкретный анализ и конкретные модули.
16. Анализ указателей. Анализ псевдонимов. Анализ цели. Анализ формы. Направленный и ненаправленный анализ указателей.
Указателем является ссылка на область памяти (memory cells).
Любой анализ над значениями существенно усложняется с появлением в программе указателей, т.к. любые модификации памяти не предсказуемы.
Сами указатели являются нежелательными, т.к. подразумевают довольно долгую операцию обращения к памяти. Большинство компиляторов пытается свести количество указателей к минимуму.
Если в программе присутствуют указатели, используются анализы указателей, которые учитывают набор возможных ячеек памяти, используемых при выполнении программы.
Как правило, реальные анализы нечувствительны к потоку (задача чувствительности к потоку просто неразрешима).
Анализы можно подразделять на 2 подтипа (было объяснено не очень понятно):
- направленный - подразумевается, что, если указатели A и B указывают на одну ячейку памяти, и указатели A и C указывают на одну ячейку памяти, это не значит, что B и C указывают на одну ячейку; короче данный тип анализов использует включения множеств;
- ненаправленный - подразумевает, что B и C указывают на одну ячейку; грубо говоря данный тип анализов использует сравнение множеств.
Далее приведены возможные формулировки анализа указателей.
Анализ псевдонимов выявляет для каждой пары указателей, ссылаются ли они на одну и ту же ячейку памяти.
May-формулировка: выявляет для каждой пары указателей, могут ли они ссылаться на одну и ту же ячейку памяти.
Как альтернатива, есть менее точный анализ множеств псевдонимов (alias set analysis), разбивающий все указатели на множества, которые могут указывать на одни и те же ячейки.
Анализ цели выявляет для каждого указателя, на какую ячейку памяти он ссылается.
May-формулировка: выявляет для каждого указателя, на какие ячейки памяти он может ссылаться.
Данный анализ эквивалентен анализу псевдонимов и может быть построен на его основе (и наоборот, на основе анализа цели можно построить анализ цели).
Анализ формы выявляет структуру области памяти, достижимой по указателям. Данный анализ строит модель в виде графа, где указывается примерное расположение ячеек памяти.
По сути является аналогом анализа цели с другим описанием, но с более формальным обозначением ячеек.
Алгоритм реализует нахождения наборов псевдонимов (alias sets) - наборов указателей, ссылающихся на одну и ту же ячейку памяти. Является ненаправленным анализом (направление присваивания не имеет значения).
Для анализа уравнения строятся следующим образом:
X = alloc-Xприсваивается индексированный указатель на выделенную ячейку памяти; каждая аллокация рассматривается как отдельный индекс;X = Y- присвоение одного указателя другим;XиYравны;X = &Y-Xявляется указателем наY;*X = Y-Xявляется указателем на некоторую ячейку памяти, равнойY;X = *Y-Yявляется указателем на некоторую ячейку памяти, равнойX;
Анализ оперирует именно ячейками памяти, т.к. указатели могут ссылаться друг на друга или самих себя, что не стабильно.
Для решения уравнений используется унифицирующий солвер (как при анализе типов). На выходе получаются равенства, обозначающие множества псевдономов.
Также, как мы помним по анализу типов, унифицирующий солвер учитывает рекурсию, т.е. может учитывать указатели на самих себя.
Алгоритм имеет сложность
Проблемой является то, что алгоритм работает с множествами: он объединяет в одно множество указатели, которые могут ссылаться на одну и ту же ячейку памяти или указатель; при этом, если указатель из другого множества ссылается на один из указателей этого множества, то алгоритм посчитает, что он ссылается на каждый указатель этого множества. Это снижает точность анализа.
18. Алгоритм Андерсена, его реализация на основе кубического фреймворка. Контекстно-чувствительный анализ указателей.
Cubic framework - солвер для решения уравнений следующего вида:
где
Задача состоит в поиске множеств, в которых находится определенный элемент.
В нашем случае рассматриваются множества указателей в программе и наборы переменных, для которых определяется, как в нем расположены элементы.
Солвер использует универсальный алгоритм, имеющий сложность
Структура решения представляет собой ориентированный ациклический граф (DAG), в вершинах которого хранится:
- множество токенов;
- отображение из токенов в множества пар переменных.
Каждой переменной
Каждой дуге соответствует отношение включения (стрелка означает, что одно множество включает в себя другое множество).
Уравнения обрабатываются по одному, структура всегда содержит минимальное решение.
Решение уравнений вида
Решение уравнений вида
Если при добавлении дуги в граф получился цикл (что означает равенство множеств):
- Соответствующие вершины сливаются вместе
- Сливаются множества токенов
- Сливаются множества пар для каждого токена
В отличие от алгоритма Стенсгаарда, алгоритм Андерсена является направленным анализом и не сваливает все в сеты.
Если в алгоритме Стенсгаарде использовалось равенство множеств, то для алгоритма Андерсена используется включение, что позволяет множествам лишь частично пересекаться.
Для анализа уравнения строятся следующим образом:
X = alloc- индексированный указатель на выделенную ячейку памяти относится к множеству токенов переменнойX; каждая аллокация все еще рассматривается как отдельный индекс;X = Y- множество токеновXвключает в себя множество токеновY;X = &Y- указатель наYотносится к множеству токеновX;*X = Y- если указатель на некоторую ячейку памяти, относится к множеству токеновX, то к его множеству токенов относится множество токеновY;X = *Y- если указатель на некоторую ячейку памяти, относится к множеству токеновY, то его множество токенов относится к множеству токеновX;
Алгоритм Андерсена является более точным, но гораздо медленнее алгоритма Стенсгаарда.
Алгоритмы Стенсгаарда и Андерсена работают на всю кодовую базу целиком, а потому они не могут делать анализ в конкретной точке программы.
Можно использовать монотонный фреймворк, построив решетку булеан над парами всех ячеек памяти, определяемые наборами аллокаций (можно сопоставить с дугами в points to задаче). Нижней границей решетки является граф без дуг, верхней – все возможные пары указателей.
На практике алгоритм является очень медленным и редко используется.
Данный анализ основан на том, что в языке с системой типизации 2 указателя на 2 разных типа скорее всего не будут одинаковыми.
Благодаря этому факту можно анализировать указатели на разные типы отдельно, используя любой из рассмотренных нами ранее анализов, значительно уменьшая пространство поиска.
В реальных системах анализа строится стек анализов. Если на этапе, соответствующего одному из анализов был получен точный ответ, что 2 указателя – разные, то анализ останавливается. Если анализ не дал точного ответа, то запускается следующий по стеку анализ.
Нами на лекциях был рассмотрен интервальный анализ, позволявший для каждой переменной определить набор значений, который они могут принимать. Данный анализ не может учитывать отношения между переменными.
Существует реляционные численные домены, которые позволяют анализировать линейные зависимости пар переменных.
Convex Polyhedra – домен выпуклых многогранников, где отношения между переменными описываются 2 способами:
- системой линейных равенств или неравенств;
- перечислить все возможные значения переменных:
На основе ограничений составляется N-мерный многогранник, где N – размер множества анализируемых переменных
Данный домен учитывает варианты, при которых область допустимых значений может быть бесконечной (Infinity Polyhedra):
Представление в виде уравнений также называют матричным.
Представление в виде вершин также называют генераторным.
Генераторное представление для Infinity Polyhedra:
Элементами решетки является пара
Отношение включения согласно геометрическому смыслу:
- Пересечение — геометрическое пересечение
- Объединение — выпуклая оболочка геометрического объединения
- Как и с диапазонами, для каждого оператора свой eval и нужно проводить widening.
В оригинальной статье про Convex Polyhedra используется 40 операторов. На лекциях были рассмотрены 2 из них: объединение и пересечение.
В случае с генераторным представлением можно без проблем объединить все множества вершин, дуг и прямых из 2 исходных доменов, и получить новый домен (возможно избыточный, но корректный).
Пересечение из генераторного представления сделать почти невозможно (сложная операция), но зато это легко сделать при помощи матричного представления путем слияния всех уравнений.
Поэтому составляются многогранники двумя способами: одни операции легко сделать с уравнениями, другие с множеством значений.
Трансформация из одной формы представления в другую возможно при помощи алгоритма Черниковой, алгоритмическая сложность
равна
Если у нас есть оба представления – получить хотя бы одно всегда просто.
Позволяет приводить ограничения в форме
Между всеми парами и 0 переменных формируется разреженная матрица имеющихся зависимостей.
Также эту матрицу можно рассматривать как матрицу смежности некоторого графа (граф потенциалов). По сути меняется просто отображение: переменные не столбцы и строки матрицы, а узлы графа.
Основные операции над матрицами довольно просты (например отношение меньше/больше выражается просто применение этой операции на соответствующие элементы матриц), таким образом на основе этого домена можно без проблем строить решетки.
Главной фишкой является то, что операции над матрицами нереально оптимизированы как программно, так и аппаратно, что делает данный анализ очень быстрым.
Единственным минусом является ограниченная форма, в которой нужно представить уравнения.
В отличие от предыдущего домена позволяет выражать ограничения в форме
Т.к. теперь рассматриваются одновременно порицательные и положительные значения, матрица увеличивается в 4 раза, но в целом это приемлемо. В остальном решение ищется также, как и для гексагонального домена.
Все домены названы в соответствии с тем, как они выглядят на графике.
Над любым выпуклым доменом можно построить булеан, который будет N-отдельных фигур. Соответственно, когда требуется провести любую операцию (например пересечение), будет проводиться операция этих фигур друг с другом.
Редко используется из-за высокой алгоритмической сложности.
Строится на основе 2 любых доменов
Говорят в каких-то задачах она эффективна.
Является булеаном над доменом диапазонов, но вместо хранения интервалов по отдельности, хранится в виде графовой структуры - линейных решающих диаграмм (LDD).
Фишка в том, что существует множество эффективных алгоритмов для LDD.
Элементом домена является конгруэнтное соотношение
В данном случае
Дальше буквально вынос мозга, Беляев сам плохо понимает
По сути получается набор разбросанных по пространству точек. Для каких-нибудь for (i in 0..10 step 2) подходит хорошо.
















