按规范「注释只讲解作用,不掺杂历史原因,尽量英文」:把 includes 下 75 个 .vp 的中文 # 注释与三引号 docstring 改写为英文,每个注释块压到 2 行以内。 保留要点:作用、约束、危险警告(不得/必须/注意)、ABI 与类型约定(如 BYTEPTR | t.CPtr = i8** 的跨模块坍缩坑)、位宽/长度约定、fail-fast 要求、内存与句柄所有权。 未动:文件头注释(规范豁免,仍是中文)、运行时中文字符串常量(printf / fatal_error / MlirFail 等用户可见消息,属代码字面量)。 验证:独立脚本比对 HEAD 与工作区的代码骨架(剥离 # 注释与三引号 docstring 后的非空行)——75 个文件逐字节一致;行尾 CRLF/LF 未被改动;全文剩余中文仅 547 行头注释、0 条行尾注释。bootstrap 三重重编 SUCCESS;全量 77/77 顺序回归通过。
2142 lines
126 KiB
Plaintext
2142 lines
126 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 位标志做「检查并占用」:并发回调最多导致一次重复转储(诊断工具可接受),
|
||
# 且不引入任何链接期依赖。
|
||
|
||
# =============================================================================
|
||
# traceback — 运行时「按键取栈」模块(Ctrl+C / Ctrl+Break 时转储主线程调用栈)
|
||
#
|
||
# 解决的问题:编译器(或任何长任务)卡死时,用户按一个键就能拿到「现在卡在哪一行」
|
||
# 的完整调用栈——从 CRT 入口(mainCRTStartup / __tmainCRTStartup)到当前帧。
|
||
#
|
||
# -----------------------------------------------------------------------------
|
||
# 一、触发语义(SetConsoleCtrlHandler 回调)
|
||
# -----------------------------------------------------------------------------
|
||
# Ctrl+C (CTRL_C_EVENT) → 打印调用栈后**返回 FALSE**(交还系统默认处理 ⇒ 进程终止)
|
||
# Ctrl+Break (CTRL_BREAK_EVENT)→ 打印调用栈后**返回 TRUE**(已处理 ⇒ 进程继续运行)
|
||
# 其它控制事件(关窗/注销/关机,CTRL_CLOSE_EVENT 等)→ 一律返回 FALSE,不转储、不干预。
|
||
# **崩溃**(未处理异常,经 SetUnhandledExceptionFilter 捕获)→ 在**出错线程**上打印调用栈后
|
||
# 返回 EXCEPTION_CONTINUE_SEARCH(异常交还系统 ⇒ 退出码 / WER / ASan 报告行为原样保留),
|
||
# 见第十四节。
|
||
# 回调由系统在**新建的线程**上执行,不是主线程——所以「当前执行到哪」必须靠
|
||
# SuspendThread(主线程) + GetThreadContext 取得主线程寄存器,再回溯主线程的栈。
|
||
# 同一时刻**只允许一次转储**:连续按键会由系统在多个新线程上并发回调,而它们共用
|
||
# 同一个 LineBuf 与输出句柄 ⇒ 输出互相撕裂(实测出现过"降级:"行重复且交织)。
|
||
# 故 DumpMainThread 入口用内联汇编 `lock cmpxchg`(AtomicTryEnterDump)对 _DumpLock
|
||
# 做原子"检查并占用":抢占失败的调用直接返回(不挂起主线程、不输出任何内容)。
|
||
# 每次转储还会打印自增 Seq 与回调线程 id:若两条转储记录的 Seq 不同 ⇒ 顺序两次;
|
||
# 同一 Seq 的行却互相截断 ⇒ 说明仍存在共享缓冲的并发写(据此定位,不再靠猜)。
|
||
#
|
||
# -----------------------------------------------------------------------------
|
||
# 二、安装时机与总开关(本模块的开关落地点)
|
||
# -----------------------------------------------------------------------------
|
||
# 约定:**旁文件存在** ⟺ 编译时用了 TPC 的 `--traceback`。
|
||
# 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,再二分查找记录区。
|
||
# 命中 → 帧行 " File \"<文件>\", line <行>, in <函数名>",其下再打一行 4 空格缩进的源码
|
||
# (运行期重开该文件读第 <行> 行;读不到就整行不打,见第十三节);
|
||
# 未命中 → " File \"<0x<地址> +0x<rva> no source line mapping>\", line 0, in <unknown>";
|
||
# 地址落在主模块之外(更低,典型为 DLL/系统帧)→ " File \"<0x<地址> external>\", line 0, in <unknown>"。
|
||
# 三种形态都把地址/RVA 保留在同一行(见第十三节)。
|
||
# 装载时做一次全表校验(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、
|
||
# Install 时 SymInitialize/SymRefreshModuleList 成功、两个回调地址与帧缓冲齐备、且
|
||
# SymGetModuleBase64(主模块) 探针通过;**ASan 运行时存在只表示 DbgHelp 状态共享,不再
|
||
# 一票否决**);任一前提不满足都会打印明确原因(`[diag] symwalk=disabled reason=…`),
|
||
# **不静默 0 帧**。
|
||
# 本轮新增:每步无条件打 `[diag] rbp:` 原始读数 + 判定、以及 `walk: fallback=stack_scan`
|
||
# 扫描推断帧(`~` 前缀)——详见第十二节。
|
||
# 主路径实现:打印当前 RIP,然后沿主线程 RBP 链读 [rbp](调用者 rbp)与 [rbp+8]
|
||
# (返回地址)逐帧回溯;要求 rbp 8 字节对齐、严格向高地址增长(`[rbp] > rbp`)、
|
||
# 单步跨度 <= TRACEBACK_RBP_MAX_STEP、返回地址落在主模块基址区间内,否则立即停止
|
||
# (防野指针把回溯带飞);**读内存一律经 ReadProcessMemory,绝不直接解引用野 rbp**。
|
||
# RBP 链只覆盖保持帧指针的函数;帧指针一旦未保留([rbp+8] 不是返回地址)链即在第 1
|
||
# 步断——此时由 ScanStack 兜底,另叠一层 dbghelp 加强(可用时)。
|
||
# --traceback 时 TPC 给 llc 加 `-frame-pointer=all`(Phase2Translator.py:2187-2193);
|
||
# 该 flag 是否真正生效见第十二节 C 的调查结论与修法落点。
|
||
# 【取证:先取证,再决策】每次转储在 banner 之后打印一组前置诊断(**TRACEBACK_DIAG=1 时**)——
|
||
# 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,
|
||
# 把 TRACEBACK_USE_DBGHELP 置 0:该常量比较会在编译期把唯一含 dbghelp 调用的分支
|
||
# 整体去掉(见 TRACEBACK_USE_DBGHELP 的注释),只保留 RBP 链回溯。
|
||
#
|
||
# -----------------------------------------------------------------------------
|
||
# 六、TPC 侧调用入口(符号与签名)
|
||
# -----------------------------------------------------------------------------
|
||
# import traceback
|
||
# traceback.Install() # 返回 int:1=handler 已注册;0=未安装(无旁文件/失败)
|
||
# # 注册两个:SetConsoleCtrlHandler(Ctrl+C/Break)+
|
||
# # SetUnhandledExceptionFilter(崩溃),见第一节/第十四节
|
||
# 调用时机:进程启动期一次即可。两种等价做法——
|
||
# (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 成功时打印一行
|
||
# `[traceback] 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;` File "<文件>", line <行>, in <函数名>` 帧行保留源路径
|
||
# 原文(分隔符/新增说明文字一律 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 + 自备常量
|
||
# TRACEBACK_FILE_APPEND_DATA(FILE_APPEND_DATA,wf 未定义);
|
||
# 转储重入保护用内联汇编的一条 `lock cmpxchg`(AtomicTryEnterDump)对模块级
|
||
# 32 位 _DumpLock 做原子"检查并占用"(不用 InterlockedCompareExchange:x64 上是
|
||
# 编译器内部函数、无导出符号、不可链接)。
|
||
# 本轮新增(真 Python traceback 输出):kernel32!GetEnvironmentVariableA 的**本模块自声明**
|
||
# (win32base 未声明,仓库仅 VET/App/lib/VDirs.vp:45 有;kernel32 本就链接,TPC/TPV 均
|
||
# 可解析),用于 TRACEBACK_DIAG 开关;其余仅用既有 API(见第十三节)。
|
||
#
|
||
# -----------------------------------------------------------------------------
|
||
# 九、"0 帧"的可执行结论(哪种情形 → 看哪个现场字段)
|
||
# -----------------------------------------------------------------------------
|
||
# ⚠ 本节所有取证行都是 `[diag]` 行,故**只在 TRACEBACK_DIAG=1 时产出**(见第十三节 C);
|
||
# 排查"0 帧"时请先用 `set TRACEBACK_DIAG=1` 启动目标进程。
|
||
# 首帧取证两行(" [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 的既有输出通道;
|
||
# 这些行**只在 TRACEBACK_DIAG=1 时输出**):
|
||
# * DumpMainThread 在**调用前**打 " [diag] walk: enter use_dbghelp=… symready=…
|
||
# modinited=… frame_buf=… mapok=… asan=…",**调用后**打 " [diag] walk: leave
|
||
# frames=… mapok=…"。两行都在 ⇒ walk 一定进了 RBP 主路径(dbghelp 加强可选)。
|
||
# * 若 **连 walk: enter 都不出现**(且 TRACEBACK_DIAG=1)⇒ 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 在 **TRACEBACK_DIAG=1** 时给**每一行**加全局递增前缀 " [dump#<k>] "
|
||
# (k 由 _DumpLineNo 每次 +1);关闭(默认)时**不加前缀**,使帧行保持纯 Python
|
||
# traceback 形态(见第十三节)。
|
||
# * 同一段文本出现多份且**序号重复**(同一 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 帧 / DbgHelp 状态共享**:ASan 运行时会自己注册 DbgHelp 符号化器,本模块的
|
||
# SymInitialize 因此被拒(DbgHelp 进程上下文是**进程级**状态、按句柄唯一)。但这**不是
|
||
# 冲突**:状态是共享的,用同一个 GetCurrentProcess() 伪句柄即可复用其模块表。故 Install
|
||
# 的判定改为**取证式**三步:SymInitialize 成功 → SymReady=1;失败 → SymRefreshModuleList
|
||
# 刷新共享模块表(官方"外部初始化后再刷新"写法);再统一用 SymGetModuleBase64(主模块)
|
||
# 探针裁决——探针为 0 则 SymReady 回退 0 并打印原因,绝不静默 0 帧。回溯路径顺序:
|
||
# * 主路径 = `WalkRbpChain`(纯 RBP 链,**零 dbghelp 依赖**);
|
||
# * **第二兜底 = `ScanStack` 栈扫描**(零 dbghelp、且**不依赖 RBP / 帧指针**):
|
||
# RBP 链只拿到当前 RIP(<=1 帧)时启用(`walk: fallback=stack_scan`),见第十二节;
|
||
# * dbghelp 加强:只有 RBP 链拿不到更多帧(<=1)且 DbgUsable != 0(= 探针通过)时才走
|
||
# `WalkViaDbgHelp`。它**清空已收帧并整段重建**(顺带丢掉扫描推断帧),是唯一能用 PE
|
||
# 展开表(.pdata)穿过**无帧指针的系统帧**、回到 .vp 栈的路径——Ctrl+C 的典型现场
|
||
# (主线程阻塞在系统 DLL 里、RBP=0)只有它能还原真实调用链。
|
||
# * 降级**必须给原因**(不静默 0 帧):`[diag] symwalk=disabled reason=…`,
|
||
# reason ∈ {compile_time_switch, syminitialize_failed_or_main_module_missing,
|
||
# 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+TRACEBACK_MODULE_MAX) 内才**接受**该帧。
|
||
#
|
||
# -----------------------------------------------------------------------------
|
||
# 十二、本轮新增:RBP 链逐步原始读数 + 栈扫描兜底(应答"栈走不长")
|
||
# -----------------------------------------------------------------------------
|
||
# 现场(用户实测,`#0` 已正确解析:RIP 在主模块、映射/查表/帧 #0 全对):`#1 = 0x121fa`
|
||
# ——远小于主模块基址、也不是栈地址 ⇒ 链在第 1 步即断。⇒ 先把"断在哪一步、为什么断"
|
||
# 变成可读数据,再补一条**不依赖 RBP**的路径。
|
||
# A. **逐步原始读数**(`WalkRbpChain` 内,前缀 `[diag] rbp:`,**先于任何 reject 早退**,
|
||
# TRACEBACK_DIAG=1 时才输出):
|
||
# 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。
|
||
# 仅被接受的步才 FramePush 入帧缓冲(旧实现在校验前就输出,会把 `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>`);推断帧入缓冲时带 Scanned=1,打印时在帧行末尾加
|
||
# ` [inferred]`(` File "…", line 0, in <unknown> [inferred]`)明确标注**扫描推断**
|
||
# (可含误报,不与权威帧混淆,见第十三节)。全部读取只经 ReadProcessMemory,
|
||
# 绝不直接解引用主线程内存(诊断设施不许把"卡死"变成"崩溃")。
|
||
# C. **帧指针是否保留**(调查结论与 TPC 侧修法落点,本模块只报不改):
|
||
# * 单一 llc 调用点 = `TransPyC/lib/Projectrans/Phase2Translator.py:2160 _llc_compile`,
|
||
# 它在 `--traceback` 打开时把 `-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),但 `--traceback` 时 `hotspot_on`(1144)为真 ⇒ 1196-1204 会删除旧 obj
|
||
# 强制重编;include 模块另有隔离目录 `includes.binary_traceback`(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 是否采纳默认值"。
|
||
# * 【补充实测,Test/LLvmLiteTest Ctrl+C】`RBP=0x0`、RIP 落在系统 DLL:拒绝原因是
|
||
# `rbp_zero`(不是 `ret_out_of_module`)⇒ 断链点**不在 .vp 函数**,而在被中断的那一层
|
||
# 系统帧。RBP 是 callee-saved,但系统/CRT 的免帧指针代码会把它当**临时寄存器**用
|
||
# (`xor ebp,ebp` 是常见的清零写法),故中断现场的 RBP 与"最近一个维护帧指针的祖先"
|
||
# 无关。⇒ 结论:**给 .vp 函数保留帧指针无法救这条路**(链在更内层就已断),只有基于
|
||
# PE 展开表(.pdata)的回溯(dbghelp!StackWalk64,见第十一节 B)才能从这种现场还原
|
||
# 真实调用链。帧指针开关因此**不是**本轮的修法落点,保留在上仅作历史记录。
|
||
#
|
||
# -----------------------------------------------------------------------------
|
||
# 十三、本轮:真 Python traceback 风格输出 + TRACEBACK_DIAG 开关
|
||
# -----------------------------------------------------------------------------
|
||
# A. 输出形状(默认,TRACEBACK_DIAG 未开)——整段 traceback 形如:
|
||
# Traceback (most recent call last):
|
||
# File "D:\...\App\main.vp", line 238, in main
|
||
# w_upper: str = w_str.upper()
|
||
# File "<0x7ff6... +0x1a2b no source line mapping>", line 0, in <unknown>
|
||
# File "<0x7ffb... external>", line 0, in <unknown> [inferred]
|
||
# * **帧序反过来了**:回溯从最内层(当前 RIP)向调用者走,收集顺序 = 从内到外,打印时
|
||
# 逆序 ⇒ **最外层(CRT/调用者)在前、最内层最后**(与 Python traceback 一致)。故必须
|
||
# 先把帧收集进缓冲(FramePush),走栈结束后再逆序打印(DumpMainThread)。
|
||
# * 命中源位置的帧打两行:第一行 2 空格缩进 `File "路径", line N, in 函数名`(函数名取不到
|
||
# 写 `<unknown>`);第二行 4 空格缩进 + 该行源码——**运行期重开该文件**顺序读第 N 行
|
||
# (去行首尾空白、超长按 TRACEBACK_LINE_CAP 截断);**打不开 / 行号越界 / 读失败则整行
|
||
# 不打**(无占位、无报错,静默跳过),实现见 EmitSourceText(栈上缓冲,回调线程零堆分配)。
|
||
# * ScanStack 推断帧在帧行末尾加 ` [inferred]`(保留"可能是误报"的信息,不再用 `~` 前缀)。
|
||
# * 未命中映射的帧 → `File "<0x<地址> +0x<rva> no source line>", line 0, in <unknown>`
|
||
# (地址+RVA 保留在同一行);落在主模块之外的帧 → `File "<0x<地址> external>", line 0, in <unknown>`。
|
||
# B. 帧缓冲:TracebackState.FrameAddr / FrameScan(`t.CArray[uint64/int32, TRACEBACK_FRAME_BUF]`,
|
||
# `TRACEBACK_FRAME_BUF = TRACEBACK_MAX_FRAMES + TRACEBACK_SCAN_MAX_HITS = 128`),
|
||
# **内嵌于状态对象** ⇒ 由 Install 的 `TracebackState.__sizeof__()` 分配覆盖,无需独立 malloc、
|
||
# 回调线程零分配。缓冲满时新帧直接丢弃(不越界),最终 `frames=` 以实际收集数为准。
|
||
# C. **TRACEBACK_DIAG 开关**:读环境变量 `TRACEBACK_DIAG`,值 == "1" 才开;**每进程只读一次
|
||
# 并缓存**(_DiagFlag -1/0/1,见 DiagEnabled)。关闭(默认)时不输出任何 `[diag] ...` /
|
||
# `srcline map: ...` 行,也不加 ` [dump#<k>] ` 行号前缀(见第十节 B),使 dump 成为干净的
|
||
# traceback;打开时诊断信息量与既有一致(含逐帧 `[diag] rbp:` / `[diag] scan:` / symwalk 行)。
|
||
# 取值 API = kernel32!GetEnvironmentVariableA(本模块顶部自声明,见第八节)。
|
||
# D. 转储序号与落文件提示移到整段 traceback **之后**(` [traceback] dump seq=… also appended
|
||
# to <exe>.stackdump.txt` 行),不再插在帧中间;`Ctrl+C` 转储后返回 FALSE(进程退出)、
|
||
# `Ctrl+Break` 转储后返回 TRUE(继续运行)语义不变。
|
||
#
|
||
# -----------------------------------------------------------------------------
|
||
# 十四、崩溃转储(未处理异常;应答"崩溃无错误堆栈")
|
||
# -----------------------------------------------------------------------------
|
||
# A. 动机:Ctrl+C 有栈,**崩溃没有**——ASan 一旦过了它的报告窗口就什么都不打印,OS 默认只留一个
|
||
# 退出码(同一处崩溃实测:开 ASan = 0x80000003 STATUS_BREAKPOINT(int3),关 ASan =
|
||
# 0xC0000005 ACCESS_VIOLATION,两者都**没有堆栈**)。故 Install 追加注册
|
||
# kernel32!SetUnhandledExceptionFilter(CrashHandler),由 CrashHandler → DumpCrashThread
|
||
# 在**出错线程**上补出这条缺失的栈。
|
||
# B. DumpCrashThread 的三条纪律:
|
||
# * **绝不吞异常**:过滤器恒定返回 EXCEPTION_CONTINUE_SEARCH(0),退出码 / WER / ASan 报告
|
||
# 的行为原样保留,本模块只加堆栈;
|
||
# * **不解引用 OS 指针**:EXCEPTION_POINTERS({ExceptionRecord, ContextRecord})与
|
||
# EXCEPTION_RECORD 头部(u32 ExceptionCode / u32[…] ExceptionAddress)一律经
|
||
# ReadProcessMemory 读取;
|
||
# * **不写 OS 的 CONTEXT**:StackWalk64 每走一帧都会回写它遍历的那条记录,故先把 OS 给的
|
||
# CONTEXT 拷进 _State.ContextBuf(尺寸已与 winnt.h 对齐 0x4D0)再从副本走栈。
|
||
# C. 走栈顺序与打印:dbghelp 优先(崩溃点通常在系统 DLL 里,RBP 链必然无从下手)→ RBP 链 →
|
||
# 栈扫描,与 Ctrl+C 路径同一套 WalkViaDbgHelp / WalkRbpChain / ScanStack 与 PrintFrames;
|
||
# 与 Ctrl+C 转储**共用同一把重入锁**(AtomicTryEnterDump),故一次只可能有一份 dump:
|
||
# 崩溃若发生在转储进行中,本次崩溃不转储(锁被占),避免二次伤害。
|
||
# 输出形状:`=== TPC crash dump (unhandled exception; the process keeps terminating) ===`
|
||
# → ` Trigger=unhandled-exception code=0x… addr=0x… av_kind=… av_addr=0x… crashthread=…`
|
||
# → ` exe base=… RIP=… RSP=… RBP=…` → ` regs: rcx=… rdx=… r8=… r9=… r12=…`
|
||
# → 与 Ctrl+C 逐字同形的 Traceback 帧(含源码行回读)→ ` frames=N` → 落文件提示
|
||
# → `=== crash dump end ===`。
|
||
# * `av_kind`/`av_addr` 取自 EXCEPTION_RECORD 的 ExceptionInformation[0/1](0=读 1=写 8=执行;
|
||
# 数据地址),只在 0xC0000005 类访问违规时有效——崩溃现场正是"原始数字胜过猜测"的场合;
|
||
# * `regs` 给的是**出错帧的实参现场**(Win64 前 4 个整型实参 = rcx/rdx/r8/r9;r12 是该帧
|
||
# 用来解引用指针的寄存器):一条就能区分「实参传错」与「实参对、指针所指内容错」,
|
||
# 不必再来一轮重编。Ctrl+C 路径不打印它(那里没有"出错指令的算子"可看)。
|
||
# * **不打印 `[RBP+8]` 当返回地址**:.vp 帧的 RBP 是 locals base(`leaq N(%rsp), %rbp`),
|
||
# 返回地址在 RBP + 帧字节数 - 8(逐函数不同;实测 _TypePrintDepth 是 RBP+0x38),
|
||
# `[RBP+8]` 只是无关局部量。调用者一律以 .stackmap 走栈结果为准。
|
||
# 中文件头(栈转储/触发=)只属于 Ctrl+C 路径,崩溃路径不动它。
|
||
# D. 已知边界:ASan 的 VEH 优先于未处理异常过滤器——若某次崩溃由 ASan 正常报告(它自带堆栈),
|
||
# 本路径不会被调用;本路径覆盖的正是「ASan 报不出来 / 没开 ASan」那两类(实测的两个退出码
|
||
# 都在其中)。栈耗尽类崩溃(本路径自身也需要栈)不在保障范围内。
|
||
# =============================================================================
|
||
# This module needs kernel32!GetEnvironmentVariableA for the TRACEBACK_DIAG switch, but
|
||
# includes/w32/win32base.vp does not declare it (the repository declares it only in
|
||
# VET/App/lib/VDirs.vp:45). Declared here with the same style; kernel32 is linked anyway,
|
||
# so both TPC and TPV resolve it.
|
||
# DWORD GetEnvironmentVariableA(LPCSTR lpName, LPSTR lpBuffer, DWORD nSize);
|
||
extern def GetEnvironmentVariableA(lpName: LPCSTR, lpBuffer: CHARPTR, nSize: DWORD) -> DWORD
|
||
|
||
|
||
# ---- Compile-time switch -------------------------------------------------------------
|
||
|
||
# 1 = allow dbghelp!StackWalk64 on the optional-augmentation path (artifacts MUST link -ldbghelp);
|
||
# the main path is WalkRbpChain (no dbghelp) and dbghelp is only added when the RBP chain yields no
|
||
# more frames and the Install probe passes; 0 = no dbghelp at all, and constant folding removes the
|
||
# whole `if TRACEBACK_USE_DBGHELP != 0:` branch (same mechanism as MEMBUDDY_THREAD_SAFE in
|
||
# includes/memhub.vp), leaving no dbghelp link-time symbols.
|
||
define TRACEBACK_USE_DBGHELP = 1
|
||
|
||
# Trigger event values: see includes/w32/win32console.vp (CTRL_C_EVENT=0 / CTRL_BREAK_EVENT=1).
|
||
# Crash-dump filter return value (winnt.h EXCEPTION_*): the unhandled-exception dump must never
|
||
# swallow a crash, so its filter always returns CONTINUE_SEARCH (= 0) and hands the exception back
|
||
# to the system default handling (exit code / WER / ASan report stay exactly as before).
|
||
define TRACEBACK_EXCEPTION_CONTINUE_SEARCH = 0
|
||
|
||
# ---- Sidecar (.stackmap) format ----
|
||
# 20-byte header (magic "TPCS" read little-endian as u32); 20-byte records.
|
||
define TRACEBACK_MAP_MAGIC = 0x53435054# "TPCS" ('T','P','C','S' little-endian)
|
||
define TRACEBACK_MAP_VERSION = 1
|
||
define TRACEBACK_MAP_HEADER = 20# header size in bytes
|
||
define TRACEBACK_ENTRY_SIZE = 20# bytes per record
|
||
define TRACEBACK_ENTRY_FILEOFF = 8# file_off offset within a record
|
||
define TRACEBACK_ENTRY_LINE = 12# line offset within a record
|
||
define TRACEBACK_ENTRY_FUNCOFF = 16# func_off offset within a record
|
||
# Sidecar size cap (defence only: a corrupt entry_count/str_bytes must not demand a huge alloc).
|
||
define TRACEBACK_MAP_LIMIT = 67108864# 64 MiB
|
||
|
||
# ---- Runtime buffers and limits ----
|
||
define TRACEBACK_PATH_CAP = 4096# exe path + ".stackmap" buffer
|
||
define TRACEBACK_LINE_CAP = 1024# single output line buffer
|
||
define TRACEBACK_MAX_FRAMES = 64# max frames printed
|
||
define TRACEBACK_RBP_MAX_STEP = 0x100000# max gap between adjacent rbp values (1 MiB)
|
||
# RBP chain sanity check: a return address is accepted only within [Base, Base+this).
|
||
# 128 MiB is far above any real PE image, a coarse "obviously out of range" filter.
|
||
define TRACEBACK_MODULE_MAX = 0x8000000
|
||
|
||
# ---- Collected-frame buffer (Python-traceback output) ----
|
||
# Frames are collected innermost-first by the walk paths and printed in reverse (outermost
|
||
# first), so the buffer must hold every frame the walk paths can produce: the RBP chain
|
||
# (TRACEBACK_MAX_FRAMES) plus the ScanStack candidates (TRACEBACK_SCAN_MAX_HITS). It is an
|
||
# inline array inside TracebackState, so Install's TracebackState.__sizeof__() allocation
|
||
# covers it (the callback thread allocates nothing). Frames beyond the limit are dropped.
|
||
define TRACEBACK_FRAME_BUF = 128# = TRACEBACK_MAX_FRAMES + TRACEBACK_SCAN_MAX_HITS
|
||
# A hit frame re-reads its source line from the .vp file at dump time, sequentially in fixed
|
||
# stack chunks (no heap allocation from the callback thread).
|
||
define TRACEBACK_SRC_CHUNK = 4096# bytes per ReadFile while scanning for a source line
|
||
|
||
# ---- Stack scan fallback (ScanStack) ----
|
||
# Scan window [RSP, RSP + this) towards higher addresses (caller direction).
|
||
define TRACEBACK_SCAN_MAX_BYTES = 0x200000# 2 MiB
|
||
# Chunked reads: one ReadProcessMemory per chunk instead of one per 8 bytes.
|
||
define TRACEBACK_SCAN_CHUNK_BYTES = 4096# 4 KiB per chunk
|
||
define TRACEBACK_SCAN_CHUNK_WORDS = 512# 4096 / 8 u64 words per chunk
|
||
define TRACEBACK_SCAN_MAX_HITS = 64# max candidate return addresses (scan_candidates=)
|
||
define TRACEBACK_SUSPEND_FAILED = 0xFFFFFFFF# SuspendThread failure return value
|
||
# The suffixes ".stackmap" (10) and ".stackdump.txt" (16, incl. NUL) share the same 16-byte
|
||
# slack check in Install; no extra constant is needed.
|
||
# FILE_APPEND_DATA(0x0004) is not defined by wf; open the dump file with it so every WriteFile
|
||
# appends to the end (combined with OPEN_ALWAYS to create the file when missing).
|
||
define TRACEBACK_FILE_APPEND_DATA = 0x00000004
|
||
|
||
|
||
class TracebackState():
|
||
"""The module's single global state object (allocated once by Install; read-only after).
|
||
|
||
All buffers are allocated during Install (the callback thread allocates nothing) and are
|
||
never freed: this is a process-lifetime diagnostic facility (there is no uninstall path).
|
||
"""
|
||
|
||
Installed: int # 1 = SetConsoleCtrlHandler registered
|
||
MapOk: int # 1 = sidecar loaded and fully validated (address -> source line)
|
||
SymReady: int # 1 = DbgHelp usable (SymInitialize succeeded, or the shared state of
|
||
# another component was adopted AND the module-table probe passed)
|
||
ModInited: int # 1 = dbghelp module table contains the main module
|
||
Dumping: int # reentrancy flag (readable mirror of the _DumpLock atomic lock)
|
||
Seq: int # dump sequence number, +1 per dump entered (concurrency evidence)
|
||
ReadMemFailed: int # 1 = the read-memory callback has failed at least once
|
||
MissReported: int # 1 = first "source line not hit" diagnostic already printed
|
||
ReadMemAddr: uint64 # address of the first failed read-memory call
|
||
ReadMemSize: uint32 # byte count requested by the first failed read-memory call
|
||
ReadMemErr: uint32 # GetLastError of the first failed read-memory call
|
||
MainThreadId: ULONG # thread that ran Install (= the main thread)
|
||
OutHandle: HANDLE # dump output target (stderr, falling back to stdout)
|
||
EntryCount: uint32 # number of records
|
||
StrBytes: uint32 # string pool byte count
|
||
MapBase: bytes # sidecar file base (stdlib.malloc)
|
||
StrBase: bytes # string pool base = MapBase + 20 + EntryCount*20
|
||
FuncTableAddr: VOIDPTR # dbghelp!SymFunctionTableAccess64 (StackWalk64 callback 2)
|
||
ModuleBaseAddr: VOIDPTR # dbghelp!SymGetModuleBase64 (StackWalk64 callback 3)
|
||
ContextBuf: VOIDPTR # CONTEXT buffer (calloc, 16-byte aligned)
|
||
FrameBuf: VOIDPTR # STACKFRAME64 buffer (calloc)
|
||
LineBuf: bytes # output line buffer
|
||
FrameAddr: t.CArray[uint64, TRACEBACK_FRAME_BUF] # collected frames, innermost first
|
||
FrameScan: t.CArray[int32, TRACEBACK_FRAME_BUF] # 0 = authoritative, 1 = inferred (ScanStack)
|
||
FrameCount: int # frames collected so far (also the printed frame count)
|
||
|
||
|
||
# Sole global state: None = not installed (zero side effects while the sidecar is absent).
|
||
# Pointer + lazy allocation instead of a module-level object constant, which would emit a
|
||
# constructor that may run after Install and wipe the state it wrote.
|
||
_State: rawptr[TracebackState] = None
|
||
|
||
|
||
# ---- Dump atomic lock / locked output handle ----
|
||
# _DumpLock: dump reentrancy lock, 0 = free / 1 = held. AtomicTryEnterDump uses one inline
|
||
# `lock cmpxchg` to test-and-set it, closing the read-modify-write race of a plain flag.
|
||
# _State.Dumping is its readable mirror.
|
||
export _DumpLock: uint32 = 0
|
||
|
||
# _DumpOut: output handle locked for the whole dump. Chosen once at DumpMainThread entry so a
|
||
# single dump never splits across stdout/stderr. EmitLine prefers it; Install sets it too.
|
||
_DumpOut: HANDLE = None
|
||
|
||
# _HandlerReg: "register once" guard for SetConsoleCtrlHandler.
|
||
_HandlerReg: int = 0
|
||
|
||
# _DumpFile: append handle of the dump file (OpenAlways + FILE_APPEND_DATA). When None/invalid
|
||
# only the file write is skipped; console output is unaffected.
|
||
_DumpFile: HANDLE = None
|
||
|
||
# _DumpFileFailWarned: report a dump-file write failure only once.
|
||
_DumpFileFailWarned: int = 0
|
||
|
||
# _DumpLineNo: global output line counter, incremented by EmitLine and written as the
|
||
# " [dump#<k>] " prefix of every line (only while TRACEBACK_DIAG is enabled). Repeated
|
||
# numbers prove concurrent writes to the shared LineBuf; strictly increasing numbers mean any
|
||
# truncation is only terminal wrapping.
|
||
_DumpLineNo: int = 0
|
||
|
||
# _DiagFlag: cached TRACEBACK_DIAG state; -1 = not read yet, 0 = off, 1 = on (read once/process).
|
||
_DiagFlag: int = -1
|
||
|
||
|
||
def DiagEnabled() -> int:
|
||
"""Return 1 when the environment variable TRACEBACK_DIAG equals "1" (read once, then cached).
|
||
|
||
With the switch off (default) every "[diag] ..." / "srcline map: ..." line and the per-line
|
||
" [dump#<k>] " counter prefix are suppressed, so a dump is a plain Python-style traceback;
|
||
with it on the previous diagnostic detail is kept. The single cached global keeps both the
|
||
console and the file output consistent within one process.
|
||
"""
|
||
global _DiagFlag
|
||
if _DiagFlag >= 0:
|
||
return _DiagFlag
|
||
Val: t.CArray[char, 8]
|
||
Val[0] = char(0)
|
||
R: DWORD = GetEnvironmentVariableA("TRACEBACK_DIAG", &Val, DWORD(8))
|
||
_DiagFlag = 0
|
||
if R == 1:
|
||
if Val[0] == char(49): # '1'
|
||
_DiagFlag = 1
|
||
return _DiagFlag
|
||
|
||
|
||
# ============================================================
|
||
# Output (WriteFile, bypassing CRT stream locks; see header section 7)
|
||
# Defined before the sidecar reader so load-time diagnostics share this single path.
|
||
# Console text is always ASCII (code-page independent, greppable); the same bytes are also
|
||
# appended to <exe>.stackdump.txt.
|
||
# ============================================================
|
||
|
||
def WriteConsoleOnly(N: int):
|
||
"""Write the first N bytes of LineBuf to the console only (no file).
|
||
|
||
Split out of EmitLine so the "dump file write failed" notice can reach the console without
|
||
re-entering EmitLine (and thus the file write), avoiding recursion. Handle rules match.
|
||
"""
|
||
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), &Wr, None)
|
||
|
||
|
||
def EmitToFile(N: int):
|
||
"""Append the first N bytes of LineBuf to the dump file (raw UTF-8, code-page independent).
|
||
|
||
The file handle is opened at Install (FILE_APPEND_DATA + OPEN_ALWAYS) and kept open; the
|
||
callback thread opens nothing and allocates nothing. A write failure never affects the
|
||
console (EmitLine already wrote it); it is reported once, written straight to the console
|
||
handle via a local stack buffer (not through EmitLine) to avoid recursion.
|
||
"""
|
||
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), &Wr, None) != 0:
|
||
return
|
||
if _DumpFileFailWarned != 0:
|
||
return
|
||
_DumpFileFailWarned = 1
|
||
LB: t.CArray[char, 160]
|
||
M: int = stdio.snprintf(&LB, sizet(160),
|
||
"[traceback] 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, &LB, ULONG(M), &W2, None)
|
||
|
||
|
||
def EmitLine(N: int):
|
||
"""Write the first N bytes of LineBuf to console + dump file (N is snprintf's return value).
|
||
|
||
The single output choke point for EmitText / EmitFrame / every diagnostic:
|
||
0) line-number prefix " [dump#<k>] " (see _DumpLineNo), only while TRACEBACK_DIAG is on:
|
||
inserted in place via string.memmove so callers are unchanged and no extra buffer is
|
||
used; a repeated k proves concurrent writes to LineBuf, a strictly increasing k means
|
||
display wrapping. With the switch off the bytes are written verbatim, keeping the
|
||
Python-traceback frame lines clean.
|
||
1) console (WriteFile, bypassing CRT stream locks): prefers the dump-locked _DumpOut,
|
||
falling back to _State.OutHandle during load (both resolved once at Install).
|
||
2) dump file (EmitToFile): appends the same bytes (UTF-8, code-page independent).
|
||
A file write failure never blocks the console (see EmitToFile); handles are never
|
||
re-resolved mid-dump.
|
||
"""
|
||
global _DumpLineNo
|
||
if _State == None or N <= 0:
|
||
return
|
||
if N > TRACEBACK_LINE_CAP - 1:
|
||
N = TRACEBACK_LINE_CAP - 1
|
||
# Line-number prefix: shift the buffer right by the prefix length, then write the prefix.
|
||
if DiagEnabled() != 0:
|
||
_DumpLineNo = _DumpLineNo + 1
|
||
Pfx: t.CArray[char, 32]
|
||
Pl: int = stdio.snprintf(&Pfx, sizet(32), " [dump#%d] ", _DumpLineNo)
|
||
if Pl > 0:
|
||
# Keep room for the trailing '\0' after prefixing.
|
||
if N + Pl > TRACEBACK_LINE_CAP - 1:
|
||
N = TRACEBACK_LINE_CAP - 1 - Pl
|
||
# Move the trailing '\0' too (N+1) so the buffer stays NUL-terminated.
|
||
string.memmove(t.CPtr(_State.LineBuf + int(Pl)), t.CPtr(_State.LineBuf), sizet(N + 1))
|
||
string.memmove(t.CPtr(_State.LineBuf), &Pfx, sizet(Pl))
|
||
N = N + Pl
|
||
WriteConsoleOnly(N)
|
||
EmitToFile(N)
|
||
|
||
|
||
def EmitText(Text: str):
|
||
"""Write one constant line with no format specifiers (same buffer / WriteFile path)."""
|
||
if _State == None or _State.LineBuf == None:
|
||
return
|
||
N: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP), "%s", Text)
|
||
EmitLine(N)
|
||
|
||
|
||
def EmitEarly(Text: str):
|
||
"""Install-stage early diagnostic (_State / LineBuf not ready yet) on the same output path.
|
||
|
||
With _State and LineBuf ready it forwards to EmitText (same buffer, same handle); otherwise
|
||
it writes via WriteFile(GetStdHandle(STD_ERROR_HANDLE)) directly (the dump's own sink), so
|
||
stdout and stderr never interleave. Install is not a callback thread, so resolving a handle
|
||
once here is harmless.
|
||
"""
|
||
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), &Wr, None)
|
||
|
||
|
||
def EmitEarlyCode(Text: str, Code: ULONG):
|
||
"""EmitEarly variant that appends a GetLastError code (same output path)."""
|
||
if _State != None and _State.LineBuf != None:
|
||
N: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP), "%s(GetLastError=%u)\n", Text, Code)
|
||
EmitLine(N)
|
||
return
|
||
LB: t.CArray[char, 256]
|
||
N2: int = stdio.snprintf(&LB, sizet(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, &LB, ULONG(N2), &Wr, None)
|
||
|
||
|
||
def AtomicTryEnterDump() -> int:
|
||
"""Atomically test-and-set _DumpLock (0→1); returns the previous value (0 = acquired).
|
||
|
||
One inline `lock cmpxchg`: eax = expected old value 0, ecx = new value 1.
|
||
Only this module-level lock and registers are touched; nothing is allocated.
|
||
"""
|
||
Old: uint32 = 0
|
||
__asm__(f"""xor eax, eax
|
||
mov ecx, 1
|
||
lock cmpxchg [{asminp(&_DumpLock, ASM_DESCR.REG_ANY)}], ecx
|
||
mov {asmout(Old, ASM_DESCR.OUTPUT_REG)}, eax""",
|
||
op=[ASM_DESCR.CLOBBER_RAX, ASM_DESCR.CLOBBER_RCX, ASM_DESCR.CLOBBER_MEMORY, ASM_DESCR.CLOBBER_CC])
|
||
return int(Old)
|
||
|
||
|
||
def AtomicLeaveDump():
|
||
"""Release the dump lock: _DumpLock -> 0 (and mirror _State.Dumping -> 0).
|
||
|
||
Only the holder reaches here; another callback thread seeing 1 returns immediately, so a
|
||
plain write of 0 is safe.
|
||
"""
|
||
global _DumpLock
|
||
_DumpLock = 0
|
||
if _State != None:
|
||
_State.Dumping = 0
|
||
|
||
|
||
# ============================================================
|
||
# Sidecar reader (little-endian, byte by byte, avoids unaligned access)
|
||
# ============================================================
|
||
|
||
def ReadU32(Buf: bytes, Off: int) -> uint32:
|
||
"""Read a little-endian u32 from Buf[Off..Off+3] (Buf is an unsigned byte base).
|
||
|
||
Each byte must be masked with 0xFF: Viper's `Buf[i]` is a signed char, so a byte >= 0x80
|
||
would sign-extend through the u32 cast (0xE8 -> 0xFFFFFFE8) and corrupt the high bits,
|
||
blowing up `20+count*20+str_bytes` and making the sidecar unloadable. Masking after the
|
||
cast yields 0..255 whether the cast sign- or zero-extends.
|
||
"""
|
||
Val: uint32 = uint32(Buf[Off]) & uint32(0xFF)
|
||
Val = Val | ((uint32(Buf[Off + 1]) & uint32(0xFF)) << 8)
|
||
Val = Val | ((uint32(Buf[Off + 2]) & uint32(0xFF)) << 16)
|
||
Val = Val | ((uint32(Buf[Off + 3]) & uint32(0xFF)) << 24)
|
||
return Val
|
||
|
||
|
||
def ReadU64(Buf: bytes, Off: int) -> uint64:
|
||
"""Read a little-endian u64 as two u32 halves (avoids an 8-byte unaligned read)."""
|
||
Lo: uint64 = uint64(ReadU32(Buf, Off))
|
||
Hi: uint64 = uint64(ReadU32(Buf, Off + 4))
|
||
return Lo | (Hi << 32)
|
||
|
||
|
||
# ---- Validation-failure diagnostics (each prints and returns 0) ----
|
||
|
||
def MapFailPlain(Size: int64, Reason: str) -> int:
|
||
"""Validation failed before the header could be read: print reason + actual file size."""
|
||
if _State == None or _State.LineBuf == None:
|
||
return 0
|
||
N: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
||
"[traceback] WARN: <exe>.stackmap invalid -- %s (file %lld bytes), addresses only this run\n",
|
||
Reason, Size)
|
||
EmitLine(N)
|
||
return 0
|
||
|
||
|
||
def MapFailHdr(Size: int64, Reason: str, Head: bytes) -> int:
|
||
"""Validation failed with the header read: also print the actual header fields."""
|
||
if _State == None or _State.LineBuf == None:
|
||
return 0
|
||
N: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
||
"[traceback] 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: int64, Index: int, Reason: str, Got: uint64) -> int:
|
||
"""Record-table validation failed: print the offending index and value."""
|
||
if _State == None or _State.LineBuf == None:
|
||
return 0
|
||
N: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
||
"[traceback] 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) -> uint64:
|
||
"""RVA of record Index (record area starts at MapBase + 20).
|
||
|
||
The bare `_State` null-check below is REQUIRED: the attribute layer registers `_State` per
|
||
function before it can resolve this module-level `rawptr[TracebackState]`; without it
|
||
`_State.MapBase` is silently folded to null (see header section 11 A). It also guards
|
||
against an uninstalled state (returns 0).
|
||
"""
|
||
if _State == None:
|
||
return 0
|
||
Off: int = TRACEBACK_MAP_HEADER + Index * TRACEBACK_ENTRY_SIZE
|
||
return ReadU64(_State.MapBase, Off)
|
||
|
||
|
||
def LookupRva(Rva: uint64) -> int:
|
||
"""Look up Rva in the ascending table: return the largest index with rva <= Rva, else -1.
|
||
|
||
Granularity matches the writing side (TPC `_WriteStackMap`): the table stores DWARF
|
||
line-start addresses, and a stack-frame return address almost never lands exactly on one,
|
||
so a floor search is required; otherwise every frame degrades to +0x...(unknown).
|
||
Hit = some rva <= target (the largest such); miss = target below the smallest rva.
|
||
|
||
The bare `_State` null-check below is REQUIRED for the same reason as EntryRva: without it
|
||
`_State.EntryCount` folds to null (Hi = -1, the search never runs).
|
||
"""
|
||
if _State == None:
|
||
return -1
|
||
Lo: int = 0
|
||
Hi: int = int(_State.EntryCount) - 1
|
||
Best: int = -1
|
||
while Lo <= Hi:
|
||
Mid: int = Lo + (Hi - Lo) / 2
|
||
E: uint64 = EntryRva(int(Mid))
|
||
if E <= Rva:
|
||
Best = Mid
|
||
Lo = Mid + 1
|
||
else:
|
||
Hi = Mid - 1
|
||
return Best
|
||
|
||
|
||
def LoadMapFile(hFile: HANDLE, Size: int64) -> int:
|
||
"""Read and validate the sidecar (header / ascending records / string offsets). 1 = MapOk.
|
||
|
||
Any validation failure disables the map (returns 0): a broken table yields wrong line
|
||
numbers, worse than addresses only. This function prints exactly where it failed itself
|
||
(MapFailPlain/MapFailHdr/MapFailEntry, with the actual header values and file size).
|
||
|
||
Field-by-field checks, aligned with the writing side (TPC `_WriteStackMap`):
|
||
magic little-endian u32 == 0x53435054 ("TPCS")
|
||
version u32 == 1
|
||
entry_count part of `20+count*20+str_bytes <= file size`
|
||
str_bytes same (str_count is diagnostic only, not checked)
|
||
record rva strictly ascending
|
||
file_off < str_bytes and >= 1
|
||
func_off == 0 means no name; otherwise < str_bytes
|
||
"""
|
||
if Size < int64(TRACEBACK_MAP_HEADER):
|
||
return MapFailPlain(Size, "file smaller than the 20-byte header")
|
||
if Size > int64(TRACEBACK_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), &Got, None) == 0:
|
||
Err: ULONG = GetLastError()
|
||
stdlib.free(t.CPtr(Buf))
|
||
if _State != None and _State.LineBuf != None:
|
||
RN: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
||
"[traceback] WARN: read <exe>.stackmap failed (GetLastError=%u),"
|
||
" addresses only this run\n", Err)
|
||
EmitLine(RN)
|
||
return 0
|
||
if sizet(Got) != sizet(Size):
|
||
stdlib.free(t.CPtr(Buf))
|
||
return MapFailPlain(Size, "read fewer bytes than the file size")
|
||
# Header: magic / version / entry_count / str_count / str_bytes
|
||
if ReadU32(Buf, 0) != uint32(TRACEBACK_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) != uint32(TRACEBACK_MAP_VERSION):
|
||
stdlib.free(t.CPtr(Buf))
|
||
return MapFailHdr(Size, "version is not 1 (stackmap written by another TPC version)", Buf)
|
||
Cnt: uint32 = ReadU32(Buf, 8)
|
||
StrCnt: uint32 = ReadU32(Buf, 12)
|
||
SBytes: uint32 = ReadU32(Buf, 16)
|
||
# Records + string pool must fit inside the file (u64 math to avoid u32 wrap).
|
||
Need: uint64 = uint64(TRACEBACK_MAP_HEADER) \
|
||
+ uint64(Cnt) * uint64(TRACEBACK_ENTRY_SIZE) \
|
||
+ uint64(SBytes)
|
||
if Need > uint64(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)
|
||
# The last pool byte must be '\0' so any later %s print stays inside the pool.
|
||
PoolEnd: int = TRACEBACK_MAP_HEADER + int(Cnt) * TRACEBACK_ENTRY_SIZE + int(SBytes) - 1
|
||
if Buf[PoolEnd] != 0:
|
||
stdlib.free(t.CPtr(Buf))
|
||
return MapFailHdr(Size, "string pool does not end with '\\0' (pool corrupt)", Buf)
|
||
# Full-table check: strictly ascending rva + string offsets inside the pool.
|
||
Prev: uint64 = 0
|
||
for i in range(int(Cnt)):
|
||
Off: int = TRACEBACK_MAP_HEADER + int(i) * TRACEBACK_ENTRY_SIZE
|
||
Cur: uint64 = ReadU64(Buf, Off)
|
||
if i > 0 and Cur <= Prev:
|
||
stdlib.free(t.CPtr(Buf))
|
||
return MapFailEntry(Size, int(i),
|
||
"record rva not strictly ascending (breaks binary search)", Cur)
|
||
Prev = Cur
|
||
FO: uint32 = ReadU32(Buf, Off + TRACEBACK_ENTRY_FILEOFF)
|
||
FN: uint32 = ReadU32(Buf, Off + TRACEBACK_ENTRY_FUNCOFF)
|
||
if uint64(FO) >= uint64(SBytes):
|
||
stdlib.free(t.CPtr(Buf))
|
||
return MapFailEntry(Size, int(i), "file_off outside the string pool", uint64(FO))
|
||
if FN != 0 and uint64(FN) >= uint64(SBytes):
|
||
stdlib.free(t.CPtr(Buf))
|
||
return MapFailEntry(Size, int(i),
|
||
"func_off is neither 0 (no name) nor inside the string pool",
|
||
uint64(FN))
|
||
_State.MapBase = Buf
|
||
_State.EntryCount = Cnt
|
||
_State.StrBytes = SBytes
|
||
_State.StrBase = Buf + (TRACEBACK_MAP_HEADER + int(Cnt) * TRACEBACK_ENTRY_SIZE)
|
||
_State.MapOk = 1
|
||
return 1
|
||
|
||
|
||
# ============================================================
|
||
# Frame collection and single-frame printing (Python traceback style)
|
||
# ============================================================
|
||
|
||
def FramePush(Addr: uint64, Scanned: int) -> int:
|
||
"""Append one frame (address + inferred flag) to the dump buffer; 1 = stored, 0 = full.
|
||
|
||
The walk paths call this innermost-first (the order they discover frames); the caller prints
|
||
the buffer in reverse afterwards, giving the outermost-first Python traceback order. When the
|
||
buffer is full the new frame is dropped (no overflow). The bare `_State` null-check is
|
||
REQUIRED (it touches _State.FrameAddr).
|
||
"""
|
||
if _State == None:
|
||
return 0
|
||
if _State.FrameCount >= TRACEBACK_FRAME_BUF:
|
||
return 0
|
||
_State.FrameAddr[_State.FrameCount] = Addr
|
||
_State.FrameScan[_State.FrameCount] = int32(Scanned)
|
||
_State.FrameCount = _State.FrameCount + 1
|
||
return 1
|
||
|
||
|
||
def EmitSourceText(Path: str, LineNo: int):
|
||
"""Print the 4-space-indented text of line LineNo from Path (console + dump file).
|
||
|
||
Best effort: the file is read sequentially in fixed stack chunks (no heap allocation -- the
|
||
callback thread must not take the CRT heap lock the suspended main thread may hold). Leading
|
||
whitespace is skipped while collecting and trailing whitespace is trimmed afterwards, so the
|
||
printed text is the source line with its leading/trailing whitespace removed. Any failure --
|
||
the file cannot be opened, a read fails, or LineNo is past EOF -- prints nothing at all (no
|
||
placeholder line, no error).
|
||
"""
|
||
if _State == None or _State.LineBuf == None or LineNo <= 0:
|
||
return
|
||
h: HANDLE = wf.CreateFileA(Path, wf.GENERIC_READ, wf.FILE_SHARE_READ | wf.FILE_SHARE_WRITE,
|
||
t.CPtr(0), wf.OPEN_EXISTING, wf.FILE_ATTRIBUTE_NORMAL, t.CPtr(0))
|
||
if h == INVALID_HANDLE_VALUE:
|
||
return
|
||
Chunk: t.CArray[char, TRACEBACK_SRC_CHUNK]
|
||
Src: t.CArray[char, TRACEBACK_LINE_CAP]
|
||
Line: int = 1
|
||
Out: int = 0 # bytes collected for the target line
|
||
Done: int = 0
|
||
while Done == 0:
|
||
Got: ULONG = 0
|
||
if wf.ReadFile(h, &Chunk, ULONG(TRACEBACK_SRC_CHUNK), &Got, None) == 0:
|
||
CloseHandle(h)
|
||
return
|
||
if Got == 0:
|
||
break # EOF: the requested line does not exist
|
||
k: int = 0
|
||
while k < int(Got):
|
||
Ch: int = int(Chunk[int(k)]) & 0xFF
|
||
if Ch == 10: # '\n' ends the current line
|
||
if Line == LineNo:
|
||
Done = 1
|
||
break
|
||
Line += 1
|
||
k += 1
|
||
continue
|
||
if Line == LineNo and Ch != 13:
|
||
if Out > 0 or (Ch != 32 and Ch != 9):
|
||
if Out < TRACEBACK_LINE_CAP - 8:
|
||
Src[Out] = char(Ch)
|
||
Out += 1
|
||
k += 1
|
||
CloseHandle(h)
|
||
if Out == 0:
|
||
return # out of range or blank line: print nothing
|
||
while Out > 0:
|
||
E: int = int(Src[Out - 1]) & 0xFF
|
||
if E != 32 and E != 9:
|
||
break
|
||
Out -= 1
|
||
if Out == 0:
|
||
return
|
||
Src[Out] = char(0)
|
||
N: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP), " %s\n", Src)
|
||
EmitLine(N)
|
||
|
||
|
||
def EmitFrame(Addr: uint64, Base: uint64, Scanned: int):
|
||
"""Print one collected frame in Python traceback style.
|
||
|
||
Hit frame : ` File "<path>", line <N>, in <func>` and, when the source line can be re-read
|
||
at dump time, a second ` <source text>` line (see EmitSourceText).
|
||
Unmapped : ` File "<0xaddr +0xrva no source line mapping>", line 0, in <unknown>`.
|
||
Outside : ` File "<0xaddr external>", line 0, in <unknown>`.
|
||
Scanned (inferred, from ScanStack) frames carry a trailing " [inferred]": they may be false
|
||
positives and stay distinguishable from authoritative frames.
|
||
|
||
The bare `_State` null-check below is REQUIRED: this function uses `_State.LineBuf` (snprintf
|
||
target) and `_State.MapOk`, both of which would otherwise fold to null (see header section 11 A).
|
||
"""
|
||
if _State == None:
|
||
return
|
||
Tail: str = ""
|
||
if Scanned != 0:
|
||
Tail = " [inferred]"
|
||
N: int = 0
|
||
# Above the module is as "external" as below it. WalkRbpChain pushes the raw RIP
|
||
# unconditionally (the interrupted thread may be blocked inside ntdll/kernel32), and such a
|
||
# system address is far ABOVE Base without being below it — the old `Addr < Base` test let it
|
||
# through, and the floor search in LookupRva() then returned the LAST record, so a system
|
||
# frame printed as a plausible-looking `File "...", line N, in <func>` (measured: the bogus
|
||
# `line 13` frame beside a real-looking function name). TRACEBACK_MODULE_MAX is the same
|
||
# coarse upper bound WalkRbpChain / ScanStack already use for "inside the main module".
|
||
if Base == 0 or Addr < Base or Addr >= Base + uint64(TRACEBACK_MODULE_MAX):
|
||
# Outside the main image (DLL / system / corrupt frame): address only.
|
||
N = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
||
" File \"<0x%llx external>\", line 0, in <unknown>%s\n", Addr, Tail)
|
||
EmitLine(N)
|
||
return
|
||
Rva: uint64 = Addr - Base
|
||
if _State.MapOk == 0:
|
||
# No map (sidecar missing/corrupt): keep the address + RVA in the same File line.
|
||
N = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
||
" File \"<0x%llx +0x%llx no source line mapping>\", line 0, in <unknown>%s\n",
|
||
Addr, Rva, Tail)
|
||
EmitLine(N)
|
||
return
|
||
Hit: int = LookupRva(Rva)
|
||
if Hit < 0:
|
||
# Table loaded but not hit: report why (empty table / rva below the first entry), once.
|
||
if DiagEnabled() != 0:
|
||
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: uint64 = EntryRva(0)
|
||
Last: uint64 = EntryRva(int(_State.EntryCount) - 1)
|
||
M: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_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, sizet(TRACEBACK_LINE_CAP),
|
||
" File \"<0x%llx +0x%llx no source line mapping>\", line 0, in <unknown>%s\n",
|
||
Addr, Rva, Tail)
|
||
EmitLine(N)
|
||
return
|
||
Off: int = TRACEBACK_MAP_HEADER + Hit * TRACEBACK_ENTRY_SIZE
|
||
FileOff: uint32 = ReadU32(_State.MapBase, Off + TRACEBACK_ENTRY_FILEOFF)
|
||
Line: uint32 = ReadU32(_State.MapBase, Off + TRACEBACK_ENTRY_LINE)
|
||
FuncOff: uint32 = ReadU32(_State.MapBase, Off + TRACEBACK_ENTRY_FUNCOFF)
|
||
FileName: str = str(_State.StrBase + int(FileOff))
|
||
FuncName: str = "unknown"
|
||
if FuncOff != 0:
|
||
FuncName = str(_State.StrBase + int(FuncOff))
|
||
N = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
||
" File \"%s\", line %u, in %s%s\n", FileName, Line, FuncName, Tail)
|
||
EmitLine(N)
|
||
EmitSourceText(FileName, int(Line))
|
||
|
||
|
||
# ============================================================
|
||
# The two walk paths
|
||
# ============================================================
|
||
|
||
def ReadMemCb(hProcess: HANDLE, Addr: ULONGLONG, Buf: VOIDPTR, Size: DWORD,
|
||
pRead: t.CPtr) -> BOOL:
|
||
"""This module's stand-in for the StackWalk64 memory callback (PREAD_PROCESS_MEMORY_ROUTINE64).
|
||
|
||
Not kernel32!ReadProcessMemory directly:
|
||
(1) it returns only BOOL, and a callback returning 0 stops StackWalk64 immediately (the
|
||
"0 frames" symptom) while swallowing GetLastError; the first failure's address/size/
|
||
error is recorded here and printed next to the first-frame diagnostic (once);
|
||
(2) dbghelp's prototype is `DWORD nSize` + `LPDWORD`, while kernel32's ReadProcessMemory is
|
||
`SIZE_T nSize` + `SIZE_T*` (both 8 bytes on x64); handing dbghelp's 4-byte LPDWORD to
|
||
kernel32 would write through 4 bytes on success. A local `sizet` receives the count,
|
||
written back to dbghelp's buffer as DWORD.
|
||
Passing this function by name as a t.CPtr mirrors SetConsoleCtrlHandler(CtrlHandler).
|
||
"""
|
||
Read: sizet = 0
|
||
R: BOOL = wm.ReadProcessMemory(hProcess, t.CPtr(Addr), Buf, sizet(Size), &Read)
|
||
if pRead != None:
|
||
Rd: rawptr[uint32] = (rawptr[uint32])(pRead)
|
||
Rd[0] = uint32(Read)
|
||
if R == 0 and _State != None and _State.ReadMemFailed == 0:
|
||
# Record only the first failure (address/size/error); WalkViaDbgHelp prints it next to the
|
||
# first-frame return code so "callback returned 0 -> 0 frames" is visible at once.
|
||
_State.ReadMemFailed = 1
|
||
_State.ReadMemAddr = uint64(Addr)
|
||
_State.ReadMemSize = uint32(Size)
|
||
_State.ReadMemErr = GetLastError()
|
||
return R
|
||
|
||
|
||
def WalkViaDbgHelp(hProcess: HANDLE, hThread: HANDLE, CtxPtr: VOIDPTR,
|
||
Base: uint64) -> int:
|
||
"""dbghelp!StackWalk64 frame walk; returns the number of frames printed (0 = caller degrades).
|
||
|
||
Optional enhancement path: the primary path is WalkRbpChain (no dbghelp). Called only when
|
||
the RBP chain found no more frames and the caller judged dbghelp usable - that judgement comes
|
||
from the Install-time SymGetModuleBase64 probe (see header section 11 B); otherwise the caller
|
||
never enters. It is the only path that can cross a frameless system frame (RBP=0 in the Ctrl+C
|
||
context) via the PE unwind tables (.pdata) and reach the real .vp stack.
|
||
|
||
STACKFRAME64 fields are accessed through explicit sub-struct pointers (rawptr[dbg.ADDRESS64]):
|
||
the ADDRESS64 structures sit at offsets 0 / 16 / 32 / 48 (= AddrPC / AddrReturn / AddrFrame /
|
||
AddrStack, see includes/w32/win32dbghelp.vp). After zeroing, AddrPC/AddrFrame/AddrStack are
|
||
pre-filled with the context's Rip/Rbp/Rsp and Mode = AddrModeFlat(3); MachineType is passed on
|
||
every call. STACKFRAME64 has no size/field-count member (that is StackWalkEx's STACKFRAME_EX);
|
||
the repository's sizeof must simply match the C side (0xF8). First-frame evidence (return code,
|
||
returned AddrPC, SymFunctionTableAccess64 result) is produced unconditionally, and every return
|
||
prints a "[diag] walk: return reason=... frames=..." line. The whole body is under
|
||
`if TRACEBACK_USE_DBGHELP != 0`, so with the switch at 0 no dbghelp symbol is emitted.
|
||
"""
|
||
# A short diagnostic precedes every return so "was walk called, and which exit" is always
|
||
# visible. reason values:
|
||
# null_state global _State missing
|
||
# disabled compile-time switch TRACEBACK_USE_DBGHELP == 0
|
||
# null_frame_buf frame buffer missing (Install-time calloc failed)
|
||
# null_context CONTEXT pointer null (bad caller argument)
|
||
# stackwalk_first_failed first StackWalk64 returned 0 (cause in the first-frame lines)
|
||
# stackwalk_failed a later StackWalk64 returned 0 (normal top of stack)
|
||
# addrpc_zero StackWalk64 succeeded but AddrPC == 0
|
||
# frame_limit reached TRACEBACK_MAX_FRAMES
|
||
#
|
||
# The bare `_State` null-check below MUST be the first statement: the attribute layer
|
||
# registers `_State` per function before it can resolve its struct type; otherwise every
|
||
# `_State.<field>` folds to null (header section 11 A).
|
||
if _State == None:
|
||
if DiagEnabled() != 0:
|
||
EmitText(" [diag] walk: return reason=null_state frames=0\n")
|
||
return 0
|
||
if TRACEBACK_USE_DBGHELP == 0:
|
||
if DiagEnabled() != 0:
|
||
EmitText(" [diag] walk: return reason=disabled (TRACEBACK_USE_DBGHELP=0) frames=0\n")
|
||
return 0
|
||
# The frame buffer goes through a local, the most reliable channel, pinning the riskiest
|
||
# memset to an already-checked value.
|
||
FBuf: VOIDPTR = _State.FrameBuf
|
||
if FBuf == None:
|
||
if DiagEnabled() != 0:
|
||
EmitText(" [diag] walk: return reason=null_frame_buf frames=0\n")
|
||
return 0
|
||
if CtxPtr == None:
|
||
if DiagEnabled() != 0:
|
||
EmitText(" [diag] walk: return reason=null_context frames=0\n")
|
||
return 0
|
||
Frames: int = 0
|
||
Reason: str = "frame_limit"
|
||
if TRACEBACK_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])(FBuf + 32)
|
||
St: rawptr[dbg.ADDRESS64] = (rawptr[dbg.ADDRESS64])(FBuf + 48)
|
||
Pc.Offset = ctx.Rip
|
||
Pc.Mode = uint32(dbg.ADDR_MODE_FLAT)
|
||
Fm.Offset = ctx.Rbp
|
||
Fm.Mode = uint32(dbg.ADDR_MODE_FLAT)
|
||
St.Offset = ctx.Rsp
|
||
St.Mode = uint32(dbg.ADDR_MODE_FLAT)
|
||
i: int = 0
|
||
while i < TRACEBACK_MAX_FRAMES:
|
||
# Read before the call: StackWalk64 rewrites STACKFRAME64 in place.
|
||
InRip: uint64 = uint64(Pc.Offset)
|
||
InRsp: uint64 = uint64(St.Offset)
|
||
InRbp: uint64 = uint64(Fm.Offset)
|
||
Ok: BOOL = dbg.StackWalk64(uint32(dbg.IMAGE_FILE_MACHINE_AMD64), hProcess, hThread, t.CPtr(FBuf),
|
||
CtxPtr, ReadMemCb, _State.FuncTableAddr, _State.ModuleBaseAddr, None)
|
||
Cur: uint64 = uint64(Pc.Offset)
|
||
if i == 0 and DiagEnabled() != 0:
|
||
# First-frame evidence (before any failure test): return code + returned AddrPC +
|
||
# the SymFunctionTableAccess64(AddrPC) result; together they classify the "0
|
||
# frames" cause (modinited / read-memory failure / context).
|
||
Fta: uint64 = 0
|
||
if Cur != 0:
|
||
Fta = uint64(dbg.SymFunctionTableAccess64(hProcess, Cur))
|
||
D0: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
||
" [diag] StackWalk64 first: ret=%d addrpc=0x%llx fta=0x%llx\n", int(Ok), Cur, Fta)
|
||
EmitLine(D0)
|
||
D1: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
||
" [diag] StackWalk64 in: machine=0x%x rip=0x%llx rsp=0x%llx rbp=0x%llx\n",
|
||
uint32(dbg.IMAGE_FILE_MACHINE_AMD64), InRip, InRsp, InRbp)
|
||
EmitLine(D1)
|
||
if Ok == 0:
|
||
if i == 0:
|
||
Reason = "stackwalk_first_failed"
|
||
else:
|
||
Reason = "stackwalk_failed"
|
||
if DiagEnabled() != 0:
|
||
D2: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
||
" [diag] StackWalk64 returned 0 at frame %d (addrpc=0x%llx), stop\n", i, Cur)
|
||
EmitLine(D2)
|
||
if _State.ReadMemFailed != 0 and DiagEnabled() != 0:
|
||
# Right after the first-frame code, print the read-memory failure (one "0
|
||
# frames" cause: the callback returned 0 and StackWalk64 stopped).
|
||
DR: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_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"
|
||
if DiagEnabled() != 0:
|
||
D3: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
||
" [diag] StackWalk64 frame %d addrpc=0, stop\n", i)
|
||
EmitLine(D3)
|
||
break
|
||
FramePush(Cur, 0)
|
||
i += 1
|
||
Frames = i
|
||
# Unified "leave walk" diagnostic covering every break and the normal end; Reason is set.
|
||
if DiagEnabled() != 0:
|
||
N: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
||
" [diag] walk: return reason=%s frames=%d\n", Reason, Frames)
|
||
EmitLine(N)
|
||
return Frames
|
||
|
||
|
||
def WalkRbpChain(Rip: uint64, Rbp: uint64, Base: uint64) -> int:
|
||
"""Primary walk path: collect RIP, then follow the RBP chain reading [rbp] (caller rbp) and
|
||
[rbp+8] (return address) frame by frame.
|
||
|
||
While TRACEBACK_DIAG is enabled every iteration — including a rejected step — first emits two
|
||
ASCII `[diag] rbp:` lines:
|
||
line 1: `frame=<i> rbp=0x... [rbp]=0x... [rbp+8]=0x...` (the raw 8-byte values)
|
||
line 2: `frame=<i> rpm_ok=<0/1> rpm_err=<GetLastError> accept=<0/1> reject_reason=...`
|
||
This makes "why the chain broke at step 1" directly readable (read failure / value not a
|
||
return address / rejected). `reject_reason` is one of: rbp_zero / not_aligned / rpm_failed /
|
||
ret_zero / ret_out_of_module / not_growing / step_too_large; an accepted step prints accept=1.
|
||
Only an accepted step calls FramePush (authoritative frame); rejected values go to diagnostics
|
||
only, never into the stack.
|
||
|
||
* Zero dbghelp dependency (usable even where another component owns DbgHelp's process context).
|
||
* All memory reads go through ReadProcessMemory, never dereferencing a wild rbp: a diagnostic
|
||
must not turn a hang into a crash.
|
||
* A step is accepted only if rbp is non-zero, 8-byte aligned, the read succeeds, [rbp+8] is a
|
||
non-zero address inside the main module [Base, Base+TRACEBACK_MODULE_MAX), the caller rbp
|
||
grows strictly ([rbp] > rbp), the step size <= TRACEBACK_RBP_MAX_STEP, and the frame limit is
|
||
not reached. (Base == 0 skips the range check.)
|
||
|
||
The bare `_State` null-check below is REQUIRED: this function builds diagnostics into
|
||
`_State.LineBuf` (header section 11 A).
|
||
"""
|
||
if _State == None:
|
||
return 0
|
||
FramePush(Rip, 0)
|
||
Cur: uint64 = Rbp
|
||
i: int = 1
|
||
HProc: HANDLE = wp.GetCurrentProcess()
|
||
Two: t.CArray[uint64, 2] # [0] = [rbp] (caller rbp), [1] = [rbp+8] (return address)
|
||
while i < TRACEBACK_MAX_FRAMES:
|
||
# Raw readings + decision for this step, produced before any early reject.
|
||
Rd: sizet = 0
|
||
RpmOk: int = 0
|
||
RpmErr: ULONG = 0
|
||
V0: uint64 = 0 # [rbp] caller rbp
|
||
V1: uint64 = 0 # [rbp+8] return address
|
||
Reason: str = "accept"
|
||
Reject: int = 0 # 0 = accept this step, 1 = reject (reason in Reason)
|
||
if Cur == 0:
|
||
Reason = "rbp_zero"
|
||
Reject = 1
|
||
elif (Cur & uint64(7)) != 0:
|
||
Reason = "not_aligned"
|
||
Reject = 1
|
||
else:
|
||
# Read 16 bytes via ReadProcessMemory (invalid/unreadable addresses return 0 without
|
||
# faulting); dereferencing a wild rbp directly would crash, which the primary path
|
||
# must never do.
|
||
OkRead: BOOL = wm.ReadProcessMemory(HProc, t.CPtr(Cur), &Two, sizet(16), &Rd)
|
||
if OkRead == 0 or sizet(Rd) < sizet(16):
|
||
# GetLastError must be read immediately after ReadProcessMemory.
|
||
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 + uint64(TRACEBACK_MODULE_MAX):
|
||
# Not a return address (below the base / outside the image) => this rbp is
|
||
# not a frame pointer, so the chain breaks here.
|
||
Reason = "ret_out_of_module"
|
||
Reject = 1
|
||
if Reject == 0:
|
||
if V0 <= Cur:
|
||
Reason = "not_growing"
|
||
Reject = 1
|
||
elif V0 - Cur > uint64(TRACEBACK_RBP_MAX_STEP):
|
||
Reason = "step_too_large"
|
||
Reject = 1
|
||
if DiagEnabled() != 0:
|
||
D0: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
||
" [diag] rbp: frame=%d rbp=0x%llx [rbp]=0x%llx [rbp+8]=0x%llx\n", i, Cur, V0, V1)
|
||
EmitLine(D0)
|
||
Acc: int = 1
|
||
if Reject != 0:
|
||
Acc = 0
|
||
if DiagEnabled() != 0:
|
||
D1: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_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
|
||
# Accepted: collect the authoritative frame, then keep walking up.
|
||
FramePush(V1, 0)
|
||
i += 1
|
||
Cur = V0
|
||
return i
|
||
|
||
|
||
def ScanStack(StartIndex: int, Rsp: uint64, Base: uint64) -> int:
|
||
"""Second fallback: stack scan (after the RBP chain; no RBP / frame-pointer dependency).
|
||
|
||
Needed when the build kept no frame pointer, so [rbp+8] is not a return address and the RBP
|
||
chain breaks at step 1; then the only way on is scanning the used stack directly.
|
||
|
||
Read-only, never dereferencing main-thread memory:
|
||
1. walk [Rsp, Rsp+TRACEBACK_SCAN_MAX_BYTES) in 8-byte steps towards higher addresses (caller
|
||
direction), so candidates come out in ascending stack order;
|
||
2. one ReadProcessMemory per chunk (TRACEBACK_SCAN_CHUNK_BYTES = 4 KiB, a local array, the
|
||
callback thread allocates nothing) instead of one per 8 bytes; a failed chunk stops the
|
||
scan and prints address/GetLastError (only with TRACEBACK_DIAG on);
|
||
3. a candidate word W lies in [Base, Base+TRACEBACK_MODULE_MAX) and a neighbouring 8-byte word
|
||
([A-8], the saved rbp when frame pointers are kept, or [A+8]) looks like a stack address
|
||
in [Rsp, upper); either is accepted, covering both frame layouts;
|
||
4. deduplicate by candidate value, capped at TRACEBACK_SCAN_MAX_HITS (scan_candidates=<N>);
|
||
5. collect via FramePush(..., Scanned=1); EmitFrame later marks them with " [inferred]".
|
||
Returns the number of inferred frames collected (0 = none).
|
||
|
||
The bare `_State` null-check below is REQUIRED (this function uses _State.LineBuf).
|
||
"""
|
||
if _State == None:
|
||
return 0
|
||
if Rsp == 0 or Base == 0:
|
||
# No RSP or no module base: skip to avoid a range of false positives; give a greppable
|
||
# reason when diagnostics are on.
|
||
if DiagEnabled() != 0:
|
||
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: uint64 = Rsp + uint64(TRACEBACK_SCAN_MAX_BYTES)
|
||
ModEnd: uint64 = Base + uint64(TRACEBACK_MODULE_MAX)
|
||
Words: t.CArray[uint64, TRACEBACK_SCAN_CHUNK_WORDS] # 4 KiB chunk buffer (local)
|
||
Found: t.CArray[uint64, TRACEBACK_SCAN_MAX_HITS] # deduplicated candidate return addresses
|
||
Hits: int = 0
|
||
Prev: uint64 = 0 # [A-8]: previous word unknown at chunk start, 0 (one less confirmation)
|
||
Addr: uint64 = Rsp
|
||
Stop: str = "reached_scan_limit"
|
||
if DiagEnabled() != 0:
|
||
D0: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
||
" [diag] scan: rsp=0x%llx upper=0x%llx chunk=%d hits_max=%d\n",
|
||
Rsp, Upper, TRACEBACK_SCAN_CHUNK_BYTES, TRACEBACK_SCAN_MAX_HITS)
|
||
EmitLine(D0)
|
||
while Addr < Upper:
|
||
Rd: sizet = 0
|
||
OkRead: BOOL = wm.ReadProcessMemory(HProc, t.CPtr(Addr), &Words, sizet(TRACEBACK_SCAN_CHUNK_BYTES), &Rd)
|
||
N: int = int(sizet(Rd) / sizet(8)) # u64 words actually read in this chunk
|
||
if OkRead == 0 or N <= 0:
|
||
Er: ULONG = GetLastError()
|
||
if DiagEnabled() != 0:
|
||
D1: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
||
" [diag] scan: block read failed addr=0x%llx err=%u, stop\n", Addr, Er)
|
||
EmitLine(D1)
|
||
Stop = "block_read_failed"
|
||
break
|
||
for k in range(N):
|
||
W: uint64 = Words[int(k)]
|
||
if W >= Base and W < ModEnd:
|
||
# A neighbouring 8-byte word (A-8 or A+8) looks like a stack address: accept W.
|
||
NearStack: int = 0
|
||
if Prev >= Rsp and Prev < Upper:
|
||
NearStack = 1
|
||
if k + 1 < N:
|
||
Q: uint64 = Words[int(k + 1)]
|
||
if Q >= Rsp and Q < Upper:
|
||
NearStack = 1
|
||
if NearStack != 0 and Hits < TRACEBACK_SCAN_MAX_HITS:
|
||
# Deduplicate by candidate value (discovered in ascending stack order, so the
|
||
# order is kept).
|
||
Dup: int = 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
|
||
Addr = Addr + uint64(TRACEBACK_SCAN_CHUNK_BYTES)
|
||
if DiagEnabled() != 0:
|
||
D2: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
||
" [diag] scan: scan_candidates=%d stop=%s\n", Hits, Stop)
|
||
EmitLine(D2)
|
||
# Collect the inferred frames, capped by the global frame limit (authoritative frames already
|
||
# used StartIndex slots).
|
||
Lim: int = TRACEBACK_MAX_FRAMES - StartIndex
|
||
j: int = 0
|
||
while j < Hits:
|
||
if j >= Lim:
|
||
break
|
||
FramePush(Found[j], 1)
|
||
j += 1
|
||
return j
|
||
|
||
|
||
# ============================================================
|
||
# Shared walk/print helpers (Ctrl+C dump + crash dump)
|
||
# ============================================================
|
||
|
||
def DbgHelpUsable() -> int:
|
||
"""1 = StackWalk64 may be used right now, 0 = it may not (single source of truth for both dumps).
|
||
|
||
True only when the compile-time switch is on, Install's SymGetModuleBase64 probe passed
|
||
(SymReady), and both cross-module callbacks plus the STACKFRAME64 buffer are in place. The
|
||
reason for a 0 is printed by DumpMainThread's symwalk lines, never inferred from here.
|
||
|
||
The bare `_State` null-check below is REQUIRED (this function reads _State fields).
|
||
"""
|
||
if _State == None:
|
||
return 0
|
||
if TRACEBACK_USE_DBGHELP == 0:
|
||
return 0
|
||
if _State.SymReady == 0:
|
||
return 0
|
||
if _State.FuncTableAddr == None or _State.ModuleBaseAddr == None or _State.FrameBuf == None:
|
||
return 0
|
||
return 1
|
||
|
||
|
||
def PrintFrames(Base: uint64):
|
||
"""Print the buffered frames outermost-first (Python "most recent call last") + the frame count.
|
||
|
||
Both walk paths collect frames innermost-first into _State.FrameAddr, so the reverse loop prints
|
||
the outermost/CRT frame first and the interrupted RIP last. The count is read from
|
||
_State.FrameCount (frames actually buffered), never from a caller-side tally.
|
||
|
||
The bare `_State` null-check below is REQUIRED (this function uses _State.LineBuf).
|
||
"""
|
||
if _State == None or _State.LineBuf == None:
|
||
return
|
||
EmitText("Traceback (most recent call last):\n")
|
||
j: int = _State.FrameCount - 1
|
||
while j >= 0:
|
||
EmitFrame(_State.FrameAddr[j], Base, int(_State.FrameScan[j]))
|
||
j -= 1
|
||
N: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
||
" frames=%d\n", _State.FrameCount)
|
||
EmitLine(N)
|
||
|
||
|
||
# ============================================================
|
||
# Dump the main thread
|
||
# ============================================================
|
||
|
||
def WriteDumpHeaderFile(IsBreak: int):
|
||
"""Append a separator header to the dump file (file only, not the console).
|
||
|
||
The file accumulates many dumps, so each section gets a small greppable header: local
|
||
timestamp / pid / trigger key / main thread id / Seq. The labels are Chinese on purpose (the
|
||
file is UTF-8, unaffected by the console code page, unlike the always-ASCII console text).
|
||
"""
|
||
if _State == None or _DumpFile == None or _DumpFile == INVALID_HANDLE_VALUE:
|
||
return
|
||
St: SYSTEMTIME = SYSTEMTIME()
|
||
GetLocalTime(&St)
|
||
Trig: str = "Ctrl+C"
|
||
if IsBreak != 0:
|
||
Trig = "Ctrl+Break"
|
||
N: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
||
"\n===== 栈转储 %04u-%02u-%02u %02u:%02u:%02u.%03u pid=%u 触发=%s 主线程id=%u Seq=%d =====\n",
|
||
uint32(St.wYear), uint32(St.wMonth), uint32(St.wDay),
|
||
uint32(St.wHour), uint32(St.wMinute), uint32(St.wSecond),
|
||
uint32(St.wMilliseconds), wp.GetCurrentProcessId(),
|
||
Trig, _State.MainThreadId, _State.Seq)
|
||
if N <= 0:
|
||
return
|
||
if N > TRACEBACK_LINE_CAP - 1:
|
||
N = TRACEBACK_LINE_CAP - 1
|
||
Wr: ULONG = 0
|
||
wf.WriteFile(_DumpFile, _State.LineBuf, ULONG(N), &Wr, None)
|
||
|
||
|
||
def DumpMainThread(IsBreak: int) -> int:
|
||
"""Suspend the main thread -> read CONTEXT -> collect frames -> print -> resume. Returns frames.
|
||
|
||
Invariants:
|
||
* after a successful SuspendThread, every return path must ResumeThread + CloseHandle, or the
|
||
main thread freezes forever (worse than a hang);
|
||
* reentrancy: AtomicTryEnterDump (inline `lock cmpxchg`) test-and-sets _DumpLock; every
|
||
return path calls AtomicLeaveDump; a caller that fails to acquire returns immediately
|
||
printing nothing (it does not suspend the main thread or touch LineBuf);
|
||
* evidence: the "[diag] ..." lines are printed only while TRACEBACK_DIAG is enabled; when it
|
||
is off the dump is a plain Python-style traceback, which is why the diagnostic lines must
|
||
never be interleaved between the frames;
|
||
* frame order: the walk paths collect frames innermost-first into _State.FrameAddr; this
|
||
function prints that buffer in reverse (outermost/CRT first, innermost last).
|
||
"""
|
||
global _DumpOut
|
||
if _State == None or _State.LineBuf == None:
|
||
return 0
|
||
# Reentrancy guard: the SetConsoleCtrlHandler callbacks run on new threads and share the same
|
||
# LineBuf/out handle, so an atomic test-and-set avoids interleaved output from a race.
|
||
EnterOld: int = AtomicTryEnterDump()
|
||
if EnterOld != 0:
|
||
return 0
|
||
_State.Dumping = 1
|
||
_State.Seq = _State.Seq + 1
|
||
# The output handle is fixed and locked at dump start; only _DumpOut is written afterwards.
|
||
_DumpOut = _State.OutHandle
|
||
# Reset the per-dump flags: anti-spam flags plus the collected-frame count.
|
||
_State.ReadMemFailed = 0
|
||
_State.MissReported = 0
|
||
_State.FrameCount = 0
|
||
EmitText("=== TPC stack dump (Ctrl+Break: keep running / Ctrl+C: exit) ===\n")
|
||
# File-side separator header (file only: timestamp / pid / trigger / main thread id).
|
||
WriteDumpHeaderFile(int(IsBreak))
|
||
if _DumpOut == None or _DumpOut == INVALID_HANDLE_VALUE:
|
||
EmitText(" WARN: no usable console handle (stderr/stdout invalid), console output discarded\n")
|
||
# ---- Pre-diagnostics (evidence before decisions), obtained without suspending ----
|
||
# Emitted only while TRACEBACK_DIAG is enabled; all fields are ASCII.
|
||
MBase: uint64 = uint64(GetModuleHandleA(None))
|
||
ModBaseRet: uint64 = 0
|
||
if TRACEBACK_USE_DBGHELP != 0:
|
||
if _State.SymReady != 0:
|
||
ModBaseRet = uint64(dbg.SymGetModuleBase64(wp.GetCurrentProcess(), MBase))
|
||
FB: int = 1 if _State.FrameBuf != None else 0
|
||
FT: int = 1 if _State.FuncTableAddr != None else 0
|
||
MB: int = 1 if _State.ModuleBaseAddr != None else 0
|
||
N: int = 0
|
||
if DiagEnabled() != 0:
|
||
N = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_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, sizet(TRACEBACK_LINE_CAP),
|
||
" [diag] modbase=0x%llx SymGetModuleBase64(main)=0x%llx\n", MBase, ModBaseRet)
|
||
EmitLine(N)
|
||
N = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_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, sizet(TRACEBACK_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, sizet(TRACEBACK_LINE_CAP),
|
||
" fallback: OpenThread(main) failed (GetLastError=%u), cannot dump stack\n",
|
||
GetLastError())
|
||
EmitLine(N)
|
||
AtomicLeaveDump()
|
||
return 0
|
||
if wp.SuspendThread(hThread) == ULONG(TRACEBACK_SUSPEND_FAILED):
|
||
N = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
||
" fallback: SuspendThread(main) failed (GetLastError=%u), cannot dump stack\n",
|
||
GetLastError())
|
||
EmitLine(N)
|
||
CloseHandle(hThread)
|
||
AtomicLeaveDump()
|
||
return 0
|
||
# CONTEXT: zero it first, then set ContextFlags (x64 needs the CONTEXT_AMD64 bit).
|
||
string.memset(_State.ContextBuf, 0, wp.CONTEXT.__sizeof__())
|
||
ctx: rawptr[wp.CONTEXT] = (rawptr[wp.CONTEXT])(_State.ContextBuf)
|
||
ctx.ContextFlags = uint32(wp.CONTEXT_FULL)
|
||
if wp.GetThreadContext(hThread, t.CPtr(_State.ContextBuf)) == 0:
|
||
N = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
||
" fallback: GetThreadContext failed (GetLastError=%u), cannot dump stack\n",
|
||
GetLastError())
|
||
EmitLine(N)
|
||
wp.ResumeThread(hThread)
|
||
CloseHandle(hThread)
|
||
AtomicLeaveDump()
|
||
return 0
|
||
Base: uint64 = uint64(GetModuleHandleA(None))
|
||
Rip: uint64 = uint64(ctx.Rip)
|
||
Rsp: uint64 = uint64(ctx.Rsp)
|
||
Rbp: uint64 = uint64(ctx.Rbp)
|
||
# ---- dbghelp usability (decided at Install from evidence, never assumed here) ----
|
||
# An ASan runtime registers its own DbgHelp symbolizer, but DbgHelp's process context is
|
||
# process-global and shared, so its presence is informational only: Install refreshes the
|
||
# shared module table and keeps SymReady=1 ONLY when the SymGetModuleBase64(main module) probe
|
||
# succeeded. This walk matters because the RBP chain cannot cross a frameless system frame -
|
||
# the Ctrl+C case: the main thread is blocked inside a system DLL with RBP=0, so [rbp] is not
|
||
# a frame pointer and the chain dies at step 1. StackWalk64 unwinds through the PE unwind
|
||
# tables (.pdata) of both the system DLLs and the .vp functions, which is the only path that
|
||
# can reconstruct the real .vp stack from such a context.
|
||
AsanPresent: int = 0
|
||
if GetModuleHandleA("clang_rt.asan_dynamic-x86_64.dll") != None:
|
||
AsanPresent = 1
|
||
if GetModuleHandleA("libclang_rt.asan_dynamic-x86_64.dll") != None:
|
||
AsanPresent = 1
|
||
DbgUsable: int = DbgHelpUsable()
|
||
if DiagEnabled() != 0:
|
||
N = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
||
" [diag] symwalk: use_dbghelp=%d symready=%d asan=%d dbg_usable=%d\n",
|
||
TRACEBACK_USE_DBGHELP, _State.SymReady, AsanPresent, DbgUsable)
|
||
EmitLine(N)
|
||
if DbgUsable == 0:
|
||
# Each degradation gives an explicit reason (never silent 0 frames), most specific first.
|
||
if TRACEBACK_USE_DBGHELP == 0:
|
||
EmitText(" [diag] symwalk=disabled reason=compile_time_switch"
|
||
" (TRACEBACK_USE_DBGHELP=0)\n")
|
||
elif _State.SymReady == 0:
|
||
EmitText(" [diag] symwalk=disabled reason=syminitialize_failed_or_main_module_missing\n")
|
||
elif _State.FuncTableAddr == None or _State.ModuleBaseAddr == None:
|
||
EmitText(" [diag] symwalk=disabled reason=callback_missing\n")
|
||
else:
|
||
EmitText(" [diag] symwalk=disabled reason=null_frame_buf\n")
|
||
MapTag: str = "none (see [traceback] warnings above; addresses only)"
|
||
if _State.MapOk != 0:
|
||
MapTag = "loaded"
|
||
# Primary path is the RBP chain (no dbghelp); dbghelp is optional enhancement only.
|
||
SymTag: str = "rbp-chain primary (dbghelp not required)"
|
||
if DbgUsable != 0:
|
||
SymTag = "rbp-chain primary, dbghelp optional"
|
||
N = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
||
" exe base=0x%llx RIP=0x%llx RSP=0x%llx RBP=0x%llx\n",
|
||
Base, Rip, Rsp, Rbp)
|
||
EmitLine(N)
|
||
if DiagEnabled() != 0:
|
||
N = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
||
" srcline_map=%s symwalk=%s\n", MapTag, SymTag)
|
||
EmitLine(N)
|
||
Frames: int = 0
|
||
# "enter walk" evidence (only with diagnostics on); if it is missing after a key press while
|
||
# TRACEBACK_DIAG=1, DumpMainThread returned earlier at one of the three fallbacks above.
|
||
if DiagEnabled() != 0:
|
||
N = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
||
" [diag] walk: enter use_dbghelp=%d symready=%d modinited=%d "
|
||
"frame_buf=%d mapok=%d asan=%d\n",
|
||
TRACEBACK_USE_DBGHELP, _State.SymReady, _State.ModInited, FB, _State.MapOk,
|
||
AsanPresent)
|
||
EmitLine(N)
|
||
if DbgUsable == 0:
|
||
# Keep a "[diag] StackWalk64 first:" line before the RBP chain (here ret=not-called, with
|
||
# the prerequisites); a real call would emit it from inside WalkViaDbgHelp.
|
||
DNC: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_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)
|
||
EmitText(" [diag] walk: primary=rbp_chain\n")
|
||
# ---- Primary path: RBP chain (no dbghelp) ----
|
||
Frames = WalkRbpChain(Rip, Rbp, Base)
|
||
# RBP-chain yield ALONE decides the fallback gates below. The stack scan must not mask the
|
||
# (far stronger) dbghelp unwinder by inflating this count first: measured on
|
||
# Test/LLvmLiteTest, RBP was 0 (the main thread was blocked inside a system DLL), the chain
|
||
# produced the single RIP frame, the scan added 1 inferred candidate -> Frames became 2 ->
|
||
# the `Frames <= 1` dbghelp gate below silently skipped StackWalk64, which is the only path
|
||
# able to unwind through the frameless ntdll/kernel32 frames and print the real .vp stack.
|
||
RbpFrames: int = Frames
|
||
if DiagEnabled() != 0:
|
||
N = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
||
" [diag] walk: rbp_chain frames=%d\n", Frames)
|
||
EmitLine(N)
|
||
# ---- Second fallback: stack scan (no RBP / frame pointer) ----
|
||
# Enabled only when the RBP chain got just the current RIP (<=1 frame); scanned frames are
|
||
# marked " [inferred]" and are not mixed with authoritative frames.
|
||
if Frames <= 1:
|
||
if DiagEnabled() != 0:
|
||
EmitText(" [diag] walk: fallback=stack_scan\n")
|
||
Sc: int = ScanStack(Frames, Rsp, Base)
|
||
Frames = Frames + Sc
|
||
if DiagEnabled() != 0:
|
||
N = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
||
" [diag] walk: scan frames=%d total=%d\n", Sc, Frames)
|
||
EmitLine(N)
|
||
# ---- Optional enhancement: only when the RBP chain got <=1 frame and dbghelp is usable ----
|
||
# The outer constant condition is a separate if so the switch folds this block away entirely,
|
||
# leaving no dbghelp link-time symbol.
|
||
if TRACEBACK_USE_DBGHELP != 0:
|
||
if RbpFrames <= 1 and DbgUsable != 0:
|
||
if DiagEnabled() != 0:
|
||
EmitText(" [diag] walk: dbghelp enhancement engaged\n")
|
||
# dbghelp re-walks from the same CONTEXT and its first frame is the same RIP already
|
||
# collected: save ALL frames collected so far (the RIP plus any stack-scan candidates,
|
||
# each already carrying its own origin flag), let dbghelp rebuild the authoritative
|
||
# list from scratch, and restore the saved frames only if dbghelp produced nothing.
|
||
# Why all of them (and not just frame[0]): this block is gated on the RBP-chain yield
|
||
# (see RbpFrames), so it now runs exactly in the cases where the stack scan had
|
||
# contributed candidates — clearing them and restoring only frame[0] would silently
|
||
# shrink the dump whenever dbghelp fails. Locals only: the handler thread allocates
|
||
# nothing (same discipline as ScanStack's chunk buffers).
|
||
SavedCount: int = _State.FrameCount
|
||
SaveAddrBuf: t.CArray[uint64, TRACEBACK_FRAME_BUF]
|
||
SaveScanBuf: t.CArray[int32, TRACEBACK_FRAME_BUF]
|
||
for si in range(SavedCount):
|
||
SaveAddrBuf[si] = _State.FrameAddr[si]
|
||
SaveScanBuf[si] = _State.FrameScan[si]
|
||
_State.FrameCount = 0
|
||
F2: int = WalkViaDbgHelp(wp.GetCurrentProcess(), hThread, _State.ContextBuf, Base)
|
||
if F2 == 0:
|
||
for ri in range(SavedCount):
|
||
if FramePush(SaveAddrBuf[ri], int(SaveScanBuf[ri])) == 0:
|
||
break
|
||
# The count that is actually buffered is authoritative.
|
||
Frames = _State.FrameCount
|
||
if DiagEnabled() != 0:
|
||
# "leave walk" evidence, paired with walk: enter.
|
||
N = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
||
" [diag] walk: leave frames=%d mapok=%d\n", Frames, _State.MapOk)
|
||
EmitLine(N)
|
||
if Frames == 0:
|
||
# Neither the RBP chain, the stack scan, nor any dbghelp enhancement produced a frame.
|
||
EmitText(" [diag] walk: no frames from any path (rbp_chain/stack_scan/dbghelp)\n")
|
||
# ---- Print the collected frames outermost-first (Python traceback order) ----
|
||
PrintFrames(Base)
|
||
# Summary after the whole traceback: dump sequence + dump-file notice.
|
||
if _DumpFile != None and _DumpFile != INVALID_HANDLE_VALUE:
|
||
N = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
||
" [traceback] dump seq=%d also appended to <exe>.stackdump.txt\n", _State.Seq)
|
||
else:
|
||
N = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
||
" [traceback] dump seq=%d console only (dump file unavailable)\n", _State.Seq)
|
||
EmitLine(N)
|
||
EmitText("=== stack dump end ===\n")
|
||
wp.ResumeThread(hThread)
|
||
CloseHandle(hThread)
|
||
AtomicLeaveDump()
|
||
return Frames
|
||
|
||
|
||
# ============================================================
|
||
# Unhandled-exception dump (crash)
|
||
# ============================================================
|
||
|
||
def DumpCrashThread(Info: VOIDPTR) -> int:
|
||
"""SetUnhandledExceptionFilter body: dump the FAULTING thread's stack, then let the process die.
|
||
|
||
A crash otherwise leaves no stack at all: once the ASan runtime is past its reporting window it
|
||
prints nothing, and the OS default is a bare exit code (measured on the same fault: ASan on =
|
||
0x80000003 STATUS_BREAKPOINT, ASan off = 0xC0000005 ACCESS_VIOLATION, both stackless). This path
|
||
reuses the Ctrl+C dump's walk/print machinery on the CONTEXT the OS hands over in
|
||
EXCEPTION_POINTERS.
|
||
|
||
Invariants:
|
||
* runs on the FAULTING thread, so the thread handle is GetCurrentThread() and no
|
||
suspend/resume is needed;
|
||
* allocates nothing: every buffer already exists (same discipline as the console callback);
|
||
* never dereferences the OS pointers directly - EXCEPTION_POINTERS / EXCEPTION_RECORD are read
|
||
through ReadProcessMemory (the callback thread must not turn a crash into a second crash);
|
||
* the OS-owned CONTEXT is **copied** into _State.ContextBuf before walking, because
|
||
StackWalk64 rewrites the record it walks - handing it the exception's own record would
|
||
corrupt the reported crash context;
|
||
* shares AtomicTryEnterDump with the Ctrl+C dump, so only one dump can ever be in flight (a
|
||
crash that happens inside a dump is skipped instead of corrupting the output).
|
||
|
||
Returns 1 = a dump was produced, 0 = nothing dumped (state missing, another dump in flight, or
|
||
the OS records unreadable). The caller returns EXCEPTION_CONTINUE_SEARCH either way, so the
|
||
crash keeps its own exit code and ASan/WER reporting.
|
||
|
||
The bare `_State` null-check below is REQUIRED (this function uses _State.LineBuf).
|
||
"""
|
||
global _DumpOut
|
||
if _State == None or _State.LineBuf == None or _State.Installed == 0:
|
||
return 0
|
||
if AtomicTryEnterDump() != 0:
|
||
return 0
|
||
_State.Dumping = 1
|
||
_State.Seq = _State.Seq + 1
|
||
_DumpOut = _State.OutHandle
|
||
_State.ReadMemFailed = 0
|
||
_State.MissReported = 0
|
||
_State.FrameCount = 0
|
||
EmitText("=== TPC crash dump (unhandled exception; the process keeps terminating) ===\n")
|
||
HProc: HANDLE = wp.GetCurrentProcess()
|
||
Rd: sizet = 0
|
||
Two: t.CArray[uint64, 2] # [0] = EXCEPTION_RECORD*, [1] = CONTEXT* (EXCEPTION_POINTERS)
|
||
Two[0] = 0
|
||
Two[1] = 0
|
||
if wm.ReadProcessMemory(HProc, t.CPtr(Info), &Two, sizet(16), &Rd) == 0 \
|
||
or sizet(Rd) < sizet(16):
|
||
EmitText(" fallback: EXCEPTION_POINTERS unreadable, crash dump skipped\n")
|
||
AtomicLeaveDump()
|
||
return 0
|
||
Rec: uint64 = Two[0]
|
||
CtxPtr: uint64 = Two[1]
|
||
Code: uint32 = 0
|
||
ExAddr: uint64 = 0
|
||
AvKind: uint32 = 0
|
||
AvAddr: uint64 = 0
|
||
# EXCEPTION_RECORD head (0x30 bytes):
|
||
# u32[0] ExceptionCode, u32[1] ExceptionFlags, u32[2..3] nested record,
|
||
# u32[4..5] ExceptionAddress (offset 0x10), u32[6] NumberParameters (offset 0x18),
|
||
# u32[8] ExceptionInformation[0] (offset 0x20 = 0 read / 1 write / 8 execute),
|
||
# u32[10..11] ExceptionInformation[1] (offset 0x28 = the faulting DATA address).
|
||
# The two ExceptionInformation fields are only filled for access violations (code 0xC0000005);
|
||
# they are printed unconditionally because a hard crash is exactly when raw numbers beat guesses.
|
||
RecWords: t.CArray[uint32, 12]
|
||
RecWords[0] = 0
|
||
if Rec != 0:
|
||
if wm.ReadProcessMemory(HProc, t.CPtr(Rec), &RecWords, sizet(48), &Rd) != 0 \
|
||
and sizet(Rd) >= sizet(48):
|
||
Code = RecWords[0]
|
||
ExAddr = uint64(RecWords[4]) | (uint64(RecWords[5]) << 32)
|
||
AvKind = RecWords[8]
|
||
AvAddr = uint64(RecWords[10]) | (uint64(RecWords[11]) << 32)
|
||
N: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
||
" Trigger=unhandled-exception code=0x%x addr=0x%llx"
|
||
" av_kind=%u av_addr=0x%llx crashthread=%u\n",
|
||
Code, ExAddr, AvKind, AvAddr, wp.GetCurrentThreadId())
|
||
EmitLine(N)
|
||
if CtxPtr == 0:
|
||
EmitText(" fallback: CONTEXT pointer null, crash dump skipped\n")
|
||
AtomicLeaveDump()
|
||
return 0
|
||
if wm.ReadProcessMemory(HProc, t.CPtr(CtxPtr), t.CPtr(_State.ContextBuf),
|
||
wp.CONTEXT.__sizeof__(), &Rd) == 0 \
|
||
or sizet(Rd) < sizet(wp.CONTEXT.__sizeof__()):
|
||
EmitText(" fallback: CONTEXT unreadable, crash dump skipped\n")
|
||
AtomicLeaveDump()
|
||
return 0
|
||
Base: uint64 = uint64(GetModuleHandleA(None))
|
||
ctx: rawptr[wp.CONTEXT] = (rawptr[wp.CONTEXT])(_State.ContextBuf)
|
||
Rip: uint64 = uint64(ctx.Rip)
|
||
Rsp: uint64 = uint64(ctx.Rsp)
|
||
Rbp: uint64 = uint64(ctx.Rbp)
|
||
N = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
||
" exe base=0x%llx RIP=0x%llx RSP=0x%llx RBP=0x%llx\n",
|
||
Base, Rip, Rsp, Rbp)
|
||
EmitLine(N)
|
||
# Fault-site argument registers. On Win64 the first four integer arguments travel in
|
||
# rcx/rdx/r8/r9, so this line shows exactly what the faulting call received (r12 is the
|
||
# register the faulting instruction loaded its pointer from, when the fault is a pointer
|
||
# dereference). Printed for crashes only: the Ctrl+C path has no faulting instruction whose
|
||
# operands matter, and this is the one datum that distinguishes "wrong argument" from
|
||
# "argument right, pointee contents wrong" without another rebuild.
|
||
N = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
||
" regs: rcx=0x%llx rdx=0x%llx r8=0x%llx r9=0x%llx r12=0x%llx\n",
|
||
uint64(ctx.Rcx), uint64(ctx.Rdx), uint64(ctx.R8), uint64(ctx.R9),
|
||
uint64(ctx.R12))
|
||
EmitLine(N)
|
||
# NOTE: [RBP+8] is deliberately NOT read as a "return address". In .vp frames the compiler emits
|
||
# `leaq N(%rsp), %rbp` (RBP = a locals base, N = the spill area size), so the incoming return
|
||
# address sits at RBP + callee_frame_bytes - 8 - a per-function offset (measured on
|
||
# _TypePrintDepth: RBP = rsp+0x70 and the return address is at RBP+0x38, so [RBP+8] is an
|
||
# unrelated local, not the caller). The .stackmap walker is the authority for callers.
|
||
# dbghelp first: a fault usually sits inside a system DLL, where the RBP chain has nothing to
|
||
# follow. The RBP chain and the stack scan stay as fallbacks, exactly as in the Ctrl+C path.
|
||
Frames: int = 0
|
||
if DbgHelpUsable() != 0:
|
||
Frames = WalkViaDbgHelp(HProc, wp.GetCurrentThread(), t.CPtr(_State.ContextBuf), Base)
|
||
if Frames <= 1:
|
||
_State.FrameCount = 0
|
||
Frames = WalkRbpChain(Rip, Rbp, Base)
|
||
if Frames <= 1:
|
||
Frames = Frames + ScanStack(Frames, Rsp, Base)
|
||
PrintFrames(Base)
|
||
if _DumpFile != None and _DumpFile != INVALID_HANDLE_VALUE:
|
||
N = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
||
" [traceback] crash dump seq=%d also appended to <exe>.stackdump.txt\n",
|
||
_State.Seq)
|
||
else:
|
||
N = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
||
" [traceback] crash dump seq=%d console only (dump file unavailable)\n",
|
||
_State.Seq)
|
||
EmitLine(N)
|
||
EmitText("=== crash dump end ===\n")
|
||
AtomicLeaveDump()
|
||
return 1
|
||
|
||
|
||
def CrashHandler(Info: VOIDPTR) -> LONG:
|
||
"""SetUnhandledExceptionFilter callback: dump the faulting thread, then hand the exception back.
|
||
|
||
Always EXCEPTION_CONTINUE_SEARCH: this path only supplies the missing stack, it never swallows
|
||
the crash - the OS/WER handling, the exit code and the ASan report keep their behaviour.
|
||
"""
|
||
DumpCrashThread(Info)
|
||
return LONG(TRACEBACK_EXCEPTION_CONTINUE_SEARCH)
|
||
|
||
|
||
# ============================================================
|
||
# Control handler / install
|
||
# ============================================================
|
||
|
||
def CtrlHandler(CtrlType: ULONG) -> BOOL:
|
||
"""SetConsoleCtrlHandler callback (invoked by the system on a new thread).
|
||
|
||
Ctrl+C -> dump then return FALSE (default handling resumes => process exits);
|
||
Ctrl+Break -> dump then return TRUE (handled => process continues);
|
||
any other control event -> no dump, return FALSE (default behaviour untouched).
|
||
Thread-safe: only reads global state and writes reserved buffers, allocates nothing.
|
||
"""
|
||
if _State == None or _State.Installed == 0:
|
||
return 0
|
||
IsBreak: int = 0
|
||
if CtrlType == ULONG(wc.CTRL_BREAK_EVENT):
|
||
IsBreak = 1
|
||
elif CtrlType != ULONG(wc.CTRL_C_EVENT):
|
||
return 0
|
||
DumpMainThread(int(IsBreak))
|
||
if IsBreak != 0:
|
||
return 1
|
||
return 0
|
||
|
||
|
||
def NoteError(Text: str, Code: ULONG):
|
||
"""Install/degradation diagnostic: one '<Text>(GetLastError=%u)' line to the dump sink."""
|
||
if _State == None or _State.LineBuf == None:
|
||
return
|
||
N: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
||
"%s(GetLastError=%u)\n", Text, Code)
|
||
EmitLine(N)
|
||
|
||
|
||
def Install() -> int:
|
||
"""Register the console control handler + the unhandled-exception filter.
|
||
1 = registered (possibly degraded), 0 = not installed.
|
||
|
||
0 has two cases, both diagnosed (a missing sidecar is the expected "off" state, returned
|
||
silently): <exe>.stackmap absent (--traceback not used) -> silent 0, zero side effects; or
|
||
the sidecar exists but open/read/parse fails, or a key buffer cannot be allocated -> warning
|
||
then 0. Idempotent: once attempted (success or not) it returns the previous result without
|
||
re-allocating, so a retry cannot leak the state object or buffers. Installed = 1 is set only
|
||
after both handlers are armed, so a Ctrl+C or a crash cannot arrive half-armed.
|
||
"""
|
||
global _State, _DumpOut, _HandlerReg, _DumpFile
|
||
if _State != None:
|
||
return int(_State.Installed)
|
||
# (1) <exe full path> + ".stackmap"; _State does not exist yet, so failures go through
|
||
# EmitEarly (the dump's own sink), never stdio.printf, to avoid stdout/stderr interleaving.
|
||
Pb: bytes = stdlib.malloc(TRACEBACK_PATH_CAP)
|
||
if Pb == None:
|
||
EmitEarly("[traceback] WARN: path buffer alloc failed, ctrl handler not installed\n")
|
||
return 0
|
||
Plen: ULONG = wf.GetModuleFileNameA(None, Pb, ULONG(TRACEBACK_PATH_CAP))
|
||
if Plen == 0 or sizet(Plen) >= sizet(TRACEBACK_PATH_CAP):
|
||
stdlib.free(t.CPtr(Pb))
|
||
EmitEarly("[traceback] WARN: GetModuleFileNameA failed, ctrl handler not installed\n")
|
||
return 0
|
||
if sizet(Plen) + 16 > sizet(TRACEBACK_PATH_CAP):
|
||
# 16 bytes of slack covers both ".stackmap" (10) and ".stackdump.txt" (16).
|
||
stdlib.free(t.CPtr(Pb))
|
||
EmitEarly("[traceback] WARN: exe path too long to append suffix safely, ctrl handler not installed\n")
|
||
return 0
|
||
string.strcpy(str(Pb + int(Plen)), ".stackmap")
|
||
# (2) sidecar existence = master switch (absent -> return immediately, no side effects).
|
||
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("[traceback] WARN: cannot open <exe>.stackmap, ctrl handler not installed ", Err0)
|
||
return 0
|
||
# (3) state object + all buffers (the callback thread allocates nothing). The collected-frame
|
||
# arrays (FrameAddr/FrameScan) are inline fields of TracebackState, so the single
|
||
# __sizeof__() allocation below covers them and the memset zeroes FrameCount.
|
||
_State = stdlib.malloc(TracebackState.__sizeof__())
|
||
if _State == None:
|
||
stdlib.free(t.CPtr(Pb))
|
||
CloseHandle(hFile)
|
||
EmitEarly("[traceback] WARN: state alloc failed, ctrl handler not installed\n")
|
||
return 0
|
||
string.memset(_State, 0, TracebackState.__sizeof__())
|
||
_State.LineBuf = stdlib.malloc(TRACEBACK_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)
|
||
# The output handle is fixed and locked into _DumpOut here; dumps only use it afterwards.
|
||
_DumpOut = _State.OutHandle
|
||
_State.MainThreadId = wp.GetCurrentThreadId()
|
||
if _State.LineBuf == None or _State.ContextBuf == None or _State.FrameBuf == None:
|
||
# Name the missing buffer now (not later in the callback thread); any missing buffer makes
|
||
# Install return 0. _State exists here, so EmitEarly forwards to EmitText; if LineBuf is
|
||
# the missing one, EmitEarly falls back to a direct WriteFile and the notice is not lost.
|
||
stdlib.free(t.CPtr(Pb))
|
||
CloseHandle(hFile)
|
||
if _State.LineBuf == None:
|
||
EmitEarly("[traceback] WARN: LineBuf alloc failed, ctrl handler not installed\n")
|
||
elif _State.ContextBuf == None:
|
||
EmitEarly("[traceback] WARN: ContextBuf alloc failed, ctrl handler not installed\n")
|
||
elif _State.FrameBuf == None:
|
||
EmitEarly("[traceback] WARN: FrameBuf alloc failed, ctrl handler not installed\n")
|
||
else:
|
||
EmitEarly("[traceback] WARN: runtime buffer alloc failed, ctrl handler not installed\n")
|
||
return 0
|
||
# (3b) open the dump file <exe>.stackdump.txt (append: OPEN_ALWAYS creates it, FILE_APPEND_DATA
|
||
# appends every WriteFile). Opened before loading the sidecar so load-time diagnostics are
|
||
# written too. Raw UTF-8 bytes, unaffected by the console code page; kept open for the
|
||
# whole process lifetime.
|
||
string.strcpy(str(Pb + int(Plen)), ".stackdump.txt")
|
||
_DumpFile = wf.CreateFileA(Pb, TRACEBACK_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("[traceback] WARN: cannot open stack dump file, console-only output\n")
|
||
else:
|
||
DN: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
||
"[traceback] stack dump also written to: %s\n", str(Pb))
|
||
EmitLine(DN)
|
||
# (4) load the sidecar (a failure only disables the map; the handler is still installed so at
|
||
# least addresses are available). LoadMapFile prints the exact validation failure itself.
|
||
Sz: LARGE_INTEGER = LARGE_INTEGER()
|
||
if wf.GetFileSizeEx(hFile, &Sz) == 0:
|
||
NoteError("[traceback] WARN: GetFileSizeEx(<exe>.stackmap) failed, addresses only ", GetLastError())
|
||
else:
|
||
LoadMapFile(hFile, int64(Sz.QuadPart))
|
||
CloseHandle(hFile)
|
||
stdlib.free(t.CPtr(Pb))
|
||
# (5) dbghelp initialization (must happen here: Install runs at startup on the main thread,
|
||
# while the Ctrl+C/Break callbacks run on new threads where dbghelp's module enumeration
|
||
# may be unavailable) + the two cross-module callback addresses, resolved dynamically.
|
||
if TRACEBACK_USE_DBGHELP != 0:
|
||
# SymSetOptions must precede SymInitialize: DEFERRED_LOADS avoids loading symbols/PDBs and
|
||
# NO_PROMPTS avoids an interactive dialog blocking startup.
|
||
dbg.SymSetOptions(uint32(dbg.SYMOPT_DEFERRED_LOADS | dbg.SYMOPT_UNDNAME | dbg.SYMOPT_NO_PROMPTS))
|
||
# fInvadeProcess MUST be 1 (TRUE) so dbghelp enumerates loaded modules into its table; with
|
||
# 0 the table stays empty, SymFunctionTableAccess64 gets no RUNTIME_FUNCTION and
|
||
# SymGetModuleBase64 returns 0, so StackWalk64 fails on the first call and walks no frames.
|
||
if dbg.SymInitialize(wp.GetCurrentProcess(), None, 1) != 0:
|
||
_State.SymReady = 1
|
||
else:
|
||
# SymInitialize fails when another component ALREADY initialized DbgHelp in this
|
||
# process (the ASan runtime registers a symbolizer with the same GetCurrentProcess()
|
||
# pseudo-handle). DbgHelp's process context is process-global and keyed by that handle,
|
||
# so there is no conflict to avoid: refresh the SHARED module table (the documented
|
||
# "refresh after a foreign initialize" call) and let the SymGetModuleBase64 probe below
|
||
# decide usability from evidence instead of assuming the worst.
|
||
ErInit: ULONG = GetLastError()
|
||
if dbg.SymRefreshModuleList(wp.GetCurrentProcess()) != 0:
|
||
_State.SymReady = 1
|
||
EmitText("[traceback] note: DbgHelp already initialized in this process"
|
||
" (SymInitialize refused it); shared module table refreshed,"
|
||
" SymGetModuleBase64 probe decides StackWalk64 usability\n")
|
||
else:
|
||
NoteError("[traceback] WARN: SymInitialize/SymRefreshModuleList failed,"
|
||
" StackWalk64 unavailable ", ErInit)
|
||
# An ASan runtime registers its own DbgHelp symbolizer at startup; that only means the
|
||
# state is shared with us, not that it is unusable. Informational only - the
|
||
# SymGetModuleBase64 probe below is the decision maker.
|
||
HAsan: HANDLE = GetModuleHandleA("clang_rt.asan_dynamic-x86_64.dll")
|
||
if HAsan == None:
|
||
HAsan = GetModuleHandleA("libclang_rt.asan_dynamic-x86_64.dll")
|
||
if HAsan != None:
|
||
EmitText("[traceback] note: ASan runtime detected, it shares DbgHelp with this module;"
|
||
" the module-table probe below decides whether StackWalk64 can walk\n")
|
||
Hdbg: HANDLE = GetModuleHandleA("dbghelp.dll")
|
||
if Hdbg != None and Hdbg != INVALID_HANDLE_VALUE:
|
||
_State.FuncTableAddr = GetProcAddress(Hdbg, "SymFunctionTableAccess64")
|
||
_State.ModuleBaseAddr = GetProcAddress(Hdbg, "SymGetModuleBase64")
|
||
if _State.FuncTableAddr == None or _State.ModuleBaseAddr == None:
|
||
EmitText("[traceback] WARN: StackWalk64 callback addresses incomplete,"
|
||
" dbghelp enhancement unavailable (RBP chain is the primary path)\n")
|
||
# Self-check (the decision maker for dbg_usable): does dbghelp's module table really
|
||
# contain the main module? A 0 here means SymFunctionTableAccess64 gets no
|
||
# RUNTIME_FUNCTION and StackWalk64 fails on its first call, so SymReady is dropped back to
|
||
# 0 (the dump then prints the explicit symwalk=disabled reason instead of a silent 0 frames).
|
||
if _State.SymReady != 0:
|
||
if dbg.SymGetModuleBase64(wp.GetCurrentProcess(), uint64(GetModuleHandleA(None))) != 0:
|
||
_State.ModInited = 1
|
||
else:
|
||
_State.SymReady = 0
|
||
EmitText("[traceback] WARN: dbghelp module table lacks main module (SymGetModuleBase64=0),"
|
||
" StackWalk64 unavailable (RBP chain is the primary path)\n")
|
||
# (6) register last, once all state is ready. _HandlerReg guards "register once" so a repeated
|
||
# callback cannot re-register (and thus cannot repeat the same dump).
|
||
if _HandlerReg == 0:
|
||
if wc.SetConsoleCtrlHandler(CtrlHandler, 1) == 0:
|
||
NoteError("[traceback] WARN: SetConsoleCtrlHandler failed ", GetLastError())
|
||
return 0
|
||
_HandlerReg = 1
|
||
# (7) crash coverage: an unhandled exception has no stack otherwise (see DumpCrashThread). Armed
|
||
# here, before _State.Installed = 1, so a crash cannot arrive while the handler is half-ready.
|
||
# The previous filter's value is deliberately not checked: it is NULL on the first
|
||
# registration (the normal case), and our filter never swallows the exception anyway.
|
||
SetUnhandledExceptionFilter(CrashHandler)
|
||
_State.Installed = 1
|
||
EmitText("[traceback] ready: Ctrl+C prints stack then exits, Ctrl+Break prints then continues,"
|
||
" unhandled exceptions print their own stack\n")
|
||
return 1
|