BTCrashFilter writes `[crash] ... addr=0x... (btl4+0xNNNN)` plus an EBP-chain
walk into the tester's day log, and its own comment claims "we hold the PDB".
We did not. Two independent gaps, both fatal to the point of the thing:
1. No Release PDB was produced AT ALL -- only build/Debug/btl4.pdb, and the
shipped exe embedded no PDB path. btl4+0xNNNN from a tester could not be
turned into a function name by anyone.
2. Release flags were /O2 /Ob2 /DNDEBUG with no /Oy- anywhere, and /O2 implies
/Oy on x86. The walker follows EBP, so the stack it printed was unreliable
even when the faulting address was not.
Net effect: when the Owens crash finally lands we would have received an address
nobody could resolve, and a call chain we could not trust. Cheaper to fix before
tonight's session than to wait for the crash to happen twice.
/Zi emit debug info -> a PDB. Does not change codegen.
/Oy- keep EBP as a frame pointer so the walk is trustworthy.
/DEBUG + /OPT:REF + /OPT:ICF -- /DEBUG turns the last two OFF by default,
which would have quietly bloated the shipped exe with unreferenced
code. The exe grew 512 bytes, the debug directory entry, and nothing else.
Also emitting /MAP, which turned out to matter: this machine has no cdb, and a
tester's operator may not have one either. The .map is plain text, so an offset
can be resolved with a text editor. tools/symcrash.py does it properly --
nearest-preceding-symbol against the archived map, module-external addresses
(ntdll, kernel32) passed through untouched since they are not ours.
Verified end to end rather than assumed, using the BT_CRASHTEST=1 hook that
exists for exactly this:
[crash] addr=0xb8260 (btl4+0x8260) access=1 target=0x0
[crash] stack: btl4+0x8260 btl4+0xfe7ed 0x763ffcc9 ...
btl4+0x8260 -> _WinMain@16 +0x7e0
btl4+0xfe7ed -> __scrt_common_main_seh +0xf8
which is exactly right: BT_CRASHTEST does *(volatile int *)0 = 0 inside WinMain,
called from the CRT entry. Correct stack AND correct names.
The PDB and map are archived per build and never shipped: .gitignore already
covers *.pdb and dist/, and mkdist only picks up .dll next to the exe, so
neither can reach a zip by accident. The session header stamps
build=4.11.<n> (<hash>), so an archived pair matches a tester's log exactly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
88 lines
3.0 KiB
Python
88 lines
3.0 KiB
Python
#!/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.<n> (<hash>)` and
|
|
why the linker .map is archived per build alongside the PDB.
|
|
|
|
Usage:
|
|
python tools/symcrash.py <btl4.map> "btl4+0x8260"
|
|
python tools/symcrash.py <btl4.map> <tester.log> (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 <before first symbol>" % rva)
|
|
else:
|
|
print(" btl4+0x%-8x %s +0x%x" % (rva, hit[1], rva - hit[0]))
|
|
print()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|