[C Series 14] Finding and Debugging Memory Errors

한국어 버전

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

  1. Segmentation fault: A forced termination when the OS detects an illegal memory access
  2. Use-after-free: A bug where code uses memory after calling free
  3. Double free: Calling free twice on the same pointer
  4. Memory checking tools: Debuggers such as valgrind and 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:

  1. Invalid addresses: Accessing beyond an array or dereferencing NULL
  2. Using memory after its valid lifetime: Using memory after free or freeing twice
  3. 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 valgrind for 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 call free once 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 valgrind become 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.

Live Practice

C Practice Sandbox

CodeSandbox

Run the starter project in CodeSandbox, compare it with the lesson code, and keep experimenting.

Universal starterCterminal
  1. Fork the starter and create a practice file such as hello.c
  2. Paste in the lesson code and compile it if clang or gcc is available
  3. Edit the code, rebuild it, and compare the new output

For C, the terminal build loop matters more than a browser preview. Compiler availability can vary by environment, so first confirm that clang or gcc is present in the Universal starter.

Practice

  • Try it: Build each example with and without -fsanitize=address and compare the output.
  • Extend it: Write a small struct management function that calls malloc/free multiple times and verify with valgrind that no leaks remain.
  • Debug it: Intentionally break the loop bounds on numbers[i], then set breakpoints in gdb or lldb to watch the values change.
  • Done when: You can label an error type and name a tool that detects it in one sentence.

Wrap-up

Pointers inevitably bring memory bugs. The key is to develop both “eyes for error messages” and “lifetime management rules.” The next lesson introduces the preprocessor and macros so you can cut repetition and vary builds by environment.

💬 댓글

이 글에 대한 의견을 남겨주세요