← Back to DevBytes

Fix 'Segmentation fault' in C/C++: Complete Troubleshooting Guide

Understanding Segmentation Faults

A segmentation fault (often shortened to segfault) is a specific kind of memory access violation that occurs when a C or C++ program attempts to read from or write to a memory address that it is not allowed to access. The operating system's memory protection mechanism detects this illegal access and sends the SIGSEGV signal to the process, which by default causes the program to terminate abruptly with the infamous message: Segmentation fault (core dumped).

At the hardware level, the CPU's Memory Management Unit (MMU) works with the operating system to define valid memory regions for each process. When your code tries to touch an address outside these regions—whether it's address zero, a freed chunk of heap memory, or a random pointer value—the MMU triggers a page fault that the OS cannot resolve, resulting in the segmentation fault signal.

Why Understanding Segfaults Is Critical

Segmentation faults are not just annoying crashes. They are symptoms of deeper bugs in your code that can lead to:

Every C/C++ developer must master segfault diagnosis. It's not a question of if you'll encounter one, but when. This guide gives you a systematic, repeatable process to find and fix segmentation faults quickly.


Common Causes of Segmentation Faults

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Before diving into tools, let's catalog the root causes. Recognizing patterns speeds up diagnosis dramatically.

1. Dereferencing a NULL Pointer

The most classic cause. A pointer initialized to NULL or nullptr points to address zero, which is never mapped in user space.

// Example: NULL pointer dereference
int *ptr = NULL;
*ptr = 42;  // SEGFAULT — writing to address 0x0

2. Dereferencing a Dangling Pointer (Use-After-Free)

After free() or delete, the memory may be returned to the OS or reused. Accessing it afterward is undefined behavior and often causes a segfault.

// Example: use-after-free
int *ptr = new int(100);
delete ptr;
*ptr = 200;  // SEGFAULT — ptr now points to freed memory

3. Stack Overflow

Exceeding the stack limit—commonly 8 MB on Linux—by allocating huge local variables or through infinite (or very deep) recursion causes a segfault when the stack pointer hits a guard page.

// Example: infinite recursion causing stack overflow
void recurse() {
    recurse();  // Each call consumes stack; eventually segfaults
}

// Example: huge stack allocation
void bad_function() {
    char buffer[100000000];  // ~100 MB, exceeds typical stack limit
    buffer[0] = 'A';         // SEGFAULT on many systems
}

4. Buffer Overflow / Out-of-Bounds Access

Writing past the end of an array corrupts adjacent memory. This may or may not cause an immediate segfault, depending on whether the corrupted address falls in a valid page. However, if the overflow reaches an unmapped region, a segfault occurs.

// Example: buffer overflow on the heap
int *arr = new int[10];
for (int i = 0; i <= 1000; i++) {
    arr[i] = i;  // Eventually segfaults when i goes far enough out of bounds
}

5. Reading from Uninitialized / Wild Pointers

A pointer that was never initialized contains garbage data (whatever bits happened to be on the stack). Dereferencing it is a lottery—sometimes it lands in a valid address, sometimes it segfaults.

// Example: uninitialized pointer
int *p;       // p contains random stack garbage
*p = 42;      // Undefined behavior; often segfaults

6. Double Free or Invalid Free

Freeing memory that was already freed, or freeing a pointer that was not obtained from malloc/new, corrupts the heap allocator's internal data structures. Subsequent allocation or deallocation calls may segfault.

// Example: double free
int *p = new int(5);
delete p;
delete p;  // Undefined behavior; may segfault immediately or later

7. Modifying Read-Only Memory (String Literals)

String literals in C/C++ are typically stored in a read-only data segment. Attempting to modify them causes a segfault.

// Example: modifying a string literal
char *s = "Hello, world!";  // s points to read-only memory
s[0] = 'h';                 // SEGFAULT — writing to .rodata section

8. Misaligned Memory Access (Architecture-Specific)

Some architectures (like SPARC or older ARM) require aligned access. On x86/x86-64 this is usually tolerated (with a performance penalty), but it can still cause issues in certain contexts, especially with SIMD instructions.

