C Notes
Master C debugging with GDB debugger, Valgrind memory checker, AddressSanitizer, printf debugging, and professional techniques to find and fix bugs efficiently.
Debugging is where C programmers spend a significant portion of their time. Without garbage collection or runtime safety checks, C programs can crash with cryptic segmentation faults, corrupt memory silently, or leak resources slowly. Mastering debugging tools transforms you from someone who stares at code helplessly into a developer who tracks down bugs systematically.
This guide covers the essential debugging tools and techniques every C programmer should know.
Compiling for Debugging
Before using any debugger, compile with debug information and without optimization:
// Compile with debug symbols
gcc -g -O0 -Wall -Wextra -o program program.c
// Flags explained:
// -g : Include debug symbols (line numbers, variable names)
// -O0 : No optimization (code maps directly to source)
// -Wall : Enable all warnings
// -Wextra : Even more warningsNever skip -Wall -Wextra – the compiler catches many bugs before you even run the program.
Printf Debugging
The simplest and most universal technique. While frowned upon by purists, strategic printf statements solve many problems quickly:
[search.c:22] Searching for 23 in array of size 10 [search.c:26] low=0, mid=4, high=9, arr[mid]=16 [search.c:26] low=5, mid=7, high=9, arr[mid]=56 [search.c:26] low=5, mid=6, high=6, arr[mid]=38 [search.c:26] low=5, mid=5, high=5, arr[mid]=23 Found at index: 5
GDB – The GNU Debugger
GDB is the standard debugger for C programs on Linux. It lets you pause execution, inspect variables, step through code line by line, and examine memory.
Essential GDB Commands
| Command | Short | Action |
|---|---|---|
run | r | Start the program |
break main | b main | Set breakpoint at main |
break file.c:25 | b file.c:25 | Breakpoint at line 25 |
next | n | Execute next line (step over) |
step | s | Step into function call |
continue | c | Continue to next breakpoint |
print var | p var | Print variable value |
print *ptr | p *ptr | Dereference and print |
backtrace | bt | Show call stack |
info locals | Show all local variables | |
watch var | Break when variable changes | |
quit | q | Exit GDB |
GDB Session Example
Consider this buggy program:
GDB session to find the bug:
$ gcc -g -O0 -o greet greet.c $ gdb ./greet (gdb) break create_greeting Breakpoint 1 at 0x401156: file greet.c, line 6. (gdb) run Breakpoint 1, create_greeting (name=0x402010 "World") at greet.c:6 (gdb) next (gdb) next (gdb) print buffer $1 = "Hello, World!\000\000\000\000\000\000" (gdb) print &buffer $2 = (char (*)[20]) 0x7fffffffe3b0 (gdb) finish (gdb) print msg $3 = 0x7fffffffe3b0 "" <-- Points to freed stack frame! (gdb) quit
The fix: use malloc() or pass a buffer as parameter.
Valgrind – Memory Error Detector
Valgrind detects memory leaks, use-after-free, buffer overflows, and uninitialized reads. It runs your program on a virtual CPU and tracks every memory operation.
Running with Valgrind:
$ gcc -g -O0 -o buggy buggy.c $ valgrind --leak-check=full ./buggy ==12345== Invalid write of size 4 ==12345== at 0x401180: main (buggy.c:13) ==12345== Address 0x4a47054 is 0 bytes after a block of size 20 alloc'd ==12345== Invalid read of size 4 ==12345== at 0x4011A0: main (buggy.c:17) ==12345== Address 0x4a47040 is 0 bytes inside a block of size 20 free'd ==12345== LEAK SUMMARY: ==12345== definitely lost: 100 bytes in 1 blocks ==12345== indirectly lost: 0 bytes in 0 blocks
AddressSanitizer (ASan)
AddressSanitizer is a compiler-based tool that's faster than Valgrind and catches many of the same bugs. It's built into GCC and Clang.
$ gcc -g -fsanitize=address -o overflow overflow.c
$ ./overflow
=================================================================
==12345==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x604000000028
WRITE of size 4 at 0x604000000028 thread T0
#0 0x4011a5 in main overflow.c:8
#1 0x7f1234 in __libc_start_main
0x604000000028 is located 0 bytes to the right of 40-byte regionOther Sanitizers
| Sanitizer | Flag | Detects |
|---|---|---|
| AddressSanitizer | -fsanitize=address | Buffer overflow, use-after-free, memory leaks |
| UndefinedBehaviorSanitizer | -fsanitize=undefined | Integer overflow, null deref, shift errors |
| ThreadSanitizer | -fsanitize=thread | Data races in multithreaded code |
| MemorySanitizer | -fsanitize=memory | Uninitialized memory reads (Clang only) |
Example with UBSan:
ubsan_demo.c:6:17: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
Assertions for Defensive Programming
Use assert() to catch impossible states during development:
10/2 = 5 data[0] = 42
Core Dump Analysis
When a program crashes, you can analyze the core dump:
$ ulimit -c unlimited # Enable core dumps $ ./crashing_program # Program crashes Segmentation fault (core dumped) $ gdb ./crashing_program core # Analyze the dump (gdb) bt # See where it crashed #0 0x00401180 in process_data (ptr=0x0) at main.c:15 #1 0x004011c0 in main () at main.c:22 (gdb) frame 0 (gdb) info locals ptr = 0x0 # Found it! NULL pointer
Debugging Strategy Flowchart
- Reproduce – Make the bug happen consistently
- Isolate – Find the smallest code that triggers it
- Identify – Use tools to find the exact location
- Fix – Apply the correction
- Verify – Confirm the fix and check for regressions
Interview Questions on Debugging
Q1: What is the difference between Valgrind and AddressSanitizer? Valgrind runs the program on a virtual CPU (10-50x slowdown) and detects issues at runtime. ASan uses compile-time instrumentation (2-3x slowdown) and is faster but requires recompilation. ASan catches most of what Valgrind catches, plus it's more precise with stack buffer overflows.
Q2: How do you find a memory leak in a large program? Use Valgrind with --leak-check=full --show-leak-kinds=all to get exact allocation locations. For production, use ASan with ASAN_OPTIONS=detect_leaks=1.
Q3: What causes a segmentation fault? Accessing memory that doesn't belong to your process: dereferencing NULL, accessing freed memory, buffer overflow past allocated bounds, stack overflow, or writing to read-only memory.
Q4: How do you debug a program that only crashes in production? Enable core dumps, add logging, use signal handlers to capture state on crash, compile with -g even for release builds (strippable), and try to reproduce with production data in a test environment.
Q5: What is a watchpoint in GDB? A watchpoint pauses execution whenever a specific memory location or variable changes value. Useful for finding where a variable gets corrupted: watch variable_name.
Summary
Effective debugging in C requires a combination of tools and techniques. Start with compiler warnings (-Wall -Wextra -Werror), use assertions for invariants, employ AddressSanitizer during development, run Valgrind for thorough memory checking, and use GDB for interactive debugging of complex issues. The best debugging strategy is prevention – write clean code, check return values, validate inputs, and use static analysis tools to catch bugs before they run.
Exam Focus
Revise definitions, diagrams, examples, and short-answer points for Debugging Techniques in C – GDB, Valgrind, and Sanitizers.
Interview Use
Prepare one clear explanation, one practical example, and one common mistake for this C Programming topic.
Search Terms
c-programming, c programming, programming, advanced, debugging, techniques, debugging techniques in c – gdb, valgrind, and sanitizers
Related C Programming Topics