Debugging Segmentation Faults in C on Linux: A Practical Hands-On Guide
If you have ever written C code on Linux, you have probably seen the dreaded message: Segmentation fault (core dumped). It is one of the most common and frustrating errors in C programming, but with the right tools and approach, finding the exact line that crashes your program takes only a few minutes.
This tutorial walks you through the full workflow: reproducing the bug, capturing a core dump, loading it in GDB, and using commands like backtrace, frame, and print to nail down the root cause. We will also cover the most frequent segfault patterns with working code examples.

What Is a Segmentation Fault?
A segmentation fault (SIGSEGV, signal 11) happens when a process tries to access a memory region it is not allowed to touch. The Linux kernel detects the violation through the MMU, kills the process, and (if configured) writes a core dump containing a snapshot of the program’s memory at the moment of the crash.
Common triggers include:
- Dereferencing a NULL pointer
- Writing past the end of an array (buffer overflow)
- Using a dangling pointer after
free() - Infinite recursion causing a stack overflow
- Writing to read-only memory (string literals)
Step 1: Compile With Debug Symbols
Before you can debug anything meaningful, recompile your program with the -g flag and disable optimizations:
gcc -g -O0 -Wall -Wextra myprogram.c -o myprogram
Without -g, GDB will only show you addresses and assembly instructions instead of source lines and variable names.

Step 2: Enable Core Dumps
On most modern Linux distributions, core dumps are disabled by default. Check the current limit:
ulimit -c
If it returns 0, enable unlimited core dumps for the current shell:
ulimit -c unlimited
You also need to know where the core file will land. On systems using systemd-coredump (Ubuntu, Fedora, Debian), cores are stored in the journal. Retrieve them with:
coredumpctl list
coredumpctl gdb
On systems without systemd-coredump, check the core_pattern:
cat /proc/sys/kernel/core_pattern
To force core files into the current directory:
echo "core.%e.%p" | sudo tee /proc/sys/kernel/core_pattern
Step 3: Reproduce the Crash
Let’s use a buggy program with a classic null pointer dereference:
// segfault.c
#include <stdio.h>
#include <string.h>
void corrupt(char *dest, const char *src) {
strcpy(dest, src);
}
int main(void) {
char *buffer = NULL;
corrupt(buffer, "hello world");
printf("%s\n", buffer);
return 0;
}
Compile and run:
gcc -g -O0 segfault.c -o segfault
./segfault
Segmentation fault (core dumped)
Step 4: Load the Core Dump in GDB
If you got a core file in the directory:
gdb ./segfault core.segfault.12345
If using systemd-coredump:
coredumpctl gdb segfault
You can also run the binary directly inside GDB without needing a core file:
gdb ./segfault
(gdb) run

