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:

  1. Implement the GDB Remote Serial Protocol: Understand packet format, checksums, and command structure
  2. Master ARM debug architecture: Configure Debug Monitor, understand debug events, and access debug registers
  3. Implement software breakpoints: Insert BKPT instructions and restore original code
  4. Handle single-stepping: Configure debug flags for instruction-by-instruction execution
  5. Access CPU context: Read and write registers from exception stack frames
  6. Perform arbitrary memory access: Safely read and write any memory address
  7. 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

  1. 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
  2. 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
  3. 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
  4. 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
  5. 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

  1. 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, ?)
  2. Breakpoint Management:
    • Insert software breakpoints (BKPT instruction)
    • Track breakpoints (address, original instruction)
    • Remove breakpoints and restore code
    • Support at least 16 simultaneous breakpoints
  3. Execution Control:
    • Continue execution from current PC
    • Single-step one instruction
    • Properly handle breakpoint at current PC
    • Stop on signal (Ctrl+C)
  4. Register Access:
    • Read all 16 general registers + PSR
    • Write to any register
    • Format as GDB expects (hex, little-endian)
  5. 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:

  1. Packet Parsing: How will you buffer incoming characters and detect complete packets? What about corrupted packets?

  2. Breakpoint Storage: How will you store breakpoint information (address, original instruction)? Array? Linked list?

  3. Register Access: Where are registers when Debug Monitor runs? How do you access R4-R11 which aren’t in the exception frame?

  4. Memory Safety: What happens if GDB asks to read from address 0xFFFFFFFF? How do you prevent your stub from crashing?

  5. Self-Debugging: What if the user sets a breakpoint in your GDB stub code? How do you prevent this?

  6. UART Sharing: The stub uses UART for GDB. How does the target program debug-print? Separate UART? Redirect?

  7. Interrupt Handling: What happens if an interrupt fires while in Debug Monitor? How do you handle nested exceptions?

  8. 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

  1. User sets breakpoint at 0x08000234
  2. Target runs and reaches 0x08000234
  3. What exactly happens, instruction by instruction?
  4. What does the stack look like when Debug Monitor starts?
  5. What packet does the stub send to GDB?

Scenario 2: Single Step Over Breakpoint

  1. Target is stopped at breakpoint at 0x08000234
  2. User types “step”
  3. What does the stub need to do?
  4. Why can’t it just set MON_STEP and return?
  5. Trace the sequence of events

Scenario 3: Memory Read

  1. GDB sends: $m20000000,40#xx
  2. How does the stub parse this?
  3. What response format does GDB expect?
  4. 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:

  1. Implement UART driver (or use existing from Project 4)
  2. Implement packet parser (find $, extract data, verify checksum)
  3. Implement packet sender (format $data#checksum)
  4. Send/receive acknowledgments (+/-)
  5. 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:

  1. Enable Debug Monitor in DEMCR
  2. Implement DebugMon_Handler in assembly
  3. Save all registers to memory
  4. Enter stub main loop on debug event
  5. 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:

  1. Implement ‘g’ command (read all registers)
  2. Implement ‘G’ command (write all registers)
  3. Implement ‘m’ command (read memory)
  4. Implement ‘M’ command (write memory)
  5. Implement ‘c’ command (continue execution)
  6. Implement ‘?’ command (halt reason)
  7. 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:

  1. Implement ‘Z0’ command (set breakpoint)
  2. Implement ‘z0’ command (clear breakpoint)
  3. Save original instruction before inserting BKPT
  4. Handle stepping off breakpoint
  5. Flush I-cache when modifying code
  6. 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:

  1. Implement ‘s’ command
  2. Set MON_STEP flag in DEMCR
  3. Handle re-inserting breakpoint after step
  4. 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:

  1. Handle Ctrl+C (break)
  2. Handle invalid memory access gracefully
  3. Add ‘p’ and ‘P’ commands (single register)
  4. Add thread commands (H)
  5. Comprehensive testing

Checkpoint: Robust GDB stub ready for real use.


Hints in Layers

Hint 1: Getting Started

Start with the simplest possible interaction:

  1. Connect GDB with target remote /dev/ttyUSB0
  2. GDB sends qSupported query
  3. Respond with empty packet (not supported)
  4. GDB sends ? (halt reason)
  5. 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:

  1. Use a try/catch-like mechanism with fault handlers
  2. Or check address ranges before access
  3. 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:

  1. First, manually trigger BKPT and verify handler works
  2. Then, test packet exchange with target remote
  3. Then, test each command in isolation
  4. 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

  1. Connection: GDB connects and negotiates
  2. Register Read/Write: All 17 registers correctly
  3. Memory Read: Various addresses and sizes
  4. Memory Write: Verify writes work
  5. Breakpoint Set/Clear: Multiple breakpoints
  6. Continue: Resume and stop at breakpoint
  7. Single Step: Step one instruction
  8. Step Over BP: Step when stopped at breakpoint
  9. Ctrl+C: Break running program
  10. 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

  1. Protocol Debugging: In GDB, use set debug remote 1 to see all packets
  2. LED Indicators: Toggle LED at various points in stub
  3. Serial Logging: Second UART for debug output (not GDB channel)
  4. Fault Analysis: Check DFSR for debug event type
  5. 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:

  1. “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
  2. “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
  3. “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
  4. “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
  5. “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

Example Implementations


This guide was expanded from LEARN_ARM_DEEP_DIVE.md. For the complete learning path, see the project index.