feat(traceback): 崩溃转储补出错帧实参寄存器与 AV 数据地址(一次拿全现场)

Trigger 行新增 av_kind/av_addr(EXCEPTION_RECORD 的 ExceptionInformation[0/1]:0=读 1=写 8=执行、数据地址;仅访问违规有效,但崩溃现场就该给原始数字)

新增 regs 行:rcx/rdx/r8/r9/r12(Win64 前 4 个整型实参 + 出错帧解引用用的寄存器),用于区分「实参传错」与「实参对、指针所指内容错」,避免多一轮重编

文档第十四节 C 同步输出形状与这两行的用途
This commit is contained in:
TermiNexus committed 2026-09-28 12:13:13 +08:00
1 parent 7651da8ac9
commit 1fabb46e8c
1 file changed
+39 -10
+39 -10
View File
@@ -408,10 +408,16 @@ import w32.win32dbghelp as dbg
# 与 Ctrl+C 转储**共用同一把重入锁**(AtomicTryEnterDump),故一次只可能有一份 dump:
# 崩溃若发生在转储进行中,本次崩溃不转储(锁被占),避免二次伤害。
# 输出形状:`=== TPC crash dump (unhandled exception; the process keeps terminating) ===`
# → ` Trigger=unhandled-exception code=0x… addr=0x… crashthread=…` → ` exe base=… RIP=…
# RSP=… RBP=…` → 与 Ctrl+C 逐字同形的 Traceback 帧(含源码行回读)→ ` frames=N` →
# 落文件提示 → `=== crash dump end ===`。中文件头(栈转储/触发=)只属于 Ctrl+C 路径,
# 崩溃路径不动它。
# → ` Trigger=unhandled-exception code=0x… addr=0x… av_kind=… av_addr=0x… crashthread=…`
# → ` exe base=… RIP=… RSP=… RBP=…` → ` regs: rcx=… rdx=… r8=… r9=… r12=…`
# → 与 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 是该帧
# 用来解引用指针的寄存器):一条就能区分「实参传错」与「实参对、指针所指内容错」,
# 不必再来一轮重编。Ctrl+C 路径不打印它(那里没有"出错指令的算子"可看)。
# 中文件头(栈转储/触发=)只属于 Ctrl+C 路径,崩溃路径不动它。
# D. 已知边界:ASan 的 VEH 优先于未处理异常过滤器——若某次崩溃由 ASan 正常报告(它自带堆栈),
# 本路径不会被调用;本路径覆盖的正是「ASan 报不出来 / 没开 ASan」那两类(实测的两个退出码
# 都在其中)。栈耗尽类崩溃(本路径自身也需要栈)不在保障范围内。
@@ -1823,6 +1829,7 @@ def DumpCrashThread(Info: VOIDPTR) -> int:
The bare `_State` null-check below is REQUIRED (this function uses _State.LineBuf).
"""
global _DumpOut
if _State == None or _State.LineBuf == None or _State.Installed == 0:
return 0
if AtomicTryEnterDump() != 0:
@@ -1848,17 +1855,28 @@ def DumpCrashThread(Info: VOIDPTR) -> int:
CtxPtr: uint64 = Two[1]
Code: uint32 = 0
ExAddr: uint64 = 0
# EXCEPTION_RECORD head: u32[0] = ExceptionCode, u32[4..5] = ExceptionAddress (offset 0x10).
RecWords: t.CArray[uint32, 6]
AvKind: uint32 = 0
AvAddr: uint64 = 0
# EXCEPTION_RECORD head (0x30 bytes):
# u32[0] ExceptionCode, u32[1] ExceptionFlags, u32[2..3] nested record,
# u32[4..5] ExceptionAddress (offset 0x10), u32[6] NumberParameters (offset 0x18),
# u32[8] ExceptionInformation[0] (offset 0x20 = 0 read / 1 write / 8 execute),
# u32[10..11] ExceptionInformation[1] (offset 0x28 = the faulting DATA address).
# The two ExceptionInformation fields are only filled for access violations (code 0xC0000005);
# they are printed unconditionally because a hard crash is exactly when raw numbers beat guesses.
RecWords: t.CArray[uint32, 12]
RecWords[0] = 0
if Rec != 0:
if wm.ReadProcessMemory(HProc, t.CPtr(Rec), &RecWords, sizet(24), &Rd) != 0 \
and t.CSizeT(Rd) >= t.CSizeT(24):
if wm.ReadProcessMemory(HProc, t.CPtr(Rec), &RecWords, sizet(48), &Rd) != 0 \
and t.CSizeT(Rd) >= t.CSizeT(48):
Code = RecWords[0]
ExAddr = uint64(RecWords[4]) | (uint64(RecWords[5]) << 32)
AvKind = RecWords[8]
AvAddr = uint64(RecWords[10]) | (uint64(RecWords[11]) << 32)
N: int = stdio.snprintf(_State.LineBuf, t.CSizeT(TRACEBACK_LINE_CAP),
" Trigger=unhandled-exception code=0x%x addr=0x%llx crashthread=%u\n",
Code, ExAddr, wp.GetCurrentThreadId())
" Trigger=unhandled-exception code=0x%x addr=0x%llx"
" av_kind=%u av_addr=0x%llx crashthread=%u\n",
Code, ExAddr, AvKind, AvAddr, wp.GetCurrentThreadId())
EmitLine(N)
if CtxPtr == 0:
EmitText(" fallback: CONTEXT pointer null, crash dump skipped\n")
@@ -1879,6 +1897,17 @@ def DumpCrashThread(Info: VOIDPTR) -> int:
" exe base=0x%llx RIP=0x%llx RSP=0x%llx RBP=0x%llx\n",
Base, Rip, Rsp, Rbp)
EmitLine(N)
# Fault-site argument registers. On Win64 the first four integer arguments travel in
# rcx/rdx/r8/r9, so this line shows exactly what the faulting call received (r12 is the
# register the faulting instruction loaded its pointer from, when the fault is a pointer
# dereference). Printed for crashes only: the Ctrl+C path has no faulting instruction whose
# operands matter, and this is the one datum that distinguishes "wrong argument" from
# "argument right, pointee contents wrong" without another rebuild.
N = stdio.snprintf(_State.LineBuf, t.CSizeT(TRACEBACK_LINE_CAP),
" regs: rcx=0x%llx rdx=0x%llx r8=0x%llx r9=0x%llx r12=0x%llx\n",
uint64(ctx.Rcx), uint64(ctx.Rdx), uint64(ctx.R8), uint64(ctx.R9),
uint64(ctx.R12))
EmitLine(N)
# 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