Two essential debugging tools

Real-World Analogy

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 / Java (what you know)
# 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 (what you need gdb/valgrind for)
# 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
Always compile with -g for debugging

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

CommandShort formWhat it does
run [args]rStart the program (with optional arguments)
break mainb mainSet breakpoint at function main
break 42b 42Set breakpoint at line 42 of current file
nextnExecute next line (step over function calls)
stepsExecute next line (step into function calls)
continuecContinue running until next breakpoint or end
print xp xPrint the value of variable x
display xAuto-print x after every step
backtracebtShow the call stack (which functions were called to get here)
listlShow source code around current line
info localsPrint all local variables in current frame
quitqExit 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)
$sp is the stack pointer register. Examining memory directly helps when a pointer is corrupted and 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
valgrind runs your program ~10–50x slower and reports exact line numbers of errors (when compiled with -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
TSan reports the exact lines where the conflicting accesses occur in each thread, and identifies which threads are involved. This makes it far faster to diagnose races than inserting printf statements. Compile with -g for line numbers in the report.
Cannot combine TSan with ASan or MSan

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

Example 1 — Using gdb to find a segfault Course Lessons
/* 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 session
$ gcc -g -o bug bug.c
$ 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...
Example 2 — Using valgrind to find a memory leak and out-of-bounds write Course Lessons + Assignment Pattern
/* 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;
}
valgrind --leak-check=full output
==12345== Invalid write of size 4
==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
Example 3 — Setting breakpoints and inspecting in a loop gdb workflow
/* 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;
}
gdb session: inspect loop variable each iteration
(gdb) break 6
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

P1 — Identify the bug from a gdb backtrace Exam-style

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
Bug: 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.
P2 — Interpret valgrind output Assignment debugging pattern

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?

What happened: At line 8, a block of 40 bytes was allocated (enough for 5 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.
P3 — Fix the use-after-free bug Memory errors

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;
}
Use-after-free: After 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.
P4 — What gdb command would you use? gdb workflow

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.

(a) 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

Card 1 of 11
Question — click to flip
Answer
Click card to flip • Use buttons to navigate

Test your understanding

Topic 24 Quiz — Debugging Score: 0 / 7
1
What gcc flag is required for gdb to show source code line numbers and variable names?LO6
multiple choice
2
In gdb, what does the backtrace (bt) command show?LO6
multiple choice
3
True or False: In gdb, next steps over function calls while step steps into them.LO6
true / false
4
valgrind reports "definitely lost: 40 bytes". What does this mean?LO9
multiple choice
5
What is the difference between print x and display x in gdb?LO6
multiple choice
6
valgrind reports "Invalid write of size 4 at main.c:8. Address is 0 bytes after a block of size 12." What is the most likely bug?LO9
multiple choice
7
Which compiler flag enables ThreadSanitizer, and what class of bug does it detect?LO9
multiple choice
0/7
Quiz complete!