Skip to content

C Debugging Techniques

This guide covers C programming debugging techniques, tools, and strategies for identifying and fixing issues in C code.

🎯 Debugging Fundamentals

Common C Debugging Challenges

  • Memory Issues: Segmentation faults, memory leaks, buffer overflows
  • Pointer Problems: Null pointers, dangling pointers, pointer arithmetic errors
  • Compilation Issues: Linker errors, undefined references
  • Runtime Errors: Logic errors, uninitialized variables, type mismatches

Debugging Methodology

  1. Reproduce the Issue: Create minimal test case
  2. Isolate the Problem: Binary search through code
  3. Form Hypothesis: Guess the root cause
  4. Test Hypothesis: Verify with debugging tools
  5. Fix and Verify: Apply fix and test thoroughly

🛠️ Debugging Tools

GDB (GNU Debugger)

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

# Start GDB
gdb ./program

# Common GDB commands
(gdb) break main          # Set breakpoint at main
(gdb) run               # Start program
(gdb) next              # Next line (step over)
(gdb) step              # Next line (step into)
(gdb) continue          # Continue execution
(gdb) print variable    # Print variable value
(gdb) bt                # Show backtrace
(gdb) info locals       # Show local variables
(gdb) quit              # Exit GDB

Advanced GDB Usage

// program.c
#include <stdio.h>
#include <stdlib.h>

void problematic_function(int* ptr, int size) {
    for (int i = 0; i <= size; i++) {  // Bug: should be i < size
        ptr[i] = i * 2;  // Buffer overflow
    }
}

int main() {
    int* data = malloc(sizeof(int) * 10);
    if (!data) return 1;

    problematic_function(data, 10);

    free(data);
    return 0;
}
# Debugging session with GDB
$ gdb ./program
(gdb) break problematic_function
(gdb) run
(gdb) print size
$1 = 10
(gdb) watch i
(gdb) continue
Hardware watchpoint 1: i

Old value = 9
New value = 10
0x0000000000400567 in problematic_function (ptr=0x601010, size=10)
(gdb) backtrace
#0  problematic_function (ptr=0x601010, size=10) at program.c:6
#1  0x0000000000400589 in main () at program.c:14
(gdb) quit

Valgrind (Memory Debugging)

# Install Valgrind
sudo apt-get install valgrind

# Check for memory leaks
valgrind --leak-check=full --show-leak-kinds=all ./program

# Check for memory errors
valgrind --tool=memcheck --track-origins=yes ./program

# Check for buffer overflows
valgrind --tool=address-sanitizer ./program

AddressSanitizer (ASan)

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

# Run the program
./program

# Output example:
==12345==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000050
WRITE of size 4 at 0x602000000050 thread T0
    #0 0x4007b7 in problematic_function program.c:6
    #1 0x400839 in main program.c:14

Static Analysis Tools

# Using cppcheck
cppcheck --enable=all program.c

# Using clang-static-analyzer
scan-build gcc program.c

# Using splint
splint program.c

🔍 Debugging Techniques

#include <stdio.h>

void debug_function(int* data, int size) {
    printf("DEBUG: Entering function with size=%d\n", size);

    for (int i = 0; i < size; i++) {
        printf("DEBUG: Processing index %d, value=%d\n", i, data[i]);

        // Some processing
        data[i] *= 2;

        printf("DEBUG: New value at index %d: %d\n", i, data[i]);
    }

    printf("DEBUG: Exiting function\n");
}

// Conditional debugging
#ifdef DEBUG
    #define DEBUG_PRINT(fmt, ...) printf(fmt, ##__VA_ARGS__)
#else
    #define DEBUG_PRINT(fmt, ...)
#endif

void optimized_function(int* data, int size) {
    DEBUG_PRINT("DEBUG: Processing %d elements\n", size);

    for (int i = 0; i < size; i++) {
        data[i] *= 2;
        DEBUG_PRINT("DEBUG: Element %d processed\n", i);
    }
}

Assertion-Based Debugging

#include <assert.h>
#include <stdio.h>

int divide(int a, int b) {
    assert(b != 0 && "Division by zero!");
    return a / b;
}

void process_array(int* arr, int size) {
    assert(arr != NULL && "Array pointer is NULL!");
    assert(size > 0 && "Array size must be positive!");

    for (int i = 0; i < size; i++) {
        assert(i >= 0 && i < size && "Array index out of bounds!");
        arr[i] *= 2;
    }
}

// Custom assertion macro
#define ASSERT(condition, message) \
    do { \
        if (!(condition)) { \
            fprintf(stderr, "Assertion failed: %s, file %s, line %d\n", \
                    message, __FILE__, __LINE__); \
            abort(); \
        } \
    } while(0)

void safe_function(int* ptr) {
    ASSERT(ptr != NULL, "Pointer cannot be null");
    // Function logic
}

Memory Debugging

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

// Custom memory tracker
typedef struct {
    void* ptr;
    size_t size;
    const char* file;
    int line;
} AllocationRecord;

#define MAX_ALLOCATIONS 1000
static AllocationRecord allocations[MAX_ALLOCATIONS];
static int allocation_count = 0;

void* debug_malloc(size_t size, const char* file, int line) {
    void* ptr = malloc(size);
    if (ptr && allocation_count < MAX_ALLOCATIONS) {
        allocations[allocation_count].ptr = ptr;
        allocations[allocation_count].size = size;
        allocations[allocation_count].file = file;
        allocations[allocation_count].line = line;
        allocation_count++;
    }
    return ptr;
}

void debug_free(void* ptr) {
    if (!ptr) return;

    for (int i = 0; i < allocation_count; i++) {
        if (allocations[i].ptr == ptr) {
            // Move last element to current position
            allocations[i] = allocations[allocation_count - 1];
            allocation_count--;
            break;
        }
    }

    free(ptr);
}

void print_leaks() {
    printf("Memory leaks detected:\n");
    for (int i = 0; i < allocation_count; i++) {
        printf("Leak: %p (%zu bytes) at %s:%d\n",
               allocations[i].ptr,
               allocations[i].size,
               allocations[i].file,
               allocations[i].line);
    }
}

// Macros for automatic file/line tracking
#define MALLOC(size) debug_malloc(size, __FILE__, __LINE__)
#define FREE(ptr) debug_free(ptr)

// Usage
void test_memory_tracking() {
    int* ptr1 = MALLOC(sizeof(int) * 10);
    int* ptr2 = MALLOC(sizeof(int) * 20);

    FREE(ptr1);
    // ptr2 is not freed - will be reported as leak

    print_leaks();
}

🐛 Common Bug Patterns

Segmentation Faults

// Common causes and fixes

// 1. Null pointer dereference
void fix_null_pointer() {
    int* ptr = NULL;

    // Bad: Dereferencing null pointer
    // *ptr = 42;  // Segmentation fault

    // Good: Check for null
    if (ptr != NULL) {
        *ptr = 42;
    }
}

// 2. Array out of bounds
void fix_array_bounds() {
    int arr[10];

    // Bad: Out of bounds access
    // arr[10] = 42;  // Segmentation fault

    // Good: Bounds checking
    for (int i = 0; i < 10; i++) {
        arr[i] = i;
    }
}

// 3. Use after free
void fix_use_after_free() {
    int* ptr = malloc(sizeof(int));
    *ptr = 42;
    free(ptr);

    // Bad: Using freed memory
    // *ptr = 100;  // Segmentation fault

    // Good: Set pointer to NULL after free
    ptr = NULL;
    if (ptr != NULL) {
        *ptr = 100;
    }
}

// 4. Stack overflow
void fix_stack_overflow() {
    // Bad: Too large stack allocation
    // char huge_array[10000000];  // Stack overflow

    // Good: Use heap allocation
    char* huge_array = malloc(10000000);
    if (huge_array) {
        // Use array
        free(huge_array);
    }
}

Memory Leaks

// Common memory leak patterns and fixes

// 1. Forgetting to free
void fix_memory_leak() {
    // Bad: Memory leak
    // int* ptr = malloc(sizeof(int));
    // *ptr = 42;
    // return;  // Forgot to free

    // Good: Always free allocated memory
    int* ptr = malloc(sizeof(int));
    if (ptr) {
        *ptr = 42;
        free(ptr);
    }
}

// 2. Lost pointer
void fix_lost_pointer() {
    int* ptr = malloc(sizeof(int) * 10);
    ptr = malloc(sizeof(int) * 20);  // Lost first allocation

    // Good: Free before reassigning
    int* ptr = malloc(sizeof(int) * 10);
    if (ptr) {
        free(ptr);
    }
    ptr = malloc(sizeof(int) * 20);
    if (ptr) {
        free(ptr);
    }
}

// 3. Leak in error handling
void fix_error_handling_leak() {
    int* ptr1 = malloc(sizeof(int) * 10);
    int* ptr2 = malloc(sizeof(int) * 10);

    if (!ptr1 || !ptr2) {
        // Bad: Incomplete cleanup
        // return;

        // Good: Clean up all allocations
        if (ptr1) free(ptr1);
        if (ptr2) free(ptr2);
        return;
    }

    // Use pointers
    free(ptr1);
    free(ptr2);
}

Logic Errors

// Common logic errors and fixes

// 1. Off-by-one errors
void fix_off_by_one() {
    int arr[10];

    // Bad: Off-by-one error
    // for (int i = 0; i <= 10; i++) {  // i = 10 is out of bounds

    // Good: Correct loop bounds
    for (int i = 0; i < 10; i++) {
        arr[i] = i;
    }
}

// 2. Integer overflow
void fix_integer_overflow() {
    int max_int = 2147483647;

    // Bad: Integer overflow
    // int result = max_int + 1;  // Overflow

    // Good: Check for overflow
    if (max_int > INT_MAX - 1) {
        printf("Would overflow\n");
    } else {
        int result = max_int + 1;
    }
}

// 3. Uninitialized variables
void fix_uninitialized() {
    int x;  // Uninitialized

    // Bad: Using uninitialized variable
    // printf("%d\n", x);

    // Good: Initialize variables
    int y = 0;
    printf("%d\n", y);
}

🔧 Advanced Debugging

Core Dump Analysis

# Enable core dumps
ulimit -c unlimited

# Run program that crashes
./program

# Analyze core dump with GDB
gdb ./program core

# In GDB
(gdb) bt full          # Full backtrace
(gdb) info registers   # Register values
(gdb) x/20x $esp       # Examine stack memory
(gdb) quit

Signal Handling

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

void signal_handler(int sig) {
    printf("Received signal %d\n", sig);

    switch (sig) {
        case SIGSEGV:
            printf("Segmentation fault occurred\n");
            break;
        case SIGFPE:
            printf("Floating point exception occurred\n");
            break;
        case SIGINT:
            printf("Interrupt received\n");
            break;
    }

    exit(1);
}

void setup_signal_handlers() {
    signal(SIGSEGV, signal_handler);
    signal(SIGFPE, signal_handler);
    signal(SIGINT, signal_handler);
}

void test_signal_handling() {
    setup_signal_handlers();

    // This will trigger SIGSEGV
    int* ptr = NULL;
    *ptr = 42;
}

Debugging Macros

// Comprehensive debugging macros
#include <stdio.h>
#include <time.h>

// Timestamp macro
#define TIMESTAMP() printf("[%ld] ", (long)time(NULL))

// Function entry/exit macros
#define FUNCTION_ENTRY() \
    TIMESTAMP(); printf("ENTER: %s\n", __func__)

#define FUNCTION_EXIT() \
    TIMESTAMP(); printf("EXIT: %s\n", __func__)

