diff --git a/traceback.vp b/traceback.vp index 17d5048..546bc93 100644 --- a/traceback.vp +++ b/traceback.vp @@ -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: diff --git a/w32/win32dbghelp.vp b/w32/win32dbghelp.vp index 0d9f631..7ca144f 100644 --- a/w32/win32dbghelp.vp +++ b/w32/win32dbghelp.vp @@ -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