# 16 - 栈回溯与调试信息 ViperC 里的 "traceback" 有**两套完全不同的东西**,务必区分: | | 编译期 traceback | 运行期栈回溯 | |---|---|---| | 时机 | 编译源码出错时 | 编译出的**目标程序运行**时 | | 触发 | 解析失败 / 类型错误 | 用户按键(Ctrl+C / Ctrl+Break)或进程崩溃 | | 输出 | `Traceback (most recent call last, compile-time error):` + `File "...", line N` | `Traceback (most recent call last):` + `File "路径", line N, in 函数名` | | 依赖 | 无 | `--traceback` 编出的 `.stackmap` 旁文件 | ## 编译期 traceback 编译期错误(AST 解析失败、类型错误等)由编译器打印 Python 风格定位,例如: ```text Traceback (most recent call last, compile-time error): File "D:\...\App\main.vp", line 238 ``` 实现位于 [ViperC/App/lib/core/Gen/Type/TypeGenErr.vp](../ViperC/App/lib/core/Gen/Type/TypeGenErr.vp)(`fatal_error`),入口 `GenApi.dump_parse_traceback`,在 Phase1 / Phase2 失败路径上调用。它与运行期无关。 ## 运行期栈回溯(`--traceback`) ### 它做什么 运行期模块 [includes/traceback.vp](../includes/traceback.vp) 通过 `SetConsoleCtrlHandler` + `SetUnhandledExceptionFilter` 捕获「按键」或「崩溃」,转储主线程 / 出错线程的调用栈,并按 `.stackmap` 旁文件符号化成 `File "路径", line N, in 函数名`。 触发条件: - `Ctrl+C`(`CTRL_C_EVENT`):打印调用栈后返回 `FALSE`(交还系统默认处理 ⇒ 进程终止); - `Ctrl+Break`(`CTRL_BREAK_EVENT`):打印调用栈后返回 `TRUE`(已处理 ⇒ 进程继续); - 其它控制事件(关窗 / 注销 / 关机):一律返回 `FALSE`,不转储、不干预; - **崩溃**(未处理异常,经 `SetUnhandledExceptionFilter` 捕获):在**出错线程**上打印调用栈后返回 `EXCEPTION_CONTINUE_SEARCH`(异常交还系统,退出码 / WER / ASan 报告行为原样保留)。 **总开关 = 旁文件是否存在**:`Install()` 先求 `<当前 exe 完整路径>.stackmap`;该文件不存在则立刻返回、零副作用(不分配、不注册 handler);存在才注册 handler。因此在编译期显式加 `--traceback` 才会生效。 ### 输出格式 ```text Traceback (most recent call last): File "D:\...\App\main.vp", line 238, in main w_upper: str = w_str.upper() File "<0x7ff6... +0x1a2b no source line mapping>", line 0, in File "<0x7ffb... external>", line 0, in [inferred] ``` - 帧行格式 `File "路径", line N, in 函数名`,其下再打一行 4 空格缩进的源码行;最外层在前、最内层在后(与 Python 一致)。 - 未命中映射的帧退化为纯地址形式(`no source line mapping` / `external`)。 - 栈扫描得到的帧标注 `[inferred]`(可能含误报)。 ### 符号化:`.stackmap` 旁文件 运行期**不读 DWARF**,而是读链接后由编译器生成的 `.stackmap`。`BuildPipeline.WriteStackMap` 在链接成功后调用 `llvm-dwarfdump --debug-line --debug-info ` 解析最终 PE 的 `.debug_line` / `.debug_info`,把每条地址归一化为运行期 RVA(`addr - ImageBase`),写出「地址 → 源行 / 函数名」映射。 文件布局:header 20B(`"TPCS"` + `u32 version=1` + `u32 entry_count` + `u32 str_count` + `u32 str_bytes`),记录区每条 20B(`u64 rva | u32 file_off | u32 line | u32 func_off`,按 `rva` 严格升序),字符串池偏移 0 为空串哨兵。运行期加载后按 `rva` 做 floor 二分查找。 ### DWARF 元数据由编译器自己发射 DWARF 不是靠 llc 的 `-g`,而是编译器在 LLVM IR 里自己发 DI 元数据([ViperC/App/lib/core/Gen/DebugInfoGen.vp](../ViperC/App/lib/core/Gen/DebugInfoGen.vp)): - 节点层次:`!DIFile → !{ null } → !DISubroutineType → distinct !DICompileUnit → distinct !DISubprogram(每个 define)→ !DILocation(每条语句)`; - 模块尾部追加 `!llvm.dbg.cu` 与 `!llvm.module.flags`(Debug Info Version 3); - **`DIFile` 用绝对路径**(正斜杠归一)——否则 `llvm-dwarfdump --debug-line` 拿不到 comp_dir,行记录会退化成裸文件名 `main.vp`,运行期既显示不全也无法按路径重读源码行; - 生效开关 = `--traceback`(`DebugInfoGen.SetEnabled` 镜像 `BuildPipeline._traceback_mode`);关闭时 IR 与未开 DWARF 前逐字节一致。 ### 编译 / 链接参数 | 环节 | 参数 | 说明 | |------|------|------| | llc | `-frame-pointer=all` | 保留帧指针,保 RBP 链 | | 链接 | `-ldbghelp` | `StackWalk64` 等符号 | | 链接后 | `llvm-dwarfdump --debug-line --debug-info ` | 生成 `.stackmap` | | IR | `!llvm.dbg.cu` + `!llvm.module.flags(Debug Info Version 3)` | DWARF 元数据 | | 启动期 | `@llvm.global_ctors` 构造器调用 `Install()` | 自动安装 handler | `--traceback` 还会把运行期模块 `traceback` 加入可达集强制编入产物(在 includes 目录找不到 `traceback.vp` 会报错)。 ### 取栈路径 - **主路径** = 纯 RBP 链(`WalkRbpChain`,零 dbghelp 依赖); - **第二兜底** = 栈扫描(`ScanStack`,零 dbghelp、不依赖帧指针,结果为推断帧); - **可选加强** = dbghelp `StackWalk64`(仅在 RBP 链拿不到 >1 帧且探针判定可用时启用,能穿过免帧指针的系统帧)。 ASan 运行时也会注册 DbgHelp 符号化器,但 DbgHelp 状态是**进程级共享**而非冲突;ASan 的 VEH 优先于未处理异常过滤器,故 ASan 正常报告时崩溃转储不会被调用。 ## ASan 与 MemBuddy 检测 - **`--asan`**:在 `.ll → obj` 之前先跑 `opt -passes=forceattrs,asan -force-attribute=sanitize_address -asan-globals=false` 插桩。ASan 运行库**不由 VPC 注入**,必须由 `project.vpj` 的 `linker.static_libs` / `dynamic_libs` / `flags` 显式声明(出现 "asan" 即视为已声明),否则 fail-fast。 - **`--detection` / `--detection-deep`**:`MemBuddy` 内存池的分配检测(用户区前后各 2 字节 `0xCDCD` 标记 + 记录表探针),与 traceback 无关,是独立的内存诊断开关,作用于 `mb` 与 `perm_pool` 两个池。 ## 已知限制 - **仅支持 Windows PE 产物**:读到非 PE 或 `SizeOfImage <= 0` 直接终止编译。 - **依赖 `llvm-dwarfdump` 在 PATH**:调用失败或解析不到任何行记录都会终止编译。 - **RBP 链可能断**:`-frame-pointer=all` 对系统 / CRT 免帧指针代码无效,被中断的系统帧可能 `RBP=0x0` 断链,需靠 dbghelp `StackWalk64` 还原。 - 栈扫描帧是推断帧,可能含误报;RBP 链 + 栈扫描上限 64 帧。 - DWARF 生成有深度上限(函数嵌套 `DBG_STACK_MAX = 256`);发射了 define 但 IR 无 DI 的模块,其帧只显示地址。 - `--repl` 与 `--traceback` / `--asan` 互斥(片段 DLL 无 `.stackmap`)。 ## 相关文件 - [includes/traceback.vp](../includes/traceback.vp)(运行期栈回溯模块) - [ViperC/App/lib/core/Gen/DebugInfoGen.vp](../ViperC/App/lib/core/Gen/DebugInfoGen.vp)(DWARF IR 发射) - [ViperC/App/lib/Phase/BuildPipeline.vp](../ViperC/App/lib/Phase/BuildPipeline.vp)(llc / 链接 / stackmap 旁文件) - [ViperC/App/lib/core/Gen/Type/TypeGenErr.vp](../ViperC/App/lib/core/Gen/Type/TypeGenErr.vp)(编译期 traceback) - [includes/memhub.vp](../includes/memhub.vp)(`--detection` 语义)