Debugging Techniques
Master debugging C programs: GDB debugger, print debugging, assertions, sanitizers, core dumps, and systematic debugging approaches. Learn to find and fix bugs efficiently in complex C codebases.
Understanding Debugging in C
C debugging is challenging: no runtime checks, manual memory management, undefined behavior. Good debugging skills are essential. Combine tools (GDB, Valgrind, sanitizers) with systematic approaches. Prevention through defensive programming is better than cure.
/* Common C bugs */
/*
1. Segmentation faults (null/invalid pointers)
2. Memory leaks (malloc without free)
3. Buffer overflows (array bounds)
4. Use-after-free
5. Uninitialized variables
6. Off-by-one errors
7. Integer overflow
8. Logic errors
*/
/* Debugging approaches */
/*
1. Print debugging (printf)
2. Interactive debugging (GDB)
3. Static analysis (compiler warnings)
4. Dynamic analysis (Valgrind, sanitizers)
5. Assertions
6. Logging
7. Core dumps
8. Systematic testing
*/
/* Compile for debugging */
/* gcc -g -O0 program.c -o program */
/* -g = include debug symbols */
/* -O0 = no optimization (easier debugging) */
/* Enable warnings */
/* gcc -Wall -Wextra -Werror program.c */
/* -Wall = all warnings */
/* -Wextra = extra warnings */
/* -Werror = treat warnings as errors */GDB (GNU Debugger)
GDB is the standard C debugger. Set breakpoints, step through code, inspect variables, examine memory. Essential for understanding program behavior and finding complex bugs. Learning GDB pays dividends throughout C career.
/* GDB basics */
/* Start GDB */
/* gdb ./program */
/* gdb --args ./program arg1 arg2 */
/* Basic commands */
/*
run (r) - Start program
break (b) - Set breakpoint
continue (c) - Continue execution
next (n) - Step over
step (s) - Step into
finish - Step out
print (p) - Print variable
backtrace (bt) - Stack trace
quit (q) - Exit GDB
*/
/* Example debugging session */
/* program.c */
#include <stdio.h>
void process_data(int *arr, int size) {
for (int i = 0; i <= size; i++) { /* Bug: should be < */
arr[i] = i * 2;
}
}
int main(void) {
int data[5] = {0};
process_data(data, 5);
for (int i = 0; i < 5; i++) {
printf("%d ", data[i]);
}
return 0;
}
/* Compile with debug info */
/* gcc -g -O0 program.c -o program */
/* GDB session */
/*
$ gdb ./program
(gdb) break main
Breakpoint 1 at 0x...: file program.c, line 10.
(gdb) run
Starting program...
Breakpoint 1, main () at program.c:10
(gdb) next
11 process_data(data, 5);
(gdb) step
process_data (arr=0x..., size=5) at program.c:4
(gdb) print size
$1 = 5
(gdb) break 5
Breakpoint 2 at 0x...: file program.c, line 5.
(gdb) continue
Breakpoint 2, process_data (arr=0x..., size=5) at program.c:5
(gdb) print i
$2 = 0
(gdb) print arr[i]
$3 = 0
(gdb) continue
Breakpoint 2, process_data (arr=0x..., size=5) at program.c:5
(gdb) print i
$4 = 1
(gdb) continue
...
(gdb) continue
Breakpoint 2, process_data (arr=0x..., size=5) at program.c:5
(gdb) print i
$5 = 5
(gdb) print arr[i]
*** Buffer overflow! ***
*/
/* Advanced GDB commands */
/* Conditional breakpoints */
/* (gdb) break process_data if i == 5 */
/* Watch variables */
/* (gdb) watch i */
/* Breaks when i changes */
/* Examine memory */
/* (gdb) x/10x arr */
/* Show 10 hex words at arr */
/* (gdb) x/10d arr */
/* Show 10 decimal words */
/* (gdb) x/s string_ptr */
/* Show string */
/* Call functions */
/* (gdb) call printf("Debug: %d\n", variable) */
/* Display auto-print */
/* (gdb) display i */
/* Shows i after each step */
/* Thread debugging */
/* (gdb) info threads */
/* (gdb) thread 2 */
/* (gdb) thread apply all bt */
/* Attach to running process */
/* gdb -p PID */
/* Core dump analysis */
/* gdb ./program core */
/* GDB scripting */
/* .gdbinit file */
/*
set print pretty on
set pagination off
break main
*/
/* TUI mode (text UI) */
/* gdb -tui ./program */
/* Or: (gdb) layout src */
/* (gdb) layout split */
/* Reverse debugging (record) */
/*
(gdb) record
(gdb) continue
(gdb) reverse-step
(gdb) reverse-continue
*/Print Debugging
Simple but effective: add printf statements to trace execution. Log variable values, function entry/exit, conditions. Remove or disable after debugging. Macros help with conditional compilation. Works everywhere, no special tools needed.
/* Basic print debugging */
void buggy_function(int x) {
printf("DEBUG: Entered buggy_function, x=%d\n", x);
if (x > 0) {
printf("DEBUG: x is positive\n");
/* ... */
} else {
printf("DEBUG: x is zero or negative\n");
/* ... */
}
printf("DEBUG: Exiting buggy_function\n");
}
/* Debug macros */
#ifdef DEBUG
#define DBG_PRINT(fmt, ...) \
fprintf(stderr, "[DEBUG %s:%d] " fmt "\n", \
__FILE__, __LINE__, ##__VA_ARGS__)
#else
#define DBG_PRINT(fmt, ...) ((void)0)
#endif
/* Usage */
DBG_PRINT("Processing item %d", i);
DBG_PRINT("Array size: %zu", size);
/* Compile with debug */
/* gcc -DDEBUG program.c */
/* Function entry/exit tracing */
#define TRACE_ENTER() \
fprintf(stderr, "ENTER: %s\n", __func__)
#define TRACE_EXIT() \
fprintf(stderr, "EXIT: %s\n", __func__)
void my_function(void) {
TRACE_ENTER();
/* Function body */
TRACE_EXIT();
}
/* Variable dumping */
#define DUMP_INT(var) \
printf("%s = %d\n", #var, var)
#define DUMP_PTR(var) \
printf("%s = %p\n", #var, (void*)var)
#define DUMP_STR(var) \
printf("%s = \"%s\"\n", #var, var)
/* Usage */
DUMP_INT(count);
DUMP_PTR(buffer);
DUMP_STR(message);
/* Conditional debug output */
int debug_level = 1; /* 0=off, 1=errors, 2=warnings, 3=info */
#define DEBUG_ERROR(fmt, ...) \
if (debug_level >= 1) \
fprintf(stderr, "[ERROR] " fmt "\n", ##__VA_ARGS__)
#define DEBUG_WARN(fmt, ...) \
if (debug_level >= 2) \
fprintf(stderr, "[WARN] " fmt "\n", ##__VA_ARGS__)
#define DEBUG_INFO(fmt, ...) \
if (debug_level >= 3) \
fprintf(stderr, "[INFO] " fmt "\n", ##__VA_ARGS__)
/* Array dumping */
void dump_array(const int *arr, size_t size, const char *name) {
printf("%s[%zu] = {", name, size);
for (size_t i = 0; i < size; i++) {
printf("%d%s", arr[i], (i < size - 1) ? ", " : "");
}
printf("}\n");
}
/* Memory dumping */
void dump_hex(const void *data, size_t size) {
const unsigned char *bytes = data;
for (size_t i = 0; i < size; i++) {
printf("%02X ", bytes[i]);
if ((i + 1) % 16 == 0) {
printf("\n");
}
}
printf("\n");
}
/* Execution counter */
static int exec_count[10] = {0};
#define COUNT_EXEC(id) exec_count[id]++
void report_counts(void) {
for (int i = 0; i < 10; i++) {
if (exec_count[i] > 0) {
printf("Location %d: %d times\n", i, exec_count[i]);
}
}
}
/* Timestamp debugging */
#include <time.h>
void debug_timestamp(const char *msg) {
time_t now = time(NULL);
printf("[%s] %s\n", ctime(&now), msg);
}
/* Performance timing */
#include <time.h>
#define TIME_START() clock_t _start = clock()
#define TIME_END(label) do { \
clock_t _end = clock(); \
double _elapsed = (double)(_end - _start) / CLOCKS_PER_SEC; \
printf("%s: %.6f seconds\n", label, _elapsed); \
} while(0)
/* Usage */
void slow_function(void) {
TIME_START();
/* Code to measure */
TIME_END("slow_function");
}Assertions and Defensive Programming
Assertions catch bugs early by checking assumptions. Use assert() for conditions that must be true. Enable in debug, disable in release with NDEBUG. Defensive programming prevents bugs: validate inputs, check returns, handle edge cases.
#include <assert.h>
/* Basic assertions */
void process_buffer(const char *buffer, size_t size) {
assert(buffer != NULL); /* Must not be NULL */
assert(size > 0); /* Must have size */
assert(size < 1000000); /* Sanity check */
/* Now safe to use buffer */
}
/* Assertions are compiled out with NDEBUG */
/* gcc -DNDEBUG program.c */
/* In release builds, asserts become no-ops */
/* Custom assert with message */
#ifdef DEBUG
#define ASSERT_MSG(cond, msg) \
if (!(cond)) { \
fprintf(stderr, "Assertion failed: %s (%s:%d)\n", \
msg, __FILE__, __LINE__); \
abort(); \
}
#else
#define ASSERT_MSG(cond, msg) ((void)0)
#endif
ASSERT_MSG(ptr != NULL, "Pointer must not be NULL");
/* Static assertions (compile-time) */
_Static_assert(sizeof(int) == 4, "int must be 4 bytes");
_Static_assert(sizeof(void*) == 8, "64-bit pointers required");
/* Defensive programming patterns */
/* 1. Validate all inputs */
int divide(int a, int b) {
if (b == 0) {
fprintf(stderr, "Error: Division by zero\n");
return 0; /* Or handle error */
}
return a / b;
}
/* 2. Check all allocations */
int* allocate_array(size_t size) {
int *arr = malloc(size * sizeof(int));
if (arr == NULL) {
fprintf(stderr, "Allocation failed\n");
return NULL;
}
return arr;
}
/* 3. Check function returns */
FILE *file = fopen("data.txt", "r");
if (file == NULL) {
perror("fopen failed");
return -1;
}
/* 4. Bounds checking */
void set_element(int *arr, size_t size, size_t index, int value) {
if (index >= size) {
fprintf(stderr, "Index out of bounds\n");
return;
}
arr[index] = value;
}
/* 5. Null pointer checks */
void process_string(const char *str) {
if (str == NULL) {
fprintf(stderr, "NULL string\n");
return;
}
/* Safe to use str */
}
/* 6. Initialize variables */
int value = 0; /* Always initialize */
char *ptr = NULL; /* Initialize pointers */
/* 7. Use const for read-only */
void print_string(const char *str) {
/* str cannot be modified */
}
/* 8. Check overflow */
int safe_add(int a, int b, int *result) {
if (a > 0 && b > INT_MAX - a) {
return -1; /* Overflow */
}
if (a < 0 && b < INT_MIN - a) {
return -1; /* Underflow */
}
*result = a + b;
return 0;
}
/* 9. Use safe string functions */
/* BAD */
strcpy(dest, src); /* No bounds check */
/* GOOD */
strncpy(dest, src, sizeof(dest) - 1);
dest[sizeof(dest) - 1] = '\0';
/* 10. Document preconditions */
/**
* Process buffer
* @pre buffer != NULL
* @pre size > 0
* @pre size <= MAX_SIZE
*/
void process(const char *buffer, size_t size);Dynamic Analysis Tools
Dynamic analysis tools detect runtime errors: Valgrind finds memory leaks, AddressSanitizer catches buffer overflows, ThreadSanitizer detects races. These tools are invaluable for finding bugs that don't crash immediately.
/* Valgrind - Memory error detector */
/* Run with Valgrind */
/* valgrind --leak-check=full ./program */
/* Common Valgrind options */
/*
--leak-check=full Detailed leak info
--show-leak-kinds=all All leak types
--track-origins=yes Track uninitialized values
--verbose More output
*/
/* Example Valgrind output */
/*
==12345== Invalid write of size 4
==12345== at 0x400542: main (program.c:10)
==12345== Address 0x... is 0 bytes after a block of size 40 alloc'd
*/
/* AddressSanitizer (ASan) - Fast memory error detector */
/* Compile with ASan */
/* gcc -fsanitize=address -g program.c -o program */
/* Detects:
- Buffer overflows
- Use-after-free
- Use-after-return
- Memory leaks
*/
/* Example ASan output */
/*
=================================================================
==12345==ERROR: AddressSanitizer: heap-buffer-overflow
WRITE of size 4 at 0x... thread T0
#0 0x... in main program.c:10
*/
/* UndefinedBehaviorSanitizer (UBSan) */
/* gcc -fsanitize=undefined program.c */
/* Detects:
- Integer overflow
- Null pointer dereference
- Invalid shifts
- Invalid casts
*/
/* ThreadSanitizer (TSan) - Race detector */
/* gcc -fsanitize=thread program.c */
/* Detects data races in multithreaded code */
/* MemorySanitizer (MSan) - Uninitialized memory */
/* clang -fsanitize=memory program.c */
/* Detects use of uninitialized memory */
/* Combining sanitizers */
/* gcc -fsanitize=address,undefined program.c */
/* Static analyzers */
/* Clang Static Analyzer */
/* scan-build gcc program.c */
/* Cppcheck */
/* cppcheck --enable=all program.c */
/* Splint */
/* splint program.c */
/* Using tools in development workflow */
/* 1. Compile with warnings */
/* gcc -Wall -Wextra -Werror program.c */
/* 2. Run with sanitizers */
/* gcc -fsanitize=address,undefined program.c */
/* ./a.out */
/* 3. Check with Valgrind */
/* valgrind --leak-check=full ./a.out */
/* 4. Static analysis */
/* cppcheck program.c */
/* 5. Review and fix */Core Dumps and Post-Mortem Debugging
Core dumps capture program state at crash. Analyze with GDB to find crash cause. Enable core dumps, reproduce crash, load dump in GDB, examine stack and variables. Essential for debugging crashes in production.
/* Enable core dumps */
/* Check current limit */
/* ulimit -c */
/* Enable unlimited core dumps */
/* ulimit -c unlimited */
/* Set in /etc/security/limits.conf */
/* * soft core unlimited */
/* Trigger core dump */
void crash_function(void) {
int *ptr = NULL;
*ptr = 42; /* Segmentation fault */
}
/* After crash, core file created */
/* core or core.12345 */
/* Analyze core dump */
/* gdb ./program core */
/*
(gdb) bt
#0 0x... in crash_function () at program.c:10
#1 0x... in main () at program.c:20
(gdb) frame 0
(gdb) print ptr
$1 = (int *) 0x0
(gdb) list
*/
/* Generate core dump manually */
#include <signal.h>
void trigger_core_dump(void) {
abort(); /* Generates core dump */
}
/* Or */
void trigger_core_dump2(void) {
raise(SIGABRT);
}
/* Core dump location */
/* Linux: Check with */
/* cat /proc/sys/kernel/core_pattern */
/* Set pattern */
/* echo "core.%e.%p" > /proc/sys/kernel/core_pattern */
/* %e = executable name */
/* %p = PID */
/* Debugging without core dump */
/* Attach to running process */
/* ps aux | grep program */
/* gdb -p PID */
/* Generate core from running process */
/* gcore PID */
/* Remote debugging */
/* Start gdbserver on remote */
/* gdbserver :1234 ./program */
/* Connect from local */
/* gdb ./program */
/* (gdb) target remote hostname:1234 */
/* Debugging tips */
/* 1. Reproduce reliably */
/* Find minimal test case */
/* 2. Check recent changes */
/* git bisect to find breaking commit */
/* 3. Simplify */
/* Remove code until bug disappears */
/* 4. Explain to rubber duck */
/* Explaining often reveals the bug */
/* 5. Take breaks */
/* Fresh perspective helps */
/* 6. Use version control */
/* Commit working states */
/* 7. Write tests */
/* Prevent regressions */
/* 8. Read error messages carefully */
/* They often point to the problem */
/* 9. Check documentation */
/* API might work differently than expected */
/* 10. Ask for help */
/* Fresh eyes find bugs faster */Summary & What's Next
Key Takeaways:
- ✅ GDB is essential for C debugging
- ✅ Print debugging works everywhere
- ✅ Use assertions to catch bugs early
- ✅ Valgrind finds memory leaks
- ✅ AddressSanitizer catches buffer overflows
- ✅ Core dumps enable post-mortem analysis
- ✅ Compile with -g and warnings
- ✅ Prevention beats debugging