How Do I Debug Segmentation Faults in C and C++?

Type: Software Reference Confidence: 0.95 Sources: 7 Verified: 2026-02-20 Freshness: stable

TL;DR

Constraints

Quick Reference

# Cause Likelihood Signature Fix
1 NULL pointer dereference ~25% Crash at address 0x0; si_addr=(nil) Check pointer for NULL before dereference; initialize pointers [src2]
2 Array/buffer out-of-bounds ~20% Crash near valid heap/stack; Valgrind: "Invalid read/write of size N" Use bounds checking; replace char[] with std::vector [src2]
3 Use-after-free (dangling pointer) ~15% ASan: "heap-use-after-free"; Valgrind: "Invalid read/write" Set pointer to NULL after free()/delete; use smart pointers [src2, src5]
4 Uninitialized pointer (wild pointer) ~12% Crash at random address; si_addr is garbage value Always initialize pointers to NULL/nullptr [src1]
5 Stack overflow (deep recursion) ~10% Crash near stack boundary; GDB frame count is huge Add recursion base case; increase stack with ulimit -s; convert to iteration [src2]
6 Double free ~8% free(): double free detected; ASan: "double-free" Set pointer to NULL after free; use smart pointers [src2]
7 Writing to read-only memory ~5% Crash on string literal address; char *s = "hello"; s[0] = 'H'; Use const char* for literals; use char s[] = "hello" for mutable [src2, src7]
8 Mismatched alloc/dealloc ~3% ASan: "alloc-dealloc-mismatch"; malloc freed with delete Match pairs: malloc/free, new/delete, new[]/delete[] [src2]
9 Format string mismatch ~2% printf("%d", string_ptr) — crash or garbage output Use -Wformat flag; match format specifiers to argument types [src1]

Decision Tree

START — Program crashes with "Segmentation fault (core dumped)"
├── Do you have the source code?
│   ├── YES → Recompile: gcc -g -fsanitize=address -fno-omit-frame-pointer
│   │   ├── ASan prints error report?
│   │   │   ├── YES → Read report: file:line, access type, alloc/free stack
│   │   │   │   ├── "heap-use-after-free" → Cause #3: dangling pointer
│   │   │   │   ├── "heap-buffer-overflow" → Cause #2: array out-of-bounds
│   │   │   │   ├── "stack-buffer-overflow" → Cause #2: local array overflow
│   │   │   │   ├── "double-free" → Cause #6
│   │   │   │   └── "SEGV on unknown address 0x000000000000" → Cause #1: NULL
│   │   │   └── NO → Run under GDB (see step-by-step below)
│   │   └── ASan unavailable (old compiler)?
│   │       └── valgrind --tool=memcheck ./program
│   └── NO (binary only) → Valgrind or core dump + GDB
│
├── Is there a core dump file?
│   ├── YES → gdb ./program core → bt → frame N → info locals
│   └── NO → ulimit -c unlimited → reproduce → analyze core
│
└── Crash is intermittent / hard to reproduce?
    ├── Valgrind for sustained testing (2x slowdown, no recompile)
    ├── ASan for always-on detection (2x slowdown, needs recompile)
    └── Multithreaded? → gcc -fsanitize=thread

Step-by-Step Guide

1. Compile with debug symbols and warnings

Always start by recompiling with maximum diagnostics. [src1, src2]

# C
gcc -g -Wall -Wextra -Werror -O0 program.c -o program

# C++
g++ -g -Wall -Wextra -Werror -O0 -std=c++17 program.cpp -o program

Verify: file program → should show "with debug_info, not stripped"

2. Run under GDB to find the crash location

GDB stops at the exact instruction that triggered SIGSEGV. [src1, src5]

gdb ./program
(gdb) run
# ... program crashes ...
(gdb) bt                     # Backtrace — full call stack
(gdb) bt full                # Backtrace with local variables
(gdb) frame 0                # Go to the crashing frame
(gdb) info locals            # Show all local variables
(gdb) print ptr              # Inspect a specific variable
(gdb) list                   # Show source around crash point

Verify: bt output → the top frame shows the file and line of the crash.

3. Enable core dumps for post-mortem debugging

Analyze crashes after the fact when you cannot run interactively. [src5]

# Enable core dumps
ulimit -c unlimited

# Optional: set core dump location (Linux)
echo "/tmp/core.%e.%p" | sudo tee /proc/sys/kernel/core_pattern

# Load core dump in GDB
gdb ./program /tmp/core.program.12345
(gdb) bt
(gdb) bt full
(gdb) frame 3
(gdb) info locals

