From 0bea485f4b33bc7d9d48a00be411e1c650535c09 Mon Sep 17 00:00:00 2001 From: redoste Date: Sun, 21 May 2023 19:49:23 +0200 Subject: [PATCH] gdb: first implementation with basic memory and register read support --- CMakeLists.txt | 2 + include/gint/gdb.h | 31 +++++ src/gdb/gdb.c | 299 ++++++++++++++++++++++++++++++++++++++++++ src/gdb/gdb_private.h | 50 +++++++ 4 files changed, 382 insertions(+) create mode 100644 include/gint/gdb.h create mode 100644 src/gdb/gdb.c create mode 100644 src/gdb/gdb_private.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 2723835..5449fb4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -70,6 +70,8 @@ set(SOURCES_COMMON src/fs/fugue/fugue_rmdir.c src/fs/fugue/fugue_unlink.c src/fs/fugue/util.c + # GDB remote serial protocol + src/gdb/gdb.c # Interrupt Controller driver src/intc/intc.c src/intc/inth.s diff --git a/include/gint/gdb.h b/include/gint/gdb.h new file mode 100644 index 0000000..41b6e3c --- /dev/null +++ b/include/gint/gdb.h @@ -0,0 +1,31 @@ +//--- +// gint:gdb - GDB remote serial protocol +//--- + +#ifndef GINT_GDB +#define GINT_GDB + +#ifdef __cplusplus +extern "C" { +#endif + +/* Error codes for GDB functions */ +enum { + GDB_NO_INTERFACE = -1, + GDB_ALREADY_STARTED = -2, + GDB_USB_ERROR = -3, +}; + +/* gdb_start(): Start the GDB remote serial protocol server + + This function will start the GDB remote serial protocol implementation and + block until the program is resumed from the connected debugger. + It currently only supports USB communication and will fail if USB is already + in use.*/ +int gdb_start(void); + +#ifdef __cplusplus +} +#endif + +#endif /* GINT_GDB */ diff --git a/src/gdb/gdb.c b/src/gdb/gdb.c new file mode 100644 index 0000000..5b95416 --- /dev/null +++ b/src/gdb/gdb.c @@ -0,0 +1,299 @@ +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "gdb_private.h" + +static void gdb_hexlify(char* output_string, const uint8_t* input_buffer, size_t input_size) +{ + const char* hex = "0123456789ABCDEF"; + for (size_t i = 0; i < input_size; i++) { + uint8_t byte = input_buffer[i]; + output_string[i*2 + 0] = hex[(byte & 0xF0) >> 4]; + output_string[i*2 + 1] = hex[byte & 0x0F]; + } +} + +// TODO : bug in fxlibc ? strtoul doesn't support uppercase +static uint32_t gdb_unhexlify(const char* input_string) +{ + size_t input_length = strlen(input_string); + uint32_t ret = 0; + for (size_t i = 0; i < input_length; i++) { + uint8_t nibble_hex = tolower(input_string[i]); + uint8_t nibble = nibble_hex >= 'a' && nibble_hex <= 'f' ? nibble_hex - 'a' + 10 : + nibble_hex >= '0' && nibble_hex <= '9' ? nibble_hex - '0' : 0; + ret = (ret << 4) | nibble; + } + return ret; +} + +static bool gdb_started = false; + +static void gdb_send(const char *data, size_t size) +{ + usb_fxlink_header_t header; + usb_fxlink_fill_header(&header, "gdb", "remote", size); + + int pipe = usb_ff_bulk_output(); + usb_write_sync(pipe, &header, sizeof(header), false); + usb_write_sync(pipe, data, size, false); + usb_commit_sync(pipe); +} + +static char gdb_recv_buffer[1024]; +static size_t gdb_recv_buffer_size = 0; +static ssize_t gdb_recv(char *buffer, size_t buffer_size) +{ + if (gdb_recv_buffer_size >= buffer_size) { + memcpy(buffer, gdb_recv_buffer, buffer_size); + memmove(gdb_recv_buffer, &gdb_recv_buffer[buffer_size], gdb_recv_buffer_size - buffer_size); + gdb_recv_buffer_size -= buffer_size; + return buffer_size; + } + + usb_fxlink_header_t header; + while (!usb_fxlink_handle_messages(&header)) { + sleep(); + } + + // TODO : should we abort or find a way to gracefully shutdown the debugger ? + if (strncmp(header.application, "gdb", 16) == 0 + && strncmp(header.type, "remote", 16) == 0) { + if (header.size > sizeof(gdb_recv_buffer) - gdb_recv_buffer_size) { + abort(); + } + usb_read_sync(usb_ff_bulk_input(), &gdb_recv_buffer[gdb_recv_buffer_size], header.size, false); + gdb_recv_buffer_size += header.size; + return gdb_recv(buffer, buffer_size); + } else { + abort(); + } +} + +static ssize_t gdb_recv_packet(char* buffer, size_t buffer_size) +{ + char read_char; + + // Waiting for packet start '$' + do { + if (gdb_recv(&read_char, 1) != 1) { + return -1; + } + } while (read_char != '$'); + + uint8_t checksum = 0; + size_t packet_len = 0; + while (true) { + if (gdb_recv(&read_char, 1) != 1) { + return -1; + } + + if (read_char != '#') { + // -1 to ensure space for a NULL terminator + if (packet_len >= (buffer_size - 1)) { + return -1; + } + buffer[packet_len++] = read_char; + checksum += read_char; + } else { + break; + } + } + buffer[packet_len] = '\0'; + + char read_checksum_hex[3]; + if (gdb_recv(read_checksum_hex, 2) != 2) { + return -1; + } + read_checksum_hex[2] = '\0'; + uint8_t read_checksum = gdb_unhexlify(read_checksum_hex); + + if (read_checksum != checksum) { + read_char = '-'; + gdb_send(&read_char, 1); + return -1; + } else { + read_char = '+'; + gdb_send(&read_char, 1); + return packet_len; + } +} + +static ssize_t gdb_send_packet(const char* packet, size_t packet_length) +{ + if (packet == NULL || packet_length == 0) { + // Empty packet + gdb_send("$#00", 4); + return 4; + } + + size_t buffer_length = packet_length + 1 + 4; + // TODO : find if it's more efficient to malloc+copy on each packet or send 3 small fxlink messages + char* buffer = malloc(buffer_length); + + uint8_t checksum = 0; + for (size_t i = 0; i < packet_length; i++) { + checksum += packet[i]; + } + + buffer[0] = '$'; + memcpy(&buffer[1], packet, packet_length); + snprintf(&buffer[buffer_length - 4], 4, "#%02X", checksum); + + // -1 to not send the NULL terminator of snprintf + gdb_send(buffer, buffer_length - 1); + free(buffer); + return buffer_length; +} + +static void gdb_send_stop_reply(void) +{ + gdb_send_packet("S05", 3); // SIGTRAP +} + +static void gdb_handle_query_packet(const char* packet) +{ + if (strncmp("qSupported", packet, 10) == 0) { + const char* qsupported_ans = "PacketSize=255;qXfer:memory-map:read"; + gdb_send_packet(qsupported_ans, strlen(qsupported_ans)); + } else if (strncmp("qXfer:memory-map:read::", packet, 23) == 0) { + /* TODO : Implement qXfer and memory map XML + * https://sourceware.org/gdb/onlinedocs/gdb/Memory-Map-Format.html#Memory-Map-Format + * Required for enforcing hbreak : https://sourceware.org/gdb/onlinedocs/gdb/Set-Breaks.html + */ + gdb_send_packet(NULL, 0); + } else { + gdb_send_packet(NULL, 0); + } +} + +static void gdb_handle_read_general_registers(gdb_cpu_state_t* cpu_state) +{ + char reply_buffer[23*8]; + if (!cpu_state) { + memset(reply_buffer, 'x', sizeof(reply_buffer)); + memcpy(&reply_buffer[offsetof(gdb_cpu_state_t, reg.pc)*2], + "A0000000", 8); // pc needs to be set to make GDB happy + } else { + gdb_hexlify(reply_buffer, (uint8_t*)cpu_state->regs, + sizeof(cpu_state->regs)); + } + gdb_send_packet(reply_buffer, sizeof(reply_buffer)); +} + +static void gdb_handle_read_register(gdb_cpu_state_t* cpu_state, const char* packet) +{ + uint8_t register_id = gdb_unhexlify(&packet[1]); + char reply_buffer[8]; + if (!cpu_state || register_id >= sizeof(cpu_state->regs)/sizeof(uint32_t)) { + memset(reply_buffer, 'x', sizeof(reply_buffer)); + } else { + gdb_hexlify(reply_buffer, (uint8_t*)&cpu_state->regs[register_id], + sizeof(cpu_state->regs[register_id])); + } + gdb_send_packet(reply_buffer, sizeof(reply_buffer)); +} + +static void gdb_handle_read_memory(const char* packet) +{ + char address_hex[16] = {0}, size_hex[16] = {0}; + void* read_address; + size_t read_size; + + packet++; // consume 'm' + for (size_t i = 0; i < sizeof(address_hex); i++) { + address_hex[i] = *(packet++); // consume address + if (*packet == ',') break; + } + packet++; // consume ',' + for (size_t i = 0; i < sizeof(size_hex); i++) { + size_hex[i] = *(packet++); // consume size + if (*packet == '\0') break; + } + + read_address = (void*) gdb_unhexlify(address_hex); + read_size = (size_t) gdb_unhexlify(size_hex); + + // TODO : Detect invalid reads and prevent TLB misses + char *reply_buffer = malloc(read_size * 2); + gdb_hexlify(reply_buffer, read_address, read_size); + gdb_send_packet(reply_buffer, read_size * 2); + free(reply_buffer); +} + +static void gdb_main(gdb_cpu_state_t* cpu_state) +{ + if (cpu_state != NULL) { + gdb_send_stop_reply(); + } + while (1) { + char packet_buffer[256]; + ssize_t packet_size = gdb_recv_packet(packet_buffer, sizeof(packet_buffer)); + if (packet_size <= 0) { + // TODO : Should we break or log on recv error ? + continue; + } + + switch (packet_buffer[0]) { + case '?': // Halt reason + gdb_send_stop_reply(); + break; + case 'q': + gdb_handle_query_packet(packet_buffer); + break; + + case 'g': + gdb_handle_read_general_registers(cpu_state); + break; + case 'p': + gdb_handle_read_register(cpu_state, packet_buffer); + break; + case 'm': + gdb_handle_read_memory(packet_buffer); + break; + + // case 'G': // Write general register + // case 'P': // Write register + // case 'M': // Write memory + // case 'z': // Insert hbreak + // case 'Z': // Remove hbreak + // case 'k': // Kill request + // case 's': // Single step + + case 'c': // Continue + return; + + default: // Unsupported packet + gdb_send_packet(NULL, 0); + break; + } + } +} + +int gdb_start(void) +{ + if (gdb_started) { + return GDB_ALREADY_STARTED; + } + + if (usb_is_open()) { + return GDB_NO_INTERFACE; + } + + usb_interface_t const *interfaces[] = { &usb_ff_bulk, NULL }; + if (usb_open(interfaces, GINT_CALL_NULL) < 0) { + return GDB_USB_ERROR; + } + usb_open_wait(); + + gdb_started = true; + gdb_main(NULL); + return 0; +} diff --git a/src/gdb/gdb_private.h b/src/gdb/gdb_private.h new file mode 100644 index 0000000..04474ee --- /dev/null +++ b/src/gdb/gdb_private.h @@ -0,0 +1,50 @@ +//--- +// gint:gdb:gdb-private - Private definitions for the GDB implementation +//--- + +#ifndef GINT_GDB_PRIVATE +#define GINT_GDB_PRIVATE + +#include + +/* gdb_cpu_state_t: State of the CPU when breaking + This struct keep the same register indices as those declared by GDB to allow + easy R/W without needing a "translation" table. + See : https://sourceware.org/git/?p=binutils-gdb.git;a=blob;f=gdb/sh-tdep.c; + h=c402961b80a0b4589243023ea5362d43f644a9ec;hb=4f3e26ac6ee31f7bc4b04abd + 8bdb944e7f1fc5d2#l327 + */ +// TODO : Should we expose r*b*, ssr, spc ? are they double-saved when breaking +// inside an interrupt handler ? +typedef struct { + union { + struct { + uint32_t r0; + uint32_t r1; + uint32_t r2; + uint32_t r3; + uint32_t r4; + uint32_t r5; + uint32_t r6; + uint32_t r7; + uint32_t r8; + uint32_t r9; + uint32_t r10; + uint32_t r11; + uint32_t r12; + uint32_t r13; + uint32_t r14; + uint32_t r15; + uint32_t pc; + uint32_t pr; + uint32_t gbr; + uint32_t vbr; + uint32_t mach; + uint32_t macl; + uint32_t sr; + } reg; + uint32_t regs[23]; + }; +} gdb_cpu_state_t; + +#endif /* GINT_GDB_PRIVATE */