From 939d169aa6a9da0285aaf7c0cad32783a57d54a3 Mon Sep 17 00:00:00 2001 From: Daniel Thompson Date: Sat, 12 Oct 2024 09:20:26 +0000 Subject: [PATCH 1/2] gdbremote: Initial (and minimal) support for remote debugging Introduce the basic infrastructure to communicate with remote debuggers using the gdbremote protocol. See docs/advanced_usage.rst for both instructions on usages and a summary of the current limitations. Testing is been fairly modest but does cover pretty much all the new code paths: 1. the reported register values were compared between gdb and drgn 2. frame pointer based (fallback) stack tracing 3. x0 (argc) and x1 (argv) were checked and the pointers chased to verify that argv[0] contains the right value Signed-off-by: Daniel Thompson --- _drgn.pyi | 13 ++ docs/advanced_usage.rst | 49 ++++ drgn/cli.py | 12 + libdrgn/Makefile.am | 2 + libdrgn/arch_aarch64.c | 12 + libdrgn/drgn.h | 11 + libdrgn/gdbremote.c | 488 +++++++++++++++++++++++++++++++++++++++ libdrgn/gdbremote.h | 62 +++++ libdrgn/platform.h | 15 ++ libdrgn/program.c | 87 ++++++- libdrgn/program.h | 2 + libdrgn/python/program.c | 19 ++ libdrgn/stack_trace.c | 27 +++ tests/test_gdbremote.py | 136 +++++++++++ 14 files changed, 928 insertions(+), 7 deletions(-) create mode 100644 libdrgn/gdbremote.c create mode 100644 libdrgn/gdbremote.h create mode 100644 tests/test_gdbremote.py diff --git a/_drgn.pyi b/_drgn.pyi index 9fcd8e75d..2d48b0c06 100644 --- a/_drgn.pyi +++ b/_drgn.pyi @@ -669,6 +669,14 @@ class Program: """ ... + def set_gdbremote(self, conn: str) -> None: + """ + Set the program to the specificed elffile and connect to a gdbserver. + + :param conn: gdb connection string (e.g. localhost:2345) + """ + ... + def set_kernel(self) -> None: """ Set the program to the running operating system kernel. @@ -1067,6 +1075,11 @@ class ProgramFlags(enum.Flag): The program is running on the local machine. """ + IS_GDBREMOTE = ... + """ + The program is connected via the gdbremote protocol. + """ + class FindObjectFlags(enum.Flag): """ ``FindObjectFlags`` are flags for :meth:`Program.object()`. These can be diff --git a/docs/advanced_usage.rst b/docs/advanced_usage.rst index f51f28dc9..c0d25b909 100644 --- a/docs/advanced_usage.rst +++ b/docs/advanced_usage.rst @@ -217,3 +217,52 @@ core dumps. These special objects include: distinguish it from the kernel variable ``vmcoreinfo_data``. This is available without debugging information. + +Debugging via the gdbremote protocol +------------------------------------ + +The +`gdbremote protocol `_ +makes it possible to run drgn on one machine and use it to debug code running +on another system. drgn implements the client side of the protocol and can +connect via gdbremote to a variety of different gdbremote "server" +implementations including +`gdbserver `_, +`kgdb `_, +`OpenOCD `_ +and the +`QEMU gdbstub `_. + +Currently the gdbremote support in drgn is absolutely minimal: + +* drgn can only connect to network sockets (use socat to bridge to stubs + that are not networked) +* only a single thread is supported +* there is no support for automatically handle address space layout + randomization (ASLR) +* register packet decoding is implemented only for AArch64 + +However, even this minimal support is sufficient to connect to the gdbserver, +read memory and generate a stack trace using AArch64 frame pointers:: + + sh$ drgn --gdbremote localhost:2345 --symbols ./hello + drgn 0.0.27+67.ge8a745c3 (using Python 3.11.2, elfutils 0.188, without libkdumpfile) + For help, type help(drgn). + >>> import drgn + >>> from drgn import FaultError, NULL, Object, cast, container_of, execscript, offsetof, reinterpret, sizeof, stack_trace + >>> from drgn.helpers.common import * + >>> prog['main'] + (int (int argc, const char **argv))0x754 + >>> prog.threads() + <_drgn._ThreadIterator object at 0x7f81b570d0> + >>> prog.main_thread().stack_trace() + #0 0x5555550764 <--- Symbol lookup currently fails due to ASLR offsets + #1 0x7ff7e17740 + #2 0x7ff7e17818 + >>> prog.main_thread().stack_trace()[0].registers()['x0'] + 1 + >>> argv = prog.main_thread().stack_trace()[0].registers()['x1'] + >>> argv0 = prog.read_u64(argv) + >>> prog.read(argv0, 8) + b'./hello\x00' + >>> diff --git a/drgn/cli.py b/drgn/cli.py index 8d3497588..a60c35b73 100644 --- a/drgn/cli.py +++ b/drgn/cli.py @@ -178,6 +178,12 @@ def _main() -> None: program_group.add_argument( "-c", "--core", metavar="PATH", type=str, help="debug the given core dump" ) + program_group.add_argument( + "--gdbremote", + metavar="CONN", + type=str, + help="connect to the specified gdbserver", + ) program_group.add_argument( "-p", "--pid", @@ -295,6 +301,12 @@ def _main() -> None: sys.exit( f"{e}\nerror: attaching to live process requires ptrace attach permissions" ) + elif args.gdbremote is not None: + prog.set_gdbremote(args.gdbremote) + + # Suppress default symbol loading (at present, gdbremote always + # needs to get symbols from --symbols) + args.default_symbols = {} else: try: prog.set_kernel() diff --git a/libdrgn/Makefile.am b/libdrgn/Makefile.am index 05646609c..d24abff60 100644 --- a/libdrgn/Makefile.am +++ b/libdrgn/Makefile.am @@ -71,6 +71,8 @@ libdrgnimpl_la_SOURCES = $(ARCH_DEFS_PYS:_defs.py=.c) \ hash_table.c \ hash_table.h \ helpers.h \ + gdbremote.c \ + gdbremote.h \ io.c \ io.h \ kallsyms.c \ diff --git a/libdrgn/arch_aarch64.c b/libdrgn/arch_aarch64.c index 1f050f08c..10cd3409b 100644 --- a/libdrgn/arch_aarch64.c +++ b/libdrgn/arch_aarch64.c @@ -229,6 +229,17 @@ linux_kernel_get_initial_registers_aarch64(const struct drgn_object *task_obj, return NULL; } +static struct drgn_error * +gdbremote_get_initial_registers_aarch64(struct drgn_program *prog, + const void *regs, size_t reglen, + struct drgn_register_state **ret) +{ + // gdbremote uses the same binary format as struct user_pt_reg + // so we can just reuse that code. + return get_initial_registers_from_struct_aarch64(prog, regs, reglen, + ret); +} + static struct drgn_error * apply_elf_reloc_aarch64(const struct drgn_relocating_section *relocating, uint64_t r_offset, uint32_t r_type, const int64_t *r_addend, @@ -495,6 +506,7 @@ const struct drgn_architecture_info arch_info_aarch64 = { .prstatus_get_initial_registers = prstatus_get_initial_registers_aarch64, .linux_kernel_get_initial_registers = linux_kernel_get_initial_registers_aarch64, + .gdbremote_get_initial_registers = gdbremote_get_initial_registers_aarch64, .apply_elf_reloc = apply_elf_reloc_aarch64, .linux_kernel_pgtable_iterator_create = linux_kernel_pgtable_iterator_create_aarch64, diff --git a/libdrgn/drgn.h b/libdrgn/drgn.h index 57a003877..d2d33b7a5 100644 --- a/libdrgn/drgn.h +++ b/libdrgn/drgn.h @@ -522,6 +522,8 @@ enum drgn_program_flags { DRGN_PROGRAM_IS_LIVE = (1 << 1), /** The program is running on the local machine. */ DRGN_PROGRAM_IS_LOCAL = (1 << 2), + /** The program is connected via the gdbremote protocol. */ + DRGN_PROGRAM_IS_GDBREMOTE = (1 << 3), }; /** @@ -802,6 +804,15 @@ struct drgn_error *drgn_program_set_core_dump(struct drgn_program *prog, */ struct drgn_error *drgn_program_set_core_dump_fd(struct drgn_program *prog, int fd); +/** + * Set a @ref drgn_program to a gdbremote server. + * + * @param[in] conn gdb connection string (e.g. localhost:2345) + * @return @c NULL on success, non-@c NULL on error. + */ +struct drgn_error *drgn_program_set_gdbremote(struct drgn_program *prog, + const char *conn); + /** * Set a @ref drgn_program to the running operating system kernel. * diff --git a/libdrgn/gdbremote.c b/libdrgn/gdbremote.c new file mode 100644 index 000000000..4f775bd5e --- /dev/null +++ b/libdrgn/gdbremote.c @@ -0,0 +1,488 @@ +// Copyright (c) Daniel Thompson +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "gdbremote.h" +#include "program.h" +#include "util.h" + +#define VERBOSE_PROTOCOL 0 + +// We don't expect anyone outside this module to need to check for this error +// by address so, until that changes, we'll keep it static. +static struct drgn_error drgn_error_end_of_packet = { + .code = DRGN_ERROR_OTHER, + .message = "cannot read past end of gdbremote packet", +}; + +struct gdb_packet { + unsigned char buffer[1024]; + unsigned int buflen; +}; + +struct gdb_7bit_iterator { + unsigned char *bufp; + unsigned int remaining; + unsigned char repeat_char; + unsigned char run_length; +}; + +static char hexchar(uint8_t nibble) +{ + assert(nibble < 16); + + if (nibble < 10) + return '0' + nibble; + + return 'a' + nibble - 10; +} + +static unsigned char lookup_hexchar(unsigned char c) +{ + if (c < 'A') + return 0 + (c - '0'); + + return 10 + ((c | 0x20) - 'a'); +} + +static struct gdb_7bit_iterator gdb_7bit_iterator_init(struct gdb_packet *pkt) +{ + struct gdb_7bit_iterator it = { + .bufp = &pkt->buffer[1], + .remaining = pkt->buflen - 4, + .repeat_char = pkt->buffer[0], + .run_length = 0, + }; + + return it; +} + +/* + * Extract a single character from the packet currently being processed. + * + * Handles run length encoding and escapes. + * + * The packet *must* be checked using gdb_packet_verify_framing() before + * processing because we rely on the trailing # to mark the end of the + * packet. + */ +static struct drgn_error * +gdb_7bit_iterator_get_char(struct gdb_7bit_iterator *it, uint8_t *ret) +{ + if (it->run_length) { + it->run_length--; + *ret = it->repeat_char; + return NULL; + } + + if (it->bufp[0] == '*') { + if (it->bufp[1] == '#') + return &drgn_error_end_of_packet; + + it->run_length = it->bufp[1] - 30; + it->bufp += 2; + + *ret = it->repeat_char; + return NULL; + } + + if (it->bufp[0] == '#') + return &drgn_error_end_of_packet; + + if (it->bufp[0] == 0x7d) { + if (it->bufp[1] == '#') + return &drgn_error_end_of_packet; + + it->repeat_char = it->bufp[1] ^ 0x20; + it->bufp += 2; + } else { + it->repeat_char = *it->bufp++; + } + + *ret = it->repeat_char; + return NULL; +} + +static struct drgn_error * +gdb_7bit_iterator_get_integer(struct gdb_7bit_iterator *it, unsigned int nchars, + uint64_t *ret) +{ + uint64_t accumulator = 0; + bool valid = true; + struct drgn_error *err = NULL; + + for (int i=0; ibuffer[1]; + + for (i=2; ibuflen && pkt->buffer[i] != '#'; i++) + checksum += pkt->buffer[i]; + + return checksum; +} + +static struct drgn_error *gdb_packet_verify_framing(struct gdb_packet *pkt) +{ + if (pkt->buffer[0] != '$') + return drgn_error_format( + DRGN_ERROR_OTHER, + "Packet is badly framed (no leading '$')"); + + if (pkt->buffer[pkt->buflen - 3] != '#') + return drgn_error_format( + DRGN_ERROR_OTHER, + "Packet is badly framed (no trailing '#')"); + + uint8_t checksum = gdb_packet_get_checksum(pkt); + if (pkt->buffer[pkt->buflen - 2] != hexchar(checksum >> 4) || + pkt->buffer[pkt->buflen - 1] != hexchar(checksum & 0x0f)) + return drgn_error_format( + DRGN_ERROR_OTHER, + "Packet has bad checksum (should be %02x, got %c%c)", + checksum, pkt->buffer[pkt->buflen - 2], + pkt->buffer[pkt->buflen - 1]); + + return NULL; +} + +static void gdb_packet_fixup_checksum(struct gdb_packet *pkt) +{ + assert(pkt->buflen >= 3); + assert(pkt->buflen <= sizeof(pkt->buffer) - 2); + + uint8_t checksum = gdb_packet_get_checksum(pkt); + + pkt->buffer[pkt->buflen] = hexchar(checksum >> 4); + pkt->buffer[pkt->buflen+1] = hexchar(checksum & 0x0f); + pkt->buflen += 2; + + pkt->buffer[pkt->buflen] = '\0'; + + assert(NULL == gdb_packet_verify_framing(pkt)); +} + +static void gdb_packet_init(struct gdb_packet *pkt, const char *cmd) +{ + int len = strlen(cmd); + assert(sizeof(pkt->buffer) > len + 5); + + pkt->buffer[0] = '$'; + memcpy(&pkt->buffer[1], cmd, len); + pkt->buffer[len+1] = '#'; + pkt->buflen = len+2; + gdb_packet_fixup_checksum(pkt); + + // make the buffer printable (the assert above checks there is space for + // this) + pkt->buffer[pkt->buflen] = '\0'; +} + +static struct drgn_error *gdb_send_command(int fd, struct gdb_packet *pkt) +{ + unsigned char *bufp = pkt->buffer; + + if (VERBOSE_PROTOCOL) + fprintf(stderr, "=> %s\n", bufp); + + // this is an old school write-all loop... + while (pkt->buflen > 0) { + ssize_t res = write(fd, bufp, pkt->buflen); + if (res < 0) + return drgn_error_create_os( + "failed to send gdbserver command", errno, NULL); + bufp += res;; + pkt->buflen -= res; + } + + return 0; +} + +static struct drgn_error *gdb_await_ack(int fd, struct gdb_packet *pkt) +{ + int res; + + do { + res = read(fd, pkt->buffer, 1); + } while (res == 0); + + if (res < 0) + return drgn_error_create_os("failed to wait for gdbserver ack", + errno, NULL); + + if (VERBOSE_PROTOCOL > 1) + fprintf(stderr, "<- %c\n", pkt->buffer[0]); + + if (pkt->buffer[0] != '+') + return drgn_error_format( + DRGN_ERROR_OTHER, + "no ack from gdbserver (expected '+', got '%c')", + pkt->buffer[0]); + + return 0; +} + +static struct drgn_error *gdb_await_reply(int fd, struct gdb_packet *pkt) +{ + int res; + struct drgn_error *err; + + pkt->buflen = 0; + + // keep reading until we have an end-of-packet marker + while(pkt->buflen < 4 || pkt->buffer[pkt->buflen - 3] != '#') { + // The - 1 is important here: it's not needed to correctly + // implement the protocol but it does allow us to terminate + // the buffer (which allows debug code to treat it like a + // C-string + int nbytes = sizeof(pkt->buffer) - pkt->buflen - 1; + if (nbytes <= 0) + return drgn_error_format( + DRGN_ERROR_OTHER, + "overflow waiting for gdbserver reply"); + + res = read(fd, pkt->buffer + pkt->buflen, nbytes); + if (res < 0) + return drgn_error_create_os( + "failed to wait for gdbserver reply", errno, NULL); + + pkt->buflen += res; + } + + // we reserved space for this in the read loop + pkt->buffer[pkt->buflen] = '\0'; + if (VERBOSE_PROTOCOL) + fprintf(stderr, "<= %s\n", (char *) pkt->buffer); + + err = gdb_packet_verify_framing(pkt); + if (err) + return err; + + return 0; +} + +static struct drgn_error *gdb_send_and_receive(int fd, struct gdb_packet *pkt) +{ + struct drgn_error *err; + + err = gdb_send_command(fd, pkt); + if (err) + return err; + + err = gdb_await_ack(fd, pkt); + if (err) + return err; + + err = gdb_await_reply(fd, pkt); + if (err) + return err; + + int res = write(fd, "+", 1); + if (res != 1) + return drgn_error_create_os( + "failed to send gdbserver ack", errno, NULL); + + if (VERBOSE_PROTOCOL > 1) + fprintf(stderr, "-> +\n"); + + return NULL; +} + +static struct drgn_error *gdb_query(int fd, struct gdb_packet *pkt) +{ + struct drgn_error *err; + + gdb_packet_init(pkt, "?"); + err = gdb_send_and_receive(fd, pkt); + if (err) + return err; + + return NULL; +} + +static struct drgn_error *gdb_get_registers(int fd, struct gdb_packet *pkt) +{ + struct drgn_error *err; + + gdb_packet_init(pkt, "g"); + err = gdb_send_and_receive(fd, pkt); + if (err) + return err; + + return 0; +} + +struct drgn_error *drgn_gdbremote_connect(const char *conn, int *ret) +{ + struct drgn_error *err; + int res; + + // Currently we only support the hostname:port format + _cleanup_free_ char *host = strdup(conn); + if (!host) + return &drgn_enomem; + char *port = strrchr(host, ':'); + if (port) + *port++ = '\0'; + + struct addrinfo hints = { + .ai_family = AF_UNSPEC, + .ai_socktype = SOCK_STREAM, + }; + struct addrinfo *result, *rp; + res = getaddrinfo(host, port, &hints, &result); + if (res < 0) + return drgn_error_format(DRGN_ERROR_OTHER, + "could not connect to '%s'", conn); + + int conn_fd = -1; + for (rp = result; rp != NULL; rp = rp->ai_next) { + conn_fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); + if (conn_fd < 0) + continue; + + res = connect(conn_fd, rp->ai_addr, rp->ai_addrlen); + if (res >= 0) + break; + + close(conn_fd); + conn_fd = -1; + } + + if (conn_fd < 0) + return drgn_error_format(DRGN_ERROR_OTHER, + "failed to connect to '%s'", conn); + + // Verify that the remote stub responds to the query packet + struct gdb_packet pkt; + err = gdb_query(conn_fd, &pkt); + if (err) + return err; + + *ret = conn_fd; + return NULL; +} + +struct drgn_error *drgn_gdbremote_read_memory(void *buf, uint64_t address, + size_t count, uint64_t offset, + void *arg, bool physical) +{ + struct drgn_program *prog = arg; + struct drgn_error *err; + char cmd[32]; + struct gdb_packet pkt; + + if (physical) + return drgn_error_format(DRGN_ERROR_FAULT, + "Cannot read from physical memory at %"PRIx64, address); + + // Make sure we don't read more than we can fit in the statically + // sized packet buffer + const size_t chunksz = (sizeof(pkt.buffer) / 2) - 8; + + for (size_t i=0; i < count; i += chunksz) { + size_t remaining = min(count - i, chunksz); + sprintf(cmd, "m%"PRIx64",%zu", address + i, remaining); + gdb_packet_init(&pkt, cmd); + err = gdb_send_and_receive(prog->conn_fd, &pkt); + if (err) + return err; + + struct gdb_7bit_iterator it = gdb_7bit_iterator_init(&pkt); + for (int j = 0; j < remaining; j++) { + err = gdb_7bit_iterator_get_u8(&it, + ((uint8_t *)buf) + i + j); + if (err) + return err; + } + } + + return NULL; +} + +struct drgn_error *drgn_gdbremote_get_registers(int conn_fd, uint32_t tid, + void **regs_ret, + size_t *reglen_ret) +{ + struct drgn_error *err; + struct gdb_packet pkt; + struct gdb_7bit_iterator it; + int len; + + err = gdb_get_registers(conn_fd, &pkt); + if (err) + return err; + + // figure out how large the register set is + it = gdb_7bit_iterator_init(&pkt); + for (len=0; ; len++) { + uint8_t byte; + err = gdb_7bit_iterator_get_u8(&it, &byte); + if (err == &drgn_error_end_of_packet) + break; + } + + uint8_t *regs = calloc(len, 1); + if (regs == NULL) + return &drgn_enomem; + + it = gdb_7bit_iterator_init(&pkt); + for (int i=0; i +// SPDX-License-Identifier: LGPL-2.1-or-later + +/** + * @file + * + * gdbremote protocol implementation. + * + * See @ref GdbRemote. + */ + +#ifndef DRGN_GDBREMOTE_H +#define DRGN_GDBREMOTE_H + +#include +#include +#include + +/** + * @ingroup Internals + * + * @defgroup GdbRemote gdbremote protocol + * + * gdbremote protocol implementation. + * + * @{ + */ + +/** + * Connect to a gdbremote server or debug stub. + * + * Supported connecting strings include: + * + * * 127.0.0.1:2345 + * + * @param[in] conn gdb connection string + * @param[out] ret File descriptor for the gdbremote connection + */ +struct drgn_error *drgn_gdbremote_connect(const char *conn, int *ret); + +/** @ref drgn_memory_read_fn which reads using the gdbremote protocol. */ +struct drgn_error *drgn_gdbremote_read_memory(void *buf, uint64_t address, + size_t count, uint64_t offset, + void *arg, bool physical); + +/** + * Fetch the register set from the gdbremote. + * + * The buffer provided in ret is formatted in an architecture specific manner + * and, because it is dynamically allocated, must be freed by the caller. + * + * @param[in] conn_fd File descriptor for the gdbremote connection + * @param[in] tid Thread identifier of the desired register set + * @param[out] ret Allocated buffer containing decoded register values. + */ +struct drgn_error *drgn_gdbremote_get_registers(int conn_fd, uint32_t tid, + void **regs_ret, + size_t *reglen_ret); + +/** @} */ + +#endif // DRGN_GDBREMOTE_H diff --git a/libdrgn/platform.h b/libdrgn/platform.h index e5d348402..cb3b869bd 100644 --- a/libdrgn/platform.h +++ b/libdrgn/platform.h @@ -192,6 +192,7 @@ typedef struct drgn_error * * - @ref pt_regs_get_initial_registers * - @ref prstatus_get_initial_registers * - @ref linux_kernel_get_initial_registers + * - @ref gdbremote_get_initial_registers * - @ref demangle_cfi_registers (only if needed) * - Implement `drgn_test_get_pt_regs()` in * `tests/linux_kernel/kmod/drgn_test.c` (usually by copying @@ -403,6 +404,20 @@ struct drgn_architecture_info { */ struct drgn_error *(*linux_kernel_get_initial_registers)(const struct drgn_object *task_obj, struct drgn_register_state **ret); + /** + * Create a @ref drgn_register_state from a gdbremote register reply. + * + * This should check that @p reglen is sufficiently large, call @ref + * drgn_register_state_create() with `interrupted = true`, and + * initialize it from @p regs. + * + * @param[in] regs Reply from gdbremote (after hex decoding) + * @param[in] reglen Length of the decoded reply + * @param[out] ret Returned registers. + */ + struct drgn_error *(*gdbremote_get_initial_registers)( + struct drgn_program *prog, const void *regs, size_t reglen, + struct drgn_register_state **ret); /** * Apply an ELF relocation. * diff --git a/libdrgn/program.c b/libdrgn/program.c index 0fe7928de..427c20d12 100644 --- a/libdrgn/program.c +++ b/libdrgn/program.c @@ -23,6 +23,7 @@ #include "debug_info.h" #include "error.h" #include "helpers.h" +#include "gdbremote.h" #include "io.h" #include "language.h" #include "log.h" @@ -102,6 +103,7 @@ void drgn_program_init(struct drgn_program *prog, drgn_program_init_types(prog); drgn_debug_info_init(&prog->dbinfo, prog); prog->core_fd = -1; + prog->conn_fd = -1; if (platform) drgn_program_set_platform(prog, platform); drgn_thread_set_init(&prog->thread_set); @@ -120,7 +122,8 @@ void drgn_program_deinit(struct drgn_program *prog) * prog->thread_set and thus freed by the above call to * drgn_thread_set_deinit(). */ - if (!drgn_program_is_userspace_core(prog)) { + if (!drgn_program_is_userspace_core(prog) && + !(prog->flags & DRGN_PROGRAM_IS_GDBREMOTE)) { drgn_thread_destroy(prog->crashed_thread); drgn_thread_destroy(prog->main_thread); } @@ -152,6 +155,8 @@ void drgn_program_deinit(struct drgn_program *prog) elf_end(prog->core); if (prog->core_fd != -1) close(prog->core_fd); + if (prog->conn_fd != -1) + close(prog->conn_fd); drgn_debug_info_deinit(&prog->dbinfo); } @@ -702,6 +707,39 @@ drgn_program_set_core_dump(struct drgn_program *prog, const char *path) return drgn_program_set_core_dump_fd_internal(prog, fd, path); } +LIBDRGN_PUBLIC struct drgn_error * +drgn_program_set_gdbremote(struct drgn_program *prog, const char *conn) +{ + struct drgn_error *err; + + err = drgn_program_check_initialized(prog); + if (err) + return err; + + err = drgn_gdbremote_connect(conn, &prog->conn_fd); + if (err) + return err; + + bool had_platform = prog->has_platform; + drgn_program_set_platform(prog, &drgn_host_platform); + + err = drgn_program_add_memory_segment( + prog, 0, UINT64_MAX, drgn_gdbremote_read_memory, prog, false); + if (err) + goto out_segments; + + prog->flags |= DRGN_PROGRAM_IS_LIVE | DRGN_PROGRAM_IS_GDBREMOTE; + return NULL; + +out_segments: + drgn_memory_reader_deinit(&prog->reader); + drgn_memory_reader_init(&prog->reader); + prog->has_platform = had_platform; + close(prog->conn_fd); + prog->conn_fd = -1; + return err; +} + LIBDRGN_PUBLIC struct drgn_error * drgn_program_set_kernel(struct drgn_program *prog) { @@ -1110,6 +1148,31 @@ drgn_thread_iterator_init_linux_kernel(struct drgn_thread_iterator *it) return NULL; } +static struct drgn_error * +drgn_thread_iterator_init_gdbremote(struct drgn_thread_iterator *it) +{ + struct drgn_program *prog = it->prog; + struct drgn_thread thread = { + .prog = prog, + // Until we implement query packet parsing in the gdbremote code + // then we are only able to debug the stopped thread. + .tid = 1, + }; + + prog->main_thread = + drgn_thread_set_search(&prog->thread_set, &thread.tid).entry; + if (!prog->main_thread) { + if (drgn_thread_set_insert(&prog->thread_set, &thread, NULL) == -1) + return &drgn_enomem; + prog->main_thread = + drgn_thread_set_search(&prog->thread_set, &thread.tid) + .entry; + } + + it->iterator = drgn_thread_set_first(&it->prog->thread_set); + return NULL; +} + static struct drgn_error * drgn_thread_iterator_init_userspace_process(struct drgn_thread_iterator *it) { @@ -1150,6 +1213,8 @@ drgn_thread_iterator_create(struct drgn_program *prog, (*ret)->prog = prog; if (prog->flags & DRGN_PROGRAM_IS_LINUX_KERNEL) err = drgn_thread_iterator_init_linux_kernel(*ret); + else if (prog->flags & DRGN_PROGRAM_IS_GDBREMOTE) + err = drgn_thread_iterator_init_gdbremote(*ret); else if (drgn_program_is_userspace_process(prog)) err = drgn_thread_iterator_init_userspace_process(*ret); else if (drgn_program_is_userspace_core(prog)) @@ -1168,6 +1233,9 @@ drgn_thread_iterator_destroy(struct drgn_thread_iterator *it) if (it->prog->flags & DRGN_PROGRAM_IS_LINUX_KERNEL) { drgn_object_deinit(&it->entry.object); linux_helper_task_iterator_deinit(&it->task_iter); + } else if (it->prog->flags & DRGN_PROGRAM_IS_GDBREMOTE) { + // do nothing (but *don't* follow the IS_LIVE path + // for core dumps) } else if (drgn_program_is_userspace_process(it->prog)) { closedir(it->tasks_dir); } @@ -1233,8 +1301,8 @@ drgn_thread_iterator_next_userspace_process(struct drgn_thread_iterator *it, } static void -drgn_thread_iterator_next_userspace_core(struct drgn_thread_iterator *it, - struct drgn_thread **ret) +drgn_thread_iterator_next_from_thread_set(struct drgn_thread_iterator *it, + struct drgn_thread **ret) { *ret = it->iterator.entry; if (it->iterator.entry) @@ -1247,10 +1315,13 @@ drgn_thread_iterator_next(struct drgn_thread_iterator *it, { if (it->prog->flags & DRGN_PROGRAM_IS_LINUX_KERNEL) { return drgn_thread_iterator_next_linux_kernel(it, ret); + } else if (it->prog->flags & DRGN_PROGRAM_IS_GDBREMOTE) { + drgn_thread_iterator_next_from_thread_set(it, ret); + return NULL; } else if (drgn_program_is_userspace_process(it->prog)) { return drgn_thread_iterator_next_userspace_process(it, ret); } else if (drgn_program_is_userspace_core(it->prog)) { - drgn_thread_iterator_next_userspace_core(it, ret); + drgn_thread_iterator_next_from_thread_set(it, ret); return NULL; } else { *ret = NULL; @@ -1329,8 +1400,8 @@ drgn_program_find_thread_userspace_process(struct drgn_program *prog, } static struct drgn_error * -drgn_program_find_thread_userspace_core(struct drgn_program *prog, uint32_t tid, - struct drgn_thread **ret) +drgn_program_find_thread_from_thread_set(struct drgn_program *prog, + uint32_t tid, struct drgn_thread **ret) { struct drgn_error *err = drgn_program_cache_core_dump_threads(prog); if (err) @@ -1345,11 +1416,13 @@ drgn_program_find_thread(struct drgn_program *prog, uint32_t tid, { if (prog->flags & DRGN_PROGRAM_IS_LINUX_KERNEL) { return drgn_program_find_thread_linux_kernel(prog, tid, ret); + } else if (prog->flags & DRGN_PROGRAM_IS_GDBREMOTE) { + return drgn_program_find_thread_from_thread_set(prog, tid, ret); } else if (drgn_program_is_userspace_process(prog)) { return drgn_program_find_thread_userspace_process(prog, tid, ret); } else if (drgn_program_is_userspace_core(prog)) { - return drgn_program_find_thread_userspace_core(prog, tid, ret); + return drgn_program_find_thread_from_thread_set(prog, tid, ret); } else { *ret = NULL; return NULL; diff --git a/libdrgn/program.h b/libdrgn/program.h index bee1debca..5a55270ab 100644 --- a/libdrgn/program.h +++ b/libdrgn/program.h @@ -71,6 +71,8 @@ struct drgn_program { int core_fd; /* PID of live userspace program. */ pid_t pid; + /* File descriptor to communicate with the connected backend (e.g. gdbremote) */ + int conn_fd; #ifdef WITH_LIBKDUMPFILE kdump_ctx_t *kdump_ctx; #endif diff --git a/libdrgn/python/program.c b/libdrgn/python/program.c index 154b0a022..93f430292 100644 --- a/libdrgn/python/program.c +++ b/libdrgn/python/program.c @@ -840,6 +840,23 @@ static PyObject *Program_set_core_dump(Program *self, PyObject *args, Py_RETURN_NONE; } +static PyObject *Program_set_gdbremote(Program *self, PyObject *args, + PyObject *kwds) +{ + static char *keywords[] = {"conn", NULL}; + struct drgn_error *err; + const char *conn; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "s:set_gdbremote", + keywords, &conn)) + return NULL; + + err = drgn_program_set_gdbremote(&self->prog, conn); + if (err) + return set_drgn_error(err); + Py_RETURN_NONE; +} + static PyObject *Program_set_kernel(Program *self) { struct drgn_error *err; @@ -1469,6 +1486,8 @@ static PyMethodDef Program_methods[] = { METH_VARARGS | METH_KEYWORDS, drgn_Program_add_object_finder_DOC}, {"set_core_dump", (PyCFunction)Program_set_core_dump, METH_VARARGS | METH_KEYWORDS, drgn_Program_set_core_dump_DOC}, + {"set_gdbremote", (PyCFunction)Program_set_gdbremote, + METH_VARARGS | METH_KEYWORDS, drgn_Program_set_gdbremote_DOC}, {"set_kernel", (PyCFunction)Program_set_kernel, METH_NOARGS, drgn_Program_set_kernel_DOC}, {"set_pid", (PyCFunction)Program_set_pid, METH_VARARGS | METH_KEYWORDS, diff --git a/libdrgn/stack_trace.c b/libdrgn/stack_trace.c index 3650d8d71..771a71186 100644 --- a/libdrgn/stack_trace.c +++ b/libdrgn/stack_trace.c @@ -15,6 +15,7 @@ #include "dwarf_info.h" #include "elf_file.h" #include "error.h" +#include "gdbremote.h" #include "helpers.h" #include "minmax.h" #include "nstring.h" @@ -688,6 +689,29 @@ drgn_get_initial_registers_from_kernel_core_dump(struct drgn_program *prog, cpu); } +static struct drgn_error * +drgn_get_initial_registers_from_gdbremote(struct drgn_program *prog, + uint32_t tid, + struct drgn_register_state **ret) +{ + struct drgn_error *err; + _cleanup_free_ void *regs = NULL; + size_t reglen; + + if (!prog->platform.arch->gdbremote_get_initial_registers) + return drgn_error_format(DRGN_ERROR_NOT_IMPLEMENTED, + "gdbremote register decoding is not " + "implemented for %s architecture", + prog->platform.arch->name); + + err = drgn_gdbremote_get_registers(prog->conn_fd, tid, ®s, ®len); + if (err) + return err; + + return prog->platform.arch->gdbremote_get_initial_registers( + prog, regs, reglen, ret); +} + static struct drgn_error * drgn_get_initial_registers(struct drgn_program *prog, uint32_t tid, const struct drgn_object *thread_obj, @@ -779,6 +803,8 @@ drgn_get_initial_registers(struct drgn_program *prog, uint32_t tid, } return prog->platform.arch->linux_kernel_get_initial_registers(&obj, ret); + } else if (prog->flags & DRGN_PROGRAM_IS_GDBREMOTE) { + return drgn_get_initial_registers_from_gdbremote(prog, tid, ret); } else { struct nstring prstatus; err = drgn_program_find_prstatus(prog, tid, &prstatus); @@ -1154,6 +1180,7 @@ static struct drgn_error *drgn_get_stack_trace(struct drgn_program *prog, return drgn_error_create(DRGN_ERROR_NOT_IMPLEMENTED, "stack unwinding is not yet supported for live processes"); } else if (!(prog->flags & DRGN_PROGRAM_IS_LINUX_KERNEL) + && !(prog->flags & DRGN_PROGRAM_IS_GDBREMOTE) && !drgn_program_is_userspace_core(prog)) { return drgn_error_create(DRGN_ERROR_NOT_IMPLEMENTED, "stack unwinding is not supported for this program"); diff --git a/tests/test_gdbremote.py b/tests/test_gdbremote.py new file mode 100644 index 000000000..1562490d4 --- /dev/null +++ b/tests/test_gdbremote.py @@ -0,0 +1,136 @@ +# Copyright (c) Daniel Thompson +# SPDX-License-Identifier: LGPL-2.1-or-later + +import ctypes +import multiprocessing +import socket +import time + +from drgn import Architecture, PlatformFlags, Program, ProgramFlags, host_platform +from tests import TestCase + +# These are captured replies from gdbserver/aarch64. +# +# Currently we do not need lookup tables for other architectures because +# drgn has limited support for other architectures. We can test memory +# reads on all (64-bit) architectures using this lookup table. More lookup +# tables will be required once other architectures are able to decode +# register data. +aarch64_lookup = { + b"$?#3f": b'$T051d:40eef* 7f0*";1f:40eef* 7f0*";20:64075*"0*";thread:21cd;core:0;#c7', + b"$g#67": b'$010**d8ef*!7f0*"e8ef*!7f0*"54075*"0*"0081fff77f0*"ddda0d494bedb2c17820f9f77f0*"49564154450*"d70**20*K240**57c10**f0fff77f0*"030**f00cfdf77f0*"8000f9f77f0*"00c0190*&d8ef*!7f0*"010**d0fd565* 0*"54075*"0*"e8ef*!7f0*"98dbfff77f0*228e0fff77f0*"d0fd565* 0*240eef* 7f0*"4077e1f77f0*"40eef* 7f0*"64075*"0*(80*=2e2f68656c6c6f005348454c4c3d2f6200330*&cc0*(330* ff0*4ff003*=0*!c0*"0030*}0*}0*}0*}0*}0*}0*}0*}0*v87fff77f0*2#76', + b"$m7fffffee40,16#63": b'$60eef* 7f0*"4077e1f77f0*"d8ef*!7f00#c6', + b"$m7fffffee60,16#65": b'$70ef*!7f0*"1878e1f77f0*"f00cfdf77f00#47', + b"$m7fffffef70,16#67": b'$0*,70065*"0*.#5c', +} + + +class GdbMockProcess(multiprocessing.Process): + def __init__(self): + super().__init__(daemon=True) + self.bound = multiprocessing.Value(ctypes.c_bool, False) + self.lookup = aarch64_lookup + + def start(self): + super().start() + while not self.bound.value: + time.sleep(0.01) + + def run(self): + buf = b"" + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(("127.0.0.1", 65432)) + self.bound.value = True + s.listen() + conn, addr = s.accept() + with conn: + while True: + data = conn.recv(1024) + if not data: + break + buf += data + + i = buf.find(b"$") + if i < 0: + buf = b"" + continue + if i > 0: + buf = buf[i:] + + i = buf.find(b"#") + if i < 0 or len(buf) <= i + 2: + continue + + packet = buf[: i + 3] + buf = buf[i + 3 :] + + conn.sendall(b"+") + if packet in self.lookup: + conn.sendall(self.lookup[packet]) + else: + # $#00 means unsupported + conn.sendall(b"$#00") + + def clean_up(self): + TIMEOUT = 5.0 + + self.join(TIMEOUT) + if self.is_alive(): + self.terminate() + self.join(TIMEOUT) + if self.is_alive(): + self.kill() + self.join(TIMEOUT) + + +class TestGdbRemote(TestCase): + def setUp(self): + self.gdbmock = GdbMockProcess() + self.gdbmock.start() + self.conn_str = "localhost:65432" + + self.prog = Program() + + def tearDown(self): + # Provide socket closure (to encourage the thread terminate cleanly) + del self.prog + self.gdbmock.clean_up() + + def test_program_set_gdbremote(self): + prog = self.prog + self.assertIsNone(prog.platform) + self.assertFalse(prog.flags & ProgramFlags.IS_GDBREMOTE) + + prog.set_gdbremote(self.conn_str) + self.assertEqual(prog.platform, host_platform) + self.assertTrue(prog.flags & ProgramFlags.IS_GDBREMOTE) + + # Port 51 is for the obsolete IMP protocol and reserved since + # 2013 meaning we can be fairly confident nobody is using it + # (although that only matters if this test fails) + self.assertRaisesRegex( + ValueError, + "program memory was already initialized", + prog.set_gdbremote, + "localhost:51", + ) + + def test_gdbremote_read(self): + self.prog.set_gdbremote(self.conn_str) + if not (self.prog.platform.flags & PlatformFlags.IS_64_BIT): + self.skipTest("gdbremote test data only supports 64-bit platforms") + val = self.prog.read(0x7FFFFFEE40, 16) + self.assertEqual( + val, b"`\xee\xff\xff\x7f\x00\x00\x00@w\xe1\xf7\x7f\x00\x00\x00" + ) + + def test_gdbremote_getregs(self): + self.prog.set_gdbremote(self.conn_str) + if self.prog.platform.arch != Architecture.AARCH64: + self.skipTest("register packet decoding is not implemented for this arch") + + t = self.prog.threads().__next__() + regs = t.stack_trace()[0].registers() + self.assertEqual(regs["x0"], 1) + self.assertEqual(regs["sp"], 0x7FFFFFEE40) + self.assertEqual(regs["pstate"], 0x80000000) From 1baa195eb6b78551b59c4e0cf6edd8496ff5354d Mon Sep 17 00:00:00 2001 From: Daniel Thompson Date: Sat, 16 Nov 2024 12:17:55 +0000 Subject: [PATCH 2/2] gdbremote: Add support for error packet handling Add logic to detect error messages issued by the gdbserver and to convert them into exceptions. At the python prompt errors will be shown in the following way (this example has verbose protocol debugging enabled to we get to see the underlying gdbremote packets too): ~~~ >>> prog.read(0, 16) => $m0,16#30 <= $E01#a6 Traceback (most recent call last): File "", line 1, in Exception: gdbserver reported error: 1 ~~~ Signed-off-by: Daniel Thompson --- libdrgn/gdbremote.c | 24 ++++++++++++++++++++++++ tests/test_gdbremote.py | 13 ++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/libdrgn/gdbremote.c b/libdrgn/gdbremote.c index 4f775bd5e..8fa572ff0 100644 --- a/libdrgn/gdbremote.c +++ b/libdrgn/gdbremote.c @@ -332,6 +332,30 @@ static struct drgn_error *gdb_send_and_receive(int fd, struct gdb_packet *pkt) if (VERBOSE_PROTOCOL > 1) fprintf(stderr, "-> +\n"); + // We need to make sure we check for errors after sending the + // acknowledgement! + if (pkt->buffer[1] == 'E') { + // Error packets are naturally printable to we just have + // to deframe things slightly to make it look good in the + // error message. + // + // Format is either a two digit number (e.g. $E01#c5) or + // a textual message (e.g. $E.messaeg#c5). We can print + // either of these simply by terminating at the '#' and + // stripping of the . or leading '0' if it exists. + + char *msg = (char *) pkt->buffer + + (pkt->buffer[2] == '.' || pkt->buffer[2] == '0' ? 3 : + 2); + + // strrchr() will always find the '#' because the + // packet has already been subjected to framing checks + *strrchr(msg, '#') = '\0'; + + return drgn_error_format(DRGN_ERROR_OTHER, + "gdbserver reported error: %s", msg); + } + return NULL; } diff --git a/tests/test_gdbremote.py b/tests/test_gdbremote.py index 1562490d4..61ed780ec 100644 --- a/tests/test_gdbremote.py +++ b/tests/test_gdbremote.py @@ -22,8 +22,10 @@ b"$m7fffffee40,16#63": b'$60eef* 7f0*"4077e1f77f0*"d8ef*!7f00#c6', b"$m7fffffee60,16#65": b'$70ef*!7f0*"1878e1f77f0*"f00cfdf77f00#47', b"$m7fffffef70,16#67": b'$0*,70065*"0*.#5c', -} + # Null pointer read + b"$m0,64#33" : b'$E01#a6', +} class GdbMockProcess(multiprocessing.Process): def __init__(self): @@ -124,6 +126,15 @@ def test_gdbremote_read(self): val, b"`\xee\xff\xff\x7f\x00\x00\x00@w\xe1\xf7\x7f\x00\x00\x00" ) + def test_gdbremote_read_with_error(self): + self.prog.set_gdbremote(self.conn_str) + self.assertRaisesRegex( + Exception, + "gdbserver reported error: 1", + self.prog.read, + 0, + 64) + def test_gdbremote_getregs(self): self.prog.set_gdbremote(self.conn_str) if self.prog.platform.arch != Architecture.AARCH64: