Skip to main content

Debugging Fundamentals

Debugging C is an art. Let’s master the tools that make you dangerous.

GDB Essentials

Starting GDB

# Compile with debug symbols
gcc -g -O0 program.c -o program

# Start GDB
gdb ./program

# Start with arguments
gdb --args ./program arg1 arg2

# Attach to running process
gdb -p <pid>

# Load core dump
gdb ./program core

Basic Commands

# Running
run                    # Start program
run arg1 arg2          # With arguments
start                  # Run and stop at main
continue (c)           # Continue execution
next (n)               # Step over (don't enter functions)
step (s)               # Step into
finish                 # Run until current function returns
until <line>           # Run until line

# Breakpoints
break main             # Break at function
break file.c:42        # Break at line
break *0x400520        # Break at address
info breakpoints       # List breakpoints
delete 1               # Delete breakpoint 1
disable 1              # Disable breakpoint 1
enable 1               # Enable breakpoint 1
clear main             # Clear breakpoints at main

# Conditional breakpoints
break file.c:42 if i == 100
condition 1 x > 5      # Add condition to breakpoint 1

# Watchpoints (break when value changes)
watch x                # Break when x changes
rwatch x               # Break when x is read
awatch x               # Break on read or write

# Examining
print x (p)            # Print variable
print *ptr             # Dereference pointer
print arr[5]           # Array element
print (int*)0x7fff...  # Cast and print
print/x val            # Print in hex
print/t val            # Print in binary
print/d val            # Print as decimal
display x              # Print x every step

# Memory examination
x/10xw 0x7fff...       # 10 words in hex
x/s str                # String
x/i $pc                # Instruction at PC
x/20i main             # 20 instructions at main

# Stack
backtrace (bt)         # Show call stack
frame 2                # Select frame 2
up / down              # Move through frames
info locals            # Local variables
info args              # Function arguments
info registers         # CPU registers

# Code
list                   # Show source
list function          # Show function source
disassemble           # Show assembly
disassemble /m        # Mixed source and assembly

Advanced GDB

# Reverse debugging (record and replay)
record                 # Start recording
reverse-next           # Step backwards
reverse-step           # Step into backwards
reverse-continue       # Continue backwards

# Checkpoints
checkpoint             # Save program state
info checkpoints       # List checkpoints
restart <id>           # Restore checkpoint

# Multi-threaded debugging
info threads           # List threads
thread 2               # Switch to thread 2
thread apply all bt    # Backtrace all threads
set scheduler-locking on  # Only run current thread

# Scripting
define mycommand       # Define custom command
  bt
  info locals
end

# Pretty printing (if available)
set print pretty on
set print array on
set print elements 0   # Print all array elements

# Following forks
set follow-fork-mode child   # Follow child on fork
set follow-fork-mode parent  # Follow parent (default)
set detach-on-fork off       # Debug both

GDB Init File

# ~/.gdbinit
set history save on
set history size 10000
set print pretty on
set print array on
set print elements 0
set confirm off

# Custom prompt
set prompt (gdb) 

# Useful macros
define phead
  print *($arg0)
end
document phead
Print the head of a linked list node
end

Core Dumps

Enabling Core Dumps

# Check current limit
ulimit -c

# Enable core dumps (current session)
ulimit -c unlimited

# Enable permanently in /etc/security/limits.conf
* soft core unlimited
* hard core unlimited

# Set core dump pattern
echo "/tmp/cores/core.%e.%p" | sudo tee /proc/sys/kernel/core_pattern

# Or use systemd-coredump
sudo coredumpctl list
sudo coredumpctl gdb <pid>

Analyzing Core Dumps

# Load core dump
gdb ./program core

# In GDB
(gdb) bt                  # Where did it crash?
(gdb) frame 0             # Go to crash frame
(gdb) info locals         # What were the values?
(gdb) print *ptr          # Examine suspicious pointers

Triggering Core Dumps

#include <signal.h>
#include <stdlib.h>

// Manual core dump
void dump_core(void) {
    abort();  // Generates SIGABRT
}

// Or
raise(SIGSEGV);

Memory Debugging

Valgrind

# Memory leak detection
valgrind --leak-check=full ./program

# More detailed
valgrind --leak-check=full --show-leak-kinds=all \
         --track-origins=yes ./program

# Check for invalid memory access
valgrind --tool=memcheck ./program

# Thread error detection
valgrind --tool=helgrind ./program

# Cache profiling
valgrind --tool=cachegrind ./program

Understanding Valgrind Output

==12345== Invalid read of size 4
==12345==    at 0x400520: main (example.c:10)
==12345==  Address 0x5203040 is 0 bytes after a block of size 16 alloc'd
==12345==    at 0x4C2AB80: malloc (vg_replace_malloc.c:299)
==12345==    at 0x400510: main (example.c:9)
This means: reading 4 bytes right after a 16-byte allocation (buffer overflow).

AddressSanitizer

# Compile with ASan
gcc -fsanitize=address -g program.c -o program

# Run
./program
==12345==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000014
READ of size 4 at 0x602000000014 thread T0
    #0 0x400520 in main /path/example.c:10
    #1 0x7f123... in __libc_start_main

0x602000000014 is located 0 bytes to the right of 16-byte region

Common Memory Errors

// 1. Use after free
char *ptr = malloc(10);
free(ptr);
ptr[0] = 'x';  // ERROR!

