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¶
- Reproduce the Issue: Create minimal test case
- Isolate the Problem: Binary search through code
- Form Hypothesis: Guess the root cause
- Test Hypothesis: Verify with debugging tools
- 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¶
Print Debugging¶
#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");
}
}
📚 Related Resources¶
- C Best Practices - Write debuggable code
- C Common Mistakes - Avoid common bugs
- C Performance Tips - Performance debugging
- C Resources - Debugging tools and documentation
🔗 Related Debugging Guides¶
- Logic Errors - Logic debugging
- Runtime Errors - Runtime debugging
- Code Review Checklist - Debugging review
- Self-Assessment Techniques - Debugging skills
🔗 Language-Specific Debugging¶
- Java Debugging Techniques - Java debugging
- Python Debugging Techniques - Python debugging
- Oracle Debugging Techniques - Oracle debugging