diff --git a/CMakeLists.txt b/CMakeLists.txt index 8d1923a6f..80a77d9f7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -121,6 +121,7 @@ option(DINPUT "DirectInput" option(CPPTHREADS "C++11 threads" ON) option(NEW_DYNAREC "Use the PCem v15 (\"new\") dynamic recompiler" OFF) option(MINITRACE "Enable Chrome tracing using the modified minitrace library" OFF) +option(GDBSTUB "Enable GDB stub server for debugging" ON) option(DEV_BRANCH "Development branch" OFF) if(NOT WIN32) option(QT "QT GUI" ON) diff --git a/src/86box.c b/src/86box.c index 17a1cca9c..262dc436d 100644 --- a/src/86box.c +++ b/src/86box.c @@ -93,6 +93,7 @@ #include <86box/ui.h> #include <86box/plat.h> #include <86box/version.h> +#include <86box/gdbstub.h> /* Stuff that used to be globally declared in plat.h but is now extern there @@ -736,6 +737,8 @@ usage: if (lang_init) lang_id = lang_init; + gdbstub_init(); + /* All good! */ return(1); } @@ -1171,6 +1174,8 @@ pc_close(thread_t *ptr) mo_close(); scsi_disk_close(); + + gdbstub_close(); } diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8f1465d1e..8be7580e0 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -23,6 +23,11 @@ if(CPPTHREADS) target_sources(86Box PRIVATE thread.cpp) endif() +if(GDBSTUB) + add_compile_definitions(USE_GDBSTUB) + target_sources(86Box PRIVATE gdbstub.c) +endif() + if(NEW_DYNAREC) add_compile_definitions(USE_NEW_DYNAREC) endif() diff --git a/src/cpu/386.c b/src/cpu/386.c index 6af8173eb..48fb30798 100644 --- a/src/cpu/386.c +++ b/src/cpu/386.c @@ -22,6 +22,7 @@ #include <86box/fdd.h> #include <86box/fdc.h> #include <86box/machine.h> +#include <86box/gdbstub.h> #include "386_common.h" #ifdef USE_NEW_DYNAREC #include "codegen.h" @@ -256,6 +257,11 @@ exec386(int cycs) if (TIMER_VAL_LESS_THAN_VAL(timer_target, (uint32_t) tsc)) timer_process_inline(); + +#ifdef USE_GDBSTUB + if (gdbstub_singlestep) + return; +#endif } } } diff --git a/src/cpu/808x.c b/src/cpu/808x.c index 7a2695810..a00a4b057 100644 --- a/src/cpu/808x.c +++ b/src/cpu/808x.c @@ -34,6 +34,7 @@ #include <86box/pic.h> #include <86box/ppi.h> #include <86box/timer.h> +#include <86box/gdbstub.h> /* Is the CPU 8088 or 8086. */ int is8086 = 0; @@ -2832,5 +2833,10 @@ execx86(int cycs) cpu_alu_op = 0; } + +#ifdef USE_GDBSTUB + if (gdbstub_singlestep) + return; +#endif } } diff --git a/src/gdbstub.c b/src/gdbstub.c new file mode 100644 index 000000000..9d5c32519 --- /dev/null +++ b/src/gdbstub.c @@ -0,0 +1,1042 @@ +/* + * 86Box A hypervisor and IBM PC system emulator that specializes in + * running old operating systems and software designed for IBM + * PC systems and compatibles from 1981 through fairly recent + * system designs based on the PCI bus. + * + * This file is part of the 86Box distribution. + * + * GDB stub server for remote debugging. + * + * + * + * Authors: RichardG, + * + * Copyright 2022 RichardG. + */ +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +# include +# include +#else +# include +# include +#endif +#define HAVE_STDARG_H +#include <86box/86box.h> +#include "cpu.h" +#include "x86seg.h" +#include <86box/io.h> +#include <86box/mem.h> +#include <86box/plat.h> + + +#define GDBSTUB_MAX_REG 38 + +enum { + GDB_SIGINT = 2, + GDB_SIGTRAP = 5 +}; + +typedef struct _gdbstub_client_ { + int socket; + struct sockaddr_in addr; + + char packet[16384], response[16384]; + int packet_pos, response_pos; + + event_t *response_event; + + struct _gdbstub_client_ *next; +} gdbstub_client_t; + +#define ENABLE_GDBSTUB_LOG 1 +#ifdef ENABLE_GDBSTUB_LOG +int gdbstub_do_log = ENABLE_GDBSTUB_LOG; + +static void +gdbstub_log(const char *fmt, ...) +{ + va_list ap; + + if (gdbstub_do_log) { + va_start(ap, fmt); + pclog_ex(fmt, ap); + va_end(ap); + } +} +#else +#define gdbstub_log(fmt, ...) +#endif + +static x86seg *segment_regs[] = {&cpu_state.seg_cs, &cpu_state.seg_ss, &cpu_state.seg_ds, &cpu_state.seg_es, &cpu_state.seg_fs, &cpu_state.seg_gs}; +static uint32_t *cr_regs[] = {&cpu_state.CR0.l, &cr2, &cr3, &cr4}; +static void *fpu_regs[] = {&cpu_state.npxc, &cpu_state.npxs, NULL, x87_pc_seg, x87_pc_off, x87_op_seg, x87_op_off}; +static const char target_xml[] = /* based on qemu's i386-32bit.xml */ + "" + "" + "" + "i8086" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; + +#ifdef _WIN32 +static WSADATA wsa; +#endif +static int gdbstub_socket = -1, gdbstub_paused = 0; +static char stop_reason[2048]; +static gdbstub_client_t *first_client, *last_client; +static mutex_t *client_list_mutex; +static void (*cpu_exec_shadow)(int cycs); + +int gdbstub_singlestep = 0; + + +static void +gdbstub_break() +{ + /* Initiate pause. */ + plat_pause(1); + + /* Force CPU execution to return as soon as possible. */ + gdbstub_singlestep = 1; +} + + +static void +gdbstub_singlestep_exec(int cycs) +{ + /* Call the original cpu_exec function. */ + cpu_exec_shadow(cycs); + + /* Swap the original function back in. */ + cpu_exec = cpu_exec_shadow; + + /* Break immediately. */ + gdbstub_break(); +} + + +static void +gdbstub_jump(uint32_t new_pc) +{ + /* Nasty hack; qemu always uses the full 32-bit EIP internally... */ + if (cpu_state.op32 || ((new_pc >= cs) && (new_pc < (cs + 65536)))) { + cpu_state.pc = new_pc - cs; + } else { + loadseg((new_pc >> 4) & 0xf000, &cpu_state.seg_cs); + cpu_state.pc = new_pc & 0xffff; + } +} + + +static inline int +gdbstub_hex_decode(int c) +{ + if ((c >= '0') && (c <= '9')) + return c - '0'; + else if ((c >= 'A') && (c <= 'F')) + return c - 'A' + 10; + else if ((c >= 'a') && (c <= 'f')) + return c - 'a' + 10; + else + return 0; +} + + +static inline int +gdbstub_hex_encode(int c) +{ + if (c < 10) + return c + '0'; + else + return c - 10 + 'A'; +} + + +static int +gdbstub_client_read_word(gdbstub_client_t *client, int *dest) +{ + char *p = &client->packet[client->packet_pos], *q = p; + while (((*p >= '0') && (*p <= '9')) || ((*p >= 'A') && (*p <= 'F')) || ((*p >= 'a') && (*p <= 'f'))) + *dest = ((*dest) << 4) | gdbstub_hex_decode(*p++); + return p - q; +} + + +static int +gdbstub_client_read_hex(gdbstub_client_t *client, uint8_t *buf, int size) +{ + int pp = client->packet_pos; + while (size-- && (pp < (sizeof(client->packet) - 2))) { + *buf = gdbstub_hex_decode(client->packet[pp++]) << 4; + *buf++ |= gdbstub_hex_decode(client->packet[pp++]); + } + return pp - client->packet_pos; +} + + +static int +gdbstub_client_read_string(gdbstub_client_t *client, char *buf, int size, char terminator) +{ + int pp = client->packet_pos; + char c; + while (size-- && (pp < (sizeof(client->packet) - 1))) { + c = client->packet[pp]; + if ((c == terminator) || (c == '\0')) { + *buf = '\0'; + break; + } + pp++; + *buf++ = c; + } + return pp - client->packet_pos; +} + + +static int +gdbstub_client_write_reg(int index, uint8_t *buf) +{ + int width = 4; + switch (index) { + case 0 ... 7: /* [EAX:EDI] */ + cpu_state.regs[index].l = *((uint32_t *) buf); + break; + + case 8: /* EIP */ + gdbstub_jump(*((uint32_t *) buf)); + break; + + case 9: /* EFLAGS */ + cpu_state.flags = *((uint16_t *) &buf[0]); + cpu_state.eflags = *((uint16_t *) &buf[2]); + break; + + case 10 ... 15: /* [CS:GS] */ + width = 2; + loadseg(*((uint16_t *) buf), segment_regs[index - 10]); + break; + + case 16 ... 17: /* FSbase, GSbase */ + /* Do what qemu does and just load the base. */ + segment_regs[index - 12]->base = *((uint32_t *) buf); + break; + + case 18 ... 21: /* [CR0:CR4] */ + *cr_regs[index - 18] = *((uint32_t *) buf); + break; + + case 22: /* EFER */ + msr.amd_efer = *((uint64_t *) buf); + break; + + case 23 ... 30: /* ST(0:7) */ + width = 10; + break; + + /* FPU CONTROL REGS */ + + default: + width = 0; + } + + flushmmucache(); /* incredibly cursed to be calling that from here */ + + return width; +} + + +static void +gdbstub_client_respond(gdbstub_client_t *client) +{ + /* Calculate checksum. */ + uint8_t checksum = 0; + int i; + for (i = 0; i < client->response_pos; i++) + checksum += client->response[i]; + + /* Send response packet. */ + client->response[client->response_pos] = '\0'; +#ifdef ENABLE_GDBSTUB_LOG + i = client->response[995]; /* pclog_ex buffer too small */ + client->response[995] = '\0'; + gdbstub_log("GDB Stub: Sending response: %s\n", client->response); + client->response[995] = i; +#endif + send(client->socket, "$", 1, 0); + send(client->socket, client->response, client->response_pos, 0); + char response_cksum[3] = {'#', gdbstub_hex_encode(checksum >> 4), gdbstub_hex_encode(checksum & 0x0f)}; + send(client->socket, response_cksum, sizeof(response_cksum), 0); +} + + +static void +gdbstub_client_respond_hex(gdbstub_client_t *client, uint8_t *buf, int size) +{ + while (size-- && (client->response_pos < (sizeof(client->response) - 2))) { + client->response[client->response_pos++] = gdbstub_hex_encode((*buf) >> 4); + client->response[client->response_pos++] = gdbstub_hex_encode((*buf++) & 0x0f); + } +} + + +static int +gdbstub_client_read_reg(int index, uint8_t *buf) +{ + int width = 4; + switch (index) { + case 0 ... 7: /* [EAX:EDI] */ + *((uint32_t *) buf) = cpu_state.regs[index].l; + break; + + case 8: /* EIP */ + *((uint32_t *) buf) = cs + cpu_state.pc; + break; + + case 9: /* EFLAGS */ + *((uint16_t *) &buf[0]) = cpu_state.flags; + *((uint16_t *) &buf[2]) = cpu_state.eflags; + break; + + case 10 ... 15: /* [CS:GS] */ + *((uint16_t *) buf) = segment_regs[index - 10]->seg; + break; + + case 16 ... 17: /* FSbase, GSbase */ + *((uint32_t *) buf) = segment_regs[index - 12]->base; + break; + + case 18 ... 21: /* [CR0:CR4] */ + *((uint32_t *) buf) = *cr_regs[index - 18]; + break; + + case 22: /* EFER */ + *((uint64_t *) buf) = msr.amd_efer; + break; + + case 23 ... 30: /* ST(0:7) */ + width = 10; + *((uint64_t *) &buf[0]) = 0; + *((uint16_t *) &buf[8]) = 0; + break; + + case 31 ... 32: /* [FCTRL:FSTAT] */ + case 34: /* [FISEG] */ + case 36: /* [FOSEG] */ + width = 2; + *((uint16_t *) buf) = *((uint16_t *) fpu_regs[index - 31]); + break; + + case 33: /* FTAG */ + width = 2; + *((uint16_t *) buf) = x87_gettag(); + break; + + case 35: /* [FIOFF] */ + case 37: /* [FOOFF] */ + *((uint32_t *) buf) = *((uint32_t *) fpu_regs[index - 31]); + break; + + case 38: /* [FOP] */ + width = 2; + *((uint16_t *) buf) = 0; /* we don't store the FPU opcode */ + break; + + default: + width = 0; + } + + return width; +} + + +static void +gdbstub_client_packet(gdbstub_client_t *client) +{ + uint8_t rcv_checksum = 0, checksum = 0; + int i, j = 0, k = 0, l; + uint8_t buf[10] = {0}; + char *p; + + /* Validate checksum. */ + client->packet_pos -= 2; + gdbstub_client_read_hex(client, &rcv_checksum, 1); + client->packet[client->packet_pos] = '\0'; + client->packet[client->packet_pos + 1] = '\0'; + client->packet[--client->packet_pos] = '\0'; + for (i = 0; i < client->packet_pos; i++) + checksum += client->packet[i]; + +#if 0 /* msys2 gdb 11.1 transmits qSupported and H with invalid checksum... */ + if (checksum != rcv_checksum) { + /* Send negative acknowledgement. */ +#ifdef ENABLE_GDBSTUB_LOG + i = client->packet[953]; /* pclog_ex buffer too small */ + client->packet[953] = '\0'; + gdbstub_log("GDB Stub: Received packet with invalid checksum (expected %02X got %02X): %s\n", checksum, rcv_checksum, client->packet); + client->packet[953] = i; +#endif + send(client->socket, "-", 1, 0); + return; + } +#endif + + /* Send positive acknowledgement. */ +#ifdef ENABLE_GDBSTUB_LOG + i = client->packet[996]; /* pclog_ex buffer too small */ + client->packet[996] = '\0'; + gdbstub_log("GDB Stub: Received packet: %s\n", client->packet); + client->packet[996] = i; +#endif + send(client->socket, "+", 1, 0); + + /* Block other responses from being written while this one isn't acknowledged. */ + thread_wait_event(client->response_event, 0); + thread_reset_event(client->response_event); + client->response_pos = 0; + client->packet_pos = 1; + + /* Parse command. */ + switch (client->packet[0]) { + case '?': /* stop reason */ + /* Respond with a stop reply packet. */ + strcpy(client->response, stop_reason); + client->response_pos = strlen(client->response); + break; + + case 'c': /* continue */ + case 's': /* step */ + /* No immediate response. */ + thread_set_event(client->response_event); + + /* Jump to address if specified. */ + if (gdbstub_client_read_word(client, &j)) + gdbstub_jump(j); + + /* Resume emulation. */ + if (client->packet[0] == 'c') { + gdbstub_singlestep = 0; + } else { + /* Replace cpu_exec with our own function, which breaks after a single step. */ + gdbstub_singlestep = 1; + if (cpu_exec != gdbstub_singlestep_exec) + cpu_exec_shadow = cpu_exec; + cpu_exec = gdbstub_singlestep_exec; + } + gdbstub_paused = 0; + plat_pause(0); + return; + + case 'D': /* detach */ + /* Resume emulation. */ + gdbstub_paused = 0; + plat_pause(0); + + /* Respond positively. */ + client->response_pos = sprintf(client->response, "OK"); + break; + + case 'g': /* read all registers */ + /* Output the values of all registers. */ + for (i = 0; i <= GDBSTUB_MAX_REG; i++) + gdbstub_client_respond_hex(client, buf, gdbstub_client_read_reg(i, buf)); + break; + + case 'G': /* write all registers */ + /* Write the values of all registers. */ + for (i = 0; i <= GDBSTUB_MAX_REG; i++) { + if (!gdbstub_client_read_hex(client, buf, sizeof(buf))) + break; + client->packet_pos += gdbstub_client_write_reg(i, buf); + } + break; + + case 'H': /* set thread */ + /* Read operation type and thread ID. */ + if ((client->packet[1] == '\0') || (client->packet[2] == '\0')) { +e22: client->response_pos = sprintf(client->response, "E22"); + break; + } + + /* Respond positively only on thread 1. */ + if ((client->packet[2] == '1') && !client->packet[3]) + client->response_pos = sprintf(client->response, "OK"); + else + goto e22; + break; + + case 'm': /* read memory */ + /* Read address and length. */ + if (!(i = gdbstub_client_read_word(client, &j))) + goto e22; + client->packet_pos += i + 1; + gdbstub_client_read_word(client, &k); + if (!k) + goto e22; + + /* Clamp length. */ + if (k >= (sizeof(client->response) >> 1)) + k = (sizeof(client->response) >> 1) - 1; + + /* Read by qwords, then by dwords, then by words, then by bytes. */ + i = 0; + if (is386) { + for (; i < (k & ~7); i += 8) { + *((uint64_t *) buf) = readmemql(j); + j += 8; + gdbstub_client_respond_hex(client, buf, 8); + } + for (; i < (k & ~3); i += 4) { + *((uint32_t *) buf) = readmemll(j); + j += 4; + gdbstub_client_respond_hex(client, buf, 4); + } + } + for (; i < (k & ~1); i += 2) { + *((uint16_t *) buf) = readmemwl(j); + j += 2; + gdbstub_client_respond_hex(client, buf, 2); + } + for (; i < k; i++) { + buf[0] = readmembl(j++); + gdbstub_client_respond_hex(client, buf, 1); + } + break; + + case 'M': /* write memory */ + case 'X': /* write memory binary */ + /* Read address and length. */ + if (!(i = gdbstub_client_read_word(client, &j))) + goto e22; + client->packet_pos += i + 1; + client->packet_pos += gdbstub_client_read_word(client, &k) + 1; + if (!k) + goto e22; + + /* Clamp length. */ + if (k >= ((sizeof(client->response) >> 1) - client->packet_pos)) + k = (sizeof(client->response) >> 1) - client->packet_pos - 1; + + /* Decode the data. */ + if (client->packet[0] == 'M') { /* hex encoded */ + gdbstub_client_read_hex(client, (uint8_t *) client->packet, k); + } else { /* binary encoded */ + i = 0; + while (i < k) { + if (client->packet[client->packet_pos] == '}') { + client->packet_pos++; + client->packet[i++] = client->packet[client->packet_pos++] ^ 0x20; + } else { + client->packet[i++] = client->packet[client->packet_pos++]; + } + } + } + + /* Write by qwords, then by dwords, then by words, then by bytes. */ + p = client->packet; + i = 0; + if (is386) { + for (; i < (k & ~7); i += 8) { + writememql(j, *((uint64_t *) p)); + j += 8; + p += 8; + } + for (; i < (k & ~3); i += 4) { + writememll(j, *((uint32_t *) p)); + j += 4; + p += 4; + } + } + for (; i < (k & ~1); i += 2) { + writememwl(j, *((uint16_t *) p)); + j += 2; + p += 2; + } + for (; i < k; i++) { + writemembl(j++, p[0]); + p++; + } + + /* Respond positively. */ + client->response_pos = sprintf(client->response, "OK"); + break; + + case 'p': /* read register */ + /* Read register index. */ + if (!gdbstub_client_read_word(client, &j)) { +e14: client->response_pos = sprintf(client->response, "E14"); + break; + } + + /* Read the register's value. */ + if (!(i = gdbstub_client_read_reg(j, buf))) + goto e14; + + /* Return value. */ + gdbstub_client_respond_hex(client, buf, i); + break; + + case 'P': /* write register */ + /* Read register index and value. */ + if (!(i = gdbstub_client_read_word(client, &j))) + goto e14; + client->packet_pos += i + 1; + if (!gdbstub_client_read_hex(client, buf, sizeof(buf))) + goto e14; + + /* Write the value to the register. */ + if (!gdbstub_client_write_reg(j, buf)) + goto e14; + + /* Respond positively. */ + client->response_pos = sprintf(client->response, "OK"); + break; + + case 'q': /* query */ + /* Erase response, as we'll use it as a scratch buffer. */ + memset(client->response, 0, sizeof(client->response)); + + /* Read the query type. */ + client->packet_pos += gdbstub_client_read_string(client, client->response, sizeof(client->response) - 1, ':') + 1; + + /* Perform the query. */ + if (!strcmp(client->response, "Supported")) { + /* Go through the feature list and negate ones we don't support. */ + while ((client->response_pos < (sizeof(client->response) - 1)) && + (i = gdbstub_client_read_string(client, &client->response[client->response_pos], sizeof(client->response) - client->response_pos - 1, ';'))) { + client->packet_pos += i + 1; + if (strncmp(&client->response[client->response_pos], "PacketSize", 10) && + strcmp(&client->response[client->response_pos], "swbreak") && + strcmp(&client->response[client->response_pos], "hwbreak") && + strncmp(&client->response[client->response_pos], "xmlRegisters", 12) && + strcmp(&client->response[client->response_pos], "qXfer:features:read")) { + gdbstub_log("GDB Stub: Feature \"%s\" is not supported\n", &client->response[client->response_pos]); + client->response_pos += i; + client->response[client->response_pos++] = '-'; + client->response[client->response_pos++] = ';'; + } else { + gdbstub_log("GDB Stub: Feature \"%s\" is supported\n", &client->response[client->response_pos]); + } + } + + /* Add our supported features to the end. */ + if (client->response_pos < (sizeof(client->response) - 1)) + client->response_pos += snprintf(&client->response[client->response_pos], sizeof(client->response) - client->response_pos, + "PacketSize=%X;swbreak+;hwbreak+;qXfer:features:read+", sizeof(client->packet) - 1); + break; + } else if (!strcmp(client->response, "Xfer")) { + /* Read the transfer object. */ + client->packet_pos += gdbstub_client_read_string(client, client->response, sizeof(client->response) - 1, ':') + 1; + if (!strcmp(client->response, "features")) { + /* Read the transfer operation. */ + client->packet_pos += gdbstub_client_read_string(client, client->response, sizeof(client->response) - 1, ':') + 1; + if (!strcmp(client->response, "read")) { + /* Read the transfer annex. */ + client->packet_pos += gdbstub_client_read_string(client, client->response, sizeof(client->response) - 1, ':') + 1; + if (!strcmp(client->response, "target.xml")) + p = (char *) target_xml; + else + p = NULL; + + /* Stop if the file wasn't found. */ + if (!p) { +e00: client->response_pos = sprintf(client->response, "E00"); + break; + } + + /* Read offset and length. */ + if (!(i = gdbstub_client_read_word(client, &j))) + goto e22; + client->packet_pos += i + 1; + client->packet_pos += gdbstub_client_read_word(client, &k) + 1; + if (!k) + goto e22; + + /* Check if the offset is valid. */ + l = strlen(p); + if (j > l) + goto e00; + p += j; + + /* Return the more/less flag while also clamping the length. */ + if (k >= ((sizeof(client->response) >> 1) - 2)) + k = (sizeof(client->response) >> 1) - 3; + if (k < (l - j)) { + client->response[client->response_pos++] = 'm'; + } else { + client->response[client->response_pos++] = 'l'; + k = l - j; + } + + /* Encode the data. */ + while (k--) { + i = *p++; + if ((i == '#') || (i == '$') || (i == '*') || (i == '}')) { + client->response[client->response_pos++] = '}'; + client->response[client->response_pos++] = i ^ 0x20; + } else { + client->response[client->response_pos++] = i; + } + } + break; + } + } + } + + /* No response by default. */ + client->response_pos = 0; + break; + } + + /* Send response. */ + gdbstub_client_respond(client); +} + + +static void +gdbstub_client_thread(void *priv) +{ + gdbstub_client_t *client = (gdbstub_client_t *) priv, *other_client; + uint8_t buf[256]; + ssize_t bytes_read; + int i; + + gdbstub_log("GDB Stub: New connection from %s:%d\n", inet_ntoa(client->addr.sin_addr), client->addr.sin_port); + + /* Read data from client. */ + while ((bytes_read = recv(client->socket, (char *) buf, sizeof(buf), 0)) > 0) { + for (i = 0; i < bytes_read; i++) { + switch (buf[i]) { + case '$': /* packet start */ + client->packet_pos = 0; + break; + + case '-': /* negative acknowledgement */ + /* Retransmit the current response. */ + gdbstub_client_respond(client); + break; + + case '+': /* positive acknowledgement */ + /* Allow another response to be written. */ + thread_set_event(client->response_event); + break; + + case 0x03: /* break */ + /* Break immediately. */ + gdbstub_log("GDB Stub: Break requested\n"); + gdbstub_paused = 0; + gdbstub_break(); + break; + + default: + if (client->packet_pos < (sizeof(client->packet) - 1)) { + /* Append byte to the packet. */ + client->packet[client->packet_pos++] = buf[i]; + + /* Check if this is the end of a packet. */ + if ((client->packet_pos >= 3) && (client->packet[client->packet_pos - 3] == '#')) { /* packet checksum start */ + gdbstub_client_packet(client); + client->packet_pos = 0; + } + } + break; + } + } + } + + gdbstub_log("GDB Stub: Connection with %s:%d broken\n", inet_ntoa(client->addr.sin_addr), client->addr.sin_port); + + /* Close socket. */ + if (client->socket != -1) { + close(client->socket); + client->socket = -1; + } + + /* Unblock anyone waiting on the response event. */ + thread_set_event(client->response_event); + + /* Remove this client from the list. */ + thread_wait_mutex(client_list_mutex); + if (client == first_client) { + first_client = client->next; + if (first_client == NULL) + last_client = NULL; + gdbstub_paused = 0; /* allow user to unpause when all clients are disconnected */ + } else { + other_client = first_client; + while (other_client) { + if (other_client->next == client) { + if (last_client == client) + last_client = other_client; + other_client->next = client->next; + break; + } + other_client = other_client->next; + } + } + free(client); + thread_release_mutex(client_list_mutex); +} + + +static void +gdbstub_server_thread(void *priv) +{ + /* Listen on GDB socket. */ + listen(gdbstub_socket, 1); + + /* Accept connections. */ + gdbstub_client_t *client; + socklen_t sl = sizeof(struct sockaddr_in); + while (1) { + /* Allocate client structure. */ + client = malloc(sizeof(gdbstub_client_t)); + memset(client, 0, sizeof(gdbstub_client_t)); + client->response_event = thread_create_event(); + + /* Accept connection. */ + client->socket = accept(gdbstub_socket, (struct sockaddr *) &client->addr, &sl); + if (client->socket < 0) + break; + + /* Add to client list. */ + thread_wait_mutex(client_list_mutex); + if (first_client) { + last_client->next = client; + last_client = client; + } else { + first_client = last_client = client; + } + thread_release_mutex(client_list_mutex); + + /* Pause emulation. */ + gdbstub_paused = 1; + gdbstub_break(); + + /* Start client thread. */ + thread_create(gdbstub_client_thread, client); + } + + /* Deallocate the redundant client structure. */ + thread_destroy_event(client->response_event); + free(client); +} + + +void +gdbstub_pause(int *p) +{ + if (!(*p) && gdbstub_paused) { + /* Don't allow the user to unpause if we're pausing. */ + gdbstub_log("GDB Stub: Blocked user unpause\n"); + *p = 1; + } else if (*p) { + sprintf(stop_reason, "S%02X", gdbstub_singlestep ? GDB_SIGTRAP : GDB_SIGINT); + if (!gdbstub_paused) { + /* Send interrupt packet to all clients. */ + gdbstub_log("GDB Stub: Pausing\n"); + gdbstub_paused = 1; + thread_wait_mutex(client_list_mutex); + gdbstub_client_t *client = first_client; + while (client) { + if (!thread_wait_event(client->response_event, -1)) { + /* Block other responses from being written while this one isn't acknowledged. */ + thread_reset_event(client->response_event); + + /* Write stop reply packet. */ + client->response_pos = sprintf(client->response, "%s", stop_reason); + gdbstub_client_respond(client); + } else { + gdbstub_log("GDB Stub: Timed out waiting for client %08X\n", client); + } + client = client->next; + } + thread_release_mutex(client_list_mutex); + } + } +} + + +void +gdbstub_init() +{ +#ifdef _WIN32 + WSAStartup(MAKEWORD(2, 2), &wsa); +#endif + + /* Create GDB server socket. */ + if ((gdbstub_socket = socket(AF_INET, SOCK_STREAM, 0)) == -1) { + pclog("GDB Stub: Failed to create socket\n"); + return; + } + + /* Bind GDB server socket. */ + int port = 12345; + struct sockaddr_in bind_addr = { + .sin_family = AF_INET, + .sin_addr = { .s_addr = INADDR_ANY }, + .sin_port = htons(port) + }; + if (bind(gdbstub_socket, (struct sockaddr *) &bind_addr, sizeof(bind_addr)) == -1) { + pclog("GDB Stub: Failed to bind on port %d (%d)\n", port, WSAGetLastError()); + gdbstub_socket = -1; + return; + } + + /* Create client list mutex. */ + client_list_mutex = thread_create_mutex(); + + /* Start server thread. */ + pclog("GDB Stub: Listening on port %d\n", port); + thread_create(gdbstub_server_thread, NULL); +} + + +void +gdbstub_close() +{ + /* Stop if the GDB server hasn't initialized. */ + if (gdbstub_socket < 0) + return; + + /* Close GDB server socket. */ + close(gdbstub_socket); + + /* Clear client list. */ + thread_wait_mutex(client_list_mutex); + gdbstub_client_t *client = first_client; + int socket; + while (client) { + socket = client->socket; + client->socket = -1; + close(socket); + client = client->next; + } + thread_release_mutex(client_list_mutex); + thread_close_mutex(client_list_mutex); +} diff --git a/src/include/86box/gdbstub.h b/src/include/86box/gdbstub.h new file mode 100644 index 000000000..d46c8fe6d --- /dev/null +++ b/src/include/86box/gdbstub.h @@ -0,0 +1,38 @@ +/* + * 86Box A hypervisor and IBM PC system emulator that specializes in + * running old operating systems and software designed for IBM + * PC systems and compatibles from 1981 through fairly recent + * system designs based on the PCI bus. + * + * This file is part of the 86Box distribution. + * + * Definitions for the GDB stub server. + * + * + * + * Authors: RichardG, + * + * Copyright 2022 RichardG. + */ +#ifndef EMU_GDBSTUB_H +# define EMU_GDBSTUB_H + +#ifdef USE_GDBSTUB + +extern int gdbstub_singlestep; + +extern void gdbstub_pause(int *p); +extern void gdbstub_init(); +extern void gdbstub_close(); + +#else + +#define gdbstub_singlestep 0 + +#define gdbstub_pause(p) +#define gdbstub_init() +#define gdbstub_close() + +#endif + +#endif diff --git a/src/qt/qt_platform.cpp b/src/qt/qt_platform.cpp index 4ec31b842..b4bd38e83 100644 --- a/src/qt/qt_platform.cpp +++ b/src/qt/qt_platform.cpp @@ -92,6 +92,7 @@ extern "C" { #include "../cpu/cpu.h" #include <86box/plat.h> +#include <86box/gdbstub.h> volatile int cpu_thread_run = 1; int mouse_capture = 0; @@ -314,6 +315,8 @@ plat_pause(int p) static wchar_t oldtitle[512]; wchar_t title[512], paused_msg[64]; + gdbstub_pause(&p); + if (p == dopause) { #ifdef Q_OS_WINDOWS if (source_hwnd) diff --git a/src/unix/unix.c b/src/unix/unix.c index 83f6b4a9b..ead580372 100644 --- a/src/unix/unix.c +++ b/src/unix/unix.c @@ -32,6 +32,7 @@ #include <86box/timer.h> #include <86box/nvr.h> #include <86box/ui.h> +#include <86box/gdbstub.h> static int first_use = 1; static uint64_t StartingTime; @@ -719,6 +720,8 @@ plat_pause(int p) static wchar_t oldtitle[512]; wchar_t title[512]; + gdbstub_pause(&p); + if ((p == 0) && (time_sync & TIME_SYNC_ENABLED)) nvr_time_sync(); diff --git a/src/win/win_ui.c b/src/win/win_ui.c index 1a9ad961a..b99ca6414 100644 --- a/src/win/win_ui.c +++ b/src/win/win_ui.c @@ -44,6 +44,7 @@ #include <86box/win.h> #include <86box/version.h> #include <86box/discord.h> +#include <86box/gdbstub.h> #ifdef MTR_ENABLED #include @@ -1496,6 +1497,8 @@ plat_pause(int p) static wchar_t oldtitle[512]; wchar_t title[512]; + gdbstub_pause(&p); + /* If un-pausing, as the renderer if that's OK. */ if (p == 0) p = get_vidpause();