// Variable value macro
#define DUMP_VAR(var) \
    TIMESTAMP(); printf("%s = %d\n", #var, var)

// Array dump macro
#define DUMP_ARRAY(arr, size) \
    do { \
        TIMESTAMP(); printf("%s = [", #arr); \
        for (int i = 0; i < size; i++) { \
            printf("%d", arr[i]); \
            if (i < size - 1) printf(", "); \
        } \
        printf("]\n"); \
    } while(0)

// Usage
void debugged_function(int* data, int size) {
    FUNCTION_ENTRY();
    DUMP_VAR(size);
    DUMP_ARRAY(data, size);

    for (int i = 0; i < size; i++) {
        data[i] *= 2;
        DUMP_VAR(data[i]);
    }

    FUNCTION_EXIT();
}

📊 Performance Debugging

Profiling with Gprof

# Compile with profiling support
gcc -pg program.c -o program

# Run program to generate profile data
./program

# Generate profile report
gprof program gmon.out > profile.txt

# View profile report
cat profile.txt

Timing Measurements

#include <time.h>
#include <sys/time.h>

// High-resolution timing
double get_time_microseconds() {
    struct timeval tv;
    gettimeofday(&tv, NULL);
    return tv.tv_sec * 1000000.0 + tv.tv_usec;
}

// Function timing macro
#define TIME_FUNCTION(func, ...) \
    do { \
        double start = get_time_microseconds(); \
        func(__VA_ARGS__); \
        double end = get_time_microseconds(); \
        printf("Function %s took %.2f microseconds\n", #func, end - start); \
    } while(0)

// Usage
void expensive_function(int n) {
    // Simulate expensive operation
    volatile int sum = 0;
    for (int i = 0; i < n; i++) {
        sum += i;
    }
}

void test_timing() {
    TIME_FUNCTION(expensive_function, 1000000);
}

🎯 Debugging Best Practices

Defensive Programming

#include <assert.h>
#include <errno.h>
#include <string.h>

// Safe string operations
void safe_string_copy(char* dest, const char* src, size_t dest_size) {
    assert(dest != NULL);
    assert(src != NULL);
    assert(dest_size > 0);

    strncpy(dest, src, dest_size - 1);
    dest[dest_size - 1] = '\0';
}

// Safe file operations
FILE* safe_fopen(const char* filename, const char* mode) {
    FILE* file = fopen(filename, mode);
    if (!file) {
        fprintf(stderr, "Failed to open %s: %s\n", filename, strerror(errno));
        return NULL;
    }
    return file;
}

// Safe memory allocation
void* safe_malloc(size_t size) {
    void* ptr = malloc(size);
    if (!ptr) {
        fprintf(stderr, "Failed to allocate %zu bytes\n", size);
        exit(EXIT_FAILURE);
    }
    return ptr;
}

Error Handling Patterns

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

// Error codes
typedef enum {
    ERROR_SUCCESS = 0,
    ERROR_NULL_POINTER,
    ERROR_INVALID_PARAMETER,
    ERROR_MEMORY_ALLOCATION,
    ERROR_FILE_NOT_FOUND
} ErrorCode;

// Error handling macro
#define CHECK_ERROR(condition, error_code, message) \
    do { \
        if (!(condition)) { \
            fprintf(stderr, "Error: %s\n", message); \
            return error_code; \
        } \
    } while(0)

// Function with error handling
ErrorCode process_data(int* data, int size) {
    CHECK_ERROR(data != NULL, ERROR_NULL_POINTER, "Data pointer is NULL");
    CHECK_ERROR(size > 0, ERROR_INVALID_PARAMETER, "Size must be positive");

    // Process data
    for (int i = 0; i < size; i++) {
        data[i] *= 2;
    }

    return ERROR_SUCCESS;
}

// Usage
void test_error_handling() {
    int data[100];
    ErrorCode result = process_data(data, 100);

    if (result != ERROR_SUCCESS) {
        fprintf(stderr, "Processing failed with error %d\n", result);
    } else {
        printf("Processing succeeded\n");
    }
}

🔗 Language-Specific Debugging