diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..c418c4a --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,33 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "(gdb) Launch", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/mini-gdbstub/build/emu", + "args": ["./mini-gdbstub/build/emu_test.bin"], + "stopAtEntry": false, + "cwd": "${workspaceFolder}/mini-gdbstub", + "environment": [], + "externalConsole": false, + "MIMode": "gdb", + "setupCommands": [ + { + "description": "Enable pretty-printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + }, + { + "description": "Set Disassembly Flavor to Intel", + "text": "-gdb-set disassembly-flavor intel", + "ignoreFailures": true + } + ] + } + + ] +} \ No newline at end of file diff --git a/mini-gdbstub/.gitignore b/mini-gdbstub/.gitignore new file mode 100644 index 0000000..378eac2 --- /dev/null +++ b/mini-gdbstub/.gitignore @@ -0,0 +1 @@ +build diff --git a/mini-gdbstub/Makefile b/mini-gdbstub/Makefile new file mode 100644 index 0000000..3b098f6 --- /dev/null +++ b/mini-gdbstub/Makefile @@ -0,0 +1,47 @@ +CFLAGS = -Iinclude -Wall -Wextra -MMD #-Werror + +CURDIR := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))) + +O ?= build +OUT := $(O) +EMU_OUT := $(abspath $(OUT)) + +SHELL_HACK := $(shell mkdir -p $(OUT)) +GIT_HOOKS := .git/hooks/applied + +LIBSRCS = $(shell find ./src -name '*.c') +_LIB_OBJ = $(notdir $(LIBSRCS)) +LIB_OBJ = $(_LIB_OBJ:%.c=$(OUT)/%.o) + +TEST_OBJ = $(EMU_OUT)/emu_test.obj +TEST_BIN = $(EMU_OUT)/emu_test.bin + +vpath %.c $(sort $(dir $(LIBSRCS))) +.PHONY: all debug clean + +all: CFLAGS += -O0 -g +all: LDFLAGS += -O0 + +debug: CFLAGS += -O0 -g -DDEBUG +debug: LDFLAGS += -O0 + +$(GIT_HOOKS): + @scripts/install-git-hooks + @echo + +$(OUT)/%.o: %.c + $(CC) -c $(CFLAGS) $< -o $@ + +emu: + cd src && make all + +stub: emu + $(EMU_OUT)/emu $(TEST_BIN) + +gdb: emu + gdb-multiarch $(TEST_OBJ) -ex "set architecture riscv:rv32" -ex "target remote 127.0.0.1:1234" + +clean: + $(RM) -rf build + +-include $(OUT)/*.d diff --git a/mini-gdbstub/README.md b/mini-gdbstub/README.md new file mode 100644 index 0000000..870af08 --- /dev/null +++ b/mini-gdbstub/README.md @@ -0,0 +1,128 @@ +# mini-gdbstub + +`mini-gdbstub` is an implementation of the +[GDB Remote Serial Protocol](https://sourceware.org/gdb/onlinedocs/gdb/Remote-Protocol.html) +that gives your emulators debugging capabilities. + +## Usage + +The very first thing you should do is to build the statically-linked library `libgdbstub.a`. +``` +make +``` + +To use the library in your project, you should include the file `gdbstub.h` first. +Then, you have to initialize the pre-allocated structure `gdbstub_t` with `gdbstub_init`. + +```c +bool gdbstub_init(gdbstub_t *gdbstub, struct target_ops *ops, arch_info_t arch, char *s); +``` + +The parameters `s` is the easiest one to understand. It is a string of the socket +which your emulator would like to bind as gdb server. + +The `struct target_ops` is made up of function pointers. Each member function represents an +abstraction of your emulator's operation. The following lists the requirement +that should be provided for each method: + +Method | Description +---------------|------------------ +`cont` | Run the emulator until hitting breakpoint or exit. +`stepi` | Do one step on the emulator. You may define your own step for the emulator. For example, the common design is executing one instruction. +`get_reg_bytes` | Get the register size in bytes for the register specified by `regno` as a return value. +`read_reg` | Read the value of the register specified by `regno` to `*value`. Return zero if the operation success, otherwise return an errno for the corresponding error. +`write_reg` | Write value `value` to the register specified by `regno`. Return zero if the operation success, otherwise return an errno for the corresponding error. +`read_mem` | Read the memory according to the address specified by `addr` with size `len` to the buffer `*val`. Return zero if the operation success, otherwise return an errno for the corresponding error. +`write_mem` | Write data in the buffer `val` with size `len` to the memory which address is specified by `addr`. Return zero if the operation success, otherwise return an errno for the corresponding error. +`set_bp` | Set type `type` breakpoint on the address specified by `addr`. Return true if we set the breakpoint successfully, otherwise return false. +`del_bp` | Delete type `type` breakpoint on the address specified by `addr`. Return true if we delete the breakpoint successfully, otherwise return false. +`on_interrupt` | Do something when receiving interrupt from GDB client. This method will run concurrently with `cont`, so you should be careful if there're shared data between them. You will need a lock or something similar to avoid data race. +`set_cpu` | Set the debug target CPU to `cpuid`. +`get_cpu` | Get the current debug target CPU `cpuid` as return value. + +```c +struct target_ops { + gdb_action_t (*cont)(void *args); + gdb_action_t (*stepi)(void *args); + size_t (*get_reg_bytes)(int regno); + int (*read_reg)(void *args, int regno, void *value); + int (*write_reg)(void *args, int regno, void* value); + int (*read_mem)(void *args, size_t addr, size_t len, void *val); + int (*write_mem)(void *args, size_t addr, size_t len, void *val); + bool (*set_bp)(void *args, size_t addr, bp_type_t type); + bool (*del_bp)(void *args, size_t addr, bp_type_t type); + void (*on_interrupt)(void *args); + + void (*set_cpu)(void *args, int cpuid); + int (*get_cpu)(void *args); +}; +``` + +For `cont` and `stepi` which are used to process the execution of emulator, their return type +should be `gdb_action_t`. After performing the relevant operation, you should return `ACT_RESUME` +to continue debugging; otherwise, return `ACT_SHUTDOWN` to finish debugging. The library +typically uses `ACT_NONE` to take no action. + +```c +typedef enum { + ACT_NONE, + ACT_RESUME, + ACT_SHUTDOWN, +} gdb_action_t; +``` + +For `set_bp` and `del_bp`, the type of breakpoint which should be set or deleted is described +in the type `bp_type_t`. In fact, its value will always be `BP_SOFTWARE` currently. + +```c +typedef enum { + BP_SOFTWARE = 0, +} bp_type_t; +``` + +Another structure you have to declare is `arch_info_t`. You must explicitly specify about the +following field within `arch_info_t` while integrating into your emulator: +* `smp`: Number of target's CPU +* `reg_num`: Number of target's registers + +The `target_desc` is an optional member which could be +`TARGET_RV32`, `TARGET_RV64` if the emulator is RISC-V 32-bit or 64-bit instruction +set architecture or `TARGET_X86_64` if the emulator is x86_64 instruction set architecture. Alternatively, it can be a custom target description document +string used by gdb. If none of these apply, simply set it to NULL. + +* Although the value of `reg_num` may be determined by `target_desc`, those +members are still required to be filled correctly. + +```c +typedef struct { + char *target_desc; + int smp; + int reg_num; +} arch_info_t; +``` + +After startup, we can use `gdbstub_run` to run the emulator as gdbstub. The `args` +can be used to pass the argument to any function in `struct target_ops`. + +```c +bool gdbstub_run(gdbstub_t *gdbstub, void *args); +``` + +When exiting from `gdbstub_run`, `gdbstub_close` should be called to recycle the resource on +the initialization. + +```c +void gdbstub_close(gdbstub_t *gdbstub); +``` + +Finally, you can build you project with the statically-linked library `libgdbstub.a` now! +Additionally, it is advised that you check the reference emulator in the directory `emu,` which +demonstrates how to integrate `mini-gdbstub` into your project. + +## Reference +### Project +* [bet4it/gdbserver](https://github.com/bet4it/gdbserver) +* [devbored/minigdbstub](https://github.com/devbored/minigdbstub) +### Article +* [Howto: GDB Remote Serial Protocol](https://www.embecosm.com/appnotes/ean4/embecosm-howto-rsp-server-ean4-issue-2.html) +* [TLMBoy: Implementing the GDB Remote Serial Protocol](https://www.chciken.com/tlmboy/2022/04/03/gdb-z80.html) diff --git a/mini-gdbstub/include/conn.h b/mini-gdbstub/include/conn.h new file mode 100644 index 0000000..4017d91 --- /dev/null +++ b/mini-gdbstub/include/conn.h @@ -0,0 +1,25 @@ +#ifndef CONN_H +#define CONN_H + +#include +#include +#include "packet.h" + +#define MAX_SEND_PACKET_SIZE (0x1000) +#define MAX_DATA_PAYLOAD (MAX_SEND_PACKET_SIZE - (2 + CSUM_SIZE + 2)) + +typedef struct { + int listen_fd; + int socket_fd; + + pktbuf_t pktbuf; +} conn_t; + +bool conn_init(conn_t *conn, char *addr_str, int port); +void conn_recv_packet(conn_t *conn); +packet_t *conn_pop_packet(conn_t *conn); +bool conn_try_recv_intr(conn_t *conn); +void conn_send_str(conn_t *conn, char *str); +void conn_send_pktstr(conn_t *conn, char *pktstr); +void conn_close(conn_t *conn); +#endif diff --git a/mini-gdbstub/include/gdb_signal.h b/mini-gdbstub/include/gdb_signal.h new file mode 100644 index 0000000..f3e8516 --- /dev/null +++ b/mini-gdbstub/include/gdb_signal.h @@ -0,0 +1,6 @@ +#ifndef GDB_SIGNAL_H +#define GDB_SIGNAL_H + +#define GDB_SIGNAL_TRAP 5 + +#endif diff --git a/mini-gdbstub/include/gdbstub.h b/mini-gdbstub/include/gdbstub.h new file mode 100644 index 0000000..655d3f9 --- /dev/null +++ b/mini-gdbstub/include/gdbstub.h @@ -0,0 +1,69 @@ +#ifndef GDBSTUB_H +#define GDBSTUB_H + +#include +#include + +#define TARGET_RV32 \ + "riscv:rv32" +#define TARGET_RV64 \ + "riscv:rv64" +#define TARGET_X86_64 \ + "i386:x86-64" + +typedef enum { + EVENT_NONE, + EVENT_CONT, + EVENT_DETACH, + EVENT_STEP, +} gdb_event_t; + +typedef enum { + ACT_NONE, + ACT_RESUME, + ACT_SHUTDOWN, +} gdb_action_t; + +typedef enum { + BP_SOFTWARE = 0, +} bp_type_t; + +struct target_ops { + gdb_action_t (*cont)(void *args); + gdb_action_t (*stepi)(void *args); + size_t (*get_reg_bytes)(int regno); + int (*read_reg)(void *args, int regno, void *value); + int (*write_reg)(void *args, int regno, void *value); + int (*read_mem)(void *args, size_t addr, size_t len, void *val); + int (*write_mem)(void *args, size_t addr, size_t len, void *val); + bool (*set_bp)(void *args, size_t addr, bp_type_t type); + bool (*del_bp)(void *args, size_t addr, bp_type_t type); + void (*on_interrupt)(void *args); + + void (*set_cpu)(void *args, int cpuid); + int (*get_cpu)(void *args); +}; + +typedef struct gdbstub_private gdbstub_private_t; + +typedef struct { + char *target_desc; + int smp; + int reg_num; +} arch_info_t; + +typedef struct { + struct target_ops *ops; + arch_info_t arch; + gdbstub_private_t *priv; +} gdbstub_t; + +bool gdbstub_init(gdbstub_t *gdbstub, + struct target_ops *ops, + arch_info_t arch, + char *s); +bool gdbstub_run(gdbstub_t *gdbstub, void *args); +void gdbstub_close(gdbstub_t *gdbstub); + +#endif diff --git a/mini-gdbstub/include/packet.h b/mini-gdbstub/include/packet.h new file mode 100644 index 0000000..e8d4bba --- /dev/null +++ b/mini-gdbstub/include/packet.h @@ -0,0 +1,32 @@ +#ifndef PACKET_H +#define PACKET_H + +#include +#include +#include + +#define STR_ACK "+" +#define INTR_CHAR '\x03' + +#define CSUM_SIZE (2) + +typedef struct { + int end_pos; + uint8_t data[]; +} packet_t; + +/* A naive packet buffer: maintain a big array to fill the packet */ +typedef struct { + int size; /* the size for all valid characters in data buffer */ + int cap; /* the capacity (1 << cap) of the data buffer */ + int end_pos; /* the end position of the first packet in data buffer */ + uint8_t *data; +} pktbuf_t; + +bool pktbuf_init(pktbuf_t *pktbuf); +ssize_t pktbuf_fill_from_file(pktbuf_t *pktbuf, int fd); +bool pktbuf_is_complete(pktbuf_t *pktbuf); +packet_t *pktbuf_pop_packet(pktbuf_t *pktbuf); +void pktbuf_destroy(pktbuf_t *pktbuf); + +#endif diff --git a/mini-gdbstub/include/regbuf.h b/mini-gdbstub/include/regbuf.h new file mode 100644 index 0000000..588a5c7 --- /dev/null +++ b/mini-gdbstub/include/regbuf.h @@ -0,0 +1,15 @@ +#ifndef REG_H +#define REG_H + +#include +#include + +typedef struct { + void *buf; + size_t sz; +} regbuf_t; + +bool regbuf_init(regbuf_t *reg); +void *regbuf_get(regbuf_t *reg, size_t reg_sz); +void regbuf_destroy(regbuf_t *reg); +#endif diff --git a/mini-gdbstub/include/utils/csum.h b/mini-gdbstub/include/utils/csum.h new file mode 100644 index 0000000..8782cdc --- /dev/null +++ b/mini-gdbstub/include/utils/csum.h @@ -0,0 +1,9 @@ +#ifndef CSUM_H +#define CSUM_H + +#include +#include + +uint8_t compute_checksum(char *buf, size_t len); + +#endif diff --git a/mini-gdbstub/include/utils/log.h b/mini-gdbstub/include/utils/log.h new file mode 100644 index 0000000..970a8ea --- /dev/null +++ b/mini-gdbstub/include/utils/log.h @@ -0,0 +1,22 @@ +#ifndef LOG_H +#define LOG_H + +#include +#include +#include +#include +#include + +static inline void log_print(const char *format, ...) +{ + va_list vargs; + va_start(vargs, format); + vfprintf(stderr, format, vargs); + va_end(vargs); +} + +#define warn(format, ...) \ + log_print("ERRNO: %s\n" format, strerror(errno), ##__VA_ARGS__) +#define info(format, ...) log_print(format, ##__VA_ARGS__) + +#endif diff --git a/mini-gdbstub/include/utils/translate.h b/mini-gdbstub/include/utils/translate.h new file mode 100644 index 0000000..e1fafa8 --- /dev/null +++ b/mini-gdbstub/include/utils/translate.h @@ -0,0 +1,10 @@ +#ifndef UTILS_H +#define UTILS_H + +#include + +void hex_to_str(uint8_t *num, char *str, int bytes); +void str_to_hex(char *str, uint8_t *num, int bytes); +int unescape(char *msg, char *end); + +#endif diff --git a/mini-gdbstub/src/Makefile b/mini-gdbstub/src/Makefile new file mode 100644 index 0000000..1f7f293 --- /dev/null +++ b/mini-gdbstub/src/Makefile @@ -0,0 +1,35 @@ +CFLAGS = -I../include -Wall -Wextra -g +LDFLAGS = + +CURDIR := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))) + +O ?= ../build +OUT := $(O) + +BIN = $(OUT)/emu +SHELL_HACK := $(shell mkdir -p $(OUT)) + +CSRCS = $(shell find ./ -name '*.c') +_COBJ = $(notdir $(CSRCS)) +COBJ = $(_COBJ:%.c=$(OUT)/%.o) + +TEST_NAME = emu_test +TEST_OBJ = $(OUT)/$(TEST_NAME).obj +TEST_BIN = $(OUT)/$(TEST_NAME).bin + +vpath %.c $(sort $(dir $(CSRCS))) +.PHONY: all + +all: $(BIN) $(TEST_BIN) + +$(OUT)/%.o: %.c + $(CC) -c $(CFLAGS) $< -o $@ + +$(TEST_OBJ): emu_test.cc + riscv64-unknown-elf-gcc -march=rv32i -mabi=ilp32 -Wl,-Ttext=0x0 -nostdlib -g -o $@ $< + +$(TEST_BIN): $(TEST_OBJ) + riscv64-unknown-elf-objcopy -O binary $< $@ + +$(BIN): $(COBJ) + $(CC) $^ -o $@ $(LDFLAGS) diff --git a/mini-gdbstub/src/conn.c b/mini-gdbstub/src/conn.c new file mode 100644 index 0000000..fbfe4ff --- /dev/null +++ b/mini-gdbstub/src/conn.c @@ -0,0 +1,177 @@ +#include "conn.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "utils/csum.h" +#include "utils/log.h" + +static bool socket_poll(int socket_fd, int timeout, int events) +{ + struct pollfd pfd = (struct pollfd){ + .fd = socket_fd, + .events = events, + }; + + return (poll(&pfd, 1, timeout) > 0) && (pfd.revents & events); +} + +static bool socket_readable(int socket_fd, int timeout) +{ + return socket_poll(socket_fd, timeout, POLLIN); +} + +static bool socket_writable(int socket_fd, int timeout) +{ + return socket_poll(socket_fd, timeout, POLLOUT); +} + +bool conn_init(conn_t *conn, char *addr_str, int port) +{ + if (!pktbuf_init(&conn->pktbuf)) + return false; + + struct in_addr addr_ip; + if (inet_aton(addr_str, &addr_ip) != 0) { + struct sockaddr_in addr; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = addr_ip.s_addr; + addr.sin_port = htons(port); + conn->listen_fd = socket(AF_INET, SOCK_STREAM, 0); + if (conn->listen_fd < 0) + return false; + + int optval = 1; + if (setsockopt(conn->listen_fd, SOL_SOCKET, SO_REUSEADDR, &optval, + sizeof(optval)) < 0) { + warn("Set sockopt fail.\n"); + goto fail; + } + + if (bind(conn->listen_fd, (struct sockaddr *) &addr, sizeof(addr)) < + 0) { + warn("Bind fail.\n"); + goto fail; + } + } else { + struct sockaddr_un addr; + addr.sun_family = AF_UNIX; + strncpy(addr.sun_path, addr_str, sizeof(addr.sun_path) - 1); + unlink(addr_str); + conn->listen_fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (conn->listen_fd < 0) + return false; + + if (bind(conn->listen_fd, (struct sockaddr *) &addr, sizeof(addr)) < + 0) { + warn("Bind fail.\n"); + goto fail; + } + } + + if (listen(conn->listen_fd, 1) < 0) { + warn("Listen fail.\n"); + goto fail; + } + + conn->socket_fd = accept(conn->listen_fd, NULL, NULL); + if (conn->socket_fd < 0) { + warn("Accept fail.\n"); + goto fail; + } + + return true; + +fail: + close(conn->listen_fd); + return false; +} + +void conn_recv_packet(conn_t *conn) +{ + while (!pktbuf_is_complete(&conn->pktbuf) && + socket_readable(conn->socket_fd, -1)) { + ssize_t nread = pktbuf_fill_from_file(&conn->pktbuf, conn->socket_fd); + if (nread == -1) + break; + } + + conn_send_str(conn, STR_ACK); +} + +packet_t *conn_pop_packet(conn_t *conn) +{ + packet_t *pkt = pktbuf_pop_packet(&conn->pktbuf); + + return pkt; +} + +bool conn_try_recv_intr(conn_t *conn) +{ + char ch; + + if (!socket_readable(conn->socket_fd, 0)) + return false; + + ssize_t nread = read(conn->socket_fd, &ch, 1); + if (nread != 1) + return false; + + /* FIXME: The character must be INTR_CHAR, otherwise the library + * may work incorrectly. However, I'm not sure if this implementation + * can always meet our expectation (concurrent is so hard QAQ). */ + assert(ch == INTR_CHAR); + return true; +} + +void conn_send_str(conn_t *conn, char *str) +{ + size_t len = strlen(str); + + while (len > 0 && socket_writable(conn->socket_fd, -1)) { + ssize_t nwrite = write(conn->socket_fd, str, len); + if (nwrite == -1) + break; + len -= nwrite; + } +} + +void conn_send_pktstr(conn_t *conn, char *pktstr) +{ + char packet[MAX_SEND_PACKET_SIZE]; + size_t len = strlen(pktstr); + + /* 2: '$' + '#' + * 2: checksum digits(maximum) + * 1: '\0' */ + assert(len + 2 + CSUM_SIZE + 1 < MAX_SEND_PACKET_SIZE); + + packet[0] = '$'; + memcpy(packet + 1, pktstr, len); + packet[len + 1] = '#'; + + char csum_str[4]; + uint8_t csum = compute_checksum(pktstr, len); + size_t csum_len = snprintf(csum_str, sizeof(csum_str) - 1, "%02x", csum); + assert(csum_len == CSUM_SIZE); + memcpy(packet + len + 2, csum_str, csum_len); + packet[len + 2 + csum_len] = '\0'; + +#ifdef DEBUG + printf("send packet = %s,", packet); + printf(" checksum = %d\n", csum); +#endif + conn_send_str(conn, packet); +} + +void conn_close(conn_t *conn) +{ + close(conn->socket_fd); + close(conn->listen_fd); + pktbuf_destroy(&conn->pktbuf); +} diff --git a/mini-gdbstub/src/emu.c b/mini-gdbstub/src/emu.c new file mode 100644 index 0000000..6f3667c --- /dev/null +++ b/mini-gdbstub/src/emu.c @@ -0,0 +1,549 @@ +#include +#include +#include +#include +#include +#include + +#include "gdbstub.h" + +#define DEBUG + +#define read_len(bit, ptr, value) \ + do { \ + value = 0; \ + for (int i = 0; i < ((bit) >> 3); i++) \ + value |= ((uint64_t) (*((ptr) + i))) << (i << 3); \ + } while (0) + +#define write_len(bit, ptr, value) \ + do { \ + for (int i = 0; i < ((bit) >> 3); i++) \ + *((ptr) + i) = (value >> (i << 3)) & 0xff; \ + } while (0) + +#define MEM_SIZE (0x1000) +#define TOHOST_ADDR (MEM_SIZE - 4) + +struct mem { + uint8_t *mem; + size_t code_size; +}; + +struct emu { + struct mem m; + uint64_t x[32]; + uint64_t pc; + + bool bp_is_set; + uint64_t bp_addr; + + bool halt; + + gdbstub_t gdbstub; +}; + +static inline void emu_halt(struct emu *emu) +{ + __atomic_store_n(&emu->halt, true, __ATOMIC_RELAXED); +} + +static inline void emu_start_run(struct emu *emu) +{ + __atomic_store_n(&emu->halt, false, __ATOMIC_RELAXED); +} + +static inline bool emu_is_halt(struct emu *emu) +{ + return __atomic_load_n(&emu->halt, __ATOMIC_RELAXED); +} + +typedef struct inst { + uint64_t inst; + + uint8_t rd; + uint8_t rs1; + uint8_t rs2; + uint8_t funct3; + uint8_t funct7; +} inst_t; + +static inline int opcode_3(struct emu *emu, inst_t *inst) +{ + uint8_t *ptr; + uint64_t value; + uint64_t imm = (int32_t) (inst->inst & 0xfff00000) >> 20; + + switch (inst->funct3) { + case 0x2: + // lw + ptr = emu->m.mem + emu->x[inst->rs1] + imm; + read_len(32, ptr, value); + emu->x[inst->rd] = value; + return 0; + case 0x3: + // ld + ptr = emu->m.mem + emu->x[inst->rs1] + imm; + read_len(64, ptr, value); + emu->x[inst->rd] = value; + return 0; + default: + break; + } + + return -1; +} + +static inline int opcode_13(struct emu *emu, inst_t *inst) +{ + uint64_t imm = (int32_t) (inst->inst & 0xfff00000) >> 20; + + switch (inst->funct3) { + case 0x0: + // addi + emu->x[inst->rd] = emu->x[inst->rs1] + imm; + return 0; + case 0x2: + // slti + emu->x[inst->rd] = (int64_t) emu->x[inst->rs1] < (int64_t) imm ? 1 : 0; + return 0; + default: + break; + } + + return -1; +} + +static inline int opcode_17(struct emu *emu, inst_t *inst) +{ + // auipc + uint64_t imm = (int32_t) (inst->inst & 0xfffff000); + emu->x[inst->rd] = emu->pc + imm - 4; + return 0; +} + +static inline int opcode_1b(struct emu *emu, inst_t *inst) +{ + uint64_t imm = (int32_t) (inst->inst & 0xfff00000) >> 20; + + switch (inst->funct3) { + case 0x0: + // addiw + emu->x[inst->rd] = + (int32_t) (((uint32_t) emu->x[inst->rs1] + (uint32_t) imm)); + return 0; + default: + break; + } + + return -1; +} + +static inline int opcode_23(struct emu *emu, inst_t *inst) +{ + uint8_t *ptr; + uint64_t imm = (((int32_t) (inst->inst & 0xfe000000) >> 20) | + (int32_t) ((inst->inst >> 7) & 0x1f)); + + switch (inst->funct3) { + case 0x0: + // sb + ptr = emu->m.mem + emu->x[inst->rs1] + imm; + write_len(8, ptr, emu->x[inst->rs2]); + return 0; + case 0x2: + // sw + ptr = emu->m.mem + emu->x[inst->rs1] + imm; + write_len(32, ptr, emu->x[inst->rs2]); + return 0; + case 0x3: + // sd + ptr = emu->m.mem + emu->x[inst->rs1] + imm; + write_len(64, ptr, emu->x[inst->rs2]); + return 0; + default: + break; + } + + return -1; +} + +static inline int opcode_33(struct emu *emu, inst_t *inst) +{ + switch (inst->funct3) { + case 0x0: + switch (inst->funct7) { + case 0x00: + // add + emu->x[inst->rd] = emu->x[inst->rs1] + emu->x[inst->rs2]; + return 0; + default: + break; + } + break; + default: + break; + } + + return -1; +} + +static inline int opcode_37(struct emu *emu, inst_t *inst) +{ + // lui + uint64_t imm = (int32_t) (inst->inst & 0xfffff000); + emu->x[inst->rd] = imm; + return 0; +} + +static inline int opcode_3b(struct emu *emu, inst_t *inst) +{ + switch (inst->funct3) { + case 0x0: + switch (inst->funct7) { + case 0x00: + // addw + emu->x[inst->rd] = (int32_t) ((uint32_t) emu->x[inst->rs1] + + (uint32_t) emu->x[inst->rs2]); + return 0; + default: + break; + } + break; + default: + break; + } + + return -1; +} + +static inline int opcode_67(struct emu *emu, inst_t *inst) +{ + // jalr + uint64_t imm = (int32_t) (inst->inst & 0xfff00000) >> 20; + emu->x[inst->rd] = emu->pc; + emu->pc = (emu->x[inst->rs1] + imm) & ~1; + + return 0; +} + +static inline int opcode_6f(struct emu *emu, inst_t *inst) +{ + uint64_t imm; + + // jal + emu->x[inst->rd] = emu->pc; + // imm[20|10:1|11|19:12] = inst[31|30:21|20|19:12] + imm = ((int32_t) (inst->inst & 0x80000000) >> 11) // 20 + | (int32_t) ((inst->inst & 0xff000)) // 19:12 + | (int32_t) ((inst->inst >> 9) & 0x800) // 11 + | (int32_t) ((inst->inst >> 20) & 0x7fe); // 10:1 + emu->pc = emu->pc + imm - 4; + return 0; +} + +static int emu_exec(struct emu *emu, uint32_t raw_inst) +{ + int ret = -1; + uint8_t opcode = raw_inst & 0x7f; + + inst_t inst; + + // Emulate register x0 to 0 + emu->x[0] = 0; + + inst.inst = raw_inst; + inst.rd = (raw_inst >> 7) & 0x1f; + inst.rs1 = (raw_inst >> 15) & 0x1f; + inst.rs2 = (raw_inst >> 20) & 0x1f; + inst.funct3 = (raw_inst >> 12) & 0x7; + inst.funct7 = (raw_inst >> 25) & 0x7f; + +#ifdef DEBUG + printf("[%4lx] opcode: %2x, funct3: %x, funct7: %2x\n", emu->pc - 4, opcode, + inst.funct3, inst.funct7); +#endif + + switch (opcode) { + case 0x3: + ret = opcode_3(emu, &inst); + break; + case 0x13: + ret = opcode_13(emu, &inst); + break; + case 0x17: + ret = opcode_17(emu, &inst); + break; + case 0x1b: + ret = opcode_1b(emu, &inst); + break; + case 0x23: + ret = opcode_23(emu, &inst); + break; + case 0x33: + ret = opcode_33(emu, &inst); + break; + case 0x37: + ret = opcode_37(emu, &inst); + break; + case 0x3b: + ret = opcode_3b(emu, &inst); + break; + case 0x67: + ret = opcode_67(emu, &inst); + break; + case 0x6f: + ret = opcode_6f(emu, &inst); + break; + default: + break; + } + +#ifdef DEBUG + static char *abi_name[] = { + "z", "ra", "sp", "gp", "tp", "t0", "t1", "t2", "s0", "s1", "a0", + "a1", "a2", "a3", "a4", "a5", "a6", "a7", "s2", "s3", "s4", "s5", + "s6", "s7", "s8", "s9", "s10", "s11", "t3", "t4", "t5", "t6"}; + + for (size_t i = 0; i < 32; i++) { + printf("x%-2ld(%-3s) = 0x%-16lx, ", i, abi_name[i], emu->x[i]); + if (!((i + 1) & 1)) + printf("\n"); + } + printf("\n"); +#endif + + if (ret != 0) { + printf("Not implemented or invalid instruction@%lx\n", emu->pc - 4); + printf("opcode:%x, funct3:%x, funct7:%x\n", opcode, inst.funct3, + inst.funct7); + } + + return ret; +} + +static void emu_init(struct emu *emu) +{ + memset(emu, 0, sizeof(struct emu)); + emu->pc = 0; + emu->x[2] = TOHOST_ADDR; + emu->bp_addr = -1; + emu_halt(emu); +} + +static int init_mem(struct mem *m, const char *filename) +{ + if (!filename) { + return -1; + } + + FILE *fp = fopen(filename, "rb"); + if (!fp) + return -1; + + fseek(fp, 0, SEEK_END); + size_t sz = ftell(fp) * sizeof(uint8_t); + rewind(fp); + + /* We leave extra four bytes as the hint stop the emulator, so + * the size of the binary should not exceed this. */ + if (sz > TOHOST_ADDR) + return -1; + + m->mem = malloc(MEM_SIZE); + if (!m->mem) { + fclose(fp); + return -1; + } + memset(m->mem, 0, MEM_SIZE); + size_t read_size = fread(m->mem, sizeof(uint8_t), sz, fp); + + if (read_size != sz) { + fclose(fp); + return -1; + } + fclose(fp); + m->code_size = read_size; + return 0; +} + +static void free_mem(struct mem *m) +{ + free(m->mem); +} + +static size_t emu_get_reg_bytes(int regno __attribute__((unused))) +{ + return 4; +} + +static int emu_read_reg(void *args, int regno, void *reg_value) +{ + struct emu *emu = (struct emu *) args; + if (regno > 32) { + return EFAULT; + } + + if (regno == 32) { + memcpy(reg_value, &emu->pc, 4); + } else { + memcpy(reg_value, &emu->x[regno], 4); + } + return 0; +} + +static int emu_write_reg(void *args, int regno, void *data) +{ + struct emu *emu = (struct emu *) args; + + if (regno > 32) { + return EFAULT; + } + + if (regno == 32) { + memcpy(&emu->pc, data, 4); + } else { + memcpy(&emu->x[regno], data, 4); + } + return 0; +} + +static int emu_read_mem(void *args, size_t addr, size_t len, void *val) +{ + struct emu *emu = (struct emu *) args; + if (addr + len > MEM_SIZE) { + return EFAULT; + } + memcpy(val, (void *) emu->m.mem + addr, len); + return 0; +} + +static int emu_write_mem(void *args, size_t addr, size_t len, void *val) +{ + struct emu *emu = (struct emu *) args; + if (addr + len > MEM_SIZE) { + return EFAULT; + } + memcpy((void *) emu->m.mem + addr, val, len); + return 0; +} + +static gdb_action_t emu_cont(void *args) +{ + int ret; + struct emu *emu = (struct emu *) args; + uint8_t *tohost_addr = emu->m.mem + TOHOST_ADDR; + + emu_start_run(emu); + while (emu->pc < emu->m.code_size && emu->pc != emu->bp_addr && + !emu_is_halt(emu)) { + uint32_t inst; + uint8_t value; + emu_read_mem(args, emu->pc, 4, &inst); + emu->pc += 4; + ret = emu_exec(emu, inst); + if (ret < 0) + break; + + /* We assume the binary that run on this emulator will + * be stopped after writing the specific memory address. + * In this way, we can simply design our testing binary. */ + read_len(8, tohost_addr, value); + if (value) + return ACT_SHUTDOWN; + } + + return ACT_RESUME; +} + +static gdb_action_t emu_stepi(void *args) +{ + struct emu *emu = (struct emu *) args; + + emu_start_run(emu); + if (emu->pc < emu->m.code_size) { + uint32_t inst; + emu_read_mem(args, emu->pc, 4, &inst); + emu->pc += 4; + emu_exec(emu, inst); + } + + return ACT_RESUME; +} + +static bool emu_set_bp(void *args, size_t addr, bp_type_t type) +{ + struct emu *emu = (struct emu *) args; + if (type != BP_SOFTWARE || emu->bp_is_set) + return false; + + emu->bp_is_set = true; + emu->bp_addr = addr; + return true; +} + +static bool emu_del_bp(void *args, size_t addr, bp_type_t type) +{ + struct emu *emu = (struct emu *) args; + + // It's fine when there's no matching breakpoint, just doing nothing + if (type != BP_SOFTWARE || !emu->bp_is_set || emu->bp_addr != addr) + return true; + + emu->bp_is_set = false; + emu->bp_addr = 0; + return true; +} + +static void emu_on_interrupt(void *args) +{ + struct emu *emu = (struct emu *) args; + emu_halt(emu); +} + +struct target_ops emu_ops = { + .get_reg_bytes = emu_get_reg_bytes, + .read_reg = emu_read_reg, + .write_reg = emu_write_reg, + .read_mem = emu_read_mem, + .write_mem = emu_write_mem, + .cont = emu_cont, + .stepi = emu_stepi, + .set_bp = emu_set_bp, + .del_bp = emu_del_bp, + .on_interrupt = emu_on_interrupt, +}; + +int main(int argc, char *argv[]) +{ + if (argc != 2) { + return -1; + } + + struct emu emu; + emu_init(&emu); + + if (init_mem(&emu.m, argv[1]) == -1) { + return -1; + } + + if (!gdbstub_init(&emu.gdbstub, &emu_ops, + (arch_info_t){ + .smp = 1, + .reg_num = 33, + .target_desc = TARGET_RV32, + }, + "127.0.0.1:1234")) { + fprintf(stderr, "Fail to create socket.\n"); + return -1; + } + + if (!gdbstub_run(&emu.gdbstub, (void *) &emu)) { + fprintf(stderr, "Fail to run in debug mode.\n"); + return -1; + } + gdbstub_close(&emu.gdbstub); + free_mem(&emu.m); + + return 0; +} diff --git a/mini-gdbstub/src/emu_test.cc b/mini-gdbstub/src/emu_test.cc new file mode 100644 index 0000000..909bbb3 --- /dev/null +++ b/mini-gdbstub/src/emu_test.cc @@ -0,0 +1,21 @@ +int add(int a, int b); + +// The main function put on 0x0 intentionally +int main() +{ + volatile char *tohost_addr; + + int c = add(3, 4); + + /* Because the binary will be run on baremetal environment + * which doesn't support C-runtime. Stop the program by writing + * to this specicial address */ + tohost_addr = (volatile char *) (0x1000 - 4); + *tohost_addr = 0xff; + return 0; +} + +int add(int a, int b) +{ + return a + b; +} diff --git a/mini-gdbstub/src/gdbstub.c b/mini-gdbstub/src/gdbstub.c new file mode 100644 index 0000000..c92818a --- /dev/null +++ b/mini-gdbstub/src/gdbstub.c @@ -0,0 +1,725 @@ +#include "gdbstub.h" +#include +#include +#include +#include +#include +#include "conn.h" +#include "gdb_signal.h" +#include "packet.h" +#include "regbuf.h" +#include "utils/csum.h" +#include "utils/translate.h" + +#define DEBUG + +struct gdbstub_private { + conn_t conn; + regbuf_t regbuf; + + pthread_t tid; + bool async_io_enable; + void *args; +}; + +static inline void async_io_enable(struct gdbstub_private *priv) +{ + __atomic_store_n(&priv->async_io_enable, true, __ATOMIC_RELAXED); +} + +static inline void async_io_disable(struct gdbstub_private *priv) +{ + __atomic_store_n(&priv->async_io_enable, false, __ATOMIC_RELAXED); +} + +static inline bool async_io_is_enable(struct gdbstub_private *priv) +{ + return __atomic_load_n(&priv->async_io_enable, __ATOMIC_RELAXED); +} + +static volatile bool thread_stop = false; +static void *socket_reader(gdbstub_t *gdbstub) +{ + void *args = gdbstub->priv->args; + int socket_fd = gdbstub->priv->conn.socket_fd; + + fd_set readfds; + struct timeval timeout; + + while (!__atomic_load_n(&thread_stop, __ATOMIC_RELAXED)) { + if (!async_io_is_enable(gdbstub->priv)) { + usleep(10000); + continue; + } + + FD_ZERO(&readfds); + FD_SET(socket_fd, &readfds); + + timeout.tv_sec = 0; + timeout.tv_usec = 100000; + + int result = select(socket_fd + 1, &readfds, NULL, NULL, &timeout); + + if (result > 0 && FD_ISSET(socket_fd, &readfds)) { + char ch; + ssize_t nread = read(socket_fd, &ch, 1); + if (nread == 1 && ch == INTR_CHAR) { + gdbstub->ops->on_interrupt(args); + } + } else if (result < 0) { + perror("select error in socket_reader"); + break; + } + } + + return NULL; +} + +bool gdbstub_init(gdbstub_t *gdbstub, + struct target_ops *ops, + arch_info_t arch, + char *s) +{ + char *addr_str = NULL; + + if (s == NULL || ops == NULL) + return false; + + memset(gdbstub, 0, sizeof(gdbstub_t)); + gdbstub->ops = ops; + gdbstub->arch = arch; + gdbstub->priv = calloc(1, sizeof(struct gdbstub_private)); + if (gdbstub->priv == NULL) + return false; + + // This is a naive implementation to parse the string + addr_str = strdup(s); + char *port_str = strchr(addr_str, ':'); + int port = 0; + if (addr_str == NULL) + goto addr_fail; + + if (port_str != NULL) { + *port_str = '\0'; + port_str += 1; + + if (sscanf(port_str, "%d", &port) <= 0) + goto addr_fail; + } + + if (!regbuf_init(&gdbstub->priv->regbuf)) + goto addr_fail; + + if (!conn_init(&gdbstub->priv->conn, addr_str, port)) + goto conn_fail; + + free(addr_str); + return true; + +conn_fail: + regbuf_destroy(&gdbstub->priv->regbuf); +addr_fail: + free(addr_str); + return false; +} + +#define SEND_ERR(gdbstub, err) conn_send_pktstr(&gdbstub->priv->conn, err) +#define SEND_EPERM(gdbstub) SEND_ERR(gdbstub, "E01") +#define SEND_EINVAL(gdbstub) SEND_ERR(gdbstub, "E22") + +static void process_reg_read(gdbstub_t *gdbstub, void *args) +{ + char packet_str[MAX_SEND_PACKET_SIZE]; + + for (int i = 0; i < gdbstub->arch.reg_num; i++) { + size_t reg_sz = gdbstub->ops->get_reg_bytes(i); + void *reg_value = regbuf_get(&gdbstub->priv->regbuf, reg_sz); + + int ret = gdbstub->ops->read_reg(args, i, reg_value); +#ifdef DEBUG + char debug_hex[MAX_SEND_PACKET_SIZE]; + hex_to_str((uint8_t *) reg_value, debug_hex, reg_sz); + printf("reg read = regno %d data 0x%s (size %zu)\n", i, debug_hex, + reg_sz); +#endif + if (!ret) { + hex_to_str((uint8_t *) reg_value, &packet_str[i * reg_sz * 2], + reg_sz); + } else { + sprintf(packet_str, "E%d", ret); + break; + } + } + + conn_send_pktstr(&gdbstub->priv->conn, packet_str); +} + +static void process_reg_read_one(gdbstub_t *gdbstub, char *payload, void *args) +{ + char packet_str[MAX_SEND_PACKET_SIZE]; + int regno; + + assert(sscanf(payload, "%x", ®no) == 1); + size_t reg_sz = gdbstub->ops->get_reg_bytes(regno); + void *reg_value = regbuf_get(&gdbstub->priv->regbuf, reg_sz); + + int ret = gdbstub->ops->read_reg(args, regno, reg_value); +#ifdef DEBUG + char debug_hex[MAX_SEND_PACKET_SIZE]; + hex_to_str((uint8_t *) reg_value, debug_hex, reg_sz); + printf("reg read = regno %d data 0x%s (size %zu)\n", regno, debug_hex, + reg_sz); +#endif + if (!ret) { + hex_to_str((uint8_t *) reg_value, packet_str, reg_sz); + } else { + sprintf(packet_str, "E%d", ret); + } + conn_send_pktstr(&gdbstub->priv->conn, packet_str); +} + +static void process_reg_write(gdbstub_t *gdbstub, char *payload, void *args) +{ + for (int i = 0; i < gdbstub->arch.reg_num; i++) { + size_t reg_sz = gdbstub->ops->get_reg_bytes(i); + void *reg_value = regbuf_get(&gdbstub->priv->regbuf, reg_sz); + + str_to_hex(&payload[i * reg_sz * 2], (uint8_t *) reg_value, reg_sz); +#ifdef DEBUG + char debug_hex[MAX_SEND_PACKET_SIZE]; + hex_to_str((uint8_t *) reg_value, debug_hex, reg_sz); + printf("reg write = regno %d data 0x%s (size %zu)\n", i, debug_hex, + reg_sz); +#endif + int ret = gdbstub->ops->write_reg(args, i, reg_value); + if (ret) { + /* FIXME: Even if we fail to modify this register, some + * registers could be writen before. This may not be + * an expected behavior. */ + char packet_str[MAX_SEND_PACKET_SIZE]; + sprintf(packet_str, "E%d", ret); + conn_send_pktstr(&gdbstub->priv->conn, packet_str); + return; + } + } + + conn_send_pktstr(&gdbstub->priv->conn, "OK"); +} + +static void process_reg_write_one(gdbstub_t *gdbstub, char *payload, void *args) +{ + int regno; + char *regno_str = payload; + char *data_str = strchr(payload, '='); + if (data_str) { + *data_str = '\0'; + data_str++; + } + + assert(sscanf(regno_str, "%x", ®no) == 1); + size_t reg_sz = gdbstub->ops->get_reg_bytes(regno); + void *data = regbuf_get(&gdbstub->priv->regbuf, reg_sz); + + assert(strlen(data_str) == reg_sz * 2); + + str_to_hex(data_str, (uint8_t *) data, reg_sz); +#ifdef DEBUG + printf("reg write = regno %d data 0x%s (size %zu)\n", regno, data_str, + reg_sz); +#endif + + int ret = gdbstub->ops->write_reg(args, regno, data); + + if (!ret) { + conn_send_pktstr(&gdbstub->priv->conn, "OK"); + } else { + char packet_str[MAX_SEND_PACKET_SIZE]; + sprintf(packet_str, "E%d", ret); + conn_send_pktstr(&gdbstub->priv->conn, packet_str); + } +} + +static void process_mem_read(gdbstub_t *gdbstub, char *payload, void *args) +{ + size_t maddr, mlen; + assert(sscanf(payload, "%lx,%lx", &maddr, &mlen) == 2); +#ifdef DEBUG + printf("mem read = addr %lx / len %lx\n", maddr, mlen); +#endif + char packet_str[MAX_SEND_PACKET_SIZE]; + + uint8_t *mval = malloc(mlen); + int ret = gdbstub->ops->read_mem(args, maddr, mlen, mval); + if (!ret) { + hex_to_str(mval, packet_str, mlen); + } else { + sprintf(packet_str, "E%d", ret); + } + conn_send_pktstr(&gdbstub->priv->conn, packet_str); + free(mval); +} + +static void process_mem_write(gdbstub_t *gdbstub, char *payload, void *args) +{ + size_t maddr, mlen; + char *content = strchr(payload, ':'); + if (content) { + *content = '\0'; + content++; + } + assert(sscanf(payload, "%lx,%lx", &maddr, &mlen) == 2); +#ifdef DEBUG + printf("mem write = addr %lx / len %lx\n", maddr, mlen); + printf("mem write = content %s\n", content); +#endif + uint8_t *mval = malloc(mlen); + str_to_hex(content, mval, mlen); + int ret = gdbstub->ops->write_mem(args, maddr, mlen, mval); + + if (!ret) { + conn_send_pktstr(&gdbstub->priv->conn, "OK"); + } else { + char packet_str[MAX_SEND_PACKET_SIZE]; + sprintf(packet_str, "E%d", ret); + conn_send_pktstr(&gdbstub->priv->conn, packet_str); + } + free(mval); +} + +static void process_mem_xwrite(gdbstub_t *gdbstub, + char *payload, + uint8_t *packet_end, + void *args) +{ + size_t maddr, mlen; + char *content = strchr(payload, ':'); + if (content) { + *content = '\0'; + content++; + } + assert(sscanf(payload, "%lx,%lx", &maddr, &mlen) == 2); + assert(unescape(content, (char *) packet_end) == (int) mlen); +#ifdef DEBUG + printf("mem xwrite = addr %lx / len %lx\n", maddr, mlen); + for (size_t i = 0; i < mlen; i++) { + printf("\tmem xwrite, byte %ld: %x\n", i, content[i]); + } +#endif + + gdbstub->ops->write_mem(args, maddr, mlen, content); + conn_send_pktstr(&gdbstub->priv->conn, "OK"); +} + + +void process_xfer(gdbstub_t *gdbstub, char *s) +{ + char *name = s; + char *args = strchr(s, ':'); + if (args) { + *args = '\0'; + args++; + } +#ifdef DEBUG + printf("xfer = %s %s\n", name, args); +#endif + if (!strcmp(name, "features") && gdbstub->arch.target_desc != NULL) { + /* Check the args */ + char *action = strtok(args, ":"); + assert(strcmp(action, "read") == 0); + char *annex = strtok(NULL, ":"); + assert(strcmp(annex, "target.xml") == 0); + + char buf[MAX_SEND_PACKET_SIZE]; + int offset = 0, length = 0; + sscanf(strtok(NULL, ":"), "%x,%x", &offset, &length); + + int total_len = strlen(gdbstub->arch.target_desc); + int payload_length = + MAX_DATA_PAYLOAD > length ? length : MAX_DATA_PAYLOAD; + + // Determine if the remaining data fits within the buffer + buf[0] = (total_len - offset < payload_length) ? 'l' : 'm'; + snprintf(buf + 1, payload_length, "%s", + gdbstub->arch.target_desc + offset); + + conn_send_pktstr(&gdbstub->priv->conn, buf); + } else { + conn_send_pktstr(&gdbstub->priv->conn, ""); + } +} + +static void process_query(gdbstub_t *gdbstub, char *payload, void *args) +{ + char packet_str[MAX_SEND_PACKET_SIZE]; + char *name = payload; + char *qargs = strchr(payload, ':'); + if (qargs) { + *qargs = '\0'; + qargs++; + } +#ifdef DEBUG + printf("query = %s %s\n", name, qargs); +#endif + + if (!strcmp(name, "C")) { + if (gdbstub->ops->get_cpu != NULL) { + int cpuid = gdbstub->ops->get_cpu(args); + sprintf(packet_str, "QC%04d", cpuid); + conn_send_pktstr(&gdbstub->priv->conn, packet_str); + } else + conn_send_pktstr(&gdbstub->priv->conn, ""); + } else if (!strcmp(name, "Supported")) { + if (gdbstub->arch.target_desc != NULL) + conn_send_pktstr(&gdbstub->priv->conn, + "PacketSize=1024;qXfer:features:read+"); + else + conn_send_pktstr(&gdbstub->priv->conn, "PacketSize=1024"); + } else if (!strcmp(name, "Attached")) { + /* assume attached to an existing process */ + conn_send_pktstr(&gdbstub->priv->conn, "1"); + } else if (!strcmp(name, "Xfer")) { + process_xfer(gdbstub, qargs); + } else if (!strcmp(name, "Symbol")) { + conn_send_pktstr(&gdbstub->priv->conn, "OK"); + } else if (!strcmp(name, "fThreadInfo")) { + /* Assume at least 1 CPU if user didn't specific + * the CPU counts */ + int smp = gdbstub->arch.smp ? gdbstub->arch.smp : 1; + char *ptr; + char cpuid_str[6]; + + /* Make assumption on the CPU counts, so + * that we can use the buffer very simply. */ + assert(smp < 10000); + + packet_str[0] = 'm'; + ptr = packet_str + 1; + for (int cpuid = 0; cpuid < smp; cpuid++) { + sprintf(cpuid_str, "%04d,", cpuid); + memcpy(ptr, cpuid_str, 5); + ptr += 5; + } + *ptr = 0; + conn_send_pktstr(&gdbstub->priv->conn, packet_str); + } else if (!strcmp(name, "sThreadInfo")) { + conn_send_pktstr(&gdbstub->priv->conn, "l"); + } else { + conn_send_pktstr(&gdbstub->priv->conn, ""); + } +} + +static inline gdb_event_t process_vcont(gdbstub_t *gdbstub, char *args) +{ + gdb_event_t event = EVENT_NONE; + + switch (args[0]) { + case 'c': + if (gdbstub->ops->cont != NULL) + event = EVENT_CONT; + else + SEND_EPERM(gdbstub); + break; + case 's': + if (gdbstub->ops->stepi != NULL) + event = EVENT_STEP; + else + SEND_EPERM(gdbstub); + break; + default: + SEND_EPERM(gdbstub); + break; + } + + return event; +} + +#define VCONT_DESC "vCont;%s%s" +static inline void process_vcont_support(gdbstub_t *gdbstub) +{ + char packet_str[MAX_SEND_PACKET_SIZE]; + char *str_s = (gdbstub->ops->stepi == NULL) ? "" : "s;S;"; + char *str_c = (gdbstub->ops->cont == NULL) ? "" : "c;C;"; + sprintf(packet_str, VCONT_DESC, str_s, str_c); + + conn_send_pktstr(&gdbstub->priv->conn, packet_str); +} + +static gdb_event_t process_vpacket(gdbstub_t *gdbstub, char *payload) +{ + gdb_event_t event = EVENT_NONE; + char *name = payload; + char *args = strchr(payload, ';'); + if (args) { + *args = '\0'; + args++; + } +#ifdef DEBUG + printf("vpacket = %s %s\n", name, args); +#endif + + if (!strcmp("Cont", name)) + event = process_vcont(gdbstub, args); + else if (!strcmp("Cont?", name)) + process_vcont_support(gdbstub); + else + conn_send_pktstr(&gdbstub->priv->conn, ""); + + return event; +} + +static void process_del_break_points(gdbstub_t *gdbstub, + char *payload, + void *args) +{ + size_t type, addr, kind; + assert(sscanf(payload, "%zx,%zx,%zx", &type, &addr, &kind) == 3); + +#ifdef DEBUG + printf("remove breakpoints = %zx %zx %zx\n", type, addr, kind); +#endif + + bool ret = gdbstub->ops->del_bp(args, addr, type); + if (ret) + conn_send_pktstr(&gdbstub->priv->conn, "OK"); + else + SEND_EINVAL(gdbstub); +} + +static void process_set_break_points(gdbstub_t *gdbstub, + char *payload, + void *args) +{ + size_t type, addr, kind; + assert(sscanf(payload, "%zx,%zx,%zx", &type, &addr, &kind) == 3); + +#ifdef DEBUG + printf("set breakpoints = %zx %zx %zx\n", type, addr, kind); +#endif + + bool ret = gdbstub->ops->set_bp(args, addr, type); + if (ret) + conn_send_pktstr(&gdbstub->priv->conn, "OK"); + else + SEND_EINVAL(gdbstub); +} + +static void process_set_cpu(gdbstub_t *gdbstub, char *payload, void *args) +{ + int cpuid; + /* We don't support deprecated Hc packet, GDB + * should send only send vCont;c and vCont;s here. */ + if (payload[0] == 'g') { + assert(sscanf(payload, "g%d", &cpuid) == 1); + gdbstub->ops->set_cpu(args, cpuid); + } + conn_send_pktstr(&gdbstub->priv->conn, "OK"); +} + +static bool packet_csum_verify(packet_t *inpkt) +{ + /* We add extra 1 for leading '$' and minus extra 1 for trailing '#' */ + uint8_t csum_rslt = compute_checksum((char *) inpkt->data + 1, + inpkt->end_pos - CSUM_SIZE - 1); + uint8_t csum_expected; + str_to_hex((char *) &inpkt->data[inpkt->end_pos - CSUM_SIZE + 1], + &csum_expected, sizeof(uint8_t)); +#ifdef DEBUG + printf("csum rslt = %x / csum expected = %s / ", csum_rslt, + &inpkt->data[inpkt->end_pos - CSUM_SIZE + 1]); + printf("csum expected = %x \n", csum_expected); +#endif + return csum_rslt == csum_expected; +} + +static gdb_event_t gdbstub_process_packet(gdbstub_t *gdbstub, + packet_t *inpkt, + void *args) +{ + assert(inpkt->data[0] == '$'); + assert(packet_csum_verify(inpkt)); + + /* After checking the checksum result, ignore those bytes */ + inpkt->data[inpkt->end_pos - CSUM_SIZE] = 0; + uint8_t request = inpkt->data[1]; + char *payload = (char *) &inpkt->data[2]; + gdb_event_t event = EVENT_NONE; + + switch (request) { + case 'g': + if (gdbstub->ops->read_reg != NULL) { + process_reg_read(gdbstub, args); + } else { + SEND_EPERM(gdbstub); + } + break; + case 'm': + if (gdbstub->ops->read_mem != NULL) { + process_mem_read(gdbstub, payload, args); + } else { + SEND_EPERM(gdbstub); + } + break; + case 'p': + if (gdbstub->ops->read_reg != NULL) { + process_reg_read_one(gdbstub, payload, args); + } else { + SEND_EPERM(gdbstub); + } + break; + case 'q': + process_query(gdbstub, payload, args); + break; + case 'v': + event = process_vpacket(gdbstub, payload); + break; + case 'z': + if (gdbstub->ops->del_bp != NULL) { + process_del_break_points(gdbstub, payload, args); + } else { + SEND_EPERM(gdbstub); + } + break; + case '?': + conn_send_pktstr(&gdbstub->priv->conn, "S05"); + break; + case 'D': + event = EVENT_DETACH; + break; + case 'G': + if (gdbstub->ops->write_reg != NULL) { + process_reg_write(gdbstub, payload, args); + } else { + SEND_EPERM(gdbstub); + } + break; + case 'H': + if (gdbstub->ops->set_cpu != NULL) { + process_set_cpu(gdbstub, payload, args); + } else { + SEND_EPERM(gdbstub); + } + break; + case 'M': + if (gdbstub->ops->write_mem != NULL) { + process_mem_write(gdbstub, payload, args); + } else { + SEND_EPERM(gdbstub); + } + break; + case 'P': + if (gdbstub->ops->write_reg != NULL) { + process_reg_write_one(gdbstub, payload, args); + } else { + SEND_EPERM(gdbstub); + } + break; + case 'T': + /* FIXME: Assume all CPUs are alive here, any exception case + * that user may want to handle? */ + conn_send_pktstr(&gdbstub->priv->conn, "OK"); + break; + case 'X': + if (gdbstub->ops->write_mem != NULL) { + /* It is important for xwrite to know the end position of packet, + * because there're escape characters which block us interpreting + * the packet as a string just like other packets do. */ + process_mem_xwrite(gdbstub, payload, + &inpkt->data[inpkt->end_pos - CSUM_SIZE], args); + } else { + SEND_EPERM(gdbstub); + } + break; + case 'Z': + if (gdbstub->ops->set_bp != NULL) { + process_set_break_points(gdbstub, payload, args); + } else { + SEND_EPERM(gdbstub); + } + break; + default: + conn_send_pktstr(&gdbstub->priv->conn, ""); + break; + } + + return event; +} + +static gdb_action_t gdbstub_handle_event(gdbstub_t *gdbstub, + gdb_event_t event, + void *args) +{ + gdb_action_t act = ACT_NONE; + + switch (event) { + case EVENT_CONT: + async_io_enable(gdbstub->priv); + act = gdbstub->ops->cont(args); + async_io_disable(gdbstub->priv); + break; + case EVENT_STEP: + act = gdbstub->ops->stepi(args); + break; + case EVENT_DETACH: + act = ACT_SHUTDOWN; + break; + default: + break; + } + + return act; +} + +static void gdbstub_act_resume(gdbstub_t *gdbstub) +{ + char packet_str[32]; + sprintf(packet_str, "S%02x", GDB_SIGNAL_TRAP); + conn_send_pktstr(&gdbstub->priv->conn, packet_str); +} + +bool gdbstub_run(gdbstub_t *gdbstub, void *args) +{ + // Bring the user-provided argument in the gdbstub_t structure + gdbstub->priv->args = args; + + /* Create a thread to receive interrupt when running the gdbstub op */ + if (gdbstub->ops->on_interrupt != NULL && gdbstub->priv->tid == 0) { + async_io_disable(gdbstub->priv); + pthread_create(&gdbstub->priv->tid, NULL, (void *) socket_reader, + (void *) gdbstub); + } + + while (true) { + conn_recv_packet(&gdbstub->priv->conn); + packet_t *pkt = conn_pop_packet(&gdbstub->priv->conn); +#ifdef DEBUG + printf("packet = %s\n", pkt->data); +#endif + gdb_event_t event = gdbstub_process_packet(gdbstub, pkt, args); + free(pkt); + + gdb_action_t act = gdbstub_handle_event(gdbstub, event, args); + switch (act) { + case ACT_RESUME: + gdbstub_act_resume(gdbstub); + break; + case ACT_SHUTDOWN: + return true; + default: + break; + } + } + + return false; +} + +void gdbstub_close(gdbstub_t *gdbstub) +{ + /* Use thread ID to make sure the thread was created */ + if (gdbstub->priv->tid != 0) { + __atomic_store_n(&thread_stop, true, __ATOMIC_RELAXED); + pthread_join(gdbstub->priv->tid, NULL); + } + + conn_close(&gdbstub->priv->conn); + free(gdbstub->priv); +} diff --git a/mini-gdbstub/src/packet.c b/mini-gdbstub/src/packet.c new file mode 100644 index 0000000..846ad73 --- /dev/null +++ b/mini-gdbstub/src/packet.c @@ -0,0 +1,102 @@ +#include "packet.h" +#include +#include +#include + +static void pktbuf_clear(pktbuf_t *pktbuf) +{ + pktbuf->end_pos = -1; + pktbuf->size = 0; +} + +#define DEFAULT_CAP (10) +bool pktbuf_init(pktbuf_t *pktbuf) +{ + pktbuf->cap = DEFAULT_CAP; + pktbuf->data = calloc(1, (1 << pktbuf->cap) * sizeof(uint8_t)); + pktbuf_clear(pktbuf); + + return true; +} + +ssize_t pktbuf_fill_from_file(pktbuf_t *pktbuf, int fd) +{ + assert((1 << pktbuf->cap) >= pktbuf->size); + + /* enlarge the buffer to read from file when it is full */ + if ((1 << pktbuf->cap) == pktbuf->size) { + pktbuf->cap++; + pktbuf->data = + realloc(pktbuf->data, (1 << pktbuf->cap) * sizeof(uint8_t)); + } + + int left = (1 << pktbuf->cap) - pktbuf->size; + uint8_t *buf = pktbuf->data + pktbuf->size; + ssize_t nread = read(fd, buf, left); + + if (nread > 0) + pktbuf->size += nread; + + return nread; +} + +bool pktbuf_is_complete(pktbuf_t *pktbuf) +{ + int head = -1; + + /* skip to the head of next packet */ + for (int i = 0; i < pktbuf->size; i++) { + if (pktbuf->data[i] == '$') { + head = i; + break; + } + } + + if (head < 0) { + pktbuf_clear(pktbuf); + return false; + } + + if (head > 0) { + /* moving memory for a valid packet */ + memmove(pktbuf->data, pktbuf->data + head, pktbuf->size - head); + pktbuf->size -= head; + } + + /* check the end of the buffer */ + uint8_t *end_pos_ptr = memchr(pktbuf->data, '#', pktbuf->size); + if (end_pos_ptr == NULL) + return false; + + int end_pos = (end_pos_ptr - pktbuf->data) + CSUM_SIZE; + if (end_pos > pktbuf->size) + return false; + + pktbuf->end_pos = end_pos; + + return true; +} + +packet_t *pktbuf_pop_packet(pktbuf_t *pktbuf) +{ + if (pktbuf->end_pos == -1) { + return NULL; + } + + int old_pkt_size = pktbuf->end_pos + 1; + packet_t *pkt = calloc(1, sizeof(packet_t) + old_pkt_size + 1); + memcpy(pkt->data, pktbuf->data, old_pkt_size); + pkt->end_pos = pktbuf->end_pos; + pkt->data[old_pkt_size] = 0; + + memmove(pktbuf->data, pktbuf->data + old_pkt_size + 1, + pktbuf->size - old_pkt_size); + pktbuf->size -= old_pkt_size; + pktbuf->end_pos = -1; + return pkt; +} + +void pktbuf_destroy(pktbuf_t *pktbuf) +{ + free(pktbuf->data); +} diff --git a/mini-gdbstub/src/regbuf.c b/mini-gdbstub/src/regbuf.c new file mode 100644 index 0000000..f2ae112 --- /dev/null +++ b/mini-gdbstub/src/regbuf.c @@ -0,0 +1,35 @@ +#include "regbuf.h" + +#include + +bool regbuf_init(regbuf_t *reg) +{ + /* Default to 8 bytes register size. */ + reg->sz = 8; + reg->buf = malloc(reg->sz); + + if (!reg->buf) + return false; + + return true; +} + +void *regbuf_get(regbuf_t *reg, size_t reg_sz) +{ + if (reg_sz <= reg->sz) + return reg->buf; + + free(reg->buf); + + while (reg_sz > reg->sz) { + reg->sz <<= 1; + } + + reg->buf = malloc(reg->sz); + return reg->buf; +} + +void regbuf_destroy(regbuf_t *reg) +{ + free(reg->buf); +} diff --git a/mini-gdbstub/src/utils/csum.c b/mini-gdbstub/src/utils/csum.c new file mode 100644 index 0000000..8c34561 --- /dev/null +++ b/mini-gdbstub/src/utils/csum.c @@ -0,0 +1,9 @@ +#include "utils/csum.h" + +uint8_t compute_checksum(char *buf, size_t len) +{ + uint8_t csum = 0; + for (size_t i = 0; i < len; ++i) + csum += buf[i]; + return csum; +} diff --git a/mini-gdbstub/src/utils/translate.c b/mini-gdbstub/src/utils/translate.c new file mode 100644 index 0000000..c7930d6 --- /dev/null +++ b/mini-gdbstub/src/utils/translate.c @@ -0,0 +1,48 @@ +#include "utils/translate.h" + +static char hexchars[] = "0123456789abcdef"; + +void hex_to_str(uint8_t *num, char *str, int bytes) +{ + for (int i = 0; i < bytes; i++) { + uint8_t ch = *(num + i); + *(str + i * 2) = hexchars[ch >> 4]; + *(str + i * 2 + 1) = hexchars[ch & 0xf]; + } + str[bytes * 2] = '\0'; +} + +static uint8_t char_to_hex(char ch) +{ + const uint8_t letter = ch & 0x40; + const uint8_t offset = (letter >> 3) | (letter >> 6); + return (ch + offset) & 0xf; +} + +void str_to_hex(char *str, uint8_t *num, int bytes) +{ + for (int i = 0; i < bytes; i++) { + uint8_t ch_high = char_to_hex(*(str + i * 2)); + uint8_t ch_low = char_to_hex(*(str + i * 2 + 1)); + + *(num + i) = (ch_high << 4) | ch_low; + } +} + +int unescape(char *msg, char *end) +{ + char *w = msg; + char *r = msg; + while (r < end) { + if (*r == '}') { + *w = *(r + 1) ^ 0x20; + r += 2; + } else { + *w = *r; + r += 1; + } + w += 1; + } + + return w - msg; +}