使用LLVM分析函数CFG

本文介绍如何使用 LLVM 构建和分析控制流图 (CFG),包括拓扑排序、强连通分量 (SCC) 的识别及遍历,并提供实用代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

作者:Eli Bendersky

http://eli.thegreenplace.net/2013/09/16/analyzing-function-cfgs-with-llvm

在Stack Overflow上,关于LLVM一个常见问题是如何构建一个函数的控制流图(CFG),并对它进行拓扑排序,或者拓扑排序的某些变形。为了节省我将来的回答时间,我认为我应该抛出一篇简明的博文,展示LLVM在这个领域的能力。

首先,问这个问题的人通常忘了这个事实:在一个CFG里,基本块(BB)已经组织好了,无需构建一个新的图来运行感兴趣的分析。

每个BB有一组后继者——控制流从这个BB传递到这些BB。很容易通过查看BB的终结符指令获得它(由定义,一个BB有单个终结符):

// BB is a BasicBlock*

// ...

const TerminatorInst *TInst= BB->getTerminator();

for (unsigned I = 0, NSucc =TInst->getNumSuccessors(); I < NSucc; ++I) {

  BasicBlock *Succ =TInst->getSuccessor(I);

  // Do stuff with Succ

}

BB的这个互联构成了一张图,我们可以任何我们觉得合适的方式遍历之。例如,下面是拓扑排序的一个实现:

classTopoSorter {

public:

  void runToposort(const Function &F) {

    outs() << "Topological sort of " << F.getName() << ":\n";

    // Initialize the color map by marking all the vertices white.

    for(Function::const_iterator I = F.begin(), IE = F.end(); I != IE; ++I) {

      ColorMap[I] =TopoSorter::WHITE;

    }

 

    // The BB graph has a single entry vertex from which the otherBBs should

    // be discoverable - the function entry block.

    bool success =recursiveDFSToposort(&F.getEntryBlock());

    if (success) {

      // Now we have all the BBs inside SortedBBs in reversetopological order.

      for(BBVector::const_reverse_iterator RI = SortedBBs.rbegin(),

                                            RE= SortedBBs.rend();

                                            RI!= RE; ++RI) {

        outs() << " " <<(*RI)->getName() << "\n";

      }

    } else {

      outs() << " Sorting failed\n";

    }

  }

private:

  enum Color {WHITE, GREY,BLACK};

  // Color marks per vertex (BB).

  typedef DenseMap<const BasicBlock *,Color> BBColorMap;

  // Collects vertices (BBs) in "finish" order. Thefirst finished vertex is

  // first, and so on.

  typedefSmallVector<const BasicBlock *, 32> BBVector;

  BBColorMap ColorMap;

  BBVector SortedBBs;

 

  // Helper function to recursively run topological sort from agiven BB.

  // Returns true if the sort succeeded and false otherwise;topological sort

  // may fail if, for example, the graph is not a DAG (detected acycle).

  bool recursiveDFSToposort(const BasicBlock *BB) {

    ColorMap[BB] =TopoSorter::GREY;

    // For demonstration, using the lowest-level APIs here. A BB'ssuccessors

    // are determined by looking at its terminator instruction.

    const TerminatorInst *TInst= BB->getTerminator();

    for (unsigned I = 0, NSucc =TInst->getNumSuccessors(); I < NSucc; ++I) {

      BasicBlock *Succ =TInst->getSuccessor(I);

      Color SuccColor =ColorMap[Succ];

      if (SuccColor ==TopoSorter::WHITE) {

        if(!recursiveDFSToposort(Succ))

          returnfalse;

      } elseif (SuccColor ==TopoSorter::GREY) {

        // This detects a cycle because grey vertices are all ancestorsof the

        // currently explored vertex (in other words, they're "onthe stack").

        outs() << " Detected cycle: edge from " << BB->getName() <<

                  " to " <<Succ->getName() << "\n";

        returnfalse;

      }

    }

    // This BB is finished (fully explored), so we can add it to thevector.

    ColorMap[BB] =TopoSorter::BLACK;

   SortedBBs.push_back(BB);

    returntrue;

  }

};

[本文里还包含其他片段的完整代码在这里]

它使用简单的,在Cormen等著的《Introduction to Algorithms》中给出的递归DFS算法。在递归查找期间,在第一次遭遇时,顶点被标记为“灰”,在处理完成时,标记为“黑”。一个完成的顶点所有的外出边都已经被探查了。拓扑排序是所有顶点按完成时刻排序,从最后到第一(这也称为“反后序”)。在我们特定的情形里,一个BB是一个顶点,到其后继者的连接是边。