Verify: ls -la /tmp/core.* → core file exists and is non-empty.

4. Use Valgrind Memcheck to find the root cause

Valgrind finds memory errors that happen BEFORE the crash. No recompilation needed. [src4, src5]

valgrind --tool=memcheck \
         --leak-check=full \
         --show-leak-kinds=all \
         --track-origins=yes \
         --verbose \
         ./program

Verify: Valgrind exit summary shows "ERROR SUMMARY: 0 errors" when all memory errors are fixed.

5. Use AddressSanitizer (fastest, most detailed)

ASan is built into GCC (>= 4.8) and Clang (>= 3.1). ~2x slowdown vs Valgrind's ~20x. [src3, src6]

# Compile with ASan
gcc -g -fsanitize=address -fno-omit-frame-pointer -O1 program.c -o program

# Run — ASan prints detailed error report on crash
./program

# Combine with GDB for interactive debugging
ASAN_OPTIONS=abort_on_error=1 gdb ./program
(gdb) run
(gdb) bt

Verify: Run with ASAN_OPTIONS=detect_leaks=1 → ASan also reports memory leaks at exit.

6. Use UndefinedBehaviorSanitizer for subtle bugs

Many segfaults are caused by undefined behavior that corrupts memory silently. [src6]

# Compile with both ASan and UBSan
gcc -g -fsanitize=address,undefined -fno-omit-frame-pointer program.c -o program

Verify: ./program → UBSan prints runtime errors with file:line:col precision.

Code Examples

C: common segfault patterns and fixes

// Input:  None (demonstration of segfault causes)
// Output: Shows 4 common segfault patterns with correct alternatives

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// Pattern 1: NULL pointer dereference
void null_deref_bad(void) {
    int *ptr = NULL;
    *ptr = 42;  // SEGFAULT
}
void null_deref_good(void) {
    int *ptr = malloc(sizeof(int));
    if (ptr == NULL) { perror("malloc"); return; }
    *ptr = 42;
    free(ptr);
}

// Pattern 2: Use after free
void uaf_bad(void) {
    int *ptr = malloc(sizeof(int));
    *ptr = 42;
    free(ptr);
    printf("%d\n", *ptr);  // SEGFAULT — use after free
}
void uaf_good(void) {
    int *ptr = malloc(sizeof(int));
    *ptr = 42;
    printf("%d\n", *ptr);
    free(ptr);
    ptr = NULL;  // Prevent accidental reuse
}

// Pattern 3: Writing to string literal
void literal_bad(void) {
    char *s = "hello";
    s[0] = 'H';  // SEGFAULT — read-only memory
}
void literal_good(void) {
    char s[] = "hello";  // Mutable copy on stack
    s[0] = 'H';          // Safe
}

C++: modern segfault prevention with smart pointers

// Input:  None (C++ safety patterns)
// Output: Smart pointer usage to prevent segfaults

#include <memory>
#include <vector>
#include <iostream>

// GOOD: unique_ptr — automatic cleanup, no double-free
void unique_ptr_good() {
    auto data = std::make_unique<int[]>(100);
    data[0] = 42;
    // Freed when scope exits, even on exception
}

// GOOD: shared_ptr — safe shared ownership
void shared_ptr_good() {
    auto data = std::make_shared<std::vector<int>>(100, 0);
    auto alias = data;  // Reference count = 2
}

// GOOD: .at() for bounds-checked access
void bounds_checked() {
    std::vector<int> v = {1, 2, 3};
    try {
        int val = v.at(10);  // Throws std::out_of_range
    } catch (const std::out_of_range& e) {
        std::cerr << "Bounds error: " << e.what() << "\n";
    }
}

Bash: automated segfault debugging workflow

#!/bin/bash
# Input:  Source file path (C or C++)
# Output: ASan + Valgrind diagnostics

SRC="$1"
BIN="${SRC%.c*}.debug"

if [[ -z "$SRC" ]]; then
    echo "Usage: $0 <source.c|source.cpp>"; exit 1
fi

if [[ "$SRC" == *.cpp ]]; then CC="g++ -std=c++17"; else CC="gcc"; fi

echo "=== Step 1: Compile with ASan + UBSan ==="
$CC -g -fsanitize=address,undefined -fno-omit-frame-pointer \
    -Wall -Wextra -O1 "$SRC" -o "$BIN"

echo "=== Step 2: Run with AddressSanitizer ==="
ASAN_OPTIONS=detect_leaks=1:abort_on_error=0 ./"$BIN"

