Files
Viper_VRTE/dbgstack.vp
T
TermiNexus b208eae8f3 feat(dbgstack,parser,asm): 新增按键栈转储运行时;前置数组声明符/union 语法;asm 迁移新语法
新增 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。
2026-09-25 13:11:15 +08:00

1743 lines
108 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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