3. 函数调用图构建

任务目标

编写一个 LLVM FunctionPass,遍历当前模块中所有函数体内的 CallInst,统计每个函数被调用的次数,并记录调用者与被调用者的对应关系。最终将每个函数的被调用次数及调用关系输出到标准错误流(errs())。

环境与依赖

  • LLVM 14+(本案例使用 LLVM 17 接口编写,低版本需微调头文件路径)
  • CMake 3.13+
  • 已安装的 LLVM 开发库(含 llvm-config)

核心实现

以下代码实现完整的调用图统计 Pass。核心逻辑在 runOnModule 中:先遍历每个函数的指令,收集所有直接调用点;再统一输出统计结果。间接调用(通过函数指针)不在本案例范围内,代码中会跳过并计数。

// CallGraphStats.cpp
#include "llvm/IR/Function.h"
#include "llvm/IR/Instructions.h"
#include "llvm/IR/Module.h"
#include "llvm/Pass.h"
#include "llvm/Support/raw_ostream.h"
#include <map>
#include <set>
#include <string>

using namespace llvm;

namespace {
struct CallGraphStats : public ModulePass {
  static char ID;
  CallGraphStats() : ModulePass(ID) {}

  bool runOnModule(Module &M) override {
    // callee -> count
    std::map<std::string, unsigned> callCount;
    // callee -> set of callers
    std::map<std::string, std::set<std::string>> callers;
    unsigned indirectCalls = 0;

    for (Function &F : M) {
      if (F.isDeclaration())
        continue; // 只统计有函数体的函数

      for (BasicBlock &BB : F) {
        for (Instruction &I : BB) {
          auto *CI = dyn_cast<CallInst>(&I);
          if (!CI)
            continue;

          Function *callee = CI->getCalledFunction();
          if (!callee) {
            ++indirectCalls; // 间接调用,跳过
            continue;
          }

          std::string calleeName = callee->getName().str();
          std::string callerName = F.getName().str();
          callCount[calleeName]++;
          callers[calleeName].insert(callerName);
        }
      }
    }

    // 输出结果
    errs() << "=== Call Graph Statistics ===\n";
    for (auto &[callee, count] : callCount) {
      errs() << "Function '" << callee << "' called " << count
             << " time(s) by: ";
      bool first = true;
      for (const auto &caller : callers[callee]) {
        if (!first)
          errs() << ", ";
        errs() << caller;
        first = false;
      }
      errs() << "\n";
    }
    errs() << "Indirect calls skipped: " << indirectCalls << "\n";
    return false; // 未修改模块
  }
};
} // namespace

char CallGraphStats::ID = 0;
static RegisterPass<CallGraphStats> X("callgraph-stats",
                                      "Count calls and build caller-callee map",
                                      false /* Only looks at CFG */,
                                      false /* Analysis Pass */);

构建与运行

使用以下 CMakeLists.txt 构建 Pass 动态库:

cmake_minimum_required(VERSION 3.13)
project(CallGraphStats)

find_package(LLVM REQUIRED CONFIG)
message(STATUS "Found LLVM ${LLVM_PACKAGE_VERSION}")
include_directories(${LLVM_INCLUDE_DIRS})
add_definitions(${LLVM_DEFINITIONS})

add_library(CallGraphStats MODULE CallGraphStats.cpp)
target_link_libraries(CallGraphStats PRIVATE LLVM)

构建命令:

mkdir build && cd build
cmake -DLLVM_DIR=$(llvm-config --cmakedir) ..
make

生成测试用 C 代码 test.c:

void foo() {}
void bar() { foo(); }
int main() {
  foo();
  bar();
  foo();
  return 0;
}

编译并运行 Pass:

clang -S -emit-llvm test.c -o test.ll
opt -load ./libCallGraphStats.so -callgraph-stats test.ll -o /dev/null

验证方法

运行后标准错误流应输出:

=== Call Graph Statistics ===
Function 'foo' called 3 time(s) by: bar, main
Function 'bar' called 1 time(s) by: main
Indirect calls skipped: 0

该输出与 test.c 中的调用关系完全一致:main 调用 foo 两次、bar 一次;bar 内部调用 foo 一次。若输出与上述一致,则 Pass 工作正常。注意 main 自身未被任何函数调用,因此不会出现在被调用者列表中。