Files
Viper_VRTE/traceback.vp
T
TermiNexus e25e8958f0 WIP 快照:includes 现有改动(含 M2.4 new 描述符的语法支持)
语法侧(M2.4):ast/base.vp 枚举尾部追加 ASTKind.New;ast/exprs.vp 新增 NEW_SRC_*/NEW_LIFE_* 与 class New;ast/parser.vp 新增 _is_new_expr/_parse_new_expr(new 为软关键字,不进 Keyword 表,new=1/new.x/new(1) 仍是标识符);ast/__init__.vp 再导出 New 与常量。

其余为既有 WIP 改动一并快照,保持基线一致(39 文件,+312/-237)。

未纳入:ctraits.vpi(已删除)、ctraits.vpnci、tomllib/(均未跟踪)。
2026-10-08 17:45:31 +08:00

2136 lines
129 KiB
Plaintext

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
# Dump reentrancy protection (see DumpMainThread) does **NOT use InterlockedCompareExchange**: on x64 that
# name is a winnt.h compiler intrinsic (mapped to the lock cmpxchg instruction), **not a linkable symbol**, and
# referring to it directly fails at link time with an undefined symbol (observed: ld.lld: undefined symbol: InterlockedCompareExchange).
# This module instead uses a plain 32-bit flag for test-and-set: concurrent callbacks cause at most one duplicate
# dump (acceptable for a diagnostic facility) and introduce no link-time dependency.
# =============================================================================
# traceback -- runtime "press-key-to-capture-stack" module (dumps the main-thread call stack on Ctrl+C / Ctrl+Break)
#
# Problem solved: when the compiler (or any long task) hangs, one keypress gives the user "where it is stuck right now"
# as a complete call stack -- from the CRT entry (mainCRTStartup / __tmainCRTStartup) down to the current frame.
#
# -----------------------------------------------------------------------------
# 1. Trigger semantics (SetConsoleCtrlHandler callback)
# -----------------------------------------------------------------------------
# Ctrl+C (CTRL_C_EVENT) -> prints the call stack, then **returns FALSE** (back to default handling => process terminates)
# Ctrl+Break (CTRL_BREAK_EVENT) -> prints the call stack, then **returns TRUE** (handled => process keeps running)
# Other control events (close/logoff/shutdown, CTRL_CLOSE_EVENT, etc.) -> always return FALSE, no dump, no intervention.
# **Crash** (unhandled exception, caught via SetUnhandledExceptionFilter) -> prints the call stack on the **faulting
# thread**, then returns EXCEPTION_CONTINUE_SEARCH (exception handed back to the system => exit code / WER / ASan report
# behavior is preserved); see section 14.
# The callback runs on a **newly created thread**, not the main thread -- so "where execution currently is" must come
# from SuspendThread(main thread) + GetThreadContext to obtain the main-thread registers, then walking its stack.
# **Only one dump at a time** is allowed: repeated keypresses make the system invoke the callback concurrently on
# several new threads, and they share the same LineBuf and output handle => output is torn apart (observed: duplicated,
# interleaved "degrade:" lines). So DumpMainThread's entry uses one inline-asm `lock cmpxchg` (AtomicTryEnterDump) to
# test-and-set _DumpLock atomically: a call that loses the race returns immediately (does not suspend the main
# thread, outputs nothing). Each dump also prints an auto-incrementing Seq and the callback thread id: two dump records
# with different Seqs => two sequential dumps; lines with the same Seq that truncate each other => concurrent writes to the shared buffer still exist (locate by this, not by guessing).
#
# -----------------------------------------------------------------------------
# 2. Install timing and master switch (where this module's switch lands)
# -----------------------------------------------------------------------------
# Convention: **the sidecar file exists** <=> the build used TPC's `--traceback`.
# Install() first forms <current exe full path> + ".stackmap"; if that file is absent (CreateFileA reports
# ERROR_FILE_NOT_FOUND) it **returns 0 immediately with zero side effects** (no allocation, no handler registered);
# only when present does it register SetConsoleCtrlHandler (and attach the unhandled-exception filter, see
# section 14). So the TPC side needs no other switch code: it only has to make the artifact link dbghelp (see section 5)
# and call Install() once (see section 6) when the option is enabled.
# If the file exists but its header/record area is invalid: print a locatable warning and mark the mapping unusable -- the
# call stack is still dumped (addresses only); it never exits silently (a diagnostic gives degraded info instead of hiding errors).
#
# -----------------------------------------------------------------------------
# 3. Sidecar (<exe>.stackmap) format -- little-endian
# -----------------------------------------------------------------------------
# Header 20 bytes: magic="TPCS"(4B) + u32 version=1 + u32 entry_count + u32 str_count
# + u32 str_bytes
# Record area entry_count x 20B (**sorted by ascending rva**): u64 rva; u32 file_off; u32 line; u32 func_off
# String pool str_bytes bytes: NUL-terminated UTF-8 strings; file_off/func_off are pool offsets
# (func_off = 0 means no function name)
# Runtime: GetModuleHandleA(NULL) gets the image base, frame address - base = rva, then binary-search the record area.
# Hit -> frame line " File \"<file>\", line <line>, in <function>", followed by one more line of 4-space-indented source
# (at runtime the file is reopened and line <line> is read; if it cannot be read the whole line is omitted, see section 13);
# Miss -> " File \"<0x<address> +0x<rva> no source line mapping>\", line 0, in <unknown>";
# Address below the main module (typically a DLL/system frame) -> " File \"<0x<address> external>\", line 0, in <unknown>".
# All three forms keep the address/RVA on the same line (see section 13).
# Load time does one full-table validation (rva strictly ascending, string offsets inside the pool); if invalid the mapping is disabled --
# the binary search relies on ascending order, and a corrupt table yields **wrong line numbers**, worse than "addresses only".
# A validation failure always **prints which item it failed on** (including the actually-read header magic/version/count/str_bytes
# and the real file size); the reason is never collapsed into a single "sidecar invalid".
# NOTE: each byte read must be masked with 0xFF: Viper's Buf[i] is a **signed** char, so a byte >= 0x80 cast
# to u32 sign-extends (e.g. record count 1000=0x3E8, first byte 0xE8 -> 0xFFFFFFE8), making the header's
# entry_count/str_bytes astronomical and `20+count*20+str_bytes > file size` always true
# => the mapping can never load (this is the real cause of "source line mapping = none"; see ReadU32).
#
# -----------------------------------------------------------------------------
# 4. The two walk paths and degrade conditions (**main/aux swapped this round, see section 11 B**)
# -----------------------------------------------------------------------------
# NOTE: read section 11 B first: from this round **the main path = the RBP chain** (zero dbghelp dependency) and dbghelp is demoted
# to "optional augmentation". The dbghelp notes below still hold; they describe the historical detail of the **augmentation path**.
# Augmentation path (optional): dbghelp!StackWalk64 (MachineType=IMAGE_FILE_MACHINE_AMD64).
# SymSetOptions(SYMOPT_DEFERRED_LOADS|SYMOPT_UNDNAME|SYMOPT_NO_PROMPTS) +
# SymInitialize(GetCurrentProcess(), NULL, **TRUE**) -- TRUE is a hard requirement: it makes dbghelp
# enumerate loaded modules into its own module table; with FALSE the table is empty, SymFunctionTableAccess64
# cannot get a RUNTIME_FUNCTION, SymGetModuleBase64 returns 0, and StackWalk64 **fails on its very first call**
# (seen as "symbol walk = available but 0 frames"). Combined with DEFERRED_LOADS it does not actually load PDBs.
# Of the three callback addresses, FunctionTableAccess/GetModuleBase are still fetched in Install via GetProcAddress
# (this repo's existing dynamic-resolution style; precedent VET/App/lib/VGuard.vp:852-867): this avoids relying on
# "a cross-module imported function name as a function-pointer argument", unverified in this repo; pass NULL if unavailable.
# The ReadProcessMemory callback uses ReadMemCb **defined inside this module** (an in-module "function name as
# function-pointer argument" is already proven by the CtrlHandler precedent): passing kernel32!ReadProcessMemory
# itself gives no failure reason, and a 0 return makes StackWalk64 stop immediately (exactly the "0 frames").
# Main path (**manual RBP-chain walk**): always tried first. When it yields no more frames (<=1) it **first runs the second
# fallback = stack scan (ScanStack, no frame-pointer dependency)**; only after that comes the dbghelp augmentation (compile-time switch = 1,
# SymInitialize/SymRefreshModuleList succeeded at Install, both callback addresses and the frame buffer are ready, and
# the SymGetModuleBase64(main module) probe passes; **the presence of an ASan runtime only indicates DbgHelp state sharing, no longer
# a veto**); any unsatisfied precondition prints an explicit reason (`[diag] symwalk=disabled reason=...`),
# **never a silent 0 frames**.
# New this round: every step unconditionally prints `[diag] rbp:` raw readings + verdict, and `walk: fallback=stack_scan`
# plus scanned inferred frames (`~` prefix) -- see section 12.
# Main-path implementation: print the current RIP, then walk the main-thread RBP chain reading [rbp] (caller rbp) and [rbp+8]
# (return address) frame by frame; requires rbp to be 8-byte aligned, strictly increasing towards higher addresses (`[rbp] > rbp`),
# a single-step span <= TRACEBACK_RBP_MAX_STEP, and the return address within the main-module base range, else it stops at once
# (to keep a wild pointer from flying the walk off track); **all memory reads go through ReadProcessMemory, never a direct dereference of a wild rbp**.
# The RBP chain only covers functions that keep a frame pointer; once the frame pointer is not preserved ([rbp+8] is not the return address) the chain
# breaks at step 1 -- then ScanStack is the fallback, with a further dbghelp augmentation layer on top (when available).
# With --traceback, TPC adds `-frame-pointer=all` to llc (Phase2Translator.py:2187-2193);
# whether that flag actually takes effect is covered by the investigation result and fix location in section 12 C.
# [Evidence first: gather evidence, then decide] After the banner each dump prints a set of preflight diagnostics (**only when TRACEBACK_DIAG=1**) --
# Seq / callback thread id / main thread id / pre-entry Dumping value / MapOk / SymReady / ModInited /
# the **actual return value** of SymGetModuleBase64(main module) / main-module base / whether the frame buffer and the two callback addresses
# are in place; after entering the dbghelp main path the **first frame** additionally prints StackWalk64's return code, the
# machine/RIP/RSP/RBP passed in, the returned AddrPC, and the return value of SymFunctionTableAccess64(proc, AddrPC).
# These lines are produced **before any degrade decision**, so the root cause of "0 frames" can be judged directly (see section 9).
#
# -----------------------------------------------------------------------------
# 5. Link-time dependencies (the TPC side must know this)
# -----------------------------------------------------------------------------
# dbghelp (-ldbghelp) + kernel32 (existing). SymInitialize / StackWalk64 / SymSetOptions /
# SymGetModuleBase64 / SymFunctionTableAccess64 are **static** references (the latter two are used for first-frame evidence),
# so the artifact must resolve dbghelp's import library or the link fails. If adding -ldbghelp is inconvenient,
# set TRACEBACK_USE_DBGHELP to 0: that constant comparison folds away at compile time the only branch containing
# dbghelp calls (see the TRACEBACK_USE_DBGHELP comment), leaving only the RBP-chain walk.
#
# -----------------------------------------------------------------------------
# 6. TPC-side call entry (symbol and signature)
# -----------------------------------------------------------------------------
# import traceback
# traceback.Install() # returns int: 1 = handler registered; 0 = not installed (no sidecar/failure)
# # registers two: SetConsoleCtrlHandler (Ctrl+C/Break) +
# # SetUnhandledExceptionFilter (crash), see section 1 / section 14
# Call timing: once during process startup. Two equivalent ways --
# (a) call it as the first statement of entry main;
# (b) inject an @llvm.global_ctors (.CRT$XCU semantics) constructor that calls this symbol (repo precedent:
# TransPyC/lib/core/LLVMCG/BaseGen.py:2061-2097 registers a Ctrl+C handler via llvm.global_ctors);
# the constructor runs during CRT initialization, covering the "stuck before main" case.
# This module itself has **no** module-level constructor (the state object is lazily allocated by Install), so it has
# no initialization-order dependency on TPC's injected constructor, and repeated calls are idempotent.
# NOTE: this module defines the "main thread" as **the thread that called Install()** (Install records its id
# via GetCurrentThreadId; that is the thread suspended/walked on callback). So it must be called on the real entry
# thread (main, or the thread running a CRT constructor); if called on a startup worker thread, the dump is that thread's stack.
#
# -----------------------------------------------------------------------------
# 7. Why WriteFile directly instead of printf
# -----------------------------------------------------------------------------
# The dump happens at a moment when **the main thread is already suspended**: if the main thread happens to be stopped inside
# the CRT's stdout lock, the callback thread calling printf would **wait forever on the same lock**, turning "hang" into "key press and nothing moves".
# So output goes through kernel32!WriteFile(GetStdHandle(STD_ERROR_HANDLE)) (no user-mode CRT stream lock),
# and formatting goes through CRT snprintf (writes only to a memory buffer, never touching FILE locks). Another benefit of stderr: stdout is often
# taken by a pipe (e.g. read by a parent tool) while stderr usually goes straight to the terminal. Handles and all buffers are prepared at Install,
# and the callback thread does **zero allocation** (MemBuddy is not thread-safe; the callback never touches the pool).
#
# -----------------------------------------------------------------------------
# 7b. Dump **also written to a file** + pure-ASCII console (two fixes this round)
# -----------------------------------------------------------------------------
# (a) File output (solves "no Break key" and "console code page eats characters"): on successful Install, open an
# **append** handle for `<exe full path> + ".stackdump.txt"` (OPEN_ALWAYS +
# FILE_APPEND_DATA, creating it if absent); EmitLine writes **the same content** both to the console and appending to disk.
# The file is raw UTF-8 bytes, unaffected by the console code page; each dump prepends a small separator header
# (local timestamp/pid/trigger key/main thread id/Seq, Chinese labels, file only). A file write failure **does not block**
# the console and is reported only once (see EmitToFile). On successful Install it prints a line
# `[traceback] stack dump also written to: <path>` to confirm the destination.
# (b) Pure-ASCII console: the runtime text was originally Chinese (UTF-8 bytes), while cmd's console output code page
# defaults to 936 (GBK) -- WriteFile to the console interprets UTF-8 bytes as 936, and a multi-byte sequence split/rejoined
# across several WriteFile calls appears as **whole lines disappearing** (the longest "first frame" line first) or adjacent lines shifting,
# looking "duplicated and mangled" (including the U+FFFD replacement character). So **all runtime text sent to the console is changed to English/ASCII**
# (banner / [diag] / Trigger= / exe base= / walk: primary=rbp_chain / frames=N, etc.),
# readable and greppable under any code page; the ` File "<file>", line <line>, in <function>` frame line keeps the source path
# text as-is (separators/added explanatory text are always ASCII).
# Chinese is kept only in: this file's comments/docstrings, and the **file-only** separator header.
#
# -----------------------------------------------------------------------------
# 8. Existing declarations relied on (unchanged) and declarations added by this module
# -----------------------------------------------------------------------------
# Existing (`from w32.win32base import *` brings bare names): 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.
# New this round: wc.SetConsoleCtrlHandler / wc.CTRL_C_EVENT / wc.CTRL_BREAK_EVENT;
# wp.CONTEXT (full x64 layout) / wp.CONTEXT_FULL / wp.GetThreadContext;
# wm.ReadProcessMemory (wrapped by this module's ReadMemCb as the StackWalk64 callback);
# w32/win32dbghelp.vp (STACKFRAME64/ADDRESS64/KDHELP64 + SymSetOptions/
# SYMOPT_* + SymInitialize/SymFunctionTableAccess64/SymGetModuleBase64/
# StackWalk64);
# Dump file: wf.WriteFile appends to <exe>.stackdump.txt + this module's own constant
# TRACEBACK_FILE_APPEND_DATA (FILE_APPEND_DATA, not defined by wf);
# Dump reentrancy protection uses one inline-asm `lock cmpxchg` (AtomicTryEnterDump) on the module-level
# 32-bit _DumpLock for atomic test-and-set (not InterlockedCompareExchange: on x64 it is
# a compiler intrinsic, has no exported symbol, and is not linkable).
# New this round (real Python traceback output): **this module's own declaration** of kernel32!GetEnvironmentVariableA
# (not declared by win32base; the repo declares it only in VET/App/lib/VDirs.vp:45; kernel32 is linked anyway, so both TPC and
# TPV can resolve it), used for the TRACEBACK_DIAG switch; everything else uses only existing APIs (see section 13).
#
# -----------------------------------------------------------------------------
# 9. Actionable conclusions for "0 frames" (which case -> which live field to look at)
# -----------------------------------------------------------------------------
# NOTE: every evidence line in this section is a `[diag]` line, so it is **produced only when TRACEBACK_DIAG=1** (see section 13 C);
# when investigating "0 frames" first start the target process with `set TRACEBACK_DIAG=1`.
# The two first-frame evidence lines (" [diag] StackWalk64 first: ret=... addrpc=... fta=..." and the one immediately after,
# " [diag] StackWalk64 in: machine=... rip=... rsp=... rbp=..." -- originally one 143-char line,
# split into two short lines to remove the "mangled display" illusion caused by wrapping in a narrow terminal, with no field dropped)
# together with the preflight lines ("[diag] Seq=... ... modinited=..." / "SymGetModuleBase64(main)=...") directly give the conclusion:
# (1) modinited == 0 and SymGetModuleBase64(main) == 0 => dbghelp's module table does not contain the main module.
# Root cause is SymInitialize's fInvadeProcess argument (must be TRUE) or the call timing (it must be called during process
# startup while module enumeration is available; this module does it in Install). Fix: confirm Install's SymInitialize
# third argument is 1, and that Install is called on the entry thread after all modules are loaded; if needed use
# SymRefreshModuleList to re-enumerate.
# (2) first frame ret == 0 with an adjacent "[diag] readmem failed addr=... size=... GetLastError=..."
# => reading stack memory failed. A `ReadProcessMemory` returning 0 in the callback makes StackWalk64 stop at once (= 0 frames).
# Fix: check ReadMemCb's width convention (dbghelp side is DWORD nSize + LPDWORD, kernel32 side is
# SIZE_T; this module receives into a local sizet then writes back as DWORD) and the process handle/privileges.
# (3) first frame ret != 0 but returned addrpc == 0, or fta (SymFunctionTableAccess64(addrpc)) == 0
# => a context/module-base convention problem (the RIP/RSP/RBP passed to StackWalk64 or the module base is wrong).
# Fix: check CONTEXT's Rip/Rsp/Rbp values against the GetModuleHandleA(NULL) base.
# Note: `ret=not-called` in the first-frame line means the SymInitialize/callback-address/frame-buffer preconditions are incomplete and
# StackWalk64 was never called (the line is still produced then, guaranteeing first-frame evidence always exists before "switching to the RBP chain").
# The three cases are mutually exclusive and decidable: just compare the two printed field groups above against the criteria to determine **on the spot** which case it is.
#
# -----------------------------------------------------------------------------
# 10. Two groups of locating prints added this round (answering "did the walk run at all" and "the real cause of duplicate/mangled")
# -----------------------------------------------------------------------------
# A. Whether the walk was called and which exit it took (all via DumpMainThread / WalkViaDbgHelp's existing output channels;
# these lines are **output only when TRACEBACK_DIAG=1**):
# * DumpMainThread prints, **before the call**, " [diag] walk: enter use_dbghelp=... symready=...
# modinited=... frame_buf=... mapok=... asan=...", and, **after the call**, " [diag] walk: leave
# frames=... mapok=...". Both lines present => the walk did enter the RBP main path (dbghelp augmentation optional).
# * If **even walk: enter does not appear** (and TRACEBACK_DIAG=1) => DumpMainThread returned before this line:
# it can only be one of three fallbacks (OpenThread / SuspendThread / GetThreadContext failing, each with its own
# "fallback: ... cannot dump stack" line) -- that line locates the skipped branch.
# * **Every return** in WalkViaDbgHelp first prints " [diag] walk: return reason=<...> frames=...",
# with reason in {null_state, disabled, null_frame_buf, null_context, stackwalk_first_failed,
# stackwalk_failed, addrpc_zero, frame_limit} (meanings in the in-function comments), so the walk's path
# is always decidable, never guessed. Since this round the main path is the RBP chain; DumpMainThread additionally prints
# `walk: primary=rbp_chain` / `walk: rbp_chain frames=...` / `walk: dbghelp enhancement engaged`;
# when dbghelp is unavailable it additionally prints `[diag] symwalk=disabled reason=...` (see section 11 B).
# B. Evidence for the real cause of "duplicate/mangled" (the only evidence = whether line numbers repeat):
# * EmitLine adds a globally increasing prefix " [dump#<k>] " to **every line** when **TRACEBACK_DIAG=1**
# (k is incremented per call via _DumpLineNo); when off (default) it adds **no prefix**, keeping frame lines as plain Python
# traceback form (see section 13).
# * The same text appearing several times with **repeated numbers** (the same k twice) => that text was written twice, i.e. **concurrent writes to
# the same LineBuf**; only then is "output mutual exclusion" needed. The same number cannot come from a single-threaded EmitLine.
# * Text **truncated / line-wrapped out of place** but with **strictly increasing, non-repeating numbers** => the content was written once (numbers cannot be scrambled),
# so it can only be a **terminal long-line wrap/echo** display effect; the fix is to shorten long lines (this round split the three longest),
# **without touching the lock**.
# * Added this round: `[dump#skip]` is only needed when output mutual exclusion is used; this module judged it a "display-layer"
# issue, so no mutual exclusion was added and that marker is not printed (criteria and conclusion in the delivery notes).
#
# -----------------------------------------------------------------------------
# 11. Two root-cause fixes this round (crash = attribute access silently demoted; 0 frames = dbghelp conflicting with the environment)
# -----------------------------------------------------------------------------
# A. **The crash root cause is not an allocation failure but TPC silently demoting `_State.<field>` to the null constant.**
# Evidence (build-artifact IR; module prefix = first 16 hex digits of this file's sha1, 9b4ee486e08837c5):
# * The source `string.memset(_State.FrameBuf, 0, ...)` was generated as
# `call i8* @memset(i8* null, i32 0, i64 248)` -- the argument is the literal null;
# in the same function `_State.LineBuf` (snprintf), `_State.FuncTableAddr / ModuleBaseAddr`
# (StackWalk64's two callback slots) were likewise all folded to null.
# * `Install`'s `calloc(1,248)` and its null check "return 0 if allocation failed" **are both present and correct**
# (in the IR `@calloc`'s return value is stored to field 20 normally), so it is **not** calloc returning NULL.
# Mechanism (TPC side, report only, no change): `TransPyC/lib/core/Handles/HandlesExprAttr.py`
# `_resolve_var_classname` (approx. L285-293) looks only for a module-level global of the form `rawptr[X]` at
# `GVar.type.pointee` (= `X*`, still a pointer) => it fails to recognize `X` and returns None; the fallback
# `_recover_struct_class` (approx. L723-740) can peel one more pointer level, but requires `X` to already be in
# the **function-level** `Gen.variables/global_vars` (`HandlesFunctions.py:1848` clears it per function),
# and that registration only happens when "the bare name `X` is evaluated once" (`HandlesExpr.py:479/487`).
# => For any function that **reads/writes `_State.<field>` right at the start and never separately evaluates the bare name `_State`
# inside the function**, `_handle_attr_name` returns None at L507-508:
# - as a call argument => silently replaced with null (-> memset(null) crash);
# - as the condition of `if X == None:` => the whole `if` is dropped by `HandlesIf` L170-171
# (-> the `if _State.FrameBuf == None: ...` we originally wrote **was never generated**).
# Affected functions (IR corroboration: these function bodies load `@_State` 0 times):
# `EntryRva` / `LookupRva` / `EmitFrame` / `WalkViaDbgHelp`.
# Other functions (`EmitLine` / `DumpMainThread` / `ReadMemCb` / `Install` ...) all have a
# `_State == None` null check at entry => the bare name is registered => field access works (this is why
# the `frame_buf=1`, `modinited=1` printed by `DumpMainThread` are real).
# This module's fix (**without changing TPC**): every function that uses `_State.<field>` directly gets
# **`if _State == None: ...` as its very first statement** (even if redundant) -- it is both a genuine fallback
# and registers the bare name `_State` into `Gen.variables`, so later field accesses resolve normally.
# At the crash point add one more local-variable fallback: `FBuf: VOIDPTR = _State.FrameBuf` -> check `FBuf == None`
# -> `memset(FBuf, ...)`, using the most reliable channel (a local variable) on the most dangerous line.
# B. **0 frames / DbgHelp state sharing**: the ASan runtime registers its own DbgHelp symbolizer, so this module's
# SymInitialize is rejected (the DbgHelp process context is a **process-level** state, unique per handle). But this is **not
# a conflict**: the state is shared, and using the same GetCurrentProcess() pseudo-handle reuses its module table. So Install's
# decision becomes an **evidence-based** three-step one: SymInitialize succeeds -> SymReady=1; fails -> SymRefreshModuleList
# refreshes the shared module table (the official "refresh after external init" idiom); then a SymGetModuleBase64(main module)
# probe arbitrates -- if the probe is 0, SymReady falls back to 0 and the reason is printed, never a silent 0 frames. Walk-path order:
# * main path = `WalkRbpChain` (pure RBP chain, **zero dbghelp dependency**);
# * **second fallback = `ScanStack` stack scan** (zero dbghelp and **no RBP / frame-pointer dependency**):
# enabled when the RBP chain yields only the current RIP (<=1 frame) (`walk: fallback=stack_scan`), see section 12;
# * dbghelp augmentation: only when the RBP chain yields no more frames (<=1) and DbgUsable != 0 (= probe passed) does it run
# `WalkViaDbgHelp`. It **clears the collected frames and rebuilds the whole stack** (dropping scan-inferred frames too) and is the only path that can use the PE
# unwind table (.pdata) to cross **frame-pointer-less system frames** back to the .vp stack -- the typical Ctrl+C scene
# (main thread blocked in a system DLL, RBP=0) can only have its real call chain recovered by it.
# * A degrade **must give a reason** (no silent 0 frames): `[diag] symwalk=disabled reason=...`,
# reason in {compile_time_switch, syminitialize_failed_or_main_module_missing,
# callback_missing, null_frame_buf}; the ASan probe uses GetModuleHandleA with two common
# ASan runtime names (clang_rt / libclang_rt prefixes), **for diagnostic lines only**, no longer part of the decision.
# `WalkRbpChain` is hardened in step (after becoming the main path it must "never turn a hang into a crash"):
# * reads of [rbp]/[rbp+8] always go through `wm.ReadProcessMemory` (**no direct dereference** of a wild rbp);
# * **every step unconditionally prints `[diag] rbp:` raw readings + verdict** (evidence before decision, including rejected steps; see section 12);
# * self-consistency checks: non-zero / 8-byte aligned / `[rbp] > rbp` / single-step span <= RBP_MAX_STEP /
# return address within the main-module base range [Base, Base+TRACEBACK_MODULE_MAX) -- only then is the frame **accepted**.
#
# -----------------------------------------------------------------------------
# 12. New this round: per-step RBP-chain raw readings + stack-scan fallback (answering "the walk is too short")
# -----------------------------------------------------------------------------
# Live case (user measurement, `#0` resolved correctly: RIP in the main module, mapping/lookup/frame #0 all correct): `#1 = 0x121fa`
# -- far below the main-module base and not a stack address => the chain breaks at step 1. => First turn "where it broke and why"
# into readable data, then add a path that **does not depend on RBP**.
# A. **Per-step raw readings** (inside `WalkRbpChain`, prefix `[diag] rbp:`, **before any reject early-return**,
# output only when TRACEBACK_DIAG=1):
# frame=<i> rbp=0x... [rbp]=0x... [rbp+8]=0x... <- raw 8-byte values read
# frame=<i> rpm_ok=<0/1> rpm_err=<GetLastError> accept=<0/1> reject_reason=<...>
# reject_reason in {rbp_zero, not_aligned, rpm_failed, ret_zero, ret_out_of_module,
# not_growing, step_too_large}; when accepted, reason=`accept`, accept=1.
# Only accepted steps are FramePush-ed into the frame buffer (the old implementation output before validating, printing garbage values like `0x121fa`
# as an external frame, misleading the reader). The three chain-break root causes are distinguishable at a glance: read failure / the value itself is not a return
# address (frame pointer not preserved) / rejected by the self-consistency checks.
# B. **Stack-scan fallback** (`ScanStack`): enabled when the RBP chain yields <=1 frame (`walk: fallback=stack_scan`).
# Scans [RSP, RSP+2MiB) in 8-byte steps, **reading 4 KiB per chunk whole** (stops on failure and prints address/err);
# a candidate = a word within the main-module range whose **adjacent 8-byte word (A-8 or A+8) looks like a stack address**; de-duplicated by value,
# capped at 64 (diagnostic prints `scan_candidates=<N>`); inferred frames enter the buffer with Scanned=1 and append
# ` [inferred]` (` File "...", line 0, in <unknown> [inferred]`) to the frame line to mark them clearly as **scan inferences**
# (may contain false positives; never confused with authoritative frames; see section 13). All reads go only through ReadProcessMemory,
# never a direct dereference of main-thread memory (a diagnostic facility must not turn a "hang" into a "crash").
# C. **Whether the frame pointer is preserved** (investigation conclusion and TPC-side fix location; this module reports only, no change):
# * The single llc call site = `TransPyC/lib/Projectrans/Phase2Translator.py:2160 _llc_compile`,
# which appends `-frame-pointer=all` to the command when `--traceback` is on (2187-2193);
# the App module (e.g. `TransPyV/App/lib/core/Gen/Expr/ExprGenOps.vp`, module sha1
# `8a96a2885bae09ca`) goes through the **same** call site (2608's _dso.ll / 2319's inlining merge),
# so the flag's location itself is fine.
# * Cache invalidation: App module obj = `output/{sha1}.obj`, `_compile_ll_files` skips it if it exists
# (2621), but with `--traceback` `hotspot_on` (1144) is true => 1196-1204 deletes the old obj
# to force recompilation; the include module has a separate isolated dir `includes.binary_traceback` (3097-3099) +
# old-obj deletion (3181-3190). => Neither App nor include silently reuses a "frame-pointer-less old obj".
# * Delivery conclusion: if `[diag] rbp:` shows `[rbp+8]` is still not a return address (`ret_out_of_module`),
# then `-frame-pointer=all` (a flag at the level of an llc default) did not take effect; the most robust fix is to annotate
# `"frame-pointer"="all"` explicitly per function **in the IR** -- location
# `TransPyV/App/lib/core/Gen/Func/FuncGenBase.vp` (attribute assembly `_append_llvm_attr`, 154-181)
# and `TransPyC/lib/core/Handles/HandlesFunctions.py:1735` (next to `func.attributes.add('noredzone')`
# ) -- making the frame pointer a **function attribute** rather than an llc command-line default, immune at the root to "whether llc honors the default".
# * [Extra measurement, Test/LLvmLiteTest Ctrl+C] `RBP=0x0`, RIP in a system DLL: the reject reason is
# `rbp_zero` (not `ret_out_of_module`) => the break is **not in a .vp function** but in the interrupted
# system frame. RBP is callee-saved, but frame-pointer-less system/CRT code uses it as a **scratch register**
# (`xor ebp,ebp` is a common zeroing idiom), so the interrupted site's RBP is unrelated to "the nearest ancestor
# that maintains a frame pointer". => Conclusion: **keeping a frame pointer for .vp functions cannot save this path** (the chain is already broken further in); only
# PE unwind-table (.pdata) based walking (dbghelp!StackWalk64, see section 11 B) can recover
# the real call chain from such a site. The frame-pointer switch is therefore **not** this round's fix location and is kept above only as a historical record.
#
# -----------------------------------------------------------------------------
# 13. This round: real Python-traceback-style output + the TRACEBACK_DIAG switch
# -----------------------------------------------------------------------------
# A. Output shape (default, TRACEBACK_DIAG off) -- the whole traceback looks like:
# 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]
# * **Frame order is reversed**: the walk goes from the innermost (current RIP) outward to callers, so collection order = inner to outer, and printing reverses it
# => **the outermost (CRT/caller) first, the innermost last** (matching Python traceback). So frames must
# first be collected into a buffer (FramePush), then printed in reverse after the walk ends (DumpMainThread).
# * A frame that hits a source location prints two lines: the first is 2-space-indented `File "path", line N, in function` (function name
# is `<unknown>` if unavailable); the second is 4-space-indented + that source line -- **the file is reopened at runtime** and line N read sequentially
# (leading/trailing whitespace stripped, over-long truncated to TRACEBACK_LINE_CAP); **if it cannot be opened / the line number is out of range / the read fails, the whole line is
# omitted** (no placeholder, no error, silently skipped); see EmitSourceText (stack buffer, zero heap allocation from the callback thread).
# * A ScanStack inferred frame appends ` [inferred]` to the frame line (keeping "may be a false positive" information, no longer a `~` prefix).
# * A frame missing from the mapping -> `File "<0x<address> +0x<rva> no source line>", line 0, in <unknown>`
# (address+RVA kept on the same line); a frame below the main module -> `File "<0x<address> external>", line 0, in <unknown>`.
# B. Frame buffer: TracebackState.FrameAddr / FrameScan (`t.CArray[uint64/int32, TRACEBACK_FRAME_BUF]`,
# `TRACEBACK_FRAME_BUF = TRACEBACK_MAX_FRAMES + TRACEBACK_SCAN_MAX_HITS = 128`),
# **inlined into the state object** => covered by Install's `TracebackState.__sizeof__()` allocation, no separate malloc,
# zero allocation from the callback thread. When full, new frames are simply dropped (no overflow); the final `frames=` reflects the actual collected count.
# C. **TRACEBACK_DIAG switch**: reads env var `TRACEBACK_DIAG`, enabled only when the value == "1"; **read once per process
# and cached** (_DiagFlag -1/0/1, see DiagEnabled). When off (default) it emits no `[diag] ...` /
# `srcline map: ...` lines and adds no ` [dump#<k>] ` line-number prefix (see section 10 B), making the dump a clean
# traceback; when on, the diagnostic detail matches the previous amount (including per-frame `[diag] rbp:` / `[diag] scan:` / symwalk lines).
# The read API = kernel32!GetEnvironmentVariableA (declared at the top of this module, see section 8).
# D. The dump sequence number and the file-output notice move to **after** the whole traceback (
# ` [traceback] dump seq=... also appended to <exe>.stackdump.txt`), no longer inserted between frames; `Ctrl+C` dumping returns FALSE (process exits),
# `Ctrl+Break` dumping returns TRUE (keeps running) -- semantics unchanged.
#
# -----------------------------------------------------------------------------
# 14. Crash dump (unhandled exception; answering "crash has no error stack")
# -----------------------------------------------------------------------------
# A. Motivation: Ctrl+C has a stack, **a crash does not** -- once ASan passes its report window it prints nothing, and the OS leaves only
# an exit code (measurements of the same crash: ASan on = 0x80000003 STATUS_BREAKPOINT(int3), ASan off =
# 0xC0000005 ACCESS_VIOLATION; neither has **a stack**). So Install additionally registers
# kernel32!SetUnhandledExceptionFilter(CrashHandler), and CrashHandler -> DumpCrashThread
# produces the missing stack on the **faulting thread**.
# B. DumpCrashThread's three rules:
# * **Never swallow the exception**: the filter always returns EXCEPTION_CONTINUE_SEARCH(0); exit code / WER / ASan report
# behavior is preserved; this module only adds the stack;
# * **Never dereference OS pointers**: EXCEPTION_POINTERS ({ExceptionRecord, ContextRecord}) and
# the EXCEPTION_RECORD header (u32 ExceptionCode / u32[...] ExceptionAddress) are always read via
# ReadProcessMemory;
# * **Never write the OS's CONTEXT**: StackWalk64 writes back to the record it walks on every frame, so first copy the OS-provided
# CONTEXT into _State.ContextBuf (size already aligned with winnt.h, 0x4D0) and walk from the copy.
# C. Walk order and printing: dbghelp first (the crash point is usually in a system DLL, where the RBP chain is hopeless) -> RBP chain ->
# stack scan, the same WalkViaDbgHelp / WalkRbpChain / ScanStack and PrintFrames as the Ctrl+C path;
# it **shares the same reentrancy lock** with the Ctrl+C dump (AtomicTryEnterDump), so only one dump can exist at a time:
# if a crash happens while a dump is in progress, this crash is not dumped (lock held), avoiding a second hit.
# Output shape: `=== 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=...`
# -> Traceback frames word-for-word identical to Ctrl+C (including source-line re-read) -> ` frames=N` -> file-output notice
# -> `=== crash dump end ===`.
# * `av_kind`/`av_addr` come from EXCEPTION_RECORD's ExceptionInformation[0/1] (0=read 1=write 8=execute;
# data address) and are valid only for 0xC0000005 access-violation-type faults -- a crash site is exactly where "raw numbers beat guessing";
# * `regs` gives **the argument state of the faulting frame** (Win64's first 4 integer arguments = rcx/rdx/r8/r9; r12 is the register that frame
# uses to dereference a pointer): one line distinguishes "wrong argument passed" from "argument correct, pointed-to content wrong",
# with no need for another rebuild. The Ctrl+C path does not print it (there is no "operand of the faulting instruction" to see there).
# * **Does not print `[RBP+8]` as a return address**: a .vp frame's RBP is a locals base (`leaq N(%rsp), %rbp`),
# and the return address is at RBP + frame size - 8 (varies per function; measured _TypePrintDepth is RBP+0x38),
# `[RBP+8]` is just an unrelated local. Callers always rely on the .stackmap walk result.
# The Chinese file header (stack dump/trigger=) belongs only to the Ctrl+C path; the crash path leaves it alone.
# D. Known boundaries: ASan's VEH takes priority over the unhandled-exception filter -- if a crash is reported normally by ASan (which has its own stack),
# this path is not called; this path covers exactly the two cases where "ASan cannot report it / ASan is off" (the two measured exit codes
# are both among them). Stack-exhaustion crashes (this path itself needs stack) are out of scope.
# =============================================================================
# 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: [TRACEBACK_FRAME_BUF]uint64 # collected frames, innermost first
FrameScan: [TRACEBACK_FRAME_BUF]int32 # 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: [8]char
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: [160]char
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: [32]char
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(rawptr(_State.LineBuf + int(Pl)), rawptr(_State.LineBuf), sizet(N + 1))
string.memmove(rawptr(_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: [256]char
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(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(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(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(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(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(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(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(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(Buf)
return MapFailEntry(Size, int(i), "file_off outside the string pool", uint64(FO))
if FN != 0 and uint64(FN) >= uint64(SBytes):
stdlib.free(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,
None, wf.OPEN_EXISTING, wf.FILE_ATTRIBUTE_NORMAL, None)
if h == INVALID_HANDLE_VALUE:
return
Chunk: [TRACEBACK_SRC_CHUNK]char
Src: [TRACEBACK_LINE_CAP]char
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: rawptr) -> 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, rawptr(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: [2]uint64 # [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, rawptr(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: [TRACEBACK_SCAN_CHUNK_WORDS]uint64 # 4 KiB chunk buffer (local)
Found: [TRACEBACK_SCAN_MAX_HITS]uint64 # 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, rawptr(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===== stack dump %04u-%02u-%02u %02u:%02u:%02u.%03u pid=%u trigger=%s main_thread_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, rawptr(_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: [TRACEBACK_FRAME_BUF]uint64
SaveScanBuf: [TRACEBACK_FRAME_BUF]int32
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: [2]uint64 # [0] = EXCEPTION_RECORD*, [1] = CONTEXT* (EXCEPTION_POINTERS)
Two[0] = 0
Two[1] = 0
if wm.ReadProcessMemory(HProc, rawptr(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: [12]uint32
RecWords[0] = 0
if Rec != 0:
if wm.ReadProcessMemory(HProc, rawptr(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, rawptr(CtxPtr), rawptr(_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(), rawptr(_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(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(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, None, wf.OPEN_EXISTING, wf.FILE_ATTRIBUTE_NORMAL, None)
if hFile == INVALID_HANDLE_VALUE:
Err0: ULONG = GetLastError()
stdlib.free(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(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 = rawptr(stdlib.calloc(1, wp.CONTEXT.__sizeof__()))
_State.FrameBuf = rawptr(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(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, None, wf.OPEN_ALWAYS, wf.FILE_ATTRIBUTE_NORMAL, None)
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(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