After learning to allocate and free memory in lesson 13, the next challenge is understanding what happens when you misuse it. Memory bugs can behave differently each run, which makes them intimidating, but the patterns repeat. This post recreates the most common bugs and walks through lightweight debugging flows.
New Terms in This Post
- Segmentation fault: A forced termination when the OS detects an illegal memory access
- Use-after-free: A bug where code uses memory after calling
free - Double free: Calling
freetwice on the same pointer - Memory checking tools: Debuggers such as
valgrindand AddressSanitizer that monitor accesses at runtime
Key Ideas
Study Notes
- Time required: about 60 minutes
- Prerequisites: pointers, basic dynamic memory, comfort running programs from the command line
- Goal: recognize common memory error types, reproduce them, and debug them with tools
Most memory errors fall into three categories:
- Invalid addresses: Accessing beyond an array or dereferencing
NULL - Using memory after its valid lifetime: Using memory after
freeor freeing twice - Uninitialized values: Reading variables before writing to them
The symptoms may look similar, but the causes differ. Practice the routine "observe symptom -> form a guess -> reproduce -> fix." We will cover:
- Out-of-bounds, use-after-free, double free, and missing initialization
- A debugging loop you can run even without
gdb/lldb - AddressSanitizer and
valgrindfor pinpointing bugs - Habits such as invalidating pointers, checking ranges, and recording lifetimes
Code Walkthrough
Going Out of Bounds
#include <stdio.h>
int main(void) {
int numbers[3] = {1, 2, 3};
for (int i = 0; i <= 3; i++) {
printf("numbers[%d] = %d\n", i, numbers[i]);
}
return 0;
}
The loop uses i <= 3, so it touches numbers[3]. That index does not exist, so you might print garbage or crash. The fix is to use i < 3 instead.
for (int i = 0; i < 3; i++) {
printf("numbers[%d] = %d\n", i, numbers[i]);
}
Make it a habit to double-check loop bounds.
Reproducing Use-after-free
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *value = malloc(sizeof(int));
if (value == NULL) {
return 1;
}
*value = 42;
free(value);
printf("value = %d\n", *value); // using freed memory
return 0;
}
Sometimes this still prints 42; sometimes it crashes. Either way, the behavior is undefined. Once memory is freed the OS may reuse it, so set the pointer to NULL and never touch it again. That does not solve every use-after-free scenario, but it prevents accidental reuse through the same variable.
Double Free and a Guard Pattern
#include <stdlib.h>
int main(void) {
int *data = malloc(sizeof(int) * 2);
if (data == NULL) {
return 1;
}
free(data);
free(data); // freeing the same pointer again
return 0;
}
Freeing the same pointer twice corrupts the heap metadata, and the result is undefined. Prevent this by setting the pointer to NULL right after free, or by centralizing the responsibility for freeing in a single function.
Using Uninitialized Memory
#include <stdio.h>
int main(void) {
int total;
if (total > 0) {
printf("positive\n");
}
return 0;
}
total contains garbage because nothing assigned to it. Always provide an initial value or zero the memory (with memset or calloc).
Catching Bugs with AddressSanitizer
AddressSanitizer (ASan) ships with GCC and Clang through the -fsanitize=address flag. It detects memory errors while the program runs on your machine.
clang -g -fsanitize=address -o uaf use_after_free.c
./uaf
When you run the program, ASan reports the exact address and stack trace where the invalid access occurred and where the memory was freed. Early on, get used to copying the message and translating it so you understand which parts matter.
Checking for Leaks with valgrind
Run valgrind --leak-check=full ./program to have the tool report blocks that were not freed at program exit. It is common on Linux. If your OS does not support it well, use AddressSanitizer instead. Keep fixing code until the "definitely lost" count reaches zero.
Creating a Memory Debugging Routine
For small projects, maintain a lifetime table like this:
| Pointer | Allocated in | Freed in | Notes |
|---|---|---|---|
buffer |
read_file |
cleanup |
Only cleanup frees, even on failure |
- Record which function allocates and which frees each pointer to make ownership explicit.
- For larger functions, converge exits with a
goto cleanup;pattern so you callfreeonce at the end.
Why It Matters
- Memory errors can crash programs immediately or, worse, silently corrupt data.
- Naming the bug type helps you understand tool messages and search with precise keywords.
- Tools like AddressSanitizer and
valgrindbecome indispensable once you handle pointer-heavy code.
Practice in CodeSandbox
The sandbox below uses CodeSandbox's Universal starter. For C, the key learning loop is still compile and run in the terminal, so recreate the lesson code as a source file and repeat that cycle directly.
💬 댓글
이 글에 대한 의견을 남겨주세요