LEARN GDB DEEP DIVE
GDB is the quintessential tool for inspecting a program's soul. While IDE debuggers are convenient, mastering GDB gives you a superpower: the ability to debug anything, anywhere—on a remote server, in a minimal Docker container, or on an embedded device—with maximum control and insight. It's the difference between driving a car and being the mechanic who can build the engine.
Learn GDB: From Zero to Debugging Master
Goal: Deeply understand the GNU Debugger (GDB)—from basic commands and crash analysis to advanced scripting, reverse debugging, and the underlying mechanisms that make it all work.
Why Learn GDB?
GDB is the quintessential tool for inspecting a program’s soul. While IDE debuggers are convenient, mastering GDB gives you a superpower: the ability to debug anything, anywhere—on a remote server, in a minimal Docker container, or on an embedded device—with maximum control and insight. It’s the difference between driving a car and being the mechanic who can build the engine.
After completing these projects, you will:
- Confidently navigate the state of any C/C++ program: stack, heap, registers.
- Diagnose and fix segmentation faults and other crashes in minutes.
- Use advanced features like conditional breakpoints and watchpoints to find complex bugs.
- Automate your debugging workflows with Python scripting.
- Understand how a debugger actually controls a process at the OS level.
Core Concept Analysis
1. The Debugger-Debuggee Relationship (via ptrace)
On Linux, GDB works its magic using the ptrace (process trace) system call. This is the kernel-level API that allows one process (GDB) to observe and control another (the “debuggee”).
┌──────────────────┐ ┌──────────────────┐
│ GDB │ │ Target Program │
│ (The Tracer) │ │ (The "Tracee") │
└──────────────────┘ └──────────────────┘
│ ▲
│ PTRACE_ATTACH │ (Process stops)
├──────────────────────────────────────────────────►
│ │
│ PTRACE_PEEKDATA (Read memory/registers) │
├──────────────────────────────────────────────────►
│ │
│ PTRACE_POKEDATA (Write memory/registers) │
├──────────────────────────────────────────────────►
│ │
│ PTRACE_SINGLESTEP (Execute one instruction) │
├──────────────────────────────────────────────────►
│ ▲
│ PTRACE_CONT (Continue execution) │ (Process runs until
├──────────────────────────────────────────────────► next signal)
│ │
2. Anatomy of a Breakpoint
A software breakpoint is not magic. GDB simply replaces an instruction in the target’s code with a special trap instruction (int 3 on x86).
Before GDB:
Address Instruction
0x401000 mov eax, ebx
0x401002 add ecx, edx <-- You want to break here
0x401004 sub eax, 1
After `break *0x401002`:
Address Instruction
0x401000 mov eax, ebx
0x401002 int 3 <-- Trap instruction (1 byte)
0x401004 sub eax, 1
When the CPU hits `int 3`:
1. The CPU generates a "trap" signal.
2. The kernel catches the signal and notifies the tracer (GDB).
3. GDB inspects the program state.
4. To continue, GDB replaces `int 3` with the original instruction (`add ecx, edx`), single-steps the CPU, and then puts the `int 3` back.
Project List
The best way to learn GDB is to solve a series of increasingly difficult debugging challenges. Each “project” is a C program with a specific bug, designed to teach you a new GDB skill.
Project 1: The Basics - First Steps
- File: LEARN_GDB_DEEP_DIVE.md
- Main Programming Language: GDB Commands (on a C target)
- Alternative Programming Languages: N/A
- Coolness Level: Level 2: Practical but Forgettable
- Business Potential: 1. The “Resume Gold”
- Difficulty: Level 1: Beginner
- Knowledge Area: Debugging Fundamentals
- Software or Tool: GDB, GCC
- Main Book: “The Art of Debugging with GDB” by Matloff & Salzman
What you’ll build: A simple C program with a for loop and a function call. You will use GDB to step through it, inspect variables, and understand the flow of execution.
Why it teaches GDB: This project builds the foundational muscle memory for the most common debugging loop: run, stop, inspect, continue.
Core challenges you’ll face:
- Compiling for debugging → maps to using the
-gflag in GCC - Starting a GDB session → maps to
gdb ./a.out - Controlling execution → maps to
run,break,continue,next,step - Inspecting state → maps to
printfor variables,backtracefor the call stack
Key Concepts:
- Symbols and the
-gflag: “The Art of Debugging” Chapter 1 - Basic GDB Commands: GDB
helpcommand - Stack Frames: “The Art of Debugging” Chapter 3
Difficulty: Beginner Time estimate: 1-2 hours Prerequisites: Basic C knowledge.
Real world outcome:
You will compile this C code with gcc -g -o target target.c and debug it.
// target.c
#include <stdio.h>
void greet(int count) {
printf("Hello for the %dth time!\n", count);
}
int main() {
int i;
for (i = 0; i < 5; ++i) {
greet(i);
}
return 0;
}
Debugging Session:
$ gdb ./target
(gdb) break main # Set a breakpoint at the start of main
(gdb) run # Start the program
(gdb) next # Execute 'int i;'
(gdb) next # Execute 'for (i = 0; ...)'
(gdb) print i # See the value of i
$1 = 0
(gdb) step # Step *into* the greet() function
greet (count=0) at target.c:4
(gdb) backtrace # See the call stack (main -> greet)
(gdb) continue # Run until the program finishes
Learning milestones:
- Set a breakpoint and run to it → You can control where execution starts.
- Distinguish
nextfromstep→ You understand stepping over vs. into functions. - Print a variable’s value → You can inspect program state.
- View the call stack → You understand program structure.
Real World Outcome
Complete Compilation and GDB Session
Save as target.c and compile with debug symbols:
$ gcc -g -o target target.c
$ file target
target: ELF 64-bit LSB executable, with debug_info, not stripped
Full GDB Session Output:
$ gdb ./target
GNU gdb (GDB) 14.1
Reading symbols from ./target...
(gdb) break main
Breakpoint 1 at 0x401160: file target.c, line 9.
(gdb) run
Starting program: ./target
[Thread debugging using libthread_db enabled]
Breakpoint 1, main () at target.c:9
9 {
(gdb) next
10 for (i = 0; i < 5; ++i) {
(gdb) print i
$1 = 0
(gdb) next
11 greet(i);
(gdb) step
greet (count=0) at target.c:5
5 printf("Hello for the %dth time!\n", count);
(gdb) backtrace
#0 greet (count=0) at target.c:5
#1 0x00005555555551ac in main () at target.c:11
(gdb) backtrace full
#0 greet (count=0) at target.c:5
#1 0x00005555555551ac in main () at target.c:11
(gdb) print count
$2 = 0
(gdb) info frame
Stack level 0, frame at 0x7fffffffdf20:
rip = 0x40513b in greet; saved rip 0x40518f
called by frame at 0x7fffffffdf30
source language c.
Arglist at 0x7fffffffdf10, args: count=0
Locals at 0x7fffffffdf10, Previous frame's sp is 0x7fffffffdf20
(gdb) finish
Run till exit from #0 greet (count=0) at target.c:5
Hello for the 0th time!
0x00005555555551b8 in main () at target.c:10
(gdb) info locals
i = 1
(gdb) continue
Continuing.
Hello for the 1st time!
Hello for the 2nd time!
Hello for the 3rd time!
Hello for the 4th time!
[Inferior 1 (process 12345) exited normally]
(gdb) quit
Key Points:
- Breakpoints work because of
-gflag - Variables have correct values at each step
- Call stack clearly shows function chain
nextvsstepdistinction is evident
Common Troubleshooting:
| Problem | Solution |
|---|---|
| “No line numbers” | Recompile: gcc -g -o target target.c |
| “Cannot access memory” | Variable optimized away; use -O0 |
Variable shows <optimized out> |
Recompile without optimization flags |
The Core Question You’re Answering
“How do I control a program’s execution and inspect its state at any point?”
This separates professional debugging (GDB) from ad-hoc debugging (printf statements).
Concepts You Must Understand First
1. Debug Symbols (-g flag)
- Includes DWARF info: maps instructions to source lines and variable names
- Without it: only see memory addresses, not code
2. Stack Frames
- Each function call creates a frame: parameters, return address, locals
- Stack grows down;
rbp= frame start,rsp= top - Trace shows entire call chain
3. x86-64 Registers
- Arguments:
rdi,rsi,rdx,rcx,r8,r9 - Return:
rax - Pointers:
rsp(stack),rbp(frame),rip(instruction)
4. How ptrace Works
- GDB uses
ptrace()system call to observe and control the target - Can read/write memory, registers, single-step, set breakpoints
- Kernel mediates all interaction
Questions to Guide Your Design
- Difference between
nextandstep?nextstays in function;stepenters calls. - How does GDB map instructions to source lines? Debug symbols in binary.
- Why can’t you print variables without
-g? GDB doesn’t know variable names/types. - What does
backtraceshow? Function call chain with line numbers and args. - Does modifying a variable in GDB persist? Yes; program uses the new value.
Thinking Exercise
Manually trace the program, predicting:
- Value of
iat iteration 2? - Value of
countinsidegreetat that point? - What’s on the call stack?
The Interview Questions They’ll Ask
- “What’s the difference between
nextandstep?”next: execute one line, don’t enter functionsstep: execute one line, enter function calls
- “How do you interpret a backtrace?”
- Bottom frame is
main; top is current location - Each frame shows function name, file, line, and arguments
- Bottom frame is
- “How do you view function parameters?”
print <name>,info args, orinfo locals
- “Why is the
-gflag necessary?”- Embeds DWARF symbols to map machine code back to source
- “How would you debug a crash you didn’t witness?”
- Load core dump:
gdb ./program corethenbacktrace
- Load core dump:
Hints in Layers
Hint Layer 1: Setup
gcc -g -o target target.cgdb ./targetbreak mainrun
Hint Layer 2: Stepping
nextexecutes one linestepenters function callsprint ishows variable valuebacktraceshows call stackfinishexits current function
Hint Layer 3: Inspection
break 11sets breakpoint at lineinfo frameshows frame detailsinfo registersshows CPU statex/s $rdiexamines memoryinfo localslists local variables
Hint Layer 4: Advanced
set var i = 10modifies variabledisassemble greetshows assemblystepisingle-steps instructionsinfo breaklists all breakpointsdelete 1removes breakpoint 1
Books That Will Help
| Topic | Book | Chapter | Focus |
|---|---|---|---|
| GDB Basics | “The Art of Debugging with GDB” | Chapters 1–3 | Fundamental commands |
| Stack Frames | “Computer Systems” (CSAPP) | Chapter 3 | Call stack organization |
| Debug Symbols | GDB Manual | “Symbols” | DWARF format |
| Calling Conventions | “Computer Systems” | Chapter 3 | Register usage |
| C Functions | “The C Programming Language” | Chapter 4 | Function mechanics |
Project 2: The Crash - Core Dump Analysis
- File: LEARN_GDB_DEEP_DIVE.md
- Main Programming Language: GDB Commands (on a C target)
- Alternative Programming Languages: N/A
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 1. The “Resume Gold”
- Difficulty: Level 1: Beginner
- Knowledge Area: Crash Analysis / Debugging
- Software or Tool: GDB, ulimit
- Main Book: “The Art of Debugging with GDB” by Matloff & Salzman
What you’ll build: A C program that reliably segfaults (e.g., by dereferencing a NULL pointer). You will learn to use GDB to perform a “post-mortem” analysis on the resulting core dump file.
Why it teaches GDB: Debugging a live process isn’t always possible. Understanding how to analyze a core dump is a critical skill for debugging crashes in production or non-interactive environments.
Core challenges you’ll face:
- Triggering a segfault → maps to writing buggy code
- Enabling core dumps → maps to
ulimit -c unlimitedshell command - Loading a core dump → maps to
gdb <executable> <corefile> - Inspecting the crashed state → maps to finding the exact line and reason for the crash
Key Concepts:
- Pointers and Memory: “The C Programming Language” Chapter 5
- Core Dump Analysis: “The Linux Programming Interface” Chapter 23
Difficulty: Beginner Time estimate: 1-2 hours Prerequisites: Project 1.
Real world outcome:
You will compile gcc -g -o crash crash.c, run ./crash, and then analyze the crash.
// crash.c
#include <stdio.h>
void crash_me() {
char *p = NULL;
*p = 'A'; // Segfault!
}
int main() {
crash_me();
return 0;
}
Debugging Session:
# First, enable core dumps in your shell
$ ulimit -c unlimited
# Run the program to make it crash
$ ./crash
Segmentation fault (core dumped)
# Analyze the core dump with GDB
$ gdb ./crash core
(gdb) backtrace
#0 0x000055555555513d in crash_me () at crash.c:5
#1 0x0000555555555152 in main () at crash.c:9
(gdb) frame 0
#0 0x000055555555513d in crash_me () at crash.c:5
5 *p = 'A'; // Segfault!
(gdb) print p
$1 = 0x0 <_start-0x100> # The pointer is NULL!
Learning milestones:
- Generate a core dump → You know how to configure the shell for debugging.
- Load the core dump into GDB → You can start a post-mortem session.
- Use
backtraceto find the faulting function → You can pinpoint the location of a crash. - Inspect variables to find the root cause → You can determine why it crashed.
Real World Outcome
Setup and Compilation
$ gcc -g -o crash crash.c
$ ulimit -c unlimited
$ ./crash
Segmentation fault (core dumped)
$ ls -lh core
-rw------- 1 user user 400K Nov 27 10:15 core
Full GDB Post-Mortem Session:
$ gdb ./crash core
GNU gdb (GDB) 14.1
Core was generated by './crash'.
Program terminated with signal SIGSEGV, Segmentation fault.
Reading symbols from ./crash...
(gdb) backtrace
#0 0x000055555555513d in crash_me () at crash.c:5
#1 0x0000555555555152 in main () at crash.c:9
(gdb) # We immediately see the crash was in crash_me at line 5
(gdb) frame 0
#0 0x000055555555513d in crash_me () at crash.c:5
5 *p = 'A'; // Segfault!
(gdb) # Now we're in the crashing frame. Let's inspect the pointer.
(gdb) print p
$1 = (char *) 0x0
(gdb) # The pointer is NULL! That's the bug.
(gdb) print &p
$2 = (char **) 0x7fffffffdf1c
(gdb) # Let's see what was on the stack
(gdb) info locals
p = 0x0
(gdb) # Now let's examine the memory at the time of crash
(gdb) x/10x $rsp
0x7fffffffdf10: 0x555555551152 0x0000000000000000 0x7fffffff0000
0x7fffffffdf1c: 0x0000000000000000 0x0000000000000000 0x0000000000000000
(gdb) # Let's look at registers at crash time
(gdb) info registers
rax 0x55555555513d 93824992235837 (instruction pointer location)
rbx 0x0 0
rcx 0x2 2
rdx 0x2 2
rsi 0x7fffffff0000 140737488355328
rdi 0x7fffffff0000 140737488355328
rbp 0x7fffffffdf10 0x7fffffffdf10
rsp 0x7fffffffdf10 0x7fffffffdf10
r8 0x0 0
r9 0x0 0
r10 0x0 0
r11 0x2 2
r12 0x555555554000 93824992235520
r13 0x0 0
r14 0x0 0
r15 0x0 0
rip 0x55555555513d 0x55555555513d <crash_me+4>
(gdb) # The instruction pointer RIP shows exactly where it faulted
(gdb) # Let's disassemble to see the exact instruction
(gdb) disassemble crash_me
Dump of assembler code for function crash_me:
0x0000555555555139 <+0>: push %rbp
0x000055555555513a <+1>: mov %rsp,%rbp
0x000055555555513d <+4>: movq $0x0,-0x8(%rbp) # Store NULL into p
0x0000555555555145 <+12>: mov -0x8(%rbp),%rax
0x0000555555555149 <+16>: movb $0x41,(%rax) # Try to write to NULL address <- SEGFAULT HERE
0x000055555555514c <+19>: nop
0x000055555555514d <+20>: pop %rbp
0x000055555555514e <+28>: ret
End of dump.
(gdb) # Perfect! We can see the exact instruction that caused the crash
(gdb) # Let's also look at frame 1 to see who called this
(gdb) frame 1
#1 0x0000555555555152 in main () at crash.c:9
9 }
(gdb) backtrace full
#0 0x000055555555513d in crash_me () at crash.c:5
#1 0x0000555555555152 in main () at crash.c:9
(gdb) quit
Key Observations:
- Signal was SIGSEGV (segmentation violation)
backtracepinpoints exact function and lineprint pshows the NULL pointerdisassembleconfirms the instruction that faulted- Core dump captured exact register state at crash
Finding Core Files:
# Modern systems may put core files in /var/crash or /proc/sys/kernel/core_pattern
$ find /var -name "core*" 2>/dev/null
$ cat /proc/sys/kernel/core_pattern
$ # On macOS: core files go to /cores
Common Troubleshooting:
| Problem | Cause | Solution |
|---|---|---|
| “Could not open core dump file” | Wrong path | Use gdb ./crash ./core with full paths |
| “No core file generated” | ulimit not set | Run ulimit -c unlimited in same shell |
| “Symbols not found” | Stripped binary | Recompile with -g flag |
| “Cannot access memory at address” | Address invalid at core time | Memory not in core dump; use x/[size] |
The Core Question You’re Answering
“How do I debug a crash that happened when I wasn’t watching?”
This is critical for production debugging: you get a core dump, not a running process. You must extract all information from that frozen state.
Concepts You Must Understand First
1. Signals and SIGSEGV
SIGSEGVis sent when you access invalid memory- The kernel catches it and creates a core dump (if enabled)
- Core dump is a snapshot of entire memory + registers at crash time
2. Core Dump Files
- Contains complete memory image, registers, stack
- Location controlled by
ulimit -cand kernel settings - Format: ELF core file (machine-readable)
3. Pointer Dereference Errors
- NULL pointer: accessing address 0x0
- Invalid pointer: garbage address
- Out-of-bounds: valid pointer but wrong offset
4. Stack Frame at Crash
- Last frame shows exactly where crash happened
- Registers capture CPU state at crash moment
- Memory dump shows what was on stack
5. How Core Dumps Are Generated
- Kernel catches signal and suspends process
- Entire memory and register state dumped to file
- File location: current working directory (usually) or
/var/crash
Questions to Guide Your Design
- Why is compiling with
-gessential for core dump analysis? Without symbols, you only see addresses, not variable names or line numbers. - What’s the difference between a NULL pointer and a garbage pointer? NULL is predictable (address 0); garbage is unpredictable.
- How do you know if the crash was due to a pointer error vs. a stack overflow? Check the faulting address and
backtracedepth. - Can you modify a program and still load the same core dump? Yes, but symbol mappings may be wrong; recompile the same version.
- What information is lost from a core dump that you’d have from a live process? Nothing critical; core dump has everything; you just can’t step forward.
Thinking Exercise
Imagine a core dump shows a crash in malloc(). The backtrace shows:
#0 malloc (...)
#1 my_allocate_buffer ()
#2 main ()
What are possible causes?
- Heap corruption from previous code
- Integer overflow in size calculation
- Double-free attempt
- Stack smash corrupting heap metadata
The Interview Questions They’ll Ask
- “You get a core dump from production. Walk me through your debugging approach.”
- Load with GDB
- Check backtrace
- Inspect crash site variables
- Look at registers and memory
- Correlate with code
- “How do you determine if a pointer is valid?”
- Check if address is in valid memory range
- Check if it points to heap or stack
- Try to dereference and see if accessible
- “What does it mean if RIP (instruction pointer) points to a garbage address?”
- Likely stack smash or return address corruption
- Function will return to random address
- “How would you debug a NULL pointer dereference?”
- Check
backtraceto see calling context - Look at the code at crash line
- Print the pointer variable
- Trace backwards to find where it became NULL
- Check
- “Can you get a core dump without
ulimit -c unlimited?”- On some systems, yes (macOS, newer Linux)
- But it’s not guaranteed; best to set it explicitly
Hints in Layers
Hint Layer 1: Generating a Core Dump
- Compile with:
gcc -g -o crash crash.c - Enable core dumps:
ulimit -c unlimited - Run program:
./crash - Look for core file:
ls -la core
Hint Layer 2: Loading Core Dump
- Load into GDB:
gdb ./crash core - Check signal: top of GDB output shows “SIGSEGV”
- Get backtrace:
backtrace - Switch to frame:
frame 0
Hint Layer 3: Crash Site Inspection
- Print the faulting pointer:
print p - Check its value: Is it NULL? Garbage?
- Look at locals:
info locals - Examine memory:
x/10x $rsp
Hint Layer 4: Deep Analysis
- Disassemble the crashing function:
disassemble crash_me - Look at the instruction at RIP
- Check all registers:
info registers - Search backtrace for unexpected functions
- Check if stack looks corrupted
Books That Will Help
| Topic | Book | Chapter | Focus |
|---|---|---|---|
| Memory & Pointers | “The C Programming Language” | Chapter 5 | Pointer basics and dereferencing |
| Core Dumps | “The Linux Programming Interface” | Chapter 23 | Core dump generation and analysis |
| Signals | “The Linux Programming Interface” | Chapter 20 | Signal handling and SIGSEGV |
| GDB Post-Mortem | “The Art of Debugging with GDB” | Chapters 4-5 | Core dump debugging techniques |
| Stack Layout | “Computer Systems” (CSAPP) | Chapter 3 | Memory layout and stack structure |
Project 3: The Hang - Attaching to a Running Process
- File: LEARN_GDB_DEEP_DIVE.md
- Main Programming Language: GDB Commands (on a C target)
- Alternative Programming Languages: N/A
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 1. The “Resume Gold”
- Difficulty: Level 2: Intermediate
- Knowledge Area: Dynamic Analysis / Process Control
- Software or Tool: GDB, ps
- Main Book: “The Linux Programming Interface” by Michael Kerrisk
What you’ll build: A program with an infinite loop. You’ll then attach GDB to the already-running process, pause it, inspect its state, and detach without killing it.
Why it teaches GDB: This simulates debugging a production server or long-running service that has become unresponsive. You can’t just restart it; you need to attach to it live, figure out what it’s doing, and get out cleanly.
Core challenges you’ll face:
- Finding the Process ID (PID) → maps to using
ps aux | grep <name> - Attaching GDB → maps to
gdb -p <PID> - Interrupting execution → maps to using Ctrl-C inside GDB
- Detaching cleanly → maps to the
detachcommand
Key Concepts:
- Process IDs: “How Linux Works” Chapter 4
- Attaching and Detaching:
help attach,help detachin GDB.
Difficulty: Intermediate
Time estimate: 1 hour
Prerequisites: Basic Linux command-line knowledge (ps).
Real world outcome:
Compile gcc -g -o hang hang.c, run it in one terminal, and debug it from another.
// hang.c
#include <stdio.h>
#include <unistd.h>
int main() {
int counter = 0;
while (1) {
printf("Looping... counter = %d\n", counter++);
sleep(1);
}
return 0;
}
Debugging Session:
# Terminal 1: Run the program
$ ./hang
Looping... counter = 0
Looping... counter = 1
...
# Terminal 2: Find the PID and attach GDB
$ ps aux | grep ./hang
user 12345 ... ./hang # The PID is 12345
$ gdb -p 12345
(gdb) # Program is now paused. Let's see where it is.
(gdb) backtrace
#0 __GI___nanosleep (remaining=0x0, requested_time=0x7ffc...) at ...
#1 __sleep (seconds=0) at ../sysdeps/posix/sleep.c:113
#2 0x000055555555518d in main () at hang.c:9
(gdb) # It's in the sleep() call. Let's step out of it.
(gdb) finish
Run till exit from #0 __GI___nanosleep ...
main () at hang.c:10
10 }
(gdb) print counter
$1 = 2
(gdb) # Okay, we're done. Let the program continue.
(gdb) detach
Detaching from program: ./hang, process 12345
[Inferior 1 (process 12345) detached]
# Back in Terminal 1, the program resumes running
Looping... counter = 2
Looping... counter = 3
...
Learning milestones:
- Successfully attach to a running process → You can debug live systems.
- Interrupt and inspect the program → You can find out what a “stuck” program is doing.
- Modify a variable in a live process →
set var counter = 100. - Detach without killing the process → You can perform non-destructive inspection.
Real World Outcome
Terminal 1: Start the hanging program
$ gcc -g -o hang hang.c
$ ./hang
Looping... counter = 0
Looping... counter = 1
Looping... counter = 2
Looping... counter = 3
[program continues...]
Terminal 2: Attach GDB and debug
$ ps aux | grep hang
user 23456 0.0 0.1 2024 512 pts/0 S 10:15 0:00 ./hang
user 23457 0.0 0.1 1824 236 pts/1 S+ 10:15 0:00 grep hang
$ gdb -p 23456
GNU gdb (GDB) 14.1
Attaching to process 23456
Reading symbols from ./hang...
Reading symbols from /lib64/libc.so.6...
(no debugging symbols found)
Reading symbols from /lib64/ld-linux-x86-64.so.2...
(no debugging symbols found)
0x00007ffff7ec4250 in __nanosleep () from /lib64/libc.so.6
(gdb) # Program is now stopped. Let's see the call stack.
(gdb) backtrace
#0 0x00007ffff7ec4250 in __nanosleep () from /lib64/libc.so.6
#1 0x00007ffff7ec3a69 in sleep () from /lib64/libc.so.6
#2 0x0000555555555189 in main () at hang.c:8
(gdb) # Perfect! It's sleeping in the sleep(1) call.
(gdb) # Let's look at the frame with source code
(gdb) frame 2
#2 0x0000555555555189 in main () at hang.c:8
8 sleep(1);
(gdb)
(gdb) # Let's examine the variables at this frame
(gdb) info locals
counter = 6
(gdb) # Let's print the counter variable
(gdb) print counter
$1 = 6
(gdb) # Let's modify it while the process is paused
(gdb) set var counter = 100
(gdb) print counter
$2 = 100
(gdb) # Now let's continue the program
(gdb) continue
Continuing.
Back in Terminal 1: Program continues with modified state
[continues from counter = 6...]
Looping... counter = 100
Looping... counter = 101
Looping... counter = 102
[continues normally...]
Complete GDB Attach Session:
$ gdb -p 23456
GNU gdb (GDB) 14.1
Attaching to process 23456
[symbols loading...]
0x00007ffff7ec4250 in __nanosleep () from /lib64/libc.so.6
(gdb) # First, let's understand what we're dealing with
(gdb) info program
Using the running image of child process 23456.
Program stopped at 0x7ffff7ec4250.
(gdb) # Get full backtrace
(gdb) backtrace full
#0 0x00007ffff7ec4250 in __nanosleep () from /lib64/libc.so.6
No symbol table info available.
#1 0x00007ffff7ec3a69 in sleep () from /lib64/libc.so.6
No symbol table info available.
#2 0x0000555555555189 in main () at hang.c:8
counter = 6
(gdb) # Switch to main frame
(gdb) frame 2
#2 0x0000555555555189 in main () at hang.c:8
8 sleep(1);
(gdb) # Let's see the source around this point
(gdb) list
1 #include <stdio.h>
2 #include <unistd.h>
3
4 int main() {
5 int counter = 0;
6 while (1) {
7 printf("Looping... counter = %d\n", counter++);
8 sleep(1);
9 }
10 return 0;
11 }
(gdb) # Now let's inspect more deeply
(gdb) info locals
counter = 6
(gdb) # Let's see the registers
(gdb) info registers
rax 0xfffffff -1
rbx 0x0 0
rcx 0x1 1
rdx 0x0 0
rsi 0x0 0
rdi 0x1 1 (the sleep argument)
rbp 0x7fffffffdf20 0x7fffffffdf20
rsp 0x7fffffffdf00 0x7fffffffdf00
r8 0x7ffff7ec4a80 140737351984768
r9 0xffffffffffffffff -1
r10 0x0 0
r11 0x202 514
r12 0x555555555070 93824992235632
r13 0x0 0
r14 0x0 0
r15 0x0 0
rip 0x7ffff7ec4250 0x7ffff7ec4250 <__nanosleep+16>
(gdb) # Modify the counter
(gdb) set var counter = 999
(gdb) print counter
$1 = 999
(gdb) # Let's verify the modification worked
(gdb) x/w &counter
0x7fffffffdf2c: 0x000003e7 (0x3e7 = 999 in hex)
(gdb) # Now continue and finish the sleep
(gdb) finish
Run till exit from #0 0x7ffff7ec4250 in __nanosleep ()
0x00007ffff7ec3a69 in sleep () from /lib64/libc.so.6
(gdb) finish
Run till exit from #0 0x00007ffff7ec3a69 in sleep ()
main () at hang.c:9
9 }
(gdb) # We're back at the loop condition
(gdb) continue
Continuing.
[program now prints "Looping... counter = 999" and continues]
(gdb) # Interrupt it again to check progress
(gdb) ^C
Program received signal SIGINT, Interrupt.
0x00007ffff7ec4250 in __nanosleep () from /lib64/libc.so.6
(gdb) frame 2
#2 0x0000555555555189 in main () at hang.c:9
9 }
(gdb) print counter
$2 = 1003 (incremented 4 times since we set it to 999)
(gdb) # Everything is working perfectly. Let's detach.
(gdb) detach
Detaching from program: ./hang, process 23456
[Inferior 1 (process 23456) detached]
(gdb) quit
Back in Terminal 1:
Looping... counter = 999
Looping... counter = 1000
Looping... counter = 1001
Looping... counter = 1002
Looping... counter = 1003
[continues running normally]
Key Observations:
- Non-destructive debugging - Process continues after detach
- Live state modification - Changed counter while paused
- ptrace system call - All operations via kernel
- Permission requirements - Must own process or be root
- Symbol resolution - Works even with partial debug info
Common Troubleshooting:
| Problem | Cause | Solution |
|---|---|---|
| “Could not attach to process” | Wrong PID or no permission | Check ps aux for correct PID; may need sudo |
| “Cannot access local variables” | Optimized out | Recompile with -O0 |
| “Process won’t respond to Ctrl-C” | Stuck in uninterruptible syscall | Detach and investigate differently |
| “Symbols not found” | Binary stripped | Recompile with -g flag |
| “Inferior 1 (process X) exited” | Process died while attached | Check if process should still be running |
The Core Question You’re Answering
“How do I debug a live production service without shutting it down?”
This is the most critical debugging skill for ops and production engineers. You need to inspect and potentially modify a running system without interrupting service.
Concepts You Must Understand First
1. Process IDs (PIDs)
- Unique identifier per process
- Found via
ps aux,pgrep, or system calls - Valid only for running processes
2. ptrace(2) and Process Control
- GDB uses
ptrace()to attach to processes - Requires same user or root privilege
- Can read/write memory, registers, single-step
- Can detach without killing the process
3. Signal Handling During Attachment
- Process is stopped when GDB attaches
- Signals can be forwarded to process
- Ctrl-C in GDB sends SIGINT to target
4. Permissions and Security
- Linux: only process owner (or root) can attach
- macOS: similar restrictions apply
- Some systems restrict ptrace for security
5. Detaching vs Killing
detach: Stop controlling process, let it continuequitalone: May kill the process- Always use
detachfor non-destructive inspection
Questions to Guide Your Design
- How do you find the PID of a process?
ps aux | grep <name>orpgrep <name> - What does
gdb -p <PID>do? Uses ptrace to attach to existing process - Can you detach from a process without killing it? Yes, with
detachcommand - What permissions are needed to attach? Must be process owner or root
- What happens to breakpoints when you detach? They’re removed; process continues normally
Thinking Exercise
Imagine you attach to a web server. It’s stuck somewhere. You check backtrace and see it’s in an infinite loop in process_request(). The loop variable count is at 1,000,000. What would you do?
Possible approaches:
- Set
count = 0to break the loop - Skip to next iteration with
finish - Set a conditional breakpoint to stop at certain
countvalue - Detach and check logs to understand what request caused the hang
The Interview Questions They’ll Ask
- “You attach to a running process with GDB. What are the security implications?”
- Can read any memory the process can access
- Can steal credentials, encryption keys
- Therefore: only attach to processes you own or as root
- “How would you debug a production web server without downtime?”
- Attach with GDB
- Inspect state with
backtrace,print, etc. - Potentially modify variables
- Detach cleanly with
detach - Process continues serving
- “What if you accidentally run
quitinstead ofdetach?”- Depends on GDB version and settings
- Older GDB: quits and kills the process
- Modern GDB: may prompt; always use
detachto be safe
- “How do you find which process is consuming all CPU?”
ps aux --sort=-%cpu | headshows CPU hogs- Attach to top consumer with
gdb -p <PID> backtraceshows what it’s doing
- “Can you attach to a process running as a different user?”
- No, only as same user or root
- Exception: root can attach to any process
Hints in Layers
Hint Layer 1: Finding and Attaching
- Find process:
ps aux | grep hang - Get PID from first column
- Attach:
gdb -p <PID> - Wait for GDB to load symbols
Hint Layer 2: Inspection
backtraceshows where it’s stuckframe Nswitches to frame Nlistshows source codeinfo localsshows variablesprint <var>shows value
Hint Layer 3: Modification
set var counter = 100changes variablefinishexits current functioncontinueresumes executionnextexecutes one line- Ctrl-C interrupts again
Hint Layer 4: Advanced Control
break <line>sets breakpoint in live processwatch <var>watches for changescall <function>()calls function in targetdetachstops control but keeps process alivestepenters function calls
Books That Will Help
| Topic | Book | Chapter | Focus |
|---|---|---|---|
| Process IDs | “The Linux Programming Interface” | Chapter 5 | Process management |
| ptrace System Call | “The Linux Programming Interface” | Chapter 19 | Process tracing |
| Signals | “The Linux Programming Interface” | Chapter 20 | Signal handling in debugger |
| GDB Attach | “The Art of Debugging with GDB” | Chapters 2-3 | Attaching techniques |
| Unix Processes | “Advanced Programming in UNIX” | Chapter 3 | Process concepts |
Project 4: The Corruption - Using Watchpoints
- File: LEARN_GDB_DEEP_DIVE.md
- Main Programming Language: GDB Commands (on a C target)
- Alternative Programming Languages: N/A
- Coolness Level: Level 4: Hardcore Tech Flex
- Business Potential: 1. The “Resume Gold”
- Difficulty: Level 2: Intermediate
- Knowledge Area: Advanced Debugging / Memory Analysis
- Software or Tool: GDB
- Main Book: “The Art of Debugging with GDB” by Matloff & Salzman
What you’ll build: A C program where a variable is mysteriously overwritten by a buggy function. Instead of stepping through the whole program, you’ll use a “watchpoint” to make GDB stop at the exact moment the variable’s value changes.
Why it teaches GDB: This is one of GDB’s most powerful features. For “what the heck changed my variable?” bugs, watchpoints are infinitely faster than manual searching. It feels like magic.
Core challenges you’ll face:
- Identifying the symptom → maps to noticing a variable has the wrong value
- Setting a watchpoint → maps to the
watchcommand - Running to the trigger point → maps to
continueand letting the watchpoint stop execution - Analyzing the context → maps to using
backtraceto see the culprit
Key Concepts:
- Watchpoints:
help watchin GDB. - Hardware vs. Software Watchpoints: GDB documentation. Hardware watchpoints are much faster but limited in number.
Difficulty: Intermediate Time estimate: 2 hours Prerequisites: Project 1.
Real world outcome:
Compile and debug gcc -g -o corrupt corrupt.c.
// corrupt.c
#include <stdio.h>
int global_value = 100;
void buggy_function() {
int *p = &global_value;
// Oops, off-by-one pointer arithmetic
*(p + 1) = 0;
}
int main() {
int local_value = 200;
printf("Before: global=%d, local=%d\n", global_value, local_value);
buggy_function();
printf("After: global=%d, local=%d\n", global_value, local_value);
// Why is local_value 0?
return 0;
}
Debugging Session:
$ gdb ./corrupt
(gdb) break 17 # Break before the second printf
(gdb) run
(gdb) # We are stopped. Let's check the values.
(gdb) print local_value
$1 = 0 # That's wrong! It should be 200.
(gdb) # Let's restart and watch that variable.
(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y
(gdb) # Set a watchpoint *before* the corruption happens.
(gdb) watch local_value
Hardware watchpoint 1: local_value
(gdb) continue
Continuing.
Hardware watchpoint 1: local_value
Old value = 200
New value = 0
buggy_function () at corrupt.c:8
8 *(p + 1) = 0;
(gdb) # GDB stopped on the exact line that changed the value!
(gdb) backtrace
#0 buggy_function () at corrupt.c:8
#1 0x00005555555551b8 in main () at corrupt.c:15
Learning milestones:
- Set a watchpoint on a variable → You can monitor memory for changes.
- Let GDB find the exact line of corruption → You’ve automated the search for memory bugs.
- Use
rwatchandawatch→ You can break on read-access and read/write-access, respectively.
Real World Outcome
This is a real GDB session debugging a memory corruption bug in a production-like scenario. You compile with debugging symbols and run through the entire process:
$ gcc -g -O0 -o corrupt corrupt.c
$ gdb ./corrupt
Full Debugging Session:
(gdb) run
Starting program: ./corrupt
Before: global=100, local=200
[Inferior 1 (process 12875) exited normally]
(gdb) # Program finished but we didn't catch the corruption. Let's restart and set the watchpoint first.
(gdb) break main
Breakpoint 1 at 0x401125: file corrupt.c, line 14.
(gdb) run
Starting program: ./corrupt
Breakpoint 1, main () at corrupt.c:14
14 int local_value = 200;
(gdb) # Now set the watchpoint on local_value
(gdb) watch local_value
Hardware watchpoint 2: local_value
(gdb) # Examine memory layout to understand what's happening
(gdb) info locals
local_value = 0
(gdb) # Let's continue and let the watchpoint catch the change
(gdb) continue
Continuing.
Hardware watchpoint 2: local_value
Old value = 200
New value = 0
buggy_function () at corrupt.c:8
8 *(p + 1) = 0;
(gdb) # Perfect! GDB stopped on the exact line causing the corruption.
(gdb) # Let's examine the context
(gdb) print p
$1 = (int *) 0x7fffffffdf2c
(gdb) # Print the address of global_value
(gdb) print &global_value
$2 = (int *) 0x7fffffffdf28
(gdb) # Print the address of local_value
(gdb) print &local_value
$3 = (int *) 0x7fffffffdf2c
(gdb) # So p points to global_value, and (p + 1) points to local_value!
(gdb) # The bug is that pointer arithmetic is corrupting the adjacent variable.
(gdb) backtrace
#0 buggy_function () at corrupt.c:8
#1 0x0000555555555150 in main () at corrupt.c:15
(gdb) # Let's look at the memory dump to see the corruption
(gdb) x/4w &global_value
0x7fffffffdf28: 0x00000064 0x00000000 0x000000c8 0xf7dd0000
(global=100) (corrupted!) (local=200) (...)
(gdb) # Before corruption, let's restart and watch the write
(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Breakpoint 1, main () at corrupt.c:14
14 int local_value = 200;
(gdb) # Set a write watchpoint this time
(gdb) watch -l local_value
Hardware watchpoint 2: -l local_value
(gdb) # Also, let's look at hardware watchpoint capabilities
(gdb) info breakpoints
Num Type Disp Enb Address What
1 breakpoint keep y 0x0000555555555125 in main at corrupt.c:14
2 hw watchpoint keep y -l local_value
(gdb) continue
Continuing.
Hardware watchpoint 2: -l local_value
Old value = 200
New value = 0
buggy_function () at corrupt.c:8
8 *(p + 1) = 0;
(gdb) # Excellent! Hardware watchpoints are very fast. Now let's see the exact instruction
(gdb) disassemble
Dump of assembler code for function buggy_function:
0x0000555555555129 <+0>: push %rbp
0x000055555555512a <+1>: mov %rsp,%rbp
0x000055555555512d <+4>: lea 0x2ecd(%rip),%rax # 0x404000 <global_value>
0x0000555555555134 <+11>: mov %rax,-0x8(%rbp)
0x0000555555555138 <+15>: mov -0x8(%rbp),%rax
0x000055555555513c <+19>: movl $0x0,0x4(%rax) # THIS IS THE BUG!
0x0000555555555143 <+26>: nop
0x0000555555555144 <+27>: pop %rbp
0x0000555555555145 <+28>: ret
End of dump.
(gdb) # The instruction `movl $0x0,0x4(%rax)` writes 0 to 4 bytes at (rax + 4)
(gdb) # rax points to global_value, so (rax + 4) = next 4 bytes = local_value!
(gdb) # Now let's examine the values at the point of corruption
(gdb) info registers rax
rax 0x7fffffffdf28 140737488343848
(gdb) # That matches the address of global_value. The damage is done: local_value is 0.
(gdb) continue
Continuing.
After: global=100, local=0
[Inferior 1 (process 12875) exited normally]
(gdb) quit
Memory Layout Diagram (at the point of corruption):
Stack (growing downward):
┌─────────────────┐
│ (higher addr) │
├─────────────────┤
│ return address │ (caller's return address)
├─────────────────┤
│ p = 0x...df28 │ (local var in buggy_function)
├─────────────────┤
│ global_value │ 0x7fffffffdf28 ← p points here
│ = 100 │
├─────────────────┤
│ local_value │ 0x7fffffffdf2c ← p+1 points here
│ = 200 (→ 0) │ BUG: *(p+1) = 0 corrupts this!
├─────────────────┤
│ (lower addr) │
└─────────────────┘
The Core Question You’re Answering
“How do I find which line of code is modifying a variable I’m not expecting to change?”
This is the quintessential memory corruption debugging problem. You don’t know what is changing the variable (could be a typo, pointer math error, or integer overflow), but you know when it happens (when the variable’s value changes). Watchpoints let you automate the search.
Concepts You Must Understand First
- Pointer Arithmetic: Understanding that
p + 1moves the pointer bysizeof(*p)bytes, not just 1 byte. - Stack Layout: Variables are stored adjacently on the stack. A pointer mistake can corrupt a neighbor.
- Hardware vs. Software Watchpoints:
- Hardware: Uses CPU debug registers (limited, but very fast). Set with
watch. - Software: Uses code instrumentation (slower, unlimited). Automatically used if hardware unavailable.
- Hardware: Uses CPU debug registers (limited, but very fast). Set with
- Memory Protection: Without watchpoints, you’d have to manually step through and print the variable after each statement—tedious and error-prone.
Questions to Guide Your Design
- Which variable should I watch? → The one with the wrong value.
- When should I set the watchpoint? → After the variable is initialized but before it goes wrong.
- Does my CPU support hardware watchpoints? → Try
info watchpointsto see the type. - What if the watchpoint doesn’t trigger? → The variable might not be in memory (register allocation), or the corruption happens outside GDB’s visibility (e.g., in a signal handler).
Thinking Exercise
Imagine you have a multi-threaded program, and a global counter is being decremented by a thread you don’t recognize. How would you extend the watchpoint approach?
Answer: Use a conditional watchpoint:
watch counter if thread_id != expected_thread
This way, GDB only stops when a different thread modifies the counter.
The Interview Questions They’ll Ask
- “How would you debug a memory leak in a large program?” → Use watchpoints on allocation counters or track memory usage over time with Python scripting.
- “What’s the difference between
watchandrwatch?” →watchbreaks on write (change).rwatchbreaks on read. Both are useful depending on the bug. - “Can you set a watchpoint on a member of a struct?” → Yes:
watch my_struct.field(as long as the field is in memory). - “What happens if a watchpoint is too slow?” → Use conditional breakpoints or sample-based approaches instead.
Hints in Layers
Hint 1: Basic Watchpoint Setup
The simplest approach:
(gdb) break main
(gdb) run
(gdb) print &suspicious_var
$1 = 0x7fffffffdf2c
(gdb) watch suspicious_var
Hardware watchpoint 1: suspicious_var
(gdb) continue # GDB will stop when the variable changes
Hint 2: Advanced Watchpoint Conditions
If you want to catch changes within a certain range or under specific conditions:
(gdb) watch my_var if (my_var > 100) # Stop only if value exceeds 100
(gdb) rwatch my_var # Break on read-access (to detect peeping)
(gdb) awatch my_var # Break on any access (read or write)
Hint 3: Inspecting Nearby Memory
When a watchpoint fires, immediately check the surrounding memory to understand the layout:
(gdb) x/8w &suspicious_var - 16 # Dump 8 words starting 4 ints before the variable
(gdb) info locals # List all local variables and their addresses
Hint 4: Hardware Watchpoint Limitations
If you hit “Resource temporarily unavailable,” you’ve exceeded the CPU’s debug register limit (usually 4):
(gdb) info watchpoints # See all current watchpoints
Num Type Disp Enb Address What
2 hw watchpoint keep y my_var1
3 hw watchpoint keep y my_var2
4 hw watchpoint keep y my_var3
5 hw watchpoint keep y my_var4
6 hw watchpoint keep y my_var5 ← This one may fail!
(gdb) delete 2 # Free up a slot
(gdb) watch my_var5 # Try again
Hint 5: Automatic Analysis with Python
Write a GDB Python script to monitor multiple variables:
import gdb
class SmartWatchpoint(gdb.Breakpoint):
def __init__(self, var_name):
super(SmartWatchpoint, self).__init__(f"file.c:{var_name}", gdb.BP_WATCHPOINT)
self.var_name = var_name
self.watch(var_name)
def stop(self):
print(f"[WATCHPOINT] {self.var_name} modified")
frame = gdb.selected_frame()
print(f" Location: {frame.name()} at {frame.find_sal().symtab.filename}:{frame.find_sal().line}")
return True
Books That Will Help
| Book | Chapter | Key Topic | Why It Helps |
|---|---|---|---|
| “The Art of Debugging with GDB” | Chapter 4 | “Watching Variables and Memory” | Comprehensive coverage of watchpoint types and strategies |
| “The Linux Programming Interface” | Chapter 20 | “Signals: Fundamental Concepts” | Explains how watchpoints work via CPU interrupts and signals |
| “Computer Systems: A Programmer’s Perspective” | Chapter 12 | “Concurrent Programming” | How multi-threaded programs interact with watchpoints |
| GDB Manual | “Breakpoints, Watchpoints, and Catchpoints” | Detailed GDB watchpoint documentation | The definitive reference for all watchpoint options |
| “Debugging with GDB” (Official Manual) | Chapter 5 | “Stopping and Continuing” | All stopping mechanisms, including advanced watchpoint features |
Project 5: The Assembly Level - disassemble and stepi
- File: LEARN_GDB_DEEP_DIVE.md
- Main Programming Language: GDB Commands / x86 Assembly
- Alternative Programming Languages: N/A
- Coolness Level: Level 4: Hardcore Tech Flex
- Business Potential: 1. The “Resume Gold”
- Difficulty: Level 3: Advanced
- Knowledge Area: Low-level Debugging / Reverse Engineering
- Software or Tool: GDB
- Main Book: “Computer Systems: A Programmer’s Perspective” by Bryant & O’Hallaron
What you’ll build: A simple C program which you will compile with optimizations (-O2) and debug at the assembly-instruction level.
Why it teaches GDB: Sometimes the source code lies. Optimizations can reorder operations, eliminate variables, and make line-by-line debugging misleading. Dropping down to the assembly level shows you the ground truth of what your program is actually doing.
Core challenges you’ll face:
- Viewing disassembly → maps to the
disassemblecommand - Correlating source to assembly → maps to GDB’s TUI or
layout asm - Stepping instruction-by-instruction → maps to
stepiandnexti - Inspecting CPU registers → maps to
info registersorlayout regs
Key Concepts:
- x86 Assembly: “Computer Systems: A Programmer’s Perspective” Chapter 3
- GDB TUI (Text User Interface): Press
Ctrl-X Ain GDB. - CPU Registers:
rip(instruction pointer),rsp(stack pointer),rax(return value).
Difficulty: Advanced Time estimate: 3-4 hours Prerequisites: Basic understanding of what assembly and registers are.
Real world outcome:
Compile gcc -g -O2 -o optimized optimized.c and debug it.
// optimized.c
int calculate(int a, int b) {
int result = (a + b) * 2;
return result;
}
int main() {
calculate(10, 20);
return 0;
}
Debugging Session:
$ gdb ./optimized
(gdb) break main
(gdb) run
(gdb) layout asm # Show disassembly
(gdb) layout regs # Show registers
(gdb) # Step through assembly instructions
(gdb) nexti
... (GDB output showing assembly instructions)
(gdb) nexti
0x55555555513d <main+4> mov $0x14,%esi # Move 20 into esi (arg 2)
(gdb) nexti
0x555555555142 <main+9> mov $0xa,%edi # Move 10 into edi (arg 1)
(gdb) nexti
0x555555555147 <main+14> call 0x555555555129 <calculate>
(gdb) # Let's look at the calculate function
(gdb) disassemble calculate
Dump of assembler code for function calculate:
0x0000555555555129 <+0>: lea (%rdi,%rsi,1),%eax # eax = rdi + rsi (a + b)
0x000055555555512c <+3>: add %eax,%eax # eax = eax + eax (result * 2)
0x000055555555512e <+5>: ret
End of dump.
# The optimizer got rid of the 'result' variable entirely!
(gdb) # After the call, the return value is in RAX
(gdb) nexti
(gdb) info registers rax
rax 0x3c 60 # The correct result, 30 * 2
Learning milestones:
- View the disassembly of a function → You can see the machine code.
- Use
stepito execute a single instruction → You have fine-grained control. - Inspect registers → You can see data outside of C variables.
- Understand how optimizations change your code → You trust the assembly, not the source.
Real World Outcome
This is a complete GDB session debugging optimized code at the assembly level. You compile with -O2 optimization and step through the assembly instruction by instruction:
$ gcc -g -O2 -o optimized optimized.c
$ gdb ./optimized
Full Debugging Session with Assembly:
(gdb) break main
Breakpoint 1 at 0x401100: file optimized.c, line 9.
(gdb) run
Starting program: ./optimized
Breakpoint 1, main () at optimized.c:9
9 calculate(10, 20);
(gdb) # The source says calculate(10, 20), but let's see the assembly.
(gdb) disassemble main
Dump of assembler code for function main:
0x0000555555555105 <+0>: push %rbx
0x0000555555555106 <+1>: sub $0x18,%rsp
0x000055555555510a <+5>: mov $0xa,%edi # edi = 10 (first arg)
0x000055555555510f <+10>: mov $0x14,%esi # esi = 20 (second arg)
0x0000555555555114 <+15>: call 0x555555555100 <calculate>
0x0000555555555119 <+20>: add $0x18,%rsp
0x000055555555511d <+24>: xor %eax,%eax # Return value = 0
0x000055555555511f <+26>: pop %rbx
0x0000555555555120 <+27>: ret
End of dump.
(gdb) # Good! The optimizer loaded constants directly into registers for speed.
(gdb) # Let's step to the call and examine the state.
(gdb) layout asm # Enter TUI mode to show assembly as we step
(gdb) layout regs # Also show registers in the TUI
(gdb) # The display now shows assembly code and registers. Step through:
(gdb) nexti # Execute the "push %rbx" instruction
0x0000555555555106 <+1>: sub $0x18,%rsp
(gdb) info registers rsp
rsp 0x7fffffffdf00 140737488343808
(gdb) nexti # Execute "sub $0x18,%rsp" (allocate stack frame)
0x000055555555510a <+5>: mov $0xa,%edi
(gdb) info registers rsp
rsp 0x7ffffffff0e8 140737488343784
(gdb) nexti # Execute "mov $0xa,%edi" (load first argument)
0x000055555555510f <+10>: mov $0x14,%esi
(gdb) info registers rdi
rdi 0xa 10
(gdb) nexti # Execute "mov $0x14,%esi" (load second argument)
0x0000555555555114 <+15>: call 0x555555555100 <calculate>
(gdb) info registers rsi rdi
rsi 0x14 20
rdi 0xa 10
(gdb) # Now let's step INTO the calculate function at the assembly level.
(gdb) stepi # Execute the "call" instruction
0x0000555555555100 <+0>: lea (%rdi,%rsi,1),%eax
(gdb) # We're now inside calculate! The optimizer folded it completely.
(gdb) disassemble calculate
Dump of assembler code for function calculate:
0x0000555555555100 <+0>: lea (%rdi,%rsi,1),%eax # eax = rdi + rsi (a + b)
0x0000555555555103 <+3>: add %eax,%eax # eax = eax + eax (double it)
0x0000555555555105 <+5>: ret
End of dump.
(gdb) # The optimizer:
(gdb) # 1. Eliminated the 'result' variable
(gdb) # 2. Combined (a+b)*2 into two instructions
(gdb) # 3. Put the final result directly in RAX
(gdb) # Let's trace through calculate's computation:
(gdb) info registers rdi rsi rax
rdi 0xa 10
rsi 0x14 20
rax 0x0 0
(gdb) stepi # Execute "lea (%rdi,%rsi,1),%eax"
0x0000555555555103 <+3>: add %eax,%eax
(gdb) info registers rax
rax 0x1e 30 # rax = 10 + 20 = 30
(gdb) stepi # Execute "add %eax,%eax"
0x0000555555555105 <+5>: ret
(gdb) info registers rax
rax 0x3c 60 # rax = 30 + 30 = 60 (the final answer!)
(gdb) stepi # Execute the "ret" instruction
0x0000555555555119 <+20>: add $0x18,%rsp
(gdb) # We're back in main. The return value (60) is in RAX.
(gdb) info registers rax
rax 0x3c 60
(gdb) # Note: the return value is never used in C code, so the optimizer
(gdb) # could have omitted the calculation entirely! Let's verify by checking
(gdb) # if the program even uses the return value.
(gdb) disassemble main # Look again
0x0000555555555119 <+20>: add $0x18,%rsp # Clean up stack
0x000055555555511d <+24>: xor %eax,%eax # Clobber RAX (set to 0)
0x000055555555511f <+26>: pop %rbx
0x0000555555555120 <+27>: ret
(gdb) # Interesting! After the call returns, the optimizer clobbers RAX with xor.
(gdb) # This tells us the return value was never used by the C code.
(gdb) continue
[Inferior 1 (process 14289) exited normally]
(gdb) quit
Assembly Annotation (what each instruction does):
calculate assembly breakdown:
┌──────────────────────────────────────────────────────┐
│ lea (%rdi,%rsi,1),%eax │
│ Load Effective Address: eax = rdi + rsi*1 │
│ x86-64 calling convention: rdi=10, rsi=20 │
│ Result: eax = 10 + 20 = 30 │
├──────────────────────────────────────────────────────┤
│ add %eax,%eax │
│ Add eax to itself: eax = eax + eax │
│ Result: eax = 30 + 30 = 60 │
├──────────────────────────────────────────────────────┤
│ ret │
│ Return from function │
│ The return value (60) remains in eax │
└──────────────────────────────────────────────────────┘
Register State Evolution:
Instruction | RAX | RDI | RSI | Effect
─────────────────────────────────────────────────
[entry] | 0x0 | 0xa | 0x14 | Arguments loaded
lea (...) | 0x1e | 0xa | 0x14 | sum computed
add %eax... | 0x3c | 0xa | 0x14 | sum doubled
[return] | 0x3c | 0xa | 0x14 | result in rax
Key Insight: The optimizer eliminated the C variable result entirely. Without looking at the assembly, you’d be confused:
- The C code says
int result = (a + b) * 2; return result; - But the assembly has no memory location for
result—it exists only in theraxregister
The Core Question You’re Answering
“What is my code actually doing at the CPU level, and how does it differ from what the source code suggests?”
Optimizations can be aggressive: dead code elimination, variable elimination, instruction reordering, and loop unrolling. The source code is a suggestion to the compiler; the assembly is the truth.
Concepts You Must Understand First
- x86-64 Calling Convention:
- First argument:
rdi - Second argument:
rsi - Third argument:
rdx - Return value:
rax
- First argument:
- Common x86 Instructions:
lea: Load Effective Address (address calculation without memory access)mov: Move dataadd: Additioncall/ret: Function calls
- Optimization Levels:
-O0(no optimization),-O2(balanced),-O3(aggressive) - TUI Mode:
layout asm,layout regs,Ctrl-X Ain GDB
Questions to Guide Your Design
- What instruction is executing right now? → Use
info registers ripto find the instruction pointer. - Are my variables in registers or memory? → Use
info localsand thenxto dump memory. - How does the assembly differ from my source? → Compare line-by-line using
disassemble. - Why did the optimizer eliminate that variable? → Check if the variable’s value is ever read.
Thinking Exercise
You have a loop that the compiler optimized away. How would you force the compiler to keep it?
Answer: Mark the variable as volatile or force a side-effect (e.g., write to a pointer). Example:
int total = 0;
for (int i = 0; i < 1000000; ++i) {
total += i;
}
// The loop might be optimized to: total = 999999 * 1000000 / 2
// To prevent this: volatile int total = 0;
// Or: asm("" : "+r"(total)); (inline assembly barrier)
The Interview Questions They’ll Ask
- “How would you debug a race condition in multi-threaded code?” → Look at the assembly to see if operations are truly atomic, or if the compiler reordered them.
- “Can you explain why this optimization is unsafe?” → Show the assembly and point out memory access patterns.
- “What’s the difference between
pushandmovfor saving registers?” →pushis faster on some CPUs;movhas fewer dependencies. - “Why does
-O3sometimes make code slower?” → Show assembly that has worse cache behavior due to aggressive loop unrolling.
Hints in Layers
Hint 1: Setting Up TUI Mode
The best way to debug assembly is with TUI (Text User Interface):
(gdb) layout asm # Show assembly code
(gdb) layout regs # Add register window
(gdb) layout split # Show source + assembly side-by-side
(gdb) Ctrl-X A # Toggle TUI on/off
(gdb) Ctrl-X 2 # Split windows
Hint 2: Key Assembly Commands
(gdb) disassemble function_name # Show all assembly for a function
(gdb) disassemble /m main # Show mixed source + assembly
(gdb) stepi # Step one instruction
(gdb) nexti # Step over (not into) one instruction
(gdb) info registers rax rbx rcx # Show specific registers
(gdb) info registers # Show all registers
(gdb) x/10i $rip # Dump 10 instructions at RIP
Hint 3: Correlating Source to Assembly
Use the “mixed” disassembly to see source and assembly together:
(gdb) disassemble /m calculate
Dump of assembler code for function calculate:
5 int result = (a + b) * 2;
0x0000555555555100 <+0>: lea (%rdi,%rsi,1),%eax
0x0000555555555103 <+3>: add %eax,%eax
6 return result;
0x0000555555555105 <+5>: ret
End of dump.
This shows exactly which assembly instructions implement each C statement.
Hint 4: Understanding Register Allocation
After stepping through code, print registers to understand allocation:
(gdb) info registers # Shows all CPU registers
rax 0x0 0
rbx 0x0 0
rcx 0x0 0
rdx 0x0 0
rsi 0x14 20
rdi 0xa 10
...
Each register holds a value. The optimizer chose these to minimize memory accesses.
Hint 5: Detecting Instruction Reordering
Compare the source order to assembly order:
// Source
a = b + c; // Line 10
d = a * 2; // Line 11
e = f + g; // Line 12
If assembly shows instruction for Line 12 before Line 11, the compiler reordered for parallelism.
(gdb) disassemble /m my_function
10 a = b + c;
0x... mov (%rax),%r8d # Load b+c into r8d
12 e = f + g; # REORDERED UP (no data dependency on line 11!)
0x... mov (%rdx),%r9d # Load f+g into r9d
11 d = a * 2;
0x... shl %r8d # Double a
The compiler moved the independent operation up to hide memory latency.
Hint 6: Assembly Debugging Script
Automate assembly inspection with a GDB Python script:
import gdb
class AssemblyBreakdown(gdb.Command):
def __init__(self):
super(AssemblyBreakdown, self).__init__("asm_state", gdb.COMMAND_USER)
def invoke(self, args, from_tty):
# Print current instruction and registers
frame = gdb.selected_frame()
pc = frame.pc()
disasm = gdb.execute(f"disassemble /m {frame.name()}", to_string=True)
print(disasm)
regs = gdb.execute("info registers rax rdi rsi rdx", to_string=True)
print("\n[Current Registers]")
print(regs)
AssemblyBreakdown()
Load with source script.py and call with asm_state.
Books That Will Help
| Book | Chapter | Key Topic | Why It Helps |
|---|---|---|---|
| “Computer Systems: A Programmer’s Perspective” | Chapter 3 | “Machine-Level Representation of Programs” | Fundamental x86-64 assembly and calling conventions |
| “Computer Systems: A Programmer’s Perspective” | Chapter 5 | “Optimizing Program Performance” | How compilers optimize and reorder code |
| “The Art of Debugging with GDB” | Chapter 5 | “More Sophisticated Debugging Techniques” | GDB’s assembly debugging features |
| “Intel 64 and IA-32 Architectures Software Developer’s Manual” | Volume 1 | x86-64 instruction set | The authoritative reference for assembly instructions |
| GDB Manual | Chapter 10 | “Examining Source Files” | Mixed source/assembly display and disassembly commands |
Project 6: GDB Python Scripting
- File: LEARN_GDB_DEEP_DIVE.md
- Main Programming Language: Python (in GDB)
- Alternative Programming Languages: GDB’s own command language
- Coolness Level: Level 4: Hardcore Tech Flex
- Business Potential: 1. The “Resume Gold”
- Difficulty: Level 3: Advanced
- Knowledge Area: Debugging Automation
- Software or Tool: GDB with Python support
- Main Book: GDB Documentation (Chapter “Python API”)
What you’ll build: A Python script that runs inside GDB to automate a debugging task. For example, a script that automatically prints all string arguments every time printf is called.
Why it teaches GDB: Manual debugging is repetitive. Scripting is the path to mastery. It allows you to create your own powerful debugging commands and automate complex analysis that would be impossible by hand.
Core challenges you’ll face:
- Running a Python script → maps to the
sourcecommand in GDB - Accessing GDB from Python → maps to the
gdbPython module - Creating a new GDB command → maps to subclassing
gdb.Command - Scripting breakpoints → maps to subclassing
gdb.Breakpoint
Key Concepts:
- GDB Python API: Official GDB documentation is the best source.
- Extending GDB:
gdb.Command,gdb.Function,gdb.Breakpoint.
Difficulty: Advanced Time estimate: 1-2 weeks Prerequisites: Python knowledge, Project 1.
Real world outcome:
A Python script (myscripts.py) that you load into GDB to add a new command.
# myscripts.py
import gdb
class PrintfTracer(gdb.Breakpoint):
def __init__(self):
super(PrintfTracer, self).__init__("printf", gdb.BP_BREAKPOINT, internal=True)
def stop(self):
# x86-64 calling convention: first arg is in RDI
# Let's get the string at the address stored in RDI
string_addr = gdb.parse_and_eval("$rdi")
first_arg = gdb.selected_inferior().read_memory(string_addr, 64).tobytes().split(b'\x00')[0]
print(f"[printf called with: {first_arg.decode('utf-8', 'ignore')}]")
return False # Don't actually stop, just print and continue
class HelloCommand(gdb.Command):
def __init__(self):
super(HelloCommand, self).__init__("hello_gdb", gdb.COMMAND_USER)
def invoke(self, arg, from_tty):
print("Hello from your custom GDB command!")
HelloCommand()
PrintfTracer()
Debugging Session:
$ gdb ./my_program
(gdb) source myscripts.py # Load the script
(gdb) hello_gdb
Hello from your custom GDB command!
(gdb) run
[printf called with: Hello for the 0th time!]
Hello for the 0th time!
[printf called with: Hello for the 1st time!]
Hello for the 1st time!
...
Learning milestones:
- Create a custom GDB command → You can extend GDB’s vocabulary.
- Create a scripted breakpoint → You can run code automatically when a breakpoint is hit.
- Inspect memory and registers from Python → You can perform complex analysis.
- Build a useful tool → e.g., a “pretty printer” for one of your custom data structures.
Real World Outcome
This is a complete Python scripting session that extends GDB with custom commands and automated debugging. You create a Python script to trace printf calls and analyze their arguments:
Script: debug_printf_tracer.py
import gdb
import struct
class PrintfTracer(gdb.Breakpoint):
"""Automatically intercepts and logs printf calls with their arguments"""
def __init__(self):
super(PrintfTracer, self).__init__("printf", internal=True)
self.call_count = 0
def stop(self):
"""Called every time the breakpoint is hit"""
self.call_count += 1
# Get the current frame to inspect the call
frame = gdb.selected_frame()
# x86-64 calling convention: first arg (format string) is in RDI
format_addr = int(gdb.parse_and_eval("$rdi"))
# Read the format string from memory
try:
inferior = gdb.selected_inferior()
format_bytes = inferior.read_memory(format_addr, 256)
format_str = format_bytes.tobytes().split(b'\x00')[0].decode('utf-8', 'ignore')
except:
format_str = "[unable to read]"
# Extract additional arguments (rsi, rdx, rcx, r8, r9 for subsequent args)
args_regs = ["$rsi", "$rdx", "$rcx", "$r8", "$r9"]
args = []
for arg_reg in args_regs:
try:
arg_val = int(gdb.parse_and_eval(arg_reg))
args.append(f"0x{arg_val:x}")
except:
break
# Print the trace
print(f"[printf #{self.call_count}] Format: \"{format_str}\"")
if args:
print(f" Args: {', '.join(args)}")
print(f" Called from: {frame.name()} at {frame.find_sal().symtab.filename}:{frame.find_sal().line}")
# Don't actually stop execution—just log and continue
return False
class ListBreakpoints(gdb.Command):
"""Custom command to list all breakpoints with their status"""
def __init__(self):
super(ListBreakpoints, self).__init__("bp_list", gdb.COMMAND_USER)
def invoke(self, args, from_tty):
bps = gdb.breakpoints()
print(f"Total breakpoints: {len(bps)}")
for bp in bps:
print(f" #{bp.number}: {bp.location} (enabled: {bp.enabled})")
class DumpMemoryRegion(gdb.Command):
"""Dump a region of memory in hex format"""
def __init__(self):
super(DumpMemoryRegion, self).__init__("memdump", gdb.COMMAND_USER)
def invoke(self, args, from_tty):
"""
Usage: memdump ADDRESS SIZE
Example: memdump 0x7fffffffdf00 64
"""
parts = args.split()
if len(parts) < 2:
print("Usage: memdump ADDRESS SIZE")
return
address = int(parts[0], 16)
size = int(parts[1])
try:
inferior = gdb.selected_inferior()
memory = inferior.read_memory(address, size)
# Print in hex + ASCII format (like 'xxd')
for i in range(0, len(memory), 16):
chunk = memory[i:i+16]
hex_str = ' '.join(f'{b:02x}' for b in chunk)
ascii_str = ''.join(chr(b) if 32 <= b < 127 else '.' for b in chunk)
print(f"0x{address + i:08x}: {hex_str:<48} {ascii_str}")
except Exception as e:
print(f"Error reading memory: {e}")
class CallTracer(gdb.Breakpoint):
"""Trace function calls with arguments"""
def __init__(self, function_name):
super(CallTracer, self).__init__(function_name, internal=True)
self.function_name = function_name
self.call_stack = []
def stop(self):
frame = gdb.selected_frame()
# x86-64: first 6 arguments are in rdi, rsi, rdx, rcx, r8, r9
arg_regs = ["$rdi", "$rsi", "$rdx", "$rcx", "$r8", "$r9"]
arg_vals = []
for reg in arg_regs:
try:
val = gdb.parse_and_eval(reg)
arg_vals.append(str(val))
except:
break
indent = " " * len(self.call_stack)
print(f"{indent}→ {self.function_name}({', '.join(arg_vals)})")
self.call_stack.append(self.function_name)
return False # Don't stop, just log
# Register all commands and breakpoints
ListBreakpoints()
DumpMemoryRegion()
PrintfTracer() # Automatically trace all printf calls
Session with the Script:
$ gcc -g -O0 -o test_program test_program.c
$ gdb ./test_program
(gdb) source debug_printf_tracer.py
(gdb) # The script is now loaded. printf calls will be automatically traced.
(gdb) run
[printf #1] Format: "Hello for the %dth time!"
Args: 0x0
Called from: greet at test_program.c:5
[printf #2] Format: "Hello for the %dth time!"
Args: 0x1
Called from: greet at test_program.c:5
[Inferior 1 (process 14567) exited normally]
(gdb) # Let's try the custom memdump command:
(gdb) break main
(gdb) run
(gdb) # Allocate some data
(gdb) print &global_value
$1 = 0x404000 <global_value>
(gdb) memdump 0x404000 32
0x404000: 64 00 00 00 c8 00 00 00 00 00 00 00 00 00 00 00 d...........
0x404010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
More Advanced Example: Tracking struct modifications
import gdb
class StructFieldWatcher(gdb.Breakpoint):
"""Watch for changes to a struct field across all functions"""
def __init__(self, struct_name, field_name):
super(StructFieldWatcher, self).__init__(f"my_function", internal=True)
self.struct_name = struct_name
self.field_name = field_name
self.last_value = None
def stop(self):
try:
frame = gdb.selected_frame()
# Evaluate the struct.field expression in the current frame
expr = f"(({self.struct_name}*)$rdi)->{self.field_name}"
current_value = gdb.parse_and_eval(expr)
current_val_int = int(current_value)
if self.last_value is None or self.last_value != current_val_int:
print(f"[STRUCT] {self.struct_name}.{self.field_name} changed: {self.last_value} → {current_val_int}")
self.last_value = current_val_int
except Exception as e:
print(f"[ERROR] {e}")
return False
Pretty Printer Example: Make custom types display nicely
import gdb.printing
class LinkedListPrinter:
"""Pretty printer for a custom linked list struct"""
def __init__(self, val):
self.val = val
def to_string(self):
node = self.val
items = []
while node != 0: # Loop until NULL
# Read the data field
data = int(node['data'])
items.append(str(data))
# Move to next node
try:
node = node['next']
except:
break
return f"LinkedList({' -> '.join(items)})"
def display_hint(self):
return 'string'
def build_pretty_printers():
pp = gdb.printing.RegexObjectPrinter("MyProgram")
pp.add_printer('LinkedList', '^LinkedList$', LinkedListPrinter)
return pp
gdb.printing.register_pretty_printer(gdb.current_objfile(), build_pretty_printers())
The Core Question You’re Answering
“How do I automate repetitive debugging tasks and extend GDB with custom tools tailored to my codebase?”
Manual debugging is tedious. With Python scripting, you can:
- Automatically log specific function calls
- Create custom commands for your domain
- Build “pretty printers” for complex data structures
- Analyze memory patterns automatically
- Create breakpoints with complex conditions
Concepts You Must Understand First
- GDB’s Python API:
gdb.Breakpoint: Define breakpoints in Pythongdb.Command: Create custom GDB commandsgdb.selected_frame(): Get the current stack framegdb.parse_and_eval(): Evaluate expressionsgdb.selected_inferior(): Access the target process
-
x86-64 Calling Convention: Arguments are in
rdi,rsi,rdx,rcx,r8,r9. -
Memory Access: Use
inferior.read_memory(addr, size)to read raw memory. - Frame Inspection: Access function names, file names, line numbers from the frame.
Questions to Guide Your Design
- What information do I need to log? → Arguments, return values, memory state.
- How often will the breakpoint be hit? → Affects performance; use conditional breakpoints.
- Should I stop execution or just log? → Usually log without stopping (
return False). - How do I handle errors gracefully? → Wrap memory reads in try/except.
Thinking Exercise
You have a program that crashes randomly. You want to log all memory allocations and frees to find a use-after-free bug. How would you script this?
Answer: Create breakpoints at malloc and free, log the addresses and sizes, and maintain a set of allocated blocks. When a block is freed twice or used after free, alert the user.
import gdb
allocated_blocks = {}
class MallocBreakpoint(gdb.Breakpoint):
def stop(self):
addr = int(gdb.parse_and_eval("$rax")) # malloc returns in rax
size = int(gdb.parse_and_eval("$rdi")) # size is first arg
allocated_blocks[addr] = size
print(f"[MALLOC] 0x{addr:x} (size: {size})")
return False
class FreeBreakpoint(gdb.Breakpoint):
def stop(self):
addr = int(gdb.parse_and_eval("$rdi")) # free arg is in rdi
if addr in allocated_blocks:
del allocated_blocks[addr]
print(f"[FREE] 0x{addr:x}")
else:
print(f"[FREE] WARNING: Freeing unallocated block 0x{addr:x}")
return False
# Register the breakpoints
MallocBreakpoint("malloc")
FreeBreakpoint("free")
The Interview Questions They’ll Ask
- “How would you create a GDB command that finds all calls to a specific function?” → Use
gdb.Breakpointwith logging and frame inspection. - “Can you script a breakpoint that only stops under specific conditions?” → Yes, use conditional logic in the
stop()method. - “How would you extract and pretty-print a complex data structure?” → Create a pretty printer that walks the structure recursively.
- “What’s the performance impact of extensive Python scripting?” → Each
stop()call adds overhead. Useinternal=Trueand conditional logic to minimize impact.
Hints in Layers
Hint 1: Basic Custom Command
Here’s the minimum for a custom command:
import gdb
class HelloCommand(gdb.Command):
def __init__(self):
super(HelloCommand, self).__init__("hello", gdb.COMMAND_USER)
def invoke(self, args, from_tty):
print(f"Hello, {args}!")
HelloCommand()
Load with source script.py and call with hello World.
Hint 2: Accessing Registers and Memory
Common patterns for reading data:
import gdb
# Read a register value
rax = int(gdb.parse_and_eval("$rax"))
rdi = int(gdb.parse_and_eval("$rdi"))
# Read memory at an address
inferior = gdb.selected_inferior()
memory = inferior.read_memory(0x7fffffffdf00, 64) # Read 64 bytes
# Parse the bytes as integers
value = int.from_bytes(memory[:4], byteorder='little') # First 4 bytes as little-endian int
Hint 3: Conditional Breakpoints with State
Store state across breakpoint hits:
import gdb
class StatefulBreakpoint(gdb.Breakpoint):
def __init__(self, location):
super(StatefulBreakpoint, self).__init__(location, internal=True)
self.hit_count = 0
self.history = []
def stop(self):
self.hit_count += 1
# Store data from this hit
frame = gdb.selected_frame()
self.history.append({
'count': self.hit_count,
'function': frame.name(),
'line': frame.find_sal().line
})
# Only stop every 10th hit
return self.hit_count % 10 == 0
StatefulBreakpoint("printf")
Hint 4: Iterating Frames in the Call Stack
Walk the entire call stack programmatically:
import gdb
def print_stack_trace():
frame = gdb.selected_frame()
depth = 0
while frame:
print(f"#{depth} {frame.name()} at {frame.find_sal().symtab.filename}:{frame.find_sal().line}")
try:
frame = frame.older()
except:
break
depth += 1
# Call this from a breakpoint's stop() method
class StackPrinter(gdb.Breakpoint):
def stop(self):
print_stack_trace()
return False
StackPrinter("malloc")
Hint 5: Building a Memory Analyzer
Automatically analyze memory patterns:
import gdb
class MemoryAnalyzer(gdb.Command):
def __init__(self):
super(MemoryAnalyzer, self).__init__("analyze_mem", gdb.COMMAND_USER)
def invoke(self, args, from_tty):
# Find all global variables and their addresses
symtab_and_line = gdb.decode_line("*")
print("[Global Variables]")
for var in gdb.current_objfile().global_symbols():
if var.is_variable:
addr = var.value()
print(f" {var.name} @ {addr}")
MemoryAnalyzer()
Hint 6: Creating Custom Function Signatures
Define helper functions that wrap GDB operations:
import gdb
def read_string(addr):
"""Read a null-terminated string from memory"""
inferior = gdb.selected_inferior()
chars = []
for i in range(1024): # Limit to 1024 chars
try:
byte = inferior.read_memory(addr + i, 1)[0]
if byte == 0:
break
chars.append(chr(byte))
except:
break
return ''.join(chars)
def read_int(addr):
"""Read a 4-byte integer"""
inferior = gdb.selected_inferior()
data = inferior.read_memory(addr, 4)
return int.from_bytes(data, byteorder='little')
# Use these in breakpoints:
class PrintfTracer(gdb.Breakpoint):
def stop(self):
fmt_addr = int(gdb.parse_and_eval("$rdi"))
fmt_str = read_string(fmt_addr)
print(f"printf: {fmt_str}")
return False
PrintfTracer("printf")
Books That Will Help
| Book | Chapter | Key Topic | Why It Helps |
|---|---|---|---|
| GDB Manual | “Python API” | Python scripting in GDB | The definitive reference for GDB’s Python API |
| “The Art of Debugging with GDB” | Chapter 7 | “Debugging with Python” | Practical Python debugging patterns and examples |
| “Debugging with GDB” (Official Manual) | “Extending GDB” | GDB extensibility overview | Overview of Python API capabilities |
| Python Official Docs | “ctypes” | C data structures in Python | How to structure data when reading from memory |
| “The Linux Programming Interface” | Chapter 28 | “Process Execution” | Understanding the process being debugged |
Project Comparison Table
| Project | Difficulty | Time | Depth of Understanding | Fun Factor |
|---|---|---|---|---|
| The Basics | Level 1: Beginner | 1-2 hours | Low | ★★☆☆☆ |
| The Crash | Level 1: Beginner | 1-2 hours | Medium | ★★★☆☆ |
| The Hang | Level 2: Intermediate | 1 hour | Medium | ★★★☆☆ |
| The Corruption | Level 2: Intermediate | 2 hours | High | ★★★★☆ |
| The Assembly Level | Level 3: Advanced | 3-4 hours | High | ★★★★☆ |
| Python Scripting | Level 3: Advanced | 1-2 weeks | Very High | ★★★★★ |
Recommendation
Follow the projects in order. Projects 1-4 are essential for any developer. They cover the most common use cases and provide the biggest return on time investment. If you can analyze a crash, attach to a running process, and use watchpoints, you are already a more effective debugger than most.
Once you are comfortable, move to Project 5 (The Assembly Level). This will forever change how you think about your code.
Finally, invest time in Project 6 (Python Scripting). This is the gateway to true GDB mastery and will allow you to build your own toolkit tailored to your specific needs.
Final Overall Project: Build a Mini-Debugger
- File: LEARN_GDB_DEEP_DIVE.md
- Main Programming Language: C
- Alternative Programming Languages: Rust, Go
- Coolness Level: Level 5: Pure Magic (Super Cool)
- Business Potential: 1. The “Resume Gold”
- Difficulty: Level 5: Master
- Knowledge Area: Systems Programming / OS Internals
- Software or Tool: A
ptrace-based debugger - Main Book: “The Linux Programming Interface” by Michael Kerrisk
What you’ll build: A command-line program that uses the ptrace system call to start, stop, and inspect another program. It will be a very simplified GDB.
Why it’s the final goal: This project demystifies debugging entirely. You will no longer see GDB as a black box, but as a C program that is masterfully using the ptrace API. You will understand the fundamental OS mechanics that enable all debuggers.
Core challenges you’ll face:
- Controlling a child process → maps to
PTRACE_TRACEMEand thefork/execpattern - Setting a breakpoint → maps to reading memory (
PTRACE_PEEKTEXT), writing anint 3instruction (PTRACE_POKETEXT), and then restoring it - Reading registers → maps to
PTRACE_GETREGS - Single-stepping → maps to
PTRACE_SINGLESTEP
Real world outcome: A simple REPL that lets you control a target program.
$ ./mini_gdb ./my_program
mini_gdb> break 0x40100a
Breakpoint set at 0x40100a
mini_gdb> continue
Stopped at breakpoint 1: 0x40100a
mini_gdb> regs
rax: 0x5
rbx: 0x0
rip: 0x40100a
mini_gdb> step
Stopped at 0x40100b
mini_gdb> quit
Summary
| Project | Main Programming Language |
|---|---|
| The Basics | GDB Commands (on a C target) |
| The Crash | GDB Commands (on a C target) |
| The Hang | GDB Commands (on a C target) |
| The Corruption | GDB Commands (on a C target) |
| The Assembly Level | GDB Commands / x86 Assembly |
| Python Scripting | Python (in GDB) |
| Build a Mini-Debugger | C |