GDB: Debugging C, C++ and Rust Programs
In this tutorial, you'll learn GDB debugging including breakpoints, watchpoints, backtrace, memory inspection, conditional breakpoints, and debugging multi-threaded programs in C, C++, and Rust.
Why GDB Matters
Printf debugging works for simple programs, but fails when bugs are subtle, rare, or involve complex state. GDB lets you pause execution, inspect variables, step through code line by line, and examine memory. It is the standard debugger on Unix systems and supports C, C++, Rust, and many other compiled languages.
By the end of this guide, you will debug programs with breakpoints, watchpoints, backtraces, and memory inspection using GDB.
What is GDB?
GDB (GNU Project Debugger) is a portable debugger that lets you see what is happening inside a program while it runs or what it was doing at the moment of a crash.
flowchart TD A[GDB Debugging] --> B[Start Program] A --> C[Set Breakpoints] A --> D[Step Through Code] A --> E[Inspect State] B --> F[Run / Continue] C --> G[Function Breakpoints] C --> H[Line Breakpoints] C --> I[Conditional Breakpoints] E --> J[Variables] E --> K[Memory] E --> L[Registers] E --> M[Stack Trace]
Compiling for Debugging
Programs must be compiled with debug symbols (-g flag).
# C
gcc -g -o program program.c
# C++
g++ -g -o program program.cpp
# Rust
rustc -g main.rs
# or
cargo build
Example C Program
// crash.c
#include <stdio.h>
#include <stdlib.h>
void cause_crash() {
int *ptr = NULL;
*ptr = 42; // Segmentation fault
}
int main() {
printf("About to crash...\n");
cause_crash();
printf("This never runs\n");
return 0;
}
gcc -g -o crash crash.c
./crash
# Segmentation fault (core dumped)
Starting GDB
gdb ./crash
GDB Prompt
(gdb)
Running the Program
(gdb) run
Starting program: /home/user/crash
About to crash...
Program received signal SIGSEGV, Segmentation fault.
0x000055555555515e in cause_crash () at crash.c:6
6 *ptr = 42; // Segmentation fault
GDB stopped at the exact line where the crash occurred.
Breakpoints
# Set breakpoint at a function
(gdb) break main
Breakpoint 1 at 0x1149: file crash.c, line 9.
# Set breakpoint at a line
(gdb) break crash.c:6
Breakpoint 2 at 0x1156: file crash.c, line 6.
# List breakpoints
(gdb) info breakpoints
Num Type Disp Enb Address What
1 breakpoint keep y 0x0000555555555149 in main at crash.c:9
2 breakpoint keep y 0x0000555555555156 in cause_crash at crash.c:6
# Delete breakpoint
(gdb) delete 1
# Disable breakpoint
(gdb) disable 2
Conditional Breakpoints
#include <stdio.h>
int main() {
for (int i = 0; i < 100; i++) {
printf("i = %d\n", i);
}
return 0;
}
gcc -g -o loop loop.c
gdb ./loop
(gdb) break loop.c:5 if i == 50
(gdb) run
GDB stops only when i equals 50, saving you 50 manual iterations.
Expected Output
(gdb) run
Starting program: /home/user/loop
i = 0
...
i = 49
Breakpoint 1, main () at loop.c:5
5 printf("i = %d\n", i);
(gdb) print i
$1 = 50
Stepping Through Code
# Step into function
(gdb) step
# Step over (next line)
(gdb) next
# Continue until next breakpoint
(gdb) continue
# Step out of current function
(gdb) finish
# Execute next instruction in assembly
(gdb) stepi
Inspecting Variables
// vars.c
#include <stdio.h>
int main() {
int a = 10;
int b = 20;
int c = a + b;
char *name = "Alice";
printf("Result: %d\n", c);
printf("Name: %s\n", name);
return 0;
}
gcc -g -o vars vars.c
gdb ./vars
Variable Inspection Commands
(gdb) break 9
(gdb) run
(gdb) print a
$1 = 10
(gdb) print b
$2 = 20
(gdb) print c
$3 = 30
(gdb) print name
$4 = 0x555555556004 "Alice"
(gdb) print &a
$5 = (int *) 0x7fffffffe4dc
(gdb) display c # Auto-print every stop
1: c = 30
Examining Memory
(gdb) x/4xb &a # Examine 4 bytes in hex at address of a
0x7fffffffe4dc: 0x0a 0x00 0x00 0x00
(gdb) x/s name # Examine as string
0x555555556004: "Alice"
(gdb) x/16xw $rsp # Examine stack memory
Backtrace
When a program crashes or stops at a breakpoint, backtrace shows the call stack.
(gdb) backtrace
#0 cause_crash () at crash.c:6
#1 0x0000555555555173 in main () at crash.c:11
(gdb) backtrace full # With local variables
#0 cause_crash () at crash.c:6
No locals.
#1 0x0000555555555173 in main () at crash.c:10
No locals.
Frame Navigation
(gdb) frame 0 # Go to innermost frame
(gdb) frame 1 # Go to caller frame
(gdb) up # Move up one frame
(gdb) down # Move down one frame
Watchpoints
Watchpoints stop execution when a variable's value changes.
// watch.c
#include <stdio.h>
int main() {
int counter = 0;
for (int i = 0; i < 5; i++) {
counter += i;
}
printf("Counter: %d\n", counter);
return 0;
}
gdb ./watch
(gdb) break 6
(gdb) run
(gdb) watch counter
(gdb) continue
Expected Output
(gdb) continue
Continuing.
Hardware watchpoint 2: counter
Old value = 0
New value = 0
main () at watch.c:7
7 counter += i;
(gdb) continue
Continuing.
Hardware watchpoint 2: counter
Old value = 0
New value = 1
main () at watch.c:7
7 counter += i;
Debugging Rust Programs
// main.rs
fn divide(a: f64, b: f64) -> f64 {
if b == 0.0 {
panic!("Division by zero!");
}
a / b
}
fn main() {
let x = 10.0;
let y = 0.0;
let result = divide(x, y);
println!("Result: {}", result);
}
rustc -g main.rs -o divide
gdb ./divide
Rust-Specific Commands
(gdb) break 10 # Break at main function line
(gdb) run
(gdb) print x
$1 = 10
(gdb) print y
$2 = 0
(gdb) step
(gdb) step # Step into divide function
(gdb) print a
$3 = 10
(gdb) print b
$4 = 0
Multi-Threaded Debugging
// threads.c
#include <pthread.h>
#include <stdio.h>
void *worker(void *arg) {
int id = *(int *)arg;
for (int i = 0; i < 3; i++) {
printf("Thread %d: iteration %d\n", id, i);
}
return NULL;
}
int main() {
pthread_t t1, t2;
int id1 = 1, id2 = 2;
pthread_create(&t1, NULL, worker, &id1);
pthread_create(&t2, NULL, worker, &id2);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
return 0;
}
gcc -g -o threads threads.c -lpthread
gdb ./threads
Thread Commands
(gdb) break worker
(gdb) run
(gdb) info threads
Id Target Id Frame
1 Thread 0x7ffff7f... (process 1234) main ()
* 2 Thread 0x7ffff7f... worker (arg=0x7fffffffe4dc)
3 Thread 0x7ffff7e... worker (arg=0x7fffffffe4e0)
(gdb) thread 2
[Switching to thread 2]
(gdb) print id # Local variable in this thread
$1 = 1
Common Errors
| Problem | Cause | Fix |
|---|---|---|
No <a href="/compiler-design/symbol-table-management/">Symbol Table</a> is loaded |
Compiled without -g flag |
Recompile with gcc -g |
Cannot find bounds of current function |
Optimized code | Compile with -O0 to disable optimization |
Breakpoint not inserted |
Shared library not loaded | Use break after the library is loaded |
SIGSEGV in libc |
Invalid pointer | Check the return value of malloc |
| Thread breakpoint hits multiple times | Breakpoint not thread-specific | Use break worker thread 2 |
Practice Questions
1. What compiler flag is required for debugging symbols?
-g flag with GCC or Clang.
2. How do you set a breakpoint at a specific line number?
break filename.c:42.
3. What command shows the call stack?
backtrace (or bt).
4. How do you inspect the value of a variable?
print variable_name.
5. What is the difference between step and next in GDB?
step steps into function calls; next steps over them without entering.
Challenge
Write a C program with a memory leak or buffer overflow. Use GDB to identify the bug: set breakpoints at relevant functions, inspect memory, and trace the execution flow. Fix the bug and verify with GDB that the fix works.
Real-World Task
Use GDB to debug a multi-threaded C or C++ program that has a Race Condition. Set breakpoints in each thread, inspect shared variables at different execution points, and identify the interleaving that causes the bug. Apply a fix using mutexes and verify with GDB.
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro