C++ Segmentation Fault: Five Causes and Debugging with GDB
Key takeaways
A segfault is the OS rejecting an access to memory your process has no valid mapping for, and the crashing line is often far from the real bug. The post walks through each cause with examples, then shows how core dumps, GDB backtraces and AddressSanitizer point to the original mistake.
Introduction: “Segmentation fault (core dumped)”
A segmentation fault (often called a segfault) is a runtime failure: the operating system or CPU reports an invalid access to the process’s virtual address space, and the runtime typically delivers SIGSEGV (signal number 11 on Linux and other POSIX systems). The message you see in the shell:
Segmentation fault (core dumped)
reflects that the default disposition for an unhandled SIGSEGV is to terminate the process, optionally leaving a core file (when ulimit and core_pattern allow it) that a debugger can load for post‑mortem analysis.
This article is organized by cause: what each kind of bad access looks like in code, why it crashes where it does, and how to fix it. The step-by-step workflow for capturing and reading core dumps in GDB and LLDB, including production core collection, is in C++ Segmentation Fault and Core Dumps: GDB/LLDB Debugging. This article covers:
- What a segfault is at the hardware/OS level (virtual memory, protection, and signal 11).
- Common causes with minimal reproducible C++ and concrete fixes: null dereference, use‑after‑free, buffer overflow, stack overflow, uninitialized pointers, and related UB that often manifests as a crash.
- How to tell the causes apart from a backtrace, and how Valgrind memcheck and AddressSanitizer point at the original mistake.
- Prevention (smart pointers, bounds checking, static analysis) and the distinction between stack and heap failures.
- Multithreaded crashes (races) and how they differ from single‑threaded access violations.
- Platform notes (Linux, macOS, Windows/WSL) and a tool comparison plus CI and production considerations.
Environments: Examples assume GCC/Clang on Linux or macOS. On Windows, use MSVC with Address Sanitizer or /RTC where applicable, or develop under WSL2 for a Linux‑like toolchain.
What is a segmentation fault?
A segfault is not a C++ language exception. It is an OS/runtime event: the process attempted to read, write, or execute from a virtual address that is not mapped, is mapped with incompatible permissions, or is otherwise disallowed. On POSIX systems this usually surfaces as SIGSEGV; the C runtime maps that to the shell message Segmentation fault.
Typical user‑visible triggers include:
- Dereferencing null or a trash pointer value.
- Accessing freed heap memory (use‑after‑free) or a dangling pointer to stack storage.
- Buffer overruns that corrupt control data or land on a guard page.
- Stack overflow (often infinite or very deep recursion, or enormous stack locals).
Because many of these situations are also undefined behavior in C++, the program might appear to work in one build, then crash in another optimization level, or on a different machine. See also: undefined behavior.
Memory protection and virtual addresses
Modern systems give each process a private virtual address space. The kernel and CPU (via the MMU) translate virtual addresses to physical RAM with page granularity (commonly 4 KiB, sometimes larger with huge pages). Each page has flags such as read, write, and execute. When your instruction tries to access an address, the hardware checks the mapping; if the access violates protection or the address is unmapped, the CPU raises a fault, which the OS converts into a signal for user space (for user mode code) or a kernel panic (for bad kernel code on some paths).
Why this matters for C++: your pointers are just integers interpreted as addresses. The compiler and standard library will not, in general, “stop” a bad access at the language level. Tools like ASan and Valgrind insert instrumentation or use binary translation to detect many invalid patterns before the hardware would fault—often with precise line numbers.
SIGSEGV and signal 11
On Linux, run:
kill -l | grep -i segv
or consult man 7 signal. You will see that SIGSEGV is signal 11 (implementation details can vary, but 11 is the de facto value on Linux x86_64 for SIGSEGV).
A segfault is delivered when a thread executes an invalid access. A handler may be installed for SIGSEGV, but in general recovering from a bad memory access in arbitrary C++ code is unsafe. Debuggers, fault handlers, and some VM tricks may catch specific cases, but for application code the appropriate response is: fix the bug, not catch and continue.
Related signals: SIGBUS (bus error) can occur for misaligned access, invalid physical mapping, or other platform‑specific bus faults—sometimes confused with SIGSEGV in bug reports. See the FAQ in the frontmatter.
Common causes (overview)
Instead of another grid: here is how these failures feel when you are tired and the coffee is cold.
Null dereference is the honest crash: the fault lands at a tiny address, and print some_ptr often shows 0x0. Use‑after‑free and dangling pointers are the opposite of honest—the bad access can be pages away from the delete, and ASan or Valgrind earn their keep by naming a line that is closer to the truth than the allocator’s meltdown.
Buffer overflow sometimes explodes immediately, sometimes corrupts metadata and detonates three function calls later. Stack overflow—and here is the joke for people who have been through it—stack overflow? No, not the website: the thread stack actually runs out, and the backtrace starts to look like a broken record. Uninitialized pointers are the lottery: sometimes a segfault, sometimes silent corruption until a release build on a different CPU.
The sections below go deeper with code and mitigations.
Cause: null pointer dereference
Problem: you load or store through a pointer equal to nullptr (or NULL in C).
Broken:
int* p = nullptr;
*p = 42; // SIGSEGV: write to 0x0
Fix patterns:
- Check before use in defensive code paths.
- API design: return
std::optional<T&>orT*with documented nullness; avoid “maybe null, maybe not” without comments. - Smart defaults: use references where null is not a valid state, or
not_null(Guidelines / gsl) in codebases that adopt it.
Example with check:
void assign(int* p) {
if (p == nullptr) {
// handle: throw, return error, or assert in debug
return;
}
*p = 42;
}
Real‑world note: a null dereference in a hot path may be a logic bug (invariant broken) rather than a missing if-statement. Prefer fixing why the pointer is null: failed allocation (rare in modern 32+ bit address spaces for small objects), or missing initialization.
Cause: use‑after‑free and dangling pointers
Problem: memory is deallocated, but a raw pointer or reference still aliases it. Any read/write is undefined behavior; often a segfault when the allocator reuses the address or metadata is corrupted.
Broken:
int* p = new int{7};
delete p;
*p = 1; // UAF: undefined behavior, often ASan/segfault
Broken (dangling to stack):
int* make_bad() {
int x = 0;
return &x; // x dies when function returns: dangling
}
void use() {
int* p = make_bad();
*p = 1; // stack UAF: UB, often crash
}
Fixes:
- Prefer
std::unique_ptr,std::shared_ptr(when shared ownership is real), and containers (std::vector,std::string) over manualnew/delete. - Narrow pointer lifetime to a scope that encloses all uses; avoid returning pointers to inner stack variables.
- For observer patterns, use non-owning pointers only when the pointee’s lifetime is provably longer—often a design smell without clear documentation and runtime checks in debug builds.
Fixed shape (heap):
#include <memory>
void ok() {
auto p = std::make_unique<int>(7);
*p = 1; // valid until p is destroyed
}
Cause: buffer overflow and out‑of‑bounds access
Problem: a write extends past the end of an array or the allocated capacity. This may corrupt heap metadata, adjacent objects, or stack canaries—or hit an unmapped page and fault immediately, depending on layout.
Broken:
void stack_overflow_c() {
int a[3] = {1, 2, 3};
a[3] = 4; // out of bounds: undefined behavior
}
Fixes:
- Use
std::vectorandat()when you want checked access in debug/exception contexts. - Bounds in loops: prefer range‑based
for, algorithms, andstd::array::size(). - Turn on ASan in CI for tests.
With std::vector and checked access:
#include <vector>
void safe(std::size_t i) {
std::vector<int> v{1, 2, 3};
if (i < v.size()) {
v[i] = 99; // or v.at(i) to throw on bad i
}
}
ASan and Valgrind will report stack-buffer-overflow and heap-buffer-overflow with line numbers; the default hardware fault may give no line unless you have symbols and a core.
Cause: stack overflow
Stack overflow? No, not the website—we mean the real kind: the thread’s stack is finite (often a few MB on main thread; smaller on some embedded/thread pools). Deep recursion without a base case, or huge stack allocations (int a[1'000'000]), can exceed the limit.
Broken:
void recurse_forever() {
recurse_forever(); // until stack limit → often SIGSEGV
}
Fixes:
- Add base cases; prove recursion depth.
- Move large arrays to the heap (
std::vector,std::unique_ptr[]). - Convert recursion to iteration where possible.
Shape with heap for large data:
#include <vector>
void work(std::size_t n) {
std::vector<int> buf(n); // allocation on heap, small frame
// ...
}
In GDB, a stack overflow often shows a repeating set of stack frames, or a fault inside low‑level stack check routines—exact behavior is platform and libc dependent.
Cause: uninitialized pointers and indeterminate values
Problem: reading a pointer (or, more generally, automatic storage) before it is set yields indeterminate values in many cases. Dereferencing a garbage address may segfault if it maps to a bad page, or may corrupt memory “silently” if it points into valid but wrong storage.
Broken (illustrative; compile may still warn with -Wuninitialized):
int* p;
// *p = 1; // catastrophic if allowed: UB
Fixes:
- Initialize at declaration:
T* p = nullptr;orauto p = &known_object;. - Enable high warning levels:
-Wall -Wextra -Wuninitialized, and treat warnings as errors in CI. - Valgrind can flag uninitialized values used in branches/addresses; MSVC
/RTC1in Debug builds for stack checks.
Safer pattern:
int x = 0;
int* p = &x; // always valid for x's lifetime
Optional: bad casts and object lifetime
Downcasting in polymorphic hierarchies with static_cast to the wrong dynamic type can produce undefined behavior. Results include corrupt vtable reads and segfaults. Prefer:
dynamic_castto pointers or references in polymorphic hierarchies, and check fornullptr.
struct Base { virtual ~Base() = default; };
struct Derived : Base { int d = 1; };
void f(Base* b) {
if (auto* d = dynamic_cast<Derived*>(b)) {
(void)d->d;
}
}
Stack vs heap segfaults
Stack lives in function frames, alloca, and oversized automatic arrays; it breaks with deep recursion and “I will just put a megabyte on the stack.” Heap comes from new, malloc, and std::vector’s backing store; it fails with unbounded growth (often OOM before a classic segfault on some systems) or ownership mistakes.
UAF on the stack is the classic “return a pointer to a local”; on the heap it is “delete then touch.” ASan will say stack-buffer-overflow versus heap-buffer-overflow or heap-use-after-free—different words, same family argument. In a core file, stack overflow often shows repeating frames; heap corruption often lands you in the allocator or in a *p long after the real bug.
Heuristic for triage: if ASan points to stack in the first bad access, look at stack depth and VLA/huge local patterns. If it is heap, look at ownership and index logic.
Multithreaded segfaults: races and data races
A data race (two un‑synchronized accesses, at least one write) is undefined behavior in C++. It does not “always” crash—on some runs you get wrong answers; on others a segfault if memory is torn or invariants are broken.
Example pattern (buggy):
#include <thread>
int* g = nullptr;
void writer() {
delete g; // if another thread still reads, UB
g = nullptr;
}
void reader() {
if (g) {
*g = 1; // can race with writer
}
}
Mitigations:
- Mutexes, atomics with clear ordering, and message passing instead of ad hoc shared state.
- ThreadSanitizer (
-fsanitize=thread) for data races, separate from AddressSanitizer in many builds (they can be combined in specific configurations, but your project should follow supported compiler docs).
Segfaults that only appear under load (many cores, different timings) are a strong signal for a race or lifetime issue shared across threads.
Telling the causes apart from a core dump
When the cause is not obvious from the code, look at the state at the moment of the crash. Build with -g, allow core files (ulimit -c unlimited; check /proc/sys/kernel/core_pattern if none appears), and open the core with gdb -q ./your_binary core. The backtrace and one print usually narrow it to one of the causes above:
(gdb) bt # where the bad access happened
(gdb) frame 0
(gdb) print ptr # 0x0 -> null dereference
(gdb) thread apply all bt # several threads in the same data -> suspect a race
A pointer of 0x0 is a null dereference. A plausible heap address that crashes anyway suggests use-after-free, which ASan confirms with the allocation and free stacks. The same function repeated hundreds of times in bt is a stack overflow. A crash inside malloc or free almost always means the heap was corrupted earlier, so the useful next step is an ASan run, not a closer look at the allocator frame. Enabling cores under systemd, the LLDB equivalents on macOS, and separating debug symbols from release binaries are covered step by step in the core dump debugging walkthrough.
On Windows, the equivalent of a core is a minidump (collected by WER or procdump) analyzed in WinDbg or Visual Studio; the reasoning about stacks and threads is the same.
Valgrind memcheck (Linux, macOS on supported CPUs)
Valgrind runs the program in a virtualized execution environment, intercepting memory operations. It is slower (often 10x–50x) and does not require recompilation, but the tool shines on code where no sanitizer is available or you need a broad scan.
Typical run:
valgrind --leak-check=full --show-leak-kinds=all ./your_binary
What to expect:
- Invalid read/write of heap, stack, or freed memory.
- Use of uninitialized values (when the branch depends on uninit, etc.).
- Leak reports (leaks are not always segfaults, but they matter next to UAF in triage).
Interpreting output: look for the first error—later errors are often follow‑on corruption. Re-run with a smaller test after fixing the first report.
Limitations: does not support every platform (Apple Silicon has had a patchy story—verify your team’s current Valgrind support); not a substitute for ASan in fast developer loops, but a strong cross‑check in CI nightly jobs if runtime allows.
AddressSanitizer (ASan) setup
ASan is a compiler instrumentation pass. It catches many invalid accesses at the moment they happen, with source lines (when you compile with -g).
Command line (GCC/Clang):
g++ -g -O1 -fsanitize=address -fno-omit-frame-pointer -o app main.cpp
./app
CMake (target):
target_compile_options(myapp PRIVATE -fsanitize=address -g -O1)
target_link_options(myapp PRIVATE -fsanitize=address)
What ASan flags include:
- Heap buffer overflow/underflow, use‑after‑free, double free.
- Stack and global buffer overruns in many cases.
- Some leak detection as LSan (optionally included).
Caveat: the crash path must execute for ASan to report it. A latent bug in an untested branch can still reach production. Combine with test coverage and fuzzing for critical parsers.
Suppressions: in large codebases, you may add ASan suppression files for known third‑party false positives; treat suppressions as debt and document owners.
Example ASan error types (illustrative)
heap-buffer-overflowstack-buffer-overflowuse-after-poison(allocator patterns)heap-use-after-free
Related: for undefined behavior besides memory, consider UBSan (-fsanitize=undefined); it complements ASan. Thread Sanitizer is separate: enable with distinct flags and test plans.
Platform differences: Linux, macOS, Windows
Linux is the straight story: fault becomes SIGSEGV, cores follow ulimit and core_pattern, ASan on GCC/Clang is boring in a good way, Valgrind is happiest here, and /proc/self/maps is your friend when scripts need addresses.
macOS speaks the same signal family but you will meet EXC_BAD_ACCESS in lldb; cores land under policies that change across OS versions; Clang ASan tracks Xcode release notes; Valgrind support depends on arch—verify before you bet a release on it; vmmap and sample fill the same mental slots as Linux mapping and profiling.
Windows (native) wraps many faults as access violation 0xC0000005 under SEH; dumps are often WER, procdump, or Visual Studio “Save dump”; MSVC ASan has improved—read the version you actually ship; Valgrind is not the move; VMMap and WinDbg !address are the mapping story.
WSL2 on a Windows host gives you a Linux-shaped toolchain without pretending the native loader is the same as Ubuntu on bare metal—still validate what you ship.
Practical approach: use WSL2 or MSYS2+Clang if you need Linux‑style ASan/Valgrind on a Windows dev machine, while still validating MSVC builds in CI if you ship Windows binaries.
Tools comparison: GDB vs Valgrind vs ASan
GDB/LLDB are for when you already have a corpse: post‑mortem cores, live runs, all threads, registers, and the cold comfort of symbols. They do not invent UB for you—optimized code will still hurt your feelings.
ASan is the daily driver: rebuild, run tests, read a line number, fix the first report, go home earlier. It is not TSan; it will not pat you on the head for data races.
Valgrind is the night shift: no rebuild, broad sweeps on Linux, slow enough to brew tea. On the wrong CPU or OS it simply is not there—plan accordingly.
UBSan catches some non‑memory UB; you have to wire flags like you mean it. TSan is the multithreaded specialist—separate build, real runtime cost. Static analysis before commit catches a different class of mistakes; it also ships false positives you will negotiate with in code review.
A mature workflow uses ASan+UBSan in CI for unit/integration tests, GDB/LLDB for reproducing the one crash a customer sent, and Valgrind when you have a prebuilt binary and time overnight.
CI integration: running with sanitizers
Goals: fail fast in PR pipelines; keep job time bounded; retain repro artifacts (logs, ASan report).
Common recipe (GitHub Actions style sketch):
- name: Configure
run: |
cmake -B build -DCMAKE_CXX_FLAGS="-g -O1 -fsanitize=address -fno-omit-frame-pointer" \
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address"
- name: Test
run: ctest --output-on-failure
env:
ASAN_OPTIONS: "detect_stack_use_after_return=1:check_initialization_order=1"
Notes:
- OOM in CI: ASan increases memory. Parallel test jobs may need reduced
-jor more RAM. - Flaky tests: races may appear only in TSan jobs—separate the pipeline stage.
- Cache: sanitized object files should not be mixed with release without clean rebuild.
ASAN_OPTIONS (non‑exhaustive): halt_on_error=0 for continuous discovery (use sparingly; prefer strict failure for determinism in PR builds).
Production crashes
AddressSanitizer is a development and CI tool; production binaries usually run without it. What production needs instead is a path from a crash back to a source line: cores (or minidumps) written to controlled storage, build IDs and separately stored debug symbols that match each release, and the version and command line recorded with each crash. Setting that up with core_pattern, coredumpctl and symbol packages is part of the core dump walkthrough.
Prevention: smart pointers, bounds checking, and process
Code:
std::unique_ptrfor exclusive ownership,std::shared_ptronly when you truly have shared lifetimes, avoid cycles (weak_ptr when needed).std::vector/std::array+ algorithms over rawT[].- Span (C++20) to pass non-owning views with length when replacing pointer+size pairs.
- Avoid returning references to static locals unless intentional and documented; avoid returning pointers to inner stack in any function.
Build:
- Warnings as errors,
-Werror=…incrementally, MSVC/W4and/permissive-. - ASan/UBSan in CI, nightly Valgrind for critical paths.
- Code review checklist: ownership, threading, errors from I/O, pointer invariants.
Testing:
- Fuzzing for parsers (
libFuzzer, AFL++). - Stress tests and load tests to shake out races and allocator reuse patterns.
Quick reference: symptoms by cause
Null: you print p in GDB and the universe says 0x0. Fix the invariant, use std::optional, or a not_null policy if your codebase uses one.
Dangling / UAF: ASan or Valgrind points at a line you did not expect; a crash just after a callee returns. Prefer unique_ptr, never return addresses into dead stack.
Stack (the hardware kind again): a bt that stutters, or a frame the size of a small planet. Stack overflow? No, not the website—iterate, put big data on the heap, and raise stack limits only when the design truly needs it.
Overrun: ASan shouts *-buffer-overflow. Teach loops about size(), use at() when “throw on bad” is allowed, and pass lengths with std::span when you are honest about APIs.
Uninit: Valgrind’s “Conditional jump or move depends on uninitialised value(s)” is a mood. Initialize at declaration, crank warnings, treat -Wuninitialized as a gift.
Races: flaky tests, crashes that appear only under production load, different stacks on each core. TSan, mutex discipline, and a shutdown review beat heroic single-threaded thinking.
Bad cast in a polymorphic hierarchy: use dynamic_cast and check for nullptr, or a variant and std::visit if that matches your design.
OS-specific cores: the binary is the same but core_pattern and container settings are not—reproduce in the same class of host you ship.
More questions
Q. Why does the fault address not match my variable?
A. Optimized code, register promotion, and inlined functions change what you see in the debugger. Use -O0 for stepping; trust ASan’s line over guesswork when available.
Q. Can I catch SIGSEGV and continue?
A. Generally no for application logic—by then memory may be corrupt. For specialized runtimes, consult platform‑specific async‑signal‑safe rules; do not do heavy work in a signal handler.
Q. Are integer overflows the same as segfaults?
A. No in general—signed overflow is UB but often no immediate fault. UBSan finds many of these. Some overflows lead to bad sizes in allocations and then a fault later (indirectly).
Q. What about nullptr in delete?
A. delete of null is a no‑op (safe) for single objects; the danger is delete on not‑allocated or already freed memory—UB and often crash.
Related Articles
- C++ Undefined Behavior
- C++ Segmentation Fault and Core Dumps: GDB/LLDB Debugging
- AddressSanitizer and ThreadSanitizer