I have the following code:
#include <stacktrace>
#include <iostream>
void bar() {
for (const auto &entry : std::stacktrace::current()) {
std::cout << entry << '\n';
}
}
void foo() {
bar();
}
int main() {
foo();
}
On a debug build, this prints out what you would expect:
bar() at /app/example.cpp:5
foo() at /app/example.cpp:11
main at /app/example.cpp:15
at :0
_start at :0
However, I'm not entirely sure what the nameless at :0
entry is representing.
The point where it gets really puzzling is when we enable optimizations with -O2
:
bar() at /app/example.cpp:5
foo() at /app/example.cpp:11
at :0
_start at :0
I don't understand how this output is possible, given the assembly output:
main:
sub rsp, 8
call bar()
xor eax, eax
add rsp, 8
ret
See live example at Compiler Explorer
bar()
know that the stacktrace is foo() -> bar()
, if foo()
is inlined, and completely skipped in the call chain?bar()
somehow knows its callers -even the inlined ones-, how come main
disappeared from the stacktrace when enabling optimizations?at :0
entry with no name? Is there a way to easily filter it out (without string manipulation)?It is possible to track down inline functions in some cases. The x86-64 ABI specifies that the RBP register (frame base pointer) may be omitted:
The conventional use of %rbp as a frame pointer for the stack frame may be avoided by using %rsp (the stack pointer) to index into the stack frame. This technique saves two instructions in the prologue and epilogue and makes one additional general-purpose register (%rbp) available.
The debugger and glibc's backtrace
call deduce the function by the value of RSP and the RIP using special debug DWARF section called CFI. For each line of code it has the rules how to "unwind" the current state to the beginning of function invocation.
Then, another piece of debug information comes at service, called DW_TAG_subprogram
. Each function marked as inline explicitly or made inline implicitly has an abstract instance and the concreate instances for each inline expansion. More details may be found in Section 3.3.8 of DWARF Debugging Information Format Version 5
It's not that bar()
knows who called it, it's glibc who deduces that by virtual unwinding the stack using RSP and RIP and then using the inline-related DWARF tags.
You should not expect to get the ideal output at all times. The C++ library, along with the glibc make best effort, but that's all they do. This is the definition of the stacktrace from the Standard proposal:
A stacktrace is an approximate (marked by me) representation of an invocation sequence and consists of stacktrace entries. A stacktrace entry represents an evaluation in a stacktrace.
The at: 0
is a marking of the outermost frame in the ABI of x86_64. The function _start
, written in assembly, explicitly does that. This is an excerpt from glibc's start.S:
ENTRY (_start)
/* Clearing frame pointer is insufficient, use CFI. */
cfi_undefined (rip)
/* Clear the frame pointer. The ABI suggests this be done, to mark
the outermost frame obviously. */
xorl %ebp, %ebp
This is of course ABI-specific.
Your way to clean this out is string manipulation, or stop two stacktrace entries off the end. Usually you are not interested in _start()
either.
There is a very limited support for std::stacktrace
as of now. GCC14 has some support, GCC13 fails to show the stacktrace, GCC12 shows a smaller part of the stacktrace. Other than GCC there is no support for std::stacktrace
on Linux. The C++23 standard itself is still a draft - usually it takes years for the compilers to adopt newer standards.