From b81f4615d012ee14820b4e70cad039945aea0b2b Mon Sep 17 00:00:00 2001 From: Daniel Thompson Date: Sat, 12 Oct 2024 09:20:26 +0000 Subject: [PATCH] gdbremote: Initial (and minimal) support for remote debugging Both usage and limitations are described in docs/advanced_usage.rst. Testing is been modest but does follow 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 The autotests are based on #1 and #2 above (there are enough memory reads during a stack trace to exercise the memory read paths). 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 | 10 + libdrgn/drgn.h | 11 + libdrgn/gdbremote.c | 486 +++++++++++++++++++++++++++++++++++++++ libdrgn/gdbremote.h | 62 +++++ libdrgn/platform.h | 16 ++ libdrgn/program.c | 88 ++++++- libdrgn/program.h | 2 + libdrgn/python/program.c | 19 ++ libdrgn/stack_trace.c | 27 +++ tests/test_gdbremote.py | 127 ++++++++++ 14 files changed, 917 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 736fa7160..9959b7785 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 2bfad0f8c..c365923d7 100644 --- a/docs/advanced_usage.rst +++ b/docs/advanced_usage.rst @@ -210,3 +210,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 a4d139bbc..a85926ed9 100644 --- a/drgn/cli.py +++ b/drgn/cli.py @@ -175,6 +175,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", @@ -292,6 +298,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 5dcb1f964..88ba9341e 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 \ language.c \ diff --git a/libdrgn/arch_aarch64.c b/libdrgn/arch_aarch64.c index 9d3332e38..c93fb29ad 100644 --- a/libdrgn/arch_aarch64.c +++ b/libdrgn/arch_aarch64.c @@ -229,6 +229,15 @@ 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) +{ + 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, @@ -473,6 +482,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 8fdee29ca..4c2086d9a 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..e21e70c36 --- /dev/null +++ b/libdrgn/gdbremote.c @@ -0,0 +1,486 @@ +// 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 + +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. + * + * TODO: Provide this with it's own statically allocated error type + * (to clearly indicate end-of-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_enomem; + + it->run_length = it->bufp[1] - 30; + it->bufp += 2; + + *ret = it->repeat_char; + return NULL; + } + + if (it->bufp[0] == '#') + return &drgn_enomem; + + if (it->bufp[0] == 0x7d) { + if (it->bufp[1] == '#') + return &drgn_enomem; + + 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); + // we are currently using drgn_enomem instead of a pre-allocated + // EOF + if (err == &drgn_enomem) + 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 0826d7e54..730619411 100644 --- a/libdrgn/platform.h +++ b/libdrgn/platform.h @@ -187,6 +187,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) * * To support virtual address translation: @@ -385,6 +386,21 @@ 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 the object is sufficiently large with @ref + * drgn_object_size(), call @ref drgn_register_state_create() with + * `interrupted = true`, and initialize it from the contents of @ref + * drgn_object_buffer(). + * + * @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 8638c4c73..0e7e123df 100644 --- a/libdrgn/program.c +++ b/libdrgn/program.c @@ -12,17 +12,21 @@ #include #include #include +#include #include #include #include +#include #include #include #include #include "cleanup.h" #include "debug_info.h" +#include "drgn.h" #include "error.h" #include "helpers.h" +#include "gdbremote.h" #include "io.h" #include "language.h" #include "log.h" @@ -102,6 +106,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); @@ -122,7 +127,8 @@ void drgn_program_deinit(struct drgn_program *prog) */ if (prog->flags & DRGN_PROGRAM_IS_LINUX_KERNEL) drgn_thread_destroy(prog->crashed_thread); - else if (prog->flags & DRGN_PROGRAM_IS_LIVE) + else if (prog->flags & DRGN_PROGRAM_IS_LIVE && + !(prog->flags & DRGN_PROGRAM_IS_GDBREMOTE)) drgn_thread_destroy(prog->main_thread); if (prog->pgtable_it) prog->platform.arch->linux_kernel_pgtable_iterator_destroy(prog->pgtable_it); @@ -152,6 +158,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 +710,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) { @@ -1075,6 +1116,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_live(struct drgn_thread_iterator *it) { @@ -1115,6 +1181,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 (prog->flags & DRGN_PROGRAM_IS_LIVE) err = drgn_thread_iterator_init_userspace_live(*ret); else @@ -1131,6 +1199,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 (it->prog->flags & DRGN_PROGRAM_IS_LIVE) { closedir(it->tasks_dir); } @@ -1196,8 +1267,8 @@ drgn_thread_iterator_next_userspace_live(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) @@ -1210,10 +1281,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 (it->prog->flags & DRGN_PROGRAM_IS_LIVE) { return drgn_thread_iterator_next_userspace_live(it, ret); } else { - drgn_thread_iterator_next_userspace_core(it, ret); + drgn_thread_iterator_next_from_thread_set(it, ret); return NULL; } } @@ -1288,8 +1362,8 @@ drgn_program_find_thread_userspace_live(struct drgn_program *prog, uint32_t tid, } 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_notes(prog); if (err) @@ -1307,7 +1381,7 @@ drgn_program_find_thread(struct drgn_program *prog, uint32_t tid, else if (prog->flags & DRGN_PROGRAM_IS_LIVE) return drgn_program_find_thread_userspace_live(prog, tid, ret); else - return drgn_program_find_thread_userspace_core(prog, tid, ret); + return drgn_program_find_thread_from_thread_set(prog, tid, ret); } // Get the CPU that crashed in a Linux kernel core dump. diff --git a/libdrgn/program.h b/libdrgn/program.h index 4cafa2a3f..abb7d7714 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 407d934ce..b2149a1ba 100644 --- a/libdrgn/python/program.c +++ b/libdrgn/python/program.c @@ -827,6 +827,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; @@ -1478,6 +1495,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 5f55648b8..240902148 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); @@ -1151,6 +1177,7 @@ static struct drgn_error *drgn_get_stack_trace(struct drgn_program *prog, "cannot unwind stack without platform"); } if ((prog->flags & (DRGN_PROGRAM_IS_LINUX_KERNEL | + DRGN_PROGRAM_IS_GDBREMOTE | DRGN_PROGRAM_IS_LIVE)) == DRGN_PROGRAM_IS_LIVE) { return drgn_error_create(DRGN_ERROR_NOT_IMPLEMENTED, "stack unwinding is not yet supported for live processes"); diff --git a/tests/test_gdbremote.py b/tests/test_gdbremote.py new file mode 100644 index 000000000..bc8043419 --- /dev/null +++ b/tests/test_gdbremote.py @@ -0,0 +1,127 @@ +# Copyright (c) Daniel Thompson +# SPDX-License-Identifier: LGPL-2.1-or-later + +import ctypes +import multiprocessing +import socket +import time + +from drgn import Architecture, Program, ProgramFlags, host_platform +from tests import TestCase + +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): + self.join(5.0) + if self.is_alive(): + self.terminate() + self.join(5.0) + if self.is_alive(): + self.kill() + self.join(5.0) + + +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.IS_64_BIT.value: + 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)