LLVM 开发指南 / 第 19 章:故障排查
第 19 章:故障排查
“当你在 LLVM 中遇到问题时,你并不孤单——几乎每个 LLVM 开发者都踩过同样的坑。”
19.1 IR 验证
19.1.1 使用 opt 验证
# 验证 IR 文件
opt -passes='verify' input.ll -o /dev/null
# 每个 Pass 后都验证
opt -passes='instcombine,gvn' -verify-each input.ll -o output.ll
# 查看错误
opt -passes='verify' bad.ll 2>&1
19.1.2 常见 IR 验证错误
错误: "Instruction does not dominate all uses!"
原因: 使用了未支配的 SSA 值
修复: 检查 PHI 节点和基本块顺序
错误: "Referencing function in a different module"
原因: 跨模块引用
修复: 使用 llvm-link 合并模块
错误: "Operand is null"
原因: 指令操作数为空
修复: 检查 IR 生成代码
错误: "Basic Block has no terminator"
原因: 基本块没有终止指令
修复: 确保每个基本块以 br/ret/... 结尾
错误: "Multiple terminating instructions"
原因: 基本块有多个终止指令
修复: 删除多余的终止指令
错误: "Redefinition of function"
原因: 函数重复定义
修复: 使用不同的名称或链接类型
19.1.3 IR 验证工具
# 使用 llvm-reduce 自动缩减 bug 用例
# 创建测试脚本
cat > check.sh << 'EOF'
#!/bin/bash
! opt -passes='my-pass' $1 -o /dev/null 2>&1 | grep "error"
EOF
chmod +x check.sh
llvm-reduce --test=check.sh input.ll -o reduced.ll
19.2 Pass 调试
19.2.1 打印 Pass 流水线
# 查看 O2 包含哪些 Pass
opt -passes='default<O2>' -print-pipeline-passes input.ll -o /dev/null 2>&1
# 查看 Pass 执行顺序
opt -passes='default<O2>' -debug-pass-manager input.ll -o /dev/null 2>&1
19.2.2 调试选项
# 打印每个 Pass 前后的 IR
opt -passes='instcombine' -print-before-all -print-after-all input.ll -o /dev/null 2>&1
# 只打印特定 Pass
opt -passes='instcombine' -print-before=instcombine -print-after=instcombine input.ll -o /dev/null 2>&1
# 打印修改过的 IR
opt -passes='instcombine' -print-changed input.ll -o /dev/null 2>&1
# 打印统计
opt -passes='instcombine' -stats input.ll -o /dev/null 2>&1 | head -20
19.2.3 使用 GDB 调试 Pass
# 调试 opt
gdb --args opt -passes='my-pass' input.ll -o /dev/null
# 在 Pass 中设置断点
(gdb) break MyPass::run
(gdb) run
# 调试 Clang
gdb --args clang -O2 test.c -o test
(gdb) break InstCombinePass::run
(gdb) run
19.3 代码生成调试
19.3.1 查看各阶段 MIR
# 查看指令选择后
llc -stop-after=instruction-select input.ll -o isel.mir
# 查看寄存器分配后
llc -stop-after=regalloc input.ll -o regalloc.mir
# 查看所有阶段
llc -print-after-all input.ll -o /dev/null 2>&1 | less
# 使用 llc debug 输出
llc -debug-only=isel input.ll -o /dev/null 2>&1
llc -debug-only=regalloc input.ll -o /dev/null 2>&1
19.3.2 常见代码生成问题
问题: "Cannot select: ..." (指令选择失败)
原因: DAG 中有无法匹配的节点
解决: 检查目标后端的 TableGen 定义
问题: "ran out of registers during register allocation"
原因: 寄存器溢出过多
解决: 减少变量活跃范围,或使用更多寄存器
问题: 生成的代码效率低
解决: 1. 检查 -O2 是否启用
2. 检查目标特性是否正确
3. 使用 -Rpass 检查优化报告
19.4 Clang 编译问题
19.4.1 常见错误
# 找不到 LLVM 头文件
# 错误: fatal error: 'llvm/IR/Module.h' file not found
# 解决: 安装 llvm-dev 或设置正确的 include 路径
# 链接错误
# 错误: undefined reference to 'llvm::...'
# 解决: 使用 llvm-config --libs 获取正确的链接库列表
# 版本不匹配
# 错误: LLVM ERROR: Building for an unexpected version
# 解决: 确保所有 LLVM 库版本一致
19.4.2 诊断信息
# 查看编译详细过程
clang -v test.c -o test
# 查看预处理输出
clang -E test.c
# 查看汇编输出
clang -S test.c -o test.s
# 查看 LLVM IR
clang -S -emit-llvm test.c -o test.ll
# 查看优化报告
clang -O2 -Rpass='.*' test.c 2>&1
clang -O2 -Rpass-missed='.*' test.c 2>&1
clang -O2 -Rpass-analysis='.*' test.c 2>&1
19.5 内存和性能问题
19.5.1 编译时间分析
# 使用 -ftime-report
clang -O2 -ftime-report test.c -o test 2>&1 | head -40
# 使用 -ftime-trace (Chrome trace 格式)
clang -O2 -ftime-trace test.c -o test
# 生成 test.json,可在 chrome://tracing 查看
# 使用 opt 的时间统计
opt -O2 -time-passes input.ll -o output.ll 2>&1
19.5.2 内存使用分析
# 编译时内存使用
# 使用 /usr/bin/time
/usr/bin/time -v clang -O2 large_file.c 2>&1 | grep "Maximum resident"
# 减少内存使用
# 减少并行编译数
ninja -C build -j2 # 减少并行数
# 对于 opt/llc
ulimit -v 4000000 # 限制虚拟内存
19.6 JIT 调试
# ORC JIT 调试
LLVM_JIT_DUMP=1 ./my_jit_program
# 使用 GDB 调试 JIT 代码
# 需要注册 GDB JIT 接口
# 注册后可以在 JIT 编译的函数上设置断点
19.7 测试用例最小化
19.7.1 使用 llvm-reduce
# 创建测试脚本
cat > test_bug.sh << 'EOF'
#!/bin/bash
# 返回 0 表示 bug 仍然存在
clang -O2 $1 -o /tmp/test 2>&1 | grep "LLVM ERROR"
EOF
chmod +x test_bug.sh
# 自动缩减
llvm-reduce --test=test_bug.sh input.c -o reduced.c
19.7.2 使用 creduce
# 安装 creduce
sudo apt install creduce
# 使用
creduce test_bug.sh input.c
19.8 常见问题 FAQ
| 问题 | 原因 | 解决方案 |
|---|
LLVM ERROR: Broken function | IR 验证失败 | -verify-each 定位问题 |
Cannot select | 指令选择失败 | 检查 TableGen 定义 |
Ran out of registers | 寄存器分配失败 | 减少变量活跃范围 |
Segmentation fault | LLVM 内部 bug | 检查输入 IR 是否合法 |
undefined reference | 链接库缺失 | llvm-config --libs |
ABI mismatch | 库版本不匹配 | 统一 LLVM 版本 |
Slow compilation | 编译时间过长 | 检查 Pass 流水线 |
| 编译后代码慢 | 未启用优化 | 使用 -O2 |
ninja: error | 构建失败 | 检查 CMake 配置 |
19.9 调试工具总结
| 工具 | 用途 |
|---|
opt -verify-each | 每个 Pass 后验证 IR |
opt -print-after-all | 打印每个 Pass 后的 IR |
opt -debug-pass-manager | 查看 Pass 执行顺序 |
opt -time-passes | Pass 执行时间 |
llc -debug-only=regalloc | 调试寄存器分配 |
clang -ftime-report | 编译时间分析 |
llvm-reduce | 自动缩减测试用例 |
gdb | 调试 LLVM 代码 |
valgrind | 检测内存问题 |
19.10 本章小结
| 场景 | 推荐工具 |
|---|
| IR 错误 | opt -verify-each |
| Pass 问题 | opt -print-after-all |
| 代码生成 | llc -stop-after= |
| 编译时间 | -ftime-report |
| 测试缩减 | llvm-reduce |
| 内存问题 | /usr/bin/time -v |
扩展阅读
- LLVM FAQ — 常见问题
- LLVM Bugpoint — 自动 bug 缩减
- LLVM Debugging Tips — 调试技巧
下一章: 第 20 章:最佳实践与贡献指南 — LLVM 代码规范、社区贡献和生产应用。