Stack traces
Spice can print the call stack of a running program, or hand it to you as data. Two functions from the stack
trace runtime do this; like the other s-prefixed runtime functions they are auto-imported, so no import is
needed:
sDumpStacktrace(bool includeAddresses = true)prints the calling function's stack to stderr.sGetStacktrace() -> StackTracereturns it as a value you can walk yourself.
| Spice | |
|---|---|
Built with -g, this prints something like:
One line per frame, most recent call first, each with:
- the frame number, counting up from
#0at the most recent call, - the address the frame will return to,
- the demangled function name, followed by
+and the byte offset from where that function starts, at <file>:<line>, when the program carries debug info.
Frames below main belong to the C runtime and differ per platform. A frame whose symbol could not be resolved
prints as <unknown>; a frame in a program without debug info simply has no at ... part.
Pass false to leave the address column out, which is what you want for output you intend to compare: addresses
move between runs under ASLR.
| Text Only | |
|---|---|
Working with a trace as data¶
sGetStacktrace() returns a StackTrace, which can be looped over frame by frame:
| Spice | |
|---|---|
The frame number is available as the loop index, exactly as dump() prints it:
| Spice | |
|---|---|
| Field | Type | Meaning |
|---|---|---|
address |
byte* |
Return address within the function |
offset |
unsigned long |
Bytes from the start of that function, 0 if unresolved |
functionName |
String |
Demangled name, empty if the symbol could not be resolved |
fileName |
String |
Source file, empty without debug info |
lineNumber |
int |
Line within that file, 0 if unknown |
StackTrace also offers getSize(), isEmpty(), indexing via trace[i] or getEntry(i), and
dump(bool includeAddresses) for the whole trace. A trace holds at most STACK_TRACE_CAPACITY (64) frames;
anything deeper is dropped, since the frames nearest the capture point are the interesting ones.
The foreach loops above go through getIterator(), which hands out a StackTraceIterator<StackTraceEntry>.
You can also drive it yourself, forwards or backwards:
| Spice | |
|---|---|
To capture the stack on behalf of a caller - from a logging helper, say, whose own frame should not show up -
build a StackTrace yourself and tell capture() how many frames to skip:
| Spice | |
|---|---|
What you need for symbols¶
Resolving an address to a name happens at run time, against the executable's own symbol table and debug info, so what ends up in the binary decides how much of a trace is readable:
- Names and offsets come from the symbol table, which every executable keeps unless you build it with
--strip-symbols. Nothing else is needed. In a stripped executable every frame prints as<unknown>, so strip only what you do not expect to read a stack trace from. - File names and line numbers come from debug info, so they only appear in a build made with
-g.
The work itself is done by libbacktrace, which the linker
pulls in as -lbacktrace. GCC builds it as part of its own runtime, so where a program is linked through GCC -
Linux, most of the time - it is already there and nothing needs doing. Elsewhere it may have to be pointed at,
and linking a program that takes a stack trace otherwise fails with an undefined reference to
backtrace_create_state.
Linux with Clang resolves it too, since Clang searches GCC's directories.
Windows: some MinGW-w64 distributions ship libbacktrace.a alongside GCC and some do not, and Clang's GNU
driver does not search GCC's own library directory either way. Ask GCC whether it has one - it echoes the bare
file name back when it does not - and put the directory on LIBRARY_PATH, which Clang reads for -l search
dirs:
| PowerShell | |
|---|---|
If that comes up empty - as it does on the GitHub Actions runner, which is why this repository's CI builds its
own - build the library in an MSYS2 MINGW64 shell and point LIBRARY_PATH at the
result:
| Bash | |
|---|---|
| PowerShell | |
|---|---|
macOS has none at all: the Apple toolchain does not include libbacktrace, and Homebrew has no formula for it.
Either install the MacPorts port (sudo port install libbacktrace) or build it yourself:
| Bash | |
|---|---|
Limitations¶
- In an optimized build a function that was inlined into another still gets a frame of its own, recovered from the debug info, but its offset is counted from the start of the function it was inlined into. Without debug info, inlined functions do not appear at all - only the physical frames do.
spice build -staticleaves no dynamic symbol information behind for shared-library frames, so frames outside the executable itself resolve less well.- On Windows, a frame in a system DLL that carries no symbols of its own can come back named after a function in
a different module, with a nonsensical offset. libbacktrace searches each loaded module's symbol table in turn
and treats the last entry of one as covering every address above it, so a lookup that finds nothing where it
should can still match something where it should not. Frames in your own executable are unaffected; the ones
below
mainare the ones to distrust.