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..e69d8439d 100644 --- a/drgn/cli.py +++ b/drgn/cli.py @@ -175,6 +175,9 @@ 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 +295,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..c5e46f29f --- /dev/null +++ b/tests/test_gdbremote.py @@ -0,0 +1,130 @@ +# 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: + 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)