把 includes 下 64 个 .vp 的文件头注释(含 traceback.vp 第 13-427 行的整块文件级说明)以及 2 处漏网的正文中文注释(ast/lexer.vp、json/__writer.vp)与 1 处中文 docstring(memhub.vp)逐行译为英文;同步把 fltk/gen_ffi.py 的生成模板与其自身注释英文化,使重新生成仍为英文。 只改注释/文档串文字:行数与代码逐字节不变(骨架比对 140 文件漂移 0),运行时中文字符串字面量 305 处保持原样。验证:三重重编 SUCCESS(170.21s)+ 全量 77/77 顺序回归通过 + VET 编译 SUCCESS / --search-paths 7 条 exit 0。
2140 lines
130 KiB
Plaintext
2140 lines
130 KiB
Plaintext
import t, c
|
|
from stdint import *
|
|
from w32.win32base import *
|
|
import string
|
|
import stdio
|
|
import stdlib
|
|
import w32.win32file as wf
|
|
import w32.win32console as wc
|
|
import w32.win32process as wp
|
|
import w32.win32memory as wm
|
|
import w32.win32dbghelp as dbg
|
|
|
|
# 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: t.CArray[uint64, TRACEBACK_FRAME_BUF] # collected frames, innermost first
|
|
FrameScan: t.CArray[int32, TRACEBACK_FRAME_BUF] # 0 = authoritative, 1 = inferred (ScanStack)
|
|
FrameCount: int # frames collected so far (also the printed frame count)
|
|
|
|
|
|
# Sole global state: None = not installed (zero side effects while the sidecar is absent).
|
|
# Pointer + lazy allocation instead of a module-level object constant, which would emit a
|
|
# constructor that may run after Install and wipe the state it wrote.
|
|
_State: TracebackState* = None
|
|
|
|
|
|
# ---- Dump atomic lock / locked output handle ----
|
|
# _DumpLock: dump reentrancy lock, 0 = free / 1 = held. AtomicTryEnterDump uses one inline
|
|
# `lock cmpxchg` to test-and-set it, closing the read-modify-write race of a plain flag.
|
|
# _State.Dumping is its readable mirror.
|
|
export _DumpLock: uint32 = 0
|
|
|
|
# _DumpOut: output handle locked for the whole dump. Chosen once at DumpMainThread entry so a
|
|
# single dump never splits across stdout/stderr. EmitLine prefers it; Install sets it too.
|
|
_DumpOut: HANDLE = None
|
|
|
|
# _HandlerReg: "register once" guard for SetConsoleCtrlHandler.
|
|
_HandlerReg: int = 0
|
|
|
|
# _DumpFile: append handle of the dump file (OpenAlways + FILE_APPEND_DATA). When None/invalid
|
|
# only the file write is skipped; console output is unaffected.
|
|
_DumpFile: HANDLE = None
|
|
|
|
# _DumpFileFailWarned: report a dump-file write failure only once.
|
|
_DumpFileFailWarned: int = 0
|
|
|
|
# _DumpLineNo: global output line counter, incremented by EmitLine and written as the
|
|
# " [dump#<k>] " prefix of every line (only while TRACEBACK_DIAG is enabled). Repeated
|
|
# numbers prove concurrent writes to the shared LineBuf; strictly increasing numbers mean any
|
|
# truncation is only terminal wrapping.
|
|
_DumpLineNo: int = 0
|
|
|
|
# _DiagFlag: cached TRACEBACK_DIAG state; -1 = not read yet, 0 = off, 1 = on (read once/process).
|
|
_DiagFlag: int = -1
|
|
|
|
|
|
def DiagEnabled() -> int:
|
|
"""Return 1 when the environment variable TRACEBACK_DIAG equals "1" (read once, then cached).
|
|
|
|
With the switch off (default) every "[diag] ..." / "srcline map: ..." line and the per-line
|
|
" [dump#<k>] " counter prefix are suppressed, so a dump is a plain Python-style traceback;
|
|
with it on the previous diagnostic detail is kept. The single cached global keeps both the
|
|
console and the file output consistent within one process.
|
|
"""
|
|
global _DiagFlag
|
|
if _DiagFlag >= 0:
|
|
return _DiagFlag
|
|
Val: t.CArray[char, 8]
|
|
Val[0] = char(0)
|
|
R: DWORD = GetEnvironmentVariableA("TRACEBACK_DIAG", &Val, DWORD(8))
|
|
_DiagFlag = 0
|
|
if R == 1:
|
|
if Val[0] == char(49): # '1'
|
|
_DiagFlag = 1
|
|
return _DiagFlag
|
|
|
|
|
|
# ============================================================
|
|
# Output (WriteFile, bypassing CRT stream locks; see header section 7)
|
|
# Defined before the sidecar reader so load-time diagnostics share this single path.
|
|
# Console text is always ASCII (code-page independent, greppable); the same bytes are also
|
|
# appended to <exe>.stackdump.txt.
|
|
# ============================================================
|
|
|
|
def WriteConsoleOnly(N: int):
|
|
"""Write the first N bytes of LineBuf to the console only (no file).
|
|
|
|
Split out of EmitLine so the "dump file write failed" notice can reach the console without
|
|
re-entering EmitLine (and thus the file write), avoiding recursion. Handle rules match.
|
|
"""
|
|
if _State == None or N <= 0:
|
|
return
|
|
H: HANDLE = _DumpOut
|
|
if H == None or H == INVALID_HANDLE_VALUE:
|
|
H = _State.OutHandle
|
|
if H == None or H == INVALID_HANDLE_VALUE:
|
|
return
|
|
Wr: ULONG = 0
|
|
wf.WriteFile(H, _State.LineBuf, ULONG(N), &Wr, None)
|
|
|
|
|
|
def EmitToFile(N: int):
|
|
"""Append the first N bytes of LineBuf to the dump file (raw UTF-8, code-page independent).
|
|
|
|
The file handle is opened at Install (FILE_APPEND_DATA + OPEN_ALWAYS) and kept open; the
|
|
callback thread opens nothing and allocates nothing. A write failure never affects the
|
|
console (EmitLine already wrote it); it is reported once, written straight to the console
|
|
handle via a local stack buffer (not through EmitLine) to avoid recursion.
|
|
"""
|
|
global _DumpFileFailWarned
|
|
if _State == None or N <= 0:
|
|
return
|
|
if _DumpFile == None or _DumpFile == INVALID_HANDLE_VALUE:
|
|
return
|
|
Wr: ULONG = 0
|
|
if wf.WriteFile(_DumpFile, _State.LineBuf, ULONG(N), &Wr, None) != 0:
|
|
return
|
|
if _DumpFileFailWarned != 0:
|
|
return
|
|
_DumpFileFailWarned = 1
|
|
LB: t.CArray[char, 160]
|
|
M: int = stdio.snprintf(&LB, sizet(160),
|
|
"[traceback] WARN: stack dump file write failed (further file-write failures not reported)\n")
|
|
if M <= 0:
|
|
return
|
|
H: HANDLE = _DumpOut
|
|
if H == None or H == INVALID_HANDLE_VALUE:
|
|
H = wf.GetStdHandle(wf.STD_ERROR_HANDLE)
|
|
if H == None or H == INVALID_HANDLE_VALUE:
|
|
return
|
|
W2: ULONG = 0
|
|
wf.WriteFile(H, &LB, ULONG(M), &W2, None)
|
|
|
|
|
|
def EmitLine(N: int):
|
|
"""Write the first N bytes of LineBuf to console + dump file (N is snprintf's return value).
|
|
|
|
The single output choke point for EmitText / EmitFrame / every diagnostic:
|
|
0) line-number prefix " [dump#<k>] " (see _DumpLineNo), only while TRACEBACK_DIAG is on:
|
|
inserted in place via string.memmove so callers are unchanged and no extra buffer is
|
|
used; a repeated k proves concurrent writes to LineBuf, a strictly increasing k means
|
|
display wrapping. With the switch off the bytes are written verbatim, keeping the
|
|
Python-traceback frame lines clean.
|
|
1) console (WriteFile, bypassing CRT stream locks): prefers the dump-locked _DumpOut,
|
|
falling back to _State.OutHandle during load (both resolved once at Install).
|
|
2) dump file (EmitToFile): appends the same bytes (UTF-8, code-page independent).
|
|
A file write failure never blocks the console (see EmitToFile); handles are never
|
|
re-resolved mid-dump.
|
|
"""
|
|
global _DumpLineNo
|
|
if _State == None or N <= 0:
|
|
return
|
|
if N > TRACEBACK_LINE_CAP - 1:
|
|
N = TRACEBACK_LINE_CAP - 1
|
|
# Line-number prefix: shift the buffer right by the prefix length, then write the prefix.
|
|
if DiagEnabled() != 0:
|
|
_DumpLineNo = _DumpLineNo + 1
|
|
Pfx: t.CArray[char, 32]
|
|
Pl: int = stdio.snprintf(&Pfx, sizet(32), " [dump#%d] ", _DumpLineNo)
|
|
if Pl > 0:
|
|
# Keep room for the trailing '\0' after prefixing.
|
|
if N + Pl > TRACEBACK_LINE_CAP - 1:
|
|
N = TRACEBACK_LINE_CAP - 1 - Pl
|
|
# Move the trailing '\0' too (N+1) so the buffer stays NUL-terminated.
|
|
string.memmove(t.CPtr(_State.LineBuf + int(Pl)), t.CPtr(_State.LineBuf), sizet(N + 1))
|
|
string.memmove(t.CPtr(_State.LineBuf), &Pfx, sizet(Pl))
|
|
N = N + Pl
|
|
WriteConsoleOnly(N)
|
|
EmitToFile(N)
|
|
|
|
|
|
def EmitText(Text: str):
|
|
"""Write one constant line with no format specifiers (same buffer / WriteFile path)."""
|
|
if _State == None or _State.LineBuf == None:
|
|
return
|
|
N: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP), "%s", Text)
|
|
EmitLine(N)
|
|
|
|
|
|
def EmitEarly(Text: str):
|
|
"""Install-stage early diagnostic (_State / LineBuf not ready yet) on the same output path.
|
|
|
|
With _State and LineBuf ready it forwards to EmitText (same buffer, same handle); otherwise
|
|
it writes via WriteFile(GetStdHandle(STD_ERROR_HANDLE)) directly (the dump's own sink), so
|
|
stdout and stderr never interleave. Install is not a callback thread, so resolving a handle
|
|
once here is harmless.
|
|
"""
|
|
if _State != None and _State.LineBuf != None:
|
|
EmitText(Text)
|
|
return
|
|
H: HANDLE = wf.GetStdHandle(wf.STD_ERROR_HANDLE)
|
|
if H == None or H == INVALID_HANDLE_VALUE:
|
|
H = wf.GetStdHandle(wf.STD_OUTPUT_HANDLE)
|
|
if H == None or H == INVALID_HANDLE_VALUE:
|
|
return
|
|
Len: sizet = string.strlen(Text)
|
|
if Len == 0:
|
|
return
|
|
Wr: ULONG = 0
|
|
wf.WriteFile(H, Text, ULONG(Len), &Wr, None)
|
|
|
|
|
|
def EmitEarlyCode(Text: str, Code: ULONG):
|
|
"""EmitEarly variant that appends a GetLastError code (same output path)."""
|
|
if _State != None and _State.LineBuf != None:
|
|
N: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP), "%s(GetLastError=%u)\n", Text, Code)
|
|
EmitLine(N)
|
|
return
|
|
LB: t.CArray[char, 256]
|
|
N2: int = stdio.snprintf(&LB, sizet(256), "%s(GetLastError=%u)\n", Text, Code)
|
|
if N2 <= 0:
|
|
return
|
|
H: HANDLE = wf.GetStdHandle(wf.STD_ERROR_HANDLE)
|
|
if H == None or H == INVALID_HANDLE_VALUE:
|
|
H = wf.GetStdHandle(wf.STD_OUTPUT_HANDLE)
|
|
if H == None or H == INVALID_HANDLE_VALUE:
|
|
return
|
|
Wr: ULONG = 0
|
|
wf.WriteFile(H, &LB, ULONG(N2), &Wr, None)
|
|
|
|
|
|
def AtomicTryEnterDump() -> int:
|
|
"""Atomically test-and-set _DumpLock (0→1); returns the previous value (0 = acquired).
|
|
|
|
One inline `lock cmpxchg`: eax = expected old value 0, ecx = new value 1.
|
|
Only this module-level lock and registers are touched; nothing is allocated.
|
|
"""
|
|
Old: uint32 = 0
|
|
__asm__(f"""xor eax, eax
|
|
mov ecx, 1
|
|
lock cmpxchg [{asminp(&_DumpLock, ASM_DESCR.REG_ANY)}], ecx
|
|
mov {asmout(Old, ASM_DESCR.OUTPUT_REG)}, eax""",
|
|
op=[ASM_DESCR.CLOBBER_RAX, ASM_DESCR.CLOBBER_RCX, ASM_DESCR.CLOBBER_MEMORY, ASM_DESCR.CLOBBER_CC])
|
|
return int(Old)
|
|
|
|
|
|
def AtomicLeaveDump():
|
|
"""Release the dump lock: _DumpLock -> 0 (and mirror _State.Dumping -> 0).
|
|
|
|
Only the holder reaches here; another callback thread seeing 1 returns immediately, so a
|
|
plain write of 0 is safe.
|
|
"""
|
|
global _DumpLock
|
|
_DumpLock = 0
|
|
if _State != None:
|
|
_State.Dumping = 0
|
|
|
|
|
|
# ============================================================
|
|
# Sidecar reader (little-endian, byte by byte, avoids unaligned access)
|
|
# ============================================================
|
|
|
|
def ReadU32(Buf: bytes, Off: int) -> uint32:
|
|
"""Read a little-endian u32 from Buf[Off..Off+3] (Buf is an unsigned byte base).
|
|
|
|
Each byte must be masked with 0xFF: Viper's `Buf[i]` is a signed char, so a byte >= 0x80
|
|
would sign-extend through the u32 cast (0xE8 -> 0xFFFFFFE8) and corrupt the high bits,
|
|
blowing up `20+count*20+str_bytes` and making the sidecar unloadable. Masking after the
|
|
cast yields 0..255 whether the cast sign- or zero-extends.
|
|
"""
|
|
Val: uint32 = uint32(Buf[Off]) & uint32(0xFF)
|
|
Val = Val | ((uint32(Buf[Off + 1]) & uint32(0xFF)) << 8)
|
|
Val = Val | ((uint32(Buf[Off + 2]) & uint32(0xFF)) << 16)
|
|
Val = Val | ((uint32(Buf[Off + 3]) & uint32(0xFF)) << 24)
|
|
return Val
|
|
|
|
|
|
def ReadU64(Buf: bytes, Off: int) -> uint64:
|
|
"""Read a little-endian u64 as two u32 halves (avoids an 8-byte unaligned read)."""
|
|
Lo: uint64 = uint64(ReadU32(Buf, Off))
|
|
Hi: uint64 = uint64(ReadU32(Buf, Off + 4))
|
|
return Lo | (Hi << 32)
|
|
|
|
|
|
# ---- Validation-failure diagnostics (each prints and returns 0) ----
|
|
|
|
def MapFailPlain(Size: int64, Reason: str) -> int:
|
|
"""Validation failed before the header could be read: print reason + actual file size."""
|
|
if _State == None or _State.LineBuf == None:
|
|
return 0
|
|
N: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
|
"[traceback] WARN: <exe>.stackmap invalid -- %s (file %lld bytes), addresses only this run\n",
|
|
Reason, Size)
|
|
EmitLine(N)
|
|
return 0
|
|
|
|
|
|
def MapFailHdr(Size: int64, Reason: str, Head: bytes) -> int:
|
|
"""Validation failed with the header read: also print the actual header fields."""
|
|
if _State == None or _State.LineBuf == None:
|
|
return 0
|
|
N: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
|
"[traceback] WARN: <exe>.stackmap invalid -- %s"
|
|
" (read magic=0x%x version=%u entry_count=%u str_count=%u str_bytes=%u; file %lld bytes),"
|
|
" addresses only this run\n",
|
|
Reason, ReadU32(Head, 0), ReadU32(Head, 4), ReadU32(Head, 8), ReadU32(Head, 12),
|
|
ReadU32(Head, 16), Size)
|
|
EmitLine(N)
|
|
return 0
|
|
|
|
|
|
def MapFailEntry(Size: int64, Index: int, Reason: str, Got: uint64) -> int:
|
|
"""Record-table validation failed: print the offending index and value."""
|
|
if _State == None or _State.LineBuf == None:
|
|
return 0
|
|
N: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
|
"[traceback] WARN: <exe>.stackmap invalid -- %s (entry %d, value=0x%llx; file %lld bytes),"
|
|
" addresses only this run\n",
|
|
Reason, Index, Got, Size)
|
|
EmitLine(N)
|
|
return 0
|
|
|
|
|
|
def EntryRva(Index: int) -> uint64:
|
|
"""RVA of record Index (record area starts at MapBase + 20).
|
|
|
|
The bare `_State` null-check below is REQUIRED: the attribute layer registers `_State` per
|
|
function before it can resolve this module-level `TracebackState*`; without it
|
|
`_State.MapBase` is silently folded to null (see header section 11 A). It also guards
|
|
against an uninstalled state (returns 0).
|
|
"""
|
|
if _State == None:
|
|
return 0
|
|
Off: int = TRACEBACK_MAP_HEADER + Index * TRACEBACK_ENTRY_SIZE
|
|
return ReadU64(_State.MapBase, Off)
|
|
|
|
|
|
def LookupRva(Rva: uint64) -> int:
|
|
"""Look up Rva in the ascending table: return the largest index with rva <= Rva, else -1.
|
|
|
|
Granularity matches the writing side (TPC `_WriteStackMap`): the table stores DWARF
|
|
line-start addresses, and a stack-frame return address almost never lands exactly on one,
|
|
so a floor search is required; otherwise every frame degrades to +0x...(unknown).
|
|
Hit = some rva <= target (the largest such); miss = target below the smallest rva.
|
|
|
|
The bare `_State` null-check below is REQUIRED for the same reason as EntryRva: without it
|
|
`_State.EntryCount` folds to null (Hi = -1, the search never runs).
|
|
"""
|
|
if _State == None:
|
|
return -1
|
|
Lo: int = 0
|
|
Hi: int = int(_State.EntryCount) - 1
|
|
Best: int = -1
|
|
while Lo <= Hi:
|
|
Mid: int = Lo + (Hi - Lo) / 2
|
|
E: uint64 = EntryRva(int(Mid))
|
|
if E <= Rva:
|
|
Best = Mid
|
|
Lo = Mid + 1
|
|
else:
|
|
Hi = Mid - 1
|
|
return Best
|
|
|
|
|
|
def LoadMapFile(hFile: HANDLE, Size: int64) -> int:
|
|
"""Read and validate the sidecar (header / ascending records / string offsets). 1 = MapOk.
|
|
|
|
Any validation failure disables the map (returns 0): a broken table yields wrong line
|
|
numbers, worse than addresses only. This function prints exactly where it failed itself
|
|
(MapFailPlain/MapFailHdr/MapFailEntry, with the actual header values and file size).
|
|
|
|
Field-by-field checks, aligned with the writing side (TPC `_WriteStackMap`):
|
|
magic little-endian u32 == 0x53435054 ("TPCS")
|
|
version u32 == 1
|
|
entry_count part of `20+count*20+str_bytes <= file size`
|
|
str_bytes same (str_count is diagnostic only, not checked)
|
|
record rva strictly ascending
|
|
file_off < str_bytes and >= 1
|
|
func_off == 0 means no name; otherwise < str_bytes
|
|
"""
|
|
if Size < int64(TRACEBACK_MAP_HEADER):
|
|
return MapFailPlain(Size, "file smaller than the 20-byte header")
|
|
if Size > int64(TRACEBACK_MAP_LIMIT):
|
|
return MapFailPlain(Size, "file exceeds the 64 MiB limit (header likely corrupt)")
|
|
Buf: bytes = stdlib.malloc(sizet(Size))
|
|
if Buf == None:
|
|
return MapFailPlain(Size, "load buffer allocation failed (file too large or out of memory)")
|
|
Got: ULONG = 0
|
|
if wf.ReadFile(hFile, Buf, ULONG(Size), &Got, None) == 0:
|
|
Err: ULONG = GetLastError()
|
|
stdlib.free(t.CPtr(Buf))
|
|
if _State != None and _State.LineBuf != None:
|
|
RN: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
|
"[traceback] WARN: read <exe>.stackmap failed (GetLastError=%u),"
|
|
" addresses only this run\n", Err)
|
|
EmitLine(RN)
|
|
return 0
|
|
if sizet(Got) != sizet(Size):
|
|
stdlib.free(t.CPtr(Buf))
|
|
return MapFailPlain(Size, "read fewer bytes than the file size")
|
|
# Header: magic / version / entry_count / str_count / str_bytes
|
|
if ReadU32(Buf, 0) != uint32(TRACEBACK_MAP_MAGIC):
|
|
stdlib.free(t.CPtr(Buf))
|
|
return MapFailHdr(Size, "magic is not \"TPCS\" (writer/reader endianness or offset mismatch)", Buf)
|
|
if ReadU32(Buf, 4) != uint32(TRACEBACK_MAP_VERSION):
|
|
stdlib.free(t.CPtr(Buf))
|
|
return MapFailHdr(Size, "version is not 1 (stackmap written by another TPC version)", Buf)
|
|
Cnt: uint32 = ReadU32(Buf, 8)
|
|
StrCnt: uint32 = ReadU32(Buf, 12)
|
|
SBytes: uint32 = ReadU32(Buf, 16)
|
|
# Records + string pool must fit inside the file (u64 math to avoid u32 wrap).
|
|
Need: uint64 = uint64(TRACEBACK_MAP_HEADER) \
|
|
+ uint64(Cnt) * uint64(TRACEBACK_ENTRY_SIZE) \
|
|
+ uint64(SBytes)
|
|
if Need > uint64(Size):
|
|
stdlib.free(t.CPtr(Buf))
|
|
return MapFailHdr(Size, "20+entry_count*20+str_bytes exceeds end of file "
|
|
"(header corrupt or file truncated)", Buf)
|
|
if SBytes == 0:
|
|
stdlib.free(t.CPtr(Buf))
|
|
return MapFailHdr(Size, "str_bytes is 0 (string pool missing)", Buf)
|
|
# The last pool byte must be '\0' so any later %s print stays inside the pool.
|
|
PoolEnd: int = TRACEBACK_MAP_HEADER + int(Cnt) * TRACEBACK_ENTRY_SIZE + int(SBytes) - 1
|
|
if Buf[PoolEnd] != 0:
|
|
stdlib.free(t.CPtr(Buf))
|
|
return MapFailHdr(Size, "string pool does not end with '\\0' (pool corrupt)", Buf)
|
|
# Full-table check: strictly ascending rva + string offsets inside the pool.
|
|
Prev: uint64 = 0
|
|
for i in range(int(Cnt)):
|
|
Off: int = TRACEBACK_MAP_HEADER + int(i) * TRACEBACK_ENTRY_SIZE
|
|
Cur: uint64 = ReadU64(Buf, Off)
|
|
if i > 0 and Cur <= Prev:
|
|
stdlib.free(t.CPtr(Buf))
|
|
return MapFailEntry(Size, int(i),
|
|
"record rva not strictly ascending (breaks binary search)", Cur)
|
|
Prev = Cur
|
|
FO: uint32 = ReadU32(Buf, Off + TRACEBACK_ENTRY_FILEOFF)
|
|
FN: uint32 = ReadU32(Buf, Off + TRACEBACK_ENTRY_FUNCOFF)
|
|
if uint64(FO) >= uint64(SBytes):
|
|
stdlib.free(t.CPtr(Buf))
|
|
return MapFailEntry(Size, int(i), "file_off outside the string pool", uint64(FO))
|
|
if FN != 0 and uint64(FN) >= uint64(SBytes):
|
|
stdlib.free(t.CPtr(Buf))
|
|
return MapFailEntry(Size, int(i),
|
|
"func_off is neither 0 (no name) nor inside the string pool",
|
|
uint64(FN))
|
|
_State.MapBase = Buf
|
|
_State.EntryCount = Cnt
|
|
_State.StrBytes = SBytes
|
|
_State.StrBase = Buf + (TRACEBACK_MAP_HEADER + int(Cnt) * TRACEBACK_ENTRY_SIZE)
|
|
_State.MapOk = 1
|
|
return 1
|
|
|
|
|
|
# ============================================================
|
|
# Frame collection and single-frame printing (Python traceback style)
|
|
# ============================================================
|
|
|
|
def FramePush(Addr: uint64, Scanned: int) -> int:
|
|
"""Append one frame (address + inferred flag) to the dump buffer; 1 = stored, 0 = full.
|
|
|
|
The walk paths call this innermost-first (the order they discover frames); the caller prints
|
|
the buffer in reverse afterwards, giving the outermost-first Python traceback order. When the
|
|
buffer is full the new frame is dropped (no overflow). The bare `_State` null-check is
|
|
REQUIRED (it touches _State.FrameAddr).
|
|
"""
|
|
if _State == None:
|
|
return 0
|
|
if _State.FrameCount >= TRACEBACK_FRAME_BUF:
|
|
return 0
|
|
_State.FrameAddr[_State.FrameCount] = Addr
|
|
_State.FrameScan[_State.FrameCount] = int32(Scanned)
|
|
_State.FrameCount = _State.FrameCount + 1
|
|
return 1
|
|
|
|
|
|
def EmitSourceText(Path: str, LineNo: int):
|
|
"""Print the 4-space-indented text of line LineNo from Path (console + dump file).
|
|
|
|
Best effort: the file is read sequentially in fixed stack chunks (no heap allocation -- the
|
|
callback thread must not take the CRT heap lock the suspended main thread may hold). Leading
|
|
whitespace is skipped while collecting and trailing whitespace is trimmed afterwards, so the
|
|
printed text is the source line with its leading/trailing whitespace removed. Any failure --
|
|
the file cannot be opened, a read fails, or LineNo is past EOF -- prints nothing at all (no
|
|
placeholder line, no error).
|
|
"""
|
|
if _State == None or _State.LineBuf == None or LineNo <= 0:
|
|
return
|
|
h: HANDLE = wf.CreateFileA(Path, wf.GENERIC_READ, wf.FILE_SHARE_READ | wf.FILE_SHARE_WRITE,
|
|
t.CPtr(0), wf.OPEN_EXISTING, wf.FILE_ATTRIBUTE_NORMAL, t.CPtr(0))
|
|
if h == INVALID_HANDLE_VALUE:
|
|
return
|
|
Chunk: t.CArray[char, TRACEBACK_SRC_CHUNK]
|
|
Src: t.CArray[char, TRACEBACK_LINE_CAP]
|
|
Line: int = 1
|
|
Out: int = 0 # bytes collected for the target line
|
|
Done: int = 0
|
|
while Done == 0:
|
|
Got: ULONG = 0
|
|
if wf.ReadFile(h, &Chunk, ULONG(TRACEBACK_SRC_CHUNK), &Got, None) == 0:
|
|
CloseHandle(h)
|
|
return
|
|
if Got == 0:
|
|
break # EOF: the requested line does not exist
|
|
k: int = 0
|
|
while k < int(Got):
|
|
Ch: int = int(Chunk[int(k)]) & 0xFF
|
|
if Ch == 10: # '\n' ends the current line
|
|
if Line == LineNo:
|
|
Done = 1
|
|
break
|
|
Line += 1
|
|
k += 1
|
|
continue
|
|
if Line == LineNo and Ch != 13:
|
|
if Out > 0 or (Ch != 32 and Ch != 9):
|
|
if Out < TRACEBACK_LINE_CAP - 8:
|
|
Src[Out] = char(Ch)
|
|
Out += 1
|
|
k += 1
|
|
CloseHandle(h)
|
|
if Out == 0:
|
|
return # out of range or blank line: print nothing
|
|
while Out > 0:
|
|
E: int = int(Src[Out - 1]) & 0xFF
|
|
if E != 32 and E != 9:
|
|
break
|
|
Out -= 1
|
|
if Out == 0:
|
|
return
|
|
Src[Out] = char(0)
|
|
N: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP), " %s\n", Src)
|
|
EmitLine(N)
|
|
|
|
|
|
def EmitFrame(Addr: uint64, Base: uint64, Scanned: int):
|
|
"""Print one collected frame in Python traceback style.
|
|
|
|
Hit frame : ` File "<path>", line <N>, in <func>` and, when the source line can be re-read
|
|
at dump time, a second ` <source text>` line (see EmitSourceText).
|
|
Unmapped : ` File "<0xaddr +0xrva no source line mapping>", line 0, in <unknown>`.
|
|
Outside : ` File "<0xaddr external>", line 0, in <unknown>`.
|
|
Scanned (inferred, from ScanStack) frames carry a trailing " [inferred]": they may be false
|
|
positives and stay distinguishable from authoritative frames.
|
|
|
|
The bare `_State` null-check below is REQUIRED: this function uses `_State.LineBuf` (snprintf
|
|
target) and `_State.MapOk`, both of which would otherwise fold to null (see header section 11 A).
|
|
"""
|
|
if _State == None:
|
|
return
|
|
Tail: str = ""
|
|
if Scanned != 0:
|
|
Tail = " [inferred]"
|
|
N: int = 0
|
|
# Above the module is as "external" as below it. WalkRbpChain pushes the raw RIP
|
|
# unconditionally (the interrupted thread may be blocked inside ntdll/kernel32), and such a
|
|
# system address is far ABOVE Base without being below it — the old `Addr < Base` test let it
|
|
# through, and the floor search in LookupRva() then returned the LAST record, so a system
|
|
# frame printed as a plausible-looking `File "...", line N, in <func>` (measured: the bogus
|
|
# `line 13` frame beside a real-looking function name). TRACEBACK_MODULE_MAX is the same
|
|
# coarse upper bound WalkRbpChain / ScanStack already use for "inside the main module".
|
|
if Base == 0 or Addr < Base or Addr >= Base + uint64(TRACEBACK_MODULE_MAX):
|
|
# Outside the main image (DLL / system / corrupt frame): address only.
|
|
N = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
|
" File \"<0x%llx external>\", line 0, in <unknown>%s\n", Addr, Tail)
|
|
EmitLine(N)
|
|
return
|
|
Rva: uint64 = Addr - Base
|
|
if _State.MapOk == 0:
|
|
# No map (sidecar missing/corrupt): keep the address + RVA in the same File line.
|
|
N = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
|
" File \"<0x%llx +0x%llx no source line mapping>\", line 0, in <unknown>%s\n",
|
|
Addr, Rva, Tail)
|
|
EmitLine(N)
|
|
return
|
|
Hit: int = LookupRva(Rva)
|
|
if Hit < 0:
|
|
# Table loaded but not hit: report why (empty table / rva below the first entry), once.
|
|
if DiagEnabled() != 0:
|
|
if _State.MissReported == 0:
|
|
_State.MissReported = 1
|
|
if _State.EntryCount == 0:
|
|
EmitText(" srcline map: rva not hit -- map is empty (0 entries)\n")
|
|
else:
|
|
First: uint64 = EntryRva(0)
|
|
Last: uint64 = EntryRva(int(_State.EntryCount) - 1)
|
|
M: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
|
" srcline map: rva 0x%llx not hit (%u entries: first rva=0x%llx last rva=0x%llx;"
|
|
" target < first => writer RVA base mismatch)\n",
|
|
Rva, _State.EntryCount, First, Last)
|
|
EmitLine(M)
|
|
N = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
|
" File \"<0x%llx +0x%llx no source line mapping>\", line 0, in <unknown>%s\n",
|
|
Addr, Rva, Tail)
|
|
EmitLine(N)
|
|
return
|
|
Off: int = TRACEBACK_MAP_HEADER + Hit * TRACEBACK_ENTRY_SIZE
|
|
FileOff: uint32 = ReadU32(_State.MapBase, Off + TRACEBACK_ENTRY_FILEOFF)
|
|
Line: uint32 = ReadU32(_State.MapBase, Off + TRACEBACK_ENTRY_LINE)
|
|
FuncOff: uint32 = ReadU32(_State.MapBase, Off + TRACEBACK_ENTRY_FUNCOFF)
|
|
FileName: str = str(_State.StrBase + int(FileOff))
|
|
FuncName: str = "unknown"
|
|
if FuncOff != 0:
|
|
FuncName = str(_State.StrBase + int(FuncOff))
|
|
N = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
|
" File \"%s\", line %u, in %s%s\n", FileName, Line, FuncName, Tail)
|
|
EmitLine(N)
|
|
EmitSourceText(FileName, int(Line))
|
|
|
|
|
|
# ============================================================
|
|
# The two walk paths
|
|
# ============================================================
|
|
|
|
def ReadMemCb(hProcess: HANDLE, Addr: ULONGLONG, Buf: VOIDPTR, Size: DWORD,
|
|
pRead: t.CPtr) -> BOOL:
|
|
"""This module's stand-in for the StackWalk64 memory callback (PREAD_PROCESS_MEMORY_ROUTINE64).
|
|
|
|
Not kernel32!ReadProcessMemory directly:
|
|
(1) it returns only BOOL, and a callback returning 0 stops StackWalk64 immediately (the
|
|
"0 frames" symptom) while swallowing GetLastError; the first failure's address/size/
|
|
error is recorded here and printed next to the first-frame diagnostic (once);
|
|
(2) dbghelp's prototype is `DWORD nSize` + `LPDWORD`, while kernel32's ReadProcessMemory is
|
|
`SIZE_T nSize` + `SIZE_T*` (both 8 bytes on x64); handing dbghelp's 4-byte LPDWORD to
|
|
kernel32 would write through 4 bytes on success. A local `sizet` receives the count,
|
|
written back to dbghelp's buffer as DWORD.
|
|
Passing this function by name as a t.CPtr mirrors SetConsoleCtrlHandler(CtrlHandler).
|
|
"""
|
|
Read: sizet = 0
|
|
R: BOOL = wm.ReadProcessMemory(hProcess, t.CPtr(Addr), Buf, sizet(Size), &Read)
|
|
if pRead != None:
|
|
Rd: uint32* = (uint32*)(pRead)
|
|
Rd[0] = uint32(Read)
|
|
if R == 0 and _State != None and _State.ReadMemFailed == 0:
|
|
# Record only the first failure (address/size/error); WalkViaDbgHelp prints it next to the
|
|
# first-frame return code so "callback returned 0 -> 0 frames" is visible at once.
|
|
_State.ReadMemFailed = 1
|
|
_State.ReadMemAddr = uint64(Addr)
|
|
_State.ReadMemSize = uint32(Size)
|
|
_State.ReadMemErr = GetLastError()
|
|
return R
|
|
|
|
|
|
def WalkViaDbgHelp(hProcess: HANDLE, hThread: HANDLE, CtxPtr: VOIDPTR,
|
|
Base: uint64) -> int:
|
|
"""dbghelp!StackWalk64 frame walk; returns the number of frames printed (0 = caller degrades).
|
|
|
|
Optional enhancement path: the primary path is WalkRbpChain (no dbghelp). Called only when
|
|
the RBP chain found no more frames and the caller judged dbghelp usable - that judgement comes
|
|
from the Install-time SymGetModuleBase64 probe (see header section 11 B); otherwise the caller
|
|
never enters. It is the only path that can cross a frameless system frame (RBP=0 in the Ctrl+C
|
|
context) via the PE unwind tables (.pdata) and reach the real .vp stack.
|
|
|
|
STACKFRAME64 fields are accessed through explicit sub-struct pointers (dbg.ADDRESS64*):
|
|
the ADDRESS64 structures sit at offsets 0 / 16 / 32 / 48 (= AddrPC / AddrReturn / AddrFrame /
|
|
AddrStack, see includes/w32/win32dbghelp.vp). After zeroing, AddrPC/AddrFrame/AddrStack are
|
|
pre-filled with the context's Rip/Rbp/Rsp and Mode = AddrModeFlat(3); MachineType is passed on
|
|
every call. STACKFRAME64 has no size/field-count member (that is StackWalkEx's STACKFRAME_EX);
|
|
the repository's sizeof must simply match the C side (0xF8). First-frame evidence (return code,
|
|
returned AddrPC, SymFunctionTableAccess64 result) is produced unconditionally, and every return
|
|
prints a "[diag] walk: return reason=... frames=..." line. The whole body is under
|
|
`if TRACEBACK_USE_DBGHELP != 0`, so with the switch at 0 no dbghelp symbol is emitted.
|
|
"""
|
|
# A short diagnostic precedes every return so "was walk called, and which exit" is always
|
|
# visible. reason values:
|
|
# null_state global _State missing
|
|
# disabled compile-time switch TRACEBACK_USE_DBGHELP == 0
|
|
# null_frame_buf frame buffer missing (Install-time calloc failed)
|
|
# null_context CONTEXT pointer null (bad caller argument)
|
|
# stackwalk_first_failed first StackWalk64 returned 0 (cause in the first-frame lines)
|
|
# stackwalk_failed a later StackWalk64 returned 0 (normal top of stack)
|
|
# addrpc_zero StackWalk64 succeeded but AddrPC == 0
|
|
# frame_limit reached TRACEBACK_MAX_FRAMES
|
|
#
|
|
# The bare `_State` null-check below MUST be the first statement: the attribute layer
|
|
# registers `_State` per function before it can resolve its struct type; otherwise every
|
|
# `_State.<field>` folds to null (header section 11 A).
|
|
if _State == None:
|
|
if DiagEnabled() != 0:
|
|
EmitText(" [diag] walk: return reason=null_state frames=0\n")
|
|
return 0
|
|
if TRACEBACK_USE_DBGHELP == 0:
|
|
if DiagEnabled() != 0:
|
|
EmitText(" [diag] walk: return reason=disabled (TRACEBACK_USE_DBGHELP=0) frames=0\n")
|
|
return 0
|
|
# The frame buffer goes through a local, the most reliable channel, pinning the riskiest
|
|
# memset to an already-checked value.
|
|
FBuf: VOIDPTR = _State.FrameBuf
|
|
if FBuf == None:
|
|
if DiagEnabled() != 0:
|
|
EmitText(" [diag] walk: return reason=null_frame_buf frames=0\n")
|
|
return 0
|
|
if CtxPtr == None:
|
|
if DiagEnabled() != 0:
|
|
EmitText(" [diag] walk: return reason=null_context frames=0\n")
|
|
return 0
|
|
Frames: int = 0
|
|
Reason: str = "frame_limit"
|
|
if TRACEBACK_USE_DBGHELP != 0:
|
|
string.memset(FBuf, 0, dbg.STACKFRAME64.__sizeof__())
|
|
ctx: wp.CONTEXT* = (wp.CONTEXT*)(CtxPtr)
|
|
Pc: dbg.ADDRESS64* = (dbg.ADDRESS64*)(FBuf)
|
|
Fm: dbg.ADDRESS64* = (dbg.ADDRESS64*)(FBuf + 32)
|
|
St: dbg.ADDRESS64* = (dbg.ADDRESS64*)(FBuf + 48)
|
|
Pc.Offset = ctx.Rip
|
|
Pc.Mode = uint32(dbg.ADDR_MODE_FLAT)
|
|
Fm.Offset = ctx.Rbp
|
|
Fm.Mode = uint32(dbg.ADDR_MODE_FLAT)
|
|
St.Offset = ctx.Rsp
|
|
St.Mode = uint32(dbg.ADDR_MODE_FLAT)
|
|
i: int = 0
|
|
while i < TRACEBACK_MAX_FRAMES:
|
|
# Read before the call: StackWalk64 rewrites STACKFRAME64 in place.
|
|
InRip: uint64 = uint64(Pc.Offset)
|
|
InRsp: uint64 = uint64(St.Offset)
|
|
InRbp: uint64 = uint64(Fm.Offset)
|
|
Ok: BOOL = dbg.StackWalk64(uint32(dbg.IMAGE_FILE_MACHINE_AMD64), hProcess, hThread, t.CPtr(FBuf),
|
|
CtxPtr, ReadMemCb, _State.FuncTableAddr, _State.ModuleBaseAddr, None)
|
|
Cur: uint64 = uint64(Pc.Offset)
|
|
if i == 0 and DiagEnabled() != 0:
|
|
# First-frame evidence (before any failure test): return code + returned AddrPC +
|
|
# the SymFunctionTableAccess64(AddrPC) result; together they classify the "0
|
|
# frames" cause (modinited / read-memory failure / context).
|
|
Fta: uint64 = 0
|
|
if Cur != 0:
|
|
Fta = uint64(dbg.SymFunctionTableAccess64(hProcess, Cur))
|
|
D0: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
|
" [diag] StackWalk64 first: ret=%d addrpc=0x%llx fta=0x%llx\n", int(Ok), Cur, Fta)
|
|
EmitLine(D0)
|
|
D1: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
|
" [diag] StackWalk64 in: machine=0x%x rip=0x%llx rsp=0x%llx rbp=0x%llx\n",
|
|
uint32(dbg.IMAGE_FILE_MACHINE_AMD64), InRip, InRsp, InRbp)
|
|
EmitLine(D1)
|
|
if Ok == 0:
|
|
if i == 0:
|
|
Reason = "stackwalk_first_failed"
|
|
else:
|
|
Reason = "stackwalk_failed"
|
|
if DiagEnabled() != 0:
|
|
D2: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
|
" [diag] StackWalk64 returned 0 at frame %d (addrpc=0x%llx), stop\n", i, Cur)
|
|
EmitLine(D2)
|
|
if _State.ReadMemFailed != 0 and DiagEnabled() != 0:
|
|
# Right after the first-frame code, print the read-memory failure (one "0
|
|
# frames" cause: the callback returned 0 and StackWalk64 stopped).
|
|
DR: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
|
" [diag] readmem failed addr=0x%llx size=%u GetLastError=%u\n",
|
|
_State.ReadMemAddr, _State.ReadMemSize, _State.ReadMemErr)
|
|
EmitLine(DR)
|
|
break
|
|
if Cur == 0:
|
|
Reason = "addrpc_zero"
|
|
if DiagEnabled() != 0:
|
|
D3: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
|
" [diag] StackWalk64 frame %d addrpc=0, stop\n", i)
|
|
EmitLine(D3)
|
|
break
|
|
FramePush(Cur, 0)
|
|
i += 1
|
|
Frames = i
|
|
# Unified "leave walk" diagnostic covering every break and the normal end; Reason is set.
|
|
if DiagEnabled() != 0:
|
|
N: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
|
" [diag] walk: return reason=%s frames=%d\n", Reason, Frames)
|
|
EmitLine(N)
|
|
return Frames
|
|
|
|
|
|
def WalkRbpChain(Rip: uint64, Rbp: uint64, Base: uint64) -> int:
|
|
"""Primary walk path: collect RIP, then follow the RBP chain reading [rbp] (caller rbp) and
|
|
[rbp+8] (return address) frame by frame.
|
|
|
|
While TRACEBACK_DIAG is enabled every iteration — including a rejected step — first emits two
|
|
ASCII `[diag] rbp:` lines:
|
|
line 1: `frame=<i> rbp=0x... [rbp]=0x... [rbp+8]=0x...` (the raw 8-byte values)
|
|
line 2: `frame=<i> rpm_ok=<0/1> rpm_err=<GetLastError> accept=<0/1> reject_reason=...`
|
|
This makes "why the chain broke at step 1" directly readable (read failure / value not a
|
|
return address / rejected). `reject_reason` is one of: rbp_zero / not_aligned / rpm_failed /
|
|
ret_zero / ret_out_of_module / not_growing / step_too_large; an accepted step prints accept=1.
|
|
Only an accepted step calls FramePush (authoritative frame); rejected values go to diagnostics
|
|
only, never into the stack.
|
|
|
|
* Zero dbghelp dependency (usable even where another component owns DbgHelp's process context).
|
|
* All memory reads go through ReadProcessMemory, never dereferencing a wild rbp: a diagnostic
|
|
must not turn a hang into a crash.
|
|
* A step is accepted only if rbp is non-zero, 8-byte aligned, the read succeeds, [rbp+8] is a
|
|
non-zero address inside the main module [Base, Base+TRACEBACK_MODULE_MAX), the caller rbp
|
|
grows strictly ([rbp] > rbp), the step size <= TRACEBACK_RBP_MAX_STEP, and the frame limit is
|
|
not reached. (Base == 0 skips the range check.)
|
|
|
|
The bare `_State` null-check below is REQUIRED: this function builds diagnostics into
|
|
`_State.LineBuf` (header section 11 A).
|
|
"""
|
|
if _State == None:
|
|
return 0
|
|
FramePush(Rip, 0)
|
|
Cur: uint64 = Rbp
|
|
i: int = 1
|
|
HProc: HANDLE = wp.GetCurrentProcess()
|
|
Two: t.CArray[uint64, 2] # [0] = [rbp] (caller rbp), [1] = [rbp+8] (return address)
|
|
while i < TRACEBACK_MAX_FRAMES:
|
|
# Raw readings + decision for this step, produced before any early reject.
|
|
Rd: sizet = 0
|
|
RpmOk: int = 0
|
|
RpmErr: ULONG = 0
|
|
V0: uint64 = 0 # [rbp] caller rbp
|
|
V1: uint64 = 0 # [rbp+8] return address
|
|
Reason: str = "accept"
|
|
Reject: int = 0 # 0 = accept this step, 1 = reject (reason in Reason)
|
|
if Cur == 0:
|
|
Reason = "rbp_zero"
|
|
Reject = 1
|
|
elif (Cur & uint64(7)) != 0:
|
|
Reason = "not_aligned"
|
|
Reject = 1
|
|
else:
|
|
# Read 16 bytes via ReadProcessMemory (invalid/unreadable addresses return 0 without
|
|
# faulting); dereferencing a wild rbp directly would crash, which the primary path
|
|
# must never do.
|
|
OkRead: BOOL = wm.ReadProcessMemory(HProc, t.CPtr(Cur), &Two, sizet(16), &Rd)
|
|
if OkRead == 0 or sizet(Rd) < sizet(16):
|
|
# GetLastError must be read immediately after ReadProcessMemory.
|
|
RpmErr = GetLastError()
|
|
Reason = "rpm_failed"
|
|
Reject = 1
|
|
else:
|
|
RpmOk = 1
|
|
V0 = Two[0]
|
|
V1 = Two[1]
|
|
if V1 == 0:
|
|
Reason = "ret_zero"
|
|
Reject = 1
|
|
elif Base != 0:
|
|
if V1 < Base or V1 >= Base + uint64(TRACEBACK_MODULE_MAX):
|
|
# Not a return address (below the base / outside the image) => this rbp is
|
|
# not a frame pointer, so the chain breaks here.
|
|
Reason = "ret_out_of_module"
|
|
Reject = 1
|
|
if Reject == 0:
|
|
if V0 <= Cur:
|
|
Reason = "not_growing"
|
|
Reject = 1
|
|
elif V0 - Cur > uint64(TRACEBACK_RBP_MAX_STEP):
|
|
Reason = "step_too_large"
|
|
Reject = 1
|
|
if DiagEnabled() != 0:
|
|
D0: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
|
" [diag] rbp: frame=%d rbp=0x%llx [rbp]=0x%llx [rbp+8]=0x%llx\n", i, Cur, V0, V1)
|
|
EmitLine(D0)
|
|
Acc: int = 1
|
|
if Reject != 0:
|
|
Acc = 0
|
|
if DiagEnabled() != 0:
|
|
D1: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
|
" [diag] rbp: frame=%d rpm_ok=%d rpm_err=%u accept=%d reject_reason=%s\n",
|
|
i, RpmOk, RpmErr, Acc, Reason)
|
|
EmitLine(D1)
|
|
if Reject != 0:
|
|
break
|
|
# Accepted: collect the authoritative frame, then keep walking up.
|
|
FramePush(V1, 0)
|
|
i += 1
|
|
Cur = V0
|
|
return i
|
|
|
|
|
|
def ScanStack(StartIndex: int, Rsp: uint64, Base: uint64) -> int:
|
|
"""Second fallback: stack scan (after the RBP chain; no RBP / frame-pointer dependency).
|
|
|
|
Needed when the build kept no frame pointer, so [rbp+8] is not a return address and the RBP
|
|
chain breaks at step 1; then the only way on is scanning the used stack directly.
|
|
|
|
Read-only, never dereferencing main-thread memory:
|
|
1. walk [Rsp, Rsp+TRACEBACK_SCAN_MAX_BYTES) in 8-byte steps towards higher addresses (caller
|
|
direction), so candidates come out in ascending stack order;
|
|
2. one ReadProcessMemory per chunk (TRACEBACK_SCAN_CHUNK_BYTES = 4 KiB, a local array, the
|
|
callback thread allocates nothing) instead of one per 8 bytes; a failed chunk stops the
|
|
scan and prints address/GetLastError (only with TRACEBACK_DIAG on);
|
|
3. a candidate word W lies in [Base, Base+TRACEBACK_MODULE_MAX) and a neighbouring 8-byte word
|
|
([A-8], the saved rbp when frame pointers are kept, or [A+8]) looks like a stack address
|
|
in [Rsp, upper); either is accepted, covering both frame layouts;
|
|
4. deduplicate by candidate value, capped at TRACEBACK_SCAN_MAX_HITS (scan_candidates=<N>);
|
|
5. collect via FramePush(..., Scanned=1); EmitFrame later marks them with " [inferred]".
|
|
Returns the number of inferred frames collected (0 = none).
|
|
|
|
The bare `_State` null-check below is REQUIRED (this function uses _State.LineBuf).
|
|
"""
|
|
if _State == None:
|
|
return 0
|
|
if Rsp == 0 or Base == 0:
|
|
# No RSP or no module base: skip to avoid a range of false positives; give a greppable
|
|
# reason when diagnostics are on.
|
|
if DiagEnabled() != 0:
|
|
if Rsp == 0:
|
|
EmitText(" [diag] scan: skipped reason=no_rsp\n")
|
|
else:
|
|
EmitText(" [diag] scan: skipped reason=no_base\n")
|
|
return 0
|
|
HProc: HANDLE = wp.GetCurrentProcess()
|
|
Upper: uint64 = Rsp + uint64(TRACEBACK_SCAN_MAX_BYTES)
|
|
ModEnd: uint64 = Base + uint64(TRACEBACK_MODULE_MAX)
|
|
Words: t.CArray[uint64, TRACEBACK_SCAN_CHUNK_WORDS] # 4 KiB chunk buffer (local)
|
|
Found: t.CArray[uint64, TRACEBACK_SCAN_MAX_HITS] # deduplicated candidate return addresses
|
|
Hits: int = 0
|
|
Prev: uint64 = 0 # [A-8]: previous word unknown at chunk start, 0 (one less confirmation)
|
|
Addr: uint64 = Rsp
|
|
Stop: str = "reached_scan_limit"
|
|
if DiagEnabled() != 0:
|
|
D0: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
|
" [diag] scan: rsp=0x%llx upper=0x%llx chunk=%d hits_max=%d\n",
|
|
Rsp, Upper, TRACEBACK_SCAN_CHUNK_BYTES, TRACEBACK_SCAN_MAX_HITS)
|
|
EmitLine(D0)
|
|
while Addr < Upper:
|
|
Rd: sizet = 0
|
|
OkRead: BOOL = wm.ReadProcessMemory(HProc, t.CPtr(Addr), &Words, sizet(TRACEBACK_SCAN_CHUNK_BYTES), &Rd)
|
|
N: int = int(sizet(Rd) / sizet(8)) # u64 words actually read in this chunk
|
|
if OkRead == 0 or N <= 0:
|
|
Er: ULONG = GetLastError()
|
|
if DiagEnabled() != 0:
|
|
D1: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
|
" [diag] scan: block read failed addr=0x%llx err=%u, stop\n", Addr, Er)
|
|
EmitLine(D1)
|
|
Stop = "block_read_failed"
|
|
break
|
|
for k in range(N):
|
|
W: uint64 = Words[int(k)]
|
|
if W >= Base and W < ModEnd:
|
|
# A neighbouring 8-byte word (A-8 or A+8) looks like a stack address: accept W.
|
|
NearStack: int = 0
|
|
if Prev >= Rsp and Prev < Upper:
|
|
NearStack = 1
|
|
if k + 1 < N:
|
|
Q: uint64 = Words[int(k + 1)]
|
|
if Q >= Rsp and Q < Upper:
|
|
NearStack = 1
|
|
if NearStack != 0 and Hits < TRACEBACK_SCAN_MAX_HITS:
|
|
# Deduplicate by candidate value (discovered in ascending stack order, so the
|
|
# order is kept).
|
|
Dup: int = 0
|
|
for t_i in range(Hits):
|
|
if Found[t_i] == W:
|
|
Dup = 1
|
|
break
|
|
if Dup == 0:
|
|
Found[Hits] = W
|
|
Hits += 1
|
|
Prev = W
|
|
Addr = Addr + uint64(TRACEBACK_SCAN_CHUNK_BYTES)
|
|
if DiagEnabled() != 0:
|
|
D2: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
|
" [diag] scan: scan_candidates=%d stop=%s\n", Hits, Stop)
|
|
EmitLine(D2)
|
|
# Collect the inferred frames, capped by the global frame limit (authoritative frames already
|
|
# used StartIndex slots).
|
|
Lim: int = TRACEBACK_MAX_FRAMES - StartIndex
|
|
j: int = 0
|
|
while j < Hits:
|
|
if j >= Lim:
|
|
break
|
|
FramePush(Found[j], 1)
|
|
j += 1
|
|
return j
|
|
|
|
|
|
# ============================================================
|
|
# Shared walk/print helpers (Ctrl+C dump + crash dump)
|
|
# ============================================================
|
|
|
|
def DbgHelpUsable() -> int:
|
|
"""1 = StackWalk64 may be used right now, 0 = it may not (single source of truth for both dumps).
|
|
|
|
True only when the compile-time switch is on, Install's SymGetModuleBase64 probe passed
|
|
(SymReady), and both cross-module callbacks plus the STACKFRAME64 buffer are in place. The
|
|
reason for a 0 is printed by DumpMainThread's symwalk lines, never inferred from here.
|
|
|
|
The bare `_State` null-check below is REQUIRED (this function reads _State fields).
|
|
"""
|
|
if _State == None:
|
|
return 0
|
|
if TRACEBACK_USE_DBGHELP == 0:
|
|
return 0
|
|
if _State.SymReady == 0:
|
|
return 0
|
|
if _State.FuncTableAddr == None or _State.ModuleBaseAddr == None or _State.FrameBuf == None:
|
|
return 0
|
|
return 1
|
|
|
|
|
|
def PrintFrames(Base: uint64):
|
|
"""Print the buffered frames outermost-first (Python "most recent call last") + the frame count.
|
|
|
|
Both walk paths collect frames innermost-first into _State.FrameAddr, so the reverse loop prints
|
|
the outermost/CRT frame first and the interrupted RIP last. The count is read from
|
|
_State.FrameCount (frames actually buffered), never from a caller-side tally.
|
|
|
|
The bare `_State` null-check below is REQUIRED (this function uses _State.LineBuf).
|
|
"""
|
|
if _State == None or _State.LineBuf == None:
|
|
return
|
|
EmitText("Traceback (most recent call last):\n")
|
|
j: int = _State.FrameCount - 1
|
|
while j >= 0:
|
|
EmitFrame(_State.FrameAddr[j], Base, int(_State.FrameScan[j]))
|
|
j -= 1
|
|
N: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
|
" frames=%d\n", _State.FrameCount)
|
|
EmitLine(N)
|
|
|
|
|
|
# ============================================================
|
|
# Dump the main thread
|
|
# ============================================================
|
|
|
|
def WriteDumpHeaderFile(IsBreak: int):
|
|
"""Append a separator header to the dump file (file only, not the console).
|
|
|
|
The file accumulates many dumps, so each section gets a small greppable header: local
|
|
timestamp / pid / trigger key / main thread id / Seq. The labels are Chinese on purpose (the
|
|
file is UTF-8, unaffected by the console code page, unlike the always-ASCII console text).
|
|
"""
|
|
if _State == None or _DumpFile == None or _DumpFile == INVALID_HANDLE_VALUE:
|
|
return
|
|
St: SYSTEMTIME = SYSTEMTIME()
|
|
GetLocalTime(&St)
|
|
Trig: str = "Ctrl+C"
|
|
if IsBreak != 0:
|
|
Trig = "Ctrl+Break"
|
|
N: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
|
"\n===== 栈转储 %04u-%02u-%02u %02u:%02u:%02u.%03u pid=%u 触发=%s 主线程id=%u Seq=%d =====\n",
|
|
uint32(St.wYear), uint32(St.wMonth), uint32(St.wDay),
|
|
uint32(St.wHour), uint32(St.wMinute), uint32(St.wSecond),
|
|
uint32(St.wMilliseconds), wp.GetCurrentProcessId(),
|
|
Trig, _State.MainThreadId, _State.Seq)
|
|
if N <= 0:
|
|
return
|
|
if N > TRACEBACK_LINE_CAP - 1:
|
|
N = TRACEBACK_LINE_CAP - 1
|
|
Wr: ULONG = 0
|
|
wf.WriteFile(_DumpFile, _State.LineBuf, ULONG(N), &Wr, None)
|
|
|
|
|
|
def DumpMainThread(IsBreak: int) -> int:
|
|
"""Suspend the main thread -> read CONTEXT -> collect frames -> print -> resume. Returns frames.
|
|
|
|
Invariants:
|
|
* after a successful SuspendThread, every return path must ResumeThread + CloseHandle, or the
|
|
main thread freezes forever (worse than a hang);
|
|
* reentrancy: AtomicTryEnterDump (inline `lock cmpxchg`) test-and-sets _DumpLock; every
|
|
return path calls AtomicLeaveDump; a caller that fails to acquire returns immediately
|
|
printing nothing (it does not suspend the main thread or touch LineBuf);
|
|
* evidence: the "[diag] ..." lines are printed only while TRACEBACK_DIAG is enabled; when it
|
|
is off the dump is a plain Python-style traceback, which is why the diagnostic lines must
|
|
never be interleaved between the frames;
|
|
* frame order: the walk paths collect frames innermost-first into _State.FrameAddr; this
|
|
function prints that buffer in reverse (outermost/CRT first, innermost last).
|
|
"""
|
|
global _DumpOut
|
|
if _State == None or _State.LineBuf == None:
|
|
return 0
|
|
# Reentrancy guard: the SetConsoleCtrlHandler callbacks run on new threads and share the same
|
|
# LineBuf/out handle, so an atomic test-and-set avoids interleaved output from a race.
|
|
EnterOld: int = AtomicTryEnterDump()
|
|
if EnterOld != 0:
|
|
return 0
|
|
_State.Dumping = 1
|
|
_State.Seq = _State.Seq + 1
|
|
# The output handle is fixed and locked at dump start; only _DumpOut is written afterwards.
|
|
_DumpOut = _State.OutHandle
|
|
# Reset the per-dump flags: anti-spam flags plus the collected-frame count.
|
|
_State.ReadMemFailed = 0
|
|
_State.MissReported = 0
|
|
_State.FrameCount = 0
|
|
EmitText("=== TPC stack dump (Ctrl+Break: keep running / Ctrl+C: exit) ===\n")
|
|
# File-side separator header (file only: timestamp / pid / trigger / main thread id).
|
|
WriteDumpHeaderFile(int(IsBreak))
|
|
if _DumpOut == None or _DumpOut == INVALID_HANDLE_VALUE:
|
|
EmitText(" WARN: no usable console handle (stderr/stdout invalid), console output discarded\n")
|
|
# ---- Pre-diagnostics (evidence before decisions), obtained without suspending ----
|
|
# Emitted only while TRACEBACK_DIAG is enabled; all fields are ASCII.
|
|
MBase: uint64 = uint64(GetModuleHandleA(None))
|
|
ModBaseRet: uint64 = 0
|
|
if TRACEBACK_USE_DBGHELP != 0:
|
|
if _State.SymReady != 0:
|
|
ModBaseRet = uint64(dbg.SymGetModuleBase64(wp.GetCurrentProcess(), MBase))
|
|
FB: int = 1 if _State.FrameBuf != None else 0
|
|
FT: int = 1 if _State.FuncTableAddr != None else 0
|
|
MB: int = 1 if _State.ModuleBaseAddr != None else 0
|
|
N: int = 0
|
|
if DiagEnabled() != 0:
|
|
N = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
|
" [diag] Seq=%d cbthread=%u mainthread=%u dump_enter_prev=%d "
|
|
"mapok=%d symready=%d modinited=%d\n",
|
|
_State.Seq, wp.GetCurrentThreadId(), _State.MainThreadId, EnterOld,
|
|
_State.MapOk, _State.SymReady, _State.ModInited)
|
|
EmitLine(N)
|
|
N = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
|
" [diag] modbase=0x%llx SymGetModuleBase64(main)=0x%llx\n", MBase, ModBaseRet)
|
|
EmitLine(N)
|
|
N = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
|
" [diag] frame_buf=%d cb_fta=%d cb_mba=%d\n", FB, FT, MB)
|
|
EmitLine(N)
|
|
Trigger: str = "Ctrl+C"
|
|
AfterTag: str = "exit"
|
|
if IsBreak != 0:
|
|
Trigger = "Ctrl+Break"
|
|
AfterTag = "continue"
|
|
N = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
|
" Trigger=%s after_dump=%s mainthread=%u cbthread=%u\n",
|
|
Trigger, AfterTag, _State.MainThreadId, wp.GetCurrentThreadId())
|
|
EmitLine(N)
|
|
hThread: HANDLE = wp.OpenThread(ULONG(wp.THREAD_SUSPEND_RESUME | wp.THREAD_GET_CONTEXT), 0, _State.MainThreadId)
|
|
if hThread == None or hThread == INVALID_HANDLE_VALUE:
|
|
N = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
|
" fallback: OpenThread(main) failed (GetLastError=%u), cannot dump stack\n",
|
|
GetLastError())
|
|
EmitLine(N)
|
|
AtomicLeaveDump()
|
|
return 0
|
|
if wp.SuspendThread(hThread) == ULONG(TRACEBACK_SUSPEND_FAILED):
|
|
N = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
|
" fallback: SuspendThread(main) failed (GetLastError=%u), cannot dump stack\n",
|
|
GetLastError())
|
|
EmitLine(N)
|
|
CloseHandle(hThread)
|
|
AtomicLeaveDump()
|
|
return 0
|
|
# CONTEXT: zero it first, then set ContextFlags (x64 needs the CONTEXT_AMD64 bit).
|
|
string.memset(_State.ContextBuf, 0, wp.CONTEXT.__sizeof__())
|
|
ctx: wp.CONTEXT* = (wp.CONTEXT*)(_State.ContextBuf)
|
|
ctx.ContextFlags = uint32(wp.CONTEXT_FULL)
|
|
if wp.GetThreadContext(hThread, t.CPtr(_State.ContextBuf)) == 0:
|
|
N = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
|
" fallback: GetThreadContext failed (GetLastError=%u), cannot dump stack\n",
|
|
GetLastError())
|
|
EmitLine(N)
|
|
wp.ResumeThread(hThread)
|
|
CloseHandle(hThread)
|
|
AtomicLeaveDump()
|
|
return 0
|
|
Base: uint64 = uint64(GetModuleHandleA(None))
|
|
Rip: uint64 = uint64(ctx.Rip)
|
|
Rsp: uint64 = uint64(ctx.Rsp)
|
|
Rbp: uint64 = uint64(ctx.Rbp)
|
|
# ---- dbghelp usability (decided at Install from evidence, never assumed here) ----
|
|
# An ASan runtime registers its own DbgHelp symbolizer, but DbgHelp's process context is
|
|
# process-global and shared, so its presence is informational only: Install refreshes the
|
|
# shared module table and keeps SymReady=1 ONLY when the SymGetModuleBase64(main module) probe
|
|
# succeeded. This walk matters because the RBP chain cannot cross a frameless system frame -
|
|
# the Ctrl+C case: the main thread is blocked inside a system DLL with RBP=0, so [rbp] is not
|
|
# a frame pointer and the chain dies at step 1. StackWalk64 unwinds through the PE unwind
|
|
# tables (.pdata) of both the system DLLs and the .vp functions, which is the only path that
|
|
# can reconstruct the real .vp stack from such a context.
|
|
AsanPresent: int = 0
|
|
if GetModuleHandleA("clang_rt.asan_dynamic-x86_64.dll") != None:
|
|
AsanPresent = 1
|
|
if GetModuleHandleA("libclang_rt.asan_dynamic-x86_64.dll") != None:
|
|
AsanPresent = 1
|
|
DbgUsable: int = DbgHelpUsable()
|
|
if DiagEnabled() != 0:
|
|
N = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
|
" [diag] symwalk: use_dbghelp=%d symready=%d asan=%d dbg_usable=%d\n",
|
|
TRACEBACK_USE_DBGHELP, _State.SymReady, AsanPresent, DbgUsable)
|
|
EmitLine(N)
|
|
if DbgUsable == 0:
|
|
# Each degradation gives an explicit reason (never silent 0 frames), most specific first.
|
|
if TRACEBACK_USE_DBGHELP == 0:
|
|
EmitText(" [diag] symwalk=disabled reason=compile_time_switch"
|
|
" (TRACEBACK_USE_DBGHELP=0)\n")
|
|
elif _State.SymReady == 0:
|
|
EmitText(" [diag] symwalk=disabled reason=syminitialize_failed_or_main_module_missing\n")
|
|
elif _State.FuncTableAddr == None or _State.ModuleBaseAddr == None:
|
|
EmitText(" [diag] symwalk=disabled reason=callback_missing\n")
|
|
else:
|
|
EmitText(" [diag] symwalk=disabled reason=null_frame_buf\n")
|
|
MapTag: str = "none (see [traceback] warnings above; addresses only)"
|
|
if _State.MapOk != 0:
|
|
MapTag = "loaded"
|
|
# Primary path is the RBP chain (no dbghelp); dbghelp is optional enhancement only.
|
|
SymTag: str = "rbp-chain primary (dbghelp not required)"
|
|
if DbgUsable != 0:
|
|
SymTag = "rbp-chain primary, dbghelp optional"
|
|
N = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
|
" exe base=0x%llx RIP=0x%llx RSP=0x%llx RBP=0x%llx\n",
|
|
Base, Rip, Rsp, Rbp)
|
|
EmitLine(N)
|
|
if DiagEnabled() != 0:
|
|
N = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
|
" srcline_map=%s symwalk=%s\n", MapTag, SymTag)
|
|
EmitLine(N)
|
|
Frames: int = 0
|
|
# "enter walk" evidence (only with diagnostics on); if it is missing after a key press while
|
|
# TRACEBACK_DIAG=1, DumpMainThread returned earlier at one of the three fallbacks above.
|
|
if DiagEnabled() != 0:
|
|
N = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
|
" [diag] walk: enter use_dbghelp=%d symready=%d modinited=%d "
|
|
"frame_buf=%d mapok=%d asan=%d\n",
|
|
TRACEBACK_USE_DBGHELP, _State.SymReady, _State.ModInited, FB, _State.MapOk,
|
|
AsanPresent)
|
|
EmitLine(N)
|
|
if DbgUsable == 0:
|
|
# Keep a "[diag] StackWalk64 first:" line before the RBP chain (here ret=not-called, with
|
|
# the prerequisites); a real call would emit it from inside WalkViaDbgHelp.
|
|
DNC: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
|
" [diag] StackWalk64 first: ret=not-called symready=%d cb_fta=%d cb_mba=%d frame_buf=%d\n",
|
|
_State.SymReady, FT, MB, FB)
|
|
EmitLine(DNC)
|
|
EmitText(" [diag] walk: primary=rbp_chain\n")
|
|
# ---- Primary path: RBP chain (no dbghelp) ----
|
|
Frames = WalkRbpChain(Rip, Rbp, Base)
|
|
# RBP-chain yield ALONE decides the fallback gates below. The stack scan must not mask the
|
|
# (far stronger) dbghelp unwinder by inflating this count first: measured on
|
|
# Test/LLvmLiteTest, RBP was 0 (the main thread was blocked inside a system DLL), the chain
|
|
# produced the single RIP frame, the scan added 1 inferred candidate -> Frames became 2 ->
|
|
# the `Frames <= 1` dbghelp gate below silently skipped StackWalk64, which is the only path
|
|
# able to unwind through the frameless ntdll/kernel32 frames and print the real .vp stack.
|
|
RbpFrames: int = Frames
|
|
if DiagEnabled() != 0:
|
|
N = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
|
" [diag] walk: rbp_chain frames=%d\n", Frames)
|
|
EmitLine(N)
|
|
# ---- Second fallback: stack scan (no RBP / frame pointer) ----
|
|
# Enabled only when the RBP chain got just the current RIP (<=1 frame); scanned frames are
|
|
# marked " [inferred]" and are not mixed with authoritative frames.
|
|
if Frames <= 1:
|
|
if DiagEnabled() != 0:
|
|
EmitText(" [diag] walk: fallback=stack_scan\n")
|
|
Sc: int = ScanStack(Frames, Rsp, Base)
|
|
Frames = Frames + Sc
|
|
if DiagEnabled() != 0:
|
|
N = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
|
" [diag] walk: scan frames=%d total=%d\n", Sc, Frames)
|
|
EmitLine(N)
|
|
# ---- Optional enhancement: only when the RBP chain got <=1 frame and dbghelp is usable ----
|
|
# The outer constant condition is a separate if so the switch folds this block away entirely,
|
|
# leaving no dbghelp link-time symbol.
|
|
if TRACEBACK_USE_DBGHELP != 0:
|
|
if RbpFrames <= 1 and DbgUsable != 0:
|
|
if DiagEnabled() != 0:
|
|
EmitText(" [diag] walk: dbghelp enhancement engaged\n")
|
|
# dbghelp re-walks from the same CONTEXT and its first frame is the same RIP already
|
|
# collected: save ALL frames collected so far (the RIP plus any stack-scan candidates,
|
|
# each already carrying its own origin flag), let dbghelp rebuild the authoritative
|
|
# list from scratch, and restore the saved frames only if dbghelp produced nothing.
|
|
# Why all of them (and not just frame[0]): this block is gated on the RBP-chain yield
|
|
# (see RbpFrames), so it now runs exactly in the cases where the stack scan had
|
|
# contributed candidates — clearing them and restoring only frame[0] would silently
|
|
# shrink the dump whenever dbghelp fails. Locals only: the handler thread allocates
|
|
# nothing (same discipline as ScanStack's chunk buffers).
|
|
SavedCount: int = _State.FrameCount
|
|
SaveAddrBuf: t.CArray[uint64, TRACEBACK_FRAME_BUF]
|
|
SaveScanBuf: t.CArray[int32, TRACEBACK_FRAME_BUF]
|
|
for si in range(SavedCount):
|
|
SaveAddrBuf[si] = _State.FrameAddr[si]
|
|
SaveScanBuf[si] = _State.FrameScan[si]
|
|
_State.FrameCount = 0
|
|
F2: int = WalkViaDbgHelp(wp.GetCurrentProcess(), hThread, _State.ContextBuf, Base)
|
|
if F2 == 0:
|
|
for ri in range(SavedCount):
|
|
if FramePush(SaveAddrBuf[ri], int(SaveScanBuf[ri])) == 0:
|
|
break
|
|
# The count that is actually buffered is authoritative.
|
|
Frames = _State.FrameCount
|
|
if DiagEnabled() != 0:
|
|
# "leave walk" evidence, paired with walk: enter.
|
|
N = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
|
" [diag] walk: leave frames=%d mapok=%d\n", Frames, _State.MapOk)
|
|
EmitLine(N)
|
|
if Frames == 0:
|
|
# Neither the RBP chain, the stack scan, nor any dbghelp enhancement produced a frame.
|
|
EmitText(" [diag] walk: no frames from any path (rbp_chain/stack_scan/dbghelp)\n")
|
|
# ---- Print the collected frames outermost-first (Python traceback order) ----
|
|
PrintFrames(Base)
|
|
# Summary after the whole traceback: dump sequence + dump-file notice.
|
|
if _DumpFile != None and _DumpFile != INVALID_HANDLE_VALUE:
|
|
N = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
|
" [traceback] dump seq=%d also appended to <exe>.stackdump.txt\n", _State.Seq)
|
|
else:
|
|
N = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
|
" [traceback] dump seq=%d console only (dump file unavailable)\n", _State.Seq)
|
|
EmitLine(N)
|
|
EmitText("=== stack dump end ===\n")
|
|
wp.ResumeThread(hThread)
|
|
CloseHandle(hThread)
|
|
AtomicLeaveDump()
|
|
return Frames
|
|
|
|
|
|
# ============================================================
|
|
# Unhandled-exception dump (crash)
|
|
# ============================================================
|
|
|
|
def DumpCrashThread(Info: VOIDPTR) -> int:
|
|
"""SetUnhandledExceptionFilter body: dump the FAULTING thread's stack, then let the process die.
|
|
|
|
A crash otherwise leaves no stack at all: once the ASan runtime is past its reporting window it
|
|
prints nothing, and the OS default is a bare exit code (measured on the same fault: ASan on =
|
|
0x80000003 STATUS_BREAKPOINT, ASan off = 0xC0000005 ACCESS_VIOLATION, both stackless). This path
|
|
reuses the Ctrl+C dump's walk/print machinery on the CONTEXT the OS hands over in
|
|
EXCEPTION_POINTERS.
|
|
|
|
Invariants:
|
|
* runs on the FAULTING thread, so the thread handle is GetCurrentThread() and no
|
|
suspend/resume is needed;
|
|
* allocates nothing: every buffer already exists (same discipline as the console callback);
|
|
* never dereferences the OS pointers directly - EXCEPTION_POINTERS / EXCEPTION_RECORD are read
|
|
through ReadProcessMemory (the callback thread must not turn a crash into a second crash);
|
|
* the OS-owned CONTEXT is **copied** into _State.ContextBuf before walking, because
|
|
StackWalk64 rewrites the record it walks - handing it the exception's own record would
|
|
corrupt the reported crash context;
|
|
* shares AtomicTryEnterDump with the Ctrl+C dump, so only one dump can ever be in flight (a
|
|
crash that happens inside a dump is skipped instead of corrupting the output).
|
|
|
|
Returns 1 = a dump was produced, 0 = nothing dumped (state missing, another dump in flight, or
|
|
the OS records unreadable). The caller returns EXCEPTION_CONTINUE_SEARCH either way, so the
|
|
crash keeps its own exit code and ASan/WER reporting.
|
|
|
|
The bare `_State` null-check below is REQUIRED (this function uses _State.LineBuf).
|
|
"""
|
|
global _DumpOut
|
|
if _State == None or _State.LineBuf == None or _State.Installed == 0:
|
|
return 0
|
|
if AtomicTryEnterDump() != 0:
|
|
return 0
|
|
_State.Dumping = 1
|
|
_State.Seq = _State.Seq + 1
|
|
_DumpOut = _State.OutHandle
|
|
_State.ReadMemFailed = 0
|
|
_State.MissReported = 0
|
|
_State.FrameCount = 0
|
|
EmitText("=== TPC crash dump (unhandled exception; the process keeps terminating) ===\n")
|
|
HProc: HANDLE = wp.GetCurrentProcess()
|
|
Rd: sizet = 0
|
|
Two: t.CArray[uint64, 2] # [0] = EXCEPTION_RECORD*, [1] = CONTEXT* (EXCEPTION_POINTERS)
|
|
Two[0] = 0
|
|
Two[1] = 0
|
|
if wm.ReadProcessMemory(HProc, t.CPtr(Info), &Two, sizet(16), &Rd) == 0 \
|
|
or sizet(Rd) < sizet(16):
|
|
EmitText(" fallback: EXCEPTION_POINTERS unreadable, crash dump skipped\n")
|
|
AtomicLeaveDump()
|
|
return 0
|
|
Rec: uint64 = Two[0]
|
|
CtxPtr: uint64 = Two[1]
|
|
Code: uint32 = 0
|
|
ExAddr: uint64 = 0
|
|
AvKind: uint32 = 0
|
|
AvAddr: uint64 = 0
|
|
# EXCEPTION_RECORD head (0x30 bytes):
|
|
# u32[0] ExceptionCode, u32[1] ExceptionFlags, u32[2..3] nested record,
|
|
# u32[4..5] ExceptionAddress (offset 0x10), u32[6] NumberParameters (offset 0x18),
|
|
# u32[8] ExceptionInformation[0] (offset 0x20 = 0 read / 1 write / 8 execute),
|
|
# u32[10..11] ExceptionInformation[1] (offset 0x28 = the faulting DATA address).
|
|
# The two ExceptionInformation fields are only filled for access violations (code 0xC0000005);
|
|
# they are printed unconditionally because a hard crash is exactly when raw numbers beat guesses.
|
|
RecWords: t.CArray[uint32, 12]
|
|
RecWords[0] = 0
|
|
if Rec != 0:
|
|
if wm.ReadProcessMemory(HProc, t.CPtr(Rec), &RecWords, sizet(48), &Rd) != 0 \
|
|
and sizet(Rd) >= sizet(48):
|
|
Code = RecWords[0]
|
|
ExAddr = uint64(RecWords[4]) | (uint64(RecWords[5]) << 32)
|
|
AvKind = RecWords[8]
|
|
AvAddr = uint64(RecWords[10]) | (uint64(RecWords[11]) << 32)
|
|
N: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
|
" Trigger=unhandled-exception code=0x%x addr=0x%llx"
|
|
" av_kind=%u av_addr=0x%llx crashthread=%u\n",
|
|
Code, ExAddr, AvKind, AvAddr, wp.GetCurrentThreadId())
|
|
EmitLine(N)
|
|
if CtxPtr == 0:
|
|
EmitText(" fallback: CONTEXT pointer null, crash dump skipped\n")
|
|
AtomicLeaveDump()
|
|
return 0
|
|
if wm.ReadProcessMemory(HProc, t.CPtr(CtxPtr), t.CPtr(_State.ContextBuf),
|
|
wp.CONTEXT.__sizeof__(), &Rd) == 0 \
|
|
or sizet(Rd) < sizet(wp.CONTEXT.__sizeof__()):
|
|
EmitText(" fallback: CONTEXT unreadable, crash dump skipped\n")
|
|
AtomicLeaveDump()
|
|
return 0
|
|
Base: uint64 = uint64(GetModuleHandleA(None))
|
|
ctx: wp.CONTEXT* = (wp.CONTEXT*)(_State.ContextBuf)
|
|
Rip: uint64 = uint64(ctx.Rip)
|
|
Rsp: uint64 = uint64(ctx.Rsp)
|
|
Rbp: uint64 = uint64(ctx.Rbp)
|
|
N = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
|
" exe base=0x%llx RIP=0x%llx RSP=0x%llx RBP=0x%llx\n",
|
|
Base, Rip, Rsp, Rbp)
|
|
EmitLine(N)
|
|
# Fault-site argument registers. On Win64 the first four integer arguments travel in
|
|
# rcx/rdx/r8/r9, so this line shows exactly what the faulting call received (r12 is the
|
|
# register the faulting instruction loaded its pointer from, when the fault is a pointer
|
|
# dereference). Printed for crashes only: the Ctrl+C path has no faulting instruction whose
|
|
# operands matter, and this is the one datum that distinguishes "wrong argument" from
|
|
# "argument right, pointee contents wrong" without another rebuild.
|
|
N = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
|
" regs: rcx=0x%llx rdx=0x%llx r8=0x%llx r9=0x%llx r12=0x%llx\n",
|
|
uint64(ctx.Rcx), uint64(ctx.Rdx), uint64(ctx.R8), uint64(ctx.R9),
|
|
uint64(ctx.R12))
|
|
EmitLine(N)
|
|
# NOTE: [RBP+8] is deliberately NOT read as a "return address". In .vp frames the compiler emits
|
|
# `leaq N(%rsp), %rbp` (RBP = a locals base, N = the spill area size), so the incoming return
|
|
# address sits at RBP + callee_frame_bytes - 8 - a per-function offset (measured on
|
|
# _TypePrintDepth: RBP = rsp+0x70 and the return address is at RBP+0x38, so [RBP+8] is an
|
|
# unrelated local, not the caller). The .stackmap walker is the authority for callers.
|
|
# dbghelp first: a fault usually sits inside a system DLL, where the RBP chain has nothing to
|
|
# follow. The RBP chain and the stack scan stay as fallbacks, exactly as in the Ctrl+C path.
|
|
Frames: int = 0
|
|
if DbgHelpUsable() != 0:
|
|
Frames = WalkViaDbgHelp(HProc, wp.GetCurrentThread(), t.CPtr(_State.ContextBuf), Base)
|
|
if Frames <= 1:
|
|
_State.FrameCount = 0
|
|
Frames = WalkRbpChain(Rip, Rbp, Base)
|
|
if Frames <= 1:
|
|
Frames = Frames + ScanStack(Frames, Rsp, Base)
|
|
PrintFrames(Base)
|
|
if _DumpFile != None and _DumpFile != INVALID_HANDLE_VALUE:
|
|
N = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
|
" [traceback] crash dump seq=%d also appended to <exe>.stackdump.txt\n",
|
|
_State.Seq)
|
|
else:
|
|
N = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
|
" [traceback] crash dump seq=%d console only (dump file unavailable)\n",
|
|
_State.Seq)
|
|
EmitLine(N)
|
|
EmitText("=== crash dump end ===\n")
|
|
AtomicLeaveDump()
|
|
return 1
|
|
|
|
|
|
def CrashHandler(Info: VOIDPTR) -> LONG:
|
|
"""SetUnhandledExceptionFilter callback: dump the faulting thread, then hand the exception back.
|
|
|
|
Always EXCEPTION_CONTINUE_SEARCH: this path only supplies the missing stack, it never swallows
|
|
the crash - the OS/WER handling, the exit code and the ASan report keep their behaviour.
|
|
"""
|
|
DumpCrashThread(Info)
|
|
return LONG(TRACEBACK_EXCEPTION_CONTINUE_SEARCH)
|
|
|
|
|
|
# ============================================================
|
|
# Control handler / install
|
|
# ============================================================
|
|
|
|
def CtrlHandler(CtrlType: ULONG) -> BOOL:
|
|
"""SetConsoleCtrlHandler callback (invoked by the system on a new thread).
|
|
|
|
Ctrl+C -> dump then return FALSE (default handling resumes => process exits);
|
|
Ctrl+Break -> dump then return TRUE (handled => process continues);
|
|
any other control event -> no dump, return FALSE (default behaviour untouched).
|
|
Thread-safe: only reads global state and writes reserved buffers, allocates nothing.
|
|
"""
|
|
if _State == None or _State.Installed == 0:
|
|
return 0
|
|
IsBreak: int = 0
|
|
if CtrlType == ULONG(wc.CTRL_BREAK_EVENT):
|
|
IsBreak = 1
|
|
elif CtrlType != ULONG(wc.CTRL_C_EVENT):
|
|
return 0
|
|
DumpMainThread(int(IsBreak))
|
|
if IsBreak != 0:
|
|
return 1
|
|
return 0
|
|
|
|
|
|
def NoteError(Text: str, Code: ULONG):
|
|
"""Install/degradation diagnostic: one '<Text>(GetLastError=%u)' line to the dump sink."""
|
|
if _State == None or _State.LineBuf == None:
|
|
return
|
|
N: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
|
"%s(GetLastError=%u)\n", Text, Code)
|
|
EmitLine(N)
|
|
|
|
|
|
def Install() -> int:
|
|
"""Register the console control handler + the unhandled-exception filter.
|
|
1 = registered (possibly degraded), 0 = not installed.
|
|
|
|
0 has two cases, both diagnosed (a missing sidecar is the expected "off" state, returned
|
|
silently): <exe>.stackmap absent (--traceback not used) -> silent 0, zero side effects; or
|
|
the sidecar exists but open/read/parse fails, or a key buffer cannot be allocated -> warning
|
|
then 0. Idempotent: once attempted (success or not) it returns the previous result without
|
|
re-allocating, so a retry cannot leak the state object or buffers. Installed = 1 is set only
|
|
after both handlers are armed, so a Ctrl+C or a crash cannot arrive half-armed.
|
|
"""
|
|
global _State, _DumpOut, _HandlerReg, _DumpFile
|
|
if _State != None:
|
|
return int(_State.Installed)
|
|
# (1) <exe full path> + ".stackmap"; _State does not exist yet, so failures go through
|
|
# EmitEarly (the dump's own sink), never stdio.printf, to avoid stdout/stderr interleaving.
|
|
Pb: bytes = stdlib.malloc(TRACEBACK_PATH_CAP)
|
|
if Pb == None:
|
|
EmitEarly("[traceback] WARN: path buffer alloc failed, ctrl handler not installed\n")
|
|
return 0
|
|
Plen: ULONG = wf.GetModuleFileNameA(None, Pb, ULONG(TRACEBACK_PATH_CAP))
|
|
if Plen == 0 or sizet(Plen) >= sizet(TRACEBACK_PATH_CAP):
|
|
stdlib.free(t.CPtr(Pb))
|
|
EmitEarly("[traceback] WARN: GetModuleFileNameA failed, ctrl handler not installed\n")
|
|
return 0
|
|
if sizet(Plen) + 16 > sizet(TRACEBACK_PATH_CAP):
|
|
# 16 bytes of slack covers both ".stackmap" (10) and ".stackdump.txt" (16).
|
|
stdlib.free(t.CPtr(Pb))
|
|
EmitEarly("[traceback] WARN: exe path too long to append suffix safely, ctrl handler not installed\n")
|
|
return 0
|
|
string.strcpy(str(Pb + int(Plen)), ".stackmap")
|
|
# (2) sidecar existence = master switch (absent -> return immediately, no side effects).
|
|
hFile: HANDLE = wf.CreateFileA(Pb, wf.GENERIC_READ, wf.FILE_SHARE_READ, t.CPtr(0), wf.OPEN_EXISTING, wf.FILE_ATTRIBUTE_NORMAL, t.CPtr(0))
|
|
if hFile == INVALID_HANDLE_VALUE:
|
|
Err0: ULONG = GetLastError()
|
|
stdlib.free(t.CPtr(Pb))
|
|
if Err0 != ULONG(ERROR_FILE_NOT_FOUND):
|
|
EmitEarlyCode("[traceback] WARN: cannot open <exe>.stackmap, ctrl handler not installed ", Err0)
|
|
return 0
|
|
# (3) state object + all buffers (the callback thread allocates nothing). The collected-frame
|
|
# arrays (FrameAddr/FrameScan) are inline fields of TracebackState, so the single
|
|
# __sizeof__() allocation below covers them and the memset zeroes FrameCount.
|
|
_State = stdlib.malloc(TracebackState.__sizeof__())
|
|
if _State == None:
|
|
stdlib.free(t.CPtr(Pb))
|
|
CloseHandle(hFile)
|
|
EmitEarly("[traceback] WARN: state alloc failed, ctrl handler not installed\n")
|
|
return 0
|
|
string.memset(_State, 0, TracebackState.__sizeof__())
|
|
_State.LineBuf = stdlib.malloc(TRACEBACK_LINE_CAP)
|
|
_State.ContextBuf = t.CPtr(stdlib.calloc(1, wp.CONTEXT.__sizeof__()))
|
|
_State.FrameBuf = t.CPtr(stdlib.calloc(1, dbg.STACKFRAME64.__sizeof__()))
|
|
_State.OutHandle = wf.GetStdHandle(wf.STD_ERROR_HANDLE)
|
|
if _State.OutHandle == None or _State.OutHandle == INVALID_HANDLE_VALUE:
|
|
_State.OutHandle = wf.GetStdHandle(wf.STD_OUTPUT_HANDLE)
|
|
# The output handle is fixed and locked into _DumpOut here; dumps only use it afterwards.
|
|
_DumpOut = _State.OutHandle
|
|
_State.MainThreadId = wp.GetCurrentThreadId()
|
|
if _State.LineBuf == None or _State.ContextBuf == None or _State.FrameBuf == None:
|
|
# Name the missing buffer now (not later in the callback thread); any missing buffer makes
|
|
# Install return 0. _State exists here, so EmitEarly forwards to EmitText; if LineBuf is
|
|
# the missing one, EmitEarly falls back to a direct WriteFile and the notice is not lost.
|
|
stdlib.free(t.CPtr(Pb))
|
|
CloseHandle(hFile)
|
|
if _State.LineBuf == None:
|
|
EmitEarly("[traceback] WARN: LineBuf alloc failed, ctrl handler not installed\n")
|
|
elif _State.ContextBuf == None:
|
|
EmitEarly("[traceback] WARN: ContextBuf alloc failed, ctrl handler not installed\n")
|
|
elif _State.FrameBuf == None:
|
|
EmitEarly("[traceback] WARN: FrameBuf alloc failed, ctrl handler not installed\n")
|
|
else:
|
|
EmitEarly("[traceback] WARN: runtime buffer alloc failed, ctrl handler not installed\n")
|
|
return 0
|
|
# (3b) open the dump file <exe>.stackdump.txt (append: OPEN_ALWAYS creates it, FILE_APPEND_DATA
|
|
# appends every WriteFile). Opened before loading the sidecar so load-time diagnostics are
|
|
# written too. Raw UTF-8 bytes, unaffected by the console code page; kept open for the
|
|
# whole process lifetime.
|
|
string.strcpy(str(Pb + int(Plen)), ".stackdump.txt")
|
|
_DumpFile = wf.CreateFileA(Pb, TRACEBACK_FILE_APPEND_DATA, wf.FILE_SHARE_READ | wf.FILE_SHARE_WRITE, t.CPtr(0), wf.OPEN_ALWAYS, wf.FILE_ATTRIBUTE_NORMAL, t.CPtr(0))
|
|
if _DumpFile == INVALID_HANDLE_VALUE:
|
|
_DumpFile = None
|
|
EmitText("[traceback] WARN: cannot open stack dump file, console-only output\n")
|
|
else:
|
|
DN: int = stdio.snprintf(_State.LineBuf, sizet(TRACEBACK_LINE_CAP),
|
|
"[traceback] stack dump also written to: %s\n", str(Pb))
|
|
EmitLine(DN)
|
|
# (4) load the sidecar (a failure only disables the map; the handler is still installed so at
|
|
# least addresses are available). LoadMapFile prints the exact validation failure itself.
|
|
Sz: LARGE_INTEGER = LARGE_INTEGER()
|
|
if wf.GetFileSizeEx(hFile, &Sz) == 0:
|
|
NoteError("[traceback] WARN: GetFileSizeEx(<exe>.stackmap) failed, addresses only ", GetLastError())
|
|
else:
|
|
LoadMapFile(hFile, int64(Sz.QuadPart))
|
|
CloseHandle(hFile)
|
|
stdlib.free(t.CPtr(Pb))
|
|
# (5) dbghelp initialization (must happen here: Install runs at startup on the main thread,
|
|
# while the Ctrl+C/Break callbacks run on new threads where dbghelp's module enumeration
|
|
# may be unavailable) + the two cross-module callback addresses, resolved dynamically.
|
|
if TRACEBACK_USE_DBGHELP != 0:
|
|
# SymSetOptions must precede SymInitialize: DEFERRED_LOADS avoids loading symbols/PDBs and
|
|
# NO_PROMPTS avoids an interactive dialog blocking startup.
|
|
dbg.SymSetOptions(uint32(dbg.SYMOPT_DEFERRED_LOADS | dbg.SYMOPT_UNDNAME | dbg.SYMOPT_NO_PROMPTS))
|
|
# fInvadeProcess MUST be 1 (TRUE) so dbghelp enumerates loaded modules into its table; with
|
|
# 0 the table stays empty, SymFunctionTableAccess64 gets no RUNTIME_FUNCTION and
|
|
# SymGetModuleBase64 returns 0, so StackWalk64 fails on the first call and walks no frames.
|
|
if dbg.SymInitialize(wp.GetCurrentProcess(), None, 1) != 0:
|
|
_State.SymReady = 1
|
|
else:
|
|
# SymInitialize fails when another component ALREADY initialized DbgHelp in this
|
|
# process (the ASan runtime registers a symbolizer with the same GetCurrentProcess()
|
|
# pseudo-handle). DbgHelp's process context is process-global and keyed by that handle,
|
|
# so there is no conflict to avoid: refresh the SHARED module table (the documented
|
|
# "refresh after a foreign initialize" call) and let the SymGetModuleBase64 probe below
|
|
# decide usability from evidence instead of assuming the worst.
|
|
ErInit: ULONG = GetLastError()
|
|
if dbg.SymRefreshModuleList(wp.GetCurrentProcess()) != 0:
|
|
_State.SymReady = 1
|
|
EmitText("[traceback] note: DbgHelp already initialized in this process"
|
|
" (SymInitialize refused it); shared module table refreshed,"
|
|
" SymGetModuleBase64 probe decides StackWalk64 usability\n")
|
|
else:
|
|
NoteError("[traceback] WARN: SymInitialize/SymRefreshModuleList failed,"
|
|
" StackWalk64 unavailable ", ErInit)
|
|
# An ASan runtime registers its own DbgHelp symbolizer at startup; that only means the
|
|
# state is shared with us, not that it is unusable. Informational only - the
|
|
# SymGetModuleBase64 probe below is the decision maker.
|
|
HAsan: HANDLE = GetModuleHandleA("clang_rt.asan_dynamic-x86_64.dll")
|
|
if HAsan == None:
|
|
HAsan = GetModuleHandleA("libclang_rt.asan_dynamic-x86_64.dll")
|
|
if HAsan != None:
|
|
EmitText("[traceback] note: ASan runtime detected, it shares DbgHelp with this module;"
|
|
" the module-table probe below decides whether StackWalk64 can walk\n")
|
|
Hdbg: HANDLE = GetModuleHandleA("dbghelp.dll")
|
|
if Hdbg != None and Hdbg != INVALID_HANDLE_VALUE:
|
|
_State.FuncTableAddr = GetProcAddress(Hdbg, "SymFunctionTableAccess64")
|
|
_State.ModuleBaseAddr = GetProcAddress(Hdbg, "SymGetModuleBase64")
|
|
if _State.FuncTableAddr == None or _State.ModuleBaseAddr == None:
|
|
EmitText("[traceback] WARN: StackWalk64 callback addresses incomplete,"
|
|
" dbghelp enhancement unavailable (RBP chain is the primary path)\n")
|
|
# Self-check (the decision maker for dbg_usable): does dbghelp's module table really
|
|
# contain the main module? A 0 here means SymFunctionTableAccess64 gets no
|
|
# RUNTIME_FUNCTION and StackWalk64 fails on its first call, so SymReady is dropped back to
|
|
# 0 (the dump then prints the explicit symwalk=disabled reason instead of a silent 0 frames).
|
|
if _State.SymReady != 0:
|
|
if dbg.SymGetModuleBase64(wp.GetCurrentProcess(), uint64(GetModuleHandleA(None))) != 0:
|
|
_State.ModInited = 1
|
|
else:
|
|
_State.SymReady = 0
|
|
EmitText("[traceback] WARN: dbghelp module table lacks main module (SymGetModuleBase64=0),"
|
|
" StackWalk64 unavailable (RBP chain is the primary path)\n")
|
|
# (6) register last, once all state is ready. _HandlerReg guards "register once" so a repeated
|
|
# callback cannot re-register (and thus cannot repeat the same dump).
|
|
if _HandlerReg == 0:
|
|
if wc.SetConsoleCtrlHandler(CtrlHandler, 1) == 0:
|
|
NoteError("[traceback] WARN: SetConsoleCtrlHandler failed ", GetLastError())
|
|
return 0
|
|
_HandlerReg = 1
|
|
# (7) crash coverage: an unhandled exception has no stack otherwise (see DumpCrashThread). Armed
|
|
# here, before _State.Installed = 1, so a crash cannot arrive while the handler is half-ready.
|
|
# The previous filter's value is deliberately not checked: it is NULL on the first
|
|
# registration (the normal case), and our filter never swallows the exception anyway.
|
|
SetUnhandledExceptionFilter(CrashHandler)
|
|
_State.Installed = 1
|
|
EmitText("[traceback] ready: Ctrl+C prints stack then exits, Ctrl+Break prints then continues,"
|
|
" unhandled exceptions print their own stack\n")
|
|
return 1
|