Инструментальные коды C/С++ с использованием LLVM
Я только что прочитал о проекте LLVM и что его можно использовать для статического анализа на кодах C/С++ с использованием анализатора Clang, который является интерфейсом LLVM. Я хотел знать, можно ли извлекать все обращения к памяти (переменные, локальные, а также глобальные) в исходном коде с помощью LLVM.
Есть ли встроенная библиотека в LLVM, которую я мог бы использовать для извлечения этой информации.
Если нет, предложите мне, как писать функции, чтобы сделать то же самое (существующий исходный код, ссылка, учебник, пример...)
Из того, что я подумал, я должен сначала преобразовать исходный код в LLVM bc, а затем применить его для анализа, но точно не знаю, как это сделать.
Я попытался выяснить, какой IR я должен использовать для своей цели (Дерево синтаксиса абстрактного текста (AST) или промежуточное представление LLVM SSA (IR).), но не мог понять, какой из них использовать.
Вот что я пытаюсь сделать.
Учитывая любую программу C/С++ (например, приведенную ниже), я пытаюсь вставить вызовы в какую-либо функцию до и после каждой инструкции, которая читает/записывает в/из памяти. Например, рассмотрим приведенную ниже программу на С++ (Account.cpp)
#include <stdio.h>
class Account {
int balance;
public:
Account(int b) {
balance = b;
}
int read() {
int r;
r = balance;
return r;
}
void deposit(int n) {
balance = balance + n;
}
void withdraw(int n) {
int r = read();
balance = r - n;
}
};
int main () {
Account* a = new Account(10);
a->deposit(1);
a->withdraw(2);
delete a;
}
Итак, после инструментария моя программа должна выглядеть так:
#include <stdio.h>
class Account {
int balance;
public:
Account(int b) {
balance = b;
}
int read() {
int r;
foo();
r = balance;
foo();
return r;
}
void deposit(int n) {
foo();
balance = balance + n;
foo();
}
void withdraw(int n) {
foo();
int r = read();
foo();
foo();
balance = r - n;
foo();
}
};
int main () {
Account* a = new Account(10);
a->deposit(1);
a->withdraw(2);
delete a;
}
где foo() может быть любой функцией, например, получить текущее системное время или увеличить счетчик.. так далее. Я понимаю, что для вставки функции, как указано выше, мне нужно сначала получить ИК-порт, а затем запустить контрольно-пропускной пункт на ИК-канале, который будет вставлять такие вызовы в ИК-порт, но я действительно не знаю, как его достичь. Пожалуйста, предложите мне примеры, как это сделать.
Также я понимаю, что как только я скомпилирую программу в IR, было бы очень сложно получить сопоставление 1:1 между моей оригинальной программой и инструментальным IR. Итак, можно ли отразить изменения, внесенные в ИК (из-за инструментария) в исходную программу.
Чтобы начать работу с пропуском LLVM и как сделать это самостоятельно, я рассмотрел пример прохода, который добавляет проверки времени выполнения к нагрузкам и хранилищам LLVM IR, пропускную способность (хранение) хранилища SAFECode (http://llvm.org/viewvc/llvm-project/safecode/trunk/include/safecode/LoadStoreChecks.h?view=markup и http://llvm.org/viewvc/llvm-project/safecode/trunk/lib/InsertPoolChecks/LoadStoreChecks.cpp?view=markup). Но я не мог понять, как запустить этот пропуск. Пожалуйста, дайте мне шаги, как запустить этот прогон в какой-либо программе, скажем выше Account.cpp.
Ответы
Ответ 1
Прежде всего, вам нужно решить, хотите ли вы работать с clang или LLVM. Они работают на очень разных структурах данных, которые имеют преимущества и недостатки.
Из вашего редкого описания вашей проблемы, я порекомендую перейти на проходы оптимизации в LLVM. Работа с IR сделает его намного проще для дезинфекции, анализа и инъекции кода, потому что это то, что он должен был сделать. Недостатком является то, что ваш проект будет зависеть от LLVM, который может или не может быть проблемой для вас. Вы можете вывести результат с помощью C-сервера, но это не будет использоваться человеком.
Другим важным недостатком при работе с прохождением оптимизации является то, что вы также потеряете все символы из исходного исходного кода. Даже если класс Value
(подробнее об этом позже) имеет метод getName
, вы должны никогда полагаться на него, чтобы содержать что-либо значимое. Это помогло вам отлаживать ваши пропуски и ничего больше.
Вы также должны будете иметь базовое понимание компиляторов. Например, это немного необходимо знать о основных блоках и static единая форма назначения. К счастью, это не очень сложные концепции, чтобы учиться или понимать (статьи в Википедии должны быть адекватными).
Прежде чем вы сможете начать кодирование, сначала вам нужно сделать некоторое чтение, чтобы здесь появилось несколько ссылок:
-
Обзор архитектуры: быстрый архитектурный обзор LLVM. Дает вам хорошее представление о том, с чем вы работаете, и подходит ли LLVM для вас.
-
Заголовок документации. Здесь вы можете найти все ссылки ниже и более. Обратитесь к этому, если я что-то пропустил.
-
ссылка LLVM IR: Это полное описание LLVM IR, Будем манипулировать. Язык относительно прост, поэтому учиться не так много.
-
Руководство для программистов: быстрый обзор основных материалов, которые вам нужно знать при работе с LLVM.
-
Записи о прохождении: все, что вам нужно знать, чтобы передать преобразования или анализ.
-
LLVM Passes: полный список всех пропусков, предоставленных LLVM, которые вы можете и должны использовать. Это может помочь очистить код и упростить анализ. Например, при работе с циклами проходы lcssa
, simplify-loop
и indvar
сохраняют вашу жизнь.
-
Дерево наследования ценности. Это страница doxygen для класса Value. Важным здесь является дерево наследования, которое вы можете получить, чтобы получить документацию для всех инструкций, определенных на странице справки IR. Просто игнорируйте нечестивое чудовище, которое они называют диаграммой сотрудничества.
-
Тип дерева наследования: То же, что и выше, но для типов.
Как только вы все это поймете, то это торт. Чтобы найти доступ к памяти? Найдите инструкции store
и load
. К инструменту? Просто создайте то, что вам нужно, используя соответствующий подкласс класса Value
и вставьте его до или после инструкции по хранению и загрузке. Поскольку ваш вопрос слишком широк, я не могу вам больше помочь. (См. Исправление ниже)
Кстати, мне пришлось сделать что-то подобное несколько недель назад. Примерно через 2-3 недели я смог узнать все, что мне нужно о LLVM, создать проход анализа, чтобы найти доступ к памяти (и многое другое) в цикле и измерить их с помощью прохода преобразования, который я создал. Не было никаких привлекательных алгоритмов (кроме тех, которые были предоставлены LLVM), и все было довольно просто. Мораль этой истории заключается в том, что LLVM легко учится и работает.
Коррекция. Я сделал ошибку, когда сказал, что все, что вам нужно сделать, это поиск инструкций load
и store
.
Инструкция load
и store
будет давать только обращения, которые сделаны в кучу с помощью указателей. Чтобы получить все обращения к памяти, вам также нужно посмотреть на значения, которые могут представлять собой ячейку памяти в стеке. Независимо от того, записано ли значение в стек или хранится в регистре, определяется в фазе распределения регистров, которая возникает в переходе оптимизации бэкэнд. Это означает, что он зависит от платформы и не следует полагаться.
Теперь, если вы не предоставите больше информации о том, какие обращения к памяти вы ищете, в каком контексте и как вы собираетесь их использовать, я больше не могу вам помочь.
Ответ 2
Поскольку после двух дней ответа на ваш вопрос нет, я предлагаю его, который немного, но не полностью вне темы.
В качестве альтернативы LLVM для статического анализа программ на C вы можете рассмотреть возможность создания плагина Frama-C.
Существующий плагин, который вычисляет список входов для функции C, должен посещать каждое lvalue в теле функции. Это реализовано в файле src/inout/inputs.ml. Реализация короткая (сложность заключается в других плагинах, которые предоставляют свои результаты этому, например, разрешающие указатели) и могут использоваться в качестве скелета для вашего собственного плагина.
Посетитель для абстрактного дерева синтаксиса предоставляется каркасом. Чтобы сделать что-то особенное для lvalues, вы просто определяете соответствующий метод. Сердцем подключаемого модуля является определение метода:
method vlval lv = ...
Вот пример того, что делает подключаемый модуль:
int a, b, c, d, *p;
main(){
p = &a;
b = c + *p;
}
Входы main()
вычисляются таким образом:
$ frama-c -input t.c
...
[inout] Inputs for function main:
a; c; p;
Более подробную информацию о создании плагинов Frama-C в целом можно найти здесь.