1. LLVM Pass开发环境准备
在开始编写第一个LLVM Pass之前,我们需要确保开发环境配置正确。LLVM Pass开发通常需要以下组件:
- LLVM工具链(包括clang、opt等)
- CMake构建系统(建议3.13或更高版本)
- C++17兼容的编译器(如GCC 7+或MSVC 2017+)
- Ninja构建工具(可选但推荐)
1.1 LLVM安装与配置
对于Windows平台,建议使用预编译的LLVM发行版。可以从LLVM官网下载安装包,安装时注意勾选"Add LLVM to the system PATH"选项。安装完成后,可以通过以下命令验证:
clang --version opt --version如果使用Linux系统,可以通过包管理器安装(如apt install llvm clang),但建议从源码编译以获得完整开发环境。
1.2 项目目录结构
一个典型的LLVM Pass项目目录结构如下:
/MyLLVMPass ├── CMakeLists.txt # 项目构建配置文件 ├── build/ # 构建目录(建议外部构建) │ └── test.c # 测试用例 └── MyPass.cpp # Pass实现代码注意:强烈建议将构建目录(build)与源代码分离,这符合CMake的最佳实践,可以避免污染源代码目录。
2. LLVM Pass基础概念
2.1 Pass层级体系
LLVM Pass按照作用范围分为五个层级:
- 模块(Module)层级:处理整个LLVM IR模块(对应一个源文件)
- 调用图(CallGraph)层级:分析函数间的调用关系
- 函数(Function)层级:处理单个函数
- 基本块(BasicBlock)层级:处理函数内的基本代码块
- 指令(Instruction)层级:处理单个IR指令
2.2 Pass的类型
LLVM Pass主要分为两大类:
- 分析(Analysis) Pass:只分析IR不修改,如支配树分析
- 转换(Transform) Pass:会修改IR,如内联优化
我们即将实现的函数名打印Pass属于分析Pass,因为它只读取函数名而不修改IR。
3. 实现第一个LLVM Pass
3.1 Pass代码结构
下面是一个完整的函数名打印Pass实现(MyPass.cpp):
#include "llvm/IR/PassManager.h" #include "llvm/Passes/PassBuilder.h" #include "llvm/Passes/PassPlugin.h" #include "llvm/Support/raw_ostream.h" using namespace llvm; namespace { struct MyPass : public PassInfoMixin<MyPass> { // 主处理函数,每个函数都会调用一次 PreservedAnalyses run(Function &F, FunctionAnalysisManager &) { // 输出当前函数名 errs() << "Found Function: " << F.getName() << "\n"; // 表示没有修改IR,保持所有分析结果有效 return PreservedAnalyses::all(); } }; } // Pass插件注册入口 extern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo llvmGetPassPluginInfo() { return { LLVM_PLUGIN_API_VERSION, "MyPass", // Pass名称 "v1.0", // 版本号 [](PassBuilder &PB) { PB.registerPipelineParsingCallback( [](StringRef Name, FunctionPassManager &FPM, ArrayRef<PassBuilder::PipelineElement>) { if (Name == "my-pass") { // 命令行使用的Pass名称 FPM.addPass(MyPass()); return true; } return false; } ); } }; }3.2 关键代码解析
Pass主体结构:
- 继承自
PassInfoMixin模板类 - 必须实现
run()方法作为入口点
- 继承自
run()方法:
- 参数
Function &F是当前处理的函数 - 返回值
PreservedAnalyses表示Pass是否修改了IR errs()是LLVM提供的错误输出流
- 参数
插件注册:
llvmGetPassPluginInfo()是必须实现的入口函数- 通过
PassBuilder注册Pass名称和构造器
4. 构建与测试Pass
4.1 CMake配置
创建CMakeLists.txt文件:
cmake_minimum_required(VERSION 3.13) project(MyPass) # 设置LLVM路径(根据实际安装位置修改) set(LLVM_DIR "D:/LLVM/llvm-project/build/lib/cmake/llvm") find_package(LLVM REQUIRED CONFIG) # 添加LLVM模块路径 list(APPEND CMAKE_MODULE_PATH "${LLVM_CMAKE_DIR}") include(AddLLVM) # 包含LLVM头文件 include_directories(${LLVM_INCLUDE_DIRS}) add_definitions(${LLVM_DEFINITIONS}) # 设置C++标准 set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 构建Pass插件 add_library(MyPass MODULE MyPass.cpp) set_target_properties(MyPass PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON PREFIX "") # 链接LLVM库 target_link_libraries(MyPass PRIVATE LLVMCore LLVMSupport) # Windows下设置UTF-8编码 if(MSVC) target_compile_options(MyPass PRIVATE /utf-8) endif()4.2 构建Pass
在build目录下执行:
cmake -G "Ninja" -DCMAKE_BUILD_TYPE=RelWithDebInfo .. ninja构建成功后会在build目录生成MyPass.dll(Windows)或MyPass.so(Linux)。
4.3 测试用例
创建简单的测试程序test.c:
#include <stdio.h> void secret_function() { printf("I am secret\n"); } int main() { secret_function(); return 0; }4.4 运行Pass
- 首先将C代码编译为LLVM IR:
clang -S -emit-llvm -O1 -Xclang -disable-llvm-passes test.c -o test.ll- 然后通过opt工具运行我们的Pass:
opt -load-pass-plugin=MyPass.dll -passes=my-pass test.ll -o /dev/null4.5 预期输出
运行Pass后,你应该能看到类似以下输出:
Found Function: secret_function Found Function: main Found Function: printf5. 常见问题与调试技巧
5.1 编译问题排查
问题1:CMake找不到LLVM
- 确保
LLVM_DIR路径正确指向LLVM的cmake目录 - 检查LLVM是否安装完整
问题2:C++17特性不支持
- 更新编译器版本
- 确保CMake中正确设置了C++17标准
5.2 运行时问题
问题1:Pass没有执行
- 检查
opt命令中的Pass名称是否与注册时一致 - 确保使用了正确的
-load-pass-plugin参数
问题2:中文乱码
- Windows下添加
/utf-8编译选项 - 避免在Pass代码中使用中文
5.3 调试技巧
- 使用LLVM_DEBUG宏:
#define DEBUG_TYPE "mypass" LLVM_DEBUG(dbgs() << "Debug info: " << F.getName() << "\n");编译时添加-DCMAKE_BUILD_TYPE=Debug并运行时加-debug参数
- 打印完整IR:
F.print(errs());- 使用GDB/LLDB:
gdb --args opt -load-pass-plugin=MyPass.so -passes=my-pass test.ll6. Pass进阶开发建议
6.1 扩展Pass功能
当前Pass只是打印函数名,可以扩展以下功能:
- 统计函数基本信息(参数数量、基本块数等)
- 分析函数调用关系
- 检测特定模式(如未使用的函数)
6.2 性能优化建议
- 避免在Pass中频繁创建/销毁对象
- 使用LLVM提供的分析结果(如支配树)
- 对大型项目考虑并行处理
6.3 集成到构建系统
可以将Pass集成到:
- 编译器流水线(通过PassBuilder)
- 静态分析工具链
- 自定义编译工具
7. 实际应用案例
7.1 函数调用图生成
扩展我们的Pass来生成函数调用关系:
PreservedAnalyses run(Function &F, FunctionAnalysisManager &FAM) { errs() << "Function: " << F.getName() << " calls:\n"; for (auto &BB : F) { for (auto &I : BB) { if (auto *Call = dyn_cast<CallInst>(&I)) { if (Function *Callee = Call->getCalledFunction()) { errs() << " -> " << Callee->getName() << "\n"; } } } } return PreservedAnalyses::all(); }7.2 简单代码度量
统计函数的基本信息:
PreservedAnalyses run(Function &F, FunctionAnalysisManager &) { unsigned NumBB = 0, NumInst = 0; for (auto &BB : F) { NumBB++; NumInst += BB.size(); } errs() << F.getName() << ": " << NumBB << " BB, " << NumInst << " Inst\n"; return PreservedAnalyses::all(); }8. 进一步学习资源
官方文档:
- LLVM Programmer's Manual
- Writing an LLVM Pass
开源项目参考:
- LLVM自带的Pass(如SimplifyCFG)
- 知名开源项目中的Pass实现
调试工具:
- opt -print-after-all
- LLVM调试工具(bugpoint等)
社区资源:
- LLVM邮件列表
- LLVM Discord频道
- 相关技术博客和会议演讲