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 自身未被任何函数调用,因此不会出现在被调用者列表中。