对这个CFG:

我们得到:

Topological sort of func:

  AA

  BB

  CC

  DD

不过有一个重要的警告。拓扑排序仅对没有环的有向图(DAG)定义了。尽管基本块图是有向的,它不一定是无环的。事实上,代码里的任何循环都翻译为BB图中的一个环。上面的代码检测这并报告一个错误,在找到一个环时拒绝提供排序。例如,考虑这个带有一些环的CFG:

代码将抱怨:

Topological sort of func:

  Detected cycle: edgefrom BB4 to BB3

  Sorting failed

现在我们知道如何辛苦地实现它,让我们看一下LLVM提供的某些有用的工具。头文件llvm/ADT/PostOrderIterator.h提供了以反后序遍历一个函数BB的迭代器。下面是完整的使用片段:

outs() << "Basicblocks of " << F.getName()<< " in post-order:\n";

for(po_iterator<BasicBlock *> I = po_begin(&F.getEntryBlock()),

                              IE = po_end(&F.getEntryBlock());

                              I != IE; ++I) {

  outs() << " << (*I)->getName() << "\n";

}

回忆拓扑排序是反后序的。因此这正是你需要的顺序,考察来自同一头文件的类ReversePostOrderTraversal。注意也没检测环。在出现环时,这些迭代器将产生某种遍历,但不是拓扑序,因为在这样的情形里它是未定义的。如果你希望一个检测环的工具,在llvm/Analysis/CFG.h里有FindFunctionBackedges。它本质上运行与我上面展示的相同的DFS,虽然使用一个使用栈而不是递归的迭代算法。

关于po_iterator与其亲属的一件趣事是:它们可用于任何类型的图,不只是基本块图。它们可用于过程间分析的函数图,一个表达式图的节点,等等。这个魔法通过GraphTraits机制(llvm/ADT/GraphTraits.h)实现,这将图表示与工作在所有类型图上的实际算法解耦。使之对基本块工作的模板特化可在llvm/Support/CFG.h中找到——在这个头文件里,你还可以找到遍历BB后继(以及前驱)的迭代器,无需手动查询终结者指令。

回到拓扑排序。因为许多有用的函数有循环,因此包含环,我们怎么对付它们?答案是强连同分量(SCC)。如果我们找出BB图的SCC,我们拓扑排序这些SCC,仍然可以进行感兴趣的分析。比如,一个循环通常会收缩为一个SCC。那么我们怎么实现这个?

很幸运,LLVM已经有一个工具可以帮助我们。头文件llvm/ADT/SCCIterator.h定义了scc_iterator在一个图中以后序遍历SCC。这让我们以类似在无环图中排序BB那样的方式,拓扑排序SCC。事实上。在一个无环图中每个BB自己就是一个SCC,因此SCC的做法是一个泛化。使用scc_iterator是容易的:

// Use LLVM's Strongly Connected Components (SCCs) iterator toproduce

// a reverse topological sort of SCCs.

outs() << "SCCsfor " << F.getName() << " in post-order:\n";

for(scc_iterator<Function *> I = scc_begin(&F),

                             IE = scc_end(&F);

                             I != IE; ++I) {

  // Obtain the vector of BBs in this SCC and print it out.

  conststd::vector<BasicBlock *> &SCCBBs = *I;

  outs() << SCC: ";

  for(std::vector<BasicBlock *>::const_iterator BBI = SCCBBs.begin(),

                                                 BBIE =SCCBBs.end();

                                                BBI != BBIE; ++BBI) {

    outs() <<(*BBI)->getName() << ";

  }

  outs() << "\n";

}

对上面展示的循环CFG,这个代码将打印:

SCCs for func in post-order:

  SCC: DD

  SCC: CC2  CC1  CC

  SCC: BB4  BB3

  SCC: BB2  BB1  BB

  SCC: AA

【注意这是后序,不是反后序;因此显示在列表中的拓扑排序是自底向上的。】

我希望这是一次对LLVM的CFG分析能力有益的审视。代码中我已经给出了许多指示,它们可以作为严肃阅读代码的开端。看一下LLVM为此所拥有的许多工具是相当酷的,看到它们中许多适用各种类型的图特别棒,幸亏GraphTraits机制。


