使用LLVM仪表化C/C++代码

20
我刚刚了解了LLVM项目,得知可以使用LLVM的前端Clang进行C/C++代码的静态分析。我想知道是否可能使用LLVM提取源代码中所有对内存(包括变量、本地和全局)的访问。
LLVM中是否有内置库可用于提取这些信息?如果没有,请建议我如何编写函数来实现这一点。(现有源代码、参考资料、教程、示例...)
我想的是,首先将源代码转换为LLVM字节码(bc),然后对其进行插桩以进行分析,但不知道具体如何操作。
我试图自己找出应该为我的目的使用哪种IR(Clang的抽象语法树(AST)还是LLVM的SSA中间表示(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,将很难获得原始程序和插装IR之间的1:1映射。因此,是否有可能反映IR中所做的更改(由于插装)到原始程序中。

为了开始学习LLVM pass以及如何自己制作pass,我查看了一个示例,该示例向LLVM IR加载和存储添加运行时检查,即SAFECode的load/store instrumentation pass (http://llvm.org/viewvc/llvm-project/safecode/trunk/include/safecode/LoadStoreChecks.h?view=markuphttp://llvm.org/viewvc/llvm-project/safecode/trunk/lib/InsertPoolChecks/LoadStoreChecks.cpp?view=markup)。但我无法弄清楚如何运行此pass。请告诉我如何在某个程序上运行此pass,例如上面的Account.cpp。


1
你是否已经查看了LLVM的示例,并阅读了如何实现LLVM Pass(并将它们作为opt插件运行)的相关内容?http://llvm.org/docs/WritingAnLLVMPass.html - SK-logic
1
可能是使用LLVM对C/C++代码进行仪器化的重复问题。 - RedX
2个回答

33

首先,您必须决定是否要使用clang或LLVM。它们都使用非常不同的数据结构,具有优缺点。

根据您提供的简要问题描述,我建议选择LLVM中的优化传递。使用IR将使得对代码进行消毒、分析和注入变得更加容易,因为这就是它的设计目的。缺点是,您的项目将依赖于LLVM,这可能对您来说是问题或不是问题。您可以使用C后端输出结果,但人类无法使用。

在使用优化传递时,另一个重要的缺点是,您还会失去所有原始源代码中的符号。即使Value类(稍后会详细介绍)具有getName方法,您也永远不应该依赖它包含任何有意义的内容。它的作用是帮助您调试传递,而不是其他。

您还必须对编译器有基本的了解。例如,了解基本块静态单赋值形式是必要的。幸运的是,它们不是很难学习或理解(维基百科文章应该足够)。

在开始编码之前,您首先必须阅读一些材料,因此以下是一些链接,可供您开始使用:

  • 架构概述:LLVM的快速架构概述。将为您提供对您正在使用的工具的好的了解,并帮助您确定是否适合使用LLVM。

  • 文档头:您可以在下面找到所有链接以及更多信息。如果我漏掉了什么,请参考此处。

  • LLVM IR 参考手册:这是LLVM IR的完整描述,也是您将要操作的内容。该语言相对简单,因此没有太多需要学习的内容。

  • 程序员手册:介绍与LLVM一起使用时需要了解的基本知识。

  • 编写 Passes:关于编写转换或分析Passes的所有必要信息。

  • LLVM Passes:LLVM提供的所有Passes的全面列表,您可以并且应该使用它们。这些Passes可以真正帮助您清理代码并使其更易于分析。例如,在处理循环时,lcssasimplify-loopindvar Passes将拯救您的生命。

  • Value 继承树:这是Value类的doxygen页面。重要的是您可以遵循继承树以获取IR参考页面中定义的所有指令的文档。只需忽略他们称之为协作图的可怕怪物即可。

  • Type 继承树:与上面相同,但是针对类型。

一旦你理解了所有这些,那就是小菜一碟。要查找内存访问?搜索storeload指令。要进行仪器化?只需使用Value类的适当子类创建所需内容,并在存储和加载指令之前或之后插入它。因为你的问题有点太广泛了,我不能比这更多地帮助你。 (请参见下面的更正)

顺便说一句,几周前我也做过类似的事情。在大约2-3周的时间里,我能够学习所有关于LLVM的知识,创建一个分析通道以查找循环中的内存访问(以及更多),并使用我创建的转换通道对它们进行仪器化。没有涉及任何花哨的算法(除了LLVM提供的算法),一切都非常简单明了。故事的寓意是LLVM易于学习和使用。


更正: 当我说只需搜索loadstore指令时,我犯了错误。

只有使用指针访问堆时,loadstore指令才提供访问。为了获得所有内存访问,您还必须查看可以表示堆栈上的内存位置的值。该值是写入堆栈还是存储在寄存器中,由后端优化通过中的寄存器分配阶段确定。这意味着它取决于平台,并且不应该依赖它。

现在,除非您提供有关要查找哪种类型的内存访问、在何种情况下以及如何对其进行检测的更多信息,我无法提供更多帮助。


1
SO第25个答案的解释写得很详细!+1 - sehe
@sehe 谢谢!这基本上是我在论文项目的编译器模块中发现有用的所有东西的大型链接转储 :) - Ze Blob

4

由于两天内没有人回答你的问题,我会提供一个略微但不完全离题的答案。

作为LLVM的替代品,用于C程序的静态分析,您可以考虑编写Frama-C插件。

现有的计算C函数输入列表的插件需要访问函数体中的每个lvalue。这在文件src/inout/inputs.ml中实现。该实现很短(复杂性在提供结果给此插件的其他插件中,例如解析指针),并且可以用作自己插件的框架。

抽象语法树的访问者由框架提供。为了对lvalue执行特殊操作,您只需定义相应的方法即可。输入插件的核心是方法定义:

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 插件的更多信息,可以在 这里 找到。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接