Step 5: Use GDB to Find the Faulting Line
The backtrace command
Once GDB stops on the crash, type:
(gdb) bt
You will see something like:
#0 0x00007ffff7e3a1c0 in __strcpy_avx2 () from /lib/x86_64-linux-gnu/libc.so.6
#1 0x0000555555555169 in corrupt (dest=0x0, src=0x555555556004 "hello world") at segfault.c:5
#2 0x000055555555519a in main () at segfault.c:10
Frame #1 is your code. The argument dest=0x0 immediately tells you a NULL pointer was passed in.
The frame command
Switch to the frame that belongs to your code:
(gdb) frame 1
GDB will show the exact source line:
#1 corrupt (dest=0x0, src=0x555555556004 "hello world") at segfault.c:5
5 strcpy(dest, src);
The print command
Inspect variables and pointers in scope:
(gdb) print dest
$1 = 0x0
(gdb) print src
$2 = 0x555555556004 "hello world"
Confirmed: dest is NULL. Walking up to main shows where the bad value originated:
(gdb) frame 2
(gdb) print buffer
$3 = 0x0
Essential GDB Commands Cheat Sheet
| Command | Purpose |
|---|---|
run / r |
Start the program inside GDB |
bt / backtrace |
Show full call stack at crash |
bt full |
Backtrace with all local variables |
frame N |
Jump to stack frame number N |
list / l |
Show source code around current line |
print var / p var |
Print value of a variable |
info locals |
Show all local variables |
info registers |
Dump CPU registers |
x/10xw addr |
Examine 10 words of memory at addr |
break file:line |
Set a breakpoint |
quit |
Exit GDB |
Common Segmentation Fault Causes With Real Examples
1. NULL Pointer Dereference
int *p = NULL;
*p = 42; // CRASH
GDB clue: the faulting instruction touches address 0x0. print p shows 0x0.
2. Dangling Pointer After free()
char *s = malloc(16);
strcpy(s, "hello");
free(s);
printf("%s\n", s); // undefined behavior, often segfault
GDB clue: the pointer holds an address that no longer maps to a valid heap region. Combine GDB with Valgrind (valgrind --track-origins=yes ./prog) to confirm use-after-free.
3. Stack Overflow Through Infinite Recursion
void recurse(int n) {
char buffer[4096];
buffer[0] = (char)n;
recurse(n + 1);
}
GDB clue: bt displays thousands of identical frames. The crash address is just below the current stack pointer ($rsp).
4. Buffer Overflow on Stack
char buf[8];
strcpy(buf, "this string is way too long");
GDB clue: the return address corruption often causes a crash inside a garbage address when the function returns. Compile with -fsanitize=address to catch it instantly.
5. Writing to Read-Only Memory
char *s = "constant";
s[0] = 'C'; // CRASH on most systems
GDB clue: the address belongs to the .rodata segment. Check with info proc mappings.

Bonus: Combine GDB With Sanitizers
Modern GCC and Clang ship with AddressSanitizer (ASan), which catches many memory bugs before they turn into a generic segfault:
gcc -g -O0 -fsanitize=address -fno-omit-frame-pointer prog.c -o prog
./prog
ASan prints a precise report with the buggy line, the allocation site, and the free site. For threaded code, ThreadSanitizer (-fsanitize=thread) and UndefinedBehaviorSanitizer (-fsanitize=undefined) are equally valuable.
Workflow Summary
- Compile with
-g -O0 - Enable core dumps with
ulimit -c unlimited - Reproduce the crash
- Open the binary and core file in GDB
- Run
btto see the call stack - Use
frameto jump to your code - Use
printandinfo localsto inspect state - Cross-check with Valgrind or AddressSanitizer if needed
FAQ
Why does my program say “Segmentation fault” without creating a core dump?
Either ulimit -c is set to 0, or core_pattern sends the dump to a system service like systemd-coredump or apport. Use coredumpctl list to find them, or set ulimit -c unlimited in your shell.
Can I debug a segfault without a core dump?
Yes. Run the program directly inside GDB with gdb ./prog followed by run. GDB will halt at the moment of the crash with the same context a core dump provides.
What’s the difference between SIGSEGV and SIGBUS?
SIGSEGV means an invalid virtual memory access (no mapping or wrong permissions). SIGBUS usually means the address is mapped but unaligned, or a memory-mapped file backing was truncated.
Why does the bug disappear when I add a printf?
You are likely dealing with undefined behavior such as use of uninitialized memory or a buffer overflow. The printf changes register and stack layout, masking the symptom. Use AddressSanitizer to find the real cause.
Is GDB the only option on Linux?
No. LLDB works similarly with Clang-built binaries. Graphical front-ends like gdbgui, seer, or IDE integrations in VS Code and CLion wrap GDB with a friendlier interface.
How do I debug a segfault that only happens in production?
Ship binaries with separated debug symbols (objcopy --only-keep-debug), enable core dumps on the production host, then load the core on a developer machine with the matching debug symbols using gdb -s prog.debug -c core prog.
Final Thoughts
Debugging a segmentation fault in C on Linux is far less mysterious once you have a repeatable workflow: compile with debug info, capture a core dump, open it in GDB, and read the backtrace. Add Valgrind or AddressSanitizer to the toolbox and the vast majority of memory bugs will surrender within minutes instead of hours.