SrcForge

Lesson 63

Segfault Debugging

Covers identifying segmentation faults, common causes, step-by-step GDB debugging, inspecting pointers and the call stack, and best practices for preventing crashes.

Lesson content

What is a Segfault Debugging Walkthrough in C++?

A Segfault Debugging Walkthrough in C++ teaches you how to identify, analyze, and fix segmentation faults using debugging tools like GDB. A segmentation fault occurs when your program tries to access memory that it is not allowed to access.

Think of your computer's memory as a building with many rooms. Your program is given permission to enter only certain rooms. If it tries to enter a locked room or a room that doesn't belong to it, the operating system immediately stops the program. This is called a segmentation fault.

Segmentation faults are among the most common runtime errors in C++, especially when working with pointers, arrays, dynamic memory, and object lifetimes.

Why do segmentation faults occur?

A segmentation fault usually happens because the program dereferences a null pointer, accesses memory after it has been freed, reads or writes outside an array's bounds, uses an uninitialized pointer, overflows the stack with excessive recursion, or accesses invalid object memory.

Real-world use cases

  • Game development
  • Embedded systems
  • Operating systems
  • Database engines
  • Network servers
  • Scientific computing
  • High-performance applications

Prerequisites

  • Variables and functions
  • Pointers and references
  • Arrays
  • Dynamic memory (new and delete)
  • Basic command-line usage
  • Basic GDB commands

What is a segmentation fault?

A segmentation fault (often shown as "Segmentation fault (core dumped)" on Linux) occurs when your program performs an illegal memory access.

  • Null pointer dereference — e.g. *ptr where ptr == nullptr
  • Dangling pointer — using memory after delete
  • Out-of-bounds access — e.g. arr[10] when array size is 5
  • Uninitialized pointer — dereferencing a pointer without initialization
  • Stack overflow — infinite recursion
  • Invalid object access — accessing destroyed objects

Step 1: Compile with debug information

Always compile with debugging symbols.

g++ -g -O0 main.cpp -o program

The -O0 flag prevents compiler optimizations, makes variables easier to inspect, and keeps execution flow close to your source code.

Step 2: Start GDB

# Launch the debugger
gdb program

# Run the program
run

If a segmentation fault occurs, GDB immediately pauses execution at the crashing instruction.

Step 3: Inspect the call stack

Display the function call history using backtrace, or its shortcut bt. The first frame usually points to where the crash occurred.

backtrace

// Example:
// #0 divideNumbers()
// #1 calculateAverage()
// #2 main()

Step 4: Inspect variables

# Print variable values
print ptr

# Display local variables
info locals

# Display function parameters
info args

Step 5: Navigate the stack

Move between stack frames and inspect variables in each frame to understand how the program reached the crash.

frame 0
frame 1
frame 2

Step 6: Verify pointer validity

print ptr

// Possible output:
// $1 = (int *) 0x0

A value of 0x0 means the pointer is nullptr.

Step 7: Examine memory

# Inspect memory contents
x ptr

# Inspect multiple memory locations
x/8x ptr

Step 8: Set breakpoints

Restart the program and pause before the crash.

break main
run

# Or break at a specific line
break 24

# Step through execution
next
step

Example 1: Null pointer dereference

#include <iostream>
using namespace std;

int main()
{
    int* ptr = nullptr;

    cout << *ptr << endl;

    return 0;
}

// Compile:
// g++ -g -O0 main.cpp -o program

// Debug:
// gdb program
// run
// bt
// print ptr

// Expected GDB output:
// Program received signal SIGSEGV.
// ptr = 0x0

Example 2: Array out-of-bounds access

#include <iostream>
using namespace std;

int main()
{
    int numbers[5] = {1,2,3,4,5};

    cout << numbers[10] << endl;

    return 0;
}

// Debug:
// break main
// run
// next
// print numbers

Always ensure array indices remain within valid bounds.

Example 3: Use after delete

#include <iostream>
using namespace std;

int main()
{
    int* ptr = new int(100);

    delete ptr;

    cout << *ptr << endl;

    return 0;
}

// Correct approach:
delete ptr;          // Release memory
ptr = nullptr;       // Prevent accidental access

Example 4: Infinite recursion

#include <iostream>
using namespace std;

void recursiveFunction()
{
    recursiveFunction();
}

int main()
{
    recursiveFunction();

    return 0;
}

// Useful GDB command:
// backtrace

Symptoms include stack overflow, program crash, and an extremely deep call stack. The call stack will contain thousands of repeated function calls.

Example 5: Dangling pointer

#include <iostream>
using namespace std;

int* createPointer()
{
    int value = 25;

    return &value;
}

int main()
{
    int* ptr = createPointer();

    cout << *ptr << endl;

    return 0;
}

The local variable no longer exists after the function returns, so the pointer becomes invalid.

Common mistakes and pitfalls

  • Dereferencing nullptr. Fix: check the pointer before dereferencing
  • Using memory after delete. Fix: set the pointer to nullptr after deleting
  • Returning the address of a local variable. Fix: return by value or allocate memory correctly
  • Accessing arrays beyond their size. Fix: validate array indices
  • Infinite recursion. Fix: always include a proper base case
// Wrong
int* ptr = nullptr;

*ptr = 5;

// Correct
int value = 5;

int* ptr = &value;

cout << *ptr;

Best practices

  • Compile with -g -O0 while debugging
  • Initialize pointers immediately
  • Check pointers before dereferencing
  • Set pointers to nullptr after delete
  • Prefer smart pointers (std::unique_ptr, std::shared_ptr) to reduce manual memory management
  • Use standard containers such as std::vector instead of raw arrays when appropriate
  • Validate array indices before accessing elements
  • Use tools such as AddressSanitizer and Valgrind alongside GDB for memory-related issues
  • Keep recursive functions well-defined with a clear base case

When not to use this

A GDB walkthrough is not always the best first step when the problem is a compile-time error, when a compiler warning already identifies the issue, when static analysis tools can detect the bug automatically, when you are investigating performance rather than correctness, or when logging is sufficient to diagnose a simple logic error.

Summary

  • A segmentation fault occurs when a program accesses invalid memory
  • Common causes include null pointers, dangling pointers, invalid array access, and stack overflow
  • Compile with -g -O0 for effective debugging
  • Use GDB commands such as run, break, next, step, print, and backtrace
  • Inspect pointers and the call stack to locate the source of a crash
  • Use safe programming practices to prevent segmentation faults
  • Modern C++ features like smart pointers and standard containers help reduce memory-related bugs

Frequently asked questions

What is a segmentation fault in C++?

A segmentation fault is a runtime error that occurs when a program attempts to access memory it is not permitted to access.

Why should I compile with -g?

The -g option includes debugging information, allowing GDB to map machine instructions back to your source code and display variables and line numbers.

What does backtrace do in GDB?

The backtrace (or bt) command displays the sequence of function calls that led to the current execution point, making it easier to locate the source of a crash.

Can every out-of-bounds array access cause a segmentation fault?

No. Accessing memory outside an array's bounds is undefined behavior. It may appear to work, produce incorrect results, or crash with a segmentation fault depending on the memory being accessed.

What tools can help find segmentation faults besides GDB?

AddressSanitizer, Valgrind, compiler warnings (-Wall -Wextra), static analysis tools, and integrated debugger features in IDEs can all help identify memory-related errors.