9. Corrupted Heap Metadata

Writing past the boundaries of a heap-allocated buffer corrupts the allocator's internal bookkeeping structures (e.g., free lists, chunk headers). The crash often occurs later, during a seemingly innocent malloc or free call, making diagnosis tricky.


Systematic Diagnosis Workflow

When you encounter a segmentation fault, follow this step-by-step workflow. Do not skip steps—methodical diagnosis beats guesswork every time.

Step 1: Enable Core Dumps

A core dump is a snapshot of the process's memory at the moment of the crash. It's your single most valuable artifact for post-mortem analysis. On Linux, ensure core dumps are enabled:

# Check current core dump settings
ulimit -c

# Enable unlimited core dump size (for current shell and child processes)
ulimit -c unlimited

# Make it permanent by adding to /etc/security/limits.conf:
# * soft core unlimited

# On many modern systems, core dumps are handled by systemd and stored in
# /var/lib/systemd/coredump/ — check with:
coredumpctl list

Step 2: Reproduce the Fault Consistently

Intermittent segfaults are the hardest. Try to find a reliable reproduction scenario:

Step 3: Get a Stack Trace with GDB

GDB (GNU Debugger) is the primary tool for analyzing segfaults. Attach it to your core dump or run your program directly under GDB.

# Compile with debugging symbols (-g) and without optimization if possible (-O0)
g++ -g -O0 -o myprogram myprogram.cpp

# Run under GDB directly
gdb ./myprogram
# Within GDB: type 'run' to start, program will stop at segfault

# Alternatively, analyze a core dump
gdb ./myprogram core
# Or with systemd:
gdb ./myprogram (coredumpctl info PID | grep 'Storage' and extract the core file)

Once GDB stops at the segfault, use these commands:

# Print the stack backtrace — shows exactly which function chain led to the crash
(gdb) bt
(gdb) bt full       # includes local variable values

# Inspect the current frame
(gdb) frame 0
(gdb) info registers # shows register values, including the faulting address in rip/rsp
(gdb) list           # shows source code around the crash point

# Examine variables
(gdb) print variable_name
(gdb) print *pointer_name   # dereference a pointer safely in GDB
(gdb) x/20x address         # examine 20 hex words starting at address

# Check if a pointer is valid
(gdb) print ptr
(gdb) x/1x ptr              # if GDB says "Cannot access memory", ptr is bad

Step 4: Identify the Faulting Instruction and Address

In GDB, the signal stop will tell you the exact instruction and the address that caused the fault:

(gdb) info signals
# Look for "Program received signal SIGSEGV, Segmentation fault."
# GDB prints something like:
# 0x00007f123456789a in __strcpy_avx2 () from /lib64/libc.so.6
# or:
# 0x0000000000401234 in my_function (ptr=0x0) at myprogram.cpp:42

# The address after 'in' is the faulting instruction pointer (rip)
# The faulting data address is often in another register (like rax, rdx)
(gdb) info registers rax rdx rsi rdi

If the faulting address is 0x0, it's a NULL pointer dereference. If it looks like 0x19 or 0x20, it may be a small offset into a struct via a NULL pointer. If it looks like a heap address that seems "off" (e.g., 0x21c5018), suspect use-after-free or heap corruption.

Step 5: Use Valgrind for Heap and Use-After-Free Errors

Valgrind's Memcheck tool tracks every memory allocation, deallocation, and access. It can detect problems that don't immediately cause a segfault, like reading uninitialized memory or accessing slightly out-of-bounds.

# Install valgrind: sudo apt install valgrind  (Debian/Ubuntu)
#                 sudo dnf install valgrind  (Fedora)

# Run your program under Valgrind with full leak and error checking
valgrind --tool=memcheck --leak-check=full --track-origins=yes \
         --show-reachable=yes --error-limit=no ./myprogram

# Key flags explained:
# --leak-check=full       : detailed memory leak report
# --track-origins=yes     : shows where uninitialized values came from
# --show-reachable=yes    : reports even reachable leaks
# --error-limit=no        : don't stop after 1000 errors

