Add mini gdb stub

This commit is contained in:
Colin 2025-09-21 08:18:35 +00:00
parent e0decc5721
commit 5f7b8ee030
21 changed files with 2098 additions and 0 deletions

33
.vscode/launch.json vendored Normal file
View File

@ -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
}
]
}
]
}

1
mini-gdbstub/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
build

47
mini-gdbstub/Makefile Normal file
View File

@ -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

128
mini-gdbstub/README.md Normal file
View File

@ -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)

View File

@ -0,0 +1,25 @@
#ifndef CONN_H
#define CONN_H
#include <pthread.h>
#include <stdbool.h>
#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

View File

@ -0,0 +1,6 @@
#ifndef GDB_SIGNAL_H
#define GDB_SIGNAL_H
#define GDB_SIGNAL_TRAP 5
#endif

View File

@ -0,0 +1,69 @@
#ifndef GDBSTUB_H
#define GDBSTUB_H
#include <stdbool.h>
#include <stddef.h>
#define TARGET_RV32 \
"<target version=\"1.0\"><architecture>riscv:rv32</architecture></target>"
#define TARGET_RV64 \
"<target version=\"1.0\"><architecture>riscv:rv64</architecture></target>"
#define TARGET_X86_64 \
"<target " \
"version=\"1.0\"><architecture>i386:x86-64</architecture></target>"
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

View File

@ -0,0 +1,32 @@
#ifndef PACKET_H
#define PACKET_H
#include <stdbool.h>
#include <stdint.h>
#include <unistd.h>
#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

View File

@ -0,0 +1,15 @@
#ifndef REG_H
#define REG_H
#include <stdbool.h>
#include <stddef.h>
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

View File

@ -0,0 +1,9 @@
#ifndef CSUM_H
#define CSUM_H
#include <stddef.h>
#include <stdint.h>
uint8_t compute_checksum(char *buf, size_t len);
#endif

View File

@ -0,0 +1,22 @@
#ifndef LOG_H
#define LOG_H
#include <errno.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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

View File

@ -0,0 +1,10 @@
#ifndef UTILS_H
#define UTILS_H
#include <stdint.h>
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

35
mini-gdbstub/src/Makefile Normal file
View File

@ -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)

177
mini-gdbstub/src/conn.c Normal file
View File

@ -0,0 +1,177 @@
#include "conn.h"
#include <arpa/inet.h>
#include <assert.h>
#include <netinet/in.h>
#include <poll.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#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);
}

549
mini-gdbstub/src/emu.c Normal file
View File

@ -0,0 +1,549 @@
#include <errno.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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;
}

View File

@ -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;
}

725
mini-gdbstub/src/gdbstub.c Normal file
View File

@ -0,0 +1,725 @@
#include "gdbstub.h"
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#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", &regno) == 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", &regno) == 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);
}

102
mini-gdbstub/src/packet.c Normal file
View File

@ -0,0 +1,102 @@
#include "packet.h"
#include <assert.h>
#include <stdlib.h>
#include <string.h>
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);
}

35
mini-gdbstub/src/regbuf.c Normal file
View File

@ -0,0 +1,35 @@
#include "regbuf.h"
#include <stdlib.h>
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);
}

View File

@ -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;
}

View File

@ -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;
}