<think>好的,用户现在问的是如何使用LLVM API来判断是否存在空指针解引用。之前他们问过空指针检测是否使用某个特定函数,现在他们可能想更深入地了解在编译器或静态分析工具中如何实现这种检测。 首先,我需要回忆一下LLVM的结构。LLVM IR是中间表示,分析通常是在这个层级进行的。用户可能想知道如何通过编写一个LLVM pass来检测潜在的空指针解引用。他们可能是一位开发者,想要在自己的工具中添加这个功能,或者在学习LLVM的应用。 接下来,我需要确定空指针解引用在LLVM IR中的表现。比如,加载或存储指令的操作数是否为null指针。但要注意,LLVM IR中的指针可能不是显式的null,而是通过之前的函数调用或操作得到的。这时候可能需要数据流分析,比如追踪指针的可能取值。 然后,要考虑如何判断一个指针可能是null。可能需要使用像Alias Analysis、Dependence Analysis之类的分析工具,或者更高级的数据流分析,比如符号执行或路径敏感的分析。但用户可能希望有一个基本的实现方法,所以可能需要简化步骤,比如检查指针是否为null常量,或者是否由返回null的函数得到。 另外,LLVM中有一些现有的工具,比如Clang静态分析器,已经做了类似的事情。但用户想自己用LLVM API实现,所以需要指导他们如何构建一个自定义的pass。 还需要考虑可能的误报和漏报。比如,指针可能在某些条件下为null,但静态分析难以覆盖所有情况。这时候需要建议用户结合动态分析或其他方法。 最后,总结步骤:创建LLVM pass,遍历所有加载/存储指令,检查指针来源是否为null,使用分析结果判断潜在风险。可能需要给出示例代码和编译方法,方便用户实践。</think>### **1. 核心思路** 使用 **LLVM API** 检测空指针解引用的本质是:在 LLVM IR 层面对指针操作进行**静态分析**,识别可能导致空指针解引用的代码模式。 核心步骤包括: 1. **遍历 LLVM IR 指令**,定位所有可能解引用指针的操作(如 `LoadInst` 和 `StoreInst`)。 2. **分析指针来源**,判断指针是否可能为 `null`(通过常量分析、数据流分析或符号执行)。 3. **报告潜在风险**,标记存在空指针解引用风险的代码位置。 --- ### **2. 实现步骤详解** #### **(1) 创建 LLVM Pass** 通过编写自定义的 **LLVM Analysis Pass** 或 **Function Pass**,遍历函数内的指令: ```cpp #include "llvm/Pass.h" #include "llvm/IR/Instruction.h" #include "llvm/IR/Instructions.h" #include "llvm/Support/raw_ostream.h" using namespace llvm; namespace { struct NullDerefDetector : public FunctionPass { static char ID; NullDerefDetector() : FunctionPass(ID) {} bool runOnFunction(Function &F) override { for (auto &BB : F) { // 遍历基本块 for (auto &I : BB) { // 遍历指令 if (auto *LI = dyn_cast<LoadInst>(&I)) { // 处理加载指令 checkPointer(LI->getPointerOperand(), LI); } else if (auto *SI = dyn_cast<StoreInst>(&I)) { // 处理存储指令 checkPointer(SI->getPointerOperand(), SI); } } } return false; // 不修改代码 } void checkPointer(Value *Ptr, Instruction *I) { // 分析指针来源(关键步骤,见下文) } }; } char NullDerefDetector::ID = 0; static RegisterPass<NullDerefDetector> X("null-deref", "Detect null pointer dereferences"); ``` #### **(2) 指针来源分析** 判断指针是否为 `null` 的几种方法: | **方法** | **适用场景** | **代码示例** | |------------------------|---------------------------------------|-----------------------------------------------------------------------------| | **直接常量判断** | 指针直接来自 `null` 常量 | `if (isa<ConstantPointerNull>(Ptr)) { report_error(); }` | | **函数返回值分析** | 指针来自可能返回 `null` 的函数 | 结合 `CallInst` 分析被调用函数是否可能返回 `null`(需符号执行或外部标注) | | **分支路径敏感分析** | 指针在条件分支中被赋值为 `null` | 使用 `DominatorTree` 分析控制流关系 | | **数据流分析** | 复杂指针传播路径 | 使用 `DataFlowAnalysis` 或 `SparsePropagation` 框架追踪指针状态 | #### **(3) 示例:简单常量检测** 在 `checkPointer` 函数中添加基本判断: ```cpp void checkPointer(Value *Ptr, Instruction *I) { if (isa<ConstantPointerNull>(Ptr)) { // 直接判断是否为 null 常量 errs() << "Potential null dereference at: " << *I << "\n"; // 可获取源码位置:I->getDebugLoc() } } ``` #### **(4) 编译与使用** 1. **编译为 LLVM Pass**: ```bash clang++ -I LLVM_INCLUDE_DIR -shared -fPIC null_deref_detector.cpp -o null_deref.so ``` 2. **运行 Pass**: ```bash opt -load ./null_deref.so -null-deref -disable-output test.bc ``` --- ### **3. 高级分析技术** #### **(1) 符号执行 (Symbolic Execution)** - **原理**:通过符号化变量值,推导指针的可能取值范围。 - **工具**:使用 `Z3` 或 `LLVM Symbolic Execution` 库。 - **示例**: ```cpp if (Ptr->getType()->isPointerTy()) { auto SE = getAnalysis<ScalarEvolutionWrapperPass>().getSE(); SCEVUnionPredicate Pred; auto SCEV = SE.getSCEV(Ptr); if (SCEV->isZero()) { // 符号推导指针可能为 0 report_error(); } } ``` #### **(2) 路径敏感分析 (Path-Sensitive Analysis)** - **原理**:结合控制流图 (CFG) 分析不同分支下的指针状态。 - **实现**: ```cpp DominatorTree &DT = getAnalysis<DominatorTreeWrapperPass>().getDomTree(); for (auto *User : Ptr->users()) { if (auto *Br = dyn_cast<BranchInst>(User)) { if (Br->isConditional()) { // 分析分支条件是否排除了 null 的可能性 } } } ``` #### **(3) 静态单赋值 (SSA) 追踪** - **原理**:通过 SSA 链追踪指针的赋值历史。 - **代码**: ```cpp Value *Current = Ptr; while (auto *Phi = dyn_cast<PHINode>(Current)) { for (auto &Incoming : Phi->incoming_values()) { if (isa<ConstantPointerNull>(Incoming)) { report_error(); } } Current = Phi->getIncomingValue(0); // 简化示例,需递归遍历 } ``` --- ### **4. 局限性** | **限制** | **解决方案** | |-------------------------|------------------------------------------| | 动态内存分配无法静态分析 | 结合动态检测工具(如 ASan) | | 复杂控制流导致误报 | 使用路径敏感分析或概率模型 | | 跨函数指针传播困难 | 过程间分析(Inter-Procedural Analysis) | --- ### **5. 完整代码示例** ```cpp // 完整 Pass 实现(需根据实际需求扩展) #include "llvm/Pass.h" #include "llvm/IR/Instructions.h" #include "llvm/IR/Constants.h" #include "llvm/Support/raw_ostream.h" using namespace llvm; struct NullDerefDetector : public FunctionPass { static char ID; NullDerefDetector() : FunctionPass(ID) {} void getAnalysisUsage(AnalysisUsage &AU) const override { AU.setPreservesAll(); } bool runOnFunction(Function &F) override { for (auto &BB : F) { for (auto &I : BB) { if (auto *LI = dyn_cast<LoadInst>(&I)) { checkPointer(LI->getPointerOperand(), &I); } else if (auto *SI = dyn_cast<StoreInst>(&I)) { checkPointer(SI->getPointerOperand(), &I); } } } return false; } void checkPointer(Value *Ptr, Instruction *I) { if (isa<ConstantPointerNull>(Ptr)) { errs() << "[!] Null dereference detected at: " << *I << "\n"; if (DILocation *Loc = I->getDebugLoc()) { errs() << " Location: " << Loc->getFilename() << ":" << Loc->getLine() << "\n"; } } } }; char NullDerefDetector::ID = 0; static RegisterPass<NullDerefDetector> X("null-deref", "Detect null pointer dereferences"); ``` --- ### **6. 扩展方向** 1. **结合 Clang Static Analyzer**:利用 `checker` 机制实现更精确的空指针检测。 2. **集成动态检测**:在 LLVM IR 中插入运行时检查指令(类似 UBSan 的实现)。 3. **机器学习辅助**:训练模型识别高风险代码模式。 --- 如果需要进一步优化检测逻辑或讨论具体代码场景,可提供更多细节。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值