问题 使用LLVM检测C / C ++代码


我想写一个LLVM传递来检测每个内存访问。 这是我想要做的。

给定任何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()可以是任何函数,比如获取当前系统时间或递增计数器......等等。

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


9922
2017-10-18 11:45


起源

好吧,您可能会重载操作符,不仅要执行实际的加法,减法,赋值函数,还要调用自定义函数。 - vishakvkt
为什么要添加这些功能?如果您想调试程序,可以使用更好的方法。 - tune2fs
在您的示例中,您错过了许多潜在的内存访问(函数调用,指针解除引用,变量读取等)。事实上,每个IR代码指令都可能访问内存,在生成最终程序集之前,您无法确定。检测每一条红外线绝对是一个坏主意和一个类似的工具 的valgrind 可能更适合你的问题。您能否详细介绍一下您要完成的工作? - Ze Blob
@ SK-logic,我的理解是CPU具有有限的寄存器数量,并且可能没有足够的寄存器来存储您正在使用的所有值。在这种情况下,编译器必须将其中一个寄存器写入存储器(堆栈)以便腾出空间。这是在寄存器分配过程中确定的,这取决于目标体系结构。因为在将IR代码转换为汇编时执行此传递,所以很难通过查看IR代码来确定是从内存中写入还是读取值。 - Ze Blob
@Ze Blob,我怀疑任何人都会对检测堆栈帧访问感兴趣。无论如何,这样的操作的顺序不能得到保证,LLVM将重新调整条目而没有副作用(甚至可以在基本块中进行),因此在每条指令周围添加一些东西是没有意义的。 - SK-logic


答案:


尝试这样的事情:(你需要填写空白并使迭代器循环工作,尽管插入了项目)

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;
}

希望这可以帮助!


3
2017-12-20 06:03



你不应该在每条指令之前和之后都这样做,但仅限于 store 和 load 对于真正的指针(不是可简化的本地指针) allocaS)。 - SK-logic
您应该从函数runOnBasicBlock返回true,以指示基本块中的指令已更改。 - ConfusedAboutCPP


我对LLVM不是很熟悉,但我对GCC(及其插件机制)比较熟悉,因为我是主要的作者。 GCC MELT (一种用于扩展GCC的高级域特定语言,可以用来解决您的问题)。因此,我将尝试回答一般性问题。

您应该首先知道为什么要调整编译器(或静态分析器)。这是一个有价值的目标,但它确实有缺点(特别是,w.r.t。在C ++程序中重新定义了一些运算符或其他构造)。

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

因此,攻击一般编译器(无论是GCC还是LLVM),还是静态分析器(如Frama-C),是一项相当大的任务(超过一个月的工作,而不是几天)。要只处理像你所展示的微小的C ++程序,它是不值得的。但是,如果你明白处理大型源代码软件库,那绝对值得努力。

问候


8
2017-10-20 16:47