Debugging (gdb, valgrind)
Finding and fixing bugs with gdb — breakpoints, stepping, inspecting variables — and detecting memory errors with valgrind.
Two essential debugging tools
gdb is like a surgeon with a pause button — you stop your program mid-execution, look inside at every variable, then step forward one statement at a time. valgrind is like a health inspector who watches your program run and reports every time it touches memory it shouldn't. Use gdb for logic bugs, valgrind for memory bugs.
In Python and Java, when your program crashes you get a detailed traceback with file names, line numbers, and variable values right in the error message. In C, a crash produces Segmentation fault (core dumped) with no further explanation. You need gdb to find out where and why.
gdb (GNU Debugger) runs your program under its control. You can set breakpoints — lines where execution pauses — then inspect memory, call functions, and step through code line by line. It requires your program to be compiled with -g (debug symbols); without -g, gdb can only show assembly addresses, not your source code.
valgrind is a memory error detector. It intercepts every memory access and reports: reads/writes past array bounds ("Invalid read/write"), freeing memory twice ("Invalid free"), using memory after it was freed ("Use after free"), and memory allocated but never freed ("definitely lost"). Run valgrind on any C program that uses malloc.
# Python crash gives full traceback:
# Traceback (most recent call last):
# File "main.py", line 5, in foo
# return arr[10] # IndexError
# IndexError: list index out of range
# Java gives stack trace + exception type
# Built-in debugger in IDEs (breakpoints)
# C crash gives nothing useful:
# Segmentation fault (core dumped)
# Solution: compile with -g, use gdb
gcc -g -o prog main.c
gdb ./prog
(gdb) run
(gdb) backtrace # shows crash location
(gdb) print x # inspect variable
Without -g, gdb cannot show your source code, variable names, or line numbers — only raw memory addresses. Always add -g to your CFLAGS during development. Remove it (or use -O2 without -g) for production builds.
gdb commands and valgrind usage
Core gdb commands
| Command | Short form | What it does |
|---|---|---|
run [args] | r | Start the program (with optional arguments) |
break main | b main | Set breakpoint at function main |
break 42 | b 42 | Set breakpoint at line 42 of current file |
next | n | Execute next line (step over function calls) |
step | s | Execute next line (step into function calls) |
continue | c | Continue running until next breakpoint or end |
print x | p x | Print the value of variable x |
display x | Auto-print x after every step | |
backtrace | bt | Show the call stack (which functions were called to get here) |
list | l | Show source code around current line |
info locals | Print all local variables in current frame | |
quit | q | Exit gdb |
Examining memory in gdb
# x command: eXamine memory # Syntax: x/[count][format][size] address (gdb) x/10x $sp # 10 hex words at stack pointer (gdb) x/5d arr # 5 decimal ints starting at arr (gdb) x/s ptr # string (until null byte) at ptr # Format codes: x=hex, d=decimal, s=string, i=instruction # Size codes: b=byte, h=halfword(2), w=word(4), g=giant(8)
print *ptr crashes.
valgrind usage and output
# Basic usage (always compile with -g first) valgrind ./program valgrind --leak-check=full ./program # detailed leak report valgrind --leak-check=full --show-leak-kinds=all ./program # Key output messages: # "Invalid read of size 4" → reading past array end # "Invalid write of size 8" → writing past array end # "Invalid free()" → double-free or freeing stack mem # "definitely lost: 24 bytes" → memory leak (malloc, no free) # "Use of uninitialised value" → reading uninitialized memory
-g). It is the standard tool for diagnosing memory errors in COMP2017 assignments.
ThreadSanitizer (TSan) — Detect Data Races
ThreadSanitizer is a runtime instrumentation tool built into GCC and Clang that detects data races — when two threads access the same memory concurrently and at least one access is a write, without a lock protecting it. TSan is taught in Week 10 alongside -pthread and mutex usage.
# Compile with TSan (requires -pthread for threaded programs) gcc -fsanitize=thread -g -o prog prog.c -pthread # Run normally — TSan instruments the binary at compile time # and reports races at runtime when threads actually conflict # TSan report looks like: # WARNING: ThreadSanitizer: data race (pid=1234) # Write of size 4 at 0x... by thread T2: # #0 increment() thread_safe.c:8 # Previous read of size 4 at 0x... by thread T1: # #0 increment() thread_safe.c:8 # Cannot be combined with ASan or MSan in the same build # About 5–15x runtime overhead (less than Valgrind) # Reports: data races, lock ordering issues, use of freed mutexes
printf statements. Compile with -g for line numbers in the report.
TSan, ASan, and MSan each use incompatible runtime instrumentation — they cannot all be active in the same binary. Choose the tool that matches the bug you are hunting: use TSan for threading bugs, ASan for memory errors in single-threaded code, MSan for uninitialized read bugs.
Sanitizer and Valgrind Quick Reference
| Tool | Flag | Detects | Overhead |
|---|---|---|---|
| AddressSanitizer (ASan) | -fsanitize=address |
Memory errors (overflow, use-after-free, double-free) | ~2x |
| MemorySanitizer (MSan) | -fsanitize=memory |
Uninitialized reads | ~3x |
| ThreadSanitizer (TSan) | -fsanitize=thread |
Data races, lock ordering issues | ~5–15x |
| UBSan | -fsanitize=undefined |
Undefined behavior (integer overflow, null deref) | ~1.5x |
| Valgrind | valgrind --tool=memcheck |
Memory errors (no recompile needed) | ~10–50x |
Common debugging scenarios
/* bug.c — crashes with segfault */
#include <stdio.h>
void print_element(int *arr, int index) {
printf("Element: %d\n", arr[index]); /* line 5 */
}
int main(void) {
int arr[5] = {1, 2, 3, 4, 5};
print_element(arr, 10); /* BUG: out of bounds */
return 0;
}
$ gdb ./bug
(gdb) run
Program received signal SIGSEGV, Segmentation fault.
0x00005555555546a5 in print_element (arr=0x7ffd..., index=10) at bug.c:5
5 printf("Element: %d\n", arr[index]);
(gdb) backtrace
#0 print_element (arr=0x7ffd..., index=10) at bug.c:5
#1 main () at bug.c:10
(gdb) print index
$1 = 10
(gdb) print arr[10]
Cannot access memory at address 0x7ffd...
/* memleak.c */
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *arr = malloc(5 * sizeof(int)); /* allocate 5 ints */
for (int i = 0; i <= 5; i++) { /* BUG: i <= 5, writes index 5 */
arr[i] = i * 2;
}
/* BUG: no free(arr) — memory leak */
return 0;
}
==12345== at 0x10916F: main (memleak.c:7)
==12345== Address 0x5204054 is 0 bytes after a block of size 20 alloc'd
==12345== at 0x483B7F3: malloc
==12345==
==12345== LEAK SUMMARY:
==12345== definitely lost: 20 bytes in 1 blocks
==12345==
==12345== ERROR SUMMARY: 1 errors from 1 contexts
/* debug_demo.c */
#include <stdio.h>
int sum(int n) {
int total = 0;
for (int i = 1; i <= n; i++) {
total += i; /* line 6 — set breakpoint here */
}
return total;
}
int main(void) {
int result = sum(5);
printf("Sum = %d\n", result);
return 0;
}
Breakpoint 1 at 0x...: file debug_demo.c, line 6.
(gdb) run
Breakpoint 1, sum (n=5) at debug_demo.c:6
(gdb) display total
(gdb) display i
(gdb) continue [×5 to see each iteration]
1: total = 0, i = 1
1: total = 1, i = 2
1: total = 3, i = 3 ...
Practice problems with solutions
A program crashes and gdb shows this backtrace. What is the likely bug and on which line?
#0 0x00007f... in __strlen_avx2 ()
#1 0x00005555 in process_name (name=0x0) at names.c:12
#2 0x00005555 in main () at names.c:25
name is NULL (0x0) in process_name. The function called strlen (or a similar string function) on a NULL pointer at line 12, causing a segfault inside the C library.Fix: Add a NULL check before using
name:
void process_name(char *name) {
if (name == NULL) return; // guard
// ... use name safely
}
The backtrace reads bottom-up: main called process_name at names.c:25; inside that function at line 12, a strlen call crashed because name was NULL.
valgrind reports: Invalid read of size 8 at main.c:15. Address 0x5204068 is 8 bytes after a block of size 40 alloc'd at main.c:8. What happened and how do you fix it?
long values = 5 × 8 bytes). At line 15, the code read 8 bytes starting 40 bytes past the beginning of the block — that is index [5] of a 5-element array (valid indices are 0–4). This is an off-by-one out-of-bounds read.Typical cause:
long *arr = malloc(5 * sizeof(long)); // line 8
for (int i = 0; i <= 5; i++) { // BUG: should be i < 5
printf("%ld\n", arr[i]); // line 15: arr[5] OOB
}
Fix: Change <= 5 to < 5.
valgrind reports "Invalid read of size 4" and "Use of freed memory" on the line printf("%d\n", *p);. Find and fix the bug in this code.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *p = malloc(sizeof(int));
*p = 42;
free(p);
printf("%d\n", *p); /* BUG is here */
return 0;
}
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *p = malloc(sizeof(int));
*p = 42;
printf("%d\n", *p); /* use BEFORE free */
free(p);
p = NULL; /* optional: prevent accidental reuse */
return 0;
}
free(p), the memory at p's address is returned to the allocator and may be reused for something else. Reading it is undefined behavior — the value could be garbage, or the program could crash. Always use before free. Setting p = NULL after freeing is defensive: a NULL dereference crashes loudly, making bugs easier to find.
For each scenario, state the gdb command: (a) Your program crashed — you want to see which function was called to reach the crash. (b) You want to pause execution at the start of process_data(). (c) You are paused inside a loop and want to see the value of counter automatically at every step.
backtrace (or bt) — shows the call stack, listing each function and file/line number from innermost (crash site) to outermost (main). Read it bottom-up to understand how you got to the crash.(b)
break process_data (or b process_data) — sets a breakpoint at the entry of the named function. Alternative: break filename.c:linenum if you know the line.(c)
display counter — registers counter to be printed automatically after every step/next/continue that pauses. Unlike print counter (which you must type each time), display persists.
Key concepts to memorize
Test your understanding
backtrace (bt) command show?LO6next steps over function calls while step steps into them.LO6print x and display x in gdb?LO6