使用LLVM对C/C++代码进行仪器化

12

我想编写一个LLVM Pass来对每个内存访问进行插桩。

这是我的工作内容。

对于任何C/C++程序(例如下面给出的程序),我正在尝试在每个读/写内存的指令之前和之后插入对某些函数的调用。例如,考虑下面的C ++程序(Account.cpp):

#include <stdio.h>

class Account {
int balance;

public:
Account(int b)
{
   balance = b;   
}
~Account(){ }

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;   
}
~Account(){ }

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()可以是获取当前系统时间或者增加计数器等任何函数。

请给我提供示例(源代码、教程等)以及运行它的步骤。我已经阅读了http://llvm.org/docs/WritingAnLLVMPass.html上关于如何创建LLVM Pass的教程,但是无法弄清楚如何为上述问题编写一个Pass。


1
在你的例子中,你错过了很多潜在的内存访问(函数调用、指针解引用、变量读取等)。实际上,IR代码的每个指令都可能访问内存,直到最终生成汇编代码之前,你无法确定。对每个IR行进行检测绝对是一个坏主意,像valgrind这样的工具可能更适合你的问题。你能否给我们更多关于你想要实现的细节? - Ze Blob
@Ze Blob,为什么是“每个指令”?只有storeload可以访问内存。但当然应该排除本地变量的读/写(即由alloca返回的指针,可能会被优化掉)。 - SK-logic
1
@SK-logic,我的理解是CPU有限量的寄存器,可能没有足够的寄存器来存储您正在使用的所有值。在这种情况下,编译器必须将其中一个寄存器写入内存(堆栈)以腾出空间。这是在寄存器分配过程中确定的,该过程取决于目标架构。由于此过程在将IR代码转换为汇编代码时执行,因此很难仅通过查看IR代码确定值是否将被写入或读取自存储器。 - Ze Blob
1
@Ze Blob,我怀疑没有人会对仪器化堆栈帧访问感兴趣。无论如何,这些操作的顺序不能保证,LLVM将重新排列条目而不产生副作用(甚至可以跨基本块进行),因此在每个指令周围添加一些内容是毫无意义的。 - SK-logic
@SK-logic 我理解了,现在我再看一下这个例子,我觉得我可能错过了一个事实,即 balance 变量不在堆栈上(哎呀!)。这仍然无法解释为什么要检测 read() 函数(也许他想检测 this?)。 - Ze Blob
显示剩余3条评论
2个回答

9

我对LLVM不是很熟悉,但我对GCC(以及其插件机制)更为熟悉,因为我是GCC MELT的主要作者(一种高级领域特定语言,用于扩展GCC,顺便说一下,您可以在解决问题时使用它)。因此,我将尝试用通俗易懂的方式回答。

您应该首先了解为什么要适应编译器(或静态分析器)。这是一个值得追求的目标,但它确实有缺点(特别是在重新定义C ++程序中的某些运算符或其他构造方面)。

扩展编译器(无论是GCC、LLVM还是其他任何东西)的主要问题在于,您很可能需要处理其所有内部表示(除非您有一个非常明确定义的问题,否则您可能无法跳过其中的某些部分)。对于GCC来说,这意味着处理100多种Tree-s和近20种Gimple-s:在GCC中间端,tree-s代表操作数和声明,而gimple-s代表指令。 这种方法的优点是,一旦完成,您的扩展应该能够处理编译器接受的任何软件。缺点是编译器内部表示的复杂性(可以通过编译器接受的C和C ++源语言的定义的复杂性以及它们生成的目标机器代码的复杂性来解释,以及源语言与目标语言之间的距离的增加)。

因此,对于一个通用编译器(无论是GCC还是LLVM),或者像Frama-C这样的静态分析器,进行黑客攻击是相当大的任务(不是几天就能完成的工作,需要一个多月的时间)。只处理像您展示的小型C ++程序并不值得。但是,如果您打算处理大型源软件库,则绝对值得努力。

问候


3
尝试像这样做:(您需要填写空白并使迭代器循环正常工作,尽管正在插入项目)
class ThePass : public llvm::BasicBlockPass {
  public:
  ThePass() : BasicBlockPass() {}
  virtual bool runOnBasicBlock(llvm::BasicBlock &bb);
};
bool ThePass::runOnBasicBlock(BasicBlock &bb) {
  bool retval = false;
  for (BasicBlock::iterator bbit = bb.begin(), bbie = bb.end(); bbit != bbie;
   ++bbit) { // Make loop work given updates
   Instruction *i = bbit;

   CallInst * beforeCall = // INSERT THIS
   beforeCall->insertBefore(i);

   if (!i->isTerminator()) {
      CallInst * afterCall = // INSERT THIS
      afterCall->insertAfter(i);
   }
  }
  return retval;
}

希望这能帮到你!

你不应该在每个指令之前和之后都这样做,而只应该针对真正的指针(而不是可减少的本地alloca)进行“存储”和“加载”。 - SK-logic
你应该从函数runOnBasicBlock中返回true,以指示基本块中的指令已被更改。 - ConfusedAboutCPP

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