fix(traceback): DbgHelp 与 ASan 是状态共享而非互斥,改为探针裁决后启用 StackWalk64

Install:SymInitialize 被拒时(DbgHelp 已被 ASan 运行时占用,进程级状态按句柄唯一)改为调用 SymRefreshModuleList 刷新共享模块表,再由 SymGetModuleBase64(主模块) 探针裁决 SymReady;探针为 0 则回退 0 并打印原因,不静默 0 帧

DumpMainThread:删除 AsanPresent 强制 DbgUsable=0 的一票否决,降级原因串去掉 dbghelp_conflict_with_asan;ASan 探测只保留在诊断行

win32dbghelp.vp:新增 SymRefreshModuleList 声明与用途注释

实测依据:Ctrl+C 现场 RBP=0x0、RIP 在系统 DLL(阻断原因 rbp_zero,不是 ret_out_of_module),帧指针开关救不了这条链;.vp 目标文件已含 .pdata/.xdata,StackWalk64 可穿过系统帧还原真实 .vp 调用链;文档第十一节 B、第十二节 C 同步
This commit is contained in:
TermiNexus committed 2026-09-28 00:23:47 +08:00
1 parent ba50433cd5
commit bdf2463dfc
2 files changed
+78 -35

No files matched your search

+69 -35
View File
@@ -90,8 +90,10 @@ import w32.win32dbghelp as dbg
# 本体时拿不到失败原因,而回调返回 0 会让 StackWalk64 立即终止(恰好是"0 帧")。
# 主路径(**RBP 链手工回溯**):无条件先走它。它拿不到更多帧(<=1)时**先走第二兜底
# = 栈扫描(ScanStack,不依赖帧指针)**;再之后才轮到 dbghelp 加强(编译期开关为 1、
# SymInitialize 成功、两个回调地址与帧缓冲齐备、且**未探测到 ASan 运行时**);任一
# 前提不满足都会打印明确原因(`[diag] symwalk=disabled reason=…`),**不静默 0 帧**。
# Install 时 SymInitialize/SymRefreshModuleList 成功、两个回调地址与帧缓冲齐备、且
# SymGetModuleBase64(主模块) 探针通过;**ASan 运行时存在只表示 DbgHelp 状态共享,不再
# 一票否决**);任一前提不满足都会打印明确原因(`[diag] symwalk=disabled reason=…`),
# **不静默 0 帧**。
# 本轮新增:每步无条件打 `[diag] rbp:` 原始读数 + 判定、以及 `walk: fallback=stack_scan`
# 扫描推断帧(`~` 前缀)——详见第十二节。
# 主路径实现:打印当前 RIP,然后沿主线程 RBP 链读 [rbp](调用者 rbp)与 [rbp+8]
@@ -278,18 +280,23 @@ import w32.win32dbghelp as dbg
# 也把裸名 `_State` 登记进 `Gen.variables`,使其后的字段访问被正常解析。
# 崩溃点再加一道局部量兜底:`FBuf: VOIDPTR = _State.FrameBuf` → 判 `FBuf == None`
# → `memset(FBuf, …)`,把"局部变量"这条最可靠的通道用在最危险的一行上。
# B. **0 帧/符号化互斥**:ASan 运行时会自己初始化 DbgHelp 做符号化,同进程不允许两种
# 不兼容用法(实测 ASan 侧打印 "Failed to initialize DbgHelp!")。故本模块**不再把
# dbghelp 当主路径**,改为:
# B. **0 帧 / DbgHelp 状态共享**:ASan 运行时会自己注册 DbgHelp 符号化器,本模块的
# SymInitialize 因此被拒(DbgHelp 进程上下文是**进程级**状态、按句柄唯一)。但这**不是
# 冲突**:状态是共享的,用同一个 GetCurrentProcess() 伪句柄即可复用其模块表。故 Install
# 的判定改为**取证式**三步:SymInitialize 成功 → SymReady=1;失败 → SymRefreshModuleList
# 刷新共享模块表(官方"外部初始化后再刷新"写法);再统一用 SymGetModuleBase64(主模块)
# 探针裁决——探针为 0 则 SymReady 回退 0 并打印原因,绝不静默 0 帧。回溯路径顺序:
# * 主路径 = `WalkRbpChain`(纯 RBP 链,**零 dbghelp 依赖**);
# * **第二兜底 = `ScanStack` 栈扫描**(零 dbghelp、且**不依赖 RBP / 帧指针**):
# RBP 链只拿到当前 RIP(<=1 帧)时启用(`walk: fallback=stack_scan`),见第十二节;
# * dbghelp 仅作**可选加强**:只有 RBP 链与栈扫描都拿不到更多帧(<=1)且 dbghelp
# 确实可用时才补一次 `WalkViaDbgHelp`。
# * dbghelp 加强:只有 RBP 链拿不到更多帧(<=1)且 DbgUsable != 0(= 探针通过)时才走
# `WalkViaDbgHelp`。它**清空已收帧并整段重建**(顺带丢掉扫描推断帧),是唯一能用 PE
# 展开表(.pdata)穿过**无帧指针的系统帧**、回到 .vp 栈的路径——Ctrl+C 的典型现场
# (主线程阻塞在系统 DLL 里、RBP=0)只有它能还原真实调用链。
# * 降级**必须给原因**(不静默 0 帧):`[diag] symwalk=disabled reason=…`,
# reason ∈ {compile_time_switch, dbghelp_conflict_with_asan, syminitialize_failed,
# reason ∈ {compile_time_switch, syminitialize_failed_or_main_module_missing,
# callback_missing, null_frame_buf};ASan 探测用 GetModuleHandleA 两个常见
# ASan 运行时名(clang_rt / libclang_rt 前缀)。
# ASan 运行时名(clang_rt / libclang_rt 前缀),**只用于诊断行**,不再参与裁决。
# `WalkRbpChain` 同步加固(升级为主路径后它必须"绝不把卡死变成崩溃"):
# * 读 [rbp]/[rbp+8] 一律走 `wm.ReadProcessMemory`(**不直接解引用**野 rbp);
# * **每一步无条件打 `[diag] rbp:` 原始读数 + 判定**(先取证后决策,含被拒步;见第十二节);
@@ -334,6 +341,13 @@ import w32.win32dbghelp as dbg
# `TransPyV/App/lib/core/Gen/Func/FuncGenBase.vp`(属性拼装 `_append_llvm_attr`,154-181)
# 与 `TransPyC/lib/core/Handles/HandlesFunctions.py:1735`(`func.attributes.add('noredzone')`
# 旁)—— 让帧指针成为**函数属性**而非 llc 命令行默认值,从根上免疫"llc 是否采纳默认值"。
# * 【补充实测,Test/LLvmLiteTest Ctrl+C】`RBP=0x0`、RIP 落在系统 DLL:拒绝原因是
# `rbp_zero`(不是 `ret_out_of_module`)⇒ 断链点**不在 .vp 函数**,而在被中断的那一层
# 系统帧。RBP 是 callee-saved,但系统/CRT 的免帧指针代码会把它当**临时寄存器**用
# (`xor ebp,ebp` 是常见的清零写法),故中断现场的 RBP 与"最近一个维护帧指针的祖先"
# 无关。⇒ 结论:**给 .vp 函数保留帧指针无法救这条路**(链在更内层就已断),只有基于
# PE 展开表(.pdata)的回溯(dbghelp!StackWalk64,见第十一节 B)才能从这种现场还原
# 真实调用链。帧指针开关因此**不是**本轮的修法落点,保留在上仅作历史记录。
#
# -----------------------------------------------------------------------------
# 十三、本轮:真 Python traceback 风格输出 + TRACEBACK_DIAG 开关
@@ -378,8 +392,9 @@ extern def GetEnvironmentVariableA(lpName: LPCSTR, lpBuffer: CHARPTR, nSize: DWO
# ---- 编译期开关 -------------------------------------------------------------
# 1 = 允许在**可选加强**路径上使用 dbghelp!StackWalk64(**产物必须链接 -ldbghelp**);
# 注意:本轮起主回溯路径是 WalkRbpChain(零 dbghelp 依赖),dbghelp 只在 RBP 链
# 拿不到更多帧且进程环境允许(无 ASan 冲突)时才补一次(见模块头第十一节 B)。
# 注意:主回溯路径是 WalkRbpChain(零 dbghelp 依赖),dbghelp 只在 RBP 链拿不到更多帧
# 且 Install 探针判定其可用时才补一次——ASan 只是与它**共享** DbgHelp 状态,不再一票
# 否决(见模块头第十一节 B)。
# 0 = 完全不用 dbghelp。切换机制:常量比较在 HandleIf 处被常量折叠,凡出现在
# `if TRACEBACK_USE_DBGHELP != 0:` 分支里的语句(Install 的 SymInitialize/GetProcAddress
# 段、WalkViaDbgHelp 的整个函数体、DumpMainThread 的可选加强调用)在置 0 时整体不生成代码
@@ -445,7 +460,8 @@ class TracebackState():
Installed: int # 1 = SetConsoleCtrlHandler registered
MapOk: int # 1 = sidecar loaded and fully validated (address -> source line)
SymReady: int # 1 = SymInitialize succeeded (dbghelp usable)
SymReady: int # 1 = DbgHelp usable (SymInitialize succeeded, or the shared state of
# another component was adopted AND the module-table probe passed)
ModInited: int # 1 = dbghelp module table contains the main module
Dumping: int # reentrancy flag (readable mirror of the _DumpLock atomic lock)
Seq: int # dump sequence number, +1 per dump entered (concurrency evidence)
@@ -1090,8 +1106,10 @@ def WalkViaDbgHelp(hProcess: HANDLE, hThread: HANDLE, CtxPtr: VOIDPTR,
"""dbghelp!StackWalk64 frame walk; returns the number of frames printed (0 = caller degrades).
Optional enhancement path: the primary path is WalkRbpChain (no dbghelp). Called only when
the RBP chain found no more frames and the caller judged dbghelp usable (no ASan conflict,
see header section 11 B); on conflict the caller never enters.
the RBP chain found no more frames and the caller judged dbghelp usable - that judgement comes
from the Install-time SymGetModuleBase64 probe (see header section 11 B); otherwise the caller
never enters. It is the only path that can cross a frameless system frame (RBP=0 in the Ctrl+C
context) via the PE unwind tables (.pdata) and reach the real .vp stack.
STACKFRAME64 fields are accessed through explicit sub-struct pointers (rawptr[dbg.ADDRESS64]):
the ADDRESS64 structures sit at offsets 0 / 16 / 32 / 48 (= AddrPC / AddrReturn / AddrFrame /
@@ -1222,7 +1240,7 @@ def WalkRbpChain(Rip: uint64, Rbp: uint64, Base: uint64) -> int:
Only an accepted step calls FramePush (authoritative frame); rejected values go to diagnostics
only, never into the stack.
* Zero dbghelp dependency (immune to ASan owning DbgHelp).
* Zero dbghelp dependency (usable even where another component owns DbgHelp's process context).
* All memory reads go through ReadProcessMemory, never dereferencing a wild rbp: a diagnostic
must not turn a hang into a crash.
* A step is accepted only if rbp is non-zero, 8-byte aligned, the read succeeds, [rbp+8] is a
@@ -1542,10 +1560,15 @@ def DumpMainThread(IsBreak: int) -> int:
Rip: uint64 = uint64(ctx.Rip)
Rsp: uint64 = uint64(ctx.Rsp)
Rbp: uint64 = uint64(ctx.Rbp)
# ---- ASan vs dbghelp mutual-exclusion probe + dbghelp usability ----
# ASan initializes DbgHelp itself; the process cannot host both incompatible uses. If an ASan
# runtime is present, dbghelp is not used as the primary path and the reason is printed (never
# silent 0 frames). Both common runtime DLL names are probed.
# ---- dbghelp usability (decided at Install from evidence, never assumed here) ----
# An ASan runtime registers its own DbgHelp symbolizer, but DbgHelp's process context is
# process-global and shared, so its presence is informational only: Install refreshes the
# shared module table and keeps SymReady=1 ONLY when the SymGetModuleBase64(main module) probe
# succeeded. This walk matters because the RBP chain cannot cross a frameless system frame -
# the Ctrl+C case: the main thread is blocked inside a system DLL with RBP=0, so [rbp] is not
# a frame pointer and the chain dies at step 1. StackWalk64 unwinds through the PE unwind
# tables (.pdata) of both the system DLLs and the .vp functions, which is the only path that
# can reconstruct the real .vp stack from such a context.
AsanPresent: int = 0
if GetModuleHandleA("clang_rt.asan_dynamic-x86_64.dll") != None:
AsanPresent = 1
@@ -1556,9 +1579,6 @@ def DumpMainThread(IsBreak: int) -> int:
and _State.FuncTableAddr != None and _State.ModuleBaseAddr != None \
and _State.FrameBuf != None:
DbgUsable = 1
if AsanPresent != 0:
# ASan present: dbghelp is treated as conflicting and disabled; the RBP chain is primary.
DbgUsable = 0
if DiagEnabled() != 0:
N = stdio.snprintf(_State.LineBuf, t.CSizeT(TRACEBACK_LINE_CAP),
" [diag] symwalk: use_dbghelp=%d symready=%d asan=%d dbg_usable=%d\n",
@@ -1569,10 +1589,8 @@ def DumpMainThread(IsBreak: int) -> int:
if TRACEBACK_USE_DBGHELP == 0:
EmitText(" [diag] symwalk=disabled reason=compile_time_switch"
" (TRACEBACK_USE_DBGHELP=0)\n")
elif AsanPresent != 0:
EmitText(" [diag] symwalk=disabled reason=dbghelp_conflict_with_asan\n")
elif _State.SymReady == 0:
EmitText(" [diag] symwalk=disabled reason=syminitialize_failed\n")
EmitText(" [diag] symwalk=disabled reason=syminitialize_failed_or_main_module_missing\n")
elif _State.FuncTableAddr == None or _State.ModuleBaseAddr == None:
EmitText(" [diag] symwalk=disabled reason=callback_missing\n")
else:
@@ -1841,17 +1859,30 @@ def Install() -> int:
if dbg.SymInitialize(wp.GetCurrentProcess(), None, 1) != 0:
_State.SymReady = 1
else:
NoteError("[traceback] WARN: SymInitialize failed (ASan also uses DbgHelp"
" and conflicts with us; RBP chain is the primary path anyway) ", GetLastError())
# ASan initializes DbgHelp itself, conflicting with this module's SymInitialize; probe once
# at install and warn explicitly (the dump then prints
# symwalk=disabled reason=dbghelp_conflict_with_asan and uses the RBP chain).
# SymInitialize fails when another component ALREADY initialized DbgHelp in this
# process (the ASan runtime registers a symbolizer with the same GetCurrentProcess()
# pseudo-handle). DbgHelp's process context is process-global and keyed by that handle,
# so there is no conflict to avoid: refresh the SHARED module table (the documented
# "refresh after a foreign initialize" call) and let the SymGetModuleBase64 probe below
# decide usability from evidence instead of assuming the worst.
ErInit: ULONG = GetLastError()
if dbg.SymRefreshModuleList(wp.GetCurrentProcess()) != 0:
_State.SymReady = 1
EmitText("[traceback] note: DbgHelp already initialized in this process"
" (SymInitialize refused it); shared module table refreshed,"
" SymGetModuleBase64 probe decides StackWalk64 usability\n")
else:
NoteError("[traceback] WARN: SymInitialize/SymRefreshModuleList failed,"
" StackWalk64 unavailable ", ErInit)
# An ASan runtime registers its own DbgHelp symbolizer at startup; that only means the
# state is shared with us, not that it is unusable. Informational only - the
# SymGetModuleBase64 probe below is the decision maker.
HAsan: HANDLE = GetModuleHandleA("clang_rt.asan_dynamic-x86_64.dll")
if HAsan == None:
HAsan = GetModuleHandleA("libclang_rt.asan_dynamic-x86_64.dll")
if HAsan != None:
EmitText("[traceback] WARN: ASan runtime detected, it also initializes DbgHelp;"
" dbghelp symwalk disabled, RBP chain will be the primary path\n")
EmitText("[traceback] note: ASan runtime detected, it shares DbgHelp with this module;"
" the module-table probe below decides whether StackWalk64 can walk\n")
Hdbg: HANDLE = GetModuleHandleA("dbghelp.dll")
if Hdbg != None and Hdbg != INVALID_HANDLE_VALUE:
_State.FuncTableAddr = GetProcAddress(Hdbg, "SymFunctionTableAccess64")
@@ -1859,14 +1890,17 @@ def Install() -> int:
if _State.FuncTableAddr == None or _State.ModuleBaseAddr == None:
EmitText("[traceback] WARN: StackWalk64 callback addresses incomplete,"
" dbghelp enhancement unavailable (RBP chain is the primary path)\n")
# Self-check: does dbghelp's module table really contain the main module? The result feeds
# the dump's "[diag] StackWalk64 first:" / symwalk lines and the dbg_usable decision.
# Self-check (the decision maker for dbg_usable): does dbghelp's module table really
# contain the main module? A 0 here means SymFunctionTableAccess64 gets no
# RUNTIME_FUNCTION and StackWalk64 fails on its first call, so SymReady is dropped back to
# 0 (the dump then prints the explicit symwalk=disabled reason instead of a silent 0 frames).
if _State.SymReady != 0:
if dbg.SymGetModuleBase64(wp.GetCurrentProcess(), uint64(GetModuleHandleA(None))) != 0:
_State.ModInited = 1
else:
_State.SymReady = 0
EmitText("[traceback] WARN: dbghelp module table lacks main module (SymGetModuleBase64=0),"
" StackWalk64 will produce 0 frames\n")
" StackWalk64 unavailable (RBP chain is the primary path)\n")
# (6) register last, once all state is ready. _HandlerReg guards "register once" so a repeated
# callback cannot re-register (and thus cannot repeat the same dump).
if _HandlerReg == 0:
+9
View File
@@ -8,6 +8,8 @@ from w32.win32base import *
# 声明本仓用到的 dbghelp 栈回溯 API 与三个栈帧结构体:
# SymInitialize 初始化 dbghelp 的进程上下文(fInvadeProcess=TRUE 只枚举模块、
# 配合 SYMOPT_DEFERRED_LOADS 不加载 PDB)
# SymRefreshModuleList 本进程的 DbgHelp 已被其它组件初始化(本模块的 SymInitialize 被拒)
# 时,按其既有进程上下文重新枚举模块表("外部初始化后再刷新")
# SymSetOptions 设定 SymInitialize 之后固定的选项集
# SymFunctionTableAccess64 按 RIP 查 RUNTIME_FUNCTION(PE 异常目录;不需要调试符号)
# SymGetModuleBase64 按 RIP 取模块基址
@@ -109,6 +111,13 @@ extern def SymSetOptions(SymOptions: DWORD) -> DWORD
# SYMOPT_DEFERRED_LOADS 时枚举模块不会真的去加载任何符号文件。
extern def SymInitialize(hProcess: HANDLE, UserSearchPath: LPCSTR, fInvadeProcess: BOOL) -> BOOL
# BOOL SymRefreshModuleList(HANDLE hProcess);
# DbgHelp 的进程上下文是**进程级**状态、按句柄唯一:同一句柄重复 SymInitialize 会被拒
# (本仓实测 ASan 运行时先注册了自己的符号化器,故本模块的 SymInitialize 失败)。这不是
# 冲突——状态是共享的,用同一个 GetCurrentProcess() 伪句柄调用本函数即可按其既有上下文
# 重新枚举模块表,使 StackWalk64 拿到最新的 RUNTIME_FUNCTION 表。用法见 traceback.vp 的 Install。
extern def SymRefreshModuleList(hProcess: HANDLE) -> BOOL
# PVOID SymFunctionTableAccess64(HANDLE hProcess, DWORD64 AddrBase);
extern def SymFunctionTableAccess64(hProcess: HANDLE, AddrBase: ULONGLONG) -> VOIDPTR