fix(traceback): 撤掉 caller_ret([RBP+8] 在 .vp 帧里不是返回地址)

实测 _TypePrintDepth:RBP = rsp+0x70(locals base),入参返回地址在 RBP+0x38;[RBP+8] 只是无关局部量(值落在 .data 区),打印它会误导。调用者一律以 .stackmap 走栈结果为准

文档第十四节 C 同步;regs/av_addr 两条保留
This commit is contained in:
TermiNexus committed 2026-09-28 12:21:26 +08:00
1 parent 1379b8814c
commit 677dd13320
1 file changed
+11 -19
+11 -19
View File
@@ -410,16 +410,16 @@ import w32.win32dbghelp as dbg
# 输出形状:`=== TPC crash dump (unhandled exception; the process keeps terminating) ===`
# → ` Trigger=unhandled-exception code=0x… addr=0x… av_kind=… av_addr=0x… crashthread=…`
# → ` exe base=… RIP=… RSP=… RBP=…` → ` regs: rcx=… rdx=… r8=… r9=… r12=…`
# → ` caller_ret=0x…`(出错帧 [RBP+8],能读到时才打)→ 与 Ctrl+C 逐字同形的 Traceback 帧
# (含源码行回读)→ ` frames=N` → 落文件提示 → `=== crash dump end ===`。
# → 与 Ctrl+C 逐字同形的 Traceback 帧(含源码行回读)→ ` frames=N` → 落文件提示
# → `=== crash dump end ===`。
# * `av_kind`/`av_addr` 取自 EXCEPTION_RECORD 的 ExceptionInformation[0/1](0=读 1=写 8=执行;
# 数据地址),只在 0xC0000005 类访问违规时有效——崩溃现场正是"原始数字胜过猜测"的场合;
# * `regs` 给的是**出错帧的实参现场**(Win64 前 4 个整型实参 = rcx/rdx/r8/r9;r12 是该帧
# 用来解引用指针的寄存器):一条就能区分「实参传错」与「实参对、指针所指内容错」,
# 不必再来一轮重编;
# * `caller_ret` 是**出错帧自己的返回地址**([RBP+8]):它不依赖 .stackmap,可独立反汇编核对,
# 用来判定"走栈给出的第一个调用者帧"是真帧还是 unwind 造出来的假帧。Ctrl+C 路径不打印
# 这些(那里没有"出错指令的算子"可看)。
# 不必再来一轮重编。Ctrl+C 路径不打印它(那里没有"出错指令的算子"可看)。
# * **不打印 `[RBP+8]` 当返回地址**:.vp 帧的 RBP 是 locals base(`leaq N(%rsp), %rbp`),
# 返回地址在 RBP + 帧字节数 - 8(逐函数不同;实测 _TypePrintDepth 是 RBP+0x38),
# `[RBP+8]` 只是无关局部量。调用者一律以 .stackmap 走栈结果为准。
# 中文件头(栈转储/触发=)只属于 Ctrl+C 路径,崩溃路径不动它。
# D. 已知边界:ASan 的 VEH 优先于未处理异常过滤器——若某次崩溃由 ASan 正常报告(它自带堆栈),
# 本路径不会被调用;本路径覆盖的正是「ASan 报不出来 / 没开 ASan」那两类(实测的两个退出码
@@ -1911,19 +1911,11 @@ def DumpCrashThread(Info: VOIDPTR) -> int:
uint64(ctx.Rcx), uint64(ctx.Rdx), uint64(ctx.R8), uint64(ctx.R9),
uint64(ctx.R12))
EmitLine(N)
# Return address of the faulting frame, read from [RBP+8] (the standard frame-pointer slot).
# Printed on its own line only when that read succeeds: it is the caller's resume address, so
# it can be mapped independently of the .stackmap and proves whether the walker's first
# recovered frame is the real caller (a missing line means "not available", never a fake 0).
RetWord: t.CArray[uint64, 1]
RetWord[0] = 0
if Rbp != 0:
if wm.ReadProcessMemory(HProc, t.CPtr(Rbp + 8), &RetWord, sizet(8), &Rd) != 0 \
and t.CSizeT(Rd) >= t.CSizeT(8):
N = stdio.snprintf(_State.LineBuf, t.CSizeT(TRACEBACK_LINE_CAP),
" caller_ret=0x%llx (faulting frame's return address)\n",
uint64(RetWord[0]))
EmitLine(N)
# NOTE: [RBP+8] is deliberately NOT read as a "return address". In .vp frames the compiler emits
# `leaq N(%rsp), %rbp` (RBP = a locals base, N = the spill area size), so the incoming return
# address sits at RBP + callee_frame_bytes - 8 - a per-function offset (measured on
# _TypePrintDepth: RBP = rsp+0x70 and the return address is at RBP+0x38, so [RBP+8] is an
# unrelated local, not the caller). The .stackmap walker is the authority for callers.
# dbghelp first: a fault usually sits inside a system DLL, where the RBP chain has nothing to
# follow. The RBP chain and the stack scan stay as fallbacks, exactly as in the Ctrl+C path.
Frames: int = 0