Skip to content

Crash Script Diagnostics

BlueShank edited this page Sep 1, 2026 · 2 revisions

Point CRASHCAPTURE_SCRIPT at a .lua file and it runs on every crash/hang in a brand-new, throwaway LuaJIT state, never the game's, which is unreliable mid-crash.
Its print() output goes into the report's Diagnostics section.
The whole thing is fault-isolated, so a mistake in the script just loses that section.
Everything is read-only and bounds-checked.

# example: run myscript.lua on every crash
CRASHCAPTURE_SCRIPT=garrysmod/addons/mything/lua/autorun/myscript.lua

Addresses are opaque light pointers, pass them straight back into mem.*.
An address argument also accepts a plain number.
Reads that fail return nil.

Crash context (crash table)

  • crash.kind: string

    • What happened, e.g. unhandled exception, hang, abort, dump.
  • crash.reason: string

    • The one-line summary (same text as the report's reason).
  • crash.map: string

    • The current map name (e.g. gm_construct).
    • nil if no map was known when the plugin captured it (no heartbeat source or still loading).
  • crash.fault: address

    • The faulting data address (e.g. the bad pointer in an access violation).
    • nil for freezes and faults with no address.
  • crash.pc: address

    • Faulting instruction pointer.
  • crash.sp: address

    • Faulting stack pointer.
  • crash.regs: table

    • The register file as address values (rax...rip on x64, eax...eip on x86).
    • Empty if no context was available.
    • Pass any into mem.*.
  • crash.uptime: number

    • Milliseconds since the plugin armed.
  • crash.pulse: number\

    • Milliseconds since the last heartbeat.
    • nil if there was never a heartbeat.
  • crash.lua: table[]

    • The crashing game's Lua state(s), one entry per bound realm.
    • Each entry is {realm: string, top: number, frames: table[]}, where every frame is {level, source, line, name, what, locals}.
    • locals is a "a=1; self=Entity: 0x..." summary, present only for Lua frames.
    • nil if no realm was readable or the LuaJIT API didn't resolve.
  • crash.stack: table[]

    • The native call stack from the faulting context, as {pc: address, sym: string} entries (outermost frame first).
    • On Linux the first few frames may be the report handler itself (it unwinds from there).

The mem library

  • mem.read(addr: address, type: string): number

    • Reads the following possible types: int8/uint8/int16/uint16/int32/uint32/int64/uint64/float/double/ptr.
    • ptr returns an address, nil if unreadable.
  • mem.string(addr: address, max: number): string

    • Reads a NUL-terminated string (up to max bytes, default 256).
  • mem.bytes(addr: address, n: number): string

    • Up to n raw bytes (default 64, capped at 4096) as a Lua string, stopping at the first unreadable byte.
    • For decoding structs with string.byte / string.unpack.
  • mem.deref(addr: address): address

    • Reads the pointer stored.
  • mem.offset(addr: address, n: number): address

    • Returns addr + n.
  • mem.chain(start: address, ...: number): address

    • Walks a pointer chain.
    • Each intermediate offset is "add then dereference"; the final offset is "add" only, so the result is the address of the last field (read it with mem.read/mem.deref).
    • nil if any hop is unreadable. E.g. mem.chain(crash.regs.rdi, 0x10, 0x28) -> *(rdi+0x10) + 0x28.
  • mem.sym(addr: address): string

    • Returns module+RVA plus a symbol name when debug info is available.
  • mem.find(name: string): address, number

    • Locates a loaded module by name, returning its base and size; nil if not found.
  • mem.modules(): table[]

    • Array of {name, base, size} for every loaded module.
  • mem.scan(module: string, pattern: string): address

    • IDA-style signature scan within module (e.g. "48 8B ?? C3"); nil if no match.
  • mem.search(module: string, value: address|number, type: string): address[]

    • Addresses in module whose memory equals value interpreted as type (default ptr; same type names as mem.read).
    • Returns a table of hits (possibly empty), capped at 256; nil on bad args.
  • mem.refs(addr: address): address[]

    • Pointer-sized references to addr across all loaded modules - i.e. what points here?
    • Returns a table of hits, capped at 256.
    • Handy for tracking down a dangling/stale pointer.
  • mem.region(addr: address): table

    • Returns {base, size, read, write, execute} for the page containing addr.
  • mem.readable(addr: address, n: number): boolean

    • True if n bytes (default 1) at addr can be read.
  • mem.executable(addr: address): boolean

    • True if addr is in executable memory.
  • mem.dump(addr: address, n: number): number

    • Hexdumps up to n bytes (default 64, capped at 512) into the report, stopping at the first unreadable byte.
    • Returns the number of bytes dumped.
  • mem.symbol(name: string): address

    • mem.symbol(module: string, name: string): address
    • Reverse symbol lookup (name -> address) using PE exports / dbghelp on Windows and the ELF symbol table on Linux, optionally scoped to one module.\
    • nil if the name isn't found.
  • mem.threads(): table[]

    • Array of {id, pc, sym, name, current} for every thread in the process.
    • Mainly useful on a hang to see which thread is stuck and where.
    • pc/sym are present only when the thread's instruction pointer is obtainable (always on Windows; Linux reports id/name only).

Example

print("fault at", mem.sym(crash.pc))
local base, size = mem.find("server_srv")
if base then print("server_srv", mem.sym(base), size) end
print("dword at rip:", mem.read(crash.pc, "uint32"))

-- native call stack
for _, f in ipairs(crash.stack or {}) do print(f.sym) end

-- what still references the faulting pointer?
for _, a in ipairs(mem.refs(crash.fault) or {}) do print("referenced by", mem.sym(a)) end

-- walk a struct: health at *(ent+0x10)+0x4
local hp = mem.chain(crash.regs.rdi, 0x10, 0x4)
if hp then print("hp =", mem.read(hp, "float")) end

Clone this wiki locally