-
Notifications
You must be signed in to change notification settings - Fork 3
Crash Script Diagnostics
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.luaAddresses are opaque light pointers, pass them straight back into mem.*.
An address argument also accepts a plain number.
Reads that fail return nil.
-
crash.kind: string\- What happened, e.g.
unhandled exception,hang,abort,dump.
- What happened, e.g.
-
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). -
nilif no map was known when the plugin captured it (no heartbeat source or still loading).
- The current map name (e.g.
-
crash.fault: address\- The faulting data address (e.g. the bad pointer in an access violation).
-
nilfor 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
addressvalues (rax...ripon x64,eax...eipon x86). - Empty if no context was available.
- Pass any into
mem.*.
- The register file as
-
crash.uptime: number\- Milliseconds since the plugin armed.
-
crash.pulse: number\- Milliseconds since the last heartbeat.
-
nilif 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}. -
localsis a"a=1; self=Entity: 0x..."summary, present only for Lua frames. -
nilif 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 native call stack from the faulting context, as
-
mem.read(addr: address, type: string): number- Reads the following possible types:
int8/uint8/int16/uint16/int32/uint32/int64/uint64/float/double/ptr. -
ptrreturns anaddress,nilif unreadable.
- Reads the following possible types:
-
mem.string(addr: address, max: number): string- Reads a NUL-terminated string (up to
maxbytes, default 256).
- Reads a NUL-terminated string (up to
-
mem.bytes(addr: address, n: number): string- Up to
nraw bytes (default 64, capped at 4096) as a Lua string, stopping at the first unreadable byte. - For decoding structs with
string.byte/string.unpack.
- Up to
-
mem.deref(addr: address): address- Reads the pointer stored.
-
mem.offset(addr: address, n: number): address- Returns
addr + n.
- Returns
-
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). -
nilif any hop is unreadable. E.g.mem.chain(crash.regs.rdi, 0x10, 0x28)->*(rdi+0x10) + 0x28.
-
mem.sym(addr: address): string- Returns
module+RVAplus a symbol name when debug info is available.
- Returns
-
mem.find(name: string): address, number- Locates a loaded module by name, returning its
baseandsize;nilif not found.
- Locates a loaded module by name, returning its
-
mem.modules(): table[]- Array of
{name, base, size}for every loaded module.
- Array of
-
mem.scan(module: string, pattern: string): address- IDA-style signature scan within
module(e.g."48 8B ?? C3");nilif no match.
- IDA-style signature scan within
-
mem.search(module: string, value: address|number, type: string): address[]- Addresses in
modulewhose memory equalsvalueinterpreted astype(defaultptr; same type names asmem.read). - Returns a table of hits (possibly empty), capped at 256;
nilon bad args.
- Addresses in
-
mem.refs(addr: address): address[]- Pointer-sized references to
addracross all loaded modules - i.e. what points here? - Returns a table of hits, capped at 256.
- Handy for tracking down a dangling/stale pointer.
- Pointer-sized references to
-
mem.region(addr: address): table- Returns
{base, size, read, write, execute}for the page containingaddr.
- Returns
-
mem.readable(addr: address, n: number): boolean- True if
nbytes (default 1) ataddrcan be read.
- True if
-
mem.executable(addr: address): boolean- True if
addris in executable memory.
- True if
-
mem.dump(addr: address, n: number): number- Hexdumps up to
nbytes (default 64, capped at 512) into the report, stopping at the first unreadable byte. - Returns the number of bytes dumped.
- Hexdumps up to
-
mem.symbol(name: string): addressmem.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.\ -
nilif 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/symare present only when the thread's instruction pointer is obtainable (always on Windows; Linux reportsid/nameonly).
- Array of
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")) endGetting started
Usage
Features
For module developers