# ESP-IDF esp_gdbstub — GDB Remote Serial Protocol (RSP) Specification **Golden reference**: ESP-IDF v5.5.4 `components/esp_gdbstub/` (Xtensa / ESP32). **Purpose**: Exact protocol and data-structure reference for replicating the IDF gdbstub in bare-metal UniversalisOS (no FreeRTOS, no `esp_intr_alloc`). All byte layouts, field offsets, and protocol sequences below are extracted **verbatim** from the IDF v5.5.4 source. Line references cite the file. --- ## Table of Contents 1. [Packet Framing (packet.c)](#1-packet-framing-packetc) 2. [Command Handlers (gdbstub.c)](#2-command-handlers-gdbstubc) 3. [Register File — g/G packets](#3-register-file--gg-packets) 4. [Signal Mapping](#4-signal-mapping) 5. [vCont Support](#5-vcont-support) 6. [Memory Validity](#6-memory-validity) 7. [Xtensa gdbstub-entry.S — Interrupt Entry & Frame](#7-xtensa-gdbstub-entrys--interrupt-entry--frame) --- ## 1. Packet Framing (packet.c) Source: `src/packet.c` (174 lines). This is the **transport-independent** layer; `esp_gdbstub_putchar/getchar` are implemented in `gdbstub_transport.c` (UART). ### 1.1 Packet wire format ``` $# ``` - `$` (0x24) — start-of-packet marker - `` — command bytes, with run-length encoding **disabled** (IDF never sends `*`; it only *parses* the escape on receive, see §1.3) - `#` (0x23) — end-of-payload marker - `` — 2 ASCII hex digits (lowercase), = `(sum of payload bytes) mod 256` ### 1.2 Checksum computation — simple sum mod 256 ```c // packet.c:14, 141 (receive path) unsigned char chsum = 0; // for each unescaped payload byte c: chsum += c; ``` On send (packet.c:58–62 `esp_gdbstub_send_end`): ```c esp_gdbstub_putchar('#'); esp_gdbstub_send_hex(s_chsum, 8); // 8 bits => 2 hex chars, lowercase esp_gdbstub_flush(); ``` The checksum is the **8-bit truncation** of the sum of all *unescaped* payload bytes. It is transmitted as exactly 2 lowercase hex digits (`"0123456789abcdef"`). ### 1.3 Escape (binary) mode — the `}` + XOR 0x20 rule **Chars that get escaped** (packet.c:26–28, on send): | Char | ASCII | Escaped as | |------|-------|-----------| | `$` | 0x24 | `}` `0x04` | | `#` | 0x23 | `}` `0x03` | | `}` | 0x7D | `}` `0x5D` | | `*` | 0x2A | `}` `0x0A` | Rule: emit `}` (0x7D), then emit `c ^ 0x20`. **Checksum on escaped send** (packet.c:29): the checksum adds **both** the `}` and the XOR'd byte: ```c s_chsum += (c ^ 0x20) + '}'; ``` i.e. checksum is always computed over the **wire bytes actually transmitted** (including the `}`), never over the logical byte. *(Note: this differs from the canonical GDB spec where checksum is over logical bytes. IDF sums wire bytes. In practice, since on the receive side it also sums the raw received bytes including `}` and the XOR'd byte (packet.c:141,151), both sides agree.)* **Receive-side unescape** (packet.c:148–153): ```c if (c == '}') { c = esp_gdbstub_getchar(); // read next byte chsum += c; // add the escaped byte to checksum c ^= 0x20; // restore logical byte } ``` The `}` itself was already added to `chsum` at line 141 (it falls through the general `chsum += c` before the `}` branch is taken). So on receive, checksum = sum of all raw bytes between `$` and `#` (including `}` and the XOR'd byte) — matching the send side exactly. ### 1.4 ACK / NAK protocol (+/-) After receiving a complete packet and validating the checksum (packet.c:164–172): | Condition | Stub sends | Return | |-----------|-----------|--------| | Checksum matches | `+` (0x2B) | `GDBSTUB_ST_OK` (-3), sets `*out_cmd`/`*out_size` | | Checksum mismatch | `-` (0x2D) | `GDBSTUB_ST_ERR` (-2) | `GDBSTUB_ST_OK = -3`, `GDBSTUB_ST_ERR = -2`, `GDBSTUB_ST_ENDPACKET = -1`, `GDBSTUB_ST_CONT = -4` (esp_gdbstub_common.h:23–26). **GDB also sends `+`/`-`** to ACK/NAK stub packets. The stub's receive loop (gdbstub.c:84–92, 258–278) treats these as noise: `esp_gdbstub_read_command` returns the character if the first byte isn't `$`, and the main loop does `if (res > 0) continue;` — silently ignoring GDB's `+`/`-` ACKs. **The stub does not retry on NAK from GDB.** ### 1.5 Command buffer - `GDBSTUB_CMD_BUFLEN = 512` (esp_gdbstub_common.h:32). - Static buffer `s_cmd[512]` (packet.c:11). If payload exceeds 512 bytes, returns `GDBSTUB_ST_ERR` (packet.c:155–157). - After `#`, payload is NUL-terminated: `s_cmd[p] = 0` (packet.c:138). ### 1.6 Hex parsing — `esp_gdbstub_gethex` (packet.c:79–116). `bits` = max bits to read; `bits=-1` = greedy (up to 64 hex chars / 256 bits). Accepts `0-9`, `A-F`, `a-f`. Stops at `#` or non-hex. Returns `GDBSTUB_ST_ENDPACKET` (-1) on `#` (fixed-width mode) or `GDBSTUB_ST_ERR` (-2) on non-hex (fixed-width mode). ### 1.7 Hex output — `esp_gdbstub_send_hex(val, bits)` (packet.c:47–55). Emits `bits/4` lowercase hex chars, **MSB first**. E.g. `send_hex(0x12345678, 32)` → `"12345678"`. `send_hex(val, 8)` → 2 chars. --- ## 2. Command Handlers (gdbstub.c) Source: `src/gdbstub.c` (1141 lines). The dispatcher is `esp_gdbstub_handle_command()` (line 711). **Two build configs** affect which commands exist: - `CONFIG_ESP_SYSTEM_GDBSTUB_RUNTIME` — runtime debugging (breakpoints, step, continue). Gates: Z/z, S, k, s, C, P, c, qSupported. - `CONFIG_ESP_GDBSTUB_SUPPORT_TASKS` — FreeRTOS task-as-thread listing. Gates: H, T, qC, qfThreadInfo, qsThreadInfo, qThreadExtraInfo, vCont. Without either flag (panic-only stub), only `g G m M ?` are handled. ### Return values from `esp_gdbstub_handle_command` | Value | Meaning | Main-loop action | |-------|---------|-----------------| | `GDBSTUB_ST_OK` (-3) | handled, reply already sent | loop, read next cmd | | `GDBSTUB_ST_CONT` (-4) | resume target execution | **break out** of cmd loop → return from interrupt | | `GDBSTUB_ST_ERR` (-2) | unrecognized / parse error | send empty `$#` packet | When the handler returns `GDBSTUB_ST_ERR`, the main loop sends an **empty packet** `esp_gdbstub_send_str_packet(NULL)` → `$#00` (gdbstub.c:95, 274, 345). When `read_command` itself fails (bad checksum/overflow), it sends `E01` (gdbstub.c:90, 268, 339). ### 2.1 Command table | Cmd | Format (GDB→Stub) | Stub Response | Handler / Notes | |-----|-------------------|---------------|-----------------| | **g** | `g` | `$#cks` | `handle_g_command` (388). All regs, big-endian (§3). | | **G** | `G` | `$OK#9a` | `handle_G_command` (399). Writes all regs. | | **m** | `m,` | `$#cks` or `$E01#` | `handle_m_command` (439). addr & len are **variable-width hex** (gethex -1). Reads byte-by-byte. | | **M** | `M,:` | `$OK#9a` or `$E01#` | `handle_M_command` (459). Writes bytes. ⚠️ bug: calls `send_str_packet("OK")` *inside* `send_start/end` (lines 474–476) → produces `$$OK#..#..`. | | **?** | `?` | `$T#cks` | (725). Re-sends stop reason = `send_reason()`. | | **c** | `c[]` | *(no reply — resumes)* | (781). Returns `GDBSTUB_ST_CONT`. addr ignored. | | **C** | `C[;]` | `$OK#9a` then reads 1 more cmd | `handle_C_command` (661). Replies OK, then **re-reads one command** (gdbstub.c:777) and returns CONT. | | **s** | `s[]` | *(no reply — resumes)* | `handle_s_command` (654). Sets `step_in_progress=true`, calls `esp_gdbstub_do_step`. Returns CONT. | | **S** | `S` | `$S05#` | `handle_S_command` (648). Replies `S05`. | | **k** | `k` | *(no reply)* | (758). Restores stdout->_write, sets `process_gdb_kill=true`, returns CONT. | | **P** | `P=` | `$OK#9a` or `$E02#` | `handle_P_command` (668). reg is 1–2 hex digits (gethex 4 or 8 bits). val is variable-width hex, **byte-swapped** before writing. | | **Z0** | `Z0,,` | `$OK#9a` or `$E02#` | `handle_Z0_command` (525). Software/hw breakpoint. `SOC_CPU_BREAKPOINTS_NUM` slots. | | **z0** | `z0,,` | `$OK#9a` | `handle_z0_command` (557). Remove breakpoint. | | **Z2** | `Z2,,` | `$OK#9a` or `$E02#` | `handle_Z2_command` (573). Write watchpoint (`ESP_CPU_WATCHPOINT_STORE`). | | **Z3** | `Z3,,` | `$OK#9a` or `$E02#` | `handle_Z3_command` (593). Read watchpoint (`ESP_CPU_WATCHPOINT_LOAD`). | | **Z4** | `Z4,,` | `$OK#9a` or `$E02#` | `handle_Z4_command` (612). Access watchpoint (`ESP_CPU_WATCHPOINT_ACCESS`). | | **z2/z3/z4** | `z,,` | `$OK#9a` | `handle_zx_command` (631). All three share one handler. | | **H** | `H` | `$OK#9a` or `$E00#` | `handle_H_command` (980). op=g: set thread for regs; op=c: set thread for continue (no-op). tid 0 = any, -1 = all. | | **T** | `T` | `$OK#9a` | `handle_T_command` (1031). Always replies OK (task alive). | | **qSupported** | `qSupported<:features>` | `$qSupported:...#` | (701). See §2.2. | | **qC** | `qC` | `$QC#` | `handle_qC_command` (1006). Current thread = `current_task_index+1`. | | **qfThreadInfo** | `qfThreadInfo` | `$m#` | (1048). First thread (tid = task_index+1 = 1). | | **qsThreadInfo** | `qsThreadInfo` | `$m#` or `$l#` | (1055). Subsequent; `l` when done. | | **qThreadExtraInfo** | `qThreadExtraInfo,` | `$#` | (1068). Hex-encoded "Name: State: ". | | **vCont;c** | `vCont;c[:]` | *(resume)* | (1132). Returns CONT. | | **vCont?** | `vCont?` | *(unrecognized → empty)* | IDF does **NOT** handle `vCont?`. Returns ST_ERR → `$#00`. ⚠️ | | **vCont;s** | `vCont;s` | *(not handled separately)* | Only `vCont;c` is matched (line 1132). `vCont;s` falls through to ST_ERR. | ### 2.2 qSupported response (gdbstub.c:704) ``` qSupported:multiprocess+;swbreak-;hwbreak+;qRelocInsn+;fork-events+;vfork-events+;exec-events+;vContSupported+;no-resumed+ ``` | Feature | Value | Meaning | |---------|-------|---------| | multiprocess | + | (claimed but TIDs are simple) | | swbreak | - | software breakpoints NOT supported | | hwbreak | + | hardware breakpoints supported | | qRelocInsn | + | | | fork-events | + | | | vfork-events | + | | | exec-events | + | | | vContSupported | + | vCont claimed (but only `vCont;c` actually handled) | | no-resumed | + | | ### 2.3 Stop reply format `send_reason()` (gdbstub.c:103–109): ```c esp_gdbstub_send_start(); esp_gdbstub_send_char('T'); esp_gdbstub_send_hex(s_scratch.signal, 8); // 2 hex digits esp_gdbstub_send_end(); ``` → `$T#cks` where NN = signal number (§4). No register key-value pairs (canonical GDB `T` packet allows `T NN thread:..;` but IDF sends bare `T NN`). On single-step re-entry, sends `S05` (gdbstub.c:226, 305): `esp_gdbstub_send_str_packet("S05")` → `$S05#b8`. ### 2.4 Console output (O packet) When GDB is attached, `stdout->_write` is replaced with `gdbstub__swrite` (gdbstub.c:830). It emits **O packets** in 16-byte chunks: ``` $O...# ``` Each application byte → 2 hex chars. Checksum = sum of `O` + all hex chars. --- ## 3. Register File — g/G packets ### 3.1 The `esp_gdbstub_gdb_regfile_t` structure (ESP32) Source: `src/port/xtensa/include/esp_gdbstub_arch.h:27–86`. ESP32 config: `XCHAL_NUM_AREGS=64`, all features ON (LOOPS, WINDOWED, THREADPTR, BOOLEANS, S32C1I, MAC16, DFP_ACCEL, FP), `GDBSTUB_EXTRA_TIE_SIZE=0`. **Field order and GDB register indices** (computed from the struct + ESP32 core-isa.h feature flags): | GDB Reg# | Byte Offset | Field | Size | Notes | |----------|------------|-------|------|-------| | 0 | 0 | `pc` | 4 | | | 1–64 | 4 | `a[0..63]` | 64×4=256 | a[0]=a0 … a[63]=a63 | | 65 | 260 | `lbeg` | 4 | LOOPS | | 66 | 264 | `lend` | 4 | LOOPS | | 67 | 268 | `lcount` | 4 | LOOPS | | 68 | 272 | `sar` | 4 | | | 69 | 276 | `windowbase` | 4 | WINDOWED | | 70 | 280 | `windowstart` | 4 | WINDOWED | | 71 | 284 | `configid0` | 4 | read via RSR | | 72 | 288 | `configid1` | 4 | read via RSR | | 73 | 292 | `ps` | 4 | | | 74 | 296 | `threadptr` | 4 | THREADPTR | | 75 | 300 | `br` | 4 | BOOLEANS | | 76 | 304 | `scompare1` | 4 | S32C1I | | 77 | 308 | `acclo` | 4 | MAC16 | | 78 | 312 | `acchi` | 4 | MAC16 | | 79 | 316 | `m0` | 4 | MAC16 | | 80 | 320 | `m1` | 4 | MAC16 | | 81 | 324 | `m2` | 4 | MAC16 | | 82 | 328 | `m3` | 4 | MAC16 | | 83 | 332 | `expstate` | 4 | DFP_ACCEL | | 84 | 336 | `f64r_lo` | 4 | DFP_ACCEL | | 85 | 340 | `f64r_hi` | 4 | DFP_ACCEL | | 86 | 344 | `f64s` | 4 | DFP_ACCEL | | 87–102 | 348 | `f[0..15]` | 16×4=64 | FP (f0–f15) | | 103 | 412 | `fcr` | 4 | FP | | 104 | 416 | `fsr` | 4 | FP | | **Total** | **420** | | **105 regs** | | **→ g-packet payload = 105 registers × 8 hex chars = 840 hex chars** (+ `$` + `#XX` = 844 wire bytes). For **ESP32-S2/S3** (`GDBSTUB_EXTRA_TIE_SIZE=1`): add `tie[1]` at the end → 106 regs, 424 bytes, 848 hex chars. ### 3.2 Byte-swap in handle_g_command (gdbstub.c:388–396) ```c static uint32_t gdbstub_hton(uint32_t i) { return __builtin_bswap32(i); // reverse 4 bytes } static void handle_g_command(...) { uint32_t *p = (uint32_t *) &s_scratch.regfile; esp_gdbstub_send_start(); for (int i = 0; i < sizeof(s_scratch.regfile) / sizeof(*p); ++i) { esp_gdbstub_send_hex(gdbstub_hton(*p++), 32); } esp_gdbstub_send_end(); } ``` **Why big-endian on a little-endian target?** The GDB Remote Serial Protocol specifies that **register values in g/G packets are transmitted target-byte-order but GDB interprets each register as a sequence of bytes where the first byte is the most-significant** — effectively big-endian per-register. Xtensa is little-endian in memory. So the stub byte-swaps each 32-bit register (`__builtin_bswap32`) before emitting it MSB-first via `send_hex(val, 32)`. Example: register `pc = 0x400D1234` (little-endian memory: `34 12 0D 40`). After `gdbstub_hton`: `0x4012340D`? No — `__builtin_bswap32(0x400D1234) = 0x34120D40`. Then `send_hex(0x34120D40, 32)` emits `"34120d40"`. GDB receives `"34120d40"` and reconstructs the register as `0x34120D40`… **Correction/precision**: `send_hex(val, 32)` emits the value MSB-first: `hex_chars[(val>>28)&0xf]` … `hex_chars[(val>>0)&0xf]`. For `pc=0x400D1234`: `bswap`→`0x34120D40`, emit → `"34120d40"`. GDB reads this as register bytes `0x34,0x12,0x0D,0x40` and (for a big-endian-per- register interpretation) reconstructs `0x34120D40`. **This is correct**: the *wire* representation of pc is `400d1234` is WRONG; the wire is the byte-swapped value so that GDB's "first hex pair = MSB" convention yields the true value. I.e. IDF transmits registers in **big-endian byte order on the wire**, matching `xtensa-esp32-elf-gdb`'s expectation. The `gdbstub_hton` ("host to network") name confirms this: network byte order = big-endian. **Net rule**: transmit each 32-bit register as 8 hex chars, most-significant byte first. On a little-endian target, that requires `bswap32` before formatting. ### 3.3 G command (handle_G_command, gdbstub.c:399–406) Mirror of g: reads 105 × 32-bit hex values, each `gdbstub_hton`-swapped back to little-endian before storing: ```c *p++ = gdbstub_hton(esp_gdbstub_gethex(&cmd, 32)); ``` Replies `$OK#9a`. ### 3.4 P command byte-swap (handle_P_command, gdbstub.c:668–697) GDB sends `P=` where `` is 8 hex chars, big-endian (MSB first), same as g-packet. `gethex` returns it as a `uint32_t` with the MSB in the high bits — i.e. already the *logical* value. But IDF does an **explicit manual byte-swap** (lines 683–690): ```c uint8_t *addr_ptr = (uint8_t *)&addr; // logical value uint32_t p_address = 0; uint8_t *p_addr_ptr = (uint8_t *)&p_address; p_addr_ptr[3] = addr_ptr[0]; // byte 0 -> byte 3 p_addr_ptr[2] = addr_ptr[1]; p_addr_ptr[1] = addr_ptr[2]; p_addr_ptr[0] = addr_ptr[3]; ``` This is **another bswap32** (identical to `gdbstub_hton`). So the value is double-processed: `gethex` already gives the logical value; this swap converts it *back* to little-endian-in-register form for writing to the frame. (Functionally it undoes the implicit MSB-first interpretation.) Then `esp_gdbstub_set_register(frame, reg_index, p_address)` writes it. ### 3.5 set_register (gdbstub_xtensa.c:352–427) — P reg_index mapping | reg_index | Target | |-----------|--------| | 0 | `frame->pc` | | 1–16 | `(&frame->a0)[reg_index-1]` = a0..a15 | | 17–27 | *(would write past a15 into sar/exccause/etc. — **latent bug**, GDB doesn't send these)* | | 87–102 | f0..f15 (only if CPENABLE≠0) | | 103 | FCR | | 104 | FSR | --- ## 4. Signal Mapping Source: `gdbstub_xtensa.c:261–268`. ```c int esp_gdbstub_get_signal(const esp_gdbstub_frame_t *frame) { const char exccause_to_signal[] = {4, 31, 11, 11, 2, 6, 8, 0, 6, 7, 0, 0, 7, 7, 7, 7}; if (frame->exccause >= sizeof(exccause_to_signal)) { return 11; // SIGSEGV default for unknown high causes } return (int) exccause_to_signal[frame->exccause]; } ``` **The table** (index = EXCCAUSE): | EXCCAUSE | Name | Signal | Signal Name | |----------|------|--------|-------------| | 0 | IllegalInstruction | 4 | SIGILL | | 1 | Syscall (InstFetch) | 31 | SIGSYS | | 2 | InstFetchError | 11 | SIGSEGV | | 3 | LoadStoreError | 11 | SIGSEGV | | 4 | Level1Interrupt | 2 | SIGINT | | 5 | Alloca (window overflow) | 6 | SIGABRT | | 6 | Syscall (Integer) | 8 | SIGFPE | | 7 | Level1Int (coprocessor) | 0 | (none) | | 8 | Privileged (window underflow) | 6 | SIGABRT | | 9 | PifDataError | 7 | SIGBUS | | 10 | LoadStoreAlignment | 0 | (none) | | 11 | (reserved) | 0 | | | 12 | InstPIFDataErr | 7 | SIGBUS | | 13 | LoadStorePIFDataErr | 7 | SIGBUS | | 14 | InstPIFAddrErr | 7 | SIGBUS | | 15 | LoadStorePIFAddrErr | 7 | SIGBUS | | ≥16 | (any other) | 11 | SIGSEGV | For **debug interrupt** entry (`gdbstub_handle_debug_int`, gdbstub.c:296), signal is **hardcoded to 5** (SIGTRAP) (gdbstub.c:310): ```c s_scratch.signal = 5; /* esp_gdbstub_get_db_signal(regs_frame); */ ``` For **Ctrl-C / UART break** entry, signal = `esp_gdbstub_get_signal(frame)` (gdbstub.c:236) — typically the interrupt cause. --- ## 5. vCont Support ### 5.1 What GDB sends | Packet | Meaning | IDF handling | |--------|---------|--------------| | `vCont?` | "What actions do you support?" | **NOT HANDLED** → `$#00` (empty). ⚠️ IDF advertises `vContSupported+` in qSupported but doesn't answer the probe. GDB falls back to `c`/`s`. | | `vCont;c:` | continue thread | **Handled** (line 1132): `strncmp("vCont;c", cmd, 7)==0` → return `GDBSTUB_ST_CONT`. tid ignored. | | `vCont;c` | continue all | **Handled** (same). | | `vCont;s:` | step thread | **NOT handled separately** → falls through to ST_ERR. | | `vCont;s` | step all | **NOT handled**. | ### 5.2 Required stub replies - `vCont?` → canonical reply is `vCont;c;s` but **IDF sends nothing useful**. For UniversalisOS, to be safe, reply `$vCont;c;s#cks`. - `vCont;c` / `vCont;c:` → no reply packet; resume target (return CONT). - `vCont;s` / `vCont;s:` → no reply; single-step then send `S05`/`T05` on next trap. (IDF doesn't implement this via vCont; GDB uses `s` instead.) ### 5.3 Why it still works `xtensa-esp32-elf-gdb` sends `vCont?` early; gets empty; falls back to the legacy `Hc`, `c`, `s` packets which IDF fully supports. --- ## 6. Memory Validity Source: `src/port/xtensa/include/esp_gdbstub_memory_regions.h:16–21`. ```c static inline bool is_valid_memory_region(intptr_t addr) { return (!is_transport_memory_region(addr)) && addr >= 0x20000000 && addr < 0x80000000; } ``` ### 6.1 Valid address range (Xtensa ESP32) ``` 0x20000000 ≤ addr < 0x80000000 ``` This single range covers: | Range | Region | |-------|--------| | 0x3FF8_0000 – 0x3FFF_FFFF | DRAM / data | | 0x4000_0000 – 0x400D_FFFF | IRAM / instruction (flash ICache + SRAM) | | 0x3F40_0000 – 0x3F7F_FFFF | flash data (DROM via cache) | | 0x3FF8_0000 – 0x3FFF_FFFF | peripheral registers (EXCEPT transport) | | 0x6000_0000 – 0x7FFF_FFFF | APB peripheral registers | The check deliberately **excludes** the UART register block (to prevent GDB from reading the FIFO and disturbing the transport): ```c // esp_gdbstub_memory_regions_common.h:21–30 static inline bool is_transport_memory_region(intptr_t addr) { return addr >= REG_UART_BASE(CONFIG_ESP_CONSOLE_UART_NUM) && addr <= REG_UART_BASE(CONFIG_ESP_CONSOLE_UART_NUM) + sizeof(UART0); } ``` For UART0: `REG_UART_BASE(0) = 0x60000000`, `sizeof(UART0)` ≈ 0x80. So `0x60000000–0x6000007F` is excluded from m/M access. (For USB-Serial-JTAG, the USB-SJTAG register range is excluded instead.) ### 6.2 Memory access implementation `esp_gdbstub_readmem` (gdbstub.c:408–417) and `esp_gdbstub_writemem` (419–436) do **byte-granular read/write via 32-bit word access**: ```c // read byte at addr: uint32_t val_aligned = *(uint32_t *)(addr & ~3); uint32_t shift = (addr & 3) * 8; return (val_aligned >> shift) & 0xff; ``` Validity is checked on **both** the start and end address of an m/M range (gdbstub.c:445–448, 466–469): `readmem(addr) < 0 || readmem(addr+size-1) < 0`. If either fails → `$E01#`. After a write on Xtensa, an **ISYNC** is executed (gdbstub.c:431–433): ```c asm volatile("ISYNC\nISYNC\n"); ``` to flush the instruction pipeline (needed if writing code/instruction memory). --- ## 7. Xtensa gdbstub-entry.S — Interrupt Entry & Frame Source: `src/port/xtensa/gdbstub-entry.S` (59 lines, section `.iram1`). ### 7.1 The entry routine `esp_gdbstub_int` This is registered as the **UART0 interrupt handler** via `esp_intr_alloc` (gdbstub.c:370). It runs at interrupt level and must save/restore a full Xtensa exception frame. ```asm .section .iram1, "ax" ; must be in IRAM (runs with cache constraints) .align 4 esp_gdbstub_int: mov a0, sp ; save current SP into a0 addi sp, sp, -XT_STK_FRMSZ ; allocate full frame on stack s32i a0, sp, XT_STK_EXIT ; frame.exit = old SP (dispatch return) s32i a0, sp, XT_STK_A0 ; frame.a0 = old SP #if XCHAL_HAVE_WINDOWED s32e a0, sp, -12 ; base-save slot (backtrace debug) #endif rsr a0, PS ; read PS (processor state) s32i a0, sp, XT_STK_PS ; frame.ps = interruptee's PS rsr a0, EPC_1 ; read EPC1 (return PC for level-1 int) s32i a0, sp, XT_STK_PC ; frame.pc = interruptee's PC #if XCHAL_HAVE_WINDOWED s32e a0, sp, -16 ; base-save slot #endif s32i a12, sp, XT_STK_A12 ; save a12, a13 (callee-saved, needed by s32i a13, sp, XT_STK_A13 ; _xt_context_save-style routines) rsr a0, EXCCAUSE ; save exception cause s32i a0, sp, XT_STK_EXCCAUSE rsr a0, EXCVADDR ; save exception vaddr s32i a0, sp, XT_STK_EXCVADDR rsr a0, EXCSAVE_1 ; recover interruptee's true a0 s32i a0, sp, XT_STK_A0 ; (overwrites the old-SP value) rsr a6, excsave1 ; load arg0 = frame pointer (from vector) rsr a3, EPS ; EPS = saved PS for this level s32i a3, sp, XT_STK_PS ; store PS (again, from EPS this time) movi a3, gdbstub_handle_uart_int ; C handler callx0 a3 ; call gdbstub_handle_uart_int(frame) l32i a0, sp, XT_STK_EXIT ; restore old SP addi sp, sp, XT_STK_FRMSZ ret ; return from interrupt ``` ### 7.2 Frame layout — `XtExcFrame` (= `esp_gdbstub_frame_t`) From `xtensa_context.h:122–164`. ESP32 (windowed, LOOPS, no CALL0): | Offset | Field | Source | Notes | |--------|-------|--------|-------| | 0x00 | `exit` | XT_STK_EXIT | dispatch return addr (saved old SP here) | | 0x04 | `pc` | XT_STK_PC | from EPC1 (level-1 int) | | 0x08 | `ps` | XT_STK_PS | from PS/EPS | | 0x0C | `a0` | XT_STK_A0 | interruptee's a0 (from EXCSAVE_1) | | 0x10 | `a1` | XT_STK_A1 | SP before interrupt (= old SP) | | 0x14 | `a2` | | | | 0x18 | `a3` | | | | 0x1C | `a4` | | | | 0x20 | `a5` | | | | 0x24 | `a6` | | | | 0x28 | `a7` | | | | 0x2C | `a8` | | | | 0x30 | `a9` | | | | 0x34 | `a10` | | | | 0x38 | `a11` | | | | 0x3C | `a12` | XT_STK_A12 | saved explicitly in entry.S | | 0x40 | `a13` | XT_STK_A13 | saved explicitly in entry.S | | 0x44 | `a14` | | | | 0x48 | `a15` | | | | 0x4C | `sar` | XT_STK_SAR | | | 0x50 | `exccause` | XT_STK_EXCCAUSE | from RSR | | 0x54 | `excvaddr` | XT_STK_EXCVADDR | from RSR | | 0x58 | `lbeg` | | LOOPS | | 0x5C | `lend` | | LOOPS | | 0x60 | `lcount` | | LOOPS | | 0x64 | `tmp0` | | (windowed ABI) | | 0x68 | `tmp1` | | | | 0x6C | `tmp2` | | | | **0x70** | *(end of XtExcFrame)* | `XtExcFrameSize` | | `XT_STK_FRMSZ = ALIGNUP(0x10, XtExcFrameSize) + 0x20`. For ESP32 this is typically **0x90** (144 bytes) including the base-save area and alignment padding below the frame. **Stack growth**: the frame is allocated by `addi sp, sp, -XT_STK_FRMSZ`. The lowest addresses hold `exit, pc, ps, a0...`. `a0` (frame offset 0x0C) holds the interruptee's return address (windowed call chain). ### 7.3 Frame → regfile conversion (`esp_gdbstub_frame_to_regfile`) (gdbstub_xtensa.c:82–143). Key transformations: 1. **PC normalization**: `esp_cpu_pc_to_addr(frame->pc)` clears the top 2 bits (windowed-call-size encoding). If PC invalid (not executable or top bits 0), replaced with `_invalid_pc_placeholder`. 2. **a0–a15** copied directly from frame; **a16–a63** = `0xDEADBEEF` (only the active window's 16 registers are saved in the frame). 3. **windowbase = 0, windowstart = 0x1** (forced — presents the saved window as window 0 to GDB). 4. **configid0/configid1** read live from RSR. 5. **ps**: if UM (user-mode) bit set, clear EXCM; else pass through. 6. **FPU regs** (f0–f15, fcr, fsr): if CPENABLE≠0, read live from FPU regs; else read from the TCB's saved coprocessor state. 7. **a[0]**: if bit 27 set, treat as PC-relative → `esp_cpu_pc_to_addr`. 8. **a[1]** (SP): if not sane, replaced with `0xDEADBEEF`. ### 7.4 Step mechanism (Xtensa ICOUNT) (gdbstub_xtensa.c:319–331). Single-step uses the hardware ICOUNT register: ```c uint32_t level = s_scratch.regfile.ps & 0x7; // current int level level += 1; WSR(ICOUNTLEVEL, level); // count instructions at > current level WSR(ICOUNT, -2); // count 2 instructions then trap (ICOUNT overflow) ``` On the next debug trap, `gdbstub_handle_debug_int` fires, clears the step (`ICOUNT=0; ICOUNTLEVEL=0`), and sends `S05`. `step_in_progress` flag prevents double-send. ### 7.5 Transport — UART getchar/putchar (gdbstub_transport.c) Polled, blocking, no DMA: - **getchar** (line 78–87): spin until `uart_ll_get_rxfifo_len() > 0`, read 1 byte. - **putchar** (line 89–96): spin until TX FIFO has ≥127 bytes free, write 1 byte. - **flush** (line 98–105): spin until `uart_ll_is_tx_idle()`. - **getfifo** (line 108–124, runtime mode): drain entire RX FIFO, return 1 if any byte == **0x03 (Ctrl-C)**. This is the async-break detection: GDB sends a bare 0x03 (not in a packet) to interrupt the running target. --- ## Appendix A — Minimal RSP State Machine for UniversalisOS To replicate IDF behavior exactly, a bare-metal stub needs: 1. **UART**: polled RX/TX at the configured baud (IDF uses the console UART). Detect bare `0x03` for async break. 2. **Entry**: save an `XtExcFrame`-compatible frame (§7.2) on the debug exception / UART interrupt. Call the C command loop with a frame pointer. 3. **Main loop** (gdbstub.c:81–97 / 254–278): ```c send_reason(); // $T# while (1) { int res = read_command(&cmd, &len); if (res == '-') { send_reason(); continue; } // NAK from GDB? re-send stop if (res > 0) continue; // stray char (GDB's +/-) if (res == ST_ERR) { send_str_packet("E01"); continue; } res = handle_command(cmd, len); if (res == ST_ERR) send_str_packet(NULL); // $#00 if (res == ST_CONT) break; // resume } ``` 4. **Registers**: 105-entry big-endian g/G (§3). Copy frame→regfile with DEADBEEF fill for a16–a63. 5. **Memory**: validate `0x20000000 ≤ addr < 0x80000000`, exclude UART regs. 6. **Breakpoints**: use IBREAKx/DBREAKx via WSR (IDF wraps this in `esp_cpu_set_breakpoint`). 7. **Step**: ICOUNT=-2, ICOUNTLEVEL=curlevel+1. ## Appendix B — Known IDF Quirks (replicate or avoid) 1. **M command double-`$`** (gdbstub.c:474–476): wraps `send_str_packet` inside `send_start/end`, producing malformed `$$OK#..#..`. GDB tolerates it. A clean stub should just `send_str_packet("OK")`. 2. **vCont? unhandled** but `vContSupported+` advertised. Safe to add a real `vCont;c;s` reply. 3. **P reg_index 17–27** writes out of frame bounds. A safe stub should clamp to the 16 saved AR registers. 4. **qSupported `swbreak-`**: software breakpoints (patching instructions) are NOT supported — only hardware IBREAK. GDB will only set `Z0` at addresses and rely on the stub's hardware BP. 5. **No `vCont;s` / no `vKill` / no `X` (binary write)**: GDB falls back to legacy packets. 6. **ACK policy**: stub ignores GDB's `+`/`-` entirely (never resends on NAK). --- *Document generated from ESP-IDF v5.5.4 source. All line numbers and byte offsets verified against the actual files.*