【问题标题】:Instrumenting C/C++ code using LLVM使用 LLVM 检测 C/C++ 代码
【发布时间】:2011-10-18 11:45:59
【问题描述】:

我想编写一个 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 通行证的教程,但无法弄清楚如何为上述问题编写通行证。

【问题讨论】:

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

标签: c++ c static-analysis instrumentation


【解决方案1】:

我对 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++ 程序,这是不值得的。但是,如果您能够处理大型源软件库,那么绝对值得付出努力。

问候

【讨论】:

    【解决方案2】:

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

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

    希望这会有所帮助!

    【讨论】:

    • 您不应该在每条指令之前和之后都这样做,而仅适用于真正的指针storeload(而不是可简化的本地allocas)。
    • 您应该从函数 runOnBasicBlock 返回 true 以指示基本块中的指令已更改。
    猜你喜欢
    • 2015-08-25
    • 1970-01-01
    • 1970-01-01
    • 2011-05-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-01-29
    相关资源
    最近更新 更多