A segmentation fault in C is a runtime error that occurs when your program tries to access memory it doesn’t have permission to use. The operating system kills the process and sends a signal called SIGSEGV, which stands for “signal: segmentation violation.” If you’ve seen the message “Segmentation fault (core dumped)” in your terminal, your program attempted to read from or write to a memory address that was off-limits.
How the Operating System Detects It
Your program never works with physical memory addresses directly. Instead, the CPU’s Memory Management Unit (MMU) translates the virtual addresses your code uses into real physical locations. Every page of memory has associated permission bits that define whether it can be read, written, or executed.
When your program tries to access an address that doesn’t belong to its address space, or tries to write to a location marked read-only, the MMU can’t complete the translation. It triggers a page fault, which hands control to the operating system kernel. The kernel inspects the fault, determines it was caused by an illegal access, and sends the SIGSEGV signal to your process. Unless your program has a custom signal handler (which is rare and advanced), this terminates it immediately.
Common Causes in C Code
Dereferencing a Null Pointer
This is the most frequent cause. If a pointer holds the value NULL (address 0) and your code tries to read or write through it, you’re accessing an address that the OS has deliberately left unmapped. This happens when you forget to check the return value of malloc, when a function returns NULL to signal failure and you use the result anyway, or when you simply forget to initialize a pointer before using it.
int *p = NULL;
*p = 42; // segmentation fault
Accessing Memory After Freeing It
A dangling pointer is a pointer that still holds the address of memory your program has already deallocated with free(). The operating system may revoke your program’s access to that memory at any time after the call to free(). If you read from or write to the old address, the result is either garbage data or a segmentation fault. The tricky part is that dangling pointer bugs don’t always crash immediately. The memory might still be technically accessible for a while, making the bug intermittent and hard to reproduce.
int *p = malloc(sizeof(int));
free(p);
*p = 10; // dangling pointer: may crash, may not
Writing Past Array Boundaries
C does not check array bounds. If you write beyond the end of an array, you’re scribbling over whatever happens to be next in memory. If you’re lucky, this triggers an immediate segmentation fault. If you’re unlucky, it silently corrupts other variables or the function’s return address on the stack, causing a crash somewhere else entirely. This is why buffer overflows are notoriously difficult to debug.
int arr[10];
arr[50] = 7; // writing far past the end of the array
Modifying a String Literal
When you write char *s = "hello";, the string “hello” is stored in a read-only section of your program’s memory. The variable s is just a pointer to that location. If you try to change a character, like s[0] = 'H';, you’re attempting to write to read-only memory, and the MMU will trigger a segmentation fault. The fix is to use a character array instead: char s[] = "hello"; copies the string into writable stack memory.
Stack Overflow From Deep Recursion
Every function call adds a frame to the stack, and the stack has a fixed size (typically a few megabytes). A recursive function without a proper base case will keep adding frames until the stack runs out of space. When your program tries to write the next frame into memory beyond the stack’s boundary, the OS detects the violation and raises a segmentation fault.
Why the Crash Doesn’t Always Happen at the Bug
One of the most frustrating things about segmentation faults is that the crash location and the bug location can be completely different. Writing one byte past an array doesn’t always segfault because the adjacent memory might still belong to your process. The corruption sits there silently until some other part of the code reads the corrupted data and does something illegal with it. Memory leaks can also accumulate gradually, eventually making it impossible to access variables or allocate new memory, which then triggers the fault far from the original problem.
This is why segmentation faults in large programs can take hours to track down without the right tools.
Debugging With GDB
The GNU Debugger (GDB) is the most direct way to find where a segmentation fault occurs. Compile your code with the -g flag to include debug symbols, then run the program inside GDB:
gcc -g myprogram.c -o myprogram
gdb ./myprogram
(gdb) run
When the program crashes, GDB stops execution and shows you the exact file and line number where the fault happened. For example, you might see:
Program received signal SIGSEGV, Segmentation fault.
0x0000000000400ac1 in main () at reassemble.c:51
From there, the backtrace command (or just bt) prints the full call stack, showing the chain of function calls that led to the crash. Each line represents a function call, with frame #0 being the exact point of failure. This lets you trace the problem back through your code to understand how you arrived at the bad memory access.
Finding Hidden Memory Errors With Valgrind
GDB tells you where the crash happened, but Valgrind’s Memcheck tool goes deeper. It tracks every memory allocation and deallocation your program makes and flags two categories of errors: use of illegal addresses and use of undefined values. That’s enough to catch most memory bugs in C, including the ones that don’t immediately crash.
Valgrind is especially useful for detecting use-after-free bugs, where your code accesses memory that’s already been deallocated. It will report the exact address, tell you it was previously freed, and show you where the free happened. It also catches heap buffer overflows and memory leaks. Run it like this:
valgrind --leak-check=full ./myprogram
The tradeoff is speed. Valgrind runs your program in a virtual environment, so execution is roughly 10 to 30 times slower than normal. For large programs, this can make it impractical for everyday testing.
Catching Errors at Compile Time With AddressSanitizer
AddressSanitizer (ASan) is a faster alternative built into GCC and Clang. It instruments your code at compile time, adding checks around every memory access. It detects use-after-free bugs, heap overflows, and memory leaks with significantly less overhead than Valgrind. To enable it, add a few flags when compiling:
gcc -g -fsanitize=address -fno-omit-frame-pointer myprogram.c -o myprogram
The -g flag includes debug symbols so ASan can print line numbers in its error reports. The -fno-omit-frame-pointer flag ensures accurate stack traces. When your program hits a memory error, ASan prints a detailed report showing the type of error, the offending line, and the allocation history of the involved memory. Unlike Valgrind, ASan typically only slows your program down by about 2x, making it practical to leave enabled during development.
Practical Prevention Habits
Most segmentation faults come from a small set of recurring mistakes, and a few habits eliminate the majority of them:
- Initialize every pointer. Set pointers to
NULLwhen you declare them if you can’t assign a valid address immediately. Then check forNULLbefore dereferencing. - Set pointers to NULL after freeing. This turns a potential use-after-free into a null pointer dereference, which is easier to diagnose because it crashes immediately and consistently.
- Track array sizes explicitly. C gives you no safety net for bounds checking. Pass array lengths alongside pointers, and validate indices before using them.
- Use character arrays for mutable strings. Declare
char s[] = "hello"instead ofchar *s = "hello"whenever you need to modify the string contents. - Compile with warnings enabled. The flags
-Wall -Wextracatch uninitialized variables, suspicious pointer usage, and other issues that often lead to segmentation faults.

