Инструментарий кодов C/C++ с использованием LLVM
Я только что прочитал о проекте LLVM и о том, что его можно использовать для статического анализа кодов C/C++ с использованием анализатора Clang, входящего в LLVM. Я хотел знать, возможно ли извлечь все обращения к памяти (переменные, локальные и глобальные) в исходном коде, используя LLVM.
Есть ли в LLVM встроенная библиотека, которую я мог бы использовать для извлечения этой информации. Если нет, пожалуйста, предложите мне, как написать функции, которые делают то же самое (существующий исходный код, ссылка, учебное пособие, пример...) Из того, что я думал, я бы сначала преобразовал исходный код в LLVM bc, а затем установил его в сделать анализ, но не знаю точно, как это сделать.
Я пытался выяснить, какой IR мне следует использовать для своих целей (абстрактное синтаксическое дерево Clang (AST) или промежуточное представление SSA LLVM (IR).), Но не смог понять, какой из них использовать. Вот что я пытаюсь сделать. Для любой программы на C/C++ (например, приведенной ниже) я пытаюсь вставить вызовы какой-либо функции до и после каждой инструкции, которая читает / записывает в / из памяти. Например, рассмотрим приведенную ниже программу на 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, а затем запустить инструментарий IR, который будет вставлять такие вызовы в IR, но я не знаю, как этого добиться. Пожалуйста, предложите мне примеры того, как это сделать.
Также я понимаю, что, как только я скомпилирую программу в IR, будет очень трудно получить отображение 1:1 между моей исходной программой и инструментированным IR. Итак, возможно ли отразить изменения, внесенные в ИК (из-за контрольно-измерительных приборов) в исходную программу.
Чтобы начать с прохода LLVM и сделать его самостоятельно, я рассмотрел пример прохода, который добавляет проверки во время выполнения для загрузки и сохранения IR LLVM, проход инструктажа загрузки / хранения SAFECode ( http://llvm.org/viewvc/llvm-project/safecode/trunk/include/safecode/LoadStoreChecks.h?view=markup and http://llvm.org/viewvc/llvm-project/safecode/trunk/lib/InsertPoolChecks/LoadStoreChecks.cpp?view=markup). Но я не мог понять, как запустить этот проход. Пожалуйста, дайте мне шаги, как запустить этот проход в какой-нибудь программе, например, в Account.cpp.
2 ответа
Прежде всего, вы должны решить, хотите ли вы работать с Clang или LLVM. Они оба работают с очень разными структурами данных, которые имеют свои преимущества и недостатки.
Из вашего скудного описания вашей проблемы, я рекомендую перейти на этапы оптимизации в LLVM. Работа с IR значительно упростит санитарную обработку, анализ и внедрение кода, потому что это то, для чего он предназначен. Недостатком является то, что ваш проект будет зависеть от LLVM, что может или не может быть проблемой для вас. Вы можете вывести результат, используя бэкэнд C, но он не может быть использован человеком.
Другим важным недостатком при работе с этапами оптимизации является то, что вы также теряете все символы из исходного исходного кода. Даже если Value
класс (подробнее об этом позже) имеет getName
метод, вы никогда не должны полагаться на него, чтобы содержать что-либо значимое. Он предназначен для отладки ваших пропусков и ничего больше.
Вы также должны иметь общее представление о компиляторах. Например, необходимо знать основные блоки и форму статического одиночного назначения. К счастью, это не очень сложные концепции для изучения или понимания (статьи в Википедии должны быть адекватными).
Прежде чем приступить к написанию кода, вам сначала нужно немного прочесть, поэтому вот несколько ссылок, с которых можно начать:
Обзор архитектуры: краткий обзор архитектуры LLVM. Даст вам хорошее представление о том, с чем вы работаете и подходит ли вам LLVM.
Глава документации: где вы можете найти все ссылки ниже и многое другое. Обращайтесь к этому, если я что-то пропустил.
Ссылка IR LLVM: Это полное описание IR LLVM, которым вы будете манипулировать. Язык относительно прост, поэтому не так много, чтобы учиться.
Руководство программиста: краткий обзор основных вещей, которые вы должны знать при работе с LLVM.
Написание пропусков: все, что вам нужно знать для написания преобразований или анализа.
Проходы LLVM: полный список всех пропусков, предоставленных LLVM, которые вы можете и должны использовать. Это действительно может помочь очистить код и упростить анализ. Например, при работе с циклами
lcssa
,simplify-loop
а такжеindvar
проходы спасут вашу жизнь.Дерево наследования значений: это страница Doxygen для класса Value. Важным битом здесь является дерево наследования, которому вы можете следовать, чтобы получить документацию для всех инструкций, определенных на справочной странице IR. Просто игнорируйте безбожное чудовище, которое они называют диаграммой сотрудничества.
Дерево наследования типов: То же, что и выше, но для типов.
Раз ты все это понимаешь, тогда это торт. Чтобы найти доступ к памяти? Ищи store
а также load
инструкции. К инструменту? Просто создайте то, что вам нужно, используя соответствующий подкласс Value
Класс и вставьте его до или после инструкции сохранения и загрузки. Поскольку ваш вопрос слишком широкий, я не могу вам больше чем-то помочь. (См. Исправление ниже)
Кстати, мне пришлось делать нечто подобное несколько недель назад. Примерно через 2-3 недели я смог узнать все, что мне нужно о LLVM, создать проход анализа, чтобы найти доступ к памяти (и более) в цикле, и оснастить их созданным мной проходом преобразования. Там не было никаких причудливых алгоритмов (кроме тех, которые предоставлены LLVM), и все было довольно просто. Мораль этой истории в том, что LLVM прост в освоении и работе.
Исправление: я сделал ошибку, когда сказал, что все, что вам нужно сделать, это найти load
а также store
инструкции.
load
а также store
инструкция будет давать доступ только к куче, используя указатели. Чтобы получить все обращения к памяти, вы также должны посмотреть на значения, которые могут представлять область памяти в стеке. Записывается ли значение в стек или сохраняется в регистре, определяется на этапе выделения регистров, который происходит на этапе оптимизации бэкэнда. Это означает, что это зависит от платформы и не следует полагаться.
Теперь, если вы не предоставите больше информации о том, какой тип доступа к памяти вы ищете, в каком контексте и как вы намереваетесь его использовать, я не смогу помочь вам намного больше, чем это.
Поскольку по прошествии двух дней ответа на ваш вопрос не будет, я предложу его, который немного, но не совсем не по теме.
В качестве альтернативы LLVM для статического анализа программ на C вы можете написать плагин Frama-C.
Существующий плагин, который вычисляет список входных данных для функции C, должен посещать каждое значение l в теле функции. Это реализовано в файле src / inout / input.ml. Реализация короткая (сложность в других плагинах, которые предоставляют свои результаты этому, например, в разрешении указателей) и может использоваться в качестве каркаса для вашего собственного плагина.
Посетитель для абстрактного синтаксического дерева обеспечивается платформой. Чтобы сделать что-то особенное для lvalues, вы просто определяете соответствующий метод. Сердцем плагина входов является определение метода:
method vlval lv = ...
Вот пример того, что делает плагин input:
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 в целом можно найти здесь.