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, bool hideNonSpiceFrames = false)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 as the first argument 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 | |
|---|---|
Hiding frames that are not Spice code¶
Not every frame belongs to your program: the C runtime below main shows up in every trace, and so does any C
function that calls back into Spice, such as qsort calling your comparison function. Pass true as the second
argument to leave those frames out:
| Spice | |
|---|---|
| Text Only | |
|---|---|
The frames that remain keep the number they have in the full trace, so a gap in the numbering shows where frames were
hidden, and each number still matches trace[i].
How a frame is recognized as Spice code is described under isSpice below.
Panics and failed assertions¶
A panic, a failed assert and a read of an inactive
union field print the stack trace of the failing call to stderr right before the program exits,
so there is no need to call sDumpStacktrace() for them:
Both the message and the trace go to stderr, the message first. The first frame is the function that contains the panic
or the assertion. The trace depends on symbols and debug info in the same way as the ones above, so -g adds the file
and line of every frame and --strip-symbols leaves the names out. If error return tracing was enabled with
#![core.compiler.errorReturnTracing = true], the recorded error return trace is printed before the stack trace.
A few cases print no trace:
- Release builds compile assertions out altogether, so there is nothing to print. A
panicstill prints its trace. - Cross-compiled programs print none. The trace needs the
libbacktracethat ships with the std, which is built for the machine the std is installed on, so it is left out when the target is a different one. - A panic raised while a trace is being printed, such as running out of memory for one of its strings, ends the program without a second trace.
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 |
unsigned int |
Line within that file, 0 if unknown |
isSpice |
bool |
Whether the frame is Spice code, as opposed to C, C++ or other native code |
isSpice is decided from what the trace knows about the frame:
- With debug info, the source file decides: a frame is Spice code if and only if its file ends in
.spice. - Without it, the symbol name does. Spice mangles the name of every function except
main, and a name that reads as a Spice mangling, or ismain, counts as Spice. Plain C symbols, and the mangled names of other languages, do not. - A frame whose symbol could not be resolved - which is every frame of an executable built with
--strip-symbols- has nothing to go by and is not Spice.
A function opted out of mangling with #[core.compiler.mangle = false] looks like a C function without debug info, and
is treated like one.
StackTrace also offers getSize(), isEmpty(), indexing via trace[i] or getEntry(i), and
dump(bool includeAddresses, bool hideNonSpiceFrames) for the whole trace. A trace holds at most
STACK_TRACE_CAPACITY (128) frames; anything deeper is dropped, since the frames nearest the capture point are the
interesting ones.
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. The library ships with Spice: it is installed as std/runtime/lib/libbacktrace.a,
inside the standard library, and the compiler puts that directory on the linker's search path of every program it
builds. There is nothing to install and nothing to configure, on any platform - every release archive, package and
container image carries the copy built for that platform, together with libbacktrace's license
(std/runtime/lib/LICENSE).
The same holds for a compiler you build yourself: libbacktrace is a git submodule, and python setup-deps.py
builds it into the std tree along with the rest of the dependency setup, before CMake ever runs. It builds with
autotools, so that step needs a POSIX shell (sh or bash) and make. On Windows, install
MSYS2, run pacman -S --needed make, and put its usr/bin on PATH; elsewhere a
toolchain that can already build the compiler normally has both.
If the archive is missing - a hand-assembled std, say - the linker falls back to its own search path, and linking a
program that takes a stack trace fails with an undefined reference to backtrace_create_state wherever the
toolchain has no libbacktrace of its own. Pointing LIBRARY_PATH at a copy of the library gets you going again;
both Clang and GCC read it for -l search dirs.
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.