Question

How to Generate a Stack Trace in C++ with GCC When a Program Crashes

cppgcccrashstack-traceassert

Question

I am developing a C++ program with GCC. When the program crashes, I want it to automatically generate a stack trace.

The application is used by many different users and runs on Linux, Windows, and macOS. All versions are compiled with GCC.

I would like the program to capture a stack trace when it crashes, store it, and then ask the user on the next launch whether they want to send that crash information to me for debugging.

I can handle the part where the program sends the information, but I do not know how to generate the stack trace text itself. What are the usual ways to do this in C++?

Short Answer

By the end of this page, you will understand what a stack trace is, how crash handling works in C++ programs compiled with GCC, and how to capture a basic stack trace on Linux. You will also learn the limits of in-process crash reporting, common portability issues across Linux, Windows, and macOS, and practical patterns real applications use to record crash details safely.

Concept

A stack trace is a list of function calls that were active when a program crashed or reached a particular point in execution. It helps you answer questions like:

  • Which function failed?
  • What code path led to the failure?
  • Was the crash caused directly here or deeper below?

In C++, stack traces are especially useful for debugging:

  • segmentation faults
  • invalid memory access
  • aborts caused by assertions
  • unexpected termination

On systems using GCC, a common Linux approach is to use functions from execinfo.h, such as:

  • backtrace()
  • backtrace_symbols()
  • backtrace_symbols_fd()

These can capture return addresses from the current call stack and sometimes translate them into readable function names.

However, there is an important real-world detail: capturing a stack trace inside a crash handler is not always fully safe or portable.

Why?

  • A crash may happen when memory is already corrupted.
  • Signal handlers have strict rules about what functions are safe to call.
  • Name resolution may depend on debug symbols and compiler/linker settings.
  • Linux, Windows, and macOS use different APIs for crash handling.

That means there are usually two levels of crash reporting:

  1. Simple in-process reporting: install a signal handler and try to capture a stack trace.
  2. Robust production reporting: generate a core dump, minidump, or use an external crash reporter.

For a beginner, it is useful to start with the first option so you understand the mechanics. In real applications, teams often combine that with external tools or OS-specific crash-reporting systems.

A stack trace matters because it turns a vague bug report like "the app crashed" into something actionable like "it crashed in saveSettings() after calling parseUserConfig()".

Mental Model

Think of the call stack like a trail of breadcrumbs.

Every time one function calls another, a new breadcrumb is dropped:

  • main() calls runApp()
  • runApp() calls loadConfig()
  • loadConfig() calls parseFile()

If the program crashes inside parseFile(), a stack trace shows the breadcrumb trail back to where execution started.

So instead of only knowing where the crash happened, you also see how the program got there.

Another way to picture it: imagine a stack of plates.

  • Each function call adds a plate.
  • Returning from a function removes a plate.
  • When the program crashes, the current stack of plates tells you what was active at that moment.

The stack trace is basically a snapshot of that stack.

Take Quiz

Syntax and Examples

A common Linux/GCC technique is to use backtrace() from execinfo.h.

Basic syntax

#include <execinfo.h>
#include <unistd.h>
#include <cstdlib>

void print_stacktrace() {
    void* frames[64];
    int size = backtrace(frames, 64);
    backtrace_symbols_fd(frames, size, STDERR_FILENO);
}

What this does

  • frames stores call stack addresses.
  • backtrace() fills the array with the current stack frames.
  • backtrace_symbols_fd() writes a readable form to a file descriptor such as standard error.

Example: capture a stack trace on a crash signal

#include <execinfo.h>
#include <signal.h>
#include <unistd.h>
#include <cstdlib>

void crash_handler(int signal_number) {
    const char* message = "Program crashed. Stack trace:\n";
    write(STDERR_FILENO, message, 30);

    void* frames[64];
    int size = backtrace(frames, 64);
    backtrace_symbols_fd(frames, size, STDERR_FILENO);

    _exit(1);
}

int divide(int a, int b) {
    return a / b;
}

int main() {
    signal(SIGSEGV, crash_handler);
    signal(SIGABRT, crash_handler);
    signal(SIGFPE, crash_handler);

    return divide(10, 0);
}

Compile with useful flags

g++ -g -O0 -rdynamic crash.cpp -o crash_demo

Why these flags help

  • -g includes debug information.
  • -O0 reduces optimization, making traces easier to understand.
  • -rdynamic exports symbol names so function names are more likely to appear in the trace.

Example output

Program crashed. Stack trace:
./crash_demo(_Z13crash_handleri+0x2a)[0x55d0d0b1c1aa]
./crash_demo(_Z6divideii+0x15)[0x55d0d0b1c205]
./crash_demo(main+0x45)[0x55d0d0b1c24f]

Depending on your platform and build settings, function names may be mangled. You can later demangle them or use external tools such as addr2line to resolve addresses more clearly.

Turning addresses into readable locations

If you have the executable and debug symbols, you can use:

addr2line -e crash_demo 0x55d0d0b1c205