echo "=== Step 3: Run with Valgrind Memcheck ==="
$CC -g -Wall -Wextra -O0 "$SRC" -o "${BIN}.valgrind"
valgrind --tool=memcheck --leak-check=full \
         --show-leak-kinds=all --track-origins=yes \
         ./"${BIN}.valgrind"

Anti-Patterns

Wrong: Dereferencing without NULL check

// BAD — ptr could be NULL if function returns NULL [src1, src2]
int *ptr = find_record(id);
printf("Value: %d\n", *ptr);  // SEGFAULT if NULL

Correct: Always check before dereference

// GOOD — defensive programming prevents crash [src1, src2]
int *ptr = find_record(id);
if (ptr != NULL) {
    printf("Value: %d\n", *ptr);
} else {
    fprintf(stderr, "Record %d not found\n", id);
}

Wrong: Using pointer after free

// BAD — classic use-after-free [src2, src5]
char *buf = malloc(256);
snprintf(buf, 256, "data");
free(buf);
printf("Buffer: %s\n", buf);  // SEGFAULT or garbage

Correct: Null the pointer after free

// GOOD — pointer safely nulled [src2, src5]
char *buf = malloc(256);
if (buf == NULL) { perror("malloc"); return; }
snprintf(buf, 256, "data");
printf("Buffer: %s\n", buf);
free(buf);
buf = NULL;

Wrong: Returning pointer to local variable

// BAD — stack memory invalid after return [src1]
int* get_data(void) {
    int arr[10] = {1, 2, 3};
    return arr;  // Returns address of local variable!
}
int *data = get_data();
printf("%d\n", data[0]);  // SEGFAULT — stack frame gone

Correct: Heap-allocate for returned data

// GOOD — heap allocation survives function return [src1]
int* get_data(void) {
    int *arr = malloc(10 * sizeof(int));
    if (arr == NULL) return NULL;
    arr[0] = 1; arr[1] = 2; arr[2] = 3;
    return arr;  // Caller must free()
}

Wrong: Modifying a string literal

// BAD — string literals in read-only memory [src2, src7]
char *greeting = "hello world";
greeting[0] = 'H';  // SEGFAULT

Correct: Use a char array for mutable strings

// GOOD — array copies literal to mutable stack memory [src2, src7]
char greeting[] = "hello world";
greeting[0] = 'H';  // Safe

Common Pitfalls

Diagnostic Commands

# === Compile with sanitizer support ===
gcc -g -fsanitize=address -fno-omit-frame-pointer -O1 program.c -o program
gcc -g -fsanitize=address,undefined -fno-omit-frame-pointer program.c -o program
clang -g -fsanitize=address -fno-omit-frame-pointer -O1 program.c -o program

# === Enable core dumps ===
ulimit -c unlimited
echo "/tmp/core.%e.%p" | sudo tee /proc/sys/kernel/core_pattern

# === GDB commands ===
gdb ./program                      # Start debugger
gdb ./program core                 # Load core dump
# run / bt / bt full / frame N / info locals / print var / x/16xb ptr

# === Valgrind ===
valgrind ./program
valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes ./program

# === Check binary for debug info ===
file ./program
readelf -S ./program | grep debug

# === Check for core dumps (systemd) ===
coredumpctl list
coredumpctl gdb

Version History & Compatibility

Tool Version Key Feature Notes
GDB >= 7.0 (2009) Python scripting, reverse debugging Available on all Linux distros [src1]
Valgrind >= 3.10 (2014) Full C++11 support ~20x slowdown; no recompile needed [src4]
ASan (GCC) >= 4.8 (2013) Basic ASan support Requires recompilation [src6]
ASan (Clang) >= 3.1 (2012) Original ASan implementation Generally better ASan support than GCC [src3]
UBSan (GCC) >= 4.9 (2014) Undefined behavior detection -fsanitize=undefined [src6]
MSan (Clang) >= 3.3 (2013) Uninitialized memory detection Clang only; not in GCC
TSan >= GCC 4.8 / Clang 3.2 Data race detection For multithreaded segfaults

When to Use / When Not to Use

Use When Don't Use When Use Instead
Program crashes with "Segmentation fault" Crash message is "Aborted" (SIGABRT) Check for assert() failures or abort() calls
GDB backtrace shows your code Crash inside system library (libc, libpthread) Check your arguments to library calls
Need to debug production binary (no source) Have source and can recompile Use ASan — faster and more detailed than Valgrind
Intermittent crash in multithreaded code Single-threaded memory bug Use ThreadSanitizer: -fsanitize=thread
Linux or macOS environment Windows environment Visual Studio debugger or WinDbg

Important Caveats

Related Units