From ac8b05a5bdc1a19630c748cb7cc151871b0ae05e Mon Sep 17 00:00:00 2001 From: Stephen Brennan Date: Thu, 9 Mar 2023 10:04:14 -0800 Subject: [PATCH] kallsyms: add symbol finder for live & coredump The Linux kernel can be configured to include kallsyms, a built-in compressed symbol table which is also exposed at /proc/kallsyms. The symbol table contains most (but not all) of the ELF symbol table information. It can be used as a Symbol finder. The kallsyms information can be extracted in two ways: for live systems where we have root access, the simplest approach is to simply read /proc/kallsyms. For vmcores, or live systems where we are not root, we must parse the data from the vmcore, which is significantly more involved. To avoid tying the kallsyms system too deeply into the drgn internals, the finder is exposed as a Python class, which must be created using symbol information from the vmcoreinfo. Attaching the KallsymsFinder to the program will attach the underlying C function, so we can avoid some of the inefficiencies of the Python API. Signed-off-by: Stephen Brennan --- _drgn.pyi | 67 +++ docs/api_reference.rst | 1 + drgn/__init__.py | 2 + drgn/helpers/linux/kallsyms.py | 58 ++ libdrgn/Makefile.am | 3 + libdrgn/kallsyms.c | 887 +++++++++++++++++++++++++++++++ libdrgn/kallsyms.h | 135 +++++ libdrgn/python/drgnpy.h | 6 + libdrgn/python/kallsyms_finder.c | 147 +++++ libdrgn/python/main.c | 1 + libdrgn/python/program.c | 12 +- 11 files changed, 1317 insertions(+), 2 deletions(-) create mode 100644 drgn/helpers/linux/kallsyms.py create mode 100644 libdrgn/kallsyms.c create mode 100644 libdrgn/kallsyms.h create mode 100644 libdrgn/python/kallsyms_finder.c diff --git a/_drgn.pyi b/_drgn.pyi index f579cce43..57ae73eee 100644 --- a/_drgn.pyi +++ b/_drgn.pyi @@ -1611,6 +1611,73 @@ class Symbol: kind: Final[SymbolKind] """Kind of entity represented by this symbol.""" +class KallsymsFinder: + """ + A symbol finder which uses vmlinux kallsyms data + """ + + def __init__( + self, + prog: Program, + kallsyms_names: int, + kallsyms_token_table: int, + kallsyms_token_index: int, + kallsyms_num_syms: int, + kallsyms_offsets: int, + kallsyms_relative_base: int, + kallsyms_addresses: int, + _stext: int, + ) -> None: + """ + Manually construct a ``KallsymsFinder`` given all symbol addresses + + .. note:: + + This class should not normally be instantiated manually. See + :func:`drgn.helpers.linux.kallsyms.make_kallsyms_vmlinux_finder` + instead for a way of automatically creating the finder via + information found in the ``VMCOREINFO``. + + The finder is capable of searching the compressed table of symbol names + and addresses stored within kernel memory. It requires + ``CONFIG_KALLSYMS=y`` and ``CONFIG_KALLSYMS_ALL=y`` in your kernel + configuration -- this is common on desktop and server Linux + distributions. However, the quality of symbol information is not + excellent: the :meth:`Symbol.binding` and :meth:`Symbol.kind` values are + inferred from type code information provided by kallsyms which was + originally generated by ``nm(1)``. Further, the :meth:`Symbol.size` is + computed using the offset of the next symbol after it in memory. This + can create some unusual results. + + In order to create a ``KallsymsFinder``, drgn must know the location of + several symbols, which creates a bit of a chicken-and-egg problem. + Thankfully, starting with Linux 6.0, these symbol addresses are included + in the VMCOREINFO note. The required symbols are addresses of variables + in the vmcore: + + - ``kallsyms_names``: an array of compressed symbol name data. + - ``kallsyms_token_table``, ``kallsyms_token_index``: tables used in + decompressing symbol names. + - ``kallsyms_num_syms``: the number of kallsyms symbols + - ``_stext``: the start of the kernel text segment. This symbol addresss + is necessary for verifying decoded kallsyms data. + + Depending on the way that kallsyms is configured (see + ``CONFIG_KALLSYMS_ABSOLUTE_PERCPU`` and + ``CONFIG_KALLSYMS_BASE_RELATIVE``), the following symbols are needed. If + the symbol names are not present, they should be given as zero. + + - ``kallsyms_offsets`` + - ``kallsyms_realtive_base`` + - ``kallsyms_addresses`` + + :param prog: Program to create a finder for + :returns: A callable object suitable to provide to + :meth:`Program.add_symbol_finder()`. + """ + __call__: Callable[[Optional[str], Optional[int], bool], List[Symbol]] + """Lookup symbol by name, address, or both.""" + class SymbolBinding(enum.Enum): """ A ``SymbolBinding`` describes the linkage behavior and visibility of a diff --git a/docs/api_reference.rst b/docs/api_reference.rst index 16c0e65a4..ffbfb379b 100644 --- a/docs/api_reference.rst +++ b/docs/api_reference.rst @@ -108,6 +108,7 @@ Symbols .. drgndoc:: Symbol .. drgndoc:: SymbolBinding .. drgndoc:: SymbolKind +.. drgndoc:: KallsymsFinder Stack Traces ------------ diff --git a/drgn/__init__.py b/drgn/__init__.py index 1df95b5fd..64b060be7 100644 --- a/drgn/__init__.py +++ b/drgn/__init__.py @@ -52,6 +52,7 @@ FaultError, FindObjectFlags, IntegerLike, + KallsymsFinder, Language, MissingDebugInfoError, NoDefaultProgramError, @@ -105,6 +106,7 @@ "FaultError", "FindObjectFlags", "IntegerLike", + "KallsymsFinder", "Language", "MissingDebugInfoError", "NULL", diff --git a/drgn/helpers/linux/kallsyms.py b/drgn/helpers/linux/kallsyms.py new file mode 100644 index 000000000..52d9a04b2 --- /dev/null +++ b/drgn/helpers/linux/kallsyms.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# Copyright (c) 2023 Oracle and/or its affiliates +# SPDX-License-Identifier: LGPL-2.1-or-later +""" +Kallsyms +-------- + +The kallsyms module contains helpers which allow you to use the built-in +kallsyms symbol table for drgn object lookup. Combined with an alternative type +information source, this can enable debugging Linux kernel core dumps without +the corresponding DWARF debuginfo files. +""" +import re +from typing import Dict + +from drgn import KallsymsFinder, Program + +__all__ = ("make_kallsyms_vmlinux_finder",) + + +def _vmcoreinfo_symbols(prog: Program) -> Dict[str, int]: + vmcoreinfo_data = prog["VMCOREINFO"].string_().decode("ascii") + vmcoreinfo_symbols = {} + sym_re = re.compile(r"SYMBOL\(([^)]+)\)=([A-Fa-f0-9]+)") + for line in vmcoreinfo_data.strip().split("\n"): + match = sym_re.fullmatch(line) + if match: + vmcoreinfo_symbols[match.group(1)] = int(match.group(2), 16) + return vmcoreinfo_symbols + + +def make_kallsyms_vmlinux_finder(prog: Program) -> KallsymsFinder: + """ + Create a vmlinux kallsyms finder, which may be passed to + :meth:`drgn.Program.add_symbol_finder`. + + This function automatically finds the necessary information to create a + ``KallsymsFinder`` from the program's VMCOREINFO data. It may fail if the + information is not present. Please note that the debugged Linux kernel must + be 6.0 or later to find this information. + + :returns: a callable symbol finder object + """ + symbol_reqd = [ + "kallsyms_names", + "kallsyms_token_table", + "kallsyms_token_index", + "kallsyms_num_syms", + "kallsyms_offsets", + "kallsyms_relative_base", + "kallsyms_addresses", + "_stext", + ] + symbols = _vmcoreinfo_symbols(prog) + args = [] + for sym in symbol_reqd: + args.append(symbols.get(sym, 0)) + return KallsymsFinder(prog, *args) diff --git a/libdrgn/Makefile.am b/libdrgn/Makefile.am index dfa706374..ce1888259 100644 --- a/libdrgn/Makefile.am +++ b/libdrgn/Makefile.am @@ -66,6 +66,8 @@ libdrgnimpl_la_SOURCES = $(ARCH_DEFS_PYS:_defs.py=.c) \ helpers.h \ io.c \ io.h \ + kallsyms.c \ + kallsyms.h \ language.c \ language.h \ language_c.c \ @@ -157,6 +159,7 @@ _drgn_la_SOURCES = python/constants.c \ python/drgnpy.h \ python/error.c \ python/helpers.c \ + python/kallsyms_finder.c \ python/language.c \ python/main.c \ python/object.c \ diff --git a/libdrgn/kallsyms.c b/libdrgn/kallsyms.c new file mode 100644 index 000000000..f43e0da06 --- /dev/null +++ b/libdrgn/kallsyms.c @@ -0,0 +1,887 @@ +// Copyright (c) 2023 Oracle and/or its affiliates +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include +#include + +#include "kallsyms.h" +#include "program.h" +#include "drgn.h" + +/** + * This struct contains the tables necessary to reconstruct kallsyms names. + * + * vmlinux (core kernel) kallsyms names are compressed using table compression. + * There is some description of it in the kernel's "scripts/kallsyms.c", but + * this is a brief overview that should make the code below comprehensible. + * + * Table compression uses the remaining 128 characters not defined by ASCII and + * maps them to common substrings (e.g. the prefix "write_"). Each name is + * represented as a sequence of bytes which refers to strings in this table. + * The two arrays below comprise this table: + * + * - token_table: this is one long string with all of the tokens concatenated + * together, e.g. "a\0b\0c\0...z\0write_\0read_\0..." + * - token_index: this is a 256-entry long array containing the index into + * token_table where you'll find that token's string. + * + * To decode a string, for each byte you simply index into token_index, then use + * that to index into token_table, and copy that string into your buffer. + * + * The actual kallsyms symbol names are concatenated into a buffer called + * "names". The first byte in a name is the length (in tokens, not decoded + * bytes) of the symbol name. The remaining "length" bytes are decoded via the + * table as described above. The first decoded byte is a character representing + * what type of symbol this is (e.g. text, data structure, etc). + */ +struct kallsyms_reader { + uint32_t num_syms; + uint8_t *names; + char *token_table; + uint16_t *token_index; + bool long_names; +}; + +/* + * We determine symbol length by the start of the subsequent symbol. + * Unfortunately, there can be large gaps in the symbol table, for instance the + * Linux kernel has percpu symbols near the beginning of the address space, and + * a large gap before normal kernel symbols. The result of this is that we can + * create symbols with incredibly large sizes, and then drgn's symbolization + * will print addresses using that symbol and a very large offset, which is + * absolutely meaningless. + * + * To avoid this, we set a cap on the length of a symbol. Unfortunately, this is + * a heuristic. It's entirely possible to have very large data symbols. This + * value is chosen somewhat arbitrarily, but seems to produce decent results. + */ +#define MAX_SYMBOL_LENGTH 0x10000 + +/* + * Since 73bbb94466fd3 ("kallsyms: support "big" kernel symbols"), the + * "kallsyms_names" array may use the most significant bit to indicate that the + * initial element for each symbol (normally representing the number of tokens + * in the symbol) requires two bytes. + * + * Unfortunately, that means that values 128-255 are now ambiguous: on older + * kernels, they should be interpreted literally, but on newer kernels, they + * require treating as a two byte sequence. Since the commit included no changes + * to the symbol names or vmcoreinfo, there's no way to detect it except via + * heuristics. + * + * The commit in question is a new feature and not likely to be backported to + * stable, so our heuristic is that it was first included in kernel 6.1. + * However, we first check the environment variable DRGN_KALLSYMS_LONG: if it + * exists, then we use its first character to determine our behavior: 1, y, Y + * all indicate that we should use long names. 0, n, N all indicate that we + * should not. + */ +static bool guess_long_names(struct drgn_program *prog) +{ + const char *env = getenv("DRGN_KALLSYMS_LONG"); + const char *osrelease; + int i; + int major = 0, minor = 0; + + if (env) { + if (*env == '1' || *env == 'y' || *env == 'Y') + return true; + else if (*env == '0' || *env == 'n' || *env == 'N') + return false; + } + + osrelease = prog->vmcoreinfo.osrelease; + for (i = 0; i < sizeof(prog->vmcoreinfo.osrelease) && osrelease[i]; i++) { + char c = osrelease[i]; + if (c < '0' || c > '9') + break; + major *= 10; + major += osrelease[i] - '0'; + } + for (i = i + 1; i < sizeof(prog->vmcoreinfo.osrelease) && osrelease[i] && osrelease[i] != '.'; i++) { + char c = osrelease[i]; + if (c < '0' || c > '9') + break; + minor *= 10; + minor += osrelease[i] - '0'; + } + return (major == 6 && minor >= 1) || major > 6; +} + +/** + * Copy the kallsyms names tables from the program into host memory. + * @param prog Program to read from + * @param kr kallsyms_reader to populate + * @param vi vmcoreinfo for the program + */ +static struct drgn_error * +kallsyms_copy_tables(struct drgn_program *prog, struct kallsyms_reader *kr, + struct kallsyms_locations *loc) +{ + struct drgn_error *err; + const size_t token_index_size = (UINT8_MAX + 1) * sizeof(uint16_t); + uint64_t last_token; + size_t token_table_size, names_idx; + char data; + uint8_t len_u8; + int len; + bool bswap; + + err = drgn_program_bswap(prog, &bswap); + if (err) + return err; + + /* Read num_syms from vmcore */ + err = drgn_program_read_u32(prog, + loc->kallsyms_num_syms, + false, &kr->num_syms); + if (err) + return err; + if (bswap) + kr->num_syms = bswap_32(kr->num_syms); + + /* Read the constant-sized token_index table (256 entries) */ + kr->token_index = malloc(token_index_size); + if (!kr->token_index) + return &drgn_enomem; + err = drgn_program_read_memory(prog, kr->token_index, + loc->kallsyms_token_index, + token_index_size, false); + if (err) + return err; + if (bswap) { + for (size_t i = 0; i < kr->num_syms; i++) { + kr->token_index[i] = bswap_16(kr->token_index[i]); + } + } + + /* + * Find the end of the last token, so we get the overall length of + * token_table. Then copy the token_table into host memory. + */ + last_token = loc->kallsyms_token_table + kr->token_index[UINT8_MAX]; + do { + err = drgn_program_read_u8(prog, last_token, false, + (uint8_t *)&data); + if (err) + return err; + + last_token++; + } while (data); + token_table_size = last_token - loc->kallsyms_token_table + 1; + kr->token_table = malloc(token_table_size); + if (!kr->token_table) + return &drgn_enomem; + err = drgn_program_read_memory(prog, kr->token_table, + loc->kallsyms_token_table, + token_table_size, false); + if (err) + return err; + + /* Now find the end of the names array by skipping through it, then copy + * that into host memory. */ + names_idx = 0; + kr->long_names = guess_long_names(prog); + for (size_t i = 0; i < kr->num_syms; i++) { + err = drgn_program_read_u8(prog, + loc->kallsyms_names + names_idx, + false, &len_u8); + if (err) + return err; + len = len_u8; + if ((len & 0x80) && kr->long_names) { + err = drgn_program_read_u8(prog, + loc->kallsyms_names + names_idx + 1, + false, &len_u8); + if (err) + return err; + len = (len & 0x7F) | (len_u8 << 7); + names_idx++; + } + names_idx += len + 1; + } + kr->names = malloc(names_idx); + if (!kr->names) + return &drgn_enomem; + err = drgn_program_read_memory(prog, kr->names, + loc->kallsyms_names, + names_idx, false); + if (err) + return err; + + return NULL; +} + +/** + * Write the symbol starting at @a offset into @a result. + * @param kr Registry containing kallsyms data + * @param offset Starting index within "names" array for this symbol + * @param result Buffer to write output symbol to + * @param maxlen Size of output buffer, to avoid overruns + * @param[out] kind_ret Where to write the symbol kind data + * @param[out] bytes_ret How many bytes were output (incl. NUL) + * @returns The offset of the next symbol + */ +static unsigned int +kallsyms_expand_symbol(struct kallsyms_reader *kr, unsigned int offset, + char *result, size_t maxlen, char *kind_ret, + size_t *bytes_ret) +{ + uint8_t *data = &kr->names[offset]; + unsigned int len = *data; + bool skipped_first = false; + size_t bytes = 0; + + if ((len & 0x80) && kr->long_names) { + data++; + offset++; + len = (0x7F & len) | (*data << 7); + } + + offset += len + 1; + data += 1; + while (len) { + char *token_ptr = &kr->token_table[kr->token_index[*data]]; + while (*token_ptr) { + if (skipped_first) { + if (maxlen <= 1) + goto tail; + *result = *token_ptr; + result++; + maxlen--; + bytes++; + } else { + if (kind_ret) + *kind_ret = *token_ptr; + skipped_first = true; + } + token_ptr++; + } + + data++; + len--; + } + +tail: + *result = '\0'; + bytes++; + *bytes_ret = bytes; + return offset; +} + +/** Decode all symbol names from @a kr and place them into @a reg */ +static struct drgn_error * +kallsyms_create_symbol_array(struct kallsyms_finder *reg, struct kallsyms_reader *kr) +{ + uint8_t token_lengths[UINT8_MAX+1]; + + /* Compute the length of each token */ + for (int i = 0; i <= UINT8_MAX; i++) { + token_lengths[i] = strlen(&kr->token_table[kr->token_index[i]]); + } + + /* Now compute the length of all symbols together */ + size_t names_idx = 0; + size_t length = 0; + for (int i = 0; i < kr->num_syms; i++) { + unsigned int num_tokens = kr->names[names_idx]; + if ((num_tokens & 0x80) && kr->long_names) + num_tokens = (num_tokens & 0x7F) | (kr->names[++names_idx] << 7); + for (int j = names_idx + 1; j < names_idx + num_tokens + 1; j++) + length += token_lengths[kr->names[j]]; + length++; /* nul terminator */ + names_idx += num_tokens + 1; + } + + /* We use uint32_t to index into the array of strings. That allows for + * 4GiB of names which should be plenty, but still: check for overflow. */ + if (length >= UINT32_MAX) + return drgn_error_format(DRGN_ERROR_OUT_OF_BOUNDS, + "kallsyms string table is too large: %lu", + length); + + reg->strings = malloc(length); + reg->strings_len = length; + reg->names = calloc(kr->num_syms, sizeof(*reg->names)); + reg->types = malloc(kr->num_syms); + reg->num_syms = kr->num_syms; + if (!reg->strings || !reg->names || !reg->types) + return &drgn_enomem; + + names_idx = 0; + uint32_t symbols_idx = 0; + for (int i = 0; i < kr->num_syms; i++) { + size_t bytes = 0; + names_idx = kallsyms_expand_symbol(kr, names_idx, + reg->strings + symbols_idx, + length - symbols_idx, ®->types[i], + &bytes); + reg->names[i] = symbols_idx; + symbols_idx += (uint32_t) bytes; + } + return NULL; +} + +static int kallsyms_name_compar(const void *lhs, const void *rhs, void *arg) +{ + struct kallsyms_finder *kr = arg; + uint32_t left_ix = *(const uint32_t *)lhs; + uint32_t right_ix = *(const uint32_t *)rhs; + return strcmp(&kr->strings[kr->names[left_ix]], + &kr->strings[kr->names[right_ix]]); +} + +static struct drgn_error * +kallsyms_create_htab(struct kallsyms_finder *kr) +{ + /* + * A sorted list of symbol indices. Entries of the hash table will point + * into this list for a certain number of elements. + */ + kr->sorted = malloc(kr->num_syms * sizeof(kr->sorted[0])); + for (uint32_t i = 0; i < kr->num_syms; i++) + kr->sorted[i] = i; + + qsort_r(kr->sorted, kr->num_syms, sizeof(kr->sorted[0]), + kallsyms_name_compar, kr); + + if (!drgn_kallsyms_names_reserve(&kr->htab, kr->num_syms)) + return &drgn_enomem; + + /* For each unique symbol name, insert the index, and number of + * occurrences into the hash table. */ + struct drgn_kallsyms_names_entry entry; + uint32_t current = 0; + while (current < kr->num_syms) { + char *current_str = &kr->strings[kr->names[kr->sorted[current]]]; + uint32_t next = current + 1; + while (next < kr->num_syms) { + char *next_str = &kr->strings[kr->names[kr->sorted[next]]]; + if (strcmp(current_str, next_str) != 0) + break; + next++; + } + + entry.key = current_str; + entry.value.start = current; + entry.value.end = next; + drgn_kallsyms_names_insert(&kr->htab, &entry, NULL); + current = next; + } + return NULL; +} + +/** Copies and decodes symbol names from the program. */ +static struct drgn_error * +kallsyms_load_names(struct kallsyms_finder *reg, struct kallsyms_locations *loc) +{ + struct drgn_error *err; + struct kallsyms_reader reader = {0}; + + err = kallsyms_copy_tables(reg->prog, &reader, loc); + if (err) + goto out; + + err = kallsyms_create_symbol_array(reg, &reader); +out: + free(reader.names); + free(reader.token_index); + free(reader.token_table); + return err; +} + +/** Lookup @a name in the registry @a kr, and return the index of the symbol */ +static int drgn_kallsyms_lookup(struct kallsyms_finder *kr, const char *name) +{ + struct drgn_kallsyms_names_iterator it = + drgn_kallsyms_names_search(&kr->htab, (char **)&name); + if (it.entry) { + return kr->sorted[it.entry->value.start]; + } + return -1; +} + +/** Return the address of symbol at @a index*/ +static uint64_t +kallsyms_address(struct kallsyms_finder *kr, unsigned int index) +{ + return kr->addresses[index]; +} + +static void drgn_symbol_from_kallsyms(struct kallsyms_finder *kr, int index, + struct drgn_symbol *ret) +{ + char kind = kr->types[index]; + char kind_lower = tolower(kind); + ret->name = &kr->strings[kr->names[index]]; + ret->address = kallsyms_address(kr, index); + if (index < kr->num_syms) { + size_t size = kallsyms_address(kr, index + 1) - ret->address; + if (size < MAX_SYMBOL_LENGTH) + ret->size = size; + else + ret->size = 0; + } else { + ret->size = 0; + } + + ret->binding = DRGN_SYMBOL_BINDING_GLOBAL; + if (kind == 'u') + ret->binding = DRGN_SYMBOL_BINDING_UNIQUE; + else if (kind_lower == 'v' || kind_lower == 'w') + ret->binding = DRGN_SYMBOL_BINDING_WEAK; + else if (isupper(kind)) + ret->binding = DRGN_SYMBOL_BINDING_GLOBAL; + else + /* If lowercase, the symbol is usually local, but it's + * not guaranteed. Use unknown for safety here. */ + ret->binding = DRGN_SYMBOL_BINDING_UNKNOWN; + + switch (kind_lower) { + case 'b': /* bss */ + case 'c': /* uninitialized data */ + case 'd': /* initialized data */ + case 'g': /* initialized data (small objects) */ + case 'r': /* read-only data */ + ret->kind = DRGN_SYMBOL_KIND_OBJECT; + break; + case 't': /* text */ + ret->kind = DRGN_SYMBOL_KIND_FUNC; + break; + default: + ret->kind = DRGN_SYMBOL_KIND_UNKNOWN; + } + /* NOTE: The name field is owned by the kallsyms finder. + * Once the kallsyms finder is bound to the program, it cannot be + * unbound, and so it shares lifetime with the Program. + */ + ret->name_lifetime = DRGN_LIFETIME_STATIC; +} + +static int kallsyms_addr_compar(const void *key_void, const void *memb_void) +{ + const uint64_t *key = key_void; + const uint64_t *memb = memb_void; + + /* We are guaranteed that: (min <= key <= max), so we can fearlessly + * index one beyond memb, so long as we've checked that key > memb. + */ + if (*key == *memb) + return 0; + else if (*key < *memb) + return -1; + else if (*key < memb[1]) + return 0; + else + return 1; +} + +static inline struct drgn_error * +add_result(struct kallsyms_finder *kr, struct drgn_symbol_result_builder *builder, int index) +{ + struct drgn_symbol *symbol = malloc(sizeof(*symbol)); + if (!symbol) + return &drgn_enomem; + drgn_symbol_from_kallsyms(kr, index, symbol); + return drgn_symbol_result_builder_add(builder, symbol); +} + +struct drgn_error * +drgn_kallsyms_symbol_finder(const char *name, uint64_t address, + struct drgn_module *module, + enum drgn_find_symbol_flags flags, void *arg, + struct drgn_symbol_result_builder *builder) +{ + struct kallsyms_finder *kr = arg; + uint64_t begin = kallsyms_address(kr, 0); + uint64_t end = kallsyms_address(kr, kr->num_syms - 1); + struct drgn_error *err = NULL; + + /* We assume the last symbol is "zero length" for simplicity. + * Short-circuit the search when we're searching outside the address + * range. + */ + if (flags & DRGN_FIND_SYM_ADDR) { + uint64_t *res; + if (address < begin || address > end) + return NULL; + res = bsearch(&address, kr->addresses, kr->num_syms, sizeof(address), + kallsyms_addr_compar); + /* If the gap between symbols > MAX_SYMBOL_LENGTH, then we infer that + * the symbol doesn't contain the address, so fail. */ + if (!res || res[1] - res[0] > MAX_SYMBOL_LENGTH) + return NULL; + return add_result(kr, builder, res - kr->addresses); + } else if (flags & DRGN_FIND_SYM_NAME) { + struct drgn_kallsyms_names_iterator it = + drgn_kallsyms_names_search(&kr->htab, (char **)&name); + if (!it.entry) + return NULL; + for (uint32_t i = it.entry->value.start; i < it.entry->value.end; i++) { + err = add_result(kr, builder, kr->sorted[i]); + it = drgn_kallsyms_names_next(it); + if (err || flags & DRGN_FIND_SYM_ONE) + break; + } + return err; + } else { + for (int i = 0; i < kr->num_syms; i++) + if ((err = add_result(kr, builder, i)) + || (flags & DRGN_FIND_SYM_ONE)) + return err; + } + return NULL; +} + +/** Compute an address via the CONFIG_KALLSYMS_ABSOLUTE_PERCPU method*/ +static uint64_t absolute_percpu(uint64_t base, int32_t val) +{ + if (val >= 0) + return (uint64_t) val; + else + return base - 1 - val; +} + +/** + * Load the kallsyms address information from @a prog + * + * Just as symbol name loading is complex, so is address loading. Addresses may + * be stored directly as an array of pointers, but more commonly, they are + * stored as an array of 32-bit integers which are related to an offset. This + * function decodes the addresses into a plain array of 64-bit addresses. + * + * @param prog The program to read from + * @param kr The symbol registry to fill + * @param vi vmcoreinfo containing necessary symbols + * @returns NULL on success, or error + */ +static struct drgn_error * +kallsyms_load_addresses(struct drgn_program *prog, struct kallsyms_finder *kr, + struct kallsyms_locations *loc) +{ + struct drgn_error *err = NULL; + bool bswap, bits64; + uint32_t *addr32; + + err = drgn_program_bswap(prog, &bswap); + if (err) + return err; + err = drgn_program_is_64_bit(prog, &bits64); + if (err) + return err; + + kr->addresses = malloc(kr->num_syms * sizeof(uint64_t)); + if (!kr->addresses) + return &drgn_enomem; + + if (loc->kallsyms_addresses) { + /* + * The kallsyms addresses are stored as plain addresses in an + * array of unsigned long! Read the appropriate size array and + * do any necessary byte swaps. + */ + if (!bits64) { + addr32 = malloc(kr->num_syms * sizeof(uint32_t)); + if (!addr32) + return &drgn_enomem; + + err = drgn_program_read_memory(prog, addr32, + loc->kallsyms_addresses, + kr->num_syms * sizeof(uint32_t), + false); + if (err) { + free(addr32); + return err; + } + for (int i = 0; i < kr->num_syms; i++) { + if (bswap) + kr->addresses[i] = bswap_32(addr32[i]); + else + kr->addresses[i] = addr32[i]; + } + free(addr32); + } else { + err = drgn_program_read_memory(prog, kr->addresses, + loc->kallsyms_addresses, + kr->num_syms * sizeof(uint32_t), + false); + if (err) + return err; + if (bswap) + for (int i = 0; i < kr->num_syms; i++) + kr->addresses[i] = bswap_64(kr->addresses[i]); + } + } else { + /* + * The kallsyms addresses are stored in an array of 4-byte + * values, which can be interpreted in two ways: + * (1) if CONFIG_KALLSYMS_ABSOLUTE_PERCPU is enabled, then + * positive values are addresses, and negative values are + * offsets from a base address. + * (2) otherwise, the 4-byte values are directly used as + * addresses + * First, read the values, then figure out which way to + * interpret them. + */ + uint64_t relative_base; + if (bits64) { + err = drgn_program_read_u64(prog, loc->kallsyms_relative_base, + false, &relative_base); + if (err) + return err; + if (bswap) + relative_base = bswap_64(relative_base); + } else { + uint32_t rel32; + err = drgn_program_read_u32(prog, loc->kallsyms_relative_base, + false, &rel32); + if (err) + return err; + if (bswap) + rel32 = bswap_32(rel32); + relative_base = rel32; + } + addr32 = malloc(kr->num_syms * sizeof(uint32_t)); + if (!addr32) + return &drgn_enomem; + + err = drgn_program_read_memory(prog, addr32, + loc->kallsyms_offsets, + kr->num_syms * sizeof(uint32_t), + false); + if (err) { + free(addr32); + return err; + } + if (bswap) + for (int i = 0; i < kr->num_syms; i++) + addr32[i] = bswap_32(addr32[i]); + + /* + * Now that we've read the offsets data, we need to determine + * how to interpret them. To do this, use the _stext symbol. We + * have the correct value from vmcoreinfo. Compute it both ways + * and pick the correct interpretation. + */ + int stext_idx = drgn_kallsyms_lookup(kr,"_stext"); + if (stext_idx < 0) { + free(addr32); + return drgn_error_create( + DRGN_ERROR_OTHER, + "Could not find _stext symbol in kallsyms"); + } + + uint64_t stext_abs = relative_base + addr32[stext_idx]; + uint64_t stext_pcpu = absolute_percpu(relative_base, (int32_t)addr32[stext_idx]); + if (stext_abs == loc->_stext) { + for (int i = 0; i < kr->num_syms; i++) + kr->addresses[i] = relative_base + addr32[i]; + } else if (stext_pcpu == loc->_stext) { + for (int i = 0; i < kr->num_syms; i++) + kr->addresses[i] = absolute_percpu(relative_base, (int32_t)addr32[i]); + } else { + err = drgn_error_create( + DRGN_ERROR_OTHER, + "Unable to interpret kallsyms address data"); + } + free(addr32); + } + return err; +} + +/** Free all data held by @a kr */ +void drgn_kallsyms_destroy(struct kallsyms_finder *kr) +{ + if (kr) { + drgn_kallsyms_names_deinit(&kr->htab); + free(kr->sorted); + free(kr->addresses); + free(kr->strings); + free(kr->names); + free(kr->types); + } +} + +/** Load kallsyms data from vmcore + vmcoreinfo data */ +static struct drgn_error * +drgn_kallsyms_from_vmcore(struct kallsyms_finder *kr, struct drgn_program *prog, + struct kallsyms_locations *loc) +{ + struct drgn_error *err; + + memset(kr, 0, sizeof(*kr)); + kr->prog = prog; + drgn_kallsyms_names_init(&kr->htab); + + err = kallsyms_load_names(kr, loc); + if (err) + goto out; + + err = kallsyms_create_htab(kr); + if (err) + goto out; + + err = kallsyms_load_addresses(prog, kr, loc); + if (err) + goto out; + + return NULL; + +out: + drgn_kallsyms_destroy(kr); + return err; +} + +struct allocated { + uint32_t symbols; + size_t symbol_buffer; +}; + +/** Append a symbol onto the kallsyms finder, expanding the allocations if needed. */ +static struct drgn_error * +kallsyms_append(struct kallsyms_finder *kr, struct allocated *a, const char *name, uint64_t address, char type) +{ + size_t name_len = strlen(name) + 1; + if (kr->num_syms == a->symbols) { + a->symbols = a->symbols ? a->symbols * 2 : 1024; + kr->names = realloc(kr->names, a->symbols * sizeof(kr->names[0])); + kr->addresses = realloc(kr->addresses, a->symbols * sizeof(kr->addresses[0])); + kr->types = realloc(kr->types, a->symbols); + if (!kr->names || !kr->addresses || !kr->types) + return &drgn_enomem; + } + + while (kr->strings_len + name_len > a->symbol_buffer) { + a->symbol_buffer = a->symbol_buffer ? a->symbol_buffer * 2 : 4096; + kr->strings = realloc(kr->strings, a->symbol_buffer); + if (!kr->strings) + return &drgn_enomem; + } + memcpy(&kr->strings[kr->strings_len], name, name_len); + /* + * We can't just store the pointer, since symbol_buffer may move during + * reallocation. Store the index of the string in the buffer, and when + * we finalize everything, we will fix it up. + */ + kr->names[kr->num_syms] = kr->strings_len; + kr->addresses[kr->num_syms] = address; + kr->types[kr->num_syms] = type; + kr->num_syms++; + kr->strings_len += name_len; + return NULL; +} + +/** Reallocate buffers to fit contents, and fixup the symbol array */ +static struct drgn_error * +kallsyms_finalize(struct kallsyms_finder *kr) +{ + kr->names = realloc(kr->names, kr->num_syms * sizeof(kr->names[0])); + kr->addresses = realloc(kr->addresses, kr->num_syms * sizeof(kr->addresses[0])); + kr->types = realloc(kr->types, kr->num_syms * sizeof(kr->types[0])); + kr->strings = realloc(kr->strings, kr->strings_len); + if (!kr->names || !kr->addresses || !kr->types || !kr->strings) + return &drgn_enomem; + return NULL; +} + +/** Load kallsyms directly from the /proc/kallsyms file */ +static struct drgn_error *drgn_kallsyms_from_proc(struct kallsyms_finder *kr, + struct drgn_program *prog) +{ + char *line = NULL; + size_t line_size = 0; + ssize_t res; + size_t line_number = 1; + struct allocated allocated = {0}; + struct drgn_error *err = NULL; + FILE *fp = fopen("/proc/kallsyms", "r"); + if (!fp) + return drgn_error_create_os("Error opening kallsyms", errno, "/proc/kallsyms"); + + memset(kr, 0, sizeof(*kr)); + kr->prog = prog; + drgn_kallsyms_names_init(&kr->htab); + + while ((res = getline(&line, &line_size, fp)) != -1) { + char *save = NULL; + char *name, *addr_str, *type_str, *mod, *addr_rem; + char type; + uint64_t addr; + + addr_str = strtok_r(line, " \t\r\n", &save); + type_str = strtok_r(NULL," \t\r\n", &save); + name = strtok_r(NULL," \t\r\n", &save); + mod = strtok_r(NULL," \t\r\n", &save); + + if (!addr_str || !type_str || !name) { + err = drgn_error_format(DRGN_ERROR_SYNTAX, "Error parsing kallsyms line %zu", line_number); + break; + } + if (mod) + break; + type = *type_str; + addr = strtoull(addr_str, &addr_rem, 16); + if (*addr_rem) { + /* addr_rem should be set to the first un-parsed character, and + * since the entire string should be a valid base 16 integer, + * we expect it to be \0 */ + err = drgn_error_format(DRGN_ERROR_SYNTAX, + "Invalid address \"%s\" in kallsyms line %zu", + addr_str, line_number); + break; + } + err = kallsyms_append(kr, &allocated, name, addr, type); + if (err) + break; + line_number++; + } + + if (!err && ferror(fp)) + err = drgn_error_create_os("Error reading kallsyms", errno, "/proc/kallsyms"); + else + err = kallsyms_finalize(kr); + if (!err) + err = kallsyms_create_htab(kr); + fclose(fp); + free(line); + if (err) + drgn_kallsyms_destroy(kr); + return err; +} + +struct drgn_error *drgn_kallsyms_init(struct kallsyms_finder *kr, + struct drgn_program *prog, + struct kallsyms_locations *loc) +{ + /* + * There are two ways to parse kallsyms data: by using /proc/kallsyms, + * or by finding the necessary symbols in the vmcoreinfo and using them + * to read out the kallsyms data from the vmcore. + * + * Reading /proc/kallsyms is more straightforward, performant, and it + * has broader kernel version support: it should be preferred for live + * systems. + * + * Parsing kallsyms from a core dump is more involved, and it requires + * that the kernel publish some symbol addresses in the VMCOREINFO note. + * The following kernel commits are required, and were introduced in + * 6.0: + * + * - 5fd8fea935a10 ("vmcoreinfo: include kallsyms symbols") + * - f09bddbd86619 ("vmcoreinfo: add kallsyms_num_syms symbol") + */ + if (prog->flags & DRGN_PROGRAM_IS_LIVE) + return drgn_kallsyms_from_proc(kr, prog); + else if (loc->kallsyms_names && loc->kallsyms_token_table + && loc->kallsyms_token_index && loc->kallsyms_num_syms) + return drgn_kallsyms_from_vmcore(kr, prog, loc); + else + return drgn_error_create( + DRGN_ERROR_MISSING_DEBUG_INFO, + "The symbols: kallsyms_names, kallsyms_token_table, " + "kallsyms_token_index, and kallsyms_num_syms were not " + "found in VMCOREINFO, and the program is not live, " + "so /proc/kallsyms cannot be used. There is not enough " + "information to use the kallsyms symbol finder." + ); +} diff --git a/libdrgn/kallsyms.h b/libdrgn/kallsyms.h new file mode 100644 index 000000000..ebe94314d --- /dev/null +++ b/libdrgn/kallsyms.h @@ -0,0 +1,135 @@ +// Copyright (c) 2023 Oracle and/or its affiliates +// SPDX-License-Identifier: LGPL-2.1-or-later + +/** + * @file + * + * Kallsyms data handling + * + * See @ref Kallsyms + */ + +#ifndef DRGN_KALLSYMS_H +#define DRGN_KALLSYMS_H + +#include +#include + +#include "hash_table.h" + +struct drgn_program; +struct drgn_module; +struct vmcoreinfo; +enum drgn_find_symbol_flags; +struct drgn_symbol_result_builder; + +struct kallsyms_locations { + uint64_t kallsyms_names; + uint64_t kallsyms_token_table; + uint64_t kallsyms_token_index; + uint64_t kallsyms_num_syms; + uint64_t kallsyms_offsets; + uint64_t kallsyms_relative_base; + uint64_t kallsyms_addresses; + uint64_t _stext; +}; + +/** + * @ingroup KernelInfo + * + * @defgroup Kallsyms Kallsyms symbol table + * + * Using the kallsyms data from within the program as a symbol table. + * + * @{ + */ + +struct symbol_entry { + uint32_t start; + uint32_t end; +}; + +DEFINE_HASH_MAP(drgn_kallsyms_names, char *, struct symbol_entry, + c_string_key_hash_pair, c_string_key_eq); + +/** + * Holds kallsyms data copied from the kernel + * + * Kallsyms data are in increasing sorted order by address. Each symbol is + * identified by its index, which we can assume fits in a uint32_t. The + * essential data is stored in arrays of length "num_syms": the memory address, + * the symbol type, and the index into the string table. + * + * Strings are stored in a single buffer, all concatenated together and + * separated by nul bytes. + */ +struct kallsyms_finder { + /** Program owning this registry */ + struct drgn_program *prog; + + /** Number of symbols */ + uint32_t num_syms; + /** Array of symbol addresses */ + uint64_t *addresses; + /** Array of one-character type codes*/ + char *types; + /** Array of symbol names */ + uint32_t *names; + + /** Buffer backing the symbols array, all point into here */ + char *strings; + /** Bytes used of symbol buffer array */ + uint32_t strings_len; + + /** Array of symbol indices, sorted by name. Used by the htab. */ + uint32_t *sorted; + /** Map of symbol names to index */ + struct drgn_kallsyms_names htab; +}; + + +/** + * Initialize kallsyms data + * + * Search for a kallsyms symbol table, and if found, attempt to load it. On + * success, a kallsyms registry is returned in @a ret. If the kallsyms data is + * not found (a common failure mode), NULL will be returned to indicate no + * error, but @a ret will not be set. This indicates that initialization should + * continue. If an error occurs parsing the kallsyms data once it is found, the + * error will be returned. + * + * @param prog Program to search + * @param vi vmcoreinfo from the crash dump + * @param[out] ret Created registry + * @returns NULL on success, or when kallsyms data is not found + */ +struct drgn_error *drgn_kallsyms_init(struct kallsyms_finder *reg, + struct drgn_program *prog, + struct kallsyms_locations *locations); + +/** + * Find a symbol using the symbol finder object + * + * This object may be passed to drgn_program_add_symbol_finder, along with a + * pointer to the struct kallsyms_finder, in order to find symbols in the + * vmlinux kallsyms. + */ +struct drgn_error * +drgn_kallsyms_symbol_finder(const char *name, uint64_t address, + struct drgn_module *module, + enum drgn_find_symbol_flags flags, void *arg, + struct drgn_symbol_result_builder *builder); + +/** + * Destroy kallsyms data + * + * Frees all resources held by the kallsyms finder. Please note that if the + * finder has been added to the program, then this *will* cause errors. + * + * @param kr Finder to destroy + */ +void drgn_kallsyms_destroy(struct kallsyms_finder *kr); + +/** @} */ + +#endif // DRGN_KALLSYMS_H diff --git a/libdrgn/python/drgnpy.h b/libdrgn/python/drgnpy.h index 56608e575..b44f9a26f 100644 --- a/libdrgn/python/drgnpy.h +++ b/libdrgn/python/drgnpy.h @@ -109,6 +109,11 @@ typedef struct { PyObject *attr_cache; } DrgnType; +typedef struct { + PyObject_HEAD + struct kallsyms_finder *finder; +} KallsymsFinder; + typedef struct { PyObject_HEAD /* @@ -225,6 +230,7 @@ extern PyObject *TypeKind_class; extern PyTypeObject DrgnObject_type; extern PyTypeObject DrgnType_type; extern PyTypeObject FaultError_type; +extern PyTypeObject KallsymsFinder_type; extern PyTypeObject Language_type; extern PyTypeObject ObjectIterator_type; extern PyTypeObject Platform_type; diff --git a/libdrgn/python/kallsyms_finder.c b/libdrgn/python/kallsyms_finder.c new file mode 100644 index 000000000..d628166f9 --- /dev/null +++ b/libdrgn/python/kallsyms_finder.c @@ -0,0 +1,147 @@ +// Copyright (c) 2023 Oracle and/or its affiliates +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "drgn.h" +#include "drgnpy.h" +#include "kallsyms.h" +#include "modsupport.h" +#include "pyerrors.h" +#include "symbol.h" + +static void KallsymsFinder_dealloc(KallsymsFinder *self) +{ + /* This can't be called if the finder has been added to the program. The + * program should take a reference and prevent deallocation. */ + drgn_kallsyms_destroy(self->finder); + free(self->finder); + Py_TYPE(self)->tp_free((PyObject *)self); +} + + +static PyObject *KallsymsFinder_repr(KallsymsFinder *self) +{ + return (PyObject *)PyUnicode_FromString("KallsymsFinder()"); +} + +static PyObject *KallsymsFinder_call(KallsymsFinder *self, PyObject *args, PyObject *kwargs) +{ + PyObject *address_obj, *name_obj; + uint64_t address = 0; + const char *name = NULL; + static char *kwnames[] = {"name", "address", "one", NULL}; + unsigned int flags = 0; + bool single; + struct drgn_symbol_result_builder builder; + struct drgn_error *err; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "OOp:__call__", kwnames, + &name_obj, &address_obj, &single)) + return NULL; + + flags |= single ? DRGN_FIND_SYM_ONE : 0; + + if (address_obj != Py_None) { + if (!PyLong_Check(address_obj)) { + PyErr_SetString(PyExc_TypeError, "address: an integer is required"); + return NULL; + } + flags |= DRGN_FIND_SYM_ADDR; + address = PyLong_AsUint64(address_obj); + /* Overflow check */ + if (PyErr_Occurred()) + return NULL; + } + if (name_obj != Py_None) { + if (!PyUnicode_Check(name_obj)) { + PyErr_SetString(PyExc_TypeError, "name: a string is required"); + return NULL; + } + flags |= DRGN_FIND_SYM_NAME; + name = PyUnicode_AsUTF8(name_obj); + } + + drgn_symbol_result_builder_init(&builder, flags); + + err = drgn_kallsyms_symbol_finder(name, address, NULL, flags, self->finder, &builder); + if (err) + goto error; + + /* We return a list regardless */ + if (single) { + _cleanup_pydecref_ PyObject *list = PyList_New(1); + if (!list) + goto error; + struct drgn_symbol* symbol = drgn_symbol_result_builder_single(&builder); + PyObject *prog_obj = (PyObject *)container_of(self->finder->prog, Program, prog); + PyObject *pysym = Symbol_wrap(symbol, prog_obj); + if (!pysym) + goto error; + PyList_SET_ITEM(list, 0, pysym); + return_ptr(list); + } else { + struct drgn_symbol **syms; + size_t count; + drgn_symbol_result_builder_array(&builder, &syms, &count); + return Symbol_list_wrap(syms, count, + container_of(self->finder->prog, Program, prog)); + } + + return NULL; +error: + drgn_symbol_result_builder_destroy(&builder); + return err ? set_drgn_error(err) : NULL; +} + +static PyObject *KallsymsFinder_new(PyTypeObject *subtype, PyObject *args, PyObject *kwds) +{ + static char *kwnames[] = {"prog", "names", "token_table", "token_index", "num_syms", + "offsets", "relative_base", "addresses", "_stext", NULL}; + struct kallsyms_locations kl; + PyObject *prog_obj; + struct drgn_program *prog; + struct drgn_error *err; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OKKKKKKKK", kwnames, + &prog_obj, &kl.kallsyms_names, &kl.kallsyms_token_table, + &kl.kallsyms_token_index, &kl.kallsyms_num_syms, + &kl.kallsyms_offsets, &kl.kallsyms_relative_base, + &kl.kallsyms_addresses, &kl._stext)) + return NULL; + + if (!PyObject_TypeCheck(prog_obj, &Program_type)) + return PyErr_Format(PyExc_TypeError, "expected Program, not %s", + Py_TYPE(prog_obj)->tp_name); + + prog = &((Program *)prog_obj)->prog; + + struct kallsyms_finder *finder = calloc(1, sizeof(*finder)); + if (!finder) + return set_drgn_error(&drgn_enomem); + err = drgn_kallsyms_init(finder, prog, &kl); + if (err) + goto out; + + KallsymsFinder *finder_obj = call_tp_alloc(KallsymsFinder); + if (!finder_obj) { + drgn_kallsyms_destroy(finder); + goto out; + } + finder_obj->finder = finder; + Py_INCREF(prog_obj); + return (PyObject *)finder_obj; +out: + free(finder); + return set_drgn_error(err); +} + +PyTypeObject KallsymsFinder_type = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "_drgn.KallsymsFinder", + .tp_basicsize = sizeof(KallsymsFinder), + .tp_dealloc = (destructor)KallsymsFinder_dealloc, + .tp_repr = (reprfunc)KallsymsFinder_repr, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_doc = drgn_KallsymsFinder_DOC, + .tp_call = (ternaryfunc)KallsymsFinder_call, + .tp_new = KallsymsFinder_new, +}; diff --git a/libdrgn/python/main.c b/libdrgn/python/main.c index 2bc92517d..180e43055 100644 --- a/libdrgn/python/main.c +++ b/libdrgn/python/main.c @@ -264,6 +264,7 @@ DRGNPY_PUBLIC PyMODINIT_FUNC PyInit__drgn(void) }) if (add_module_constants(m) || + add_type(m, &KallsymsFinder_type) || add_type(m, &Language_type) || add_languages() || add_type(m, &DrgnObject_type) || PyType_Ready(&ObjectIterator_type) || diff --git a/libdrgn/python/program.c b/libdrgn/python/program.c index cc499dd13..42d01429b 100644 --- a/libdrgn/python/program.c +++ b/libdrgn/python/program.c @@ -10,6 +10,7 @@ #include "../string_builder.h" #include "../util.h" #include "../vector.h" +#include "kallsyms.h" DEFINE_HASH_SET_FUNCTIONS(pyobjectp_set, ptr_key_hash_pair, scalar_key_eq); @@ -596,8 +597,15 @@ static PyObject *Program_add_symbol_finder(Program *self, PyObject *args, if (ret == -1) return NULL; - err = drgn_program_add_symbol_finder(&self->prog, py_symbol_find_fn, - fn); + /* Fast path for the builtin kallsyms finder, avoidng Python object + * allocation overhead */ + if (PyObject_TypeCheck(fn, &KallsymsFinder_type)) + err = drgn_program_add_symbol_finder(&self->prog, + drgn_kallsyms_symbol_finder, + ((KallsymsFinder *)fn)->finder); + else + err = drgn_program_add_symbol_finder(&self->prog, py_symbol_find_fn, + fn); if (err) return set_drgn_error(err); Py_RETURN_NONE;