Project 14: ARM Debugger (GDB Stub)
Build a GDB remote stub that runs on your ARM target, allowing GDB to connect over serial and debug programs—set breakpoints, single-step, inspect memory and registers.
Quick Reference
| Attribute | Value |
|---|---|
| Language | C (alt: Rust) |
| Difficulty | Master |
| Time | 1 month+ |
| Prerequisites | Projects 4, 9 (UART, Exceptions) |
| Key Topics | Debug Architecture, GDB RSP, BKPT |
| Coolness | Level 5: Pure Magic (Super Cool) |
| Portfolio Value | Open Core Infrastructure |
| Hardware | STM32 (target) + USB-serial |
Learning Objectives
By completing this project, you will:
- Implement the GDB Remote Serial Protocol: Understand packet format, checksums, and command structure
- Master ARM debug architecture: Configure Debug Monitor, understand debug events, and access debug registers
- Implement software breakpoints: Insert BKPT instructions and restore original code
- Handle single-stepping: Configure debug flags for instruction-by-instruction execution
- Access CPU context: Read and write registers from exception stack frames
- Perform arbitrary memory access: Safely read and write any memory address
- Build debugging tools: Create the foundation for all debugger functionality
The Core Question You’re Answering
“How do debuggers actually work at the hardware level, and how does GDB communicate with embedded targets to control execution and inspect state?”
This question forces you to understand that debugging isn’t magic—it’s a well-defined protocol (GDB RSP) communicating with hardware debug features (Debug Monitor, breakpoint instructions) to observe and control program execution. Building a debugger teaches you more about CPU architecture than almost any other project.
Concepts You Must Understand First
Before starting this project, ensure you understand these concepts:
| Concept | Why It Matters | Where to Learn |
|---|---|---|
| ARM exception model | Debug events are handled as exceptions | Project 9, ARM TRM |
| UART communication | GDB connects via serial port | Project 4 |
| ARM instruction encoding | You’ll decode/encode BKPT instructions | Project 1 |
| Stack frame layout | Register values are saved on the stack | “Definitive Guide” Ch. 8 |
| Serial protocols | Packet framing, checksums, acknowledgments | Any networking book |
| Memory-mapped registers | Debug features accessed via special registers | ARM Debug TRM |
Key Concepts Deep Dive
- GDB Remote Serial Protocol (RSP)
- What is the packet format and how are checksums calculated?
- What are the essential commands a stub must support?
- How does GDB handle acknowledgments and retransmission?
- What is the difference between stop-reply packets and response packets?
- GDB Documentation: Remote Serial Protocol
- ARM Debug Architecture
- What is the Debug Monitor exception and when does it fire?
- What debug events can trigger the Debug Monitor?
- How do you enable debug features via DEMCR?
- What is the difference between Halt mode and Monitor mode debugging?
- “The Definitive Guide to ARM Cortex-M3/M4” Chapter 14
- Software Breakpoints
- What is the BKPT instruction and how is it encoded?
- How do you insert a breakpoint (save original, insert BKPT)?
- How do you resume after a breakpoint (restore, step, re-insert)?
- What happens to the PC when a BKPT hits?
- ARM Architecture Reference Manual
- Single-Stepping
- What debug flag enables single-step mode?
- How does the Debug Monitor fire after each instruction?
- How do you handle stepping over a breakpoint?
- What are the challenges with stepping into interrupt handlers?
- “Building a Debugger” by Sy Brand
- Register and Memory Access
- Where are registers stored when an exception occurs?
- How do you access the exception stack frame?
- How do you safely read memory that might be invalid?
- How do you handle reads/writes to peripheral registers?
- ARM Cortex-M Debug Technical Reference
Theoretical Foundation
The Debugging Architecture
GDB debugging on ARM Cortex-M uses Monitor Mode debugging:
Debug System Architecture:
┌─────────────────────────────────────────────────────────────────┐
│ │
│ Host Computer Target (STM32) │
│ ┌─────────────┐ ┌─────────────────────────┐ │
│ │ │ Serial/USB │ │ │
│ │ GDB │◀────────────────▶│ GDB Stub │ │
│ │ │ GDB RSP │ (your code) │ │
│ └─────────────┘ └───────────┬─────────────┘ │
│ │ │
│ ┌───────────▼─────────────┐ │
│ │ Debug Monitor │ │
│ │ Exception Handler │ │
│ └───────────┬─────────────┘ │
│ │ │
│ ┌───────────▼─────────────┐ │
│ │ Target Application │ │
│ │ (being debugged) │ │
│ └─────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
GDB Remote Serial Protocol
GDB communicates using a simple text-based protocol:
Packet Format:
┌─────────────────────────────────────────────────────────────────┐
│ │
│ $<data>#<checksum> │
│ │
│ Where: │
│ - $ marks start of packet │
│ - <data> is the command/response (ASCII) │
│ - # marks end of data │
│ - <checksum> is 2 hex digits (sum of data bytes mod 256) │
│ │
│ Examples: │
│ $g#67 Read all registers │
│ $m8000000,10#xx Read 16 bytes at 0x08000000 │
│ $c#63 Continue execution │
│ $s#73 Single step │
│ $Z0,8000234,2#xx Set breakpoint at 0x08000234 │
│ │
└─────────────────────────────────────────────────────────────────┘
Essential GDB Commands
A minimal stub must implement these commands:
Command Reference:
┌──────────┬─────────────────────────────────────────────────────┐
│ Command │ Description │
├──────────┼─────────────────────────────────────────────────────┤
│ ? │ Query halt reason (returns stop reply) │
│ g │ Read all general registers │
│ G │ Write all general registers │
│ m │ Read memory (m<addr>,<len>) │
│ M │ Write memory (M<addr>,<len>:<data>) │
│ c │ Continue execution │
│ s │ Single step │
│ Z0 │ Set software breakpoint (Z0,<addr>,<kind>) │
│ z0 │ Remove software breakpoint (z0,<addr>,<kind>) │
│ p │ Read single register (p<regnum>) │
│ P │ Write single register (P<regnum>=<value>) │
│ H │ Set thread (Hc<tid> for continue, Hg<tid> for ops) │
│ qSupported│ Query supported features │
└──────────┴─────────────────────────────────────────────────────┘
Stop Reply Packets
When the target stops, the stub sends a stop reply:
Stop Reply Format:
┌─────────────────────────────────────────────────────────────────┐
│ │
│ S<signal> │
│ Example: S05 (SIGTRAP - breakpoint) │
│ │
│ T<signal><info> │
│ Example: T05thread:01; │
│ │
│ Common signals: │
│ 02 = SIGINT (interrupt) │
│ 05 = SIGTRAP (breakpoint/step) │
│ 0b = SIGSEGV (memory fault) │
│ │
└─────────────────────────────────────────────────────────────────┘
ARM Debug Monitor Mode
Cortex-M processors support Monitor Mode debugging:
Debug Monitor Configuration:
┌─────────────────────────────────────────────────────────────────┐
│ │
│ DEMCR (Debug Exception and Monitor Control Register) │
│ Address: 0xE000EDFC │
│ │
│ Bits: │
│ [24] TRCENA - Enable trace │
│ [19] MON_REQ - Debug monitor request pending │
│ [18] MON_STEP - Step the processor │
│ [17] MON_PEND - Pend the debug monitor │
│ [16] MON_EN - Enable debug monitor exception │
│ │
│ To enable debug monitor: │
│ DEMCR |= (1 << 16); // MON_EN │
│ │
└─────────────────────────────────────────────────────────────────┘
Software Breakpoint Mechanism
Software breakpoints work by replacing instructions with BKPT:
Breakpoint Insertion:
┌─────────────────────────────────────────────────────────────────┐
│ │
│ Before breakpoint: │
│ 0x08000234: E3A00001 MOV R0, #1 │
│ │
│ After breakpoint: │
│ 0x08000234: BE00xxxx BKPT #0 │
│ (original instruction saved in breakpoint table) │
│ │
│ BKPT Encoding: │
│ - Thumb: 0xBExx (16-bit, xx = breakpoint number) │
│ - ARM: 0xE12xxx7x (32-bit) │
│ │
│ When BKPT executes: │
│ 1. CPU triggers Debug Monitor exception │
│ 2. Exception handler saves context │
│ 3. GDB stub notifies host with stop reply │
│ 4. PC points to BKPT instruction │
│ │
└─────────────────────────────────────────────────────────────────┘
Exception Stack Frame
When Debug Monitor triggers, registers are pushed to the stack:
Cortex-M Exception Stack Frame:
┌─────────────────────────────────────────────────────────────────┐
│ │
│ High Address │
│ ┌─────────────────────┐ │
│ │ xPSR │ ← SP + 28 │
│ ├─────────────────────┤ │
│ │ Return Address │ ← SP + 24 (PC when exception hit) │
│ ├─────────────────────┤ │
│ │ LR │ ← SP + 20 (R14) │
│ ├─────────────────────┤ │
│ │ R12 │ ← SP + 16 │
│ ├─────────────────────┤ │
│ │ R3 │ ← SP + 12 │
│ ├─────────────────────┤ │
│ │ R2 │ ← SP + 8 │
│ ├─────────────────────┤ │
│ │ R1 │ ← SP + 4 │
│ ├─────────────────────┤ │
│ │ R0 │ ← SP + 0 │
│ └─────────────────────┘ │
│ Low Address (SP after exception) │
│ │
│ Other registers (R4-R11) must be saved manually if needed │
│ │
└─────────────────────────────────────────────────────────────────┘
Single-Step Implementation
Single-stepping requires special handling:
Single-Step Sequence:
┌─────────────────────────────────────────────────────────────────┐
│ │
│ 1. GDB sends: $s#73 │
│ │
│ 2. Stub receives 's' command: │
│ - Set MON_STEP in DEMCR │
│ - If at breakpoint, restore original instruction │
│ - Return from Debug Monitor (execute one instruction) │
│ │
│ 3. After one instruction: │
│ - CPU triggers Debug Monitor (due to MON_STEP) │
│ - Clear MON_STEP │
│ - If was at breakpoint, re-insert BKPT │
│ - Send stop reply to GDB │
│ │
│ DEMCR->MON_STEP = 1; // Enable single-step │
│ return_from_exception(); │
│ // One instruction executes │
│ // Debug Monitor fires again │
│ │
└─────────────────────────────────────────────────────────────────┘
Common Misconceptions
Misconception 1: “Debuggers use magic hardware features” Reality: Software debuggers primarily use software breakpoints (BKPT instruction) and single-step flags. Hardware breakpoints (via debug registers) are limited in number but don’t modify code.
Misconception 2: “JTAG is required for debugging” Reality: Monitor mode debugging works over any communication channel (UART, USB, etc.). JTAG is for halt-mode debugging, which is different.
Misconception 3: “The debugger can see everything” Reality: The stub runs on the target and has the same memory access as any code. Accessing invalid memory will fault.
Misconception 4: “Single-step is simple” Reality: Edge cases abound: stepping over breakpoints, stepping into ISRs, stepping over system calls, handling multi-instruction sequences.
Project Specification
What You Will Build
A GDB remote stub that runs on your ARM target and allows full source-level debugging:
- Connect GDB from host computer over serial
- Set and clear breakpoints
- Single-step through code
- Inspect and modify registers
- Read and write memory
- Handle faults gracefully
Functional Requirements
- GDB Protocol Implementation:
- Parse incoming packets and validate checksums
- Generate properly formatted response packets
- Handle acknowledgment/retransmission
- Support essential command set (g, G, m, M, c, s, Z0, z0, ?)
- Breakpoint Management:
- Insert software breakpoints (BKPT instruction)
- Track breakpoints (address, original instruction)
- Remove breakpoints and restore code
- Support at least 16 simultaneous breakpoints
- Execution Control:
- Continue execution from current PC
- Single-step one instruction
- Properly handle breakpoint at current PC
- Stop on signal (Ctrl+C)
- Register Access:
- Read all 16 general registers + PSR
- Write to any register
- Format as GDB expects (hex, little-endian)
- Memory Access:
- Read any memory address
- Write any memory address
- Handle invalid address gracefully
- Support different access sizes
Non-Functional Requirements
- Reliability: No crashes in stub code
- Performance: Response time < 100ms for typical operations
- Robustness: Recover from communication errors
- Compatibility: Work with standard GDB (arm-none-eabi-gdb)
Real World Outcome
When complete, you can debug programs like this:
# On your computer:
$ arm-none-eabi-gdb program.elf
(gdb) target remote /dev/ttyUSB0
Remote debugging using /dev/ttyUSB0
0x08000100 in Reset_Handler ()
(gdb) break main
Breakpoint 1 at 0x08000234: file main.c, line 12.
(gdb) continue
Continuing.
Breakpoint 1, main () at main.c:12
12 int x = 42;
(gdb) print x
$1 = 0
(gdb) step
13 int y = x * 2;
(gdb) print x
$2 = 42
(gdb) info registers
r0 0x42 66
r1 0x0 0
r2 0x20000100 536871168
r3 0x0 0
r4 0x0 0
r5 0x0 0
r6 0x0 0
r7 0x20000ff8 536870904
r8 0x0 0
r9 0x0 0
r10 0x0 0
r11 0x0 0
r12 0x0 0
sp 0x20000ff0 0x20000ff0
lr 0x8000200 0x8000200 <_start+16>
pc 0x8000238 0x8000238 <main+4>
cpsr 0x61000000 1627389952
(gdb) x/4x 0x20000000
0x20000000: 0x00000042 0x00000054 0x00000000 0x00000000
(gdb) set $r0 = 100
(gdb) print $r0
$3 = 100
(gdb) backtrace
#0 main () at main.c:13
#1 0x08000200 in _start ()
(gdb) continue
Continuing.
^C
Program received signal SIGINT, Interrupt.
0x08000300 in delay_loop ()
(gdb) quit
Questions to Guide Your Design
Work through these questions BEFORE writing code:
-
Packet Parsing: How will you buffer incoming characters and detect complete packets? What about corrupted packets?
-
Breakpoint Storage: How will you store breakpoint information (address, original instruction)? Array? Linked list?
-
Register Access: Where are registers when Debug Monitor runs? How do you access R4-R11 which aren’t in the exception frame?
-
Memory Safety: What happens if GDB asks to read from address 0xFFFFFFFF? How do you prevent your stub from crashing?
-
Self-Debugging: What if the user sets a breakpoint in your GDB stub code? How do you prevent this?
-
UART Sharing: The stub uses UART for GDB. How does the target program debug-print? Separate UART? Redirect?
-
Interrupt Handling: What happens if an interrupt fires while in Debug Monitor? How do you handle nested exceptions?
-
Code Modification: When you write BKPT, you’re modifying code. Do you need to flush I-cache?
Thinking Exercise
Before writing any code, trace through these scenarios:
Scenario 1: Breakpoint Hit
- User sets breakpoint at 0x08000234
- Target runs and reaches 0x08000234
- What exactly happens, instruction by instruction?
- What does the stack look like when Debug Monitor starts?
- What packet does the stub send to GDB?
Scenario 2: Single Step Over Breakpoint
- Target is stopped at breakpoint at 0x08000234
- User types “step”
- What does the stub need to do?
- Why can’t it just set MON_STEP and return?
- Trace the sequence of events
Scenario 3: Memory Read
- GDB sends:
$m20000000,40#xx - How does the stub parse this?
- What response format does GDB expect?
- What if address is invalid?
Solution Architecture
High-Level Design
┌─────────────────────────────────────────────────────────────────┐
│ GDB Stub │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ Serial │───▶│ Packet │───▶│ Command │ │
│ │ Driver │ │ Parser │ │ Handler │ │
│ └────────────┘ └────────────┘ └────────────┘ │
│ │ │
│ ┌────────────────┼────────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌────────────┐ ┌────────────┐ ┌────────┐│
│ │ Breakpoint │ │ Register │ │ Memory ││
│ │ Manager │ │ Access │ │ Access ││
│ └────────────┘ └────────────┘ └────────┘│
│ │ │ │ │
│ └────────────────┼────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────┐ │
│ │ Debug │ │
│ │ Monitor │ │
│ │ Handler │ │
│ └────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
System Components
Component Interaction:
┌─────────────────────────────────────────────────────────────────┐
│ │
│ GDB (Host) Target │
│ ───────── ────── │
│ │
│ $g#67 ─────────────────────────────▶ Parse packet │
│ │ │
│ ▼ │
│ Read registers from │
│ exception frame │
│ │ │
│ ▼ │
│ ◀─────────────────────────────────── Send hex register dump │
│ <register data> │
│ │
│ $Z0,8000234,2#xx ──────────────────▶ Parse breakpoint cmd │
│ │ │
│ ▼ │
│ Save original instr │
│ Write BKPT to address │
│ Add to breakpoint table │
│ │ │
│ ◀─────────────────────────────────── OK │
│ │
│ $c#63 ─────────────────────────────▶ Return from Debug Mon │
│ │ │
│ ▼ │
│ Target runs... │
│ │ │
│ ▼ │
│ BKPT hit! │
│ Debug Monitor fires │
│ │ │
│ ◀─────────────────────────────────── T05 (SIGTRAP) │
│ │
└─────────────────────────────────────────────────────────────────┘
Data Structures
// Breakpoint entry
typedef struct {
uint32_t address; // Address of breakpoint
uint16_t original_instr; // Original instruction (Thumb)
uint8_t active; // Is breakpoint active?
uint8_t was_hit; // Was this BP that stopped us?
} Breakpoint;
// Breakpoint table
#define MAX_BREAKPOINTS 16
static Breakpoint breakpoints[MAX_BREAKPOINTS];
// Saved registers (full context)
typedef struct {
uint32_t r[13]; // R0-R12
uint32_t sp; // R13 (SP)
uint32_t lr; // R14 (LR)
uint32_t pc; // R15 (PC)
uint32_t psr; // xPSR
} Registers;
// Debug state
typedef struct {
volatile uint8_t stopped; // Are we stopped?
volatile uint8_t stepping; // Are we single-stepping?
Breakpoint *hit_breakpoint; // Which BP stopped us
Registers saved_regs; // Saved register context
} DebugState;
// Packet buffer
#define PACKET_BUF_SIZE 512
typedef struct {
char data[PACKET_BUF_SIZE];
int length;
uint8_t checksum;
} Packet;
State Machine
GDB Stub State Machine:
┌───────────────────┐
│ │
▼ │
┌───────────────┐ │
┌────▶│ RUNNING │───────────┤
│ └───────────────┘ │
│ │ │
│ breakpoint │
│ or step │
│ │ │
│ ▼ │
│ ┌───────────────┐ │
│ │ STOPPED │───────────┘
│ │ (in Debug Mon)│ continue
│ └───────────────┘
│ │
│ continue
│ or step
│ │
└────────────┘
Implementation Guide
Development Environment Setup
# Required tools
sudo apt-get install gcc-arm-none-eabi gdb-multiarch openocd
# Or install arm-none-eabi-gdb specifically
# Download from ARM developer site
# Create project structure
mkdir -p gdb-stub/{src,inc}
cd gdb-stub
Project Structure
gdb-stub/
├── src/
│ ├── main.c # Entry point, initialization
│ ├── gdb_stub.c # Main GDB stub logic
│ ├── gdb_packet.c # Packet parsing/generation
│ ├── gdb_commands.c # Command handlers
│ ├── breakpoints.c # Breakpoint management
│ ├── debug_monitor.c # Debug Monitor handler
│ ├── uart.c # UART driver
│ └── startup.s # Startup code
├── inc/
│ ├── gdb_stub.h
│ ├── gdb_packet.h
│ ├── breakpoints.h
│ └── stm32f4xx.h
├── linker.ld
└── Makefile
Implementation Phases
Phase 1: Packet Layer (Days 1-5)
Goals:
- Implement GDB packet parsing
- Implement packet generation with checksums
- Test with GDB connection
Tasks:
- Implement UART driver (or use existing from Project 4)
- Implement packet parser (find $, extract data, verify checksum)
- Implement packet sender (format $data#checksum)
- Send/receive acknowledgments (+/-)
- Test: Connect GDB, handle ‘qSupported’ query
Checkpoint: GDB connects and receives valid response to ‘qSupported’.
// Packet parsing
int gdb_receive_packet(char *buffer, int max_len) {
int state = 0; // 0=wait $, 1=data, 2=checksum
int idx = 0;
uint8_t checksum = 0;
char cksum_str[3];
while (1) {
char c = uart_getc();
switch (state) {
case 0: // Waiting for $
if (c == '$') {
state = 1;
idx = 0;
checksum = 0;
}
break;
case 1: // Receiving data
if (c == '#') {
buffer[idx] = '\0';
state = 2;
idx = 0;
} else {
buffer[idx++] = c;
checksum += c;
}
break;
case 2: // Receiving checksum
cksum_str[idx++] = c;
if (idx == 2) {
cksum_str[2] = '\0';
uint8_t recv_cksum = strtoul(cksum_str, NULL, 16);
if (recv_cksum == checksum) {
uart_putc('+'); // ACK
return strlen(buffer);
} else {
uart_putc('-'); // NAK
state = 0;
}
}
break;
}
}
}
void gdb_send_packet(const char *data) {
uint8_t checksum = 0;
const char *p = data;
uart_putc('$');
while (*p) {
uart_putc(*p);
checksum += *p;
p++;
}
uart_putc('#');
uart_printf("%02x", checksum);
// Wait for ACK
while (uart_getc() != '+');
}
Phase 2: Debug Monitor (Days 6-10)
Goals:
- Set up Debug Monitor exception
- Save and restore CPU context
- Handle BKPT instruction
Tasks:
- Enable Debug Monitor in DEMCR
- Implement DebugMon_Handler in assembly
- Save all registers to memory
- Enter stub main loop on debug event
- Test: Manually trigger BKPT, verify handler runs
Checkpoint: BKPT instruction triggers Debug Monitor and enters stub.
// Enable debug monitor
void debug_init(void) {
// Enable Debug Monitor exception
CoreDebug->DEMCR |= CoreDebug_DEMCR_MON_EN_Msk;
// Set priority (must be higher than other exceptions we want to debug)
NVIC_SetPriority(DebugMonitor_IRQn, 0);
}
// Debug Monitor handler (in assembly for precise register access)
// startup.s:
.global DebugMon_Handler
.thumb_func
DebugMon_Handler:
// Determine which stack was in use
tst lr, #4
ite eq
mrseq r0, msp
mrsne r0, psp
// r0 now points to exception frame
// Save additional registers
stmdb sp!, {r4-r11}
// Call C handler
bl debug_monitor_handler
// Restore additional registers
ldmia sp!, {r4-r11}
// Return from exception
bx lr
Phase 3: Essential Commands (Days 11-18)
Goals:
- Implement register read/write
- Implement memory read/write
- Implement continue
Tasks:
- Implement ‘g’ command (read all registers)
- Implement ‘G’ command (write all registers)
- Implement ‘m’ command (read memory)
- Implement ‘M’ command (write memory)
- Implement ‘c’ command (continue execution)
- Implement ‘?’ command (halt reason)
- Test: GDB can read registers and memory
Checkpoint: GDB can connect, read registers, read memory, and continue.
// Command handler
void handle_command(char *packet) {
switch (packet[0]) {
case '?': // Halt reason
gdb_send_packet("S05"); // SIGTRAP
break;
case 'g': // Read registers
send_registers();
break;
case 'G': // Write registers
write_registers(packet + 1);
gdb_send_packet("OK");
break;
case 'm': // Read memory
handle_read_memory(packet + 1);
break;
case 'M': // Write memory
handle_write_memory(packet + 1);
gdb_send_packet("OK");
break;
case 'c': // Continue
continue_execution();
break;
default:
gdb_send_packet(""); // Not supported
break;
}
}
// Read all registers
void send_registers(void) {
char buf[17 * 8 + 1]; // 17 registers, 8 hex chars each
char *p = buf;
// R0-R12
for (int i = 0; i < 13; i++) {
p += sprintf(p, "%08x", swap32(saved_regs.r[i]));
}
// SP, LR, PC, PSR
p += sprintf(p, "%08x", swap32(saved_regs.sp));
p += sprintf(p, "%08x", swap32(saved_regs.lr));
p += sprintf(p, "%08x", swap32(saved_regs.pc));
p += sprintf(p, "%08x", swap32(saved_regs.psr));
gdb_send_packet(buf);
}
// Read memory
void handle_read_memory(char *args) {
uint32_t addr, len;
sscanf(args, "%x,%x", &addr, &len);
char buf[256];
char *p = buf;
for (uint32_t i = 0; i < len; i++) {
uint8_t byte = *(uint8_t *)(addr + i);
p += sprintf(p, "%02x", byte);
}
gdb_send_packet(buf);
}
Phase 4: Breakpoints (Days 19-25)
Goals:
- Implement software breakpoint insertion
- Handle breakpoint removal
- Handle resuming from breakpoint
Tasks:
- Implement ‘Z0’ command (set breakpoint)
- Implement ‘z0’ command (clear breakpoint)
- Save original instruction before inserting BKPT
- Handle stepping off breakpoint
- Flush I-cache when modifying code
- Test: Set breakpoint, run, verify it stops
Checkpoint: Breakpoints work correctly.
// Set software breakpoint
int set_breakpoint(uint32_t addr) {
// Find free slot
for (int i = 0; i < MAX_BREAKPOINTS; i++) {
if (!breakpoints[i].active) {
breakpoints[i].address = addr;
breakpoints[i].original_instr = *(uint16_t *)addr;
breakpoints[i].active = 1;
breakpoints[i].was_hit = 0;
// Insert BKPT instruction
*(uint16_t *)addr = 0xBE00; // BKPT #0
// Flush I-cache
__DSB();
__ISB();
return 0;
}
}
return -1; // No free slot
}
// Clear breakpoint
int clear_breakpoint(uint32_t addr) {
for (int i = 0; i < MAX_BREAKPOINTS; i++) {
if (breakpoints[i].active && breakpoints[i].address == addr) {
// Restore original instruction
*(uint16_t *)addr = breakpoints[i].original_instr;
breakpoints[i].active = 0;
__DSB();
__ISB();
return 0;
}
}
return -1;
}
// Handle stepping over breakpoint
void step_over_breakpoint(void) {
// Check if we're stopped at a breakpoint
Breakpoint *bp = find_breakpoint(saved_regs.pc);
if (bp) {
// Temporarily restore original instruction
*(uint16_t *)bp->address = bp->original_instr;
__DSB();
__ISB();
// Mark that we need to re-insert after step
bp->was_hit = 1;
}
}
Phase 5: Single-Step (Days 26-30)
Goals:
- Implement single-step command
- Handle step over breakpoint
- Handle edge cases
Tasks:
- Implement ‘s’ command
- Set MON_STEP flag in DEMCR
- Handle re-inserting breakpoint after step
- Test: Step through code instruction by instruction
Checkpoint: Single-stepping works correctly.
// Single step
void single_step(void) {
// If at breakpoint, need special handling
step_over_breakpoint();
// Enable single-step mode
CoreDebug->DEMCR |= CoreDebug_DEMCR_MON_STEP_Msk;
// Return from Debug Monitor will execute one instruction
// then immediately re-enter Debug Monitor
}
// In Debug Monitor handler, after stopping:
void debug_monitor_handler(uint32_t *exception_frame) {
// Save context
save_registers(exception_frame);
// Check if this was a single-step
if (CoreDebug->DEMCR & CoreDebug_DEMCR_MON_STEP_Msk) {
// Clear step flag
CoreDebug->DEMCR &= ~CoreDebug_DEMCR_MON_STEP_Msk;
// Re-insert breakpoint if we stepped over one
restore_breakpoint_if_needed();
}
// Send stop notification
gdb_send_packet("S05");
// Enter command loop
gdb_stub_main();
}
Phase 6: Polish (Days 31+)
Goals:
- Handle all edge cases
- Add robustness
- Test thoroughly
Tasks:
- Handle Ctrl+C (break)
- Handle invalid memory access gracefully
- Add ‘p’ and ‘P’ commands (single register)
- Add thread commands (H)
- Comprehensive testing
Checkpoint: Robust GDB stub ready for real use.
Hints in Layers
Hint 1: Getting Started
Start with the simplest possible interaction:
- Connect GDB with
target remote /dev/ttyUSB0 - GDB sends
qSupportedquery - Respond with empty packet (not supported)
- GDB sends
?(halt reason) - Respond with
S05(stopped, SIGTRAP)
Now GDB thinks it’s connected and you can test other commands.
Hint 2: Register Order
GDB expects registers in a specific order for ARM:
- R0-R12 (13 registers)
- SP (R13)
- LR (R14)
- PC (R15)
- xPSR
Each register is 32 bits = 8 hex characters. Total: 17 registers * 8 chars = 136 characters.
GDB expects little-endian format, so swap bytes: 0x12345678 -> “78563412”
Hint 3: Memory Access Safety
To safely read memory that might be invalid:
- Use a try/catch-like mechanism with fault handlers
- Or check address ranges before access
- Or use MPU to detect bad accesses
Simple approach: maintain a “safe memory access” flag, catch HardFault, and return error if flag is set.
Hint 4: I-Cache Considerations
When you modify code (inserting BKPT), the I-cache may still have the old instruction. Always flush:
__DSB(); // Data synchronization barrier
__ISB(); // Instruction synchronization barrier
Hint 5: Ctrl+C Handling
GDB sends 0x03 (ETX) to break running target. Check for this character:
- While target is running, keep checking UART
- On 0x03, set a flag and trigger Debug Monitor
- Can use UART interrupt for this
Hint 6: Testing Strategy
Test incrementally:
- First, manually trigger BKPT and verify handler works
- Then, test packet exchange with
target remote - Then, test each command in isolation
- Finally, test full debugging workflows
Use GDB’s set debug remote 1 to see all protocol traffic.
Testing Strategy
Test Categories
| Category | Purpose | Examples |
|---|---|---|
| Unit Tests | Test packet parsing, hex conversion | Packet checksum calculation |
| Protocol Tests | Test GDB command/response | Each command with various inputs |
| Integration | Full debugging scenarios | Set BP, run, step, inspect |
| Edge Cases | Error conditions | Invalid memory, too many BPs |
Critical Test Cases
- Connection: GDB connects and negotiates
- Register Read/Write: All 17 registers correctly
- Memory Read: Various addresses and sizes
- Memory Write: Verify writes work
- Breakpoint Set/Clear: Multiple breakpoints
- Continue: Resume and stop at breakpoint
- Single Step: Step one instruction
- Step Over BP: Step when stopped at breakpoint
- Ctrl+C: Break running program
- Invalid Memory: Don’t crash on bad address
Test Script
#!/bin/bash
# test_gdb_stub.sh
GDB=arm-none-eabi-gdb
TARGET=/dev/ttyUSB0
# Create test commands file
cat > test_commands.gdb << 'EOF'
set pagination off
set confirm off
target remote /dev/ttyUSB0
info registers
x/10x 0x08000000
break main
continue
step
info registers
print $pc
continue
quit
EOF
# Run test
$GDB -x test_commands.gdb test_program.elf
Common Pitfalls & Debugging
Frequent Mistakes
| Pitfall | Symptom | Solution |
|---|---|---|
| Wrong checksum | GDB retransmits | Verify checksum calculation |
| Byte order | Wrong register values | Use little-endian for GDB |
| Missing ACK | GDB hangs | Send ‘+’ after valid packet |
| No I-cache flush | BP doesn’t trigger | Add DSB/ISB after BP write |
| Wrong exception frame | Bad register values | Check stack pointer selection |
| Priority issues | Nested exceptions | Set Debug Mon priority correctly |
| BKPT encoding | BP doesn’t trigger | Use 0xBE00 for Thumb |
Debugging Strategies
- Protocol Debugging: In GDB, use
set debug remote 1to see all packets - LED Indicators: Toggle LED at various points in stub
- Serial Logging: Second UART for debug output (not GDB channel)
- Fault Analysis: Check DFSR for debug event type
- Register Dumps: Print all registers on entry to Debug Mon
Debug Monitor Debug
// Useful for debugging the debugger
void dump_debug_state(void) {
printf("DEMCR: %08x\n", CoreDebug->DEMCR);
printf("DFSR: %08x\n", SCB->DFSR);
printf("HFSR: %08x\n", SCB->HFSR);
printf("CFSR: %08x\n", SCB->CFSR);
}
Extensions & Challenges
Beginner Extensions
- Hardware breakpoints: Use debug registers for limited non-code-modifying breakpoints
- Watchpoints: Break on memory read/write
- Thread info: Report thread state for RTOS debugging
- Symbol lookup: Add basic symbol table support
Intermediate Extensions
- Flash programming: Allow GDB to flash new firmware
- Semihosting: Implement ARM semihosting for printf to GDB console
- Reverse debugging: Record execution for replay
- Remote file access: Read target filesystem from GDB
Advanced Extensions
- Multi-core debugging: Support SMP targets
- Trace support: Integrate with ETM/ITM trace
- Python scripting: Expose stub functionality to Python
- RTOS awareness: Parse RTOS task structures
Self-Assessment Checklist
Understanding
- I can explain Monitor Mode vs Halt Mode debugging
- I understand the GDB Remote Serial Protocol packet format
- I can describe how software breakpoints work
- I understand the exception stack frame layout
- I can explain how single-stepping is implemented
- I know why I-cache flush is needed after code modification
Implementation
- Packet parsing handles all valid packets
- Register read returns all 17 registers correctly
- Memory read works for various addresses and sizes
- At least 8 breakpoints can be set simultaneously
- Single-step works correctly
- Stepping over breakpoint works
- Ctrl+C breaks running program
Testing
- GDB can connect and communicate
- Full debugging workflow works (BP, run, step, inspect)
- Error conditions don’t crash the stub
- Long debugging sessions remain stable
The Interview Questions They’ll Ask
After completing this project, you’ll be ready for these questions:
- “How do debuggers work?”
- Monitor mode: Uses special CPU features to pause and inspect
- Software breakpoints: Replace instructions with BKPT, trap to handler
- Hardware breakpoints: Limited debug registers for non-code breaks
- Single-step: CPU flag that causes trap after each instruction
- “Explain the GDB Remote Serial Protocol”
- Text-based protocol over serial/TCP
- Packet format: $data#checksum
- Commands for register/memory access, execution control
- Stop replies when target stops
- “What happens when a breakpoint is hit?”
- BKPT instruction executed
- CPU triggers Debug Monitor exception
- Handler saves context, notifies debugger
- Debugger queries state, user decides next action
- Debugger sends continue/step command
- “How do you implement single-stepping?”
- Set MON_STEP flag in DEMCR
- Return from exception
- CPU executes one instruction
- Debug Monitor triggered again
- Special handling if stepping over breakpoint
- “What are the challenges in writing a debugger?”
- Self-reference: Can’t set breakpoint in debugger code
- Timing: Don’t interfere with real-time behavior
- Concurrency: Handle interrupts during debugging
- Safety: Don’t crash on invalid memory access
Books That Will Help
| Topic | Book | Chapter |
|---|---|---|
| GDB Remote Protocol | GDB Documentation | Remote Serial Protocol section |
| ARM Debug Architecture | “The Definitive Guide to ARM Cortex-M3/M4” | Chapter 14 |
| Debugger Internals | “Building a Debugger” by Sy Brand | All chapters |
| Exception Handling | ARM Cortex-M Technical Reference | Debug chapter |
| Low-level debugging | “Debugging with GDB” | Remote debugging chapters |
Resources
Documentation
- GDB Remote Serial Protocol
- ARM Cortex-M Debug Technical Reference Manual
- ARM Architecture Reference Manual (Debug section)
Example Implementations
- GDB stub example for Cortex-M
- tinyemu - Includes GDB stub
- probe-rs - Modern debugger implementation
Related Projects in This Series
- Previous: Project 13: DMA Audio Player
- Next: Project 15: Tiny OS
- Prerequisite: Project 4: UART Driver, Project 9: Exception Handler
This guide was expanded from LEARN_ARM_DEEP_DIVE.md. For the complete learning path, see the project index.