Quick answer: SIGSEGV (Segmentation Fault) is a signal sent by the operating system when a program tries to access memory it is not allowed to access.

This guide covers debugging native crashes with stack traces in detail. Native crashes in C and C++ game code are the hardest category of bugs to diagnose. Unlike managed exceptions that give you a clean error message and line number, native crashes manifest as signals and access violations with cryptic addresses. This guide covers the signals, the tools, and the techniques for turning a native crash into a fixed bug.

Crash Signals and What They Mean

On Unix-based systems (Linux, macOS, Android), the operating system sends a signal to your process when a fatal error occurs. Each signal indicates a different category of problem:

SIGSEGV (Segmentation Fault) is the most common crash signal in games. It means the process tried to access memory outside its allocated address space. Null pointer dereferences, use-after-free, and out-of-bounds array access all trigger SIGSEGV. The faulting address in the crash report helps distinguish between these causes: a very low address (near zero) indicates a null dereference, while a valid-looking heap address suggests use-after-free.

SIGABRT (Abort) means the process intentionally terminated itself, usually because an assertion failed or abort() was called. In game engines, this often comes from check macros, custom assert handlers, or the C runtime detecting heap corruption via malloc integrity checks.

SIGFPE (Floating Point Exception) occurs on integer division by zero or certain floating-point operations depending on the FPU control word. Despite the name, it covers integer arithmetic errors too.

On Windows, the equivalent is a Structured Exception. The most common is EXCEPTION_ACCESS_VIOLATION (0xC0000005), which corresponds to SIGSEGV.

Capturing Native Stack Traces

When a native crash occurs, you need to capture the call stack before the process terminates. There are two approaches: install a signal handler or use an external crash reporter.

// Custom signal handler for SIGSEGV (Linux/macOS)
#include <signal.h>
#include <execinfo.h>

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

// Register at startup
signal(SIGSEGV, crash_handler);
signal(SIGABRT, crash_handler);

This basic approach has limitations: the backtrace() function is not async-signal-safe on all platforms, and the output shows raw addresses rather than symbolicated function names. For production use, Google's Crashpad library is the standard. It runs a separate watcher process that captures a full minidump when the game crashes, avoiding the problems of running complex code inside a signal handler.

Symbol Resolution for Native Code

Native crash addresses must be resolved to function names using debug symbols. The workflow depends on your platform:

# Linux: use GDB with an unstripped binary
gdb ./MyGame core.12345
(gdb) bt full

# Linux: use addr2line for individual addresses
addr2line -e MyGame -f -C 0x4a4c2

# macOS: use lldb with a dSYM bundle
lldb --core core.12345 MyGame
(lldb) bt all

# Windows: use WinDbg with PDB files
windbg -z crash.dmp -y "C:\Symbols"

The -C flag on addr2line demangles C++ function names, turning _ZN12CombatSystem11ApplyDamageEPf into CombatSystem::ApplyDamage(float*). Always use it when working with C++ game code.

Access Violation Patterns

Access violations in games follow a few common patterns that you can identify from the faulting address:

Null dereference. The faulting address is near zero (typically 0x00000000 to 0x0000FFFF). The code dereferenced a null pointer. Check the stack frame for which pointer variable was null.

Vtable corruption. The faulting address is in a code section but points to garbage. This happens when a virtual method is called on an object whose vtable pointer has been overwritten. Common causes include buffer overflows that corrupt adjacent objects and use-after-free where the memory has been reallocated for a different type.

Stack corruption. The faulting address is inside the stack region but at an invalid location. This indicates a stack buffer overflow that overwrote the return address. The stack trace itself may be garbled because the frame pointers have been corrupted.

Using Sanitizers to Find Root Causes

The stack trace from a native crash shows you where the symptom appeared, but not necessarily where the bug is. A use-after-free crash occurs at the point of the second access, which may be far from the premature free. Memory sanitizers bridge this gap.

AddressSanitizer (ASan) detects buffer overflows, use-after-free, use-after-return, and double-free errors. Enable it with -fsanitize=address in GCC or Clang. When it detects an error, it prints the stack trace of the invalid access AND the stack trace of the original allocation or deallocation, giving you both sides of the bug.

ThreadSanitizer (TSan) detects data races between threads. Enable it with -fsanitize=thread. It reports when two threads access the same memory without synchronization and at least one access is a write. The report includes both threads' stack traces.

# Build with AddressSanitizer enabled
cmake -DCMAKE_CXX_FLAGS="-fsanitize=address -fno-omit-frame-pointer" \
      -DCMAKE_LINKER_FLAGS="-fsanitize=address" ..
make

# Run and reproduce the crash
./MyGame
# ASan prints detailed error report with both access and free stacks

The 2-3x performance overhead of ASan makes it unsuitable for shipping builds, but running it during QA testing catches the majority of native memory bugs before they reach players.

"The crash stack trace tells you where the patient collapsed. The sanitizer report tells you what made them sick."

Automating Native Crash Collection

Bugnet integrates with Crashpad to collect native crash dumps from player devices automatically. When a SIGSEGV or access violation occurs, the Bugnet SDK captures a minidump, uploads it on the next launch, and symbolicates it against your archived debug symbols. The dashboard shows the resolved stack trace with function names and line numbers, grouped by crash signature so you can prioritize the most impactful native bugs.

Related Issues

For a general introduction to reading stack traces, see our beginner's guide to game stack traces. For information on preventing the most common native crashes, read common game crashes and how to prevent them. For symbolicating crash reports after collection, see how to symbolicate crash reports from player devices.

Run AddressSanitizer in every QA test cycle. The crashes it catches in testing are the ones you will not have to diagnose from player minidumps.