That can help map an address to a file and line number.

Step by Step Execution

Consider this example:

#include <execinfo.h>
#include <signal.h>
#include <unistd.h>
#include <cstdlib>

void crash_handler(int signal_number) {
    void* frames[10];
    int size = backtrace(frames, 10);
    backtrace_symbols_fd(frames, size, STDERR_FILENO);
    _exit(1);
}

void third() {
    int* p = nullptr;
    *p = 42;
}

void second() {
    third();
}

void first() {
    second();
}

int main() {
    signal(SIGSEGV, crash_handler);
    first();
}

What happens step by step

  1. main() starts running.

  2. signal(SIGSEGV, crash_handler) registers a function to run if a segmentation fault happens.

  3. main() calls first().

  4. first() calls second().

  5. second() calls third().

  6. third() creates a null pointer:

    int* p = nullptr;
    
  7. The next line tries to write through that null pointer:

    *p = 42;
    
  8. The operating system detects invalid memory access and sends SIGSEGV.

  9. The registered crash_handler() runs.

  10. backtrace() collects the current call stack.

  11. backtrace_symbols_fd() prints the captured frames.

  12. _exit(1) ends the process immediately.

Expected trace shape

The printed stack usually contains frames similar to:

  • crash_handler
  • third
  • second
  • first
  • main

The exact output depends on compiler optimization, inlining, platform behavior, and available debug symbols.

Real World Use Cases

Stack traces are used in many practical situations:

Desktop applications

A GUI application crashes on a user's machine. The program stores crash information in a file and asks the user on the next launch whether to send the report.

Command-line tools

A utility used in automation fails unexpectedly. A stack trace in a log file helps developers reproduce and fix the issue.

Game development

A game crashes only on certain hardware or drivers. Stack traces help narrow down whether the issue is in rendering, file loading, or input handling.

Server processes

A backend service crashes under unusual traffic patterns. Capturing a stack trace can reveal which request path or internal function was active.

Plugin-based systems

An app that loads external modules crashes. The stack trace can show whether the fault happened in host code or plugin code.

Internal testing and QA

Testers may not have a debugger attached. Automatic crash traces give developers useful information without requiring local debugging.

Take Quiz

Real Codebase Usage

In real projects, developers usually do more than just print a trace to the terminal.

Common patterns

Crash file creation

When a fatal error happens, the program writes a crash report to a file such as:

crash-report.txt

That file might include:

  • timestamp
  • application version
  • operating system
  • signal or exception type
  • stack trace

Early startup recovery

On next launch, the app checks whether a crash report already exists.

  • If yes, prompt the user.
  • If approved, upload or attach the report.
  • Then delete or archive it.

Guarded error boundaries

Teams often combine crash reporting with validation and early returns to reduce crashes before they happen.

For example:

if (configPath.empty()) {
    return false;
}

This does not replace crash reporting, but it reduces avoidable failures.

Separate debug symbol handling

In production builds, symbols may be stored separately instead of shipped directly with the app. The crash report may contain addresses, and developers resolve them later using symbol files.

External crash reporters

Large codebases often prefer:

  • Linux core dumps
  • Windows minidumps
  • macOS crash reports
  • third-party crash reporting libraries

These approaches are more reliable than trying to do too much work inside a signal handler.

Important production lesson

A signal handler should do as little as possible.

Good choices:

  • record minimal information
  • write to a file descriptor
  • terminate safely

Risky choices:

  • allocating memory
  • using complex C++ objects
  • logging through high-level frameworks
  • throwing exceptions
  • continuing program execution after a fatal crash

Common Mistakes

1. Assuming stack traces are fully portable

A common beginner mistake is expecting one solution to work identically on Linux, Windows, and macOS.

  • Linux often uses signals and execinfo.h
  • Windows uses different APIs such as structured exception handling and debugging libraries
  • macOS has its own crash-reporting behavior

Avoid this by treating crash reporting as partly platform-specific.

2. Forgetting debug symbols

If you compile without debug-friendly settings, your trace may be much less useful.

Broken expectation:

g++ app.cpp -o app

Better:

g++ -g -O0 -rdynamic app.cpp -o app

3. Doing too much inside a signal handler

This is a big one. After a crash, the program may already be in a bad state.

Risky code:

void crash_handler(int sig) {
    std::string msg = "crashed";   // may allocate memory
    std::cout << msg << std::endl;  // not signal-safe
    exit(1);                        // not ideal here
}

Safer direction:

void crash_handler(int sig) {
    const char* msg = "crashed\n";
    write(STDERR_FILENO, msg, 8);
    _exit(1);
}

4. Expecting file and line numbers automatically

backtrace() usually gives addresses and maybe symbol names. File names and line numbers often require:

  • debug symbols
  • external resolution tools like addr2line
  • additional symbol-processing steps

5. Losing frames because of optimization

Highly optimized builds may:

  • inline functions
  • remove frames
  • reorder code

This can make traces harder to read.

Avoid this by testing crash reporting with debug builds first.