Valgrind output for a segfault-causing bug will show something like:

==12345== Invalid write of size 4
==12345==    at 0x401234: main (badprog.cpp:15)
==12345==  Address 0x51fc040 is 0 bytes after a block of size 40 alloc'd
==12345==    at 0x4C2BABC: malloc (in /usr/lib/valgrind/vgpreload_memcheck.so)
==12345==    by 0x401220: main (badprog.cpp:12)

This tells you exactly: the write was 4 bytes, it occurred at line 15, and the memory was originally allocated as a 40-byte block at line 12—meaning you wrote past the end of that block.

Step 6: Enable AddressSanitizer (ASan) for Instant Feedback

AddressSanitizer (ASan) is a compiler-based tool (available in GCC and Clang) that instruments your code at compile time to detect memory errors at runtime. It's faster than Valgrind and catches many of the same bugs.

# Compile with AddressSanitizer
g++ -fsanitize=address -fno-omit-frame-pointer -g -O1 -o myprogram myprogram.cpp

# Or with Clang:
clang++ -fsanitize=address -fno-omit-frame-pointer -g -O1 -o myprogram myprogram.cpp

# Run normally — ASan reports errors immediately upon occurrence
./myprogram

# Additional useful sanitizers:
# -fsanitize=undefined   : catches undefined behavior (signed overflow, etc.)
# -fsanitize=leak        : lightweight leak detector
# -fsanitize=memory      : catches use of uninitialized memory (Clang only)

ASan output is extremely precise. For a heap buffer overflow, you'll see something like:

==1==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60300000f014
WRITE of size 4 at 0x60300000f014 thread T0
    #0 0x401234 in main badprog.cpp:15
0x60300000f014 is located 0 bytes to the right of 40-byte region [0x60300000efe0,0x60300000f008)
allocated by thread T0 here:
    #0 0x7f123456789a in operator new[](unsigned long) (/lib64/libasan.so.5+0x123abc)
    #1 0x401220 in main badprog.cpp:12

Step 7: Use Static Analysis for Prevention

Catch bugs before they become segfaults. Static analyzers scan your source code without running it:

# Clang Static Analyzer
clang --analyze myprogram.cpp

# Cppcheck (lightweight, fast)
cppcheck --enable=all --inconclusive myprogram.cpp

# For larger projects: Coverity, SonarQube, or PVS-Studio

Step 8: Trace System Calls with strace (Linux) / truss (Solaris)

Sometimes a segfault occurs deep in a system call. strace shows every syscall and its result, which can reveal if a segfault happens after a failed mmap or brk.

strace -f -o strace.log ./myprogram
# The log shows the exact syscall sequence leading to the crash
# Look for SIGSEGV in the output

Practical Code Examples: Buggy Code and Fixes

Example 1: Classic NULL Pointer Dereference

Buggy code:

#include <iostream>

struct Node {
    int data;
    Node* next;
};

void print_data(Node* node) {
    // BUG: No NULL check before dereference
    std::cout << node->data << std::endl;
}

int main() {
    Node* head = nullptr;
    print_data(head);  // SEGFAULT
    return 0;
}

Fixed code:

#include <iostream>

struct Node {
    int data;
    Node* next;
};

void print_data(Node* node) {
    if (node == nullptr) {
        std::cout << "Node is null" << std::endl;
        return;
    }
    std::cout << node->data << std::endl;
}

int main() {
    Node* head = nullptr;
    print_data(head);  // Safe: prints "Node is null"
    return 0;
}

Example 2: Use-After-Free (Dangling Pointer)

Buggy code:

#include <iostream>
#include <cstring>

struct Buffer {
    char* data;
    size_t size;
};

Buffer create_buffer(const char* src) {
    Buffer buf;
    buf.size = strlen(src) + 1;
    buf.data = new char[buf.size];
    strcpy(buf.data, src);
    return buf;  // Returns struct with valid pointer
}

