108 lines
7.3 KiB
Markdown
108 lines
7.3 KiB
Markdown
# 17 - 栈回溯与调试信息
|
||
|
||
TransPyV 里的 "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` 编出的 `<exe>.stackmap` 旁文件 |
|
||
|
||
## 编译期 traceback
|
||
|
||
编译期错误(AST 解析失败、类型错误等)由编译器打印 Python 风格定位,例如:
|
||
|
||
```text
|
||
Traceback (most recent call last, compile-time error):
|
||
File "D:\...\App\main.vp", line 238
|
||
```
|
||
|
||
实现位于 [TransPyV/App/lib/core/Gen/Type/TypeGenErr.vp](../TransPyV/App/lib/core/Gen/Type/TypeGenErr.vp)(`fatal_error`),入口 `GenApi.dump_parse_traceback`,在 Phase1 / Phase2 失败路径上调用。它与运行期无关。
|
||
|
||
## 运行期栈回溯(`--traceback`)
|
||
|
||
### 它做什么
|
||
|
||
运行期模块 [includes/traceback.vp](../includes/traceback.vp) 通过 `SetConsoleCtrlHandler` + `SetUnhandledExceptionFilter` 捕获「按键」或「崩溃」,转储主线程 / 出错线程的调用栈,并按 `<exe>.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 <unknown>
|
||
File "<0x7ffb... external>", line 0, in <unknown> [inferred]
|
||
```
|
||
|
||
- 帧行格式 `File "路径", line N, in 函数名`,其下再打一行 4 空格缩进的源码行;最外层在前、最内层在后(与 Python 一致)。
|
||
- 未命中映射的帧退化为纯地址形式(`no source line mapping` / `external`)。
|
||
- 栈扫描得到的帧标注 `[inferred]`(可能含误报)。
|
||
|
||
### 符号化:`<exe>.stackmap` 旁文件
|
||
|
||
运行期**不读 DWARF**,而是读链接后由编译器生成的 `<exe>.stackmap`。`BuildPipeline.WriteStackMap` 在链接成功后调用 `llvm-dwarfdump --debug-line --debug-info <exe>` 解析最终 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 元数据([TransPyV/App/lib/core/Gen/DebugInfoGen.vp](../TransPyV/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 <exe>` | 生成 `<exe>.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 运行库**不由 TPV 注入**,必须由 `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 无 `<exe>.stackmap`)。
|
||
|
||
## 相关文件
|
||
|
||
- [includes/traceback.vp](../includes/traceback.vp)(运行期栈回溯模块)
|
||
- [TransPyV/App/lib/core/Gen/DebugInfoGen.vp](../TransPyV/App/lib/core/Gen/DebugInfoGen.vp)(DWARF IR 发射)
|
||
- [TransPyV/App/lib/Phase/BuildPipeline.vp](../TransPyV/App/lib/Phase/BuildPipeline.vp)(llc / 链接 / stackmap 旁文件)
|
||
- [TransPyV/App/lib/core/Gen/Type/TypeGenErr.vp](../TransPyV/App/lib/core/Gen/Type/TypeGenErr.vp)(编译期 traceback)
|
||
- [includes/memhub.vp](../includes/memhub.vp)(`--detection` 语义)
|