// 2. Buffer overflow
char buf[10];
buf[10] = 'x';  // ERROR! Off by one

// 3. Memory leak
char *ptr = malloc(10);
ptr = malloc(20);  // Original 10 bytes leaked!

// 4. Double free
char *ptr = malloc(10);
free(ptr);
free(ptr);  // ERROR!

// 5. Invalid free
char buf[10];
free(buf);  // ERROR! Not from malloc

// 6. Uninitialized read
int x;
printf("%d\n", x);  // ERROR! Undefined value

// 7. Stack buffer overflow
char buf[10];
strcpy(buf, "This is too long!");  // ERROR!

Debugging Patterns

Printf Debugging (But Better)

#include <stdio.h>

// Compile-time debug toggle
#ifdef DEBUG
    #define DEBUG_PRINT(fmt, ...) \
        fprintf(stderr, "[DEBUG] %s:%d %s(): " fmt "\n", \
                __FILE__, __LINE__, __func__, ##__VA_ARGS__)
#else
    #define DEBUG_PRINT(fmt, ...) ((void)0)
#endif

// Usage
void process(int x) {
    DEBUG_PRINT("x = %d", x);
    // ...
}

// Compile with -DDEBUG to enable

Assertions

#include <assert.h>

void process(int *ptr, size_t size) {
    // Preconditions
    assert(ptr != NULL);
    assert(size > 0);
    
    // ... code ...
    
    // Postconditions (internal invariants)
    assert(result >= 0);
}

// Custom assertion with message
#define ASSERT_MSG(cond, msg) \
    do { \
        if (!(cond)) { \
            fprintf(stderr, "Assertion failed: %s\n  %s:%d: %s\n", \
                    #cond, __FILE__, __LINE__, msg); \
            abort(); \
        } \
    } while(0)

ASSERT_MSG(ptr != NULL, "Pointer must not be null");

Memory Debugging Wrapper

#ifdef DEBUG_MEMORY

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
    void *ptr;
    size_t size;
    const char *file;
    int line;
} Allocation;

#define MAX_ALLOCATIONS 10000
static Allocation allocations[MAX_ALLOCATIONS];
static size_t num_allocations = 0;

void *debug_malloc(size_t size, const char *file, int line) {
    void *ptr = malloc(size);
    if (ptr && num_allocations < MAX_ALLOCATIONS) {
        allocations[num_allocations++] = (Allocation){
            .ptr = ptr, .size = size, .file = file, .line = line
        };
    }
    fprintf(stderr, "[ALLOC] %p (%zu bytes) at %s:%d\n", ptr, size, file, line);
    return ptr;
}

void debug_free(void *ptr, const char *file, int line) {
    fprintf(stderr, "[FREE] %p at %s:%d\n", ptr, file, line);
    for (size_t i = 0; i < num_allocations; i++) {
        if (allocations[i].ptr == ptr) {
            allocations[i] = allocations[--num_allocations];
            free(ptr);
            return;
        }
    }
    fprintf(stderr, "[ERROR] Invalid free of %p at %s:%d\n", ptr, file, line);
    abort();
}

void debug_memory_report(void) {
    if (num_allocations == 0) {
        fprintf(stderr, "[MEMORY] No leaks detected!\n");
        return;
    }
    fprintf(stderr, "[MEMORY] %zu leaks detected:\n", num_allocations);
    for (size_t i = 0; i < num_allocations; i++) {
        fprintf(stderr, "  %p (%zu bytes) allocated at %s:%d\n",
                allocations[i].ptr, allocations[i].size,
                allocations[i].file, allocations[i].line);
    }
}

#define malloc(size) debug_malloc(size, __FILE__, __LINE__)
#define free(ptr) debug_free(ptr, __FILE__, __LINE__)

#endif // DEBUG_MEMORY

Tracing System Calls

# Trace all system calls
strace ./program

# Only specific syscalls
strace -e open,read,write ./program

# With timestamps
strace -t ./program
strace -tt ./program  # Microseconds
strace -T ./program   # Time in syscall

# Follow child processes
strace -f ./program

# Attach to running process
strace -p <pid>

# Summary statistics
strace -c ./program

ltrace (Library Calls)

# Trace library calls
ltrace ./program

# Specific libraries
ltrace -e malloc+free ./program

Debugging Crashes Systematically

1

Reproduce Consistently

Find the minimal input/steps to trigger the crash. Randomness = harder debugging.
2

Get the Stack Trace

Run in GDB or analyze core dump. Know exactly where it crashed.
3

Examine State

Check variable values, pointer validity, array bounds at crash site.
4

Find the Root Cause

The crash site is often not the bug. Trace backwards—when did state become invalid?
5

Verify Fix

Run the same reproduction steps. Then run your test suite.

Exercises

1

GDB Mastery

Write a program with a linked list. Use GDB to traverse the list, print node values, and find a bug you intentionally introduce.
2

Core Dump Analysis

Write a program that crashes. Generate a core dump and analyze it to find the crash location.
3

Memory Bug Hunt

Write a program with at least 3 different memory bugs. Use Valgrind and AddressSanitizer to find them all.
4

Debug Wrapper

Extend the debug_malloc wrapper to detect double-frees and use-after-free.

Next Up

Pointers Deep Dive

Master the heart of C programming