新增 includes/dbgstack.vp: 挂起主线程取 x64 CONTEXT + RBP 链逐步取证 + 栈扫描兜底(4KiB 分块 ReadProcessMemory);按 <exe>.stackmap 下界查表输出 <文件>.vp:行 (函数);控制台 ASCII 输出并双写 UTF-8 <exe>.stackdump.txt;ASan 与 DbgHelp 冲突显式降级不静默;lock cmpxchg 原子重入(不用 InterlockedCompareExchange: x64 上是编译器内部函数、无导出符号)。 新增/补 w32 头: win32dbghelp.vp(新)、SetConsoleCtrlHandler、x64 CONTEXT、ReadProcessMemory。 parser.vp/tokens.vp: [N]T/[]T/const [N]T 脱糖为 t.CArray;新增 union NAME: 语句(Keyword.Union 追加表尾);list[rawptr[AST]] elem_size 2/4 改 8;新增 _record_parse_err_ctx 把 token 类型与前后 token 并入错误消息,去掉 [AST] SyntaxError 遗留单行 printf。 asm.vp: 整文件迁移到 __asm__/asminp/asmout/裸 ASM_DESCR。
1743 lines
108 KiB
Plaintext
1743 lines
108 KiB
Plaintext
import t, c
|
||
from stdint import *
|
||
from w32.win32base import *
|
||
import string
|
||
import stdio
|
||
import stdlib
|
||
import w32.win32file as wf
|
||
import w32.win32console as wc
|
||
import w32.win32process as wp
|
||
import w32.win32memory as wm
|
||
import w32.win32dbghelp as dbg
|
||
|
||
# 转储重入保护(见 DumpMainThread)**不使用 InterlockedCompareExchange**:该名字在 x64 上是
|
||
# winnt.h 的编译器内部函数(映射到 lock cmpxchg 指令),**不是可链接符号**,直接引用会在链接期
|
||
# 报 undefined symbol(实测:ld.lld: undefined symbol: InterlockedCompareExchange)。
|
||
# 本模块改用普通 32 位标志做「检查并占用」:并发回调最多导致一次重复转储(诊断工具可接受),
|
||
# 且不引入任何链接期依赖。
|
||
|
||
# =============================================================================
|
||
# dbgstack — 运行时「按键取栈」模块(Ctrl+C / Ctrl+Break 时转储主线程调用栈)
|
||
#
|
||
# 解决的问题:编译器(或任何长任务)卡死时,用户按一个键就能拿到「现在卡在哪一行」
|
||
# 的完整调用栈——从 CRT 入口(mainCRTStartup / __tmainCRTStartup)到当前帧。
|
||
#
|
||
# -----------------------------------------------------------------------------
|
||
# 一、触发语义(SetConsoleCtrlHandler 回调)
|
||
# -----------------------------------------------------------------------------
|
||
# Ctrl+C (CTRL_C_EVENT) → 打印调用栈后**返回 FALSE**(交还系统默认处理 ⇒ 进程终止)
|
||
# Ctrl+Break (CTRL_BREAK_EVENT)→ 打印调用栈后**返回 TRUE**(已处理 ⇒ 进程继续运行)
|
||
# 其它控制事件(关窗/注销/关机,CTRL_CLOSE_EVENT 等)→ 一律返回 FALSE,不转储、不干预。
|
||
# 回调由系统在**新建的线程**上执行,不是主线程——所以「当前执行到哪」必须靠
|
||
# SuspendThread(主线程) + GetThreadContext 取得主线程寄存器,再回溯主线程的栈。
|
||
# 同一时刻**只允许一次转储**:连续按键会由系统在多个新线程上并发回调,而它们共用
|
||
# 同一个 LineBuf 与输出句柄 ⇒ 输出互相撕裂(实测出现过"降级:"行重复且交织)。
|
||
# 故 DumpMainThread 入口用内联汇编 `lock cmpxchg`(AtomicTryEnterDump)对 _DumpLock
|
||
# 做原子"检查并占用":抢占失败的调用直接返回(不挂起主线程、不输出任何内容)。
|
||
# 每次转储还会打印自增 Seq 与回调线程 id:若两条转储记录的 Seq 不同 ⇒ 顺序两次;
|
||
# 同一 Seq 的行却互相截断 ⇒ 说明仍存在共享缓冲的并发写(据此定位,不再靠猜)。
|
||
#
|
||
# -----------------------------------------------------------------------------
|
||
# 二、安装时机与总开关(本模块的开关落地点)
|
||
# -----------------------------------------------------------------------------
|
||
# 约定:**旁文件存在** ⟺ 编译时用了 TPC 的 `--ctrlc-stack`。
|
||
# Install() 先求 <当前 exe 完整路径> + ".stackmap";该文件不存在(CreateFileA 报
|
||
# ERROR_FILE_NOT_FOUND)则**立刻返回 0,零副作用**(不分配、不注册 handler);
|
||
# 存在才注册 SetConsoleCtrlHandler。故 TPC 侧无需生成任何其它开关代码,只需在
|
||
# 启用该选项时让产物链接 dbghelp(见第五节)并调用一次 Install()(见第六节)。
|
||
# 文件存在但其头部/记录区非法时:打印可定位的警告并把映射标记为不可用——调用栈仍
|
||
# 照常转储(只有地址),不会静默退出(诊断工具取不到信息时给降级信息,不掩盖错误)。
|
||
#
|
||
# -----------------------------------------------------------------------------
|
||
# 三、旁文件(<exe>.stackmap)格式 —— 小端
|
||
# -----------------------------------------------------------------------------
|
||
# 头部 20 字节:magic="TPCS"(4B) + u32 version=1 + u32 entry_count + u32 str_count
|
||
# + u32 str_bytes
|
||
# 记录区 entry_count × 20B(**按 rva 升序**):u64 rva; u32 file_off; u32 line; u32 func_off
|
||
# 字符串池 str_bytes 字节:\0 结尾 UTF-8 串;file_off/func_off 是池内偏移
|
||
# (func_off = 0 表示无函数名)
|
||
# 运行时:GetModuleHandleA(NULL) 取映像基址,帧地址 - 基址 = rva,再二分查找记录区。
|
||
# 命中 → 打印 "<文件>:<行> (<函数名>)";未命中 → 打印 "+0x<rva> (unknown)";
|
||
# 地址落在主模块之外(更低,典型为 DLL/系统帧)→ 打印 "0x<地址> (external)"。
|
||
# 装载时做一次全表校验(rva 严格升序、字符串偏移落在池内),不合法即禁用映射——
|
||
# 二分查找依赖升序,破损的表会给出**错行号**,比"只打地址"更坏。
|
||
# 校验失败一律**逐项打印挂在哪一项**(含头部实读的 magic/version/count/str_bytes
|
||
# 与文件实际大小),失败原因不外泄为一句"旁文件非法"。
|
||
# ⚠ 读字节必须逐个 & 0xFF:Viper 的 Buf[i] 是**有符号** char,>= 0x80 的字节若直接
|
||
# 转 u32 会符号扩展(例:记录数 1000=0x3E8 的首字节 0xE8 → 0xFFFFFFE8),使头部的
|
||
# entry_count/str_bytes 变成天文数字、`20+count*20+str_bytes > 文件大小` 必然成立
|
||
# ⇒ 映射永远装载失败(这正是"源行映射=无"的真因,详见 ReadU32)。
|
||
#
|
||
# -----------------------------------------------------------------------------
|
||
# 四、回溯两条路径与降级条件(**本轮已主副互换,见第十一节 B**)
|
||
# -----------------------------------------------------------------------------
|
||
# ⚠ 先读第十一节 B:本轮起**主路径 = RBP 链**(零 dbghelp 依赖),dbghelp 降为
|
||
# "可选加强"。下面这段 dbghelp 说明仍有效,只是它描述的是**加强路径**的历史细节。
|
||
# 加强路径(可选):dbghelp!StackWalk64(MachineType=IMAGE_FILE_MACHINE_AMD64)。
|
||
# SymSetOptions(SYMOPT_DEFERRED_LOADS|SYMOPT_UNDNAME|SYMOPT_NO_PROMPTS) +
|
||
# SymInitialize(GetCurrentProcess(), NULL, **TRUE**) —— TRUE 是硬要求:它让 dbghelp
|
||
# 枚举已加载模块填进自己的模块表;置 FALSE 时模块表为空,SymFunctionTableAccess64
|
||
# 取不到 RUNTIME_FUNCTION、SymGetModuleBase64 返回 0,StackWalk64 **首次调用即失败**
|
||
# (表现为"符号回溯=可用但 0 帧")。配合 DEFERRED_LOADS 不会真的去加载 PDB。
|
||
# 三个回调地址里 FunctionTableAccess/GetModuleBase 仍在 Install 时用 GetProcAddress
|
||
# 取(本仓既有的动态解析写法,先例 VET/App/lib/VGuard.vp:852-867):避免依赖
|
||
# 「跨模块导入的函数名作为函数指针实参」这一未经本仓验证的用法,取不到就传 NULL。
|
||
# ReadProcessMemory 回调改用**本模块内定义**的 ReadMemCb(同模块内"函数名作
|
||
# 函数指针实参"已由 CtrlHandler 先例验证):直接传 kernel32!ReadProcessMemory
|
||
# 本体时拿不到失败原因,而回调返回 0 会让 StackWalk64 立即终止(恰好是"0 帧")。
|
||
# 主路径(**RBP 链手工回溯**):无条件先走它。它拿不到更多帧(<=1)时**先走第二兜底
|
||
# = 栈扫描(ScanStack,不依赖帧指针)**;再之后才轮到 dbghelp 加强(编译期开关为 1、
|
||
# SymInitialize 成功、两个回调地址与帧缓冲齐备、且**未探测到 ASan 运行时**);任一
|
||
# 前提不满足都会打印明确原因(`[diag] symwalk=disabled reason=…`),**不静默 0 帧**。
|
||
# 本轮新增:每步无条件打 `[diag] rbp:` 原始读数 + 判定、以及 `walk: fallback=stack_scan`
|
||
# 扫描推断帧(`~` 前缀)——详见第十二节。
|
||
# 主路径实现:打印当前 RIP,然后沿主线程 RBP 链读 [rbp](调用者 rbp)与 [rbp+8]
|
||
# (返回地址)逐帧回溯;要求 rbp 8 字节对齐、严格向高地址增长(`[rbp] > rbp`)、
|
||
# 单步跨度 <= DBGSTACK_RBP_MAX_STEP、返回地址落在主模块基址区间内,否则立即停止
|
||
# (防野指针把回溯带飞);**读内存一律经 ReadProcessMemory,绝不直接解引用野 rbp**。
|
||
# RBP 链只覆盖保持帧指针的函数;帧指针一旦未保留([rbp+8] 不是返回地址)链即在第 1
|
||
# 步断——此时由 ScanStack 兜底,另叠一层 dbghelp 加强(可用时)。
|
||
# --ctrlc-stack 时 TPC 给 llc 加 `-frame-pointer=all`(Phase2Translator.py:2187-2193);
|
||
# 该 flag 是否真正生效见第十二节 C 的调查结论与修法落点。
|
||
# 【取证:先取证,再决策】每次转储在 banner 之后**无条件**打印一组前置诊断——
|
||
# Seq / 回调线程 id / 主线程 id / Dumping 进入前值 / MapOk / SymReady / ModInited /
|
||
# SymGetModuleBase64(主模块) 的**实际返回值** / 主模块基址 / 帧缓冲与两个回调地址
|
||
# 是否就位;进入 dbghelp 主路径后,**首帧**再无条件打印 StackWalk64 的返回码、传入的
|
||
# machine/RIP/RSP/RBP、返回的 AddrPC,以及 SymFunctionTableAccess64(proc, AddrPC) 的
|
||
# 返回值。这些行**先于任何降级判断**产出,使"0 帧"的根因可直接判定(见第九节)。
|
||
#
|
||
# -----------------------------------------------------------------------------
|
||
# 五、链接期依赖(TPC 侧必须知道)
|
||
# -----------------------------------------------------------------------------
|
||
# dbghelp(-ldbghelp)+ kernel32(既有)。SymInitialize / StackWalk64 / SymSetOptions /
|
||
# SymGetModuleBase64 / SymFunctionTableAccess64 是**静态**引用(后两个用于首帧取证诊断),
|
||
# 产物必须能解析 dbghelp 的导入库,否则链接失败。若不便新增 -ldbghelp,
|
||
# 把 DBGSTACK_USE_DBGHELP 置 0:该常量比较会在编译期把唯一含 dbghelp 调用的分支
|
||
# 整体去掉(见 DBGSTACK_USE_DBGHELP 的注释),只保留 RBP 链回溯。
|
||
#
|
||
# -----------------------------------------------------------------------------
|
||
# 六、TPC 侧调用入口(符号与签名)
|
||
# -----------------------------------------------------------------------------
|
||
# import dbgstack
|
||
# dbgstack.Install() # 返回 int:1=handler 已注册;0=未安装(无旁文件/失败)
|
||
# 调用时机:进程启动期一次即可。两种等价做法——
|
||
# (a) 在入口 main 的第一条语句调用;
|
||
# (b) 注入一个 @llvm.global_ctors(.CRT$XCU 语义)构造器调用该符号(本仓先例:
|
||
# TransPyC/lib/core/LLVMCG/BaseGen.py:2061-2097 用 llvm.global_ctors 注册
|
||
# Ctrl+C 处理器)。构造器在 CRT 初始化期执行,能覆盖「main 之前就卡住」的场景。
|
||
# 本模块自身**没有**模块级构造器(状态对象由 Install 懒分配),故与 TPC 注入的
|
||
# 构造器不存在初始化顺序依赖,重复调用是幂等的。
|
||
# ⚠ 本模块把「主线程」定义为**调用 Install() 的那个线程**(Install 里 GetCurrentThreadId
|
||
# 记录其 id,回调时挂起/回溯的就是它)。故必须在真正的入口线程调用(main 或 CRT
|
||
# 构造器所在线程);若在启动期的工作线程里调用,转储的将是那个线程的栈。
|
||
#
|
||
# -----------------------------------------------------------------------------
|
||
# 七、为什么直接 WriteFile 而不是 printf
|
||
# -----------------------------------------------------------------------------
|
||
# 转储发生在「主线程已被挂起」的时刻:若主线程恰好停在 CRT 的 stdout 锁内,回调线程
|
||
# 再调用 printf 会**永久等待同一把锁**,把"卡死"变成"按了键彻底不动"。
|
||
# 故输出走 kernel32!WriteFile(GetStdHandle(STD_ERROR_HANDLE))(无用户态 CRT 流锁),
|
||
# 格式化走 CRT snprintf(只写内存缓冲,不碰 FILE 锁)。stderr 的另一好处:stdout 常被
|
||
# 管道占用(如被父工具读取),stderr 一般直达终端。句柄与全部缓冲都在 Install 时备好,
|
||
# 回调线程**零分配**(MemBuddy 非线程安全,回调里绝不触碰池)。
|
||
#
|
||
# -----------------------------------------------------------------------------
|
||
# 七之二、转储**同时落文件** + 控制台纯 ASCII(本轮的两次修正)
|
||
# -----------------------------------------------------------------------------
|
||
# (a) 落文件(解决"没有 Break 键"与"控制台代码页吞字"):Install 成功时以
|
||
# `<exe 完整路径> + ".stackdump.txt"` 打开一个**追加写**句柄(OPEN_ALWAYS +
|
||
# FILE_APPEND_DATA,不存在则创建),EmitLine 把**同一份内容**既写控制台又追加落盘。
|
||
# 文件是 UTF-8 字节原样,不受控制台代码页影响;每次转储在段前追加一小段分隔头
|
||
# (本地时间戳/pid/触发键/主线程 id/Seq,中文标签,仅文件)。文件写失败**不阻断**
|
||
# 控制台,且只提示一次(见 EmitToFile)。Install 成功时打印一行
|
||
# `[dbgstack] stack dump also written to: <路径>` 便于确认去向。
|
||
# (b) 控制台纯 ASCII:原先的运行时文本是中文(UTF-8 字节),而 cmd 的控制台输出代码页
|
||
# 默认为 936(GBK)——WriteFile 到控制台会按 936 解释 UTF-8 字节,一段多字节在多次
|
||
# WriteFile 之间被拆开/重组,表现为**整行消失**(最长的"首帧"行首当其冲)或相邻行错位、
|
||
# 看似"重复且残缺"(含 '�')。故**运行时输出到控制台的文本一律改英文/ASCII**
|
||
# (banner / [diag] / Trigger= / exe base= / walk: primary=rbp_chain / frames=N 等),
|
||
# 任何代码页下都可读、可 grep;`<文件>.vp:<行>` 帧行保留源路径原文(ASCII 分隔符)。
|
||
# 中文仅保留在:本文件的注释/文档串、以及**只写文件**的分隔头。
|
||
#
|
||
# -----------------------------------------------------------------------------
|
||
# 八、依赖的既有声明(未改动)与本模块新增声明
|
||
# -----------------------------------------------------------------------------
|
||
# 既有(`from w32.win32base import *` 取裸名):GetModuleHandleA / GetProcAddress /
|
||
# CloseHandle / GetLastError / INVALID_HANDLE_VALUE / ERROR_FILE_NOT_FOUND /
|
||
# LARGE_INTEGER / SYSTEMTIME / GetLocalTime / HANDLE / BOOL / ULONG / VOIDPTR;
|
||
# wf.CreateFileA / wf.ReadFile / wf.WriteFile / wf.GetFileSizeEx / wf.GetStdHandle /
|
||
# wf.GENERIC_READ / wf.FILE_SHARE_READ / wf.FILE_SHARE_WRITE / wf.OPEN_EXISTING /
|
||
# wf.OPEN_ALWAYS / wf.FILE_ATTRIBUTE_NORMAL /
|
||
# wf.STD_ERROR_HANDLE / wf.STD_OUTPUT_HANDLE;
|
||
# wp.OpenThread / wp.SuspendThread / wp.ResumeThread / wp.GetCurrentThreadId /
|
||
# wp.GetCurrentProcess / wp.GetCurrentProcessId / wp.THREAD_SUSPEND_RESUME /
|
||
# wp.THREAD_GET_CONTEXT;
|
||
# string.memset / string.strcpy / stdlib.malloc / free / c.Addr。
|
||
# 本轮新增:wc.SetConsoleCtrlHandler / wc.CTRL_C_EVENT / wc.CTRL_BREAK_EVENT;
|
||
# wp.CONTEXT(x64 完整布局)/ wp.CONTEXT_FULL / wp.GetThreadContext;
|
||
# wm.ReadProcessMemory(由本模块 ReadMemCb 包装后作 StackWalk64 回调);
|
||
# w32/win32dbghelp.vp(STACKFRAME64/ADDRESS64/KDHELP64 + SymSetOptions/
|
||
# SYMOPT_* + SymInitialize/SymFunctionTableAccess64/SymGetModuleBase64/
|
||
# StackWalk64);
|
||
# 转储文件:wf.WriteFile 追加写 <exe>.stackdump.txt + 自备常量
|
||
# DBGSTACK_FILE_APPEND_DATA(FILE_APPEND_DATA,wf 未定义);
|
||
# 转储重入保护用内联汇编的一条 `lock cmpxchg`(AtomicTryEnterDump)对模块级
|
||
# 32 位 _DumpLock 做原子"检查并占用"(不用 InterlockedCompareExchange:x64 上是
|
||
# 编译器内部函数、无导出符号、不可链接)。
|
||
#
|
||
# -----------------------------------------------------------------------------
|
||
# 九、"0 帧"的可执行结论(哪种情形 → 看哪个现场字段)
|
||
# -----------------------------------------------------------------------------
|
||
# 首帧取证两行(" [diag] StackWalk64 first: ret=… addrpc=… fta=…" 与紧随其后的
|
||
# " [diag] StackWalk64 in: machine=… rip=… rsp=… rbp=…"——原为单条 143 字符长行,
|
||
# 拆成两条短行以消除窄终端折行造成的"显示残缺"假象,字段一个不减)
|
||
# 与前置诊断行("[diag] Seq=… … modinited=…" / "SymGetModuleBase64(main)=…")合起来直接给出结论:
|
||
# (1) modinited == 0 且 SymGetModuleBase64(main) == 0 ⇒ dbghelp 的模块表不含主模块。
|
||
# 根因是 SymInitialize 的 fInvadeProcess 参数(必须 TRUE)或调用时机(要在进程启动期、
|
||
# 模块枚举可用时调用,本模块放在 Install 里)。修法:确认 Install 里 SymInitialize 的
|
||
# 第三个实参为 1,且 Install 在入口线程、模块全部加载后调用;必要时改用
|
||
# SymRefreshModuleList 重新枚举。
|
||
# (2) 首帧 ret == 0,且紧邻有 "[diag] readmem failed addr=… size=… GetLastError=…"
|
||
# ⇒ 读栈内存失败。`ReadProcessMemory` 在回调里返回 0 会让 StackWalk64 立刻终止(=0 帧)。
|
||
# 修法:核对 ReadMemCb 的宽度口径(dbghelp 侧是 DWORD nSize + LPDWORD,kernel32 侧是
|
||
# SIZE_T,本模块已用本地 sizet 承接再写回 DWORD)与进程句柄/权限。
|
||
# (3) 首帧 ret != 0 但返回 addrpc == 0,或 fta(SymFunctionTableAccess64(addrpc))== 0
|
||
# ⇒ 上下文/模块基址口径问题(传给 StackWalk64 的 RIP/RSP/RBP 或模块基址不对)。
|
||
# 修法:核对 CONTEXT 的 Rip/Rsp/Rbp 取值与 GetModuleHandleA(NULL) 基址。
|
||
# 补充:首帧行里 `ret=not-called` 表示 SymInitialize/回调地址/帧缓冲前提不全、根本没调用
|
||
# StackWalk64(此时该行仍会产出,保证"转 RBP 链"之前永远有首帧证据)。
|
||
# 三者互斥且可判:只要把上面两组打印字段与判据对照,就能**当场**确定属于哪一类。
|
||
#
|
||
# -----------------------------------------------------------------------------
|
||
# 十、本轮新增的两组定位打印(应答"walk 到底跑到没有"与"重复残缺真因")
|
||
# -----------------------------------------------------------------------------
|
||
# A. walk 是否被调用、走了哪条出口(全部走 DumpMainThread / WalkViaDbgHelp 的既有输出通道):
|
||
# * DumpMainThread 在**调用前无条件**打 " [diag] walk: enter use_dbghelp=… symready=…
|
||
# modinited=… frame_buf=… mapok=… asan=…",**调用后无条件**打 " [diag] walk: leave
|
||
# frames=… mapok=…"。两行都在 ⇒ walk 一定进了 RBP 主路径(dbghelp 加强可选)。
|
||
# * 若 **连 walk: enter 都不出现** ⇒ DumpMainThread 在本行之前就返回了:只可能是三处
|
||
# fallback(OpenThread / SuspendThread / GetThreadContext 失败,各自都有
|
||
# "fallback: … cannot dump stack" 行)——据该行即可定位被跳过的分支。
|
||
# * WalkViaDbgHelp 的**每一条 return** 都先打 " [diag] walk: return reason=<…> frames=…",
|
||
# reason ∈ {null_state, disabled, null_frame_buf, null_context, stackwalk_first_failed,
|
||
# stackwalk_failed, addrpc_zero, frame_limit}(含义见函数内注释),故 walk 的走向
|
||
# 必可判定、不再靠猜。本轮起主路径是 RBP 链,DumpMainThread 另打
|
||
# `walk: primary=rbp_chain` / `walk: rbp_chain frames=…` / `walk: dbghelp enhancement engaged`;
|
||
# dbghelp 不可用时另打 `[diag] symwalk=disabled reason=…`(见第十一节 B)。
|
||
# B. "重复残缺"真因的判别凭据(唯一凭据 = 行号是否重复):
|
||
# * EmitLine 给**每一行**加全局递增前缀 " [dump#<k>] "(k 由 _DumpLineNo 每次 +1)。
|
||
# * 同一段文本出现多份且**序号重复**(同一 k 出现两次)⇒ 该文本被写了两次,即**并发写
|
||
# 同一 LineBuf**;此时才需要"输出互斥"。同一序号不可能出自单线程的一次 EmitLine。
|
||
# * 文本被**截断/换行错位**但**序号严格递增、无重复** ⇒ 内容只写了一次(乱不了号),
|
||
# 只可能是**终端长行折行/回显**层的显示效果;修法是缩短长行(本轮已把三条最长行拆短),
|
||
# **不动锁**。
|
||
# * 本轮补打:`[dump#skip]` 仅在使用输出互斥时才需要;本模块判定为"显示层",
|
||
# 故未加互斥、未打印该标记(判据与结论见交付说明)。
|
||
#
|
||
# -----------------------------------------------------------------------------
|
||
# 十一、本轮两根因修复(崩溃 = 属性访问被降级;0 帧 = dbghelp 与环境互斥)
|
||
# -----------------------------------------------------------------------------
|
||
# A. **崩溃根因不是分配失败,而是 TPC 把 `_State.<字段>` 静默降级成常量 null。**
|
||
# 实测证据(构建产物 IR,模块前缀 = 本文件 sha1 前 16 位 9b4ee486e08837c5):
|
||
# * 源码 `string.memset(_State.FrameBuf, 0, …)` 被生成为
|
||
# `call i8* @memset(i8* null, i32 0, i64 248)` —— 参数就是字面 null;
|
||
# 同一函数里 `_State.LineBuf`(snprintf)、`_State.FuncTableAddr / ModuleBaseAddr`
|
||
# (StackWalk64 的两个回调槽)同样全被折成 null。
|
||
# * `Install` 里的 `calloc(1,248)` 与其"分配失败即 return 0"的判空**都在且正确**
|
||
# (IR 里 `@calloc` 返回值被正常 store 到字段 20),故**不是** calloc 返回 NULL。
|
||
# 机理(TPC 侧,只报不改):`TransPyC/lib/core/Handles/HandlesExprAttr.py`
|
||
# `_resolve_var_classname`(约 L285-293)对 `rawptr[X]` 形态的模块级全局只看
|
||
# `GVar.type.pointee`(= `X*`,仍是指针)⇒ 认不出 `X`,返回 None;兜底的
|
||
# `_recover_struct_class`(约 L723-740)虽能多解一层指针,但要求 `X` 已进入
|
||
# **函数级**的 `Gen.variables/global_vars`(`HandlesFunctions.py:1848` 每函数清空),
|
||
# 而该登记只发生在"裸名 `X` 被求值一次"时(`HandlesExpr.py:479/487`)。
|
||
# ⇒ 凡**一上手就直接读写 `_State.<字段>`、且函数内从未单独求值过裸名 `_State`
|
||
# 的函数**,`_handle_attr_name` 在 L507-508 返回 None:
|
||
# - 作为调用实参 ⇒ 被静默替换为 null(→ memset(null) 崩溃);
|
||
# - 作为 `if X == None:` 的条件 ⇒ 整个 `if` 被 `HandlesIf` L170-171 丢弃
|
||
# (→ 我们原来写的 `if _State.FrameBuf == None: …` **根本没被生成**)。
|
||
# 受影响函数(IR 佐证:这些函数体内 `@_State` 的 load 次数为 0):
|
||
# `EntryRva` / `LookupRva` / `EmitFrame` / `WalkViaDbgHelp`。
|
||
# 其余函数(`EmitLine` / `DumpMainThread` / `ReadMemCb` / `Install` …)入口都有
|
||
# `_State == None` 判空 ⇒ 裸名已登记 ⇒ 字段访问正常(这就是为什么
|
||
# `DumpMainThread` 打出的 `frame_buf=1`、`modinited=1` 都是真的)。
|
||
# 本模块的修法(**不去改 TPC**):凡直接使用 `_State.<字段>` 的函数,入口
|
||
# **第一句就写 `if _State == None: …`**(即使是冗余判空)——它既是真正的兜底,
|
||
# 也把裸名 `_State` 登记进 `Gen.variables`,使其后的字段访问被正常解析。
|
||
# 崩溃点再加一道局部量兜底:`FBuf: VOIDPTR = _State.FrameBuf` → 判 `FBuf == None`
|
||
# → `memset(FBuf, …)`,把"局部变量"这条最可靠的通道用在最危险的一行上。
|
||
# B. **0 帧/符号化互斥**:ASan 运行时会自己初始化 DbgHelp 做符号化,同进程不允许两种
|
||
# 不兼容用法(实测 ASan 侧打印 "Failed to initialize DbgHelp!")。故本模块**不再把
|
||
# dbghelp 当主路径**,改为:
|
||
# * 主路径 = `WalkRbpChain`(纯 RBP 链,**零 dbghelp 依赖**);
|
||
# * **第二兜底 = `ScanStack` 栈扫描**(零 dbghelp、且**不依赖 RBP / 帧指针**):
|
||
# RBP 链只拿到当前 RIP(<=1 帧)时启用(`walk: fallback=stack_scan`),见第十二节;
|
||
# * dbghelp 仅作**可选加强**:只有 RBP 链与栈扫描都拿不到更多帧(<=1)且 dbghelp
|
||
# 确实可用时才补一次 `WalkViaDbgHelp`。
|
||
# * 降级**必须给原因**(不静默 0 帧):`[diag] symwalk=disabled reason=…`,
|
||
# reason ∈ {compile_time_switch, dbghelp_conflict_with_asan, syminitialize_failed,
|
||
# callback_missing, null_frame_buf};ASan 探测用 GetModuleHandleA 两个常见
|
||
# ASan 运行时名(clang_rt / libclang_rt 前缀)。
|
||
# `WalkRbpChain` 同步加固(升级为主路径后它必须"绝不把卡死变成崩溃"):
|
||
# * 读 [rbp]/[rbp+8] 一律走 `wm.ReadProcessMemory`(**不直接解引用**野 rbp);
|
||
# * **每一步无条件打 `[diag] rbp:` 原始读数 + 判定**(先取证后决策,含被拒步;见第十二节);
|
||
# * 自洽校验:非 0 / 8 字节对齐 / `[rbp] > rbp` / 单步跨度 <= RBP_MAX_STEP /
|
||
# 返回地址落在主模块基址区间 [Base, Base+DBGSTACK_MODULE_MAX) 内才**接受**该帧。
|
||
#
|
||
# -----------------------------------------------------------------------------
|
||
# 十二、本轮新增:RBP 链逐步原始读数 + 栈扫描兜底(应答"栈走不长")
|
||
# -----------------------------------------------------------------------------
|
||
# 现场(用户实测,`#0` 已正确解析:RIP 在主模块、映射/查表/帧 #0 全对):`#1 = 0x121fa`
|
||
# ——远小于主模块基址、也不是栈地址 ⇒ 链在第 1 步即断。⇒ 先把"断在哪一步、为什么断"
|
||
# 变成可读数据,再补一条**不依赖 RBP**的路径。
|
||
# A. **逐步原始读数**(`WalkRbpChain` 内,前缀 `[diag] rbp:`,**先于任何 reject 早退**):
|
||
# frame=<i> rbp=0x… [rbp]=0x… [rbp+8]=0x… ← 读到的原始 8 字节值
|
||
# frame=<i> rpm_ok=<0/1> rpm_err=<GetLastError> accept=<0/1> reject_reason=<…>
|
||
# reject_reason ∈ {rbp_zero, not_aligned, rpm_failed, ret_zero, ret_out_of_module,
|
||
# not_growing, step_too_large};被接受时 reason=`accept`、accept=1。
|
||
# 仅被接受的步才 EmitFrame(旧实现在校验前就 EmitFrame,会把 `0x121fa` 这类垃圾值当成
|
||
# 一帧 external 打出来,误导读者)。三种断链根因据此一眼可分:读失败 / 值本身不是返回
|
||
# 地址(帧指针未保留)/ 被自洽校验档掉。
|
||
# B. **栈扫描兜底**(`ScanStack`):RBP 链 <=1 帧时启用(`walk: fallback=stack_scan`)。
|
||
# 在 [RSP, RSP+2MiB) 内按 8 字节步长遍历,**4 KiB/块整块读**(失败即停并打地址/err);
|
||
# 候选 = 落在主模块区间、且**相邻 8 字节字(A-8 或 A+8)像栈地址**的字;按值去重、
|
||
# 上限 64(诊断打 `scan_candidates=<N>`);输出帧前缀 `~`(` ~#3 0x…`)明确标注
|
||
# **扫描推断**(可含误报,不与权威帧混淆)。全部读取只经 ReadProcessMemory,
|
||
# 绝不直接解引用主线程内存(诊断设施不许把"卡死"变成"崩溃")。
|
||
# C. **帧指针是否保留**(调查结论与 TPC 侧修法落点,本模块只报不改):
|
||
# * 单一 llc 调用点 = `TransPyC/lib/Projectrans/Phase2Translator.py:2160 _llc_compile`,
|
||
# 它在 `--ctrlc-stack` 打开时把 `-frame-pointer=all` 追加到命令(2187-2193);
|
||
# App 模块(如 `TransPyV/App/lib/core/Gen/Expr/ExprGenOps.vp`,模块 sha1
|
||
# `8a96a2885bae09ca`)走的是**同一个**调用点(2608 的 _dso.ll / 2319 的内联合并),
|
||
# 故 flag 的落点本身没问题。
|
||
# * 缓存失效:App 模块 obj = `output/{sha1}.obj`,`_compile_ll_files` 见其存在即跳过
|
||
# (2621),但 `--ctrlc-stack` 时 `hotspot_on`(1144)为真 ⇒ 1196-1204 会删除旧 obj
|
||
# 强制重编;include 模块另有隔离目录 `includes.binary_ctrlcstack`(3097-3099)+
|
||
# 旧 obj 删除(3181-3190)。⇒ App/include 都不会静默复用"无帧指针的旧 obj"。
|
||
# * 交付结论:若 `[diag] rbp:` 显示 `[rbp+8]` 仍不是返回地址(`ret_out_of_module`),
|
||
# 则说明 `-frame-pointer=all`(llc 默认值级别的开关)没有生效,最稳的改法是在**IR 里
|
||
# 按函数显式标注** `"frame-pointer"="all"` —— 落点
|
||
# `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 是否采纳默认值"。
|
||
# =============================================================================
|
||
|
||
# ---- 编译期开关 -------------------------------------------------------------
|
||
|
||
# 1 = 允许在**可选加强**路径上使用 dbghelp!StackWalk64(**产物必须链接 -ldbghelp**);
|
||
# 注意:本轮起主回溯路径是 WalkRbpChain(零 dbghelp 依赖),dbghelp 只在 RBP 链
|
||
# 拿不到更多帧且进程环境允许(无 ASan 冲突)时才补一次(见模块头第十一节 B)。
|
||
# 0 = 完全不用 dbghelp。切换机制:常量比较在 HandleIf 处被常量折叠,凡出现在
|
||
# `if DBGSTACK_USE_DBGHELP != 0:` 分支里的语句(Install 的 SymInitialize/GetProcAddress
|
||
# 段、WalkViaDbgHelp 的整个函数体、DumpMainThread 的可选加强调用)在置 0 时整体不生成代码
|
||
# ——范式与依据同 includes/memhub.vp 的 MEMBUDDY_THREAD_SAFE 注释("未取分支整体不生成
|
||
# 代码 ⇒ 函数体为空")。故置 0 后本模块不留任何 dbghelp 链接期符号。
|
||
DBGSTACK_USE_DBGHELP: t.CDefine = 1
|
||
|
||
# 触发事件取值见 includes/w32/win32console.vp:CTRL_C_EVENT=0 / CTRL_BREAK_EVENT=1
|
||
# (代码里按 wc.CTRL_C_EVENT / wc.CTRL_BREAK_EVENT 引用,不另立本地别名)
|
||
|
||
# ---- 旁文件格式 -------------------------------------------------------------
|
||
# 头部 20 字节;记录 20 字节/条;magic "TPCS" 小端读作 u32
|
||
DBGSTACK_MAP_MAGIC: t.CDefine = 0x53435054 # "TPCS"('T','P','C','S' 小端)
|
||
DBGSTACK_MAP_VERSION: t.CDefine = 1
|
||
DBGSTACK_MAP_HEADER: t.CDefine = 20 # 头部字节数
|
||
DBGSTACK_ENTRY_SIZE: t.CDefine = 20 # 每条记录字节数
|
||
DBGSTACK_ENTRY_FILEOFF: t.CDefine = 8 # 记录内 file_off 偏移
|
||
DBGSTACK_ENTRY_LINE: t.CDefine = 12 # 记录内 line 偏移
|
||
DBGSTACK_ENTRY_FUNCOFF: t.CDefine = 16 # 记录内 func_off 偏移
|
||
# 旁文件大小上限(纯防御:避免头部的 entry_count/str_bytes 被写坏后要求巨量分配)
|
||
DBGSTACK_MAP_LIMIT: t.CDefine = 67108864 # 64 MiB
|
||
|
||
# ---- 运行时缓冲与上限 -------------------------------------------------------
|
||
DBGSTACK_PATH_CAP: t.CDefine = 4096 # exe 路径 + ".stackmap" 缓冲
|
||
DBGSTACK_LINE_CAP: t.CDefine = 1024 # 单行输出缓冲
|
||
DBGSTACK_MAX_FRAMES: t.CDefine = 64 # 最多打印帧数
|
||
DBGSTACK_RBP_MAX_STEP: t.CDefine = 0x100000 # 相邻帧 rbp 跨度上限(1 MiB)
|
||
# RBP 链自洽校验用:返回地址落在 [Base, Base+本值) 内才认为"这一帧仍属主模块"并继续回溯。
|
||
# 取 128 MiB —— 远超任何实际 PE 映像,只作"明显越界即停"的粗筛(不要求等于 SizeOfImage)。
|
||
DBGSTACK_MODULE_MAX: t.CDefine = 0x8000000
|
||
|
||
# ---- 栈扫描兜底(ScanStack,见第十二节)-------------------------------------
|
||
# 从 RSP 起向**高地址**(栈向高地址方向 = 调用者方向)扫描的窗口上限:
|
||
# [RSP, RSP + DBGSTACK_SCAN_MAX_BYTES)。TransPyV 用 -Wl,--stack,0x1000000(16 MiB)
|
||
# 保留主线程栈,而 [RSP, StackBase) 只是**已用**部分;取 2 MiB 兼顾"调用链够深"与
|
||
# "不把 TEB/堆区误当栈扫描"两个目标(RBP 链只拿到 1 帧时才启用,属诊断增益路径)。
|
||
DBGSTACK_SCAN_MAX_BYTES: t.CDefine = 0x200000 # 2 MiB
|
||
# 分块读取粒度(一次 ReadProcessMemory 读一整块,不逐 8 字节读;读失败即停)。
|
||
DBGSTACK_SCAN_CHUNK_BYTES: t.CDefine = 4096 # 4 KiB/块
|
||
DBGSTACK_SCAN_CHUNK_WORDS: t.CDefine = 512 # = 4096 / 8,块内 u64 字数(缓冲用作局部数组)
|
||
DBGSTACK_SCAN_MAX_HITS: t.CDefine = 64 # 候选返回地址数量上限(诊断里打 scan_candidates=)
|
||
DBGSTACK_SUSPEND_FAILED: t.CDefine = 0xFFFFFFFF # SuspendThread 失败返回值
|
||
# 旁文件后缀见 DBGSTACK_PATH_CAP 注释(".stackmap",9 字符 + NUL);拼接处直接用
|
||
# 字符串字面量,余量按 16 字节判定,不另设常量。
|
||
# 转储文件后缀 ".stackdump.txt" 共 15 字符 + NUL = 16 字节,恰好是上面"余量按 16 字节判定"
|
||
# 的判据上限,故复用同一个余量检查即可,无需再加一道(见 Install 的路径长度校验)。
|
||
# FILE_APPEND_DATA(0x0004) 未在 wf 中定义,本模块自备:以它作 dwDesiredAccess 打开
|
||
# ⇒ 每次 WriteFile 都写到文件末尾(天然"存在则追加");配合 OPEN_ALWAYS ⇒ 不存在则创建。
|
||
DBGSTACK_FILE_APPEND_DATA: t.CDefine = 0x00000004
|
||
|
||
|
||
class DbgStackState():
|
||
"""本模块的**单一**全局状态对象(Install 时一次分配,此后只读;回调线程可读)。
|
||
|
||
缓冲区全部在 Install 阶段分配(回调线程零分配),指针不必再释放——本模块是
|
||
进程级常驻诊断设施,进程存活期间一直可用(无 uninstall 路径)。
|
||
"""
|
||
|
||
Installed: t.CInt # 1 = SetConsoleCtrlHandler 已注册成功
|
||
MapOk: t.CInt # 1 = 旁文件已装载且全表校验通过(可做 地址→源行 映射)
|
||
SymReady: t.CInt # 1 = SymInitialize 成功(dbghelp 可用)
|
||
ModInited: t.CInt # 1 = dbghelp 模块表已含主模块(SymGetModuleBase64 非 0)
|
||
Dumping: t.CInt # 转储重入标志(原子锁 _DumpLock 的可读镜像,供诊断打印)
|
||
Seq: t.CInt # 转储序号:每次成功进入转储 +1(判定并发/顺序两次的凭据)
|
||
ReadMemFailed: t.CInt # 1 = 本模块读栈内存回调已发生过首次失败(现场值见下三字段)
|
||
MissReported: t.CInt # 1 = 源行映射未命中的首次诊断已打印(防刷屏)
|
||
ReadMemAddr: t.CUInt64T # 首次读栈内存失败的地址(供首帧诊断紧邻打印)
|
||
ReadMemSize: t.CUInt32T # 首次读栈内存失败请求的字节数
|
||
ReadMemErr: t.CUInt32T # 首次读栈内存失败时的 GetLastError
|
||
MainThreadId: ULONG # Install 时所在线程(= 主线程)id
|
||
OutHandle: HANDLE # 转储输出去向(优先 stderr,退回 stdout)
|
||
EntryCount: t.CUInt32T # 记录条数
|
||
StrBytes: t.CUInt32T # 字符串池字节数
|
||
MapBase: bytes # 旁文件全文基址(stdlib.malloc)
|
||
StrBase: bytes # 字符串池基址 = MapBase + 20 + EntryCount*20
|
||
FuncTableAddr: VOIDPTR # dbghelp!SymFunctionTableAccess64(StackWalk64 回调 2)
|
||
ModuleBaseAddr: VOIDPTR # dbghelp!SymGetModuleBase64(StackWalk64 回调 3)
|
||
ContextBuf: VOIDPTR # CONTEXT 缓冲(calloc,16 字节对齐)
|
||
FrameBuf: VOIDPTR # STACKFRAME64 缓冲(calloc)
|
||
LineBuf: bytes # 输出行缓冲
|
||
|
||
|
||
# 唯一全局状态:None = 尚未安装(= 旁文件不存在时保持的零副作用状态)。
|
||
# 用「指针 + 懒分配」而不是「模块级对象常量」:模块级对象常量会生成初始化构造器,
|
||
# 与 TPC 侧注入的 Install() 构造器存在不确定的先后顺序(对象构造会把 Install 写入的
|
||
# 状态清零),指针式全局是零初始化、无构造器,天然免疫该问题。
|
||
_State: rawptr[DbgStackState] = None
|
||
|
||
|
||
# ---- 转储原子锁 / 锁定的输出句柄 --------------------------------------------
|
||
# _DumpLock:转储重入锁,0 = 空闲 / 1 = 占用。用内联汇编的一条 `lock cmpxchg`
|
||
# (AtomicTryEnterDump)做原子"检查并占用",消除普通标志"读-改-写"的竞态窗口
|
||
# (两个回调线程同时读到 0 后又同时置 1 ⇒ 共用 LineBuf,输出互相截断)。
|
||
# 32 位、模块级、零初始化(无构造器);_State.Dumping 是它的可读镜像。
|
||
# 不用 InterlockedCompareExchange:x64 上是 winnt.h 的编译器内部函数、非可链接符号。
|
||
_DumpLock: t.CUInt32T = 0
|
||
|
||
# _DumpOut:转储期间**锁定**的输出句柄。DumpMainThread 入口一次性确定,本次转储全程只写它,
|
||
# 不在中途重取(避免同一段输出分叉到 stdout/stderr 两条流而在控制台交错)。
|
||
# Install 也会写入一次(= 与 _State.OutHandle 同源);EmitLine 优先用它。
|
||
_DumpOut: HANDLE = None
|
||
|
||
# _HandlerReg:SetConsoleCtrlHandler 的"只注册一次"守卫。Install 本身对 _State 幂等,
|
||
# 这里再加一道显式守卫(防止"同一按键触发多次回调 ⇒ 同一转储被做好几遍")。
|
||
_HandlerReg: t.CInt = 0
|
||
|
||
# _DumpFile:转储文件的追加写句柄(Install 成功后被置为 OpenAlways + FILE_APPEND_DATA 的句柄)。
|
||
# 控制台与文件**双写**同一份内容(见 EmitLine);为 None/无效时只是不写文件,绝不影响控制台。
|
||
_DumpFile: HANDLE = None
|
||
|
||
# _DumpFileFailWarned:转储文件写失败只提示一次(防刷屏)。回调线程里只做一次普通写,
|
||
# 与 _DumpLock 一样是零初始化的模块级标量,不引入构造器。
|
||
_DumpFileFailWarned: t.CInt = 0
|
||
|
||
# _DumpLineNo:全局递增的**输出行号**(每次 EmitLine +1),由 EmitLine 以
|
||
# " [dump#<k>] " 前缀写进每一行。它是判定"重复残缺"真因的**唯一凭据**:
|
||
# * 若同一段文本在屏幕上出现多份、且**行号重复**(同一 k 出现两次)⇒ 该文本被写了两次,
|
||
# 即存在**并发写同一 LineBuf**(同一序号不可能来自单线程的一次 EmitLine);
|
||
# * 若文本被**截断/换行错位**但**行号严格递增、无重复** ⇒ 内容只写了一次乱不了号,
|
||
# 只能是**终端长行折行/回显**层面的显示效果,程序侧无并发。
|
||
# 单线程写(回调次数由 _DumpLock 唯一化)时,序号必然连续无重复。
|
||
_DumpLineNo: t.CInt = 0
|
||
|
||
|
||
# ============================================================
|
||
# 输出(WriteFile,避开 CRT 流锁;见模块头第七节)
|
||
# 定义在旁文件读取之前:装载期的校验诊断也要走这条唯一输出路径。
|
||
# 控制台文本一律 ASCII(代码页无关、可 grep);同一份内容同时追加到 <exe>.stackdump.txt。
|
||
# ============================================================
|
||
|
||
def WriteConsoleOnly(N: int):
|
||
"""把 LineBuf 的前 N 字节只写**控制台**(不落文件)。
|
||
|
||
从 EmitLine 里拆出来的唯一目的:让"转储文件写失败"的提示能在不进 EmitLine(⇒ 不再触发
|
||
写文件)的前提下直达控制台,避免递归。句柄口径与 EmitLine 完全一致。
|
||
"""
|
||
if _State == None or N <= 0:
|
||
return
|
||
H: HANDLE = _DumpOut
|
||
if H == None or H == INVALID_HANDLE_VALUE:
|
||
H = _State.OutHandle
|
||
if H == None or H == INVALID_HANDLE_VALUE:
|
||
return
|
||
Wr: ULONG = 0
|
||
wf.WriteFile(H, _State.LineBuf, ULONG(N), t.CPtr(c.Addr(Wr)), None)
|
||
|
||
|
||
def EmitToFile(N: int):
|
||
"""把 LineBuf 的前 N 字节**追加**写到转储文件(UTF-8 字节原样落盘,不受控制台代码页影响)。
|
||
|
||
文件句柄在 Install 时以 FILE_APPEND_DATA + OPEN_ALWAYS 打开并常驻(回调线程零打开、零分配)。
|
||
写失败**不影响**控制台:控制台已由 EmitLine 单独先写;这里只提示一次,且提示用局部栈缓冲
|
||
直写控制台句柄(不再进 EmitLine),彻底避免递归。
|
||
"""
|
||
global _DumpFileFailWarned
|
||
if _State == None or N <= 0:
|
||
return
|
||
if _DumpFile == None or _DumpFile == INVALID_HANDLE_VALUE:
|
||
return
|
||
Wr: ULONG = 0
|
||
if wf.WriteFile(_DumpFile, _State.LineBuf, ULONG(N), t.CPtr(c.Addr(Wr)), None) != 0:
|
||
return
|
||
if _DumpFileFailWarned != 0:
|
||
return
|
||
_DumpFileFailWarned = 1
|
||
LB: t.CArray[t.CChar, 160]
|
||
M: int = stdio.snprintf(
|
||
t.CPtr(c.Addr(LB)), t.CSizeT(160),
|
||
"[dbgstack] WARN: stack dump file write failed (further file-write failures not reported)\n")
|
||
if M <= 0:
|
||
return
|
||
H: HANDLE = _DumpOut
|
||
if H == None or H == INVALID_HANDLE_VALUE:
|
||
H = wf.GetStdHandle(wf.STD_ERROR_HANDLE)
|
||
if H == None or H == INVALID_HANDLE_VALUE:
|
||
return
|
||
W2: ULONG = 0
|
||
wf.WriteFile(H, t.CPtr(c.Addr(LB)), ULONG(M), t.CPtr(c.Addr(W2)), None)
|
||
|
||
|
||
def EmitLine(N: int):
|
||
"""把 LineBuf 的前 N 字节写到**控制台 + 转储文件**(N 由 snprintf 返回,含截断保护)。
|
||
|
||
EmitText / EmitFrame / 一切诊断最终都汇聚到本函数 ⇒ 全模块只有这一处输出编排:
|
||
⓪ **行号前缀**(见 _DumpLineNo 注释):在内容前原地插入 " [dump#<k>] "(k 全局递增)。
|
||
实现用 string.memmove 把 LineBuf 现有内容整体后移前缀长度,再写前缀——不改调用方、
|
||
不多占一块缓冲(回调线程零分配),WriteConsoleOnly / EmitToFile 照旧只认 LineBuf。
|
||
它是"重复残缺"真因的判别凭据:序号重复 ⇒ 并发写;序号连续仅是折行 ⇒ 显示层。
|
||
① 控制台(WriteFile,避开 CRT 流锁,见模块头第七节):优先转储期间锁定的 _DumpOut,
|
||
装载期回退到 _State.OutHandle(两者同源,Install 时解析一次);
|
||
② 转储文件(EmitToFile):把**同一份内容**追加落盘(UTF-8,不受控制台代码页影响)。
|
||
文件写失败不阻断控制台(见 EmitToFile)。句柄在转储中途**绝不**重新解析。
|
||
"""
|
||
global _DumpLineNo
|
||
if _State == None or N <= 0:
|
||
return
|
||
if N > DBGSTACK_LINE_CAP - 1:
|
||
N = DBGSTACK_LINE_CAP - 1
|
||
# ⓪ 行号前缀:原地前插(见 _DumpLineNo 注释与上面的 ⓪)
|
||
_DumpLineNo = _DumpLineNo + 1
|
||
Pfx: t.CArray[t.CChar, 32]
|
||
Pl: int = stdio.snprintf(t.CPtr(c.Addr(Pfx)), t.CSizeT(32), " [dump#%d] ", _DumpLineNo)
|
||
if Pl > 0:
|
||
# 前插后总长不得超过缓冲上限(留 1 字节给 '\0')
|
||
if N + Pl > DBGSTACK_LINE_CAP - 1:
|
||
N = DBGSTACK_LINE_CAP - 1 - Pl
|
||
# 连内容末尾的 '\0' 一起后移(N+1),保证仍以 '\0' 结尾
|
||
string.memmove(t.CPtr(_State.LineBuf + t.CInt(Pl)),
|
||
t.CPtr(_State.LineBuf), sizet(N + 1))
|
||
string.memmove(t.CPtr(_State.LineBuf), t.CPtr(c.Addr(Pfx)), sizet(Pl))
|
||
N = N + Pl
|
||
WriteConsoleOnly(N)
|
||
EmitToFile(N)
|
||
|
||
|
||
def EmitText(Text: str):
|
||
"""输出一行**不含格式符**的常量文本(统一走同一缓冲/同一 WriteFile 路径)"""
|
||
if _State == None or _State.LineBuf == None:
|
||
return
|
||
N: int = stdio.snprintf(_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP), "%s", Text)
|
||
EmitLine(N)
|
||
|
||
|
||
def EmitEarly(Text: str):
|
||
"""Install 极早期诊断(_State / LineBuf 尚未就绪)——仍并入**同一条输出通道**。
|
||
|
||
子代理提到的"极早期失败仍用 stdio.printf / stdout"就是这里:stdio.printf 走 stdout,
|
||
而转储走 stderr,两条流会在控制台交错。现统一为:
|
||
* _State 与 LineBuf 就绪 ⇒ 转交 EmitText(同一缓冲、同一句柄);
|
||
* 否则 ⇒ 直接用 WriteFile(GetStdHandle(STD_ERROR_HANDLE))(与转储同一去向)。
|
||
Install 阶段不再是回调线程,临时解析一次句柄无碍(转储期才是锁定的)。
|
||
"""
|
||
if _State != None and _State.LineBuf != None:
|
||
EmitText(Text)
|
||
return
|
||
H: HANDLE = wf.GetStdHandle(wf.STD_ERROR_HANDLE)
|
||
if H == None or H == INVALID_HANDLE_VALUE:
|
||
H = wf.GetStdHandle(wf.STD_OUTPUT_HANDLE)
|
||
if H == None or H == INVALID_HANDLE_VALUE:
|
||
return
|
||
Len: sizet = string.strlen(Text)
|
||
if Len == 0:
|
||
return
|
||
Wr: ULONG = 0
|
||
wf.WriteFile(H, Text, ULONG(Len), t.CPtr(c.Addr(Wr)), None)
|
||
|
||
|
||
def EmitEarlyCode(Text: str, Code: ULONG):
|
||
"""EmitEarly 的带 GetLastError 码版本(同上并入唯一输出通道)。
|
||
|
||
需要一次 snprintf 组行;_State 未就绪时用**局部栈缓冲**(不依赖 _State/LineBuf)。
|
||
"""
|
||
if _State != None and _State.LineBuf != None:
|
||
N: int = stdio.snprintf(_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP),
|
||
"%s(GetLastError=%u)\n", Text, Code)
|
||
EmitLine(N)
|
||
return
|
||
LB: t.CArray[t.CChar, 256]
|
||
N2: int = stdio.snprintf(t.CPtr(c.Addr(LB)), t.CSizeT(256),
|
||
"%s(GetLastError=%u)\n", Text, Code)
|
||
if N2 <= 0:
|
||
return
|
||
H: HANDLE = wf.GetStdHandle(wf.STD_ERROR_HANDLE)
|
||
if H == None or H == INVALID_HANDLE_VALUE:
|
||
H = wf.GetStdHandle(wf.STD_OUTPUT_HANDLE)
|
||
if H == None or H == INVALID_HANDLE_VALUE:
|
||
return
|
||
Wr: ULONG = 0
|
||
wf.WriteFile(H, t.CPtr(c.Addr(LB)), ULONG(N2), t.CPtr(c.Addr(Wr)), None)
|
||
|
||
|
||
def AtomicTryEnterDump() -> t.CInt:
|
||
"""原子"检查并占用" _DumpLock(0→1);返回**进入前的旧值**:0 = 本次占用成功。
|
||
|
||
实现:内联汇编一条 `lock cmpxchg`——eax 为期望旧值 0,ecx 为欲写入的新值 1:
|
||
* 若 [锁] == eax(0) ⇒ [锁] = ecx(1)(占用成功,返回 0);
|
||
* 否则 eax = [锁](占用失败,返回非 0)。
|
||
锁为 32 位、天然 4 字节对齐(_DumpLock 是模块级 t.CUInt32T),与 ecx 源操作数宽度一致。
|
||
不使用 InterlockedCompareExchange(x64 上是编译器内部函数、无导出符号)。
|
||
回调线程调用:只读/写这一个模块级锁与寄存器,不分配内存。
|
||
"""
|
||
Old: t.CUInt32T = 0
|
||
c.Asm(f"""xor eax, eax
|
||
mov ecx, 1
|
||
lock cmpxchg [{c.AsmInp(c.Addr(_DumpLock), t.ASM_DESCR.REG_ANY)}], ecx
|
||
mov {c.AsmOut(Old, t.ASM_DESCR.OUTPUT_REG)}, eax""",
|
||
op=[t.ASM_DESCR.CLOBBER_RAX, t.ASM_DESCR.CLOBBER_RCX,
|
||
t.ASM_DESCR.CLOBBER_MEMORY, t.ASM_DESCR.CLOBBER_CC])
|
||
return t.CInt(Old)
|
||
|
||
|
||
def AtomicLeaveDump():
|
||
"""释放转储锁:_DumpLock → 0(并把可读镜像 _State.Dumping → 0)。
|
||
|
||
只有持锁者会走到这里;持锁期间其他回调线程看到本锁为 1 时直接返回,故这里直接写 0
|
||
是安全的(无并发的"释放者/获取者"同时写)。
|
||
"""
|
||
global _DumpLock
|
||
_DumpLock = 0
|
||
if _State != None:
|
||
_State.Dumping = 0
|
||
|
||
|
||
# ============================================================
|
||
# 旁文件读取(小端,按字节取,规避未对齐访问)
|
||
# ============================================================
|
||
|
||
def ReadU32(Buf: bytes, Off: int) -> t.CUInt32T:
|
||
"""小端读 u32(Buf[Off..Off+3];Buf 为无符号字节基址)
|
||
|
||
⚠ 每个字节都必须 & 0xFF:Viper 的 `Buf[i]` 类型是**有符号** char,字节 >= 0x80
|
||
时 `t.CUInt32T(Buf[i])` 会符号扩展(0xE8 → 0xFFFFFFE8),高位被垃圾污染。
|
||
写端落盘的 entry_count/str_bytes 里只要有一个字节 >= 0x80(记录数 128~255、
|
||
32768~65535… 或字符串池 > 127 字节,几乎必然发生)就会把 `20+count*20+str_bytes`
|
||
算成数十亿 ⇒ 越界校验必然失败 ⇒ 映射永远装载不上。这是"源行映射=无"的真因。
|
||
先转 u32 再与 0xFF 相与,则无论该转换是符号扩展还是零扩展,结果都恒为 0..255。
|
||
"""
|
||
Val: t.CUInt32T = t.CUInt32T(Buf[Off]) & t.CUInt32T(0xFF)
|
||
Val = Val | ((t.CUInt32T(Buf[Off + 1]) & t.CUInt32T(0xFF)) << 8)
|
||
Val = Val | ((t.CUInt32T(Buf[Off + 2]) & t.CUInt32T(0xFF)) << 16)
|
||
Val = Val | ((t.CUInt32T(Buf[Off + 3]) & t.CUInt32T(0xFF)) << 24)
|
||
return Val
|
||
|
||
|
||
def ReadU64(Buf: bytes, Off: int) -> t.CUInt64T:
|
||
"""小端读 u64(由两次 u32 合成,避免 4 字节对齐偏移上的 8 字节未对齐读)"""
|
||
Lo: t.CUInt64T = t.CUInt64T(ReadU32(Buf, Off))
|
||
Hi: t.CUInt64T = t.CUInt64T(ReadU32(Buf, Off + 4))
|
||
return Lo | (Hi << 32)
|
||
|
||
|
||
# ---- 校验失败的定位诊断(一律由下面这一族函数打印后返回 0) -------------------
|
||
|
||
def MapFailPlain(Size: t.CInt64T, Reason: str) -> int:
|
||
"""校验失败但尚未读到头部(尺寸/分配/读取阶段):打印原因 + 文件实际大小。"""
|
||
if _State == None or _State.LineBuf == None:
|
||
return 0
|
||
N: int = stdio.snprintf(
|
||
_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP),
|
||
"[dbgstack] WARN: <exe>.stackmap invalid -- %s (file %lld bytes), addresses only this run\n",
|
||
Reason, Size)
|
||
EmitLine(N)
|
||
return 0
|
||
|
||
|
||
def MapFailHdr(Size: t.CInt64T, Reason: str, Head: bytes) -> int:
|
||
"""校验失败但已读到头部:额外打印**实读**的头部字段(一眼区分口径不符 / 文件写坏)。"""
|
||
if _State == None or _State.LineBuf == None:
|
||
return 0
|
||
N: int = stdio.snprintf(
|
||
_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP),
|
||
"[dbgstack] WARN: <exe>.stackmap invalid -- %s"
|
||
" (read magic=0x%x version=%u entry_count=%u str_count=%u str_bytes=%u; file %lld bytes),"
|
||
" addresses only this run\n",
|
||
Reason, ReadU32(Head, 0), ReadU32(Head, 4), ReadU32(Head, 8), ReadU32(Head, 12),
|
||
ReadU32(Head, 16), Size)
|
||
EmitLine(N)
|
||
return 0
|
||
|
||
|
||
def MapFailEntry(Size: t.CInt64T, Index: int, Reason: str, Got: t.CUInt64T) -> int:
|
||
"""记录区逐条校验失败:附上出错记录的下标与值(rva 或字符串池偏移)。"""
|
||
if _State == None or _State.LineBuf == None:
|
||
return 0
|
||
N: int = stdio.snprintf(
|
||
_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP),
|
||
"[dbgstack] WARN: <exe>.stackmap invalid -- %s (entry %d, value=0x%llx; file %lld bytes),"
|
||
" addresses only this run\n",
|
||
Reason, Index, Got, Size)
|
||
EmitLine(N)
|
||
return 0
|
||
|
||
|
||
def EntryRva(Index: int) -> t.CUInt64T:
|
||
"""第 Index 条记录的 rva(记录区起点 = MapBase + 20)
|
||
|
||
⚠ 入口第一句的裸名 `_State` 判空**不能删**:TPC 的属性访问层必须先在函数级
|
||
`Gen.variables` 里登记 `_State`,才能解析这个模块级 `rawptr[DbgStackState]` 全局的
|
||
结构体类型;否则本函数内 `_State.MapBase` 会被**静默降级为常量 null**(详见模块头
|
||
第十一节 A,附构建产物 IR 证据)。判空本身也是真的兜底(_State 未安装时给 0)。
|
||
"""
|
||
if _State == None:
|
||
return 0
|
||
Off: int = DBGSTACK_MAP_HEADER + Index * DBGSTACK_ENTRY_SIZE
|
||
return ReadU64(_State.MapBase, Off)
|
||
|
||
|
||
def LookupRva(Rva: t.CUInt64T) -> int:
|
||
"""查 rva(表按 rva 升序):返回**最大的 rva <= 目标**那条的下标;无则 -1。
|
||
|
||
粒度约定(与生成侧 TPC `_WriteStackMap` 对齐):表里写的是 **DWARF 行表的行起点
|
||
地址**,而栈帧里的返回地址几乎不会精确落在某个行起点上 ⇒ 必须用下界(floor)
|
||
查找,否则每一帧都会退化成 +0x…(unknown)、整个功能等于白做。
|
||
语义:命中 = 存在 rva <= 目标(取其中最大者,即"目标落在哪一行起点之后");
|
||
未命中 = 目标小于表内最小 rva。
|
||
|
||
⚠ 本轮**只**在入口新增一句裸名 `_State` 判空(**搜索算法逐字未动**):原因同
|
||
EntryRva —— 不求值裸名 `_State` 时,`_State.EntryCount` 会被 TPC 静默降级为
|
||
null(⇒ Hi = -1 ⇒ 二分一次都不进 ⇒ 所有帧都落 "unknown")。若不需要这个兜底,
|
||
回退本 3 行即可,但"地址→源行"会整片失效。
|
||
"""
|
||
if _State == None:
|
||
return -1
|
||
Lo: t.CInt = 0
|
||
Hi: t.CInt = t.CInt(_State.EntryCount) - 1
|
||
Best: t.CInt = -1
|
||
while Lo <= Hi:
|
||
Mid: t.CInt = Lo + (Hi - Lo) / 2
|
||
E: t.CUInt64T = EntryRva(t.CInt(Mid))
|
||
if E <= Rva:
|
||
Best = Mid
|
||
Lo = Mid + 1
|
||
else:
|
||
Hi = Mid - 1
|
||
return Best
|
||
|
||
|
||
def LoadMapFile(hFile: HANDLE, Size: t.CInt64T) -> int:
|
||
"""读入旁文件并校验(头部 / 记录区升序 / 字符串偏移范围)。返回 1=MapOk 置位。
|
||
|
||
校验失败一律禁用映射(MapOk=0)并返回 0:破损的表会给出**错行号**,比"只打地址"更坏,
|
||
故不降级使用。失败时**由本函数自己打印挂在哪一项**(MapFailPlain/MapFailHdr/
|
||
MapFailEntry,含头部实读值与文件大小),不再让调用方打一句笼统的"文件非法"——
|
||
"文件存在 ⇒ Install 装了 handler ⇒ 映射却不可用"这类问题必须能一眼定性。
|
||
|
||
与写端(TPC `_WriteStackMap`)的校验口径对照(逐字段):
|
||
magic 小端 u32 == 0x53435054("TPCS") ← 写端 b'TPCS'
|
||
version u32 == 1 ← 写端常量 1
|
||
entry_count 参与 `20+count*20+str_bytes <= 文件大` ← 写端 len(records)
|
||
str_bytes 同上 ← 写端 len(pool)
|
||
str_count **只用于诊断打印,不参与校验**(写端 len(str_offsets) 含偏移 0 的
|
||
空串哨兵;读端只依赖"偏移落池 + 池末必有 \\0"的边界性质)
|
||
记录 rva 严格升序 ← 写端 sorted(best.keys())
|
||
file_off < str_bytes 且 >= 1 ← 写端 _Intern(path) 恒 >= 1
|
||
func_off == 0 表示无函数名;否则 < str_bytes ← 写端 `_Intern(name) if name else 0`
|
||
"""
|
||
if Size < t.CInt64T(DBGSTACK_MAP_HEADER):
|
||
return MapFailPlain(Size, "file smaller than the 20-byte header")
|
||
if Size > t.CInt64T(DBGSTACK_MAP_LIMIT):
|
||
return MapFailPlain(Size, "file exceeds the 64 MiB limit (header likely corrupt)")
|
||
Buf: bytes = stdlib.malloc(sizet(Size))
|
||
if Buf == None:
|
||
return MapFailPlain(Size, "load buffer allocation failed (file too large or out of memory)")
|
||
Got: ULONG = 0
|
||
if wf.ReadFile(hFile, Buf, ULONG(Size), t.CPtr(c.Addr(Got)), None) == 0:
|
||
Err: ULONG = GetLastError()
|
||
stdlib.free(t.CPtr(Buf))
|
||
if _State != None and _State.LineBuf != None:
|
||
RN: int = stdio.snprintf(_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP),
|
||
"[dbgstack] WARN: read <exe>.stackmap failed (GetLastError=%u),"
|
||
" addresses only this run\n",
|
||
Err)
|
||
EmitLine(RN)
|
||
return 0
|
||
if t.CSizeT(Got) != t.CSizeT(Size):
|
||
stdlib.free(t.CPtr(Buf))
|
||
return MapFailPlain(Size, "read fewer bytes than the file size")
|
||
# 头部:magic / version / entry_count / str_count / str_bytes
|
||
if ReadU32(Buf, 0) != t.CUInt32T(DBGSTACK_MAP_MAGIC):
|
||
stdlib.free(t.CPtr(Buf))
|
||
return MapFailHdr(Size, "magic is not \"TPCS\" (writer/reader endianness or offset mismatch)", Buf)
|
||
if ReadU32(Buf, 4) != t.CUInt32T(DBGSTACK_MAP_VERSION):
|
||
stdlib.free(t.CPtr(Buf))
|
||
return MapFailHdr(Size, "version is not 1 (stackmap written by another TPC version)", Buf)
|
||
Cnt: t.CUInt32T = ReadU32(Buf, 8)
|
||
StrCnt: t.CUInt32T = ReadU32(Buf, 12)
|
||
SBytes: t.CUInt32T = ReadU32(Buf, 16)
|
||
# 记录区 + 字符串池必须完整落在文件内(u64 运算,杜绝 u32 回绕)
|
||
Need: t.CUInt64T = t.CUInt64T(DBGSTACK_MAP_HEADER) \
|
||
+ t.CUInt64T(Cnt) * t.CUInt64T(DBGSTACK_ENTRY_SIZE) \
|
||
+ t.CUInt64T(SBytes)
|
||
if Need > t.CUInt64T(Size):
|
||
stdlib.free(t.CPtr(Buf))
|
||
return MapFailHdr(Size, "20+entry_count*20+str_bytes exceeds end of file "
|
||
"(header corrupt or file truncated)", Buf)
|
||
if SBytes == 0:
|
||
stdlib.free(t.CPtr(Buf))
|
||
return MapFailHdr(Size, "str_bytes is 0 (string pool missing)", Buf)
|
||
# 字符串池的最后一个字节必须是 '\0':写端每条串都以 \0 结尾,故池末恒为 \0。
|
||
# 有了这条,配合下面的"偏移落池内"校验,任何 %s 打印都不会越出池尾。
|
||
PoolEnd: int = DBGSTACK_MAP_HEADER + t.CInt(Cnt) * DBGSTACK_ENTRY_SIZE + t.CInt(SBytes) - 1
|
||
if Buf[PoolEnd] != 0:
|
||
stdlib.free(t.CPtr(Buf))
|
||
return MapFailHdr(Size, "string pool does not end with '\\0' (pool corrupt)", Buf)
|
||
# 全表校验:rva 严格升序(二分查找的前提)+ 字符串偏移落在池内
|
||
Prev: t.CUInt64T = 0
|
||
i: t.CInt = 0
|
||
while i < t.CInt(Cnt):
|
||
Off: int = DBGSTACK_MAP_HEADER + t.CInt(i) * DBGSTACK_ENTRY_SIZE
|
||
Cur: t.CUInt64T = ReadU64(Buf, Off)
|
||
if i > 0 and Cur <= Prev:
|
||
stdlib.free(t.CPtr(Buf))
|
||
return MapFailEntry(Size, t.CInt(i),
|
||
"record rva not strictly ascending (breaks binary search)", Cur)
|
||
Prev = Cur
|
||
FO: t.CUInt32T = ReadU32(Buf, Off + DBGSTACK_ENTRY_FILEOFF)
|
||
FN: t.CUInt32T = ReadU32(Buf, Off + DBGSTACK_ENTRY_FUNCOFF)
|
||
if t.CUInt64T(FO) >= t.CUInt64T(SBytes):
|
||
stdlib.free(t.CPtr(Buf))
|
||
return MapFailEntry(Size, t.CInt(i), "file_off outside the string pool", t.CUInt64T(FO))
|
||
if FN != 0 and t.CUInt64T(FN) >= t.CUInt64T(SBytes):
|
||
stdlib.free(t.CPtr(Buf))
|
||
return MapFailEntry(Size, t.CInt(i),
|
||
"func_off is neither 0 (no name) nor inside the string pool",
|
||
t.CUInt64T(FN))
|
||
i += 1
|
||
_State.MapBase = Buf
|
||
_State.EntryCount = Cnt
|
||
_State.StrBytes = SBytes
|
||
_State.StrBase = Buf + (DBGSTACK_MAP_HEADER + t.CInt(Cnt) * DBGSTACK_ENTRY_SIZE)
|
||
_State.MapOk = 1
|
||
return 1
|
||
|
||
|
||
# ============================================================
|
||
# 单帧打印:地址 → rva → 源行
|
||
# ============================================================
|
||
|
||
def EmitFrame(Index: int, Addr: t.CUInt64T, Base: t.CUInt64T, Scanned: int):
|
||
"""按「命中 / 未命中 / 越出主模块」三分支打印第 Index 帧
|
||
|
||
Scanned:0 = 权威帧(当前 RIP / RBP 链逐帧读出的返回地址);非 0 = **扫描推断**帧
|
||
(ScanStack 在栈上扫出的候选返回地址)。推断帧的行前缀用 `~#`(如 ` ~#3 0x…`)
|
||
与权威帧的 ` #3` 明确区分——它们只是"栈上看起来像返回地址的值",可含误报,
|
||
不得与权威帧混为一谈(见模块头第十二节)。
|
||
|
||
⚠ 入口第一句的裸名 `_State` 判空**不能删**:本函数一上手就用 `_State.LineBuf`
|
||
(snprintf 目标)与 `_State.MapOk`;不求值裸名 `_State` 时这两处都会被 TPC 静默
|
||
降级为 null(snprintf(null) ⇒ 写零页崩溃 / `MapOk` 分支整段消失)。详见模块头
|
||
第十一节 A。本函数是 RBP 主路径的每帧出口,必须绝对安全。
|
||
"""
|
||
if _State == None:
|
||
return
|
||
Tag: str = " #"
|
||
if Scanned != 0:
|
||
Tag = " ~#"
|
||
N: int = 0
|
||
if Base == 0 or Addr < Base:
|
||
# 不在主模块映像内(DLL / 系统模块 / 已损坏的帧):只打地址,不做 rva 归属
|
||
N = stdio.snprintf(_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP),
|
||
"%s%d 0x%llx (external)\n", Tag, Index, Addr)
|
||
EmitLine(N)
|
||
return
|
||
Rva: t.CUInt64T = Addr - Base
|
||
if _State.MapOk == 0:
|
||
# 无映射(旁文件缺失/破损,或编译期未生成):打印可定位的降级信息(RVA)
|
||
N = stdio.snprintf(_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP),
|
||
"%s%d 0x%llx +0x%llx (unknown)\n", Tag, Index, Addr, Rva)
|
||
EmitLine(N)
|
||
return
|
||
Hit: int = LookupRva(Rva)
|
||
if Hit < 0:
|
||
# 表已装载却查不到:必须说清"为什么查不到"(表空 / 目标 rva 小于表内最小 rva),
|
||
# 否则又退化成一句无信息的 "(unknown)"。只打一次,避免逐帧刷屏。
|
||
if _State.MissReported == 0:
|
||
_State.MissReported = 1
|
||
if _State.EntryCount == 0:
|
||
EmitText(" srcline map: rva not hit -- map is empty (0 entries)\n")
|
||
else:
|
||
First: t.CUInt64T = EntryRva(0)
|
||
Last: t.CUInt64T = EntryRva(t.CInt(_State.EntryCount) - 1)
|
||
M: int = stdio.snprintf(
|
||
_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP),
|
||
" srcline map: rva 0x%llx not hit (%u entries: first rva=0x%llx last rva=0x%llx;"
|
||
" target < first => writer RVA base mismatch)\n",
|
||
Rva, _State.EntryCount, First, Last)
|
||
EmitLine(M)
|
||
N = stdio.snprintf(_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP),
|
||
"%s%d 0x%llx +0x%llx (unknown)\n", Tag, Index, Addr, Rva)
|
||
EmitLine(N)
|
||
return
|
||
Off: int = DBGSTACK_MAP_HEADER + Hit * DBGSTACK_ENTRY_SIZE
|
||
FileOff: t.CUInt32T = ReadU32(_State.MapBase, Off + DBGSTACK_ENTRY_FILEOFF)
|
||
Line: t.CUInt32T = ReadU32(_State.MapBase, Off + DBGSTACK_ENTRY_LINE)
|
||
FuncOff: t.CUInt32T = ReadU32(_State.MapBase, Off + DBGSTACK_ENTRY_FUNCOFF)
|
||
FileName: str = str(_State.StrBase + t.CInt(FileOff))
|
||
FuncName: str = "unknown"
|
||
if FuncOff != 0:
|
||
FuncName = str(_State.StrBase + t.CInt(FuncOff))
|
||
N = stdio.snprintf(_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP),
|
||
"%s%d 0x%llx %s:%u (%s)\n", Tag, Index, Addr, FileName, Line, FuncName)
|
||
EmitLine(N)
|
||
|
||
|
||
# ============================================================
|
||
# 两条回溯路径
|
||
# ============================================================
|
||
|
||
def ReadMemCb(hProcess: HANDLE, Addr: ULONGLONG, Buf: VOIDPTR, Size: DWORD,
|
||
pRead: t.CPtr) -> BOOL:
|
||
"""StackWalk64 的回调 1(PREAD_PROCESS_MEMORY_ROUTINE64)的**本模块替身**。
|
||
|
||
为什么不用 kernel32!ReadProcessMemory 本体:
|
||
(1) 它只返回 BOOL,回调返回 0 会让 StackWalk64 **就地终止**(实测现象正是"0 帧"),
|
||
而失败原因(GetLastError)会被静默丢掉 ⇒ 这里把首次失败的现场(地址/大小/错误码)
|
||
记录下来,由首帧诊断紧邻打印(只记一次);
|
||
(2) dbghelp 的原型是 `DWORD nSize` + `LPDWORD lpNumberOfBytesRead`,而 kernel32 的
|
||
ReadProcessMemory 是 `SIZE_T nSize` + `SIZE_T*`(x64 都是 8 字节)。把 dbghelp
|
||
传进来的 4 字节 LPDWORD 直接当 SIZE_T* 交给 kernel32,成功时会被写穿 4 字节。
|
||
故这里用本地 `sizet` 承接读出字节数,再按 DWORD 写回 dbghelp 的缓冲。
|
||
传参方式:本模块内"函数名直接作 t.CPtr 实参"(同 SetConsoleCtrlHandler(CtrlHandler)
|
||
的写法),不涉及未验证的跨模块函数名→函数指针转换。
|
||
线程安全:只读全局状态、写已预留缓冲;与调用方同线程,期间 LineBuf 空闲。
|
||
"""
|
||
Read: sizet = 0
|
||
R: BOOL = wm.ReadProcessMemory(hProcess, t.CPtr(Addr), Buf, sizet(Size),
|
||
t.CPtr(c.Addr(Read)))
|
||
if pRead != None:
|
||
Rd: rawptr[t.CUInt32T] = (rawptr[t.CUInt32T])(pRead)
|
||
Rd[0] = t.CUInt32T(Read)
|
||
if R == 0 and _State != None and _State.ReadMemFailed == 0:
|
||
# 只**记录**首次失败现场(地址/大小/错误码),不在回调里直接打印:
|
||
# 打印挪到首帧诊断里紧邻 "StackWalk64 first: ret=..." 之后(见 WalkViaDbgHelp),
|
||
# 让"回调返回 0 导致 0 帧"这条根因与首帧返回码同屏可判(见模块头第九节)。
|
||
_State.ReadMemFailed = 1
|
||
_State.ReadMemAddr = t.CUInt64T(Addr)
|
||
_State.ReadMemSize = t.CUInt32T(Size)
|
||
_State.ReadMemErr = GetLastError()
|
||
return R
|
||
|
||
|
||
def WalkViaDbgHelp(hProcess: HANDLE, hThread: HANDLE, CtxPtr: VOIDPTR,
|
||
Base: t.CUInt64T) -> int:
|
||
"""dbghelp!StackWalk64 逐帧回溯;返回打印的帧数(0 = 未走出任何帧 ⇒ 调用方降级)。
|
||
|
||
【本轮起定位为**可选加强**路径】主回溯路径是 WalkRbpChain(零 dbghelp 依赖)。
|
||
本函数只在"RBP 链拿不到更多帧"且调用方判定 dbghelp 当前可用(无 ASan 冲突)时被调,
|
||
见模块头第十一节 B;若 dbghelp 与 ASan 冲突,调用方会打印
|
||
`[diag] symwalk=disabled reason=dbghelp_conflict_with_asan` 并根本不进来。
|
||
|
||
STACKFRAME64 的字段访问一律经**显式子结构指针**(rawptr[dbg.ADDRESS64])完成,
|
||
不做 fr.AddrPC.Offset 这种嵌套成员访问:三个 ADDRESS64 在 STACKFRAME64 内的偏移
|
||
是 0 / 16 / 32 / 48(= AddrPC / AddrReturn / AddrFrame / AddrStack,见
|
||
includes/w32/win32dbghelp.vp 的布局注释)。
|
||
初始化:整块清零后预填 AddrPC/AddrFrame/AddrStack 的 Offset = 上下文的 Rip/Rbp/Rsp,
|
||
三者 Mode = AddrModeFlat(3);MachineType 每次调用都传 IMAGE_FILE_MACHINE_AMD64。
|
||
STACKFRAME64 没有"结构体尺寸/字段数"成员(那是 StackWalkEx 的 STACKFRAME_EX 才有的),
|
||
调用方无需也无需能填——只需保证本仓的 sizeof 与 C 侧一致(0xF8)。
|
||
【首帧无条件取证】i == 0 时**无论成功失败都打印两行**(先 "[diag] StackWalk64 first:
|
||
ret=… addrpc=… fta=…",再 "[diag] StackWalk64 in: machine=… rip=… rsp=… rbp=…"——返回码、
|
||
传入的 machine/RIP/RSP/RBP、返回的 AddrPC 以及 SymFunctionTableAccess64(proc, AddrPC) 的
|
||
返回值)——这是判定"0 帧"根因的唯一现场证据(见模块头第四节取证段与第九节)。拆成两行
|
||
只为避免单条 143 字符长行在窄终端折行(字段一个不减)。i>0 时返回 0 是正常走到栈顶。
|
||
【return 全打印】每条 return 前都打 "[diag] walk: return reason=… frames=…"(见函数内注释)。
|
||
函数体整体包在 `if DBGSTACK_USE_DBGHELP != 0` 里:开关为 0 时该分支(唯一一处
|
||
dbg.StackWalk64 调用所在)整体不被生成 ⇒ **不产生 dbghelp 链接期符号**。
|
||
"""
|
||
# 【return 路径全覆盖】每条 return 前都打一行短诊断(见模块头第十节),使"walk 到底有没有
|
||
# 被调用、走了哪条出口"在下一次按键后必然可见(配合 DumpMainThread 的 walk: enter/leave)。
|
||
# reason 取值一览:
|
||
# null_state 全局状态 _State 缺失(安装未完成/已损毁)
|
||
# disabled 编译期开关 DBGSTACK_USE_DBGHELP == 0(产物未链接 dbghelp)
|
||
# null_frame_buf 帧缓冲缺失(Install 阶段 calloc 失败)
|
||
# null_context CONTEXT 缓冲指针为空(调用方传参异常)
|
||
# stackwalk_first_failed 首帧 StackWalk64 返回 0(根因由首帧取证行给出)
|
||
# stackwalk_failed 后续帧 StackWalk64 返回 0(正常走到栈顶)
|
||
# addrpc_zero StackWalk64 成功但返回 AddrPC == 0
|
||
# frame_limit 达到 DBGSTACK_MAX_FRAMES 上限而结束
|
||
#
|
||
# ⚠ 本条裸名 `_State` 判空**必须是函数第一句**:TPC 的属性访问层要先在函数级
|
||
# `Gen.variables` 里登记 `_State`,才能解析它的结构体类型;否则本函数内**所有**
|
||
# `_State.<字段>` 都会被静默降级为常量 null(实测:memset 收到 i8* null 写零页崩溃、
|
||
# StackWalk64 收到两个 null 回调)。详见模块头第十一节 A(附构建产物 IR 证据)。
|
||
if _State == None:
|
||
EmitText(" [diag] walk: return reason=null_state frames=0\n")
|
||
return 0
|
||
if DBGSTACK_USE_DBGHELP == 0:
|
||
EmitText(" [diag] walk: return reason=disabled (DBGSTACK_USE_DBGHELP=0) frames=0\n")
|
||
return 0
|
||
# 帧缓冲经**局部量**兜底:局部变量这条通道最可靠(不受上面的属性访问问题影响),
|
||
# 把最危险的一行(memset)彻底钉死在"已判空"之上。
|
||
FBuf: VOIDPTR = _State.FrameBuf
|
||
if FBuf == None:
|
||
EmitText(" [diag] walk: return reason=null_frame_buf frames=0\n")
|
||
return 0
|
||
if CtxPtr == None:
|
||
EmitText(" [diag] walk: return reason=null_context frames=0\n")
|
||
return 0
|
||
Frames: int = 0
|
||
Reason: str = "frame_limit"
|
||
if DBGSTACK_USE_DBGHELP != 0:
|
||
string.memset(FBuf, 0, dbg.STACKFRAME64.__sizeof__())
|
||
ctx: rawptr[wp.CONTEXT] = (rawptr[wp.CONTEXT])(CtxPtr)
|
||
Pc: rawptr[dbg.ADDRESS64] = (rawptr[dbg.ADDRESS64])(FBuf)
|
||
Fm: rawptr[dbg.ADDRESS64] = (rawptr[dbg.ADDRESS64])(
|
||
t.CPtr(t.CUInt64T(FBuf) + 32))
|
||
St: rawptr[dbg.ADDRESS64] = (rawptr[dbg.ADDRESS64])(
|
||
t.CPtr(t.CUInt64T(FBuf) + 48))
|
||
Pc.Offset = ctx.Rip
|
||
Pc.Mode = t.CUInt32T(dbg.ADDR_MODE_FLAT)
|
||
Fm.Offset = ctx.Rbp
|
||
Fm.Mode = t.CUInt32T(dbg.ADDR_MODE_FLAT)
|
||
St.Offset = ctx.Rsp
|
||
St.Mode = t.CUInt32T(dbg.ADDR_MODE_FLAT)
|
||
i: int = 0
|
||
while i < DBGSTACK_MAX_FRAMES:
|
||
# 传入值必须在调用**之前**取(StackWalk64 会就地改写 STACKFRAME64)
|
||
InRip: t.CUInt64T = t.CUInt64T(Pc.Offset)
|
||
InRsp: t.CUInt64T = t.CUInt64T(St.Offset)
|
||
InRbp: t.CUInt64T = t.CUInt64T(Fm.Offset)
|
||
Ok: BOOL = dbg.StackWalk64(t.CUInt32T(dbg.IMAGE_FILE_MACHINE_AMD64), hProcess,
|
||
hThread, t.CPtr(FBuf), CtxPtr,
|
||
ReadMemCb, _State.FuncTableAddr,
|
||
_State.ModuleBaseAddr, None)
|
||
Cur: t.CUInt64T = t.CUInt64T(Pc.Offset)
|
||
if i == 0:
|
||
# 首帧取证(无条件,先于任何失败判断):返回码 + 返回 AddrPC
|
||
# + SymFunctionTableAccess64(AddrPC)。三者对照即可判定"0 帧"属于哪一类
|
||
# (ModInited/读内存失败/上下文口径,见模块头第九节)。
|
||
# ⚠ 原为**单条**长行(~143 字符),在窄终端会折行 ⇒ 拆成两条 <100 的短文,
|
||
# 字段不减(拆行只为避免折行造成的"显示残缺"假象)。
|
||
Fta: t.CUInt64T = 0
|
||
if Cur != 0:
|
||
Fta = t.CUInt64T(dbg.SymFunctionTableAccess64(hProcess, Cur))
|
||
D0: int = stdio.snprintf(
|
||
_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP),
|
||
" [diag] StackWalk64 first: ret=%d addrpc=0x%llx fta=0x%llx\n",
|
||
t.CInt(Ok), Cur, Fta)
|
||
EmitLine(D0)
|
||
D1: int = stdio.snprintf(
|
||
_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP),
|
||
" [diag] StackWalk64 in: machine=0x%x rip=0x%llx rsp=0x%llx rbp=0x%llx\n",
|
||
t.CUInt32T(dbg.IMAGE_FILE_MACHINE_AMD64), InRip, InRsp, InRbp)
|
||
EmitLine(D1)
|
||
if Ok == 0:
|
||
if i == 0:
|
||
Reason = "stackwalk_first_failed"
|
||
else:
|
||
Reason = "stackwalk_failed"
|
||
D2: int = stdio.snprintf(
|
||
_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP),
|
||
" [diag] StackWalk64 returned 0 at frame %d (addrpc=0x%llx), stop\n",
|
||
i, Cur)
|
||
EmitLine(D2)
|
||
if _State.ReadMemFailed != 0:
|
||
# 紧邻首帧返回码打印读栈内存失败现场("0 帧"根因之一):
|
||
# 回调返回 0 ⇒ StackWalk64 就地终止,见模块头第九节 (2)。
|
||
DR: int = stdio.snprintf(
|
||
_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP),
|
||
" [diag] readmem failed addr=0x%llx size=%u GetLastError=%u\n",
|
||
_State.ReadMemAddr, _State.ReadMemSize, _State.ReadMemErr)
|
||
EmitLine(DR)
|
||
break
|
||
if Cur == 0:
|
||
Reason = "addrpc_zero"
|
||
D3: int = stdio.snprintf(
|
||
_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP),
|
||
" [diag] StackWalk64 frame %d addrpc=0, stop\n", i)
|
||
EmitLine(D3)
|
||
break
|
||
EmitFrame(i, Cur, Base, 0)
|
||
i += 1
|
||
Frames = i
|
||
# 统一的"离开 walk"诊断(覆盖上面所有 break/循环正常结束):reason 已在各出口置好
|
||
N: int = stdio.snprintf(_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP),
|
||
" [diag] walk: return reason=%s frames=%d\n", Reason, Frames)
|
||
EmitLine(N)
|
||
return Frames
|
||
|
||
|
||
def WalkRbpChain(Rip: t.CUInt64T, Rbp: t.CUInt64T, Base: t.CUInt64T) -> int:
|
||
"""**主回溯路径**:打印 RIP,再沿 RBP 链读 [rbp](调用者 rbp)与
|
||
[rbp+8](返回地址)逐帧回溯。
|
||
|
||
【本轮改动:每一步无条件打**原始读数**(先取证,后决策)】
|
||
每一次循环迭代(**含被拒的那一步**,即所有 reject 分支与 return 之前)都先产出两条
|
||
ASCII 短诊断(前缀 `[diag] rbp:`):
|
||
行 1: `frame=<i> rbp=0x… [rbp]=0x… [rbp+8]=0x…`(**读到的原始 8 字节值**)
|
||
行 2: `frame=<i> rpm_ok=<0/1> rpm_err=<GetLastError> accept=<0/1> reject_reason=<…>`
|
||
为什么要拆成两行:合并后单行 >140 字符(再加 EmitLine 的 `[dump#k]` 前缀)在窄终端
|
||
会折行,反而看不清——与既有首帧取证拆两行同一取舍,字段一个不减。
|
||
有了它们,"链为何在第 1 步就断"下一次按键即可直接判定,三种可能一眼可分:
|
||
(a) `rpm_ok=0`(读失败,err 给原因)⇒ 地址不可读;
|
||
(b) `rpm_ok=1` 但 `[rbp+8]` 本身就不是返回地址(远小于基址/越出映像)
|
||
⇒ 该处 RBP 不是帧指针(帧指针未被保留,见模块头第十二节);
|
||
(c) 值像返回地址却被自洽校验档掉 ⇒ `reject_reason=not_growing/step_too_large`。
|
||
`reject_reason` 取值(与代码里的赋值一一对应):
|
||
rbp_zero / not_aligned / rpm_failed / ret_zero / ret_out_of_module /
|
||
not_growing / step_too_large;本步被接受时为 `accept`(打印 accept=1)。
|
||
仅当本步**被接受**时才 EmitFrame(权威帧,`#` 前缀)——被拒的值一律只进诊断行,
|
||
不再混进栈里(旧实现在校验之前就 EmitFrame,会把 `0x121fa` 这类垃圾值当成
|
||
一帧 external 打出来,误导读者)。
|
||
|
||
* **零 dbghelp 依赖** ⇒ 不受「ASan 已占用 DbgHelp」影响。
|
||
* **读内存一律经 ReadProcessMemory**,绝不直接解引用野 rbp:诊断设施不允许把"卡死"
|
||
变成"崩溃"。
|
||
* 自洽校验(任一不满足即停,宁可少打也不把回溯带飞):
|
||
rbp 为 0 / 未 8 字节对齐 / 读内存失败 / 返回地址为 0 /
|
||
返回地址不在主模块基址区间 [Base, Base+DBGSTACK_MODULE_MAX) 内 /
|
||
调用者 rbp 未严格向高地址增长(`[rbp] > rbp`)/
|
||
单步跨度 > DBGSTACK_RBP_MAX_STEP / 达帧数上限。
|
||
(Base == 0 时无法判定区间,跳过该条,不作误杀。)
|
||
|
||
⚠ 入口第一句的裸名 `_State` 判空**不能删**:本函数本轮起用 `_State.LineBuf` 组诊断行,
|
||
不求值裸名 `_State` 时该字段会被 TPC 静默降级为常量 null(snprintf(null) 写零页崩溃)。
|
||
详见模块头第十一节 A。
|
||
"""
|
||
if _State == None:
|
||
return 0
|
||
EmitFrame(0, Rip, Base, 0)
|
||
Cur: t.CUInt64T = Rbp
|
||
i: int = 1
|
||
HProc: HANDLE = wp.GetCurrentProcess()
|
||
Two: t.CArray[t.CUInt64T, 2] # [0]=[rbp](调用者 rbp) [1]=[rbp+8](返回地址)
|
||
while i < DBGSTACK_MAX_FRAMES:
|
||
# ---- 本步原始读数 + 判定(**先于任何 reject 早退**产出,见函数头)----
|
||
Rd: sizet = 0
|
||
RpmOk: t.CInt = 0
|
||
RpmErr: ULONG = 0
|
||
V0: t.CUInt64T = 0 # [rbp] 调用者 rbp
|
||
V1: t.CUInt64T = 0 # [rbp+8] 返回地址
|
||
Reason: str = "accept"
|
||
Reject: t.CInt = 0 # 0 = 接受本步;1 = 拒绝(原因见 Reason)
|
||
if Cur == 0:
|
||
Reason = "rbp_zero"
|
||
Reject = 1
|
||
elif (Cur & t.CUInt64T(7)) != 0:
|
||
Reason = "not_aligned"
|
||
Reject = 1
|
||
else:
|
||
# 用 ReadProcessMemory 读 16 字节(地址无效/不可读时返回 0,**不触发访问冲突**);
|
||
# 直接解引用(如原实现的联合类型强转)在野 rbp 上会崩,主路径不能这么干。
|
||
OkRead: BOOL = wm.ReadProcessMemory(HProc, t.CPtr(Cur), t.CPtr(c.Addr(Two)),
|
||
sizet(16), t.CPtr(c.Addr(Rd)))
|
||
if OkRead == 0 or t.CSizeT(Rd) < t.CSizeT(16):
|
||
# GetLastError 必须**紧邻** ReadProcessMemory 取,中途不得插入其它 API
|
||
RpmErr = GetLastError()
|
||
Reason = "rpm_failed"
|
||
Reject = 1
|
||
else:
|
||
RpmOk = 1
|
||
V0 = Two[0]
|
||
V1 = Two[1]
|
||
if V1 == 0:
|
||
Reason = "ret_zero"
|
||
Reject = 1
|
||
elif Base != 0:
|
||
if V1 < Base or V1 >= Base + t.CUInt64T(DBGSTACK_MODULE_MAX):
|
||
# 值明显不是返回地址(远小于主模块基址 / 越出映像)
|
||
# ⇒ 该 RBP 不是帧指针,链在此断(见模块头第十二节)
|
||
Reason = "ret_out_of_module"
|
||
Reject = 1
|
||
if Reject == 0:
|
||
if V0 <= Cur:
|
||
Reason = "not_growing"
|
||
Reject = 1
|
||
elif V0 - Cur > t.CUInt64T(DBGSTACK_RBP_MAX_STEP):
|
||
Reason = "step_too_large"
|
||
Reject = 1
|
||
D0: int = stdio.snprintf(
|
||
_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP),
|
||
" [diag] rbp: frame=%d rbp=0x%llx [rbp]=0x%llx [rbp+8]=0x%llx\n",
|
||
i, Cur, V0, V1)
|
||
EmitLine(D0)
|
||
Acc: t.CInt = 1
|
||
if Reject != 0:
|
||
Acc = 0
|
||
D1: int = stdio.snprintf(
|
||
_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP),
|
||
" [diag] rbp: frame=%d rpm_ok=%d rpm_err=%u accept=%d reject_reason=%s\n",
|
||
i, RpmOk, RpmErr, Acc, Reason)
|
||
EmitLine(D1)
|
||
if Reject != 0:
|
||
break
|
||
# 本步被接受 ⇒ 输出**权威帧**(`#` 前缀),再继续沿链上行
|
||
EmitFrame(i, V1, Base, 0)
|
||
i += 1
|
||
Cur = V0
|
||
return i
|
||
|
||
|
||
def ScanStack(StartIndex: int, Rsp: t.CUInt64T, Base: t.CUInt64T) -> int:
|
||
"""**第二兜底:栈扫描**(RBP 链之后的路径;**不依赖 RBP / 帧指针**)。
|
||
|
||
为什么要它:若编译产物根本没保留帧指针(见模块头第十二节的调查结论),
|
||
`[rbp+8]` 读到的就不是返回地址,RBP 链在第 1 步即断(实测 `frames=2`)。
|
||
此时唯一能继续的办法是**直接扫已用的栈区**。
|
||
|
||
算法(全部只读,绝不直接解引用主线程内存):
|
||
1. 在 [Rsp, Rsp+DBGSTACK_SCAN_MAX_BYTES) 区间内按 8 字节步长遍历(**向高地址 = 调用者
|
||
方向**,故发现顺序天然按栈地址升序);
|
||
2. 一次 ReadProcessMemory 读**一整块**(DBGSTACK_SCAN_CHUNK_BYTES=4 KiB,缓冲是
|
||
局部数组,回调线程零分配),而不是逐 8 字节读(把上千次系统调用压到几十次);
|
||
任一块读失败即**停止并打印原因**(地址/GetLastError);
|
||
3. 候选:字 W 落在主模块区间 [Base, Base+DBGSTACK_MODULE_MAX) 内,且**相邻 8 字节字**
|
||
像栈地址——`[A-8]`(帧指针被保留时这里正是被压入的保存 rbp)或 `[A+8]`
|
||
(返回地址之后的调用者栈帧内容)任一落在 [Rsp, 区间上界) 即接受。取"任一"以同时
|
||
覆盖两种帧布局;放宽召回,误报由下面的 `~` 标注与数量上限控制。
|
||
(块内首字的 `[A-8]` 与末字的 `[A+8]` 不可知,按 0 处理 ⇒ 仅少一次佐证。)
|
||
4. 按**候选返回地址值**去重(同一个地址在栈上出现多次只留第一个),上限
|
||
DBGSTACK_SCAN_MAX_HITS=64;`scan_candidates=<N>` 打进诊断;
|
||
5. 逐个用 EmitFrame(..., Scanned=1) 输出,帧前缀 `~` ⇒ 明确标注**扫描推断**
|
||
(可含误报,不与权威帧混淆)。
|
||
返回实际输出的推断帧数(0 = 没扫到)。
|
||
|
||
⚠ 入口第一句的裸名 `_State` 判空**不能删**(同 WalkRbpChain:本函数用 _State.LineBuf)。
|
||
"""
|
||
if _State == None:
|
||
return 0
|
||
if Rsp == 0 or Base == 0:
|
||
# 无 RSP(未取到上下文)或无主模块基址(区间无从判定)⇒ 不扫,避免整片误报。
|
||
# 与其它降级路径一致:**不静默**,给可 grep 的跳过原因。
|
||
if Rsp == 0:
|
||
EmitText(" [diag] scan: skipped reason=no_rsp\n")
|
||
else:
|
||
EmitText(" [diag] scan: skipped reason=no_base\n")
|
||
return 0
|
||
HProc: HANDLE = wp.GetCurrentProcess()
|
||
Upper: t.CUInt64T = Rsp + t.CUInt64T(DBGSTACK_SCAN_MAX_BYTES)
|
||
ModEnd: t.CUInt64T = Base + t.CUInt64T(DBGSTACK_MODULE_MAX)
|
||
Words: t.CArray[t.CUInt64T, DBGSTACK_SCAN_CHUNK_WORDS] # 4 KiB 块缓冲(局部,零分配)
|
||
Found: t.CArray[t.CUInt64T, DBGSTACK_SCAN_MAX_HITS] # 去重后的候选返回地址
|
||
Hits: int = 0
|
||
Prev: t.CUInt64T = 0 # [A-8]:块内首字的前一条未知,置 0(只少一次佐证)
|
||
Addr: t.CUInt64T = Rsp
|
||
Stop: str = "reached_scan_limit"
|
||
D0: int = stdio.snprintf(
|
||
_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP),
|
||
" [diag] scan: rsp=0x%llx upper=0x%llx chunk=%d hits_max=%d\n",
|
||
Rsp, Upper, DBGSTACK_SCAN_CHUNK_BYTES, DBGSTACK_SCAN_MAX_HITS)
|
||
EmitLine(D0)
|
||
while Addr < Upper:
|
||
Rd: sizet = 0
|
||
OkRead: BOOL = wm.ReadProcessMemory(HProc, t.CPtr(Addr), t.CPtr(c.Addr(Words)),
|
||
sizet(DBGSTACK_SCAN_CHUNK_BYTES),
|
||
t.CPtr(c.Addr(Rd)))
|
||
N: int = t.CInt(t.CSizeT(Rd) / t.CSizeT(8)) # 本块实到的 u64 字数
|
||
if OkRead == 0 or N <= 0:
|
||
Er: ULONG = GetLastError()
|
||
D1: int = stdio.snprintf(
|
||
_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP),
|
||
" [diag] scan: block read failed addr=0x%llx err=%u, stop\n", Addr, Er)
|
||
EmitLine(D1)
|
||
Stop = "block_read_failed"
|
||
break
|
||
k: int = 0
|
||
while k < N:
|
||
W: t.CUInt64T = Words[t.CInt(k)]
|
||
if W >= Base and W < ModEnd:
|
||
# 相邻 8 字节字(A-8 或 A+8)任一像栈地址 ⇒ 接受 W 为候选返回地址
|
||
NearStack: t.CInt = 0
|
||
if Prev >= Rsp and Prev < Upper:
|
||
NearStack = 1
|
||
if k + 1 < N:
|
||
Q: t.CUInt64T = Words[t.CInt(k + 1)]
|
||
if Q >= Rsp and Q < Upper:
|
||
NearStack = 1
|
||
if NearStack != 0 and Hits < DBGSTACK_SCAN_MAX_HITS:
|
||
# 去重(按候选地址值;已按栈地址升序发现 ⇒ 保持原序)
|
||
Dup: t.CInt = 0
|
||
t_i: int = 0
|
||
while t_i < Hits:
|
||
if Found[t_i] == W:
|
||
Dup = 1
|
||
break
|
||
t_i += 1
|
||
if Dup == 0:
|
||
Found[Hits] = W
|
||
Hits += 1
|
||
Prev = W
|
||
k += 1
|
||
Addr = Addr + t.CUInt64T(DBGSTACK_SCAN_CHUNK_BYTES)
|
||
D2: int = stdio.snprintf(_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP),
|
||
" [diag] scan: scan_candidates=%d stop=%s\n", Hits, Stop)
|
||
EmitLine(D2)
|
||
# 输出推断帧:总数不得越过全局帧上限(权威帧已占 StartIndex 个)
|
||
Lim: int = DBGSTACK_MAX_FRAMES - StartIndex
|
||
j: int = 0
|
||
while j < Hits:
|
||
if j >= Lim:
|
||
break
|
||
EmitFrame(StartIndex + j, Found[j], Base, 1)
|
||
j += 1
|
||
return j
|
||
|
||
|
||
# ============================================================
|
||
# 转储主线程
|
||
# ============================================================
|
||
|
||
def WriteDumpHeaderFile(IsBreak: int):
|
||
"""向转储文件追加一段**分隔头**(只写文件,不写控制台)。
|
||
|
||
目的:同一文件是**多次**追加的(每次按键一段),故每段前放一个可 grep 的小分隔头:
|
||
本地时间戳 / pid / 触发键 / 主线程 id / Seq。此处用中文标签——文件是 UTF-8,不受控制台
|
||
代码页影响(这正是"文件版保留中文、控制台版全 ASCII"两种受众的分工点);
|
||
控制台文本见 EmitLine,一律 ASCII。
|
||
"""
|
||
if _State == None or _DumpFile == None or _DumpFile == INVALID_HANDLE_VALUE:
|
||
return
|
||
St: SYSTEMTIME = SYSTEMTIME()
|
||
GetLocalTime(t.CPtr(c.Addr(St)))
|
||
Trig: str = "Ctrl+C"
|
||
if IsBreak != 0:
|
||
Trig = "Ctrl+Break"
|
||
N: int = stdio.snprintf(
|
||
_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP),
|
||
"\n===== 栈转储 %04u-%02u-%02u %02u:%02u:%02u.%03u pid=%u 触发=%s 主线程id=%u Seq=%d =====\n",
|
||
t.CUInt32T(St.wYear), t.CUInt32T(St.wMonth), t.CUInt32T(St.wDay),
|
||
t.CUInt32T(St.wHour), t.CUInt32T(St.wMinute), t.CUInt32T(St.wSecond),
|
||
t.CUInt32T(St.wMilliseconds), wp.GetCurrentProcessId(),
|
||
Trig, _State.MainThreadId, _State.Seq)
|
||
if N <= 0:
|
||
return
|
||
if N > DBGSTACK_LINE_CAP - 1:
|
||
N = DBGSTACK_LINE_CAP - 1
|
||
Wr: ULONG = 0
|
||
wf.WriteFile(_DumpFile, _State.LineBuf, ULONG(N), t.CPtr(c.Addr(Wr)), None)
|
||
|
||
|
||
def DumpMainThread(IsBreak: int) -> int:
|
||
"""挂起主线程 → 取 CONTEXT → 回溯 → 打印 → 恢复主线程。返回打印帧数(0=未取到)。
|
||
|
||
关键不变量:**SuspendThread 成功之后,任何返回路径都必须 ResumeThread + CloseHandle**,
|
||
否则主线程永久冻结(比卡死更糟)。
|
||
重入不变量:入口用 AtomicTryEnterDump(内联汇编 `lock cmpxchg`)原子"检查并占用"
|
||
_DumpLock;所有返回路径都要 AtomicLeaveDump 释放;抢占失败的调用立刻返回、
|
||
**不输出任何内容**(不挂起主线程、不碰 LineBuf)。
|
||
取证不变量:banner 之后**无条件**打印一组前置诊断(见下),先于任何失败/降级分支。
|
||
walk 取证:进主路径 gate 前打 "[diag] walk: enter …"、gate 后打 "[diag] walk: leave frames=…"
|
||
(两条都**无条件**);两行都在 ⇒ 必已进入 WalkViaDbgHelp,否则"walk: enter 缺失"即指明
|
||
在本函数更早的 fallback 处(OpenThread/SuspendThread/GetThreadContext)就已 return(见第十节)。
|
||
输出形状(可 grep):每行由 EmitLine 统一加 " [dump#<k>] " 前缀(k 全局递增,判别并发/折行)。
|
||
"""
|
||
global _DumpOut
|
||
if _State == None or _State.LineBuf == None:
|
||
return 0
|
||
# 重入保护(见模块头第一节):SetConsoleCtrlHandler 的回调由系统在**新线程**上执行,
|
||
# 连续按键会并发进入本函数,而它们共用同一个 LineBuf / 输出句柄 ⇒ 输出互相撕裂
|
||
# (实测出现过"降级:"行重复且交织)。原子"检查并占用"消掉普通标志的读-改-写竞态窗口。
|
||
EnterOld: t.CInt = AtomicTryEnterDump()
|
||
if EnterOld != 0:
|
||
return 0
|
||
_State.Dumping = 1
|
||
_State.Seq = _State.Seq + 1
|
||
# 输出句柄在转储**开始**时一次性确定并锁定:此后全程只写 _DumpOut,不在中途重取。
|
||
_DumpOut = _State.OutHandle
|
||
# 单次转储范围内重置"防刷屏"标志:让每一次转储都自带完整诊断,不因上一次已打印而沉默。
|
||
_State.ReadMemFailed = 0
|
||
_State.MissReported = 0
|
||
EmitText("=== TPC stack dump (Ctrl+Break: keep running / Ctrl+C: exit) ===\n")
|
||
# 文件侧分隔头(仅文件:时间戳/pid/触发键/主线程 id;见 WriteDumpHeaderFile)
|
||
WriteDumpHeaderFile(t.CInt(IsBreak))
|
||
if _DumpOut == None or _DumpOut == INVALID_HANDLE_VALUE:
|
||
EmitText(" WARN: no usable console handle (stderr/stdout invalid), console output discarded\n")
|
||
# ---- 前置诊断(无条件;先取证,再决策):不挂起主线程即可取到的现场状态 ----
|
||
# 这些行必须出现在任何失败/降级分支之前,才能回答"0 帧到底是哪一种"(见模块头第九节)。
|
||
# 标签与字段全 ASCII:任何代码页下都可读、可 grep(详见 WalkViaDbgHelp 首帧处的说明)。
|
||
MBase: t.CUInt64T = t.CUInt64T(GetModuleHandleA(None))
|
||
ModBaseRet: t.CUInt64T = 0
|
||
if DBGSTACK_USE_DBGHELP != 0:
|
||
if _State.SymReady != 0:
|
||
ModBaseRet = t.CUInt64T(dbg.SymGetModuleBase64(wp.GetCurrentProcess(), MBase))
|
||
FB: t.CInt = 1 if _State.FrameBuf != None else 0
|
||
FT: t.CInt = 1 if _State.FuncTableAddr != None else 0
|
||
MB: t.CInt = 1 if _State.ModuleBaseAddr != None else 0
|
||
N: int = stdio.snprintf(
|
||
_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP),
|
||
" [diag] Seq=%d cbthread=%u mainthread=%u dump_enter_prev=%d "
|
||
"mapok=%d symready=%d modinited=%d\n",
|
||
_State.Seq, wp.GetCurrentThreadId(), _State.MainThreadId, EnterOld,
|
||
_State.MapOk, _State.SymReady, _State.ModInited)
|
||
EmitLine(N)
|
||
N = stdio.snprintf(
|
||
_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP),
|
||
" [diag] modbase=0x%llx SymGetModuleBase64(main)=0x%llx\n",
|
||
MBase, ModBaseRet)
|
||
EmitLine(N)
|
||
N = stdio.snprintf(
|
||
_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP),
|
||
" [diag] frame_buf=%d cb_fta=%d cb_mba=%d\n",
|
||
FB, FT, MB)
|
||
EmitLine(N)
|
||
Trigger: str = "Ctrl+C"
|
||
AfterTag: str = "exit"
|
||
if IsBreak != 0:
|
||
Trigger = "Ctrl+Break"
|
||
AfterTag = "continue"
|
||
N = stdio.snprintf(_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP),
|
||
" Trigger=%s after_dump=%s mainthread=%u cbthread=%u\n",
|
||
Trigger, AfterTag, _State.MainThreadId, wp.GetCurrentThreadId())
|
||
EmitLine(N)
|
||
hThread: HANDLE = wp.OpenThread(
|
||
ULONG(wp.THREAD_SUSPEND_RESUME | wp.THREAD_GET_CONTEXT), 0, _State.MainThreadId)
|
||
if hThread == None or hThread == INVALID_HANDLE_VALUE:
|
||
N = stdio.snprintf(_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP),
|
||
" fallback: OpenThread(main) failed (GetLastError=%u), cannot dump stack\n",
|
||
GetLastError())
|
||
EmitLine(N)
|
||
AtomicLeaveDump()
|
||
return 0
|
||
if wp.SuspendThread(hThread) == ULONG(DBGSTACK_SUSPEND_FAILED):
|
||
N = stdio.snprintf(_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP),
|
||
" fallback: SuspendThread(main) failed (GetLastError=%u), cannot dump stack\n",
|
||
GetLastError())
|
||
EmitLine(N)
|
||
CloseHandle(hThread)
|
||
AtomicLeaveDump()
|
||
return 0
|
||
# CONTEXT:先清零再写 ContextFlags(x64 必须含 CONTEXT_AMD64 位)
|
||
string.memset(_State.ContextBuf, 0, wp.CONTEXT.__sizeof__())
|
||
ctx: rawptr[wp.CONTEXT] = (rawptr[wp.CONTEXT])(_State.ContextBuf)
|
||
ctx.ContextFlags = t.CUInt32T(wp.CONTEXT_FULL)
|
||
if wp.GetThreadContext(hThread, t.CPtr(_State.ContextBuf)) == 0:
|
||
N = stdio.snprintf(_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP),
|
||
" fallback: GetThreadContext failed (GetLastError=%u), cannot dump stack\n",
|
||
GetLastError())
|
||
EmitLine(N)
|
||
wp.ResumeThread(hThread)
|
||
CloseHandle(hThread)
|
||
AtomicLeaveDump()
|
||
return 0
|
||
Base: t.CUInt64T = t.CUInt64T(GetModuleHandleA(None))
|
||
Rip: t.CUInt64T = t.CUInt64T(ctx.Rip)
|
||
Rsp: t.CUInt64T = t.CUInt64T(ctx.Rsp)
|
||
Rbp: t.CUInt64T = t.CUInt64T(ctx.Rbp)
|
||
# ---- 问题 B:ASan 与 dbghelp 互斥探测 + dbghelp 可用性判定 ----
|
||
# ASan 运行时会自己初始化 DbgHelp 做符号化,同一进程不允许两种不兼容用法(实测 ASan
|
||
# 侧打印 "Failed to initialize DbgHelp!")。故只要探测到 ASan 运行时,就**不用 dbghelp**
|
||
# 作主路径,并把原因打出来(硬要求:不得静默 0 帧)。两个命名都探:本仓 --asan 借用的是
|
||
# clang64 的 `-l:libclang_rt.asan_dynamic-x86_64.dll.a`(Phase2Translator.py:4402),
|
||
# 官方 LLVM 发行版的运行时 DLL 名则是 `clang_rt.asan_dynamic-x86_64.dll`。
|
||
AsanPresent: t.CInt = 0
|
||
if GetModuleHandleA("clang_rt.asan_dynamic-x86_64.dll") != None:
|
||
AsanPresent = 1
|
||
if GetModuleHandleA("libclang_rt.asan_dynamic-x86_64.dll") != None:
|
||
AsanPresent = 1
|
||
DbgUsable: t.CInt = 0
|
||
if DBGSTACK_USE_DBGHELP != 0 and _State.SymReady != 0 \
|
||
and _State.FuncTableAddr != None and _State.ModuleBaseAddr != None \
|
||
and _State.FrameBuf != None:
|
||
DbgUsable = 1
|
||
if AsanPresent != 0:
|
||
# ASan 在场 ⇒ 即便 SymInitialize 报成功,符号化行为也不可靠(且会把 ASan 的符号化
|
||
# 搞坏)。统一按"冲突"处理:dbghelp 降级为不可用,主路径走 RBP 链。
|
||
DbgUsable = 0
|
||
N = stdio.snprintf(_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP),
|
||
" [diag] symwalk: use_dbghelp=%d symready=%d asan=%d dbg_usable=%d\n",
|
||
DBGSTACK_USE_DBGHELP, _State.SymReady, AsanPresent, DbgUsable)
|
||
EmitLine(N)
|
||
if DbgUsable == 0:
|
||
# 每种降级都给**明确原因**(不静默 0 帧);顺序按"最具体优先"。
|
||
if DBGSTACK_USE_DBGHELP == 0:
|
||
EmitText(" [diag] symwalk=disabled reason=compile_time_switch"
|
||
" (DBGSTACK_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")
|
||
elif _State.FuncTableAddr == None or _State.ModuleBaseAddr == None:
|
||
EmitText(" [diag] symwalk=disabled reason=callback_missing\n")
|
||
else:
|
||
EmitText(" [diag] symwalk=disabled reason=null_frame_buf\n")
|
||
MapTag: str = "none (see [dbgstack] warnings above; addresses only)"
|
||
if _State.MapOk != 0:
|
||
MapTag = "loaded"
|
||
# 本轮起主路径 = RBP 链(零 dbghelp 依赖);dbghelp 仅在 RBP 链拿不到更多帧时作可选加强。
|
||
SymTag: str = "rbp-chain primary (dbghelp not required)"
|
||
if DbgUsable != 0:
|
||
SymTag = "rbp-chain primary, dbghelp optional"
|
||
N = stdio.snprintf(_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP),
|
||
" exe base=0x%llx RIP=0x%llx RSP=0x%llx RBP=0x%llx\n",
|
||
Base, Rip, Rsp, Rbp)
|
||
EmitLine(N)
|
||
N = stdio.snprintf(_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP),
|
||
" srcline_map=%s symwalk=%s\n", MapTag, SymTag)
|
||
EmitLine(N)
|
||
Frames: int = 0
|
||
# 【A:无条件「进入 walk」取证】本行先于所有门控/分支产出。若下一次按键后连本行都没有 ⇒
|
||
# 说明 DumpMainThread 在本行之前就返回了(OpenThread/SuspendThread/GetThreadContext 三处
|
||
# fallback,各自已有 "fallback: ... cannot dump stack" 行),据此定位跳过的分支。
|
||
N = stdio.snprintf(_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP),
|
||
" [diag] walk: enter use_dbghelp=%d symready=%d modinited=%d "
|
||
"frame_buf=%d mapok=%d asan=%d\n",
|
||
DBGSTACK_USE_DBGHELP, _State.SymReady, _State.ModInited, FB, _State.MapOk,
|
||
AsanPresent)
|
||
EmitLine(N)
|
||
if DbgUsable == 0:
|
||
# 保留既有约定:转 RBP 链之前**永远**有一条 [diag] StackWalk64 first: 可判(此时是
|
||
# not-called,附前提字段;若本次真的会调 StackWalk64,则由 WalkViaDbgHelp 内部产出)。
|
||
DNC: int = stdio.snprintf(
|
||
_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP),
|
||
" [diag] StackWalk64 first: ret=not-called symready=%d cb_fta=%d cb_mba=%d frame_buf=%d\n",
|
||
_State.SymReady, FT, MB, FB)
|
||
EmitLine(DNC)
|
||
# ---- 主路径:RBP 链(**不依赖 dbghelp**;--ctrlc-stack 已给 llc 加 -frame-pointer=all)----
|
||
EmitText(" [diag] walk: primary=rbp_chain\n")
|
||
Frames = WalkRbpChain(Rip, Rbp, Base)
|
||
N = stdio.snprintf(_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP),
|
||
" [diag] walk: rbp_chain frames=%d\n", Frames)
|
||
EmitLine(N)
|
||
# ---- 第二兜底:栈扫描(**不依赖 RBP / 帧指针**;见 ScanStack)----
|
||
# 仅当 RBP 链只拿到当前 RIP(<=1 帧,即链在第 1 步就断)时才启用——被测函数的 RBP
|
||
# 不是帧指针时,`[rbp+8]` 不是返回地址,只有直接扫栈才能继续。扫描帧以 `~` 前缀
|
||
# 标注为「扫描推断」,不与权威帧混淆(见模块头第十二节)。
|
||
if Frames <= 1:
|
||
EmitText(" [diag] walk: fallback=stack_scan\n")
|
||
Sc: int = ScanStack(Frames, Rsp, Base)
|
||
Frames = Frames + Sc
|
||
N = stdio.snprintf(_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP),
|
||
" [diag] walk: scan frames=%d total=%d\n", Sc, Frames)
|
||
EmitLine(N)
|
||
# ---- 可选加强:RBP 链只拿到当前 RIP(<=1 帧)且 dbghelp 当前可用时,才补一次 dbghelp ----
|
||
# 外层常量条件单独成 if(便于编译期折叠):置 0 时本段整体不生成 ⇒ 连对 WalkViaDbgHelp
|
||
# 的调用都不出现,本模块不留任何 dbghelp 链接期符号(与 DBGSTACK_USE_DBGHELP 注释一致)。
|
||
if DBGSTACK_USE_DBGHELP != 0:
|
||
if Frames <= 1 and DbgUsable != 0:
|
||
EmitText(" [diag] walk: dbghelp enhancement engaged\n")
|
||
F2: int = WalkViaDbgHelp(wp.GetCurrentProcess(), hThread, _State.ContextBuf, Base)
|
||
if F2 > Frames:
|
||
Frames = F2
|
||
# 【A:无条件「离开 walk」取证】与 walk: enter 成对;两行都在 ⇒ walk 一定进了 RBP 主路径
|
||
# (若 dbghelp 被启用,其内部还会再产出一行 walk: return reason=…)。
|
||
N = stdio.snprintf(_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP),
|
||
" [diag] walk: leave frames=%d mapok=%d\n", Frames, _State.MapOk)
|
||
EmitLine(N)
|
||
if Frames == 0:
|
||
# 走到这里说明 RBP 链、栈扫描与(可能的)dbghelp 加强都没拿到帧:给一句可 grep 的结论。
|
||
EmitText(" [diag] walk: no frames from any path (rbp_chain/stack_scan/dbghelp)\n")
|
||
# 拆成两次 EmitLine:使 frames= 与结束行各自带上 [dump#k] 前缀(每行都可独立编号/定位)
|
||
N = stdio.snprintf(_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP),
|
||
" frames=%d\n", Frames)
|
||
EmitLine(N)
|
||
EmitText("=== stack dump end ===\n")
|
||
wp.ResumeThread(hThread)
|
||
CloseHandle(hThread)
|
||
AtomicLeaveDump()
|
||
return Frames
|
||
|
||
|
||
# ============================================================
|
||
# 控制事件处理例程 / 安装
|
||
# ============================================================
|
||
|
||
def CtrlHandler(CtrlType: ULONG) -> BOOL:
|
||
"""SetConsoleCtrlHandler 回调(系统在**新线程**上调用)。
|
||
|
||
Ctrl+C → 转储后返回 FALSE(交还默认处理 ⇒ 进程终止);
|
||
Ctrl+Break → 转储后返回 TRUE(已处理 ⇒ 进程继续);
|
||
其它控制事件 → 不转储、返回 FALSE(不干预系统默认行为)。
|
||
线程安全:只读全局状态、写已预留缓冲,不分配内存、不碰 MemBuddy 池。
|
||
"""
|
||
if _State == None or _State.Installed == 0:
|
||
return 0
|
||
IsBreak: t.CInt = 0
|
||
if CtrlType == ULONG(wc.CTRL_BREAK_EVENT):
|
||
IsBreak = 1
|
||
elif CtrlType != ULONG(wc.CTRL_C_EVENT):
|
||
return 0
|
||
DumpMainThread(t.CInt(IsBreak))
|
||
if IsBreak != 0:
|
||
return 1
|
||
return 0
|
||
|
||
|
||
def NoteError(Text: str, Code: ULONG):
|
||
"""安装期/降级诊断:'<Text>(GetLastError=%u)' 一行写到转储同一去向(stderr 优先)"""
|
||
if _State == None or _State.LineBuf == None:
|
||
return
|
||
N: int = stdio.snprintf(_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP),
|
||
"%s(GetLastError=%u)\n", Text, Code)
|
||
EmitLine(N)
|
||
|
||
|
||
def Install() -> int:
|
||
"""注册控制事件处理器。返回 1=已注册(含降级可用),0=未安装。
|
||
|
||
0 有两种情形,都有明确诊断(旁文件不存在是**预期**的关闭状态,静默返回):
|
||
* <exe>.stackmap 不存在(未启用 --ctrlc-stack)→ 静默返回 0,零副作用;
|
||
* 旁文件存在但打开/读取/解析失败,或关键缓冲分配失败 → 打印警告后返回 0。
|
||
幂等:一旦尝试过(无论成败)就不再重复分配/注册,直接返回上次结果——避免重试
|
||
泄漏状态对象与缓冲。成功路径的提示写 stderr,不污染调用方的 stdout 约定。
|
||
"""
|
||
global _State, _DumpOut, _HandlerReg, _DumpFile
|
||
if _State != None:
|
||
return t.CInt(_State.Installed)
|
||
# ① <exe 完整路径> + ".stackmap"(此时 _State 尚未建立,失败提示走 EmitEarly:
|
||
# 与转储同一去向 stderr,不再用 stdio.printf ⇒ stdout/stderr 不会双流交错)
|
||
Pb: bytes = stdlib.malloc(DBGSTACK_PATH_CAP)
|
||
if Pb == None:
|
||
EmitEarly("[dbgstack] WARN: path buffer alloc failed, ctrl handler not installed\n")
|
||
return 0
|
||
Plen: ULONG = wf.GetModuleFileNameA(None, Pb, ULONG(DBGSTACK_PATH_CAP))
|
||
if Plen == 0 or t.CSizeT(Plen) >= t.CSizeT(DBGSTACK_PATH_CAP):
|
||
stdlib.free(t.CPtr(Pb))
|
||
EmitEarly("[dbgstack] WARN: GetModuleFileNameA failed, ctrl handler not installed\n")
|
||
return 0
|
||
if t.CSizeT(Plen) + 16 > t.CSizeT(DBGSTACK_PATH_CAP):
|
||
# 余量 16 字节同时覆盖 ".stackmap"(10) 与 ".stackdump.txt"(16),见 DBGSTACK_FILE_APPEND_DATA 注释。
|
||
stdlib.free(t.CPtr(Pb))
|
||
EmitEarly("[dbgstack] WARN: exe path too long to append suffix safely, ctrl handler not installed\n")
|
||
return 0
|
||
string.strcpy(str(Pb + t.CInt(Plen)), ".stackmap")
|
||
# ② 旁文件存在性 = 总开关(不存在 ⇒ 立刻返回,零副作用)
|
||
hFile: HANDLE = wf.CreateFileA(Pb, wf.GENERIC_READ, wf.FILE_SHARE_READ, t.CPtr(0),
|
||
wf.OPEN_EXISTING, wf.FILE_ATTRIBUTE_NORMAL, t.CPtr(0))
|
||
if hFile == INVALID_HANDLE_VALUE:
|
||
Err0: ULONG = GetLastError()
|
||
stdlib.free(t.CPtr(Pb))
|
||
if Err0 != ULONG(ERROR_FILE_NOT_FOUND):
|
||
EmitEarlyCode("[dbgstack] WARN: cannot open <exe>.stackmap, ctrl handler not installed ", Err0)
|
||
return 0
|
||
# ③ 状态对象 + 全部缓冲(回调线程零分配)
|
||
_State = stdlib.malloc(DbgStackState.__sizeof__())
|
||
if _State == None:
|
||
stdlib.free(t.CPtr(Pb))
|
||
CloseHandle(hFile)
|
||
EmitEarly("[dbgstack] WARN: state alloc failed, ctrl handler not installed\n")
|
||
return 0
|
||
string.memset(_State, 0, DbgStackState.__sizeof__())
|
||
_State.LineBuf = stdlib.malloc(DBGSTACK_LINE_CAP)
|
||
_State.ContextBuf = t.CPtr(stdlib.calloc(1, wp.CONTEXT.__sizeof__()))
|
||
_State.FrameBuf = t.CPtr(stdlib.calloc(1, dbg.STACKFRAME64.__sizeof__()))
|
||
_State.OutHandle = wf.GetStdHandle(wf.STD_ERROR_HANDLE)
|
||
if _State.OutHandle == None or _State.OutHandle == INVALID_HANDLE_VALUE:
|
||
_State.OutHandle = wf.GetStdHandle(wf.STD_OUTPUT_HANDLE)
|
||
# 输出句柄在此一次性确定并锁定到 _DumpOut:转储期只认它,不再重取。
|
||
_DumpOut = _State.OutHandle
|
||
_State.MainThreadId = wp.GetCurrentThreadId()
|
||
if _State.LineBuf == None or _State.ContextBuf == None or _State.FrameBuf == None:
|
||
# 安装期就把"哪个缓冲没分配上"变成显式诊断(而不是等按了键、在回调线程里才发现):
|
||
# 三个缓冲逐个点名;任何一项失败都让 Install 返回 0(= 未安装,走零副作用降级),
|
||
# 因为缺任一缓冲都无法安全转储。此处 _State 已建立,EmitEarly 会自动转发到
|
||
# EmitText(同一输出通道 + 行号前缀);_State.LineBuf 为 None 时 EmitEarly 自行
|
||
# 退化为直接 WriteFile,不会因缺缓冲而丢提示。
|
||
stdlib.free(t.CPtr(Pb))
|
||
CloseHandle(hFile)
|
||
if _State.LineBuf == None:
|
||
EmitEarly("[dbgstack] WARN: LineBuf alloc failed, ctrl handler not installed\n")
|
||
elif _State.ContextBuf == None:
|
||
EmitEarly("[dbgstack] WARN: ContextBuf alloc failed, ctrl handler not installed\n")
|
||
elif _State.FrameBuf == None:
|
||
EmitEarly("[dbgstack] WARN: FrameBuf alloc failed, ctrl handler not installed\n")
|
||
else:
|
||
EmitEarly("[dbgstack] WARN: runtime buffer alloc failed, ctrl handler not installed\n")
|
||
return 0
|
||
# ③b 打开转储文件 <exe>.stackdump.txt(追加写:OPEN_ALWAYS 不存在则创建 + FILE_APPEND_DATA
|
||
# 使每次 WriteFile 都追加到末尾)。放在旁文件装载之前 ⇒ 装载期诊断也一并落盘。
|
||
# 写的是 UTF-8 字节原样,不受控制台代码页影响。(文件本身仅供查看,无需 CloseHandle
|
||
# 之外的清理:进程存活期间常驻,供每次转储持续追加。)
|
||
string.strcpy(str(Pb + t.CInt(Plen)), ".stackdump.txt")
|
||
_DumpFile = wf.CreateFileA(Pb, DBGSTACK_FILE_APPEND_DATA,
|
||
wf.FILE_SHARE_READ | wf.FILE_SHARE_WRITE, t.CPtr(0),
|
||
wf.OPEN_ALWAYS, wf.FILE_ATTRIBUTE_NORMAL, t.CPtr(0))
|
||
if _DumpFile == INVALID_HANDLE_VALUE:
|
||
_DumpFile = None
|
||
EmitText("[dbgstack] WARN: cannot open stack dump file, console-only output\n")
|
||
else:
|
||
DN: int = stdio.snprintf(_State.LineBuf, t.CSizeT(DBGSTACK_LINE_CAP),
|
||
"[dbgstack] stack dump also written to: %s\n", str(Pb))
|
||
EmitLine(DN)
|
||
# ④ 旁文件装载(失败只禁用映射,仍安装 handler 以便至少拿到地址栈)
|
||
# 校验失败的具体原因由 LoadMapFile 自己打印(MapFailPlain/Hdr/Entry),此处不再兜一句笼统警告。
|
||
Sz: LARGE_INTEGER = LARGE_INTEGER()
|
||
if wf.GetFileSizeEx(hFile, t.CPtr(c.Addr(Sz))) == 0:
|
||
NoteError("[dbgstack] WARN: GetFileSizeEx(<exe>.stackmap) failed, addresses only ", GetLastError())
|
||
else:
|
||
LoadMapFile(hFile, t.CInt64T(Sz.QuadPart))
|
||
CloseHandle(hFile)
|
||
stdlib.free(t.CPtr(Pb))
|
||
# ⑤ dbghelp 初始化(**必须在此完成**:Install 在进程启动期的主线程上跑,而 Ctrl+C/Break
|
||
# 回调在新线程上方可执行——dbghelp 的模块枚举在回调线程里首次调用可能不可用)
|
||
# + 两个跨模块回调地址(动态解析;不依赖跨模块函数名 → 函数指针的隐式转换)
|
||
if DBGSTACK_USE_DBGHELP != 0:
|
||
# SymSetOptions 必须在 SymInitialize 之前:DEFERRED_LOADS 让下面的模块枚举
|
||
# 不会真去加载符号/PDB,NO_PROMPTS 避免弹交互对话框卡住进程启动。
|
||
dbg.SymSetOptions(t.CUInt32T(dbg.SYMOPT_DEFERRED_LOADS | dbg.SYMOPT_UNDNAME
|
||
| dbg.SYMOPT_NO_PROMPTS))
|
||
# fInvadeProcess **必须为 1(TRUE)**:它让 dbghelp 枚举已加载模块填进自己的模块表。
|
||
# 置 0 时模块表为空 ⇒ SymFunctionTableAccess64 取不到 RUNTIME_FUNCTION(返回 NULL)、
|
||
# SymGetModuleBase64 返回 0 ⇒ StackWalk64 **首次调用即失败、一帧都走不出**
|
||
# (实测现象正是"符号回溯=可用 / RIP-RSP-RBP 正常,却 0 帧")。这是缺陷 2 的真因。
|
||
if dbg.SymInitialize(wp.GetCurrentProcess(), None, 1) != 0:
|
||
_State.SymReady = 1
|
||
else:
|
||
NoteError("[dbgstack] WARN: SymInitialize failed (ASan also uses DbgHelp"
|
||
" and conflicts with us; RBP chain is the primary path anyway) ", GetLastError())
|
||
# 问题 B:ASan 运行时会自己初始化 DbgHelp 做符号化,与本模块的 SymInitialize 互斥。
|
||
# 安装期就探测一次并显式告警(转储时 DumpMainThread 会据此打印
|
||
# symwalk=disabled reason=dbghelp_conflict_with_asan,并改以 RBP 链为主路径)。
|
||
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("[dbgstack] WARN: ASan runtime detected, it also initializes DbgHelp;"
|
||
" dbghelp symwalk disabled, RBP chain will be the primary path\n")
|
||
Hdbg: HANDLE = GetModuleHandleA("dbghelp.dll")
|
||
if Hdbg != None and Hdbg != INVALID_HANDLE_VALUE:
|
||
_State.FuncTableAddr = GetProcAddress(Hdbg, "SymFunctionTableAccess64")
|
||
_State.ModuleBaseAddr = GetProcAddress(Hdbg, "SymGetModuleBase64")
|
||
if _State.FuncTableAddr == None or _State.ModuleBaseAddr == None:
|
||
EmitText("[dbgstack] WARN: StackWalk64 callback addresses incomplete,"
|
||
" dbghelp enhancement unavailable (RBP chain is the primary path)\n")
|
||
# 自检:dbghelp 的模块表里是否真的能找到主模块(SymGetModuleBase64 返回非 0)。
|
||
# 置 0 时 StackWalk64 必然 0 帧;结果同时进 DumpMainThread 的头部诊断行
|
||
# (`[diag] StackWalk64 first:` / `symwalk=`)与 dbg_usable 判定,不必等按了键才发现。
|
||
if _State.SymReady != 0:
|
||
if dbg.SymGetModuleBase64(wp.GetCurrentProcess(),
|
||
t.CUInt64T(GetModuleHandleA(None))) != 0:
|
||
_State.ModInited = 1
|
||
else:
|
||
EmitText("[dbgstack] WARN: dbghelp module table lacks main module (SymGetModuleBase64=0),"
|
||
" StackWalk64 will produce 0 frames\n")
|
||
# ⑥ 注册(必须最后一步:此前的一切状态都已就绪)。用 _HandlerReg 守卫"只注册一次":
|
||
# 同一按键被触发多次回调会重复执行同一转储(输出重复),这里彻底杜绝重复注册。
|
||
if _HandlerReg == 0:
|
||
if wc.SetConsoleCtrlHandler(CtrlHandler, 1) == 0:
|
||
NoteError("[dbgstack] WARN: SetConsoleCtrlHandler failed ", GetLastError())
|
||
return 0
|
||
_HandlerReg = 1
|
||
_State.Installed = 1
|
||
EmitText("[dbgstack] ready: Ctrl+C prints stack then exits, Ctrl+Break prints then continues\n")
|
||
return 1
|