C++ Module 13 – Error handling
Debugging crashes: backtraces and std::stacktrace
Find why a C++ program crashed: read a backtrace from a sanitizer or a debugger, print the stack from code, and log useful facts before the program ends.
What you will learn
- Capture a backtrace from a crashing program and read it from the top
- Check whether std::stacktrace is available, and print a stack trace from code
- Log useful information from a terminate handler before the program ends
Before you start
What you’ll learn · Before you start
What you will learn
- Capture a backtrace from a crashing program and read it from the top
- Check whether std::stacktrace is available, and print a stack trace from code
- Log useful information from a terminate handler before the program ends
Before you start
On this page
total_marks reads a mark through a null pointer, and all the shell says is Segmentation fault: 11, exit status
139. A program crashes when the operating system stops it for doing something it may not do, such as reading
memory it does not own, which raises the signal: A notice the operating system delivers to a process, such as SIGSEGV for an invalid memory access or SIGABRT from std::abort. Unless the program handles it, the process ends, and a shell reports the exit status as 128 plus the signal's number. SIGSEGV, or when it calls std::abort,
which raises SIGABRT. The quickest way to see why is a backtrace: The list of function calls active at one moment, innermost first: frame 0 is the function that was running, frame 1 its caller, and so on down to main and the system code that started it. With -g, each frame shows its file and line.: the list of
function calls that were active at that moment, the innermost first, each with its file and line when the program was
built with -g.
There are three ways to get one. A sanitizer: A compiler option, such as -fsanitize=address or -fsanitize=undefined, that builds extra checks into a program, so that memory errors and undefined behaviour are reported, with their source line, while it runs. More in “Finding memory bugs with AddressSanitizer and UBSan” build, such as AddressSanitizer, prints
the stack in its report. A debugger stops at the crash and prints it when you type bt. And the program can print
its own stack, which C++23 standardises as std::stacktrace and older code does with backtrace(). Start with the
sanitizer: it needs no setup beyond a compiler option.
The shell says only that it crashed; AddressSanitizer says where
total_marks adds up the marks of a roll of students, and one entry of the roll is a null pointer:
#include <print>
#include <string>
#include <vector>
struct Student {
std::string name;
int marks;
};
int total_marks(const std::vector<Student*>& roll) {
int total = 0;
for (const Student* s : roll) total += s->marks; // a null pointer in roll crashes here
return total;
}
int main() {
Student asha{"Asha", 82};
Student ravi{"Ravi", 74};
std::vector<Student*> roll{&asha, nullptr, &ravi};
std::println("total: {}", total_marks(roll));
} from session import run
# The same crash three times: built normally with each compiler, then with AddressSanitizer.
run("""
clang++ -std=c++23 -g crash.cpp -o crash && ./crash
g++-16 -std=c++23 -g crash.cpp -o crash-gcc && ./crash-gcc
clang++ -std=c++23 -g -fsanitize=address -fno-omit-frame-pointer crash.cpp -o crash-asan && ./crash-asan
""") Output
$ clang++ -std=c++23 -g crash.cpp -o crash && ./crash
Segmentation fault: 11
[exit status 139]
$ g++-16 -std=c++23 -g crash.cpp -o crash-gcc && ./crash-gcc
Segmentation fault: 11
[exit status 139]
$ clang++ -std=c++23 -g -fsanitize=address -fno-omit-frame-pointer crash.cpp -o crash-asan && ./crash-asan
AddressSanitizer:DEADLYSIGNAL
==…==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000018 (pc 0x… bp 0x… sp 0x… T0)
==…==The signal is caused by a READ memory access.
==…==Hint: address points to the zero page.
#0 in total_marks(std::__1::vector<Student*, std::__1::allocator<Student*>> const&) crash.cpp:12
#1 in main crash.cpp:20
SUMMARY: AddressSanitizer: SEGV
[13 more lines of the report left out]
==…==ABORTING
Abort trap: 6
[exit status 134]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 crash_plain_session.py
Step through the loop over the roll, then compare what each build says about the crash:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- Asha. total_marks takes roll[0], the address of asha, and adds her 82 marks: total is 82.
- A null pointer. roll[1] is nullptr. s->marks reads the int that follows the 24-byte std::string of libc++, so the read goes to address 0 + 24, 0x18.
- SIGSEGV. Address 0x18 is in the zero page, which no program may read. The operating system stops the program with SIGSEGV, signal 11, before roll[2] is ever reached.
- The plain build. Built normally, with either compiler, the program has nothing that reports the signal. The shell prints Segmentation fault: 11 and exit status 139, which is 128 plus 11. Nothing says where.
- The sanitizer build. Built with -fsanitize=address, the program catches the signal and reports it: a SEGV on unknown address 0x000000000018, caused by a READ, with the hint that the address points to the zero page.
- Where it happened. The report ends with the stack: frame #0 is total_marks at crash.cpp:12, the line that read s->marks, and frame #1 is main at crash.cpp:20, the line that called it. Then it aborts, with exit status 134.
The plain builds say only Segmentation fault: 11 and exit with status 139, which is 128 plus 11, the number of
SIGSEGV. Nothing says where. The AddressSanitizer build says it all:
- What: a
SEGVcaused by aREAD, at address0x000000000018, with the hint that the address is in the zero page. Address 24 is a null pointer plus the offset ofmarks, which follows a 24-bytestd::stringin libc++. - Where: frame
#0istotal_marksatcrash.cpp:12, the line that reads->marks; frame#1ismainat line 20, the line that called it.
The report is shortened here to its key lines, as in the sanitizers lesson. Because a sanitizer report already contains the stack, a sanitized test build is the first place to reproduce any crash.
Checkpoint
Check: A crash, from the shell and from a sanitizer
Read a backtrace from the top, starting at your own code
Read a backtrace from the top, as the diagram reads the stack of this crash:
Reading the backtrace of the crash
Text description of the diagram
The diagram shows the three frames of the crash in this lesson, from the top.
- Frame #0, total_marks at crash.cpp line 12: the line that was running when the program crashed. It read s->marks through a null pointer.
- Frame #1, main at crash.cpp line 20: the line that called total_marks. An arrow labelled "called by" leads from frame 0 to frame 1.
- Frame #2, start in dyld: the system code that called main. Another "called by" arrow leads from frame 1 to frame 2.
A dashed note beside frame 0 gives the method: start at the first frame in your own code, then walk down the frames to find where the bad value came from.
Frame #0 is the function that was running, each later frame is the caller of the one before, and the last frames
belong to the system code that started main. The first frame in your own code is
usually the line to look at, and the frames below it show how the program got there, which is where the bad value,
here the null pointer in roll, came from. Three build options make the frames worth reading:
-gwrites the debug information: The names, types and line numbers that -g (with MSVC, /Zi) stores with a program, so that a debugger can show the running program in terms of its source. It can be combined with any -O level. More in “Compiler flags every C++ learner should use” that turns addresses into functions, files and lines.-fno-omit-frame-pointerkeeps a pointer to each frame, so that tools can walk from one frame to the next.-O0or-Ogkeeps each function as a frame of its own; higher optimisation inlines functions into their callers.
Checkpoint
Check: Reading a backtrace
A debugger stops at the crash, and std::stacktrace needs a library that has it
A debugger runs the program and stops it at the crash. With lldb, the debugger of Apple’s tools, lldb ./crash
loads the program and run starts it; when it stops, bt prints the backtrace, frame select 1 moves to main’s
frame and frame variable shows that frame’s local variables. gdb, the GNU debugger on Linux, offers the same with
run, bt, frame 1 and info locals. On macOS, the first time in a login session that a debugger examines one of
your processes, the system asks for an administrator’s password; the DevToolsSecurity manual page (man DevToolsSecurity) explains how members of the developer group can skip that prompt.
std::stacktrace, from the C++23 header <stacktrace>, captures the current stack as a value that a program can
print or write to a log, for example from an error path. GCC’s libstdc++ implements it and needs the extra library
-lstdc++exp when linking, while libc++ has not implemented it. The
feature-test macro: A macro such as __cpp_modules or __cpp_lib_modules whose value, a year and month such as 202207, says that a compiler or its library claims a feature. A toolchain may build a feature without defining its macro, or define the macro without the feature. More in “C++20 modules: export, import and partitions” __cpp_lib_stacktrace tells code which case it is in:
#include <print>
#include <version> // defines the feature-test macros of the standard library
int main() {
#if defined(_LIBCPP_VERSION)
std::println("standard library: libc++");
#elif defined(__GLIBCXX__)
std::println("standard library: libstdc++");
#endif
#if defined(__cpp_lib_stacktrace)
std::println("std::stacktrace: available (__cpp_lib_stacktrace = {})", __cpp_lib_stacktrace);
#else
std::println("std::stacktrace: not available (__cpp_lib_stacktrace is not defined)");
#endif
} from session import run
# Asks each standard library on this Mac whether it provides std::stacktrace.
run("""
clang++ -std=c++23 stacktrace_check.cpp -o stacktrace_check && ./stacktrace_check
g++-16 -std=c++23 stacktrace_check.cpp -o stacktrace_check-gcc && ./stacktrace_check-gcc
""") Output
$ clang++ -std=c++23 stacktrace_check.cpp -o stacktrace_check && ./stacktrace_check standard library: libc++ std::stacktrace: not available (__cpp_lib_stacktrace is not defined) $ g++-16 -std=c++23 stacktrace_check.cpp -o stacktrace_check-gcc && ./stacktrace_check-gcc standard library: libstdc++ std::stacktrace: not available (__cpp_lib_stacktrace is not defined)
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 stacktrace_check_session.py
On this Mac the answer is no for both. libc++ has no std::stacktrace at all, and Homebrew’s GCC 16 for macOS was
configured without it, so its <stacktrace> header declares nothing. On Linux the same GCC provides it, so portable
code tests the macro and falls back to another way of printing the stack. Either way, a program most often prints its
own stack from a terminate handler, installed with std::set_terminate, or from a handler for a signal such as
SIGSEGV. Side by side:
| Way | What you do | On this Mac |
|---|---|---|
| A sanitizer | Build with -fsanitize=address |
The stack is in the report |
| A debugger | lldb ./crash, run, bt |
A password, the first time |
| std::stacktrace | Capture the stack in code | Neither library has it |
Checkpoint
Check: A debugger, and std::stacktrace
Practise: read a crash and its stack
Eight questions on the three ideas above, starting with the one you have answered least well.
Practice that adapts to you
Practise this lesson
8 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Printing a stack trace from code, and turning it into files and lines Pro: opens with a Pro, Premium or Ultimate pass
A program that prints its own stack at the moment of a fatal error leaves a trace even where no debugger or sanitizer ran. Here a portable stack printer runs in a terminate handler with both compilers, and a short script turns its output into source lines afterwards.
In this part:
- A portable stack printer in a terminate handler
- Frames that mislead, and how to read them anyway
- From a crash log to files and lines, offline
1 diagram · 4 runnable examples (C header, C++, Python) · about 7 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Logging before the program dies: signal handlers and a terminate handler of your own Pro: opens with a Pro, Premium or Ultimate pass
A terminate handler runs as ordinary code, but a crash from a signal can interrupt anything, even malloc, so what is
allowed there is much narrower. This part writes a crash handler within those limits, compares the ways a production
program can leave a stack behind, and ends with an exercise.
In this part:
- A crash handler that uses only async-signal-safe calls
- Where a production program’s stack can come from
- Your turn: a terminate handler that names the reason
1 diagram · 4 runnable examples (C header, C++, Python) · about 6 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Interview questions
-
Fresher · Warm-up
What is a backtrace, and how do you get one from a C++ program that crashed?
A backtrace lists the function calls that were active at one moment, innermost first: frame 0 is the function that was running, frame 1 its caller, and so on down to main and the system code that started it. With debug information from -g, each frame shows its file and line. There are three common ways to get one: run the program under a debugger, lldb or gdb, and type bt when it stops; build with a sanitizer such as -fsanitize=address, whose report contains the stack; or let the program print its own stack from a terminate or signal handler, with std::stacktrace where the standard library has it, or backtrace() from execinfo.h. A good answer adds -fno-omit-frame-pointer and low optimisation for complete stacks, and reading from the first frame in your own code.
-
Mid to senior
A Linux service crashes with SIGSEGV about once a day, writes no log line, and nobody can reproduce it. Walk through how you would get from the next crash to the line of code that caused it.
-
Mid to senior
A teammate's SIGSEGV handler logs the crash with std::println and then calls std::exit(1). Sometimes the process hangs instead of exiting. Why, and how would you rewrite the handler?
-
Mid to senior
A crash report from a release build shows main calling free() directly, but in the source main calls three of your functions before anything is freed. Explain the missing frames, and how you would get complete stacks from the next release.
Key takeaways
- A crash is a signal: exit status 139 is
SIGSEGV, 134 isSIGABRT, and the shell alone never says where. - A backtrace lists the active calls, innermost first; start at the first frame in your own code.
- Build with
-g -fno-omit-frame-pointerand low optimisation for readable stacks, and reproduce crashes in a sanitizer build first, because its report contains the stack. - std::stacktrace is C++23, but neither standard library on this Mac provides it: test
__cpp_lib_stacktracebefore using it.
Terms in this lesson
- backtrace
- The list of function calls active at one moment, innermost first: frame 0 is the function that was running, frame 1 its caller, and so on down to main and the system code that started it. With -g, each frame shows its file and line.
- signal
- A notice the operating system delivers to a process, such as SIGSEGV for an invalid memory access or SIGABRT from std::abort. Unless the program handles it, the process ends, and a shell reports the exit status as 128 plus the signal's number.
References
- Working Draft, Programming Languages: C++ (N4950): [stacktrace], [except.handle], [terminate.handler], [support.signal] (ISO/IEC JTC1/SC22/WG21)
- P0881R7: A proposal to add stacktrace library (ISO/IEC JTC1/SC22/WG21)
- LLDB tutorial (LLVM Project)
- Debugging with GDB: Backtraces (GNU Project)
- The GNU C++ Library manual: linking (libstdc++exp for <stacktrace>) (GNU Project)
- libc++ C++23 status (LLVM Project)
- The GNU C Library manual: Backtraces (GNU Project)
- POSIX.1-2024, System Interfaces: signal concepts and async-signal-safe functions (The Open Group)
- AddressSanitizer (Clang documentation) (LLVM Project)
- Options That Control Optimization (-fno-omit-frame-pointer) (GNU Project)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress