Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Intermediate
  • 20 minutes
  • Examples run with Clang 21.0.0, GCC 16.2.0 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 8 of 20 minutes free
Free: The shell says only that it crashed; AddressSanitizer says where · Read a backtrace from the top, starting at your own code · A debugger stops at the crash, and std::stacktrace needs a library that has it · Practise: read a crash and its stack. With a pass: 2 in-depth parts and 3 interview answers, about 12 minutes.

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

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:

crash.cpp C++ · crash.cpp
#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));
}
The same crash with each compiler, then with AddressSanitizer Python · crash_plain_session.py
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:

Interactive · step by step One null pointer, two builds of the same program Step through total_marks on the roll of crash.cpp, then compare what the plain build and the AddressSanitizer build say about the crash.
One null pointer, two builds of the same program, step 6 of 6. Showing roll[0], total_marks (total = 82), roll[1], the operating system (sends SIGSEGV), roll[2], plain build (Segmentation fault: 11), AddressSanitizer build (#0 crash.cpp:12, #1 :20). In focus: AddressSanitizer build, total_marks.a readroll[0]&asha, marks 82total_markstotal = 82roll[1]nullptrthe operating systemsends SIGSEGVroll[2]&ravi, never reachedplain buildSegmentation fault: 11AddressSanitizer build#0 crash.cpp:12, #1 :20

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Asha. total_marks takes roll[0], the address of asha, and adds her 82 marks: total is 82.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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 SEGV caused by a READ, at address 0x000000000018, with the hint that the address is in the zero page. Address 24 is a null pointer plus the offset of marks, which follows a 24-byte std::string in libc++.
  • Where: frame #0 is total_marks at crash.cpp:12, the line that read s->marks; frame #1 is main at 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

  1. Question 1 of 2 A program ends with exit status 139 and the shell prints "Segmentation fault". What happened?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The operating system stopped it with signal 11, SIGSEGV, for an invalid memory access

    A shell reports a process killed by a signal as 128 plus the signal number: 139 is 128 + 11, SIGSEGV. 134 is 128 + 6, SIGABRT, which std::abort raises.

  2. Question 2 of 2 AddressSanitizer reports a READ at address 0x000000000018 in the zero page. What does the address tell you?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A member was read through a null pointer, 24 bytes into the object

    An address near 0 means a null pointer plus an offset. In crash.cpp, s->marks with s == nullptr read 0 + 24, and the report's frame #0, crash.cpp:12, is the line that did it.

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:

A backtrace read top down: frame 0 is the crashing line in total_marks, frame 1 its caller main, frame 2 the system start.#0 total_marks crash.cpp:12the line that was running:it read s->marks through a null pointer#1 main crash.cpp:20the line that called total_marks#2 start (dyld)the system code that called mainStart at the first frame in yourown code, then walk down to findwhere the bad value came fromcalled bycalled by

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.

  1. 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.
  2. 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.
  3. 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:

  • -g writes 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-pointer keeps a pointer to each frame, so that tools can walk from one frame to the next.
  • -O0 or -Og keeps each function as a frame of its own; higher optimisation inlines functions into their callers.

Checkpoint

Check: Reading a backtrace

  1. Question 1 of 2 In a backtrace, which frame shows the line that was running when the program crashed?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Frame #0, the top one

    Backtraces list the innermost call first. Frame #0 is the function that was running, #1 the one that called it, and so on down to the system code that started main.

  2. Question 2 of 2 Frame #0 is total_marks at crash.cpp:12 and frame #1 is main at crash.cpp:20. Where do you look for the reason the pointer was null?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: In main, where the roll was built, because frame #1 shows how the program got to the crash

    Start at the first frame in your own code, where the program failed, then walk down its callers to find where the bad value came from: here the null pointer in roll, built in main.

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:

stacktrace_check.cpp C++ · stacktrace_check.cpp
#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
}
Is std::stacktrace there? Both libraries answer Python · stacktrace_check_session.py
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

  1. Question 1 of 2 lldb has stopped the program at the crash. Which command prints the backtrace?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: bt

    lldb ./crash loads the program, run starts it, and after the crash bt prints the stack; frame select 1 and frame variable then show main's locals. gdb has run, bt, frame 1 and info locals.

  2. Question 2 of 2 Portable code wants to use std::stacktrace where it exists. What does it test, and where does the test come from?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The feature-test macro __cpp_lib_stacktrace, which <version> defines when the library has it

    Test __cpp_lib_stacktrace after including <version>, and fall back to another way of printing the stack, such as backtrace() from <execinfo.h>, where the macro is missing.

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.

  1. Question 1 of 8 Which signal does std::abort raise, and what exit status does a shell then report?

    Choose one answer.

    Show the answer: Question 1 of 8

    Answer: SIGABRT, signal 6, so 134

    A crash is a signal: 139 is SIGSEGV (128 + 11), 134 is SIGABRT (128 + 6), which std::abort raises.

  2. Question 2 of 8 A program crashes with only Segmentation fault: 11. What is the quickest way to find the line?

    Choose one answer.

    Show the answer: Question 2 of 8

    Answer: Rebuild it with -fsanitize=address and -g and reproduce the crash, because the report contains the stack

    Start with the sanitizer: it needs no setup beyond a compiler option, and a sanitized test build is the first place to reproduce any crash.

  3. Question 3 of 8 Which build options make backtraces easier to read?

    Choose every answer that is right.

    Show the answer: Question 3 of 8

    Answer:

    • -O0 or -Og, so that functions are not inlined away
    • -fno-omit-frame-pointer, so that every frame can be found
    • -g, so that frames show files and lines

    Debug information gives names, files and lines; frame pointers let tools walk from frame to frame; low optimisation keeps each function as its own frame. NDEBUG only removes assertions.

  4. Question 4 of 8 A backtrace from an -O2 build shows main where the source has main calling a small helper. Why can the helper be missing?

    Choose one answer.

    Show the answer: Question 4 of 8

    Answer: The optimiser inlined the helper into main, so it has no frame of its own

    Each frame is a function call that has not returned. An inlined function is part of its caller's code, so a walk along the frames cannot see it; build tests with low optimisation for readable stacks.

  5. Question 5 of 8 In which order do the frames of a backtrace appear?

    Choose one answer.

    Show the answer: Question 5 of 8

    Answer: Innermost call first, then each caller, down to the system code that started main

    Read a backtrace from the top: frame #0 is where the program was, the frames below it are how it got there.

  6. Question 6 of 8 Which of the two standard libraries on this lesson's Mac provides std::stacktrace?

    Choose one answer.

    Show the answer: Question 6 of 8

    Answer: Neither: libc++ has no std::stacktrace, and this GCC build was configured without it

    The feature-test macro __cpp_lib_stacktrace was undefined with both. libc++ has not implemented std::stacktrace, and Homebrew's GCC 16 on macOS was built without it; on Linux, GCC provides it when you link with -lstdc++exp.

  7. Question 7 of 8 A program that uses std::stacktrace is built with GCC on Linux, where libstdc++ provides it. What does linking it need?
    Show the answer: Question 7 of 8

    Answer: The extra library -lstdc++exp

    GCC's libstdc++ implements std::stacktrace and needs -lstdc++exp when linking, while libc++ has not implemented it; test __cpp_lib_stacktrace before relying on it.

  8. Question 8 of 8 In gdb, after the crash, how do you see the local variables of main, the caller of the function that crashed?

    Choose one answer.

    Show the answer: Question 8 of 8

    Answer: frame 1, then info locals

    gdb offers the same steps as lldb: run, bt, frame 1 and info locals, where lldb has run, bt, frame select 1 and frame variable.

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.

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

  1. 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.

  2. 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.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. 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?

    Model answer, Pro. First step and follow-ups

  4. 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.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A crash is a signal: exit status 139 is SIGSEGV, 134 is SIGABRT, 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-pointer and 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_stacktrace before 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

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.