#!/usr/bin/env python3
"""Pseudo-backtrace from a minidump: walk the faulting thread's stack and report
every qword that points into a loaded module's code (i.e. plausible return
addresses), innermost first."""
import struct, sys

p = sys.argv[1]
d = open(p, "rb").read()
nstreams, dirrva = struct.unpack_from("<II", d, 8)
streams = {}
for i in range(nstreams):
    st, size, rva = struct.unpack_from("<III", d, dirrva + i * 12)
    streams.setdefault(st, []).append((size, rva))


def mstring(rva):
    (ln,) = struct.unpack_from("<I", d, rva)
    return d[rva + 4: rva + 4 + ln].decode("utf-16-le", "replace")


mods = []
size, rva = streams[4][0]
(n,) = struct.unpack_from("<I", d, rva)
off = rva + 4
for i in range(n):
    base, sz, csum, ts, nrva = struct.unpack_from("<QIIII", d, off)
    mods.append((base, sz, mstring(nrva).split("\\")[-1]))
    off += 108
mods.sort()

# faulting thread + exception address
size, rva = streams[6][0]
tid, _ = struct.unpack_from("<II", d, rva)
code, flags, recptr, exc_addr, nparams = struct.unpack_from("<IIQQI", d, rva + 8)


def whose(a):
    for base, sz, name in mods:
        if base <= a < base + sz:
            return name, a - base
    return None, 0


# thread list
size, rva = streams[3][0]
(nthreads,) = struct.unpack_from("<I", d, rva)
off = rva + 4
target = None
for i in range(nthreads):
    t_id, susp, pcls, prio, teb, stk_start, stk_size, stk_rva, ctx_size, ctx_rva = \
        struct.unpack_from("<IIIIQQIIII", d, off)
    if t_id == tid:
        target = (stk_start, stk_size, stk_rva, ctx_size, ctx_rva)
    off += 48

print("faulting thread %#x  exception at %s+%#x" % (tid, *whose(exc_addr)))
if not target:
    print("no stack captured for the faulting thread"); sys.exit(0)
stk_start, stk_size, stk_rva, ctx_size, ctx_rva = target
print("stack %#x..%#x (%d bytes)\n" % (stk_start, stk_start + stk_size, stk_size))

# CONTEXT_AMD64: Rsp at offset 0x98, Rip at 0xF8 (RUNTIME layout)
if ctx_size >= 0x100:
    rsp = struct.unpack_from("<Q", d, ctx_rva + 0x98)[0]
    rip = struct.unpack_from("<Q", d, ctx_rva + 0xF8)[0]
    print("RSP=%#x  RIP=%#x (%s+%#x)\n" % (rsp, rip, *whose(rip)))
else:
    rsp = stk_start

print("=== plausible return addresses (innermost first) ===")
seen, out = set(), []
start = max(rsp - stk_start, 0)
for o in range(int(start), stk_size - 8, 8):
    (v,) = struct.unpack_from("<Q", d, stk_rva + o)
    name, off2 = whose(v)
    if name is None:
        continue
    key = (name, off2)
    if key in seen:
        continue
    seen.add(key)
    out.append((stk_start + o, name, off2))
for i, (sa, name, off2) in enumerate(out[:45]):
    tag = ""
    if "cards" in name.lower():
        tag = "   <-- CardsDLL  ghidra %#x" % (0x180000000 + off2)
    print("  [%2d] %#x  %s+%#x%s" % (i, sa, name, off2, tag))
