llvm ir本身不自动保留行号或调试信息,必须显式插入!dbg元数据并配合llvm.dbg.*内在函数或#dbg_value记录,clang加-g选项是注入完整调试信息的最简方式。

LLVM IR 本身不自动保留行号或调试信息;必须显式插入元数据(!dbg)并配合 llvm.dbg.* 内在函数(或新式 #dbg_value 记录),否则优化后调试器根本看不到源码位置和变量值。
clang 编译时加 -g 是最简方式
这是绝大多数场景的起点,不需要手写 IR。只要源码有行号、变量名,-g 会驱动 Clang 在生成的 IR 中注入完整调试元数据:
-
clang -emit-llvm -S -g hello.c -o hello.ll→ 输出含!dbg !12、!DILocation等元数据的文本 IR - 生成的
.ll文件里会出现define ... !dbg !10 { ... ret i32 %7, !dbg !22 }这类标记 - 关键:必须用
-g,仅-O2或--debugify不足以恢复缺失的源码映射 - 注意:
-g默认不保留宏定义或模板实例化细节,需额外加-grecord-gcc-switches或-gpubnames
!dbg 元数据必须绑定到指令和变量声明
手动构造或修改 IR 时,光有元数据定义(如 !12 = !DILocation(...))不够,必须显式挂载到具体指令上,否则调试器无法关联:
- 每条可单步的指令(
add、load、call)都应带, !dbg !N后缀 - 变量内存位置需用
llvm.dbg.declare或llvm.dbg.value声明,例如:call void @llvm.dbg.declare(metadata ptr %x, metadata !15, metadata !DIExpression()), !dbg !16 - 新 IR 格式(2025 年后)改用
#dbg_value(%x, !15)行尾注释形式,不再调用内在函数 - 漏掉
!dbg的指令在调试时会跳过,表现为“单步消失”或断点错位
优化阶段容易冲掉调试信息
LLVM Pass 默认不感知调试元数据,部分优化(如 instcombine、gvn)可能合并指令但只保留一个 !dbg,导致行号丢失或变量不可见:
- 用
opt -verify -debugify可检测 IR 是否仍含有效调试信息(-debugify会在无!dbg处插入占位符) - 避免对调试关键代码启用 aggressive optimization:编译时加
-O0 -g或用函数级 attribute:define dso_local i32 @foo() #0 { ... } attributes #0 = { optnone } -
opt -S -O2 input.ll -o output.ll后务必检查output.ll中是否仍有密集的!dbg引用;若大量消失,说明 pass 未正确传播元数据 - 自定义 Pass 必须调用
Instruction::setDebugLoc()或MDNode::replaceOperandWith()显式迁移!dbg
验证调试信息是否真正可用
生成最终二进制后,不能只看 IR 里有没有 !dbg,要确认调试器能实际读取:
- 用
llvm-dwarfdump -debug-info a.out检查 DWARF section 是否完整(非空且含DW_TAG_subprogram、DW_TAG_variable) - 用
gdb ./a.out,然后list或info line main,若显示源码行而非 “No symbol table info available”,说明成功 - 常见失败信号:
(gdb) p x返回Cannot access memory at address 0x0→ 变量位置元数据丢失或未被llvm.dbg.declare正确标记 - 注意:JIT 场景(如 Kaleidoscope)默认不支持调试,必须输出为独立可执行文件再调试
真正难的不是加 !dbg,而是让整条工具链(前端生成 → 优化传播 → 后端编码 → 调试器解析)全程不丢元数据;任何一个环节漏掉传播逻辑,行号就断了。











