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 programThe -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
runIf 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 argsStep 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 2Step 6: Verify pointer validity
print ptr
// Possible output:
// $1 = (int *) 0x0A value of 0x0 means the pointer is nullptr.
Step 7: Examine memory
# Inspect memory contents
x ptr
# Inspect multiple memory locations
x/8x ptrStep 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
stepExample 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 = 0x0Example 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 numbersAlways 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 accessExample 4: Infinite recursion
#include <iostream>
using namespace std;
void recursiveFunction()
{
recursiveFunction();
}
int main()
{
recursiveFunction();
return 0;
}
// Useful GDB command:
// backtraceSymptoms 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.