From 92c7609fc5a0c2f5ff285f166a85f5d8f349aad2 Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Tue, 13 Oct 2026 20:03:45 +0200 Subject: [PATCH] Add standaolne module gdb-gateway --- CHANGELOG.md | 1 + Makefile.platform.sample | 1 + armsrc/Standalone/Makefile.hal | 5 +- armsrc/Standalone/Makefile.inc | 4 + armsrc/Standalone/StandAloneOption.cmake | 5 +- armsrc/Standalone/StandAloneSource.cmake | 4 + armsrc/Standalone/gdb_gateway.c | 552 +++++++++++++++++++++++ armsrc/Standalone/readme.md | 1 + doc/standalone/gdb_gateway.md | 61 +++ 9 files changed, 632 insertions(+), 2 deletions(-) create mode 100644 armsrc/Standalone/gdb_gateway.c create mode 100644 doc/standalone/gdb_gateway.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a981101e..f5c284c67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ All notable changes to this project will be documented in this file. This project uses the changelog in accordance with [keepchangelog](http://keepachangelog.com/). Please use this to write notable changes, which is not the same as git commit log... ## [unreleased][unreleased] +- Added standalone mode `GDB_GATEWAY` (PM5): GDB Remote Serial Protocol server on the USB CDC link, flash and SRAM access, no probe needed (@nieldk) - Fixed `lf t55xx detect -p` - the password is no longer sent to a tag whose PWD bit could not be confirmed, which could set it and destroy the tag's data; pass `-o` to override (@mfcarroll) - Fixed `hf mf hardnested`/`hf mf autopwn` on PM5 with hardened MIFARE Classic EV1 (#3595, #3664) (@ByteOrderMarc) - Added PM5 FPGA open source (@xianglin1998) diff --git a/Makefile.platform.sample b/Makefile.platform.sample index d912ce9d6..7fd7f22fa 100644 --- a/Makefile.platform.sample +++ b/Makefile.platform.sample @@ -24,6 +24,7 @@ PLATFORM=PM3RDV4 #PLATFORM_EXTRAS=BWM #PLATFORM_EXTRAS=NO_LOWBATT_SHUTDOWN #PLATFORM_EXTRAS=NO_LOWBATT_BEEP +#STANDALONE=GDB_GATEWAY # If you want more than one PLATFORM_EXTRAS option, separate them by spaces: #PLATFORM_EXTRAS=BTADDON diff --git a/armsrc/Standalone/Makefile.hal b/armsrc/Standalone/Makefile.hal index 906f68cc9..ca279d8d8 100644 --- a/armsrc/Standalone/Makefile.hal +++ b/armsrc/Standalone/Makefile.hal @@ -149,13 +149,16 @@ define KNOWN_STANDALONE_DEFINITIONS | DANKARMULTI | Load multiple standalone modes. | | | - Daniel Karling | +-----------------------------------------------------------+ +| GDB_GATEWAY | Read-only GDB server over USB CDC | +| | (PM5) | ++-----------------------------------------------------------+ endef STANDALONE_MODES := LF_SKELETON STANDALONE_MODES += LF_EM4100EMUL LF_EM4100RSWB LF_EM4100RSWW LF_EM4100RWC LF_HIDBRUTE LF_HIDFCBRUTE LF_ICEHID LF_MULTIHID LF_NEDAP_SIM LF_NEXID LF_PROXBRUTE LF_PROX2BRUTE LF_SAMYRUN LF_THAREXDE STANDALONE_MODES += HF_14ASNIFF HF_14BSNIFF HF_15SIM HF_15SNIFF HF_AVEFUL HF_BOG HF_CARDHOPPER HF_COLIN HF_CRAFTBYTE HF_DOEGOX_AUTH0 HF_DOEGOX_COMMIT HF_EMVPNG HF_ICECLASS HF_LEGIC HF_LEGIC_RDV4 HF_LEGICSIM HF_MATTYRUN HF_MFCSIM HF_MSDSAL HF_REBLAY HF_ST25_TEAROFF HF_TCPRST HF_TMUDFORD HF_UNISNIFF HF_YOUNG -STANDALONE_MODES += DANKARMULTI +STANDALONE_MODES += DANKARMULTI GDB_GATEWAY STANDALONE_MODES_REQ_BT := HF_CARDHOPPER HF_REBLAY STANDALONE_MODES_REQ_SMARTCARD := STANDALONE_MODES_REQ_FLASH := LF_HIDFCBRUTE LF_ICEHID LF_NEXID LF_THAREXDE HF_BOG HF_COLIN HF_ICECLASS HF_LEGIC_RDV4 HF_LEGICSIM HF_MFCSIM HF_ST25_TEAROFF diff --git a/armsrc/Standalone/Makefile.inc b/armsrc/Standalone/Makefile.inc index 5c05371d8..422fe878d 100644 --- a/armsrc/Standalone/Makefile.inc +++ b/armsrc/Standalone/Makefile.inc @@ -89,6 +89,10 @@ endif ifneq (,$(findstring WITH_STANDALONE_HF_15SNIFF,$(APP_CFLAGS))) SRC_STANDALONE = hf_15sniff.c endif +# WITH_STANDALONE_GDB_GATEWAY +ifneq (,$(findstring WITH_STANDALONE_GDB_GATEWAY,$(APP_CFLAGS))) + SRC_STANDALONE = gdb_gateway.c +endif # WITH_STANDALONE_HF_15SIM ifneq (,$(findstring WITH_STANDALONE_HF_15SIM,$(APP_CFLAGS))) SRC_STANDALONE = hf_15sim.c diff --git a/armsrc/Standalone/StandAloneOption.cmake b/armsrc/Standalone/StandAloneOption.cmake index 70823139b..3850465b8 100644 --- a/armsrc/Standalone/StandAloneOption.cmake +++ b/armsrc/Standalone/StandAloneOption.cmake @@ -118,6 +118,9 @@ | HF_EMVPNG | Read and emulate EMV Visa cards | | | - Davi Mikael (Penegui) | +----------------------------------------------------------+ +| GDB_GATEWAY | Read-only GDB server over USB CDC | +| | (PM5) | ++----------------------------------------------------------+ ]] # Default standalone if no standalone specified @@ -137,7 +140,7 @@ set(STANDALONE_MODES HF_14ASNIFF HF_14BSNIFF HF_15SNIFF HF_15SIM HF_AVEFUL HF_BOG HF_CARDHOPPER HF_COLIN HF_CRAFTBYTE HF_ICECLASS HF_LEGIC HF_LEGIC_RDV4 HF_LEGICSIM HF_MATTYRUN HF_MFCSIM HF_MSDSAL HF_REBLAY - HF_ST25_TEAROFF HF_TCPRST HF_TMUDFORD HF_UNISNIFF HF_YOUNG HF_EMVPNG DANKARMULTI) + HF_ST25_TEAROFF HF_TCPRST HF_TMUDFORD HF_UNISNIFF HF_YOUNG HF_EMVPNG DANKARMULTI GDB_GATEWAY) # List of modes that require bluetooth set(STANDALONE_MODES_REQ_BT HF_CARDHOPPER HF_REBLAY) # List of modes that require smartcard diff --git a/armsrc/Standalone/StandAloneSource.cmake b/armsrc/Standalone/StandAloneSource.cmake index 48ad195ef..b7fd420a4 100644 --- a/armsrc/Standalone/StandAloneSource.cmake +++ b/armsrc/Standalone/StandAloneSource.cmake @@ -76,6 +76,10 @@ if ("${APP_CFLAGS}" MATCHES "WITH_STANDALONE_HF_15SNIFF") set(SRC_STANDALONE "${DIR_STANDALONE}/hf_15sniff.c") endif () +if ("${APP_CFLAGS}" MATCHES "WITH_STANDALONE_GDB_GATEWAY") + set(SRC_STANDALONE "${DIR_STANDALONE}/gdb_gateway.c") +endif () + if ("${APP_CFLAGS}" MATCHES "WITH_STANDALONE_HF_15SIM") set(SRC_STANDALONE "${DIR_STANDALONE}/hf_15sim.c") endif () diff --git a/armsrc/Standalone/gdb_gateway.c b/armsrc/Standalone/gdb_gateway.c new file mode 100644 index 000000000..1d1b8e7a9 --- /dev/null +++ b/armsrc/Standalone/gdb_gateway.c @@ -0,0 +1,552 @@ +//----------------------------------------------------------------------------- +// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// See LICENSE.txt for the text of the license. +//----------------------------------------------------------------------------- +// Standalone GDB gateway: GDB Remote Serial Protocol over the USB CDC link. +// Reads flash and SRAM, writes SRAM and flash (gdb 'load'), no execution control. +// Flash writes are limited to the space above the running image; the bootrom is +// never writable. 'monitor unlock-app' opens the app area, 'monitor lock' closes it. +// Start with 'hw standalone', quit the client, then: +// arm-none-eabi-gdb -ex 'target remote /dev/ttyACM0' +// Leave with 'monitor exit' or a button press. +//----------------------------------------------------------------------------- + +#include +#include +#include +#include + +#define GDB_PKT_MAX 0x400u // advertised PacketSize +#define GDB_MEM_MAX ((GDB_PKT_MAX - 8u) / 2u) +#define GDB_NREGS 17u // r0-r12, sp, lr, pc, xpsr + +#define GW_FLASH_BASE 0x08000000u +#define GW_FLASH_SIZE 0x00100000u +#define GW_APP_BASE 0x08004000u // bootrom below this is never writable +#define GW_SECTOR 0x800u // 1 MB part: 2 KB sectors +#define GW_SRAM_BASE 0x20000000u +#define GW_SRAM_SIZE 0x00080000u // 512K, set by Extend_SRAM() in the bootrom + +typedef struct { + void (*tx)(const uint8_t *data, size_t len); + // Copy up to len bytes from addr; return bytes copied, 0 if addr is unreadable. + size_t (*mem_read)(uint32_t addr, uint8_t *out, size_t len); + // The engine checks the address window before calling these. + bool (*ram_write)(uint32_t addr, const uint8_t *data, size_t len); + bool (*flash_erase)(uint32_t sector_addr); + bool (*flash_write)(uint32_t addr, const uint8_t *data, size_t len); + uint32_t app_end; // first sector above the running image +} gdb_ops_t; + +static const gdb_ops_t *s_ops; + +static const char TARGET_XML[] = + "" + "" + "" + "armv7e-m" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; + +static const char MEMORY_MAP_XML[] = + "" + "" + "" + "" + "0x800" + "" + ""; + +static const char HEXD[] = "0123456789abcdef"; + +typedef enum { ST_IDLE, ST_DATA, ST_CS1, ST_CS2 } rx_state_t; + +static rx_state_t s_state; +static uint8_t s_rx[GDB_PKT_MAX + 1]; +static uint16_t s_rxlen; +static uint8_t s_sum; +static int s_cs_hi; +static bool s_ovf; +static bool s_ack_pending; +static bool s_exit; +static bool s_unlocked; // app area writable + +static uint8_t s_out[GDB_PKT_MAX]; // reply payload +static uint8_t s_frame[GDB_PKT_MAX + 5]; // '+' '$' payload '#' cs cs +static uint16_t s_frame_len; +static uint16_t s_frame_off; // start of the resendable part (after any ack) + +static int hexval(uint8_t c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; +} + +static bool parse_hex(const char **p, const char *end, uint32_t *v) { + uint32_t r = 0; + int n = 0; + while (*p < end && hexval((uint8_t) * *p) >= 0) { + if (n++ >= 8) return false; + r = (r << 4) | (uint32_t)hexval((uint8_t) * *p); + (*p)++; + } + *v = r; + return n > 0; +} + +static bool starts_with(const char *s, size_t len, const char *pre) { + size_t n = strlen(pre); + return len >= n && memcmp(s, pre, n) == 0; +} + +static void send_payload(const uint8_t *pl, size_t len) { + size_t j = 0; + uint8_t cs = 0; + if (len > GDB_PKT_MAX) len = GDB_PKT_MAX; + if (s_ack_pending) { + s_frame[j++] = '+'; + s_ack_pending = false; + } + s_frame_off = (uint16_t)j; + s_frame[j++] = '$'; + for (size_t i = 0; i < len; i++) { + s_frame[j++] = pl[i]; + cs = (uint8_t)(cs + pl[i]); + } + s_frame[j++] = '#'; + s_frame[j++] = (uint8_t)HEXD[cs >> 4]; + s_frame[j++] = (uint8_t)HEXD[cs & 0xF]; + s_frame_len = (uint16_t)j; + s_ops->tx(s_frame, j); +} + +static void send_str(const char *s) { + send_payload((const uint8_t *)s, strlen(s)); +} + +static void send_err(uint8_t e) { + char b[4] = { 'E', HEXD[e >> 4], HEXD[e & 0xF], 0 }; + send_str(b); +} + +static bool flash_window_ok(uint32_t addr, uint32_t len) { + uint32_t floor = s_unlocked ? GW_APP_BASE : s_ops->app_end; + uint32_t end = addr + len; + return len && addr >= floor && end > addr && end <= GW_FLASH_BASE + GW_FLASH_SIZE; +} + +static bool sram_ok(uint32_t addr, uint32_t len) { + uint32_t end = addr + len; + return len && addr >= GW_SRAM_BASE && end > addr && end <= GW_SRAM_BASE + GW_SRAM_SIZE; +} + +// Undo gdb binary escaping ('}' then byte ^ 0x20). Returns -1 on a dangling '}'. +static int unescape(const uint8_t *in, size_t n, uint8_t *out) { + size_t o = 0; + for (size_t i = 0; i < n; i++) { + if (in[i] == '}') { + if (++i >= n) return -1; + out[o++] = in[i] ^ 0x20; + } else { + out[o++] = in[i]; + } + } + return (int)o; +} + +// Serve a qXfer document slice for "OFF,LEN". +static void send_qxfer(const char *doc, const char *p, const char *end) { + uint32_t off, len; + if (!parse_hex(&p, end, &off) || p >= end || *p++ != ',' || !parse_hex(&p, end, &len)) { + send_err(0x16); + return; + } + size_t total = strlen(doc); + if (off >= total) { + send_str("l"); + return; + } + size_t n = total - off; + if (n > len) n = len; + if (n > GDB_PKT_MAX - 1u) n = GDB_PKT_MAX - 1u; + s_out[0] = (off + n < total) ? 'm' : 'l'; + memcpy(&s_out[1], doc + off, n); + send_payload(s_out, n + 1); +} + +static void do_mem_read(const char *p, const char *end) { + uint32_t addr, len; + if (!parse_hex(&p, end, &addr) || p >= end || *p++ != ',' || !parse_hex(&p, end, &len)) { + send_err(0x16); + return; + } + if (len > GDB_MEM_MAX) len = GDB_MEM_MAX; + uint8_t tmp[GDB_MEM_MAX]; + size_t n = len ? s_ops->mem_read(addr, tmp, len) : 0; + if (len && n == 0) { + send_err(0x14); // EFAULT + return; + } + for (size_t i = 0; i < n; i++) { + s_out[2 * i] = (uint8_t)HEXD[tmp[i] >> 4]; + s_out[2 * i + 1] = (uint8_t)HEXD[tmp[i] & 0xF]; + } + send_payload(s_out, 2 * n); +} + +static void do_mem_write(const char *p, const char *end) { + uint32_t addr, len; + if (!parse_hex(&p, end, &addr) || p >= end || *p++ != ',' || !parse_hex(&p, end, &len) || + p >= end || *p++ != ':' || len > GDB_MEM_MAX || (size_t)(end - p) != 2 * (size_t)len) { + send_err(0x16); + return; + } + if (len == 0) { + send_str("OK"); + return; + } + uint8_t tmp[GDB_MEM_MAX]; + for (uint32_t i = 0; i < len; i++) { + int hi = hexval((uint8_t)p[2 * i]), lo = hexval((uint8_t)p[2 * i + 1]); + if (hi < 0 || lo < 0) { + send_err(0x16); + return; + } + tmp[i] = (uint8_t)((hi << 4) | lo); + } + if (!sram_ok(addr, len)) { + send_err(0x02); // flash goes through vFlashWrite + return; + } + send_str(s_ops->ram_write(addr, tmp, len) ? "OK" : "E01"); +} + +static void do_vflash(const char *s, size_t len) { + const char *end = s + len; + uint32_t addr, n; + if (starts_with(s, len, "vFlashErase:")) { + const char *p = s + strlen("vFlashErase:"); + if (!parse_hex(&p, end, &addr) || p >= end || *p++ != ',' || !parse_hex(&p, end, &n) || + n == 0 || (addr % GW_SECTOR) || (n % GW_SECTOR)) { + send_err(0x16); + return; + } + if (!flash_window_ok(addr, n)) { + send_err(0x02); + return; + } + for (uint32_t a = addr; a < addr + n; a += GW_SECTOR) { + if (!s_ops->flash_erase(a)) { + send_err(0x01); + return; + } + } + send_str("OK"); + } else if (starts_with(s, len, "vFlashWrite:")) { + const char *p = s + strlen("vFlashWrite:"); + if (!parse_hex(&p, end, &addr) || p >= end || *p++ != ':') { + send_err(0x16); + return; + } + uint8_t data[GDB_PKT_MAX]; + int dn = unescape((const uint8_t *)p, (size_t)(end - p), data); + if (dn < 0) { + send_err(0x16); + return; + } + if (!flash_window_ok(addr, (uint32_t)dn)) { + send_err(0x02); + return; + } + send_str(dn == 0 || s_ops->flash_write(addr, data, (size_t)dn) ? "OK" : "E01"); + } else if (starts_with(s, len, "vFlashDone")) { + send_str("OK"); + } else { + send_str(""); + } +} + +// monitor : reply is the hex-encoded console text. +static void do_monitor(const char *p, const char *end) { + char cmd[24] = {0}; + size_t n = 0; + while (p + 1 < end && n < sizeof(cmd) - 1) { + int hi = hexval((uint8_t)p[0]), lo = hexval((uint8_t)p[1]); + if (hi < 0 || lo < 0) break; + cmd[n++] = (char)((hi << 4) | lo); + p += 2; + } + const char *txt; + if (strcmp(cmd, "unlock-app") == 0) { + s_unlocked = true; + txt = "app area writable; erasing the running image crashes the PM5\n"; + } else if (strcmp(cmd, "lock") == 0) { + s_unlocked = false; + txt = "writes limited to flash above the running image\n"; + } else if (strcmp(cmd, "exit") == 0) { + s_exit = true; + txt = "leaving gdb gateway\n"; + } else if (strcmp(cmd, "info") == 0) { + txt = s_unlocked ? "flash 0x08000000+1M sram 0x20000000+512K app-write on\n" + : "flash 0x08000000+1M sram 0x20000000+512K app-write off\n"; + } else if (strcmp(cmd, "help") == 0) { + txt = "monitor info | unlock-app | lock | exit\n"; + } else { + txt = "unknown command\n"; + } + size_t t = strlen(txt); + for (size_t i = 0; i < t; i++) { + s_out[2 * i] = (uint8_t)HEXD[(uint8_t)txt[i] >> 4]; + s_out[2 * i + 1] = (uint8_t)HEXD[(uint8_t)txt[i] & 0xF]; + } + send_payload(s_out, 2 * t); +} + +static void do_query(const char *s, size_t len) { + const char *end = s + len; + if (starts_with(s, len, "qSupported")) { + send_str("PacketSize=400;qXfer:features:read+;qXfer:memory-map:read+"); + } else if (starts_with(s, len, "qXfer:features:read:target.xml:")) { + send_qxfer(TARGET_XML, s + strlen("qXfer:features:read:target.xml:"), end); + } else if (starts_with(s, len, "qXfer:memory-map:read::")) { + send_qxfer(MEMORY_MAP_XML, s + strlen("qXfer:memory-map:read::"), end); + } else if (starts_with(s, len, "qRcmd,")) { + do_monitor(s + 6, end); + } else if (starts_with(s, len, "qAttached")) { + send_str("1"); + } else if (starts_with(s, len, "qfThreadInfo")) { + send_str("m1"); + } else if (starts_with(s, len, "qsThreadInfo")) { + send_str("l"); + } else if (len == 2 && starts_with(s, len, "qC")) { + send_str("QC1"); + } else { + send_str(""); + } +} + +static void handle_packet(const char *s, size_t len) { + if (len == 0) { + send_str(""); + return; + } + switch (s[0]) { + case '?': + case 'c': case 'C': case 's': case 'S': // no execution control + send_str("S05"); + break; + case 'g': // registers are not available + memset(s_out, '0', GDB_NREGS * 8); + send_payload(s_out, GDB_NREGS * 8); + break; + case 'p': + memset(s_out, '0', 8); + send_payload(s_out, 8); + break; + case 'H': case 'T': case 'D': + send_str("OK"); + break; + case 'm': + do_mem_read(s + 1, s + len); + break; + case 'M': + do_mem_write(s + 1, s + len); + break; + case 'G': case 'P': // registers are read-only + send_err(0x01); + break; + case 'v': + do_vflash(s, len); + break; + case 'q': + do_query(s, len); + break; + default: // X, Z, z, unknown + send_str(""); + break; + } +} + +static void gdb_reset(const gdb_ops_t *ops) { + s_ops = ops; + s_state = ST_IDLE; + s_rxlen = 0; + s_ack_pending = false; + s_exit = false; + s_unlocked = false; + s_frame_len = 0; + s_frame_off = 0; +} + +static void gdb_feed(uint8_t b) { + switch (s_state) { + case ST_IDLE: + if (b == '$') { + s_rxlen = 0; + s_sum = 0; + s_ovf = false; + s_state = ST_DATA; + } else if (b == '-' && s_frame_len) { + s_ops->tx(s_frame + s_frame_off, s_frame_len - s_frame_off); // resend + } else if (b == 0x03) { + send_str("S05"); // Ctrl-C, nothing to stop + } + break; + case ST_DATA: + if (b == '#') { + s_state = ST_CS1; + } else if (b == '$') { + s_rxlen = 0; + s_sum = 0; + s_ovf = false; + } else { + if (s_rxlen < GDB_PKT_MAX) s_rx[s_rxlen++] = b; + else s_ovf = true; + s_sum = (uint8_t)(s_sum + b); + } + break; + case ST_CS1: + s_cs_hi = hexval(b); + s_state = ST_CS2; + break; + case ST_CS2: { + int lo = hexval(b); + s_state = ST_IDLE; + if (s_ovf || s_cs_hi < 0 || lo < 0 || (uint8_t)((s_cs_hi << 4) | lo) != s_sum) { + s_ops->tx((const uint8_t *)"-", 1); + } else { + s_ack_pending = true; + s_rx[s_rxlen] = 0; + handle_packet((const char *)s_rx, s_rxlen); + if (s_ack_pending) { // handler sent no reply + s_ops->tx((const uint8_t *)"+", 1); + s_ack_pending = false; + } + } + break; + } + } +} + +#ifndef GDB_GATEWAY_HOST_TEST + +#include "appmain.h" +#include "dbprint.h" +#include "proxmark3_arm.h" +#include "standalone.h" +#include "ticks_apis.h" +#include "util.h" +#include "usb_cdc_apis.h" +#include "usb_read_ng.h" +#include "at32f435_437_flash.h" + +extern const char __os_size__[]; // linker: size of the OS image + +static void usb_tx(const uint8_t *data, size_t len) { + usb_write(data, len); +} + +static size_t at32_mem_read(uint32_t addr, uint8_t *out, size_t len) { + uint32_t end; + if (addr - GW_FLASH_BASE < GW_FLASH_SIZE) { + end = GW_FLASH_BASE + GW_FLASH_SIZE; + } else if (addr - GW_SRAM_BASE < GW_SRAM_SIZE) { + end = GW_SRAM_BASE + GW_SRAM_SIZE; + } else { + return 0; + } + if (len > end - addr) len = end - addr; + for (size_t i = 0; i < len; i++) { + out[i] = *(const volatile uint8_t *)(addr + i); + } + return len; +} + +static bool at32_ram_write(uint32_t addr, const uint8_t *data, size_t len) { + for (size_t i = 0; i < len; i++) { + *(volatile uint8_t *)(addr + i) = data[i]; + } + return true; +} + +static bool at32_flash_erase(uint32_t addr) { + WDT_HIT(); + flash_unlock(); + flash_status_type st = flash_sector_erase(addr); + flash_lock(); + return st == FLASH_OPERATE_DONE; +} + +// Words are merged with the current flash content, so partial words work +// on erased sectors. Read back to verify. +static bool at32_flash_write(uint32_t addr, const uint8_t *data, size_t len) { + bool ok = true; + uint32_t end = addr + (uint32_t)len; + flash_unlock(); + for (uint32_t w = addr & ~3u; w < end && ok; w += 4) { + uint32_t v = *(volatile uint32_t *)w; + for (uint32_t i = 0; i < 4; i++) { + if (w + i >= addr && w + i < end) { + v = (v & ~(0xFFu << (8 * i))) | ((uint32_t)data[w + i - addr] << (8 * i)); + } + } + if (v != *(volatile uint32_t *)w) { + ok = flash_word_program(w, v) == FLASH_OPERATE_DONE && *(volatile uint32_t *)w == v; + } + } + flash_lock(); + WDT_HIT(); + return ok; +} + +static gdb_ops_t s_at32_ops = { usb_tx, at32_mem_read, at32_ram_write, at32_flash_erase, at32_flash_write, 0 }; + +void ModInfo(void) { + DbpString(" GDB gateway, RSP over USB CDC"); +} + +// No Dbprintf here: debug frames would corrupt the RSP stream. +void RunMod(void) { + s_at32_ops.app_end = (GW_APP_BASE + (uint32_t)(uintptr_t)__os_size__ + GW_SECTOR - 1) & ~(GW_SECTOR - 1); + gdb_reset(&s_at32_ops); + LED_A_ON(); + while (!s_exit && !BUTTON_PRESS()) { + WDT_HIT(); + while (usb_read_ng_has_buffered_data() || usb_poll_validate_length()) { + uint8_t b; + if (usb_read_ng(&b, 1) != 1) break; + gdb_feed(b); + } + } + LED_A_OFF(); + while (BUTTON_PRESS()) { // keep the main loop from reading the press as power-off + WDT_HIT(); + SpinDelay(10); + } +} + +#endif // GDB_GATEWAY_HOST_TEST diff --git a/armsrc/Standalone/readme.md b/armsrc/Standalone/readme.md index e85c06cc7..178938f5f 100644 --- a/armsrc/Standalone/readme.md +++ b/armsrc/Standalone/readme.md @@ -81,6 +81,7 @@ As such the information inside may be false or hallucinated. | Mode ID | Document | Description | |---------|----------|-------------| | DANKARMULTI | [Dankarmulti Loader](../../doc/standalone/dankarmulti.md) | Combine multiple standalone modes into one firmware image | +| GDB_GATEWAY | [GDB Gateway](../../doc/standalone/gdb_gateway.md) | Read-only GDB server over USB CDC | PM5 | # Developing Standalone Modes This contains functionality for different StandAlone modes. The fullimage will be built given the correct compiler flags used. Build targets for these files are contained in `Makefile.inc` and `Makefile.hal` diff --git a/doc/standalone/gdb_gateway.md b/doc/standalone/gdb_gateway.md new file mode 100644 index 000000000..ebb296109 --- /dev/null +++ b/doc/standalone/gdb_gateway.md @@ -0,0 +1,61 @@ +# GDB_GATEWAY + +GDB Remote Serial Protocol server on the USB CDC link (PM5). +No probe or extra wiring. Reads flash and SRAM, writes SRAM and flash. No breakpoints, no step. + +## Build + +``` +PLATFORM=PM5 +STANDALONE=GDB_GATEWAY +``` + +## Use + +``` +pm3 --> hw standalone +``` + +Quit the client (the port stays up), then: + +``` +arm-none-eabi-gdb -ex 'target remote /dev/ttyACM0' +(gdb) x/16xw 0x08000000 +(gdb) dump memory flash.bin 0x08000000 0x08100000 +(gdb) monitor info +(gdb) load patch.elf # flash above the running image +(gdb) monitor unlock-app # allow writes to the app area +(gdb) monitor lock +(gdb) monitor exit +``` + +Leave with `monitor exit` or a button press. + +## Readable ranges + +| Range | Size | +|-------|------| +| `0x08000000` | 1 MB flash | +| `0x20000000` | 512 KB SRAM | + +Other addresses return `E14`. Peripheral space is not exposed. + +## Writes + +| Target | Allowed | +|--------|---------| +| SRAM (`M`) | always | +| Flash above the running image (`load`, `vFlashErase`/`vFlashWrite`) | always | +| Flash from `0x08004000` (app area) | after `monitor unlock-app` | +| Bootrom, `0x08000000`-`0x08003fff` | never | + +Refused writes return `E02`. `monitor lock` restores the default; the gateway starts locked. +Erasing the running image crashes the PM5, recover it through the bootrom. + +## Notes + +- Flash is erased in 2 KB sectors. +- Register reads return zeros, the core is not halted. +- `c`/`s` return an immediate stop reply; nothing is executed. +- Debug output is off while running, it would corrupt the RSP stream. +- Set `remotetimeout 60` in gdb for large dumps.