23 KiB
TransPyC 调试器(vdb)设计文档
状态:设计阶段,暂不实现
创建时间:2026-06-26
目标:为 TransPyC 编译的程序提供源码级调试能力,支持断点、变量查看、单步、调用栈、异常栈跟踪
1. 背景与动机
TransPyC 将 Python 子集编译为 LLVM IR 再编译为原生机器码。当前编译产物完全不含调试信息(无 DWARF/PDB),运行时崩溃只能看到错误码和消息,无法定位源码行、查看变量、回溯调用栈。
本设计文档规划一个 TransPyC 专用调试器 vdb,目标是:
- 源码级断点(
break main.py:30) - 变量查看(
print x/info locals) - 单步执行(
step/next) - 调用栈回溯(
backtrace) - 异常栈跟踪(
raise时记录栈帧) - 硬错误捕获(segfault / 溢出)
2. 现状分析
2.1 已有可复用资产
| 资产 | 位置 | 复用方式 |
|---|---|---|
变量映射三件套 variables/_direct_values/_reg_values |
lib/core/LLVMCG/BaseGen.py:79-81 |
序列化到 .tpd 供变量查看 |
变量类型信息 var_type_info/var_signedness/var_struct_class |
lib/core/LLVMCG/BaseGen.py:82-112,124 |
序列化为 VarBinding.TypeDesc |
源码行号跟踪 _current_lineno/_set_node_info/_get_source_line |
lib/core/LLVMCG/BaseGen.py:126-127,138,297-440 |
扩展为 SourceMap |
类元信息表 class_members/class_member_defaults/class_parent/Vtables |
lib/core/LLVMCG/BaseGen.py:85-100 |
序列化为 TypeMeta |
符号表序列化 SymbolTable.ToDict |
lib/core/SymbolTable.py:265-319 |
vdb 直接加载,无需重建 |
异常机制 __eh_msg_out__/__eh_code_out__ |
lib/core/Handles/HandlesRaise.py / HandlesTry.py / HandlesExprCall.py |
扩展携带栈信息 |
VLogger compile_error(file, line, code) 三元组 |
lib/core/VLogger.py:286 |
复用错误位置格式 |
本地堆指针记录 _local_heap_ptrs/_var_to_heap_ptr |
lib/core/LLVMCG/MemoryOps.py:7-15 |
堆对象追踪起点 |
2.2 关键缺口
| 能力 | 现状 | 缺口说明 |
|---|---|---|
| DWARF 调试信息 | ❌ 完全未生成 | lib/ 下无任何 DIBuilder/!dbg/DICompileUnit 调用 |
| PDB 调试信息 | ❌ 完全未生成 | Windows 下 WinDbg/VS 无法源码级调试 |
| 源码行 → IR 指令映射 | ❌ 无数据结构 | 最核心缺口,需从零构建 SourceMap |
| 变量名 → 运行时内存 | ⚠️ 仅编译期 ephemeral | Gen.variables 随 Gen 实例销毁,需持久化 |
| 类型元信息运行时查询 | ❌ 无 RTTI/反射 | class_members 等均编译期状态,未发射到二进制 |
| 运行时调用栈 | ❌ 完全无 | 异常仅传 (code, msg),无栈帧链 |
| 硬错误处理 | ❌ 无 | __builtin_trap 仅声明未实现,segfault/SIGFPE 无捕获 |
| 断点/单步/变量查看 | ❌ 完全无 | lib/ 下搜索 breakpoint/step/watch 零命中 |
3. 路线选择
3.1 方案 A:纯 DWARF + GDB/LLDB
优点:生态成熟,无需自研前端
缺点:
- llvmlite 的 DIBuilder 支持度需验证
- Python 语义与 C 不完全对应(
self是指针但 Python 视为对象) - SHA1 命名导致符号名难读
- 泛型特化类型(
Stack[int])在 DWARF 中表达困难 - 跨模块符号(
6d71a13705da41ad.main)可读性差
3.2 方案 B:纯自研 vdb
优点:完全匹配 Python 语义,可显示原始源码,理解泛型/SHA1/跨模块
缺点:开发量大,需自研调试协议与前端
3.3 方案 C:混合(推荐)
- 生成轻量 DWARF(仅行号 + 变量名)让 GDB 可用作后备
- 同时生成 TransPyC 专用调试元数据
.tpd文件 - vdb 前端优先使用
.tpd,可降级到 GDB 后端
本设计采用方案 C,但优先实现 .tpd + vdb,DWARF 作为可选增强。
4. 核心数据结构
4.1 .tpd 文件格式(TransPyC Debug)
每个 .ll 文件配套生成一个 .tpd 文件(JSON 格式),包含以下结构:
from dataclasses import dataclass, field
from typing import Any
@dataclass
class IrLocation:
"""单条 IR 指令的源码位置映射"""
FuncName: str # mangled 函数名(含 SHA1 头,如 '6d71a13705da41ad.main')
BlockName: str # 基本块名(如 'entry'、'if.then')
InstrIdx: int # 指令在块中的索引
SourceFile: str # 源码相对路径
LineNo: int # 源码行号
ColNo: int # 列号
@dataclass
class VarBinding:
"""变量绑定信息(作用域内)"""
VarName: str # Python 变量名(如 'x'、'self')
LlvmName: str # alloca 名或 SSA(如 '%x'、'%".1"')
TypeDesc: str # 类型描述('i32' / 'Stack[int]*' / 'i8*')
IsSigned: bool # 是否有符号整数
StructClass: str | None # 所属结构体类名(如 'Stack[int]')
StartLine: int # 作用域起始行
EndLine: int # 作用域结束行
Storage: str # 'stack' | 'register' | 'direct' | 'global'
StackOffset: int | None # 栈偏移(仅 'stack')
@dataclass
class TypeMeta:
"""类型元数据"""
TypeName: str # 'Stack[int]' / 'IntPair' / 'memory_block'
Members: list[tuple[str, str, int]] # (成员名, 类型, 字节偏移)
Size: int # 总大小(字节)
Align: int # 对齐
Parent: str | None # 父类名
IsException: bool # 是否异常类
ExceptionCode: int # 异常码(仅 IsException=True)
HasVTable: bool # 是否有虚表
IsPacked: bool # 是否紧凑布局
@dataclass
class FuncSig:
"""函数签名"""
FuncName: str # mangled 名
RawName: str # 原始 Python 名(去 SHA1)
ReturnType: str # 返回类型描述
ParamNames: list[str]
ParamTypes: list[str]
IsExport: bool # 是否导出(@t.CExport)
IsMethod: bool # 是否实例方法
ClassName: str | None # 所属类名(方法)
SourceFile: str
StartLine: int
EndLine: int
HasException: bool # 是否可能抛异常(有 __eh_msg_out__)
@dataclass
class TpdFile:
"""完整的 .tpd 调试信息文件"""
SourceMap: list[IrLocation]
VarScopes: dict[str, list[VarBinding]] # FuncName -> bindings
TypeMeta: dict[str, TypeMeta]
FuncSignatures: dict[str, FuncSig]
ExceptionCodes: dict[int, str] # code -> exception name
SourceFiles: dict[str, list[str]] # file -> 源码行列表(供 vdb 显示上下文)
ModuleSha1: str | None
Version: str = "1.0"
4.2 序列化格式
.tpd 文件使用 JSON,字段名采用 PascalCase(与代码一致)。示例:
{
"Version": "1.0",
"ModuleSha1": "6d71a13705da41ad",
"SourceFiles": {
"App/main.py": ["import t", "import stdio", ...]
},
"SourceMap": [
{"FuncName": "main", "BlockName": "entry", "InstrIdx": 0, "SourceFile": "App/main.py", "LineNo": 255, "ColNo": 0},
...
],
"VarScopes": {
"6d71a13705da41ad.test_generic_stack": [
{"VarName": "s", "LlvmName": "%s", "TypeDesc": "Stack[int]*", "IsSigned": false, "StructClass": "Stack[int]", "StartLine": 66, "EndLine": 73, "Storage": "stack", "StackOffset": 0}
]
},
"TypeMeta": {
"Stack[int]": {
"TypeName": "Stack[int]",
"Members": [["data", "[16 x i32]", 0], ["top", "i32", 64]],
"Size": 68,
"Align": 4,
"Parent": null,
"IsException": false,
"ExceptionCode": 0,
"HasVTable": false,
"IsPacked": false
}
},
"ExceptionCodes": {
"1": "ValueError",
"2": "TypeError",
"99": "Exception",
"100": "MyCustomError"
}
}
5. 实现阶段
5.1 Phase 1:源码级断点(最小可用)
目标:break main.py:30 + continue + run
5.1.1 编译器改动
位置:lib/core/LLVMCG/BaseGen.py
- 扩展
_set_node_info:记录当前指令的源码位置到SourceMap
def _set_node_info(self, Node: ast.AST) -> None:
self._current_lineno = getattr(Node, 'lineno', 0)
self._current_col = getattr(Node, 'col_offset', 0)
self._current_node_info = f" [line {self._current_lineno}, col {self._current_col}]"
# 新增:记录到 SourceMap
if self.func and self.builder and self.builder.block:
loc: IrLocation = IrLocation(
FuncName=self.func.name,
BlockName=self.builder.block.name,
InstrIdx=len(self.builder.block.instructions),
SourceFile=self._current_source_file or '',
LineNo=self._current_lineno,
ColNo=self._current_col
)
self._source_map.append(loc)
- 插入断点桩:在每行源码对应的首条 IR 指令前插入
; 运行时断点桩声明
declare void @__tpc_breakpoint(i32 %line, i8* %file)
; 在 main.py:255 的首条指令前插入
%src_file_ptr = getelementptr [12 x i8], [12 x i8]* @.src.main_py, i32 0, i32 0
call void @__tpc_breakpoint(i32 255, i8* %src_file_ptr)
-
条件性插入:仅当
--debug编译选项启用时插入,release 构建完全剥离 -
生成 .tpd 文件:函数编译结束时,序列化
SourceMap到.tpd
5.1.2 运行时库
新建 includes/vdb_runtime.py:
import t
import c
# 全局断点表(行号 -> 是否启用)
_Breakpoints: t.CArray[t.CInt, 65536] | t.CPtr = [0]
# 当前调试器状态(0=运行, 1=暂停)
_DebugState: t.CInt = 0
# 当前源码文件名(用于断点匹配)
_CurrentFile: t.CArray[t.CInt8T, 256] | t.CPtr = [0]
@t.CExport
def __tpc_breakpoint(line: t.CInt, file: t.CPtr) -> None:
"""断点桩入口,由编译器在每个源码行前插入调用"""
# 检查是否启用断点
if _Breakpoints[line] == 0:
return
# 命中断点,陷入调试器循环
_DebugState = 1
_tpc_debug_loop(line, file)
def _tpc_debug_loop(line: t.CInt, file: t.CPtr) -> None:
"""调试器主循环,等待用户命令"""
# 通过 IPC 或 stdio 与 vdb 前端通信
# 解析命令:continue / step / next / print / info / backtrace
while _DebugState == 1:
cmd: t.CArray[t.CInt8T, 256] | t.CPtr = [0]
stdio.scanf("%s", cmd)
# ... 命令分派
5.1.3 vdb 前端
新建 lib/vdb/ 目录:
lib/vdb/
├── __init__.py
├── TpdLoader.py # .tpd 文件加载器
├── BreakpointManager.py # 断点管理
├── DebugSession.py # 调试会话主循环
├── CommandParser.py # 命令解析
└── SourceView.py # 源码显示
核心命令:
break <file>:<line>/b <file>:<line>— 设置断点delete <n>/d <n>— 删除断点continue/c— 继续执行run/r— 启动程序list/l— 显示当前源码上下文info breakpoints— 列出断点
5.2 Phase 2:变量查看
目标:print x / info locals / info args
5.2.1 编译器改动
位置:lib/core/LLVMCG/FuncGen.py 或 lib/core/Handles/HandlesFunctions.py
函数编译结束时,序列化变量绑定:
def _serialize_var_scope(self, FuncName: str) -> list[VarBinding]:
bindings: list[VarBinding] = []
for var_name, llvm_val in self.variables.items():
if llvm_val is None:
continue
binding: VarBinding = VarBinding(
VarName=var_name,
LlvmName=llvm_val.name if hasattr(llvm_val, 'name') else '',
TypeDesc=self._describe_llvm_type(llvm_val.type),
IsSigned=self.var_signedness.get(var_name, True),
StructClass=self.var_struct_class.get(var_name),
StartLine=self._func_start_line,
EndLine=self._func_end_line,
Storage='stack',
StackOffset=self._get_alloca_offset(llvm_val) # 新增:记录栈偏移
)
bindings.append(binding)
# 同样处理 _direct_values 和全局变量
return bindings
5.2.2 运行时变量读取
调试器通过以下方式读取变量值:
- 栈变量:通过
StackOffset+ 当前栈帧基址读取 - 全局变量:通过符号表查找全局符号地址
- 寄存器变量:需调试器理解 SSA 值(较复杂,Phase 2 可跳过,仅支持 stack)
5.2.3 类型渲染
def RenderValue(llvm_val: bytes, type_meta: TypeMeta, type_desc: str) -> str:
"""根据类型渲染变量值"""
if type_desc == 'i32':
return str(int.from_bytes(llvm_val, 'little', signed=True))
elif type_desc == 'i8*':
# 尝试作为 C 字符串读取
return f'"{read_cstring(llvm_val)}"'
elif type_desc.endswith('*') and type_meta:
# 结构体指针:展开成员
return RenderStruct(llvm_val, type_meta)
...
5.3 Phase 3:单步执行
目标:step / next / finish
基于 SourceMap 实现:
step(步入):在下一行源码的首条 IR 指令前设临时断点,包括被调用函数内部next(步过):在当前函数的下一行设临时断点,跳过函数调用finish(步出):在当前函数的返回点设临时断点
实现:
- 查 SourceMap 找到当前行的下一个源码行
- 设置临时断点(一次性)
continue- 命中后自动删除临时断点
5.4 Phase 4:调用栈跟踪
目标:backtrace / bt / frame <n>
5.4.1 编译器改动
每个函数入口/出口插入栈帧维护调用:
; 函数入口
call void @__tpc_push_frame(
i8* getelementptr([N x i8], [N x i8]* @.func.name, i32 0, i32 0), ; 函数名
i8* getelementptr([M x i8], [M x i8]* @.src.file, i32 0, i32 0), ; 源文件
i32 255 ; 起始行
)
; 函数出口(每个 ret 前)
call void @__tpc_pop_frame()
5.4.2 运行时栈结构
import t
@t.CStruct
class FrameInfo:
FuncName: t.CPtr # i8* 函数名
SrcFile: t.CPtr # i8* 源文件
LineNo: t.CInt # 当前行号(动态更新)
PrevFrame: 'FrameInfo' | t.CPtr # 上一帧(链表)
# 全局栈顶
_FrameTop: FrameInfo | t.CPtr = t.CPtr(0)
@t.CExport
def __tpc_push_frame(func_name: t.CPtr, src_file: t.CPtr, line: t.CInt) -> None:
frame: FrameInfo | t.CPtr = c.cast(FrameInfo, mb.alloc(FrameInfo.__sizeof__()))
frame.FuncName = func_name
frame.SrcFile = src_file
frame.LineNo = line
frame.PrevFrame = _FrameTop
_FrameTop = frame
@t.CExport
def __tpc_pop_frame() -> None:
if _FrameTop != t.CPtr(0):
old: FrameInfo | t.CPtr = _FrameTop
_FrameTop = _FrameTop.PrevFrame
mb.free(old)
5.4.3 断点桩更新当前行
__tpc_breakpoint 命中时,更新 _FrameTop.LineNo:
@t.CExport
def __tpc_breakpoint(line: t.CInt, file: t.CPtr) -> None:
if _Breakpoints[line] == 0:
return
if _FrameTop != t.CPtr(0):
_FrameTop.LineNo = line
_DebugState = 1
_tpc_debug_loop(line, file)
5.5 Phase 5:异常栈跟踪
目标:raise ValueError("msg") 时输出完整调用栈
5.5.1 编译器改动
位置:lib/core/Handles/HandlesRaise.py
在 _HandleRaiseLlvm 中,抛异常前捕获栈:
def _HandleRaiseLlvm(self, Node: ast.Raise) -> None:
# ... 原有错误码计算 ...
# 新增:捕获调用栈
capture_call = self.Gen.builder.call(
self._get_or_declare_function('__tpc_capture_trace', ir.FunctionType(ir.VoidType(), [])),
[]
)
# ... 原有异常传播逻辑 ...
5.5.2 异常结构扩展
将 __eh_msg_out__(i8**)扩展为指向包含栈的结构:
import t
@t.CStruct
class ExceptionInfo:
Code: t.CInt
Message: t.CArray[t.CInt8T, 256] | t.CPtr
TraceDepth: t.CInt
TraceFuncs: t.CArray[t.CPtr, 64] # 函数名指针数组
TraceLines: t.CArray[t.CInt, 64] # 对应行号
TraceFiles: t.CArray[t.CPtr, 64] # 对应源文件
5.5.3 未捕获异常处理
在 main 函数返回前检查异常,打印栈:
def main() -> t.CInt:
# ... 用户代码 ...
# 编译器自动插入:检查未捕获异常
if _UnhandledException != t.CPtr(0):
_tpc_print_trace(_UnhandledException)
return 1
return testcheck.end()
5.6 Phase 6:硬错误捕获
目标:segfault / 除零 / 栈溢出时进入调试器
5.6.1 信号处理器注册
import t
import c
@t.CExport
def __tpc_signal_handler(sig: t.CInt) -> None:
"""OS 信号处理入口"""
__tpc_capture_trace()
# 打印信号类型
if sig == 11: # SIGSEGV
stdio.printf("Segmentation fault\n")
elif sig == 8: # SIGFPE
stdio.printf("Arithmetic exception\n")
# 陷入调试器
_tpc_debug_loop(0, t.CPtr(0))
def __tpc_install_signal_handlers() -> None:
"""在 main 入口前自动调用"""
c.signal(11, __tpc_signal_handler) # SIGSEGV
c.signal(8, __tpc_signal_handler) # SIGFPE
5.6.2 编译器改动
在 main 函数入口自动插入 __tpc_install_signal_handlers() 调用(仅 --debug 模式)。
6. vdb 命令参考
6.1 启动与控制
| 命令 | 缩写 | 说明 |
|---|---|---|
run [args] |
r |
启动被调试程序 |
continue |
c |
继续执行至下一断点 |
step |
s |
单步进入函数 |
next |
n |
单步跳过函数 |
finish |
f |
执行至当前函数返回 |
quit |
q |
退出调试器 |
6.2 断点管理
| 命令 | 缩写 | 说明 |
|---|---|---|
break <file>:<line> |
b |
设置源码行断点 |
break <func> |
b |
设置函数入口断点 |
tbreak <file>:<line> |
tb |
临时断点(命中后删除) |
delete <n> |
d |
删除断点 n |
info breakpoints |
i b |
列出所有断点 |
6.3 变量查看
| 命令 | 缩写 | 说明 |
|---|---|---|
print <expr> |
p |
打印表达式值 |
info locals |
i lo |
列出当前局部变量 |
info args |
i ar |
列出函数参数 |
display <expr> |
- | 每次暂停自动显示 |
set <var>=<val> |
- | 修改变量值 |
6.4 栈操作
| 命令 | 缩写 | 说明 |
|---|---|---|
backtrace |
bt |
显示调用栈 |
frame <n> |
fr |
切换到第 n 帧 |
up |
- | 向上切换一帧 |
down |
do |
向下切换一帧 |
6.5 源码浏览
| 命令 | 缩写 | 说明 |
|---|---|---|
list |
l |
显示当前行上下文 |
list <file>:<line> |
l |
显示指定位置 |
info source |
i so |
显示当前源文件信息 |
6.6 类型与结构
| 命令 | 缩写 | 说明 |
|---|---|---|
ptype <typename> |
pt |
显示类型定义 |
info types |
i ty |
列出所有类型 |
info exceptions |
i ex |
列出异常码表 |
7. 实现优先级
| 优先级 | 阶段 | 价值 | 改动量 |
|---|---|---|---|
| P0 | Phase 1(源码断点)+ Phase 2(变量查看) | 80% 调试价值 | 小 |
| P1 | Phase 4(调用栈) | 崩溃定位关键 | 中 |
| P2 | Phase 5(异常栈) | Python 语义匹配 | 中 |
| P3 | Phase 3(单步) | 体验提升 | 中(依赖 Phase 1) |
| P4 | Phase 6(硬错误) | 兜底保护 | 中 |
| P5 | DWARF 生成 | GDB 互操作 | 大(可选) |
8. 风险与考量
8.1 性能开销
- 断点桩:仅
--debug构建启用,release 完全剥离 - IR 膨胀:每行插入断点桩约增加 10-20% IR 体积,可接受
- 运行时开销:断点桩检查为单次数组访问 + 分支预测,开销极低
- 栈帧维护:每函数调用增加 2 次
push/pop_frame,约 5-10% 调用开销
8.2 兼容性
- SHA1 命名:vdb 需理解 SHA1 前缀,通过
ModuleSha1字段还原为模块名.函数名显示 - 泛型特化:
Stack[int]需作为完整类型名处理,TypeMeta 中保留特化名 - 跨模块:
.tpd文件按模块生成,vdb 启动时合并所有相关.tpd main特化:main函数无 SHA1 头,vdb 需特殊处理
8.3 安全性
- 栈帧分配:使用
mb.alloc而非c.malloc,确保与 TransPyC 内存管理一致 - 信号处理器:异步信号中避免调用非异步信号安全函数,仅设置标志位
8.4 限制
- 优化构建:高优化级别(
-O2+)下变量可能被优化掉,.tpd中的 VarBinding 可能失效 - 内联函数:内联后栈帧信息丢失,需在
.tpd中标记IsInline - 尾调用优化:破坏调用栈链,需禁用或特殊处理
9. 文件结构规划
lib/vdb/ # vdb 调试器前端
├── __init__.py
├── TpdLoader.py # .tpd 文件加载与合并
├── BreakpointManager.py # 断点管理
├── DebugSession.py # 调试会话主循环
├── CommandParser.py # 命令解析
├── SourceView.py # 源码显示
├── ValueRenderer.py # 变量值渲染
├── StackWalker.py # 调用栈遍历
└── IpcChannel.py # 与被调试进程的 IPC 通信
lib/core/LLVMCG/
└── DebugInfoGen.py # 新增:.tpd 生成器(编译器侧)
includes/
└── vdb_runtime.py # 运行时断点/栈帧/信号处理库
10. 与现有系统的集成点
10.1 编译器集成
Phase2Translator.py:在生成.ll后,调用DebugInfoGen.GenerateTpd(module, gen, output_path)BaseGen.py:_set_node_info扩展记录 SourceMap;函数编译结束序列化 VarScopeHandlesFunctions.py:函数入口/出口插入__tpc_push_frame/__tpc_pop_frame(仅 debug 模式)HandlesRaise.py:_HandleRaiseLlvm插入__tpc_capture_trace
10.2 project.json 配置
新增 debug 选项:
{
"options": {
"debug": true,
"debug_info": "tpd"
}
}
debug: true— 启用调试构建(插入断点桩、栈帧维护、信号处理)debug_info: "tpd"— 生成 .tpd 文件(默认)debug_info: "dwarf"— 生成 DWARF(未来支持)debug_info: "both"— 同时生成(未来支持)
10.3 命令行集成
# 编译带调试信息
transpyc build --debug
# 直接启动调试器
transpyc debug ./output/MyApp.exe
# 等价于
vdb ./output/MyApp.exe
11. 开放问题
- llvmlite DIBuilder 支持度:需验证 llvmlite 是否暴露 DIBuilder 接口,决定 DWARF 生成可行性
- IPC 协议:vdb 前端与被调试进程的通信方式(管道/Socket/共享内存)待定
- 远程调试:是否支持远程调试(如 ViperOS 裸机目标)需进一步设计
- 表达式求值:
print x.y.z等复杂表达式是否需要完整 Python 解释器,还是简化为路径访问 - 多线程:TransPyC 当前是否支持多线程,调试器如何处理线程切换
12. 参考文档
- 现状分析:基于 2026-06-26 对
lib/core/和lib/Projectrans/的完整搜索 - 异常机制:
wiki/09-exceptions.md - 导入机制:
wiki/10-imports.md - 符号表:
lib/core/SymbolTable.py - 类型系统:
lib/core/TypeSpec.py/lib/core/Handles/HandlesBase.py - LLVM 生成:
lib/core/LLVMCG/BaseGen.py/FuncGen.py/MemoryOps.py
本文档为设计阶段产物,暂不实现。待社区需求或开发优先级提升后,按 Phase 1 → 2 → 4 → 5 → 3 → 6 顺序推进。