diff --git a/CMakeLists.txt b/CMakeLists.txt index 6a26664..2dee8d8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -51,6 +51,19 @@ if(BT_STEAM) endif() set(BT_OPTS /permissive /W0 /wd4996 /EHsc /bigobj /MP) +# FIELD CRASHES MUST BE SYMBOLIZABLE (2026-07-28). +# BTCrashFilter (btl4main.cpp) writes `[crash] ... addr=0x... (btl4+0xNNNN)` +# plus an EBP-chain walk into the tester's day log. Both halves were useless +# in a shipped build: no Release PDB was produced at all, so btl4+0xNNNN could +# not be resolved to a function, and /O2 implies /Oy (frame-pointer omission), +# so the walk itself was unreliable. A tester handed us an address nobody +# could turn into a name -- which is the whole point of the self-report. +# /Zi emit debug info -> a PDB. Does NOT change codegen. +# /Oy- keep EBP as a frame pointer so the stack walk is trustworthy. +# The PDB is ARCHIVED per build (dist/pdb/), never shipped: mkdist only picks +# up .dll next to the exe, so it cannot leak into a zip by accident. +list(APPEND BT_OPTS $<$:/Zi> $<$:/Oy->) + #===========================================================================# # Engine: MUNGA (shared sim/render engine) + MUNGA_L4 (Win32/D3D9 HAL). # The 2007 RP411 Windows engine, carrying our BT render/loader work @@ -424,6 +437,20 @@ target_link_libraries(btl4 PRIVATE # UNRESOLVED tolerates the dead offline-tool factory in mech3.cpp. See docs. target_link_options(btl4 PRIVATE /FORCE) +# ...and emit the PDB the crash filter's offsets are resolved against (see the +# /Zi note by BT_OPTS). /DEBUG turns OFF /OPT:REF and /OPT:ICF by default, which +# would silently bloat the shipped exe with unreferenced code, so put both back: +# the goal is a symbol file BESIDE an otherwise-unchanged Release binary. +# /MAP as well as the PDB, deliberately: the .map is PLAIN TEXT, so a +# `btl4+0xNNNN` from a tester's log can be resolved to a function name with a +# text editor and no debugger installed at all (this machine has no cdb). +# tools/symcrash.py does the lookup. Archive both per build. +target_link_options(btl4 PRIVATE + $<$:/DEBUG> + $<$:/OPT:REF> + $<$:/OPT:ICF> + $<$:/MAP>) + # Steam transport (BT_STEAM=ON only -- the license gate): DELAY-LOADED. # steam_api.dll is only mapped when the first SteamAPI call runs, and every # call site gates on BT_STEAM_NET=1 (btl4main step-4b bring-up; L4NET's diff --git a/tools/symcrash.py b/tools/symcrash.py new file mode 100644 index 0000000..f7b284d --- /dev/null +++ b/tools/symcrash.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +"""symcrash.py -- turn a tester's [crash] line into function names. + +BTCrashFilter (btl4main.cpp) writes module-relative offsets into the day log: + + [crash] UNHANDLED EXCEPTION code=0xc0000005 addr=0xb8260 (btl4+0x8260) ... + [crash] stack: btl4+0x8260 btl4+0xfe7ed 0x763ffcc9 ... + +Those offsets mean nothing without the symbols for the EXACT build that produced +them -- which is why every session header carries `build=4.11. ()` and +why the linker .map is archived per build alongside the PDB. + +Usage: + python tools/symcrash.py "btl4+0x8260" + python tools/symcrash.py (scans for [crash]) + +Resolution is nearest-preceding-symbol, so the answer is the function the +address falls INSIDE, plus the byte offset into it. Addresses outside the +module (kernel32, ntdll) are passed through untouched -- they are not ours. +""" +import re +import sys + + +def load_map(path): + """Return (sorted [(rva, name)], preferred_base).""" + syms = [] + base = None + with open(path, "r", encoding="utf-8", errors="replace") as fh: + for line in fh: + if base is None: + m = re.search(r"Preferred load address is ([0-9A-Fa-f]+)", line) + if m: + base = int(m.group(1), 16) + continue + # " 0001:00000a10 ?Foo@Bar@@QAEXXZ 00401a10 f thing.obj" + m = re.match(r"\s+[0-9A-Fa-f]{4}:[0-9A-Fa-f]{8}\s+(\S+)\s+([0-9A-Fa-f]{8,16})\s", + line) + if m and base is not None: + syms.append((int(m.group(2), 16) - base, m.group(1))) + syms.sort(key=lambda s: s[0]) + return syms, base + + +def resolve(syms, rva): + lo, hi, best = 0, len(syms) - 1, None + while lo <= hi: # nearest preceding symbol + mid = (lo + hi) // 2 + if syms[mid][0] <= rva: + best = syms[mid] + lo = mid + 1 + else: + hi = mid - 1 + return best + + +def main(): + if len(sys.argv) < 3: + sys.exit(__doc__) + syms, base = load_map(sys.argv[1]) + if not syms: + sys.exit("no symbols parsed from %s -- is it a linker .map?" % sys.argv[1]) + print("%d symbols, preferred base 0x%x\n" % (len(syms), base)) + + arg = sys.argv[2] + try: # a log file? + text = open(arg, "r", encoding="utf-8", errors="replace").read() + except OSError: + text = arg # ...no, a literal offset + + for line in text.splitlines() or [text]: + if "btl4+" not in line: + continue + if line.lstrip().startswith("[crash]"): + print(line.strip()) + for off in re.findall(r"btl4\+0x([0-9A-Fa-f]+)", line): + rva = int(off, 16) + hit = resolve(syms, rva) + if hit is None: + print(" btl4+0x%-8x " % rva) + else: + print(" btl4+0x%-8x %s +0x%x" % (rva, hit[1], rva - hit[0])) + print() + + +if __name__ == "__main__": + main()