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:
1 parent
ba50433cd5
commit
bdf2463dfc
2 files changed
+78
-35
No files matched your search
+69
-35
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in new issue
Block a user