6. Catching only one type of crash

A program may fail due to different signals.

Example:

  • SIGSEGV for invalid memory access
  • SIGABRT for abort()
  • SIGFPE for arithmetic faults like divide-by-zero on some systems

7. Trying to continue after a fatal crash

Broken idea:

void crash_handler(int sig) {
    // print something
    return; // dangerous
}

After a fatal signal, continuing execution is usually unsafe. Terminate and restart instead.

Comparisons

ApproachWhat it doesProsCons
backtrace() in-processCaptures current call stack inside the programSimple, quick to add, useful for debuggingLimited portability, not fully safe in crash context
Core dumpOS writes process memory/state to a fileVery detailed for postmortem debuggingLarger files, setup required, harder for end users
External crash reporterSeparate system or library records crashesMore robust in productionMore setup and platform-specific work
Logging onlyWrites recent app events before crashEasy context for reproductionDoes not show actual call stack

backtrace_symbols() vs backtrace_symbols_fd()

FunctionOutput styleNotes
backtrace_symbols()Returns an array of stringsEasier to process, but may allocate memory
backtrace_symbols_fd()Writes directly to a file descriptorSimpler for crash situations because it avoids returning allocated strings

Signal handler vs external debugger

OptionBest forLimitation
Signal handlerAutomatic crash capture in deployed appsLess reliable after severe memory corruption
Debugger (gdb)Local development and deep investigationNot practical for end users

C++ exceptions vs crash signals

SituationExampleStack trace approach
C++ exceptionthrow std::runtime_error("bad input")Handle with try/catch, optionally log current context
Fatal crashnull pointer write, invalid memory accessSignal/OS-level crash handling or external crash reporting

Exceptions are not the same as segmentation faults. A catch block will not reliably handle low-level process crashes.

Take Quiz

Cheat Sheet

Quick reference

Linux/GCC stack trace basics

#include <execinfo.h>

void* frames[64];
int size = backtrace(frames, 64);
backtrace_symbols_fd(frames, size, STDERR_FILENO);

Useful headers

#include <execinfo.h>
#include <signal.h>
#include <unistd.h>
#include <cstdlib>

Typical signals to consider

  • SIGSEGV — invalid memory access
  • SIGABRT — program aborted
  • SIGFPE — arithmetic fault
  • SIGILL — illegal instruction

Helpful compile flags

g++ -g -O0 -rdynamic file.cpp -o app

Key rules

  • A stack trace shows the active function call path.
  • -rdynamic helps preserve symbol names in the executable.
  • Optimizations can reduce trace readability.
  • Signal handlers should do minimal work.
  • Prefer _exit() over complex shutdown logic after fatal crashes.
  • Cross-platform crash reporting needs platform-specific code.

Good minimal crash handler idea

void crash_handler(int sig) {
    void* frames[64];
    int size = backtrace(frames, 64);
    backtrace_symbols_fd(frames, size, STDERR_FILENO);
    _exit(1);
}

Important limitation

This is useful for learning and simple reporting, but it is not the most robust production crash-reporting strategy.

FAQ

How do I get a stack trace in C++ on Linux with GCC?

A common approach is to use backtrace() and backtrace_symbols_fd() from execinfo.h, usually inside a signal handler for crashes like SIGSEGV.

Why are my function names missing from the stack trace?

You may need to compile with -g and -rdynamic. Without symbols, the trace may show only raw addresses.

Can I use the same stack trace code on Windows and macOS?

Not reliably. The overall idea is the same, but the APIs and crash-reporting mechanisms differ by platform.

Does a C++ try/catch block handle segmentation faults?

No, not in the normal portable sense. try/catch handles C++ exceptions, not low-level crash signals like SIGSEGV.

Is it safe to generate a stack trace after a crash?

It can work, but it is not guaranteed to be fully safe because the process may already be corrupted. Keep crash handlers minimal.

What is the difference between a stack trace and a core dump?

A stack trace is a summary of active function calls. A core dump is a much more complete snapshot of process memory and state.

How can I get file names and line numbers from stack trace addresses?

Usually by compiling with debug symbols and resolving addresses using tools like addr2line or a debugger.

Should I store the crash report and send it on the next launch?

Yes, that is a common pattern. It avoids doing too much work during the crash itself and gives the user a chance to approve sending the report.

Take Quiz

Mini Project

Description

Build a small Linux/GCC C++ program that installs a crash handler, captures a stack trace when a crash happens, and writes the trace to a text file. This demonstrates the core idea behind automatic crash reporting: collect useful debugging data at failure time, then inspect or send it later.

Goal

Create a C++ program that intentionally crashes, records a stack trace to a file, and exits safely after the crash.

Requirements

  • Install handlers for at least SIGSEGV and SIGABRT.
  • Capture the current stack trace when a crash occurs.
  • Write the stack trace to a file named crash.log.
  • Trigger a crash through a clear test function.
  • Compile the program with flags that improve stack trace readability.
Take Quiz

Keep learning

Related questions

View more