Files
Viper_VRTE/traceback.vp
T
TermiNexus 0055ab7ea7 refactor: rawptr[T] -> T* 与 t.CConst 前缀迁移(R14/R16)
R14:rawptr[X] -> X*(41 文件 / 2037 行改动),X 含模块限定名与嵌套下标(rawptr[GSList[NameEntry]] -> GSList[NameEntry]*)。保留 6 处 rawptr[t.CPtr](规范例外:存纯裸指针);# 注释与字符串里的 rawptr[...] 属元语法描述,未动。 R16(保守子集):t.CConst | str/bytes -> const str/bytes,t.CConst | char | t.CPtr -> const str;typedef 右值位置豁免(const str 在该处无法注册,与 R1 同源)。 验证:TPC 自举三重重编 SUCCESS;TransPyV.exe 全量 77/77 顺序回归通过。
2026-10-05 17:38:21 +08:00

2140 lines
126 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import t, c
from stdint import *
from w32.win32base import *
import string
import stdio
import stdlib
import w32.win32file as wf
import w32.win32console as wc
import w32.win32process as wp
import w32.win32memory as wm
import w32.win32dbghelp as dbg
# 转储重入保护(见 DumpMainThread)**不使用 InterlockedCompareExchange**:该名字在 x64 上是
# winnt.h 的编译器内部函数(映射到 lock cmpxchg 指令),**不是可链接符号**,直接引用会在链接期
# 报 undefined symbol(实测:ld.lld: undefined symbol: InterlockedCompareExchange)。
# 本模块改用普通 32 位标志做「检查并占用」:并发回调最多导致一次重复转储(诊断工具可接受),
# 且不引入任何链接期依赖。
# =============================================================================
# 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: 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 `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: uint32* = (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 (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: wp.CONTEXT* = (wp.CONTEXT*)(CtxPtr)
Pc: dbg.ADDRESS64* = (dbg.ADDRESS64*)(FBuf)
Fm: dbg.ADDRESS64* = (dbg.ADDRESS64*)(FBuf + 32)
St: dbg.ADDRESS64* = (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
for t_i in range(Hits):
if Found[t_i] == W:
Dup = 1
break
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: wp.CONTEXT* = (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: wp.CONTEXT* = (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