void use_buffer() {
    Buffer b = create_buffer("hello");
    delete[] b.data;   // Free the memory
    // ... many lines of code later ...
    std::cout << b.data << std::endl;  // SEGFAULT: use-after-free
}

int main() {
    use_buffer();
    return 0;
}

Fixed code:

#include <iostream>
#include <cstring>
#include <memory>

struct Buffer {
    std::unique_ptr<char[]> data;  // Smart pointer manages lifetime
    size_t size = 0;
};

Buffer create_buffer(const char* src) {
    Buffer buf;
    buf.size = strlen(src) + 1;
    buf.data = std::make_unique<char[]>(buf.size);
    strcpy(buf.data.get(), src);
    return buf;
}

void use_buffer() {
    Buffer b = create_buffer("hello");
    std::cout << b.data.get() << std::endl;  // Safe: data still valid
    // No manual delete needed — unique_ptr handles cleanup automatically
}

int main() {
    use_buffer();
    return 0;
}

Example 3: Stack Overflow from Deep Recursion

Buggy code:

// Recursive Fibonacci without optimization
long long fibonacci(int n) {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);  // Exponential recursion depth
}

int main() {
    long long result = fibonacci(100000);  // Stack overflow segfault
    return 0;
}

Fixed code — iterative approach:

// Iterative Fibonacci with O(n) time, O(1) stack space
long long fibonacci(int n) {
    if (n <= 1) return n;
    long long prev = 0, curr = 1;
    for (int i = 2; i <= n; i++) {
        long long next = prev + curr;
        prev = curr;
        curr = next;
    }
    return curr;
}

int main() {
    long long result = fibonacci(100000);  // Safe: no recursion, no stack overflow
    return 0;
}

Alternative fixed code — increase stack size (Linux):

// For cases where recursion is genuinely needed, increase stack limit
// Compile and then set stack size before running:
// ulimit -s unlimited   or   ulimit -s 65536  (64 MB stack)

// Or use pthreads to set a custom stack size for a specific thread:
#include <pthread.h>
#include <cstdlib>

void* thread_func(void* arg) {
    // Deep recursion happens here on a thread with larger stack
    return nullptr;
}

int main() {
    pthread_t thread;
    pthread_attr_t attr;
    pthread_attr_init(&attr);
    // Set stack size to 16 MB (16 * 1024 * 1024 bytes)
    pthread_attr_setstacksize(&attr, 16 * 1024 * 1024);
    pthread_create(&thread, &attr, thread_func, nullptr);
    pthread_attr_destroy(&attr);
    pthread_join(thread, nullptr);
    return 0;
}

Example 4: Buffer Overflow on the Heap

Buggy code:

#include <cstring>
#include <iostream>

void copy_string(const char* src) {
    // BUG: Fixed-size buffer with no bounds checking
    char* buffer = new char[16];
    strcpy(buffer, src);  // If src is > 15 chars, buffer overflows
    std::cout << buffer << std::endl;
    delete[] buffer;
}

int main() {
    copy_string("This string is way too long for a 16-byte buffer!");  // SEGFAULT or heap corruption
    return 0;
}

Fixed code:

#include <cstring>
#include <iostream>

void copy_string(const char* src) {
    // FIX: Allocate exactly what we need
    size_t len = strlen(src);
    char* buffer = new char[len + 1];  // +1 for null terminator
    strcpy(buffer, src);               // Safe: buffer is exactly the right size
    std::cout << buffer << std::endl;
    delete[] buffer;
}

int main() {
    copy_string("This string is way too long for a 16-byte buffer!");  // Works perfectly
    return 0;
}

Even safer — using standard library containers:

#include <string>
#include <iostream>

void copy_string(const std::string& src) {
    std::string buffer = src;  // std::string manages memory automatically
    std::cout << buffer << std::endl;
    // No manual memory management at all
}

int main() {
    copy_string("Any length string works, no buffer overflow possible!");
    return 0;
}

Example 5: Modifying a String Literal

Buggy code:

#include <iostream>

void to_uppercase(char* str) {
    while (*str) {
        if (*str >= 'a' && *str <= 'z') {
            *str = *str - 'a' + 'A';  // Modifies the string in-place
        }
        str++;
    }
}

int main() {
    char* greeting = "hello";  // Points to read-only string literal
    to_uppercase(greeting);    // SEGFAULT: writing to read-only memory
    std::cout << greeting << std::endl;
    return 0;
}

Fixed code:

#include <iostream>
#include <cstring>

void to_uppercase(char* str) {
    while (*str) {
        if (*str >= 'a' && *str <= 'z') {
            *str = *str - 'a' + 'A';
        }
        str++;
    }
}

int main() {
    // FIX: Use a mutable array, initialized from the literal
    char greeting[] = "hello";  // Array on stack, writable
    to_uppercase(greeting);     // Safe: modifies stack-allocated copy
    std::cout << greeting << std::endl;  // Prints "HELLO"
    return 0;
}

Example 6: Iterator Invalidation (C++ STL)

Buggy code:

#include <vector>
#include <iostream>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    for (auto it = vec.begin(); it != vec.end(); ++it) {
        if (*it == 3) {
            vec.push_back(6);  // May invalidate iterators (reallocation)
        }
        std::cout << *it << " ";  // SEGFAULT if reallocation occurred
    }
    return 0;
}

Fixed code:

#include <vector>
#include <iostream>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    // FIX: Reserve enough capacity upfront to avoid reallocation
    vec.reserve(100);
    for (auto it = vec.begin(); it != vec.end(); ++it) {
        if (*it == 3) {
            vec.push_back(6);  // Safe: no reallocation since we reserved space
        }
        std::cout << *it << " ";
    }
    return 0;
}

Advanced Diagnostic Techniques

Using Watchpoints in GDB

When you suspect a variable is being overwritten but don't know where, set a hardware watchpoint. GDB will break exactly when the memory location is modified.

(gdb) break main
(gdb) run
(gdb) watch my_variable        # Break when my_variable changes
(gdb) watch *0x60012345        # Break when memory at this address changes
(gdb) continue                 # Program runs until the watched value is modified
# When it breaks, bt shows exactly who modified it

Using MALLOC_CHECK_ and MALLOC_PERTURB_ Environment Variables (glibc)

The GNU C library (glibc) provides runtime environment variables that help expose heap bugs earlier:

# MALLOC_CHECK_ sets the heap checking level
# Level 0: no checking (default)
# Level 1: print error messages to stderr on detected corruption
# Level 2: abort() on detected corruption (generates core dump)
export MALLOC_CHECK_=2

# MALLOC_PERTURB_ fills allocated/freed memory with a pattern
# This makes use-after-free and uninitialized reads more likely to crash
export MALLOC_PERTURB_=85   # Fill with byte pattern 0x55 (85 decimal)

# Run your program with both set:
MALLOC_CHECK_=2 MALLOC_PERTURB_=85 ./myprogram

Using Electric Fence for Immediate Segfault on Buffer Overflow

Electric Fence (libefence) places a guard page (no-access) after each allocation. Any out-of-bounds access immediately segfaults instead of silently corrupting memory.

# Install: sudo apt install electric-fence
# Link with libefence: 
g++ -g -o myprogram myprogram.cpp -lefence

# Or preload it:
LD_PRELOAD=/usr/lib/libefence.so ./myprogram

# Warning: Electric Fence uses enormous amounts of virtual memory.
# It's best for debugging small, isolated test cases.

Debugging Shared Library Load Issues

Sometimes segfaults occur before main() due to shared library loading failures. Use ldd and LD_DEBUG:

# Check library dependencies
ldd ./myprogram

# Verbose loader diagnostics
LD_DEBUG=all ./myprogram 2>&1 | grep -i error

# Check for ABI mismatches with nm and objdump
nm -D ./myprogram | grep UNDEFINED
objdump -T ./myprogram | grep DF_1

Best Practices to Prevent Segmentation Faults

1. Initialize All Pointers Immediately

Never declare a pointer without initialization. Set it to nullptr, a valid address, or use a smart pointer.

// Bad
int *p;

// Good
int *p = nullptr;
// Better
std::unique_ptr<int> p = std::make_unique<int>(42);

2. Use RAII and Smart Pointers in C++

Modern C++ provides std::unique_ptr, std::shared_ptr, and std::weak_ptr. These automate memory management and prevent use-after-free and double-free errors entirely.

// Instead of raw new/delete:
std::unique_ptr<MyClass> obj = std::make_unique<MyClass>(args);
// obj automatically destroyed when it goes out of scope — no manual delete needed

3. Use Standard Containers Instead of Raw Arrays

Prefer std::vector, std::array, std::string over raw C arrays and char*. These containers manage memory and provide bounds-checked access via .at().

// Bad: raw array with potential overflow
char buf[256];
sprintf(buf, "%s", user_input);

// Good: std::string handles allocation and resizing automatically
std::string buf = user_input;

4. Enable Compiler Warnings and Treat Them as Errors

Modern compilers can statically detect many segfault-prone patterns.

# GCC: enable all warnings
g++ -Wall -Wextra -Wpedantic -Werror -Wshadow -Wconversion -o myprogram myprogram.cpp

# Clang: even more diagnostics
clang++ -Weverything -Werror -o myprogram myprogram.cpp

5. Use Bounds-Checked Access Methods

When using containers, .at() throws an exception on out-of-bounds access instead of causing undefined behavior (and potential segfaults).

std::vector<int> vec = {1, 2, 3};
// vec[100] = 42;          // Undefined behavior, might segfault
vec.at(100) = 42;          // Throws std::out_of_range exception

6. Test with Sanitizers in CI/CD Pipelines

Make AddressSanitizer, UndefinedBehaviorSanitizer, and MemorySanitizer part of your continuous integration testing. Catch memory bugs before they reach production.

# Example CI job: build and run tests with ASan + UBSan
g++ -fsanitize=address,undefined -fno-omit-frame-pointer -g -O1 \
    -o test_program test_program.cpp
./test_program

7. Avoid Recursion for Unbounded Depth Problems

If the recursion depth depends on user input or runtime data, prefer iterative algorithms or explicit stack data structures on the heap.

8. Check Return Values and Validate Input

Many segfaults originate from functions returning NULL on failure (malloc, fopen, etc.). Always check.

// Bad
FILE* f = fopen("config.txt", "r");
fscanf(f, "%s", buffer);  // SEGFAULT if fopen failed

// Good
FILE* f = fopen("config.txt", "r");
if (f == nullptr) {
    perror("Failed to open config.txt");
    return 1;
}
fscanf(f, "%s", buffer);
fclose(f);

9. Use Static Code Analysis in Your Workflow

Integrate tools like clang-tidy, cppcheck, or Coverity into your build process. They catch many potential segfault causes at compile time.

# clang-tidy example
clang-tidy myprogram.cpp --checks=* --warnings-as-errors=*

10. Document Pointer Ownership Explicitly

In team projects, use clear conventions: document which function or object owns a pointer and is responsible for freeing it. Even better, encode ownership in the type system using std::unique_ptr (exclusive ownership) or std::shared_ptr (shared ownership).


Debugging Multi-Threaded Segfaults

Segfaults in multi-threaded programs add complexity because the faulting thread may not be the one that caused the corruption. Memory corruption from a data race in one thread can cause a segfault much later in another thread.

Use ThreadSanitizer (TSan)

# Compile with ThreadSanitizer
g++ -fsanitize=thread -g -O1 -o myprogram myprogram.cpp -lpthread

# Run — TSan detects data races in real time
./myprogram

GDB Multi-Threaded Debugging Commands

(gdb) info threads                    # List all threads
(gdb) thread apply all bt             # Backtrace for every thread
(gdb) thread 2                        # Switch to thread #2
(gdb) set scheduler-locking on        # Only current thread runs during step/next

Helgrind (Valgrind Tool for Thread Errors)

valgrind --tool=helgrind --free-is-write=yes ./myprogram
# Helgrind detects: data races, inconsistent lock ordering, deadlocks

Platform-Specific Considerations

Linux

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles