From 93c00a720d1f52917bd5b85c127261ffb174991d Mon Sep 17 00:00:00 2001 From: Omar Sandoval Date: Tue, 17 Dec 2024 13:18:36 -0800 Subject: [PATCH] Introduce module and debug info finder APIs drgn currently provides limited control over how debugging information is found. drgn has hardcoded logic for where to search for debugging information. The most the user can do is provide a list of files for drgn to try in addition to the default locations (with the -s CLI option or the drgn.Program.load_debug_info() method). The implementation is also a mess. We use libdwfl, but its data model is slightly different from what we want, so we have to work around it or reimplement its functionality in several places: see commits e5874ad18a53 ("libdrgn: use libdwfl"), e6abfeac0329 ("libdrgn: debug_info: report userspace core dump debug info ourselves"), and 1d4854a5bce7 ("libdrgn: implement optimized x86-64 ELF relocations") for some examples. The mismatched combination of libdwfl and our own code is difficult to maintain, and the lack of control over the whole debug info pipeline has made it difficult to fix several longstanding issues. The solution is a major rework removing our libdwfl dependency and replacing it with our own model. This (huge) commit is that rework comprising the following components: - drgn.Module/struct drgn_module, a representation of a binary used by a program. - Automatic discovery of the modules loaded in a program. - Interfaces for manually creating and overriding modules. - Automatic discovery of debugging information from the standard locations and debuginfod. - Interfaces for custom debug info finders and for manually overriding debugging information. - Tons of test cases. A lot of care was taken to make these interfaces extremely flexible yet cohesive. The existing interfaces are also reimplemented on top of the new functionality to maintain backwards compatibility, with one exception: drgn.Program.load_debug_info()/-s would previously accept files that it didn't find loaded in the program. This turned out to be a big footgun for users, so now this must be done explicitly (with drgn.ExtraModule/--extra-symbols). The API and implementation both owe a lot to libdwfl: - The concepts of modules, module address ranges/section addresses, and file biases are heavily inspired by the libdwfl interfaces. - Ideas for determining modules in userspace processes and core dumps were taken from libdwfl. - Our implementation of ELF symbol table address lookups is based on dwfl_module_addrinfo(). drgn has taken these concepts and fine-tuned them based on lessons learned. Credit is also due to Stephen Brennan for early testing and feedback. Closes #16, closes #25, closes #332. Signed-off-by: Omar Sandoval --- _drgn.pyi | 735 +- docs/advanced_usage.rst | 139 +- docs/api_reference.rst | 40 +- docs/user_guide.rst | 41 + drgn/__init__.py | 18 + drgn/cli.py | 53 +- libdrgn/Makefile.am | 4 + libdrgn/build-aux/gen_constants.py | 6 + libdrgn/build-aux/gen_elf_sections.py | 8 +- libdrgn/cleanup.h | 10 + libdrgn/debug_info.c | 6970 ++++++++++++----- libdrgn/debug_info.h | 398 +- libdrgn/drgn.h | 553 +- libdrgn/dwarf_info.c | 313 +- libdrgn/dwarf_info.h | 34 +- libdrgn/elf_file.c | 708 +- libdrgn/elf_file.h | 110 +- libdrgn/elf_symtab.c | 450 ++ libdrgn/elf_symtab.h | 55 + libdrgn/error.c | 17 - libdrgn/error.h | 19 +- libdrgn/examples/load_debug_info.c | 3 + libdrgn/handler.h | 5 + libdrgn/linux_kernel.c | 1743 ++--- libdrgn/linux_kernel.h | 16 +- libdrgn/orc_info.c | 4 + libdrgn/program.c | 152 +- libdrgn/program.h | 16 + libdrgn/python/drgnpy.h | 49 +- libdrgn/python/main.c | 11 + libdrgn/python/module.c | 593 ++ libdrgn/python/module_section_addresses.c | 260 + libdrgn/python/program.c | 399 + libdrgn/python/util.c | 20 + libdrgn/register_state.c | 9 +- libdrgn/symbol.c | 71 +- libdrgn/symbol.h | 14 +- libdrgn/util.h | 2 + scripts/crashme/Makefile | 65 + scripts/crashme/common.c | 10 + scripts/crashme/crashme.c | 25 + scripts/crashme/crashme.h | 15 + scripts/crashme/main.c | 10 + tests/linux_kernel/test_debug_info.py | 116 +- tests/resources/crashme.alt.zst | Bin 0 -> 409 bytes tests/resources/crashme.core.zst | Bin 0 -> 18351 bytes tests/resources/crashme.dwz.zst | Bin 0 -> 2716 bytes tests/resources/crashme.so.dwz.zst | Bin 0 -> 2482 bytes tests/resources/crashme.so.zst | Bin 0 -> 2561 bytes tests/resources/crashme.zst | Bin 0 -> 2727 bytes tests/resources/crashme_pie.core.zst | Bin 0 -> 18430 bytes tests/resources/crashme_pie.zst | Bin 0 -> 2803 bytes .../resources/crashme_pie_no_headers.core.zst | Bin 0 -> 14787 bytes tests/resources/crashme_static.core.zst | Bin 0 -> 5234 bytes tests/resources/crashme_static.zst | Bin 0 -> 5299 bytes tests/resources/crashme_static_pie.core.zst | Bin 0 -> 5482 bytes tests/resources/crashme_static_pie.zst | Bin 0 -> 8036 bytes tests/test_debug_info.py | 2671 +++++++ tests/test_dwarf.py | 38 +- tests/test_module.py | 489 ++ tests/test_symbol.py | 228 +- 61 files changed, 14074 insertions(+), 3641 deletions(-) create mode 100644 libdrgn/elf_symtab.c create mode 100644 libdrgn/elf_symtab.h create mode 100644 libdrgn/python/module.c create mode 100644 libdrgn/python/module_section_addresses.c create mode 100644 scripts/crashme/Makefile create mode 100644 scripts/crashme/common.c create mode 100644 scripts/crashme/crashme.c create mode 100644 scripts/crashme/crashme.h create mode 100644 scripts/crashme/main.c create mode 100644 tests/resources/crashme.alt.zst create mode 100644 tests/resources/crashme.core.zst create mode 100755 tests/resources/crashme.dwz.zst create mode 100755 tests/resources/crashme.so.dwz.zst create mode 100755 tests/resources/crashme.so.zst create mode 100755 tests/resources/crashme.zst create mode 100644 tests/resources/crashme_pie.core.zst create mode 100755 tests/resources/crashme_pie.zst create mode 100644 tests/resources/crashme_pie_no_headers.core.zst create mode 100644 tests/resources/crashme_static.core.zst create mode 100755 tests/resources/crashme_static.zst create mode 100644 tests/resources/crashme_static_pie.core.zst create mode 100755 tests/resources/crashme_static_pie.zst create mode 100644 tests/test_debug_info.py create mode 100644 tests/test_module.py diff --git a/_drgn.pyi b/_drgn.pyi index 9fcd8e75d..d7bbb9e95 100644 --- a/_drgn.pyi +++ b/_drgn.pyi @@ -20,6 +20,8 @@ from typing import ( Iterator, List, Mapping, + MutableMapping, + NamedTuple, Optional, Sequence, Set, @@ -29,9 +31,9 @@ from typing import ( ) if sys.version_info < (3, 8): - from typing_extensions import Final, Protocol + from typing_extensions import Final, Literal, Protocol else: - from typing import Final, Protocol + from typing import Final, Literal, Protocol if sys.version_info < (3, 10): from typing_extensions import TypeAlias @@ -691,34 +693,343 @@ class Program: """ ... + def modules(self) -> Iterator[Module]: + """Get an iterator over all of the created modules in the program.""" + + def loaded_modules(self) -> Iterator[Tuple[Module, bool]]: + """ + Determine what executables, libraries, etc. are loaded in the program + and create modules to represent them. + + This may automatically load some debugging information necessary to + enumerate the modules. Other than that, it does not load debugging + information. + + See :meth:`load_debug_info()` for a higher-level interface that does + load debugging information. + + :return: Iterator of module and ``True`` if it was newly created + or ``False`` if it was previously found. + """ + ... + + @overload + def main_module( + self, name: Optional[Path] = None, *, create: Literal[False] = False + ) -> MainModule: + """ + Find the main module. + + :param name: :attr:`Module.name`, or ``None`` to match any name + :raises LookupError: if main module has not been created or its name + doesn't match + """ + ... + + @overload + def main_module( + self, name: Path, *, create: Literal[True] + ) -> Tuple[MainModule, bool]: + """ + Find or create the main module. + + :param name: :attr:`Module.name` + :return: Module and ``True`` if it was newly created or ``False`` if it + was found. + :raises LookupError: if main module was already created with a + different name + """ + ... + + @overload + def shared_library_module( + self, + name: Path, + dynamic_address: IntegerLike, + *, + create: Literal[False] = False, + ) -> SharedLibraryModule: + """ + Find a shared library module. + + :param name: :attr:`Module.name` + :param dynamic_address: :attr:`SharedLibraryModule.dynamic_address` + :return: Shared library module with the given name and dynamic address. + :raises LookupError: if no matching module has been created + """ + ... + + @overload + def shared_library_module( + self, name: Path, dynamic_address: IntegerLike, *, create: Literal[True] + ) -> Tuple[SharedLibraryModule, bool]: + """ + Find or create a shared library module. + + :param name: :attr:`Module.name` + :param dynamic_address: :attr:`SharedLibraryModule.dynamic_address` + :return: Module and ``True`` if it was newly created or ``False`` if it + was found. + """ + ... + + @overload + def vdso_module( + self, + name: Path, + dynamic_address: IntegerLike, + *, + create: Literal[False] = False, + ) -> VdsoModule: + """ + Find a vDSO module. + + :param name: :attr:`Module.name` + :param dynamic_address: :attr:`VdsoModule.dynamic_address` + :return: vDSO module with the given name and dynamic address. + :raises LookupError: if no matching module has been created + """ + ... + + @overload + def vdso_module( + self, name: Path, dynamic_address: IntegerLike, *, create: Literal[True] + ) -> Tuple[VdsoModule, bool]: + """ + Find or create a vDSO module. + + :param name: :attr:`Module.name` + :param dynamic_address: :attr:`VdsoModule.dynamic_address` + :return: Module and ``True`` if it was newly created or ``False`` if it + was found. + """ + ... + + @overload + def relocatable_module( + self, name: Path, address: IntegerLike, *, create: Literal[False] = False + ) -> RelocatableModule: + """ + Find a relocatable module. + + :param name: :attr:`Module.name` + :param address: :attr:`RelocatableModule.address` + :return: Relocatable module with the given name and address. + :raises LookupError: if no matching module has been created + """ + ... + + @overload + def relocatable_module( + self, name: Path, address: IntegerLike, *, create: Literal[True] + ) -> Tuple[RelocatableModule, bool]: + """ + Find or create a relocatable module. + + :param name: :attr:`Module.name` + :param address: :attr:`RelocatableModule.address` + :return: Module and ``True`` if it was newly created or ``False`` if it + was found. + """ + ... + + @overload + def linux_kernel_loadable_module( + self, module_obj: Object, *, create: Literal[False] = False + ) -> RelocatableModule: + """ + Find a Linux kernel loadable module from a ``struct module`` object. + + Note that kernel modules are represented as relocatable modules. + + :param module_obj: ``struct module`` or ``struct module *`` object for + the kernel module. + :return: Relocatable module with a name and address matching + *module_obj*. + :raises LookupError: if no matching module has been created + """ + ... + + @overload + def linux_kernel_loadable_module( + self, module_obj: Object, *, create: Literal[True] + ) -> Tuple[RelocatableModule, bool]: + """ + Find or create a Linux kernel loadable module from a ``struct module`` + object. + + If a new module is created, its :attr:`~Module.address_range` and + :attr:`~RelocatableModule.section_addresses` are set from *module_obj*. + + :param module_obj: ``struct module`` or ``struct module *`` object for + the kernel module. + :return: Module and ``True`` if it was newly created or ``False`` if it + was found. + """ + ... + + @overload + def extra_module( + self, name: Path, id: IntegerLike = 0, *, create: Literal[False] = False + ) -> ExtraModule: + """ + Find an extra module. + + :param name: :attr:`Module.name` + :param id: :attr:`ExtraModule.id` + :return: Extra module with the given name and ID number. + :raises LookupError: if no matching module has been created + """ + ... + + @overload + def extra_module( + self, name: Path, id: IntegerLike = 0, *, create: Literal[True] + ) -> Tuple[ExtraModule, bool]: + """ + Find or create an extra module. + + :param name: :attr:`Module.name` + :param id: :attr:`ExtraModule.id` + :return: Module and ``True`` if it was newly created or ``False`` if it + was found. + """ + ... + + def module(self, __address: IntegerLike) -> Module: + """ + Find the module containing the given address. + + Addresses are matched based on :attr:`Module.address_range`. + + :param address: Address to search for. + :raises LookupError: if no module contains the given address + """ + ... + + def register_debug_info_finder( + self, + name: str, + fn: Callable[[Sequence[Module]], None], + *, + enable_index: Optional[int] = None, + ) -> None: + """ + Register a callback for finding debugging information. + + This does not enable the finder unless *enable_index* is given. + + :param name: Finder name. + :param fn: Callable taking a list of :class:`Module`\\ s that want + debugging information. + + This should check :meth:`Module.wants_loaded_file()` and + :meth:`Module.wants_debug_file()` and do one of the following for + each module: + + * Obtain and/or locate a file wanted by the module and call + :meth:`Module.try_file()`. + * Install files for a later finder to use. + * Set :attr:`Module.loaded_file_status` or + :attr:`Module.debug_file_status` to + :attr:`ModuleFileStatus.DONT_NEED` if the finder believes that + the file is not needed. + * Ignore it, for example if the finder doesn't know how to find the + wanted files for the module. + :param enable_index: Insert the finder into the list of enabled object + finders at the given index. If -1 or greater than the number of + enabled finders, insert it at the end. If ``None`` or not given, + don't enable the finder. + :raises ValueError: if there is already a finder with the given name + """ + ... + + def registered_debug_info_finders(self) -> Set[str]: + """Return the names of all registered debugging information finders.""" + ... + + def set_enabled_debug_info_finders(self, names: Sequence[str]) -> None: + """ + Set the list of enabled debugging information finders. + + Finders are called in the same order as the list until all wanted files + have been found. + + Finders that are not in the list are not called. + + :param names: Names of finders to enable, in order. + :raises ValueError: if no finder has a given name or the same name is + given more than once + """ + ... + + def enabled_debug_info_finders(self) -> List[str]: + """ + Return the names of enabled debugging information finders, in order. + """ + ... + debug_info_path: Optional[str] + """ + Directories to search for debugging information files. + + The standard debugging information finder supports searching for files by + *build ID* (a unique byte string present in both the :ref:`loaded file + ` and the :ref:`debug file `) and by + *debug link* (a name and checksum in the loaded file that refers to the + debug file). + + This setting controls what directories the standard debugging information + finder searches. It is a sequence of paths separated by colons (``:``). + + Searches by build ID ignore relative paths. They check under each absolute + path for a file named ``.build-id/xx/yyyy`` (for loaded files) or + ``.build-id/xx/yyyy.debug`` (for debug files), where ``xxyyyy`` is the + lowercase hexadecimal representation of the build ID. + + Searches by debug link check every path for a file with the name given by + the debug link. Relative paths are relative to the directory containing the + loaded file. An empty path means the directory containing the loaded file. + + The default is ``:.debug:/usr/lib/debug``, which should work out of the box + on most Linux distributions. + + If ``None``, then searches by build ID and debug link are disabled (unless + the debug link is an absolute path). + """ + def load_debug_info( self, - paths: Optional[Iterable[Path]] = None, + paths: Optional[Iterable[Path]] = (), default: bool = False, main: bool = False, ) -> None: """ - Load debugging information for a list of executable or library files. + Load debugging information for the given set of files and/or modules. - Note that this is parallelized, so it is usually faster to load - multiple files at once rather than one by one. + This determines what executables, libraries, etc. are loaded in the + program (see :meth:`loaded_modules()`) and tries to load their + debugging information from the given *paths*. - :param paths: Paths of binary files. - :param default: Also load debugging information which can automatically - be determined from the program. + .. note:: + It is much more efficient to load multiple files at once rather + than one by one when possible. - For the Linux kernel, this tries to load ``vmlinux`` and any loaded - kernel modules from a few standard locations. + :param paths: Paths of binary files to try. - For userspace programs, this tries to load the executable and any - loaded libraries. + Files that don't correspond to any loaded modules are ignored. See + :class:`ExtraModule` for a way to provide arbitrary debugging + information. + :param default: Try to load all debugging information for all loaded + modules. - This implies ``main=True``. - :param main: Also load debugging information for the main executable. + The files in *paths* are tried first before falling back to the + enabled debugging information finders. - For the Linux kernel, this tries to load ``vmlinux``. + This implies ``main=True``. + :param main: Try to load all debugging information for the main module. - This is currently ignored for userspace programs. + The files in *paths* are tried first before falling back to the + enabled debugging information finders. :raises MissingDebugInfoError: if debugging information was not available for some files; other files with debugging information are still loaded @@ -727,10 +1038,20 @@ class Program: def load_default_debug_info(self) -> None: """ - Load debugging information which can automatically be determined from - the program. + Load all debugging information that can automatically be determined + from the program. - This is equivalent to ``load_debug_info(None, True)``. + This is equivalent to ``load_debug_info(default=True)``. + """ + ... + + def load_module_debug_info(self, *modules: Module) -> None: + """ + Load debugging information for the given modules using the enabled + debugging information finders. + + The files to search for are controlled by + :attr:`Module.loaded_file_status` and :attr:`Module.debug_file_status`. """ ... cache: Dict[Any, Any] @@ -1105,6 +1426,380 @@ class NoDefaultProgramError(Exception): ... +class Module: + """ + A ``Module`` represents an executable, library, or other binary file used + by a program. It has several subclasses representing specific types of + modules. + + Modules are uniquely identified by their type, name, and a type-specific + value. + + Modules have several attributes that are determined automatically whenever + possible but may be overridden manually if needed. + + Modules can be assigned files that provide debugging and runtime + information: + + * .. _module-loaded-file: + + The "loaded file" is the file containing the executable code, data, etc. + used by the program at runtime. + + + * .. _module-debug-file: + + The "debug file" is the file containing debugging information (e.g., + `DWARF `_). + + The loaded file and debug file may be the same file, for example, an + unstripped binary. They may be different files if the binary was stripped + and its debugging information was split into a separate file. + + + * .. _module-supplementary-debug-file: + + The debug file may depend on a "supplementary debug file" such as one + generated by `dwz(1) `_. If so, + then the supplementary debug file must be found before the debug file can + be used. + """ + + prog: Final[Program] + """Program that this module is from.""" + name: Final[str] + """ + Name of this module. + + Its exact meaning varies by module type. + """ + address_range: Optional[Tuple[int, int]] + """ + Address range where this module is loaded. + + This is a tuple of the start (inclusive) and end (exclusive) addresses. If + the module is not loaded in memory, then both are 0. If not known yet, then + this is ``None``. + + :meth:`Program.loaded_modules()` sets this automatically from the program + state/core dump when possible. Otherwise, for :class:`MainModule`, + :class:`SharedLibraryModule`, and :class:`VdsoModule`, it may be set + automatically when a file is assigned to the module. It is never set + automatically for :class:`ExtraModule`. It can also be set manually. + """ + build_id: Optional[bytes] + """ + Unique byte string (e.g., GNU build ID) identifying files used by this + module. + + If not known, then this is ``None``. + + :meth:`Program.loaded_modules()` sets this automatically from the program + state/core dump when possible. Otherwise, when a file is assigned to the + module, it is set to the file's build ID if it is not already set. It can + also be set manually. + """ + loaded_file_status: ModuleFileStatus + """Status of the module's :ref:`loaded file `.""" + loaded_file_path: Optional[str] + """ + Absolute path of the module's :ref:`loaded file `, or + ``None`` if not known. + """ + loaded_file_bias: Optional[int] + """ + Difference between the load address in the program and addresses in the + :ref:`loaded file ` itself. + + This is often non-zero due to address space layout randomization (ASLR). + + It is set automatically based on the module type: + + * For :class:`MainModule`, it is set based on metadata from the process or + core dump (the `auxiliary vector + `_ for userspace + programs, the ``VMCOREINFO`` note for the Linux kernel). + * For :class:`SharedLibraryModule` and :class:`VdsoModule`, it is set based + on :attr:`~SharedLibraryModule.dynamic_address`. + * For :class:`RelocatableModule`, it is set to zero. Addresses are adjusted + according to :attr:`~RelocatableModule.section_addresses` instead. + * For :class:`ExtraModule`, it is set based on + :attr:`~Module.address_range`. + """ + debug_file_status: ModuleFileStatus + """Status of the module's :ref:`debug file `.""" + debug_file_path: Optional[str] + """ + Absolute path of the module's :ref:`debug file `, or + ``None`` if not known. + """ + debug_file_bias: Optional[int] + """ + Difference between the load address in the program and addresses in the + :ref:`debug file `. + + See :attr:`loaded_file_bias`. + """ + supplementary_debug_file_kind: Optional[SupplementaryFileKind] + """ + Kind of the module's :ref:`supplementary debug file + `, or ``None`` if not known or not needed. + """ + supplementary_debug_file_path: Optional[str] + """ + Absolute path of the module's :ref:`supplementary debug file + `, or ``None`` if not known or not needed. + """ + + def wants_loaded_file(self) -> bool: + """ + Return whether this module wants a :ref:`loaded file + `. + + This should be preferred over checking :attr:`loaded_file_status` + directly since this is future-proof against new status types being + added. It is currently equivalent to ``module.loaded_file_status == + ModuleFileStatus.WANT``. + """ + ... + + def wants_debug_file(self) -> bool: + """ + Return whether this module wants a :ref:`debug file + `. + + This should be preferred over checking :attr:`debug_file_status` + directly since this is future-proof against new status types being + added. It is currently equivalent to ``module.debug_file_status == + ModuleFileStatus.WANT or module.debug_file_status == + ModuleFileStatus.WANT_SUPPLEMENTARY``. + """ + ... + + def wanted_supplementary_debug_file(self) -> WantedSupplementaryFile: + """ + Return information about the :ref:`supplementary debug file + ` that this module currently wants. + + :raises ValueError: if the module doesn't currently want a + supplementary debug file (i.e., ``module.debug_file_status != + ModuleFileStatus.WANT_SUPPLEMENTARY``) + """ + ... + + def try_file( + self, + path: Path, + *, + fd: int = -1, + force: bool = False, + ) -> None: + """ + Try to use the given file for this module. + + If the file does not appear to belong to this module, then it is + ignored. This currently checks that the file and the module have the + same build ID. + + If :attr:`loaded_file_status` is :attr:`~ModuleFileStatus.WANT` and the + file is loadable, then it is used as the :ref:`loaded file + ` and :attr:`loaded_file_status` is set to + :attr:`~ModuleFileStatus.HAVE`. + + If :attr:`debug_file_status` is :attr:`~ModuleFileStatus.WANT` or + :attr:`~ModuleFileStatus.WANT_SUPPLEMENTARY` and the file provides + debugging information, then it is used as the :ref:`debug file + ` and :attr:`debug_file_status` is set to + :attr:`~ModuleFileStatus.HAVE`. However, if the file requires a + supplementary debug file, then it is not used as the debug file yet and + :attr:`debug_file_status` is set to + :attr:`~ModuleFileStatus.WANT_SUPPLEMENTARY` instead. + + If :attr:`debug_file_status` is + :attr:`~ModuleFileStatus.WANT_SUPPLEMENTARY` and the file matches + :meth:`wanted_supplementary_debug_file()`, then the previously found + file is used as the debug file, the given file is used as the + :ref:`supplementary debug file `, and + :attr:`debug_file_status` is set to :attr:`~ModuleFileStatus.HAVE`. + + The file may be used as both the loaded file and debug file if + applicable. + + :param path: Path to file. + :param fd: If nonnegative, an open file descriptor referring to the + file. This always takes ownership of the file descriptor even if + the file is not used or on error, so the caller must not close it. + :param force: If ``True``, then don't check whether the file matches + the module. + """ + ... + +class MainModule(Module): + """ + Main module. + + There is only one main module in a program. For userspace programs, it is + the executable, and its name is usually the absolute path of the + executable. For the Linux kernel, it is the kernel image, a.k.a. + ``vmlinux``, and its name is "kernel". + """ + +class SharedLibraryModule(Module): + """ + Shared library (a.k.a. dynamic library, dynamic shared object, or ``.so``) + module. + + Shared libraries are uniquely identified by their name (usually the + absolute path of the shared object file) and dynamic address. + """ + + dynamic_address: Final[int] + """Address of the shared object's dynamic section.""" + +class VdsoModule(Module): + """ + Virtual dynamic shared object (vDSO) module. + + The vDSO is a special shared library automatically loaded into a process by + the kernel; see :manpage:`vdso(7)`. It is uniquely identified by its name + (the ``SONAME`` field of the shared object file) and dynamic address. + """ + + dynamic_address: Final[int] + """Address of the shared object's dynamic section.""" + +class RelocatableModule(Module): + """ + Relocatable object module. + + A relocatable object is an object file requiring a linking step to assign + section addresses and adjust the file to reference those addresses. + + Linux kernel loadable modules (``.ko`` files) are a special kind of + relocatable object. + + For userspace programs, relocatable objects are usually intermediate + products of the compilation process (``.o`` files). They are not typically + loaded at runtime. However, drgn allows manually defining a relocatable + module and assigning its section addresses if needed. + + Relocatable modules are uniquely identified by a name and address. + """ + + address: Final[int] + """ + Address identifying the module. + + For Linux kernel loadable modules, this is the module base address. + """ + + section_addresses: MutableMapping[str, int] + """ + Mapping from section names to assigned addresses. + + Once a file has been assigned to the module, this can no longer be + modified. + + :meth:`Program.linux_kernel_loadable_module()` and + :meth:`Program.loaded_modules()` prepopulate this for Linux kernel loadable + modules. + """ + +class ExtraModule(Module): + """ + Module with extra debugging information. + + For advanced use cases, it may be necessary to manually add debugging + information that does not fit into any of the categories above. + ``ExtraModule`` is intended for these use cases. For example, it can be + used to add debugging information from a standalone file that is not in use + by a particular program. + + Extra modules are uniquely identified by an arbitrary name and ID number. + """ + + id: Final[int] + """Arbitrary identification number.""" + +class ModuleFileStatus(enum.Enum): + """ + Status of a file in a :class:`Module`. + + This is usually used to communicate with debugging information finders; see + :meth:`Program.register_debug_info_finder()`. + """ + + WANT = ... + """File has not been found and should be searched for.""" + + HAVE = ... + """File has already been found and assigned.""" + + DONT_WANT = ... + """ + File has not been found, but it should not be searched for. + + :meth:`Module.try_file()` and debugging information finders are required to + honor this and will never change it. However, other operations may reset + this to :attr:`WANT` when they load debugging information automatically. + """ + + DONT_NEED = ... + """ + File has not been found and is not needed (e.g., because its debugging + information is not applicable or is provided through another mechanism). + + In contrast to :attr:`DONT_WANT`, drgn itself will never change this to + :attr:`WANT`. + """ + + WANT_SUPPLEMENTARY = ... + """ + File has been found, but it requires a supplementary file before it can be + used. See :meth:`Module.wanted_supplementary_debug_file()`. + """ + +class WantedSupplementaryFile(NamedTuple): + """Information about a wanted supplementary file.""" + + kind: SupplementaryFileKind + """Kind of supplementary file.""" + path: str + """Path of main file that wants the supplementary file.""" + supplementary_path: str + """ + Path to the supplementary file. + + This may be absolute or relative to :attr:`path`. + """ + checksum: bytes + """ + Unique identifier of the supplementary file. + + The interpretation depends on :attr:`kind`. + """ + +class SupplementaryFileKind(enum.Enum): + """ + Kind of supplementary file. + + .. note:: + DWARF 5 supplementary files are not currently supported but may be in + the future. + + DWARF package files are not considered supplementary files. They are + considered part of the debug file and must have the same path as the + debug file plus a ".dwp" extension. + """ + + GNU_DEBUGALTLINK = ... + """ + GNU-style supplementary debug file referred to by a ``.gnu_debugaltlink`` + section. + + Its :attr:`~WantedSupplementaryFile.checksum` is the file's GNU build ID. + """ + class Thread: """A thread in a program.""" diff --git a/docs/advanced_usage.rst b/docs/advanced_usage.rst index 84c44eb84..c717b0b0d 100644 --- a/docs/advanced_usage.rst +++ b/docs/advanced_usage.rst @@ -6,15 +6,128 @@ Advanced Usage The :doc:`user_guide` covers basic usage of drgn, but drgn also supports more advanced use cases which are covered here. -Loading Debugging Symbols -------------------------- +.. _advanced-modules: -drgn will automatically load debugging information based on the debugged -program (e.g., from loaded kernel modules or loaded shared libraries). -:meth:`drgn.Program.load_debug_info()` can be used to load additional debugging -information:: +Modules and Debugging Symbols +----------------------------- - >>> prog.load_debug_info(['./libfoo.so', '/usr/lib/libbar.so']) +drgn tries to determine what executable, libraries, etc. a program uses and +load debugging symbols automatically. As long as :doc:`debugging symbols are +installed `, this should work out of the box on +standard setups. + +For non-standard scenarios, drgn allows overriding the defaults with different +levels of control and complexity. + +Loading Debugging Symbols From Non-Standard Locations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +drgn searches standard locations for debugging symbols. If you have debugging +symbols available in a non-standard location, you can provide it to the CLI +with the ``-s``/``--symbols`` option: + +.. code-block:: console + + $ drgn -s ./libfoo.so -s /usr/lib/libbar.so.debug + +Or with the :meth:`drgn.Program.load_debug_info()` method:: + + >>> prog.load_debug_info(["./libfoo.so", "/usr/lib/libbar.so.debug"]) + +Loading Debugging Symbols For Specific Modules +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``-s`` and ``load_debug_info()`` try the given files against all of the modules +loaded in the program based on build IDs. You can also :ref:`look up +` a specific module and try a given file for just that +module with :meth:`drgn.Module.try_file()`:: + + >>> prog.main_module().try_file("build/vmlinux") + +Loading Additional Debugging Symbols +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``-s`` and ``load_debug_info()`` ignore files that don't correspond to a loaded +module. To load debugging symbols from an arbitrary file, pass +``--extra-symbols`` to the CLI: + +.. code-block:: console + + $ drgn --extra-symbols ./my_extra_symbols.debug + +Or create a :class:`drgn.ExtraModule`:: + + >>> module = prog.extra_module("my_extra_symbols") + >>> module.try_file("./my_extra_symbols.debug") + +Listing Modules +^^^^^^^^^^^^^^^ + +By default, drgn creates a module for everything loaded in the program. You can +disable this in the CLI with ``-no-default-symbols``. + +You can find or create the loaded modules programmatically with +:meth:`drgn.Program.loaded_modules()`:: + + >>> for module, new in prog.loaded_modules(): + ... print("Created" if new else "Found", module) + +You can see all of the created modules with :meth:`drgn.Program.modules()`. + +Overriding Modules +^^^^^^^^^^^^^^^^^^ + +You can create modules with the :ref:`module factory functions +`. You can also modify various attributes of the +:class:`drgn.Module` class. + +Debug Info Finders +^^^^^^^^^^^^^^^^^^ + +A callback for automatically finding debugging symbols for a set of modules can +be registered with :meth:`drgn.Program.register_debug_info_finder()`. Here is +an example for getting debugging symbols on Fedora Linux using DNF: + +.. code-block:: python3 + + import subprocess + + import drgn + + # Install debugging symbols using the DNF debuginfo-install plugin. Note that + # this is mainly for demonstration purposes; debuginfod, which drgn supports + # out of the box, is more reliable. + def dnf_debug_info_finder(modules: list[drgn.Module]) -> None: + packages = set() + for module in modules: + if not module.wants_debug_file(): + continue + + if not module.name.startswith("/"): + continue + + proc = subprocess.run( + ["rpm", "--query", "--file", module.name], + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + text=True, + ) + if proc.returncode == 0: + packages.add(proc.stdout.rstrip("\n")) + + # Try installing their debug info. + subprocess.call( + ["sudo", "dnf", "debuginfo-install", "--skip-broken", "--"] + + sorted(packages) + ) + + # Leave the rest to the standard debug info finder. + + + prog.register_debug_info_finder("dnf", dnf_debug_info_finder, enable_index=0) + +Currently, debug info finders must be configured explicitly by the user. In the +future, there will be a plugin system for doing so automatically. Library ------- @@ -92,9 +205,9 @@ Environment Variables Some of drgn's behavior can be modified through environment variables: ``DRGN_MAX_DEBUG_INFO_ERRORS`` - The maximum number of individual errors to report in a - :exc:`drgn.MissingDebugInfoError`. Any additional errors are truncated. The - default is 5; -1 is unlimited. + The maximum number of warnings about missing debugging information to log + on CLI startup or from :meth:`drgn.Program.load_debug_info()`. Any + additional errors are truncated. The default is 5; -1 is unlimited. ``DRGN_PREFER_ORC_UNWINDER`` Whether to prefer using `ORC @@ -104,12 +217,6 @@ Some of drgn's behavior can be modified through environment variables: vice versa. This environment variable is mainly intended for testing and may be ignored in the future. -``DRGN_USE_LIBDWFL_REPORT`` - Whether drgn should use libdwfl to find debugging information for core - dumps instead of its own implementation (0 or 1). The default is 0. This - environment variable is mainly intended as an escape hatch in case of bugs - in drgn's implementation and will be ignored in the future. - ``DRGN_USE_LIBKDUMPFILE_FOR_ELF`` Whether drgn should use libkdumpfile for ELF vmcores (0 or 1). The default is 0. This functionality will be removed in the future. diff --git a/docs/api_reference.rst b/docs/api_reference.rst index c67f82a4a..eecd0138a 100644 --- a/docs/api_reference.rst +++ b/docs/api_reference.rst @@ -7,7 +7,7 @@ Programs -------- .. drgndoc:: Program - :exclude: (void|int|bool|float|struct|union|class|enum|typedef|pointer|array|function)_type + :exclude: (void|int|bool|float|struct|union|class|enum|typedef|pointer|array|function)_type|(main|shared_library|vdso|relocatable|linux_kernel_loadable|extra)_module .. drgndoc:: ProgramFlags .. drgndoc:: FindObjectFlags @@ -159,6 +159,44 @@ can be used just like types obtained from :meth:`Program.type()`. .. drgndoc:: Program.array_type .. drgndoc:: Program.function_type +Modules +------- + +.. drgndoc:: Module +.. drgndoc:: MainModule +.. drgndoc:: SharedLibraryModule +.. drgndoc:: VdsoModule +.. drgndoc:: RelocatableModule +.. drgndoc:: ExtraModule +.. drgndoc:: ModuleFileStatus +.. drgndoc:: WantedSupplementaryFile +.. drgndoc:: SupplementaryFileKind + +.. _api-module-constructors: + +Module Lookups/Constructors +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +For each module type, there is a corresponding method to create a module of +that type or find one that was previously created:: + + >>> prog.extra_module("foo", 1234) + Traceback (most recent call last): + ... + LookupError: module not found + >>> prog.extra_module("foo", 1234, create=True) + (prog.extra_module(name='foo', id=0x4d2), True) + >>> prog.extra_module("foo", 1234) + >>> prog.extra_module("foo", 1234, create=True) + (prog.extra_module(name='foo', id=0x4d2), False) + +.. drgndoc:: Program.main_module +.. drgndoc:: Program.shared_library_module +.. drgndoc:: Program.vdso_module +.. drgndoc:: Program.relocatable_module +.. drgndoc:: Program.linux_kernel_loadable_module +.. drgndoc:: Program.extra_module + Miscellaneous ------------- diff --git a/docs/user_guide.rst b/docs/user_guide.rst index 0ebf60b4d..1040ea8c7 100644 --- a/docs/user_guide.rst +++ b/docs/user_guide.rst @@ -319,6 +319,47 @@ functions like :meth:`drgn.Program.int_type()`:: You won't usually need to work with types directly, but see :ref:`api-reference-types` if you do. +Modules +^^^^^^^ + +drgn tracks executables, shared libraries, loadable kernel modules, and other +binary files used by a program with the :class:`drgn.Module` class. Modules +store their name, identifying information, load address, and debugging symbols. + +.. code-block:: pycon + :caption: Linux kernel example + + >>> for module in prog.modules(): + ... print(module) + ... + prog.main_module(name='kernel') + prog.relocatable_module(name='rng_core', address=0xffffffffc0400000) + prog.relocatable_module(name='virtio_rng', address=0xffffffffc0402000) + prog.relocatable_module(name='binfmt_misc', address=0xffffffffc0401000) + >>> prog.main_module().debug_file_path + '/usr/lib/modules/6.13.0-rc1-vmtest34.1default/build/vmlinux' + +.. code-block:: pycon + :caption: Userspace example + + >>> for module in prog.modules(): + ... print(module) + ... + prog.main_module(name='/usr/bin/grep') + prog.shared_library_module(name='/lib64/ld-linux-x86-64.so.2', dynamic_address=0x7f51772b6e68) + prog.shared_library_module(name='/lib64/libc.so.6', dynamic_address=0x7f51771af960) + prog.shared_library_module(name='/lib64/libpcre2-8.so.0', dynamic_address=0x7f5177258c68) + prog.vdso_module(name='linux-vdso.so.1', dynamic_address=0x7f51772803e0) + >>> prog.main_module().loaded_file_path + '/usr/bin/grep' + >>> prog.main_module().debug_file_path + '/usr/lib/debug/usr/bin/grep-3.11-7.fc40.x86_64.debug' + +drgn normally initializes the appropriate modules and loads their debugging +symbols automatically. Advanced use cases can create or modify modules and load +debugging symbols manually; see the :ref:`advanced usage guide +`. + Platforms ^^^^^^^^^ diff --git a/drgn/__init__.py b/drgn/__init__.py index 5a03f5a30..981bef3e7 100644 --- a/drgn/__init__.py +++ b/drgn/__init__.py @@ -49,11 +49,15 @@ from _drgn import ( NULL, Architecture, + ExtraModule, FaultError, FindObjectFlags, IntegerLike, Language, + MainModule, MissingDebugInfoError, + Module, + ModuleFileStatus, NoDefaultProgramError, Object, ObjectAbsentError, @@ -66,8 +70,11 @@ ProgramFlags, Qualifiers, Register, + RelocatableModule, + SharedLibraryModule, StackFrame, StackTrace, + SupplementaryFileKind, Symbol, SymbolBinding, SymbolIndex, @@ -80,6 +87,8 @@ TypeMember, TypeParameter, TypeTemplateParameter, + VdsoModule, + WantedSupplementaryFile, alignof, cast, container_of, @@ -106,11 +115,15 @@ __all__ = ( "Architecture", + "ExtraModule", "FaultError", "FindObjectFlags", "IntegerLike", "Language", + "MainModule", "MissingDebugInfoError", + "Module", + "ModuleFileStatus", "NULL", "NoDefaultProgramError", "Object", @@ -124,8 +137,11 @@ "ProgramFlags", "Qualifiers", "Register", + "RelocatableModule", + "SharedLibraryModule", "StackFrame", "StackTrace", + "SupplementaryFileKind", "Symbol", "SymbolBinding", "SymbolIndex", @@ -138,6 +154,8 @@ "TypeMember", "TypeParameter", "TypeTemplateParameter", + "VdsoModule", + "WantedSupplementaryFile", "alignof", "cast", "container_of", diff --git a/drgn/cli.py b/drgn/cli.py index 8d3497588..36d6d9f22 100644 --- a/drgn/cli.py +++ b/drgn/cli.py @@ -89,19 +89,6 @@ def version_header() -> str: return f"drgn {drgn.__version__} (using Python {python_version}, elfutils {drgn._elfutils_version}, {libkdumpfile})" -class _QuietAction(argparse.Action): - def __init__( - self, option_strings: Any, dest: Any, nargs: Any = 0, **kwds: Any - ) -> None: - super().__init__(option_strings, dest, nargs=nargs, **kwds) - - def __call__( - self, parser: Any, namespace: Any, values: Any, option_string: Any = None - ) -> None: - setattr(namespace, self.dest, True) - namespace.log_level = "none" - - def _identify_script(path: str) -> str: EI_NIDENT = 16 SIZEOF_E_TYPE = 2 @@ -161,9 +148,8 @@ def _displayhook(value: Any) -> None: def _main() -> None: handler = logging.StreamHandler() - handler.setFormatter( - _LogFormatter(hasattr(sys.stderr, "fileno") and os.isatty(sys.stderr.fileno())) - ) + color = hasattr(sys.stderr, "fileno") and os.isatty(sys.stderr.fileno()) + handler.setFormatter(_LogFormatter(color)) logging.getLogger().addHandler(handler) version = version_header() @@ -193,7 +179,9 @@ def _main() -> None: metavar="PATH", type=str, action="append", - help="load additional debugging symbols from the given file; this option may be given more than once", + help="load debugging symbols from the given file. " + "If the file does not correspond to a loaded executable, library, or module, " + "then it is ignored. This option may be given more than once", ) default_symbols_group = symbol_group.add_mutually_exclusive_group() default_symbols_group.add_argument( @@ -201,15 +189,25 @@ def _main() -> None: dest="default_symbols", action="store_const", const={"main": True}, - help="only load debugging symbols for the main executable and those added with -s; " - "for userspace programs, this is currently equivalent to --no-default-symbols", + help="only load debugging symbols for the main executable " + "and those added with -s or --extra-symbols", ) default_symbols_group.add_argument( "--no-default-symbols", dest="default_symbols", action="store_const", const={}, - help="don't load any debugging symbols that were not explicitly added with -s", + help="don't load any debugging symbols that were not explicitly added " + "with -s or --extra-symbols", + ) + symbol_group.add_argument( + "--extra-symbols", + metavar="PATH", + type=str, + action="append", + help="load additional debugging symbols from the given file, " + "which is assumed not to correspond to a loaded executable, library, or module. " + "This option may be given more than once", ) advanced_group = parser.add_argument_group("advanced") @@ -235,7 +233,9 @@ def _main() -> None: parser.add_argument( "-q", "--quiet", - action=_QuietAction, + dest="log_level", + action="store_const", + const="none", help="don't print any logs or download progress", ) parser.add_argument( @@ -268,8 +268,6 @@ def _main() -> None: else: print(version, file=sys.stderr, flush=True) - if not args.quiet: - os.environ["DEBUGINFOD_PROGRESS"] = "1" if args.log_level == "none": logger.setLevel(logging.CRITICAL + 1) else: @@ -316,7 +314,14 @@ def _main() -> None: try: prog.load_debug_info(args.symbols, **args.default_symbols) except drgn.MissingDebugInfoError as e: - logger.warning("%s", e) + logger.warning("\033[1m%s\033[m" if color else "%s", e) + + if args.extra_symbols: + for extra_symbol_path in args.extra_symbols: + extra_symbol_path = os.path.abspath(extra_symbol_path) + module, new = prog.extra_module(extra_symbol_path, create=True) + if new: + module.try_file(extra_symbol_path) if args.script: sys.argv = args.script diff --git a/libdrgn/Makefile.am b/libdrgn/Makefile.am index e2899fd09..6414d95ab 100644 --- a/libdrgn/Makefile.am +++ b/libdrgn/Makefile.am @@ -66,6 +66,8 @@ libdrgnimpl_la_SOURCES = $(ARCH_DEFS_PYS:_defs.py=.c) \ elf_file.h \ elf_notes.c \ elf_notes.h \ + elf_symtab.c \ + elf_symtab.h \ elf_sections.h \ error.c \ error.h \ @@ -183,6 +185,8 @@ _drgn_la_SOURCES = python/constants.c \ python/helpers.c \ python/language.c \ python/main.c \ + python/module.c \ + python/module_section_addresses.c \ python/object.c \ python/platform.c \ python/program.c \ diff --git a/libdrgn/build-aux/gen_constants.py b/libdrgn/build-aux/gen_constants.py index 8e2180b1d..c6999292b 100644 --- a/libdrgn/build-aux/gen_constants.py +++ b/libdrgn/build-aux/gen_constants.py @@ -16,6 +16,7 @@ class ConstantClass(NamedTuple): CONSTANTS = ( ConstantClass("Architecture", "Enum", r"DRGN_ARCH_([a-zA-Z0-9_]+)"), ConstantClass("FindObjectFlags", "Flag", r"DRGN_FIND_OBJECT_([a-zA-Z0-9_]+)"), + ConstantClass("ModuleFileStatus", "Enum", r"DRGN_MODULE_FILE_([a-zA-Z0-9_]+)"), ConstantClass( "PlatformFlags", "Flag", @@ -28,6 +29,11 @@ class ConstantClass(NamedTuple): ConstantClass( "Qualifiers", "Flag", r"DRGN_QUALIFIER_([a-zA-Z0-9_]+)", [("NONE", "0")] ), + ConstantClass( + "SupplementaryFileKind", + "Enum", + r"DRGN_SUPPLEMENTARY_FILE_([a-z-A-Z0-9_]+)(? None: out_file.write(f"\t{section_enumerator_name(section_name)},\n") out_file.write( """\ - /** Indices less than this are cached when the module is loaded. */ - DRGN_SECTION_INDEX_NUM_PRECACHE, + /** Indices less than this are used by the DWARF index. */ + DRGN_SECTION_INDEX_NUM_DWARF_INDEX, """ ) for i, section_name in enumerate(CACHED_SECTIONS): if i == 0: out_file.write( - f"\t{section_enumerator_name(section_name)} = DRGN_SECTION_INDEX_NUM_PRECACHE,\n" + f"\t{section_enumerator_name(section_name)} = DRGN_SECTION_INDEX_NUM_DWARF_INDEX,\n" ) else: out_file.write(f"\t{section_enumerator_name(section_name)},\n") diff --git a/libdrgn/cleanup.h b/libdrgn/cleanup.h index 9ca90b4ab..9b71fb3d1 100644 --- a/libdrgn/cleanup.h +++ b/libdrgn/cleanup.h @@ -10,8 +10,10 @@ #ifndef DRGN_CLEANUP_H #define DRGN_CLEANUP_H +#include #include #include +#include #include #define _cleanup_(x) __attribute__((__cleanup__(x))) @@ -39,6 +41,14 @@ static inline void closep(int *fd) close(*fd); } +/** Call @c closedir() when the variable goes out of scope. */ +#define _cleanup_closedir_ _cleanup_(closedirp) +static inline void closedirp(DIR **dirp) +{ + if (*dirp) + closedir(*dirp); +} + /** * Get the value of a pointer variable and reset it to @c NULL. * diff --git a/libdrgn/debug_info.c b/libdrgn/debug_info.c index 7fee6286a..70b49cd4e 100644 --- a/libdrgn/debug_info.c +++ b/libdrgn/debug_info.c @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -10,24 +11,70 @@ #include #include #include +#include #include #include #include #include +#include +#include +#include +#include #include +#include +#include "array.h" #include "binary_buffer.h" +#include "binary_search.h" #include "cleanup.h" +#include "crc32.h" #include "debug_info.h" #include "elf_file.h" #include "elf_notes.h" #include "error.h" +#include "hexlify.h" +#include "io.h" #include "linux_kernel.h" +#include "log.h" #include "openmp.h" #include "platform.h" +#include "pp.h" #include "program.h" +#include "serialize.h" #include "util.h" +#define _cleanup_elf_end_ _cleanup_(elf_endp) +static inline void elf_endp(Elf **elfp) +{ + elf_end(*elfp); +} + +#if !_ELFUTILS_PREREQ(0, 175) +// If we don't have dwelf_elf_begin(), this is equivalent except that it doesn't +// handle compressed files. +static inline Elf *dwelf_elf_begin(int fd) +{ + return elf_begin(fd, ELF_C_READ_MMAP_PRIVATE, NULL); +} +#endif + +DEFINE_HASH_MAP_FUNCTIONS(drgn_module_section_address_map, + c_string_key_hash_pair, c_string_key_eq); + +// This is currently always DRGN_SUPPLEMENTARY_FILE_GNU_DEBUGALTLINK. +struct drgn_module_wanted_supplementary_file { + struct drgn_elf_file *file; + // supplementary_path and checksum are owned by file. + const char *supplementary_path; + const void *checksum; + size_t checksum_len; + // checksum_str is a separate allocation. + char *checksum_str; + // Used to detect when the wanted supplementary file has changed in + // order to avoid redundant attempts. + uint64_t generation; +}; + #if WITH_DEBUGINFOD #if _ELFUTILS_PREREQ(0, 179) #define DRGN_DEBUGINFOD_0_179_FUNCTIONS \ @@ -79,8 +126,7 @@ static inline bool drgn_have_debuginfod(void) } #else // GCC and Clang optimize out the function pointer. -#define X(name) __attribute__((__unused__)) \ - static const typeof(&name) drgn_##name = name; +#define X(name) static const typeof(&name) drgn_##name = name; DRGN_DEBUGINFOD_FUNCTIONS #undef X @@ -96,2107 +142,5218 @@ static inline bool drgn_have_debuginfod(void) static inline Dwarf *drgn_elf_file_dwarf_key(struct drgn_elf_file * const *entry) { - return (*entry)->dwarf; + return (*entry)->_dwarf; } DEFINE_HASH_TABLE_FUNCTIONS(drgn_elf_file_dwarf_table, drgn_elf_file_dwarf_key, ptr_key_hash_pair, scalar_key_eq); -DEFINE_VECTOR_FUNCTIONS(drgn_module_vector); - -struct drgn_module_key { - const void *build_id; - size_t build_id_len; - uint64_t start, end; -}; +DEFINE_VECTOR(drgn_module_vector, struct drgn_module *); -static inline struct drgn_module_key -drgn_module_key(struct drgn_module * const *entry) +static inline +struct drgn_module_key drgn_module_entry_key(struct drgn_module * const *entry) { - return (struct drgn_module_key){ - .build_id = (*entry)->build_id, - .build_id_len = (*entry)->build_id_len, - .start = (*entry)->start, - .end = (*entry)->end, - }; + struct drgn_module_key key; + key.kind = (*entry)->kind; + SWITCH_ENUM(key.kind) { + case DRGN_MODULE_SHARED_LIBRARY: + key.shared_library.name = (*entry)->name; + key.shared_library.dynamic_address = + (*entry)->shared_library.dynamic_address; + break; + case DRGN_MODULE_VDSO: + key.vdso.name = (*entry)->name; + key.vdso.dynamic_address = (*entry)->vdso.dynamic_address; + break; + case DRGN_MODULE_RELOCATABLE: + key.relocatable.name = (*entry)->name; + key.relocatable.address = (*entry)->relocatable.address; + break; + case DRGN_MODULE_EXTRA: + key.extra.name = (*entry)->name; + key.extra.id = (*entry)->extra.id; + break; + case DRGN_MODULE_MAIN: + default: + UNREACHABLE(); + } + return key; } static inline struct hash_pair drgn_module_key_hash_pair(const struct drgn_module_key *key) { - size_t hash = hash_bytes(key->build_id, key->build_id_len); - hash = hash_combine(hash, key->start); - hash = hash_combine(hash, key->end); + size_t hash = key->kind; + SWITCH_ENUM(key->kind) { + case DRGN_MODULE_SHARED_LIBRARY: + hash = hash_combine(hash, + hash_c_string(key->shared_library.name)); + hash = hash_combine(hash, key->shared_library.dynamic_address); + break; + case DRGN_MODULE_VDSO: + hash = hash_combine(hash, hash_c_string(key->vdso.name)); + hash = hash_combine(hash, key->vdso.dynamic_address); + break; + case DRGN_MODULE_RELOCATABLE: + hash = hash_combine(hash, hash_c_string(key->relocatable.name)); + hash = hash_combine(hash, key->relocatable.address); + break; + case DRGN_MODULE_EXTRA: + hash = hash_combine(hash, hash_c_string(key->extra.name)); + hash = hash_combine(hash, key->extra.id); + break; + case DRGN_MODULE_MAIN: + default: + UNREACHABLE(); + } return hash_pair_from_avalanching_hash(hash); } + static inline bool drgn_module_key_eq(const struct drgn_module_key *a, const struct drgn_module_key *b) { - return (a->build_id_len == b->build_id_len && - memcmp(a->build_id, b->build_id, a->build_id_len) == 0 && - a->start == b->start && a->end == b->end); -} -DEFINE_HASH_TABLE_FUNCTIONS(drgn_module_table, drgn_module_key, - drgn_module_key_hash_pair, drgn_module_key_eq); - -DEFINE_HASH_SET_FUNCTIONS(c_string_set, c_string_key_hash_pair, - c_string_key_eq); - -/** - * @c Dwfl_Callbacks::find_elf() implementation. - * - * If the ELF file was reported directly, this returns it. Otherwise, it falls - * back to an appropriate callback. - * - * Ideally we'd use @c dwfl_report_elf() instead, but that doesn't take an @c - * Elf handle, which we need for a couple of reasons: - * - * - We usually already have the @c Elf handle open in order to identify the - * file. - * - For kernel modules, we set the section addresses in the @c Elf handle - * ourselves instead of using @c Dwfl_Callbacks::section_address(). - * - * Additionally, there's a special case for vmlinux. It is usually an @c ET_EXEC - * ELF file, but when KASLR is enabled, it needs to be handled like an @c ET_DYN - * file. libdwfl has a hack for this when @c dwfl_report_module() is used, but - * @ref dwfl_report_elf() bypasses this hack. - * - * So, we're stuck using @c dwfl_report_module() and this dummy callback. - */ -static int drgn_dwfl_find_elf(Dwfl_Module *dwfl_module, void **userdatap, - const char *name, Dwarf_Addr base, - char **file_name, Elf **elfp) -{ - struct drgn_module *module = *userdatap; - if (module->elf) { - *file_name = module->path; - int fd = module->fd; - *elfp = module->elf; - // libdwfl consumes the returned path, file descriptor, and ELF - // handle, so clear the fields. - module->path = NULL; - module->fd = -1; - module->elf = NULL; - return fd; - } - if (module->prog->flags & DRGN_PROGRAM_IS_LINUX_KERNEL) { - *elfp = NULL; - return -1; - } else if (module->prog->flags & DRGN_PROGRAM_IS_LIVE) { - return dwfl_linux_proc_find_elf(dwfl_module, userdatap, name, - base, file_name, elfp); - } else { - return dwfl_build_id_find_elf(dwfl_module, userdatap, name, - base, file_name, elfp); + if (a->kind != b->kind) + return false; + SWITCH_ENUM(a->kind) { + case DRGN_MODULE_SHARED_LIBRARY: + return (strcmp(a->shared_library.name, + b->shared_library.name) == 0 + && a->shared_library.dynamic_address + == b->shared_library.dynamic_address); + break; + case DRGN_MODULE_VDSO: + return (strcmp(a->vdso.name, b->vdso.name) == 0 + && a->vdso.dynamic_address == b->vdso.dynamic_address); + break; + case DRGN_MODULE_RELOCATABLE: + return (strcmp(a->relocatable.name, b->relocatable.name) == 0 + && a->relocatable.address == b->relocatable.address); + break; + case DRGN_MODULE_EXTRA: + return (strcmp(a->extra.name, b->extra.name) == 0 + && a->extra.id == b->extra.id); + break; + case DRGN_MODULE_MAIN: + default: + UNREACHABLE(); } } -/** - * @c Dwfl_Callbacks::section_address() implementation. - * - * We set the section header @c sh_addr in memory instead of using this, but - * libdwfl requires the callback pointer to be non-@c NULL. It will be called - * for any sections that still have a zero @c sh_addr, meaning they are not - * present in memory. - */ -static int drgn_dwfl_section_address(Dwfl_Module *module, void **userdatap, - const char *name, Dwarf_Addr base, - const char *secname, Elf32_Word shndx, - const GElf_Shdr *shdr, Dwarf_Addr *addr) +DEFINE_HASH_TABLE_FUNCTIONS(drgn_module_table, drgn_module_entry_key, + drgn_module_key_hash_pair, drgn_module_key_eq); + +static inline uint64_t drgn_module_address_key(const struct drgn_module *entry) { - *addr = -1; - return DWARF_CB_OK; + return entry->start; } -static const Dwfl_Callbacks drgn_dwfl_callbacks = { - .find_elf = drgn_dwfl_find_elf, - .find_debuginfo = dwfl_standard_find_debuginfo, - .section_address = drgn_dwfl_section_address, -}; +DEFINE_BINARY_SEARCH_TREE_FUNCTIONS(drgn_module_address_tree, node, + drgn_module_address_key, + binary_search_tree_scalar_cmp, splay); -static void drgn_module_destroy(struct drgn_module *module) +static void drgn_module_free_section_addresses(struct drgn_module *module) { - if (module) { - drgn_error_destroy(module->err); - drgn_module_orc_info_deinit(module); - drgn_module_dwarf_info_deinit(module); - elf_end(module->elf); - if (module->fd != -1) - close(module->fd); - free(module->path); - for (struct drgn_elf_file_dwarf_table_iterator it = - drgn_elf_file_dwarf_table_first(&module->split_dwarf_files); - it.entry; - it = drgn_elf_file_dwarf_table_next(it)) - drgn_elf_file_destroy(*it.entry); - drgn_elf_file_dwarf_table_deinit(&module->split_dwarf_files); - if (module->debug_file != module->loaded_file) - drgn_elf_file_destroy(module->debug_file); - drgn_elf_file_destroy(module->loaded_file); - free(module->name); - free(module); - } -} - -static void drgn_module_finish_indexing(struct drgn_debug_info *dbinfo, - struct drgn_module *module) -{ - module->state = DRGN_DEBUG_INFO_MODULE_INDEXED; - if (module->name) { - int ret = c_string_set_insert(&dbinfo->module_names, - (const char **)&module->name, - NULL); - /* drgn_debug_info_update_index() should've reserved enough. */ - assert(ret != -1); - } -} - -/* - * Wrapper around dwfl_report_end() that works around a libdwfl bug which causes - * it to close stdin when it frees some modules that were reported by - * dwfl_core_file_report(). This was fixed in elfutils 0.177 by commit - * d37f6ea7e3e5 ("libdwfl: Fix fd leak/closing wrong fd after - * dwfl_core_file_report()"), but we support older versions. - */ -static int my_dwfl_report_end(struct drgn_debug_info *dbinfo, - int (*removed)(Dwfl_Module *, void *, - const char *, Dwarf_Addr, void *), - void *arg) -{ - int fd = -1; - if ((dbinfo->prog->flags - & (DRGN_PROGRAM_IS_LINUX_KERNEL | DRGN_PROGRAM_IS_LIVE)) == 0) - fd = dup(0); - int ret = dwfl_report_end(dbinfo->dwfl, removed, arg); - if (fd != -1) { - dup2(fd, 0); - close(fd); - } - return ret; -} - -struct drgn_dwfl_module_removed_arg { - struct drgn_debug_info *dbinfo; - bool finish_indexing; - bool free_all; -}; - -static int drgn_dwfl_module_removed(Dwfl_Module *dwfl_module, void *userdatap, - const char *name, Dwarf_Addr base, - void *_arg) -{ - struct drgn_dwfl_module_removed_arg *arg = _arg; - /* - * userdatap is actually a void ** like for the other libdwfl callbacks, - * but dwfl_report_end() has the wrong signature for the removed - * callback. - */ - struct drgn_module *module = *(void **)userdatap; - if (arg->finish_indexing && module && - module->state == DRGN_DEBUG_INFO_MODULE_INDEXING) - drgn_module_finish_indexing(arg->dbinfo, module); - if (arg->free_all || !module || - module->state != DRGN_DEBUG_INFO_MODULE_INDEXED) { - drgn_module_destroy(module); - } else { - /* - * The module was already indexed. Report it again so libdwfl - * doesn't remove it. - */ - Dwarf_Addr end; - dwfl_module_info(dwfl_module, NULL, NULL, &end, NULL, NULL, - NULL, NULL); - dwfl_report_module(arg->dbinfo->dwfl, name, base, end); - } - return DWARF_CB_OK; + for (auto it = + drgn_module_section_address_map_first(&module->section_addresses); + it.entry; + it = drgn_module_section_address_map_next(it)) + free(it.entry->key); } -static void drgn_debug_info_free_modules(struct drgn_debug_info *dbinfo, - bool finish_indexing, bool free_all) +LIBDRGN_PUBLIC +struct drgn_module *drgn_module_find(struct drgn_program *prog, + const struct drgn_module_key *key) { - for (struct drgn_module_table_iterator it = - drgn_module_table_first(&dbinfo->modules); it.entry; ) { - struct drgn_module *module = *it.entry; - struct drgn_module **nextp = it.entry; - do { - struct drgn_module *next = module->next; - if (finish_indexing && - module->state == DRGN_DEBUG_INFO_MODULE_INDEXING) - drgn_module_finish_indexing(dbinfo, module); - if (free_all || - module->state != DRGN_DEBUG_INFO_MODULE_INDEXED) { - if (module == *nextp) { - if (nextp == it.entry && !next) { - it = drgn_module_table_delete_iterator(&dbinfo->modules, - it); - } else { - if (!next) - it = drgn_module_table_next(it); - *nextp = next; - } - } - void **userdatap; - dwfl_module_info(module->dwfl_module, - &userdatap, NULL, NULL, NULL, - NULL, NULL, NULL); - *userdatap = NULL; - drgn_module_destroy(module); - } else { - if (!next) - it = drgn_module_table_next(it); - nextp = &module->next; - } - module = next; - } while (module); + if (key->kind == DRGN_MODULE_MAIN) { + return prog->dbinfo.main_module; + } else { + struct drgn_module_table_iterator it = + drgn_module_table_search(&prog->dbinfo.modules, key); + return it.entry ? *it.entry : NULL; } - - dwfl_report_begin(dbinfo->dwfl); - struct drgn_dwfl_module_removed_arg arg = { - .dbinfo = dbinfo, - .finish_indexing = finish_indexing, - .free_all = free_all, - }; - my_dwfl_report_end(dbinfo, drgn_dwfl_module_removed, &arg); } -struct drgn_error * -drgn_debug_info_report_error(struct drgn_debug_info_load_state *load, - const char *name, const char *message, - struct drgn_error *err) -{ - if (err && err->code == DRGN_ERROR_NO_MEMORY) { - /* Always fail hard if we're out of memory. */ - goto err; - } - if (load->num_errors == 0 && - !string_builder_append(&load->errors, - "missing some debugging symbols (see https://drgn.readthedocs.io/en/latest/getting_debugging_symbols.html):")) - goto err; - if (load->num_errors < load->max_errors) { - if (!string_builder_line_break(&load->errors)) - goto err; - if (!string_builder_append(&load->errors, " ")) - goto err; - if (name && !string_builder_append(&load->errors, name)) - goto err; - if (name && (message || err) && - !string_builder_append(&load->errors, " (")) - goto err; - if (message && !string_builder_append(&load->errors, message)) - goto err; - if (message && err && - !string_builder_append(&load->errors, ": ")) - goto err; - if (err && !string_builder_append_error(&load->errors, err)) - goto err; - if (name && (message || err) && - !string_builder_appendc(&load->errors, ')')) - goto err; - } - load->num_errors++; - drgn_error_destroy(err); - return NULL; - -err: - drgn_error_destroy(err); - return &drgn_enomem; +LIBDRGN_PUBLIC +struct drgn_module *drgn_module_find_by_address(struct drgn_program *prog, + uint64_t address) +{ + struct drgn_module_address_tree_iterator it = + drgn_module_address_tree_search_le(&prog->dbinfo.modules_by_address, + &address); + if (!it.entry || address >= it.entry->end) + return NULL; + return it.entry; } -static struct drgn_error * -drgn_debug_info_report_module(struct drgn_debug_info_load_state *load, - const void *build_id, size_t build_id_len, - uint64_t start, uint64_t end, const char *name, - Dwfl_Module *dwfl_module, const char *path, - int fd, Elf *elf, bool *new_ret) +struct drgn_error *drgn_module_find_or_create(struct drgn_program *prog, + const struct drgn_module_key *key, + const char *name, + struct drgn_module **ret, + bool *new_ret) { - struct drgn_debug_info *dbinfo = load->dbinfo; struct drgn_error *err; - char *path_key = NULL; - - if (new_ret) - *new_ret = false; struct hash_pair hp; - // Silence -Wmaybe-uninitialized false positive last seen with GCC 12 on - // i386 and Arm. - struct drgn_module_table_iterator it = {}; - if (build_id_len) { - struct drgn_module_key key = { - .build_id = build_id, - .build_id_len = build_id_len, - .start = start, - .end = end, - }; - hp = drgn_module_table_hash(&key); - it = drgn_module_table_search_hashed(&dbinfo->modules, &key, - hp); - if (it.entry && - (*it.entry)->state == DRGN_DEBUG_INFO_MODULE_INDEXED) { - /* We've already indexed this module. */ - err = NULL; - goto free; - } - } - - if (!dwfl_module) { - path_key = realpath(path, NULL); - if (!path_key) { - path_key = strdup(path); - if (!path_key) { - err = &drgn_enomem; - goto free; + if (key->kind == DRGN_MODULE_MAIN) { + if (prog->dbinfo.main_module) { + if (strcmp(prog->dbinfo.main_module->name, name) != 0) { + return drgn_error_create(DRGN_ERROR_LOOKUP, + "main module already exists with different name"); } + *ret = prog->dbinfo.main_module; + if (new_ret) + *new_ret = false; + return NULL; } - - dwfl_module = dwfl_report_module(dbinfo->dwfl, path_key, start, - end); - if (!dwfl_module) { - err = drgn_error_libdwfl(); - goto free; + } else { + hp = drgn_module_table_hash(key); + struct drgn_module_table_iterator it = + drgn_module_table_search_hashed(&prog->dbinfo.modules, + key, hp); + if (it.entry) { + *ret = *it.entry; + if (new_ret) + *new_ret = false; + return NULL; } } - void **userdatap; - dwfl_module_info(dwfl_module, &userdatap, NULL, NULL, NULL, NULL, NULL, - NULL); - if (*userdatap) { - /* We've already reported this file at this offset. */ - err = NULL; - goto free; + struct drgn_module *module = calloc(1, sizeof(*module)); + if (!module) + return &drgn_enomem; + module->start = module->end = UINT64_MAX; + + module->prog = prog; + module->kind = key->kind; + // Linux userspace core dumps usually filter out file-backed mappings + // (see coredump_filter in core(5)), so we need the loaded file to read + // the text. Additionally, .eh_frame is in the loaded file and not the + // debug file. + // + // Linux kernel core dumps preserve the main kernel and kernel module + // text, and the kernel doesn't use .eh_frame, so we don't need the + // loaded file for the kernel. + module->loaded_file_status = DRGN_MODULE_FILE_WANT; + module->debug_file_status = DRGN_MODULE_FILE_WANT; + SWITCH_ENUM(key->kind) { + case DRGN_MODULE_MAIN: + if (prog->flags & DRGN_PROGRAM_IS_LINUX_KERNEL) + module->loaded_file_status = DRGN_MODULE_FILE_DONT_NEED; + break; + case DRGN_MODULE_SHARED_LIBRARY: + module->shared_library.dynamic_address = + key->shared_library.dynamic_address; + break; + case DRGN_MODULE_VDSO: + module->vdso.dynamic_address = key->vdso.dynamic_address; + break; + case DRGN_MODULE_RELOCATABLE: + module->relocatable.address = key->relocatable.address; + if (prog->flags & DRGN_PROGRAM_IS_LINUX_KERNEL) + module->loaded_file_status = DRGN_MODULE_FILE_DONT_NEED; + break; + case DRGN_MODULE_EXTRA: + module->extra.id = key->extra.id; + break; + default: + UNREACHABLE(); } - if (new_ret) - *new_ret = true; - struct drgn_module *module = calloc(1, sizeof(*module)); - if (!module) { + module->name = strdup(name); + if (!module->name) { err = &drgn_enomem; - goto free; + goto err_module; } - module->prog = load->dbinfo->prog; - module->state = DRGN_DEBUG_INFO_MODULE_NEW; - module->build_id = build_id; - module->build_id_len = build_id_len; - module->start = start; - module->end = end; - if (name) { - module->name = strdup(name); - if (!module->name) { + + if (key->kind == DRGN_MODULE_MAIN) { + prog->dbinfo.main_module = module; + } else { + if (drgn_module_table_insert_searched(&prog->dbinfo.modules, + &module, hp, NULL) < 0) { err = &drgn_enomem; - free(module); - goto free; + goto err_name; } + prog->dbinfo.modules_generation++; } - module->dwfl_module = dwfl_module; - module->path = path_key; - module->fd = fd; - module->elf = elf; - drgn_elf_file_dwarf_table_init(&module->split_dwarf_files); - - /* path_key, fd and elf are owned by the module now. */ - if (!drgn_module_vector_append(&load->new_modules, &module)) { - drgn_module_destroy(module); - return &drgn_enomem; - } - if (build_id_len) { - if (it.entry) { - /* - * The first module with this build ID is in - * new_modules, so insert it after in the list, not - * before. - */ - module->next = (*it.entry)->next; - (*it.entry)->next = module; - } else if (drgn_module_table_insert_searched(&dbinfo->modules, - &module, hp, - NULL) < 0) { - drgn_module_vector_pop(&load->new_modules); - drgn_module_destroy(module); - return &drgn_enomem; - } + drgn_elf_file_dwarf_table_init(&module->split_dwarf_files); + drgn_module_section_address_map_init(&module->section_addresses); + + SWITCH_ENUM(module->kind) { + case DRGN_MODULE_MAIN: + drgn_log_debug(prog, "created main module %s", module->name); + break; + case DRGN_MODULE_SHARED_LIBRARY: + drgn_log_debug(prog, + "created shared library module %s@0x%" PRIx64, + module->name, + module->shared_library.dynamic_address); + break; + case DRGN_MODULE_VDSO: + drgn_log_debug(prog, + "created vDSO module %s@0x%" PRIx64, + module->name, module->vdso.dynamic_address); + break; + case DRGN_MODULE_RELOCATABLE: + drgn_log_debug(prog, + "created relocatable module %s@0x%" PRIx64, + module->name, module->relocatable.address); + break; + case DRGN_MODULE_EXTRA: + drgn_log_debug(prog, + "created extra module %s 0x%" PRIx64, + module->name, module->extra.id); + break; + default: + UNREACHABLE(); } - *userdatap = module; + + *ret = module; + if (new_ret) + *new_ret = true; return NULL; -free: - elf_end(elf); - if (fd != -1) - close(fd); - free(path_key); +err_name: + free(module->name); +err_module: + free(module); return err; } -struct drgn_error * -drgn_debug_info_report_elf(struct drgn_debug_info_load_state *load, - const char *path, int fd, Elf *elf, uint64_t start, - uint64_t end, const char *name, bool *new_ret) +LIBDRGN_PUBLIC +struct drgn_error *drgn_module_find_or_create_main(struct drgn_program *prog, + const char *name, + struct drgn_module **ret, + bool *new_ret) { + struct drgn_module_key key = { .kind = DRGN_MODULE_MAIN }; + return drgn_module_find_or_create(prog, &key, name, ret, new_ret); +} - struct drgn_error *err; - const void *build_id; - ssize_t build_id_len = drgn_elf_gnu_build_id(elf, &build_id); - if (build_id_len < 0) { - err = drgn_debug_info_report_error(load, path, NULL, - drgn_error_libelf()); - elf_end(elf); - close(fd); - return err; - } else if (build_id_len == 0) { - build_id = NULL; - } - return drgn_debug_info_report_module(load, build_id, build_id_len, - start, end, name, NULL, path, fd, - elf, new_ret); +LIBDRGN_PUBLIC struct drgn_error * +drgn_module_find_or_create_shared_library(struct drgn_program *prog, + const char *name, + uint64_t dynamic_address, + struct drgn_module **ret, + bool *new_ret) +{ + const struct drgn_module_key key = { + .kind = DRGN_MODULE_SHARED_LIBRARY, + .shared_library.name = name, + .shared_library.dynamic_address = dynamic_address, + }; + return drgn_module_find_or_create(prog, &key, name, ret, new_ret); } -static int drgn_debug_info_report_dwfl_module(Dwfl_Module *dwfl_module, - void **userdatap, - const char *name, Dwarf_Addr base, - void *arg) +LIBDRGN_PUBLIC +struct drgn_error *drgn_module_find_or_create_vdso(struct drgn_program *prog, + const char *name, + uint64_t dynamic_address, + struct drgn_module **ret, + bool *new_ret) { - struct drgn_debug_info_load_state *load = arg; - struct drgn_error *err; + const struct drgn_module_key key = { + .kind = DRGN_MODULE_VDSO, + .vdso.name = name, + .vdso.dynamic_address = dynamic_address, + }; + return drgn_module_find_or_create(prog, &key, name, ret, new_ret); +} - if (*userdatap) { - /* - * This was either reported from drgn_debug_info_report_elf() or - * already indexed. - */ - return DWARF_CB_OK; - } - - const unsigned char *build_id; - GElf_Addr build_id_vaddr; - int build_id_len = dwfl_module_build_id(dwfl_module, &build_id, - &build_id_vaddr); - if (build_id_len < 0) { - err = drgn_debug_info_report_error(load, name, NULL, - drgn_error_libdwfl()); - if (err) - goto err; - } else if (build_id_len == 0) { - build_id = NULL; - } - Dwarf_Addr end; - dwfl_module_info(dwfl_module, NULL, NULL, &end, NULL, NULL, NULL, NULL); - err = drgn_debug_info_report_module(load, build_id, build_id_len, base, - end, NULL, dwfl_module, name, -1, - NULL, NULL); - if (err) - goto err; - return DWARF_CB_OK; +LIBDRGN_PUBLIC struct drgn_error * +drgn_module_find_or_create_relocatable(struct drgn_program *prog, + const char *name, uint64_t address, + struct drgn_module **ret, bool *new_ret) +{ + const struct drgn_module_key key = { + .kind = DRGN_MODULE_RELOCATABLE, + .relocatable.name = name, + .relocatable.address = address, + }; + return drgn_module_find_or_create(prog, &key, name, ret, new_ret); +} -err: - drgn_error_destroy(err); - return DWARF_CB_ABORT; +LIBDRGN_PUBLIC +struct drgn_error *drgn_module_find_or_create_extra(struct drgn_program *prog, + const char *name, + uint64_t id, + struct drgn_module **ret, + bool *new_ret) +{ + const struct drgn_module_key key = { + .kind = DRGN_MODULE_EXTRA, + .extra.name = name, + .extra.id = id, + }; + return drgn_module_find_or_create(prog, &key, name, ret, new_ret); } -static struct drgn_error *drgn_get_nt_file(Elf *elf, const char **ret, - size_t *len_ret) +static void +drgn_module_clear_wanted_supplementary_debug_file(struct drgn_module *module) { - size_t phnum; - if (elf_getphdrnum(elf, &phnum) != 0) - return drgn_error_libelf(); - for (size_t i = 0; i < phnum; i++) { - GElf_Phdr phdr_mem, *phdr = gelf_getphdr(elf, i, &phdr_mem); - if (!phdr) - return drgn_error_libelf(); - if (phdr->p_type == PT_NOTE) { - Elf_Data *data = elf_getdata_rawchunk(elf, - phdr->p_offset, - phdr->p_filesz, - note_header_type(phdr->p_align)); - if (!data) - return drgn_error_libelf(); - GElf_Nhdr nhdr; - size_t offset = 0, name_offset, desc_offset; - while (offset < data->d_size && - (offset = gelf_getnote(data, offset, &nhdr, - &name_offset, - &desc_offset))) { - const char *name = - (char *)data->d_buf + name_offset; - if (nhdr.n_namesz == sizeof("CORE") && - memcmp(name, "CORE", sizeof("CORE")) == 0 && - nhdr.n_type == NT_FILE) { - *ret = (char *)data->d_buf + desc_offset; - *len_ret = nhdr.n_descsz; - return NULL; - } - } - } + struct drgn_module_wanted_supplementary_file *wanted = + module->wanted_supplementary_debug_file; + if (wanted) { + free(wanted->checksum_str); + if (wanted->file != module->loaded_file + && wanted->file != module->debug_file) + drgn_elf_file_destroy(wanted->file); + free(wanted); + module->wanted_supplementary_debug_file = NULL; } - *ret = NULL; - *len_ret = 0; - return NULL; } -struct drgn_mapped_file_segment { - uint64_t start; - uint64_t end; - uint64_t file_offset; -}; - -DEFINE_VECTOR(drgn_mapped_file_segment_vector, struct drgn_mapped_file_segment); - -DEFINE_HASH_MAP(drgn_mapped_files, const char *, - struct drgn_mapped_file_segment_vector, c_string_key_hash_pair, - c_string_key_eq); +// Note: this doesn't remove the module from the module tables. +static void drgn_module_destroy(struct drgn_module *module) +{ + drgn_module_free_section_addresses(module); + drgn_module_section_address_map_deinit(&module->section_addresses); + drgn_module_orc_info_deinit(module); + drgn_module_dwarf_info_deinit(module); + drgn_module_clear_wanted_supplementary_debug_file(module); + drgn_elf_file_destroy(module->supplementary_debug_file); + if (module->debug_file != module->loaded_file) + drgn_elf_file_destroy(module->debug_file); + drgn_elf_file_destroy(module->loaded_file); + free(module->build_id); + free(module->name); + free(module); +} -struct userspace_core_report_state { - struct drgn_mapped_files files; - void *phdr_buf; - size_t phdr_buf_capacity; - void *segment_buf; - size_t segment_buf_capacity; -}; +void drgn_module_delete(struct drgn_module *module) +{ + assert(!module->loaded_file); + assert(!module->debug_file); + if (module->start < module->end) { + drgn_module_address_tree_delete_entry(&module->prog->dbinfo.modules_by_address, + module); + } + if (module->kind == DRGN_MODULE_MAIN) { + module->prog->dbinfo.main_module = NULL; + } else { + struct drgn_module_key key = + drgn_module_entry_key((struct drgn_module * const *)&module); + drgn_module_table_delete(&module->prog->dbinfo.modules, &key); + module->prog->dbinfo.modules_generation++; + } + drgn_module_destroy(module); +} -static struct drgn_error *parse_nt_file_error(struct binary_buffer *bb, - const char *pos, - const char *message) +LIBDRGN_PUBLIC +struct drgn_program *drgn_module_program(const struct drgn_module *module) { - return drgn_error_create(DRGN_ERROR_OTHER, "couldn't parse NT_FILE"); + return module->prog; } -static bool -drgn_mapped_file_segments_contiguous(const struct drgn_mapped_file_segment *segment1, - const struct drgn_mapped_file_segment *segment2) +LIBDRGN_PUBLIC +struct drgn_module_key drgn_module_key(const struct drgn_module *module) { - if (segment1->end != segment2->start) - return false; - uint64_t size = segment1->end - segment1->start; - return segment1->file_offset + size == segment2->file_offset; + if (module->kind == DRGN_MODULE_MAIN) { + struct drgn_module_key key; + key.kind = DRGN_MODULE_MAIN; + return key; + } + return drgn_module_entry_key((struct drgn_module * const *)&module); } -static struct drgn_error * -userspace_core_get_mapped_files(struct drgn_debug_info_load_state *load, - struct userspace_core_report_state *core, - const char *nt_file, size_t nt_file_len) +LIBDRGN_PUBLIC +enum drgn_module_kind drgn_module_kind(const struct drgn_module *module) { - struct drgn_error *err; + return module->kind; +} - GElf_Ehdr ehdr_mem, *ehdr = gelf_getehdr(load->dbinfo->prog->core, - &ehdr_mem); - if (!ehdr) - return drgn_error_libelf(); - bool is_64_bit = ehdr->e_ident[EI_CLASS] == ELFCLASS64; - bool little_endian = ehdr->e_ident[EI_DATA] == ELFDATA2LSB; +LIBDRGN_PUBLIC const char *drgn_module_name(const struct drgn_module *module) +{ + return module->name; +} - struct binary_buffer bb; - binary_buffer_init(&bb, nt_file, nt_file_len, little_endian, - parse_nt_file_error); +LIBDRGN_PUBLIC bool drgn_module_address_range(const struct drgn_module *module, + uint64_t *start_ret, + uint64_t *end_ret) +{ + if (module->start == UINT64_MAX) + return false; + *start_ret = module->start; + *end_ret = module->end; + return true; +} - /* - * fs/binfmt_elf.c in the Linux kernel source code documents the format - * of NT_FILE as: - * - * long count -- how many files are mapped - * long page_size -- units for file_ofs - * array of [COUNT] elements of - * long start - * long end - * long file_ofs - * followed by COUNT filenames in ASCII: "FILE1" NUL "FILE2" NUL... - */ - uint64_t count, page_size; - if (is_64_bit) { - if ((err = binary_buffer_next_u64(&bb, &count))) - return err; - if (count > UINT64_MAX / 24) - return binary_buffer_error(&bb, "count is too large"); - if ((err = binary_buffer_next_u64(&bb, &page_size)) || - (err = binary_buffer_skip(&bb, count * 24))) - return err; - } else { - if ((err = binary_buffer_next_u32_into_u64(&bb, &count))) - return err; - if (count > UINT64_MAX / 12) - return binary_buffer_error(&bb, "count is too large"); - if ((err = binary_buffer_next_u32_into_u64(&bb, &page_size)) || - (err = binary_buffer_skip(&bb, count * 12))) - return err; +LIBDRGN_PUBLIC struct drgn_error * +drgn_module_set_address_range(struct drgn_module *module, uint64_t start, + uint64_t end) +{ + if (start >= end && start != 0 && end != UINT64_MAX) { + return drgn_error_create(DRGN_ERROR_INVALID_ARGUMENT, + "invalid module address range"); } - for (uint64_t i = 0; i < count; i++) { - struct drgn_mapped_file_segment segment; - if (is_64_bit) { - memcpy(&segment, nt_file + 16 + i * 24, 24); - if (bb.bswap) { - segment.start = bswap_64(segment.start); - segment.end = bswap_64(segment.end); - segment.file_offset = bswap_64(segment.file_offset); - } - } else { - struct { - uint32_t start; - uint32_t end; - uint32_t file_offset; - } segment32; - memcpy(&segment32, nt_file + 8 + i * 12, 12); - if (bb.bswap) { - segment.start = bswap_32(segment32.start); - segment.end = bswap_32(segment32.end); - segment.file_offset = bswap_32(segment32.file_offset); - } else { - segment.start = segment32.start; - segment.end = segment32.end; - segment.file_offset = segment32.file_offset; - } - } - segment.file_offset *= page_size; + if (module->start < module->end) { + drgn_module_address_tree_delete_entry(&module->prog->dbinfo.modules_by_address, + module); + } - struct drgn_mapped_files_entry entry = { - .key = bb.pos, - }; - if ((err = binary_buffer_skip_string(&bb))) - return err; - struct drgn_mapped_files_iterator it; - int r = drgn_mapped_files_insert(&core->files, &entry, &it); - if (r < 0) - return &drgn_enomem; - if (r == 1) - drgn_mapped_file_segment_vector_init(&it.entry->value); - - /* - * The Linux kernel creates separate entries for contiguous - * mappings with different memory protections even though the - * protection is not included in NT_FILE. Merge them if we can. - */ - if (!drgn_mapped_file_segment_vector_empty(&it.entry->value) - && drgn_mapped_file_segments_contiguous(drgn_mapped_file_segment_vector_last(&it.entry->value), - &segment)) - drgn_mapped_file_segment_vector_last(&it.entry->value)->end = segment.end; - else if (!drgn_mapped_file_segment_vector_append(&it.entry->value, - &segment)) - return &drgn_enomem; + module->start = start; + module->end = end; + if (start < end) { + // We don't bother checking for overlapping address ranges, + // which shouldn't happen with well-formed programs and at worst + // causes spurious failed lookups. We may need to revisit this + // if it's a problem in practice. + drgn_module_address_tree_insert(&module->prog->dbinfo.modules_by_address, + module, NULL); } return NULL; } -static bool build_id_matches(Elf *elf, const void *build_id, - size_t build_id_len) +LIBDRGN_PUBLIC +const char *drgn_module_build_id(const struct drgn_module *module, + const void **raw_ret, size_t *raw_len_ret) { - const void *elf_build_id; - ssize_t elf_build_id_len = drgn_elf_gnu_build_id(elf, &elf_build_id); - if (elf_build_id_len < 0) - return false; - return (elf_build_id_len == build_id_len && - memcmp(elf_build_id, build_id, build_id_len) == 0); + if (raw_ret) + *raw_ret = module->build_id; + if (raw_len_ret) + *raw_len_ret = module->build_id_len; + return module->build_id_str; } -static struct drgn_error * -userspace_core_elf_address_range(uint16_t e_type, size_t phnum, - struct drgn_error *(*get_phdr)(void *, size_t, GElf_Phdr *), - void *arg, - const struct drgn_mapped_file_segment *segments, - size_t num_segments, - const struct drgn_mapped_file_segment *ehdr_segment, - uint64_t *bias_ret, uint64_t *start_ret, - uint64_t *end_ret) +static void *drgn_module_alloc_build_id(size_t build_id_len) { - struct drgn_error *err; - - /* - * First, find the virtual address of the ELF header so that we can - * calculate the bias. - */ - uint64_t ehdr_vaddr; - size_t i; - for (i = 0; i < phnum; i++) { - GElf_Phdr phdr; - err = get_phdr(arg, i, &phdr); - if (err) - return err; - if (phdr.p_type == PT_LOAD) { - uint64_t align = phdr.p_align ? phdr.p_align : 1; - if ((phdr.p_offset & -align) == 0) { - ehdr_vaddr = phdr.p_vaddr & -align; - break; - } - } - } - if (i >= phnum) { - /* - * No loadable segments contain the ELF header. This can't be - * our file. - */ - *bias_ret = 0; -not_loaded: - *start_ret = *end_ret = 0; + size_t alloc_size; + if (__builtin_mul_overflow(build_id_len, 3U, &alloc_size) || + __builtin_add_overflow(alloc_size, 1U, &alloc_size)) return NULL; - } - *bias_ret = ehdr_segment->start - ehdr_vaddr; - if (*bias_ret != 0 && e_type == ET_EXEC) { - /* The executable is not loaded at the correct address. */ - goto not_loaded; - } - - /* - * Now check all of the program headers to (1) get the module address - * range and (2) make sure that they are mapped as expected. If we're - * lucky, this can detect a file that was mmap'd and not actually loaded - * by the kernel or dynamic loader. This could also be the wrong file. - */ - const struct drgn_mapped_file_segment *segment = segments; - const struct drgn_mapped_file_segment *end_segment = - segments + num_segments; - uint64_t start = 0, end = 0; - bool first = true; - for (i = 0; i < phnum; i++) { - GElf_Phdr phdr; - err = get_phdr(arg, i, &phdr); - if (err) - return err; - if (phdr.p_type != PT_LOAD) - continue; - uint64_t vaddr = phdr.p_vaddr + *bias_ret; - if (phdr.p_filesz != 0) { - /* - * Advance to the mapped segment containing the start - * address. - */ - while (vaddr >= segment->end) { - if (++segment == end_segment) - goto not_loaded; - if (vaddr < segment->start) - goto not_loaded; - } - if (segment->file_offset + (vaddr - segment->start) != - phdr.p_offset) { - /* - * The address in the core dump does not map to - * the segment's file offset. - */ - goto not_loaded; - } - if (phdr.p_filesz > segment->end - vaddr) { - /* Part of the segment is not mapped. */ - goto not_loaded; - } - } - if (first) { - uint64_t align = phdr.p_align ? phdr.p_align : 1; - start = vaddr & -align; - first = false; - } - end = vaddr + phdr.p_memsz; - } - if (start >= end) - goto not_loaded; - *start_ret = start; - *end_ret = end; - return NULL; + return malloc(alloc_size); } -/* ehdr_buf must be aligned as Elf64_Ehdr. */ -static void read_ehdr(const void *ehdr_buf, GElf_Ehdr *ret, bool *is_64_bit_ret, - bool *bswap_ret) -{ - *is_64_bit_ret = ((unsigned char *)ehdr_buf)[EI_CLASS] == ELFCLASS64; - bool little_endian = - ((unsigned char *)ehdr_buf)[EI_DATA] == ELFDATA2LSB; - *bswap_ret = little_endian != HOST_LITTLE_ENDIAN; - if (*is_64_bit_ret) { - const Elf64_Ehdr *ehdr64 = ehdr_buf; - if (*bswap_ret) { - memcpy(ret->e_ident, ehdr64->e_ident, EI_NIDENT); - ret->e_type = bswap_16(ehdr64->e_type); - ret->e_machine = bswap_16(ehdr64->e_machine); - ret->e_version = bswap_32(ehdr64->e_version); - ret->e_entry = bswap_64(ehdr64->e_entry); - ret->e_phoff = bswap_64(ehdr64->e_phoff); - ret->e_shoff = bswap_64(ehdr64->e_shoff); - ret->e_flags = bswap_32(ehdr64->e_flags); - ret->e_ehsize = bswap_16(ehdr64->e_ehsize); - ret->e_phentsize = bswap_16(ehdr64->e_phentsize); - ret->e_phnum = bswap_16(ehdr64->e_phnum); - ret->e_shentsize = bswap_16(ehdr64->e_shentsize); - ret->e_shnum = bswap_16(ehdr64->e_shnum); - ret->e_shstrndx = bswap_16(ehdr64->e_shstrndx); - } else { - *ret = *ehdr64; - } - } else { - const Elf32_Ehdr *ehdr32 = ehdr_buf; - memcpy(ret->e_ident, ehdr32->e_ident, EI_NIDENT); - if (*bswap_ret) { - ret->e_type = bswap_16(ehdr32->e_type); - ret->e_machine = bswap_16(ehdr32->e_machine); - ret->e_version = bswap_32(ehdr32->e_version); - ret->e_entry = bswap_32(ehdr32->e_entry); - ret->e_phoff = bswap_32(ehdr32->e_phoff); - ret->e_shoff = bswap_32(ehdr32->e_shoff); - ret->e_flags = bswap_32(ehdr32->e_flags); - ret->e_ehsize = bswap_16(ehdr32->e_ehsize); - ret->e_phentsize = bswap_16(ehdr32->e_phentsize); - ret->e_phnum = bswap_16(ehdr32->e_phnum); - ret->e_shentsize = bswap_16(ehdr32->e_shentsize); - ret->e_shnum = bswap_16(ehdr32->e_shnum); - ret->e_shstrndx = bswap_16(ehdr32->e_shstrndx); - } else { - ret->e_type = ehdr32->e_type; - ret->e_machine = ehdr32->e_machine; - ret->e_version = ehdr32->e_version; - ret->e_entry = ehdr32->e_entry; - ret->e_phoff = ehdr32->e_phoff; - ret->e_shoff = ehdr32->e_shoff; - ret->e_flags = ehdr32->e_flags; - ret->e_ehsize = ehdr32->e_ehsize; - ret->e_phentsize = ehdr32->e_phentsize; - ret->e_phnum = ehdr32->e_phnum; - ret->e_shentsize = ehdr32->e_shentsize; - ret->e_shnum = ehdr32->e_shnum; - ret->e_shstrndx = ehdr32->e_shstrndx; - } - } +static void drgn_module_set_build_id_impl(struct drgn_module *module, + const void *build_id, + size_t build_id_len, + void *build_id_buf) +{ + module->build_id = build_id_buf; + memcpy(module->build_id, build_id, build_id_len); + + module->build_id_len = build_id_len; + + module->build_id_str = (char *)build_id_buf + build_id_len; + hexlify(build_id, build_id_len, module->build_id_str); + module->build_id_str[2 * build_id_len] = '\0'; } -/* phdr_buf must be aligned as Elf64_Phdr. */ -static void read_phdr(const void *phdr_buf, size_t i, bool is_64_bit, - bool bswap, GElf_Phdr *ret) +LIBDRGN_PUBLIC +struct drgn_error *drgn_module_set_build_id(struct drgn_module *module, + const void *build_id, + size_t build_id_len) { - if (is_64_bit) { - const Elf64_Phdr *phdr64 = (Elf64_Phdr *)phdr_buf + i; - if (bswap) { - ret->p_type = bswap_32(phdr64->p_type); - ret->p_flags = bswap_32(phdr64->p_flags); - ret->p_offset = bswap_64(phdr64->p_offset); - ret->p_vaddr = bswap_64(phdr64->p_vaddr); - ret->p_paddr = bswap_64(phdr64->p_paddr); - ret->p_filesz = bswap_64(phdr64->p_filesz); - ret->p_memsz = bswap_64(phdr64->p_memsz); - ret->p_align = bswap_64(phdr64->p_align); - } else { - *ret = *phdr64; - } - } else { - const Elf32_Phdr *phdr32 = (Elf32_Phdr *)phdr_buf + i; - if (bswap) { - ret->p_type = bswap_32(phdr32->p_type); - ret->p_offset = bswap_32(phdr32->p_offset); - ret->p_vaddr = bswap_32(phdr32->p_vaddr); - ret->p_paddr = bswap_32(phdr32->p_paddr); - ret->p_filesz = bswap_32(phdr32->p_filesz); - ret->p_memsz = bswap_32(phdr32->p_memsz); - ret->p_flags = bswap_32(phdr32->p_flags); - ret->p_align = bswap_32(phdr32->p_align); - } else { - ret->p_type = phdr32->p_type; - ret->p_offset = phdr32->p_offset; - ret->p_vaddr = phdr32->p_vaddr; - ret->p_paddr = phdr32->p_paddr; - ret->p_filesz = phdr32->p_filesz; - ret->p_memsz = phdr32->p_memsz; - ret->p_flags = phdr32->p_flags; - ret->p_align = phdr32->p_align; - } + if (build_id_len == 0) { + free(module->build_id); + module->build_id = NULL; + module->build_id_len = 0; + module->build_id_str = NULL; + return NULL; } -} -struct core_get_phdr_arg { - const void *phdr_buf; - bool is_64_bit; - bool bswap; -}; + char *build_id_buf = drgn_module_alloc_build_id(build_id_len); + if (!build_id_buf) + return &drgn_enomem; + free(module->build_id); + drgn_module_set_build_id_impl(module, build_id, build_id_len, + build_id_buf); + return NULL; +} static struct drgn_error * -core_get_phdr(void *arg_, size_t i, GElf_Phdr *ret) +drgn_module_section_addresses_allowed(struct drgn_module *module, bool modify) { - struct core_get_phdr_arg *arg = arg_; - read_phdr(arg->phdr_buf, i, arg->is_64_bit, arg->bswap, ret); + if (module->kind != DRGN_MODULE_RELOCATABLE) { + return drgn_error_create(DRGN_ERROR_INVALID_ARGUMENT, + "section addresses are only supported for relocatable modules"); + } + if (modify && (module->loaded_file || module->debug_file)) { + return drgn_error_create(DRGN_ERROR_INVALID_ARGUMENT, + "section addresses cannot be modified after file is set"); + } return NULL; } -struct userspace_core_identified_file { - const void *build_id; - size_t build_id_len; - uint64_t start, end; - bool ignore; - bool have_address_range; -}; - -static struct drgn_error * -userspace_core_identify_file(struct drgn_program *prog, - struct userspace_core_report_state *core, - const struct drgn_mapped_file_segment *segments, - size_t num_segments, - const struct drgn_mapped_file_segment *ehdr_segment, - struct userspace_core_identified_file *ret) +LIBDRGN_PUBLIC +struct drgn_error *drgn_module_get_section_address(struct drgn_module *module, + const char *name, + uint64_t *ret) { - struct drgn_error *err; + struct drgn_error *err = + drgn_module_section_addresses_allowed(module, false); + if (err) + return err; + struct drgn_module_section_address_map_iterator it = + drgn_module_section_address_map_search(&module->section_addresses, + (char **)&name); + if (!it.entry) + return &drgn_not_found; + *ret = it.entry->value; + return NULL; +} - Elf64_Ehdr ehdr_buf; - err = drgn_program_read_memory(prog, &ehdr_buf, ehdr_segment->start, - sizeof(ehdr_buf), false); - if (err) { - if (err->code == DRGN_ERROR_FAULT) { - drgn_error_destroy(err); - err = NULL; - } +LIBDRGN_PUBLIC +struct drgn_error *drgn_module_set_section_address(struct drgn_module *module, + const char *name, + uint64_t address) +{ + struct drgn_error *err = + drgn_module_section_addresses_allowed(module, true); + if (err) return err; - } - if (memcmp(&ehdr_buf, ELFMAG, SELFMAG) != 0) { - ret->ignore = true; + + struct hash_pair hp = + drgn_module_section_address_map_hash((char **)&name); + struct drgn_module_section_address_map_iterator it = + drgn_module_section_address_map_search_hashed(&module->section_addresses, + (char **)&name, + hp); + if (it.entry) { + it.entry->value = address; return NULL; } + struct drgn_module_section_address_map_entry entry = { + .key = strdup(name), + .value = address, + }; + if (!entry.key) + return &drgn_enomem; + if (drgn_module_section_address_map_insert_searched(&module->section_addresses, + &entry, hp, + NULL) < 0) { + free(entry.key); + return &drgn_enomem; + } + module->section_addresses_generation++; + return NULL; +} + +struct drgn_error *drgn_module_delete_section_address(struct drgn_module *module, + const char *name) +{ + struct drgn_error *err = + drgn_module_section_addresses_allowed(module, true); + if (err) + return err; + + struct hash_pair hp = + drgn_module_section_address_map_hash((char **)&name); + struct drgn_module_section_address_map_iterator it = + drgn_module_section_address_map_search_hashed(&module->section_addresses, + (char **)&name, + hp); + if (!it.entry) + return &drgn_not_found; + + _cleanup_free_ _unused_ char *key_to_free = it.entry->key; + drgn_module_section_address_map_delete_iterator_hashed(&module->section_addresses, + it, hp); + module->section_addresses_generation++; + return NULL; +} + +LIBDRGN_PUBLIC +struct drgn_error *drgn_module_num_section_addresses(struct drgn_module *module, + size_t *ret) +{ + struct drgn_error *err = + drgn_module_section_addresses_allowed(module, false); + if (err) + return err; + *ret = drgn_module_section_address_map_size(&module->section_addresses); + return NULL; +} + +struct drgn_module_section_address_iterator { + struct drgn_module *module; + struct drgn_module_section_address_map_iterator map_it; + uint64_t generation; +}; + +LIBDRGN_PUBLIC struct drgn_error * +drgn_module_section_address_iterator_create(struct drgn_module *module, + struct drgn_module_section_address_iterator **ret) +{ + struct drgn_error *err = + drgn_module_section_addresses_allowed(module, false); + if (err) + return err; + + struct drgn_module_section_address_iterator *it = malloc(sizeof(*it)); + if (!it) + return &drgn_enomem; + it->module = module; + it->map_it = drgn_module_section_address_map_first(&module->section_addresses); + it->generation = module->section_addresses_generation; + *ret = it; + return NULL; +} + +LIBDRGN_PUBLIC void +drgn_module_section_address_iterator_destroy(struct drgn_module_section_address_iterator *it) +{ + free(it); +} + +LIBDRGN_PUBLIC struct drgn_module * +drgn_module_section_address_iterator_module(struct drgn_module_section_address_iterator *it) +{ + return it->module; +} + +LIBDRGN_PUBLIC struct drgn_error * +drgn_module_section_address_iterator_next(struct drgn_module_section_address_iterator *it, + const char **name_ret, + uint64_t *address_ret) +{ + if (it->map_it.entry) { + if (it->generation != it->module->section_addresses_generation) { + return drgn_error_create(DRGN_ERROR_OTHER, + "section addresses changed during iteration"); + } + *name_ret = it->map_it.entry->key; + if (address_ret) + *address_ret = it->map_it.entry->value; + it->map_it = drgn_module_section_address_map_next(it->map_it); + } else { + *name_ret = NULL; + } + return NULL; +} + +LIBDRGN_PUBLIC enum drgn_module_file_status +drgn_module_loaded_file_status(const struct drgn_module *module) +{ + return module->loaded_file_status; +} + +static bool +drgn_can_change_module_file_status(enum drgn_module_file_status old_status, + enum drgn_module_file_status new_status) +{ + SWITCH_ENUM(old_status) { + case DRGN_MODULE_FILE_WANT: + case DRGN_MODULE_FILE_DONT_WANT: + case DRGN_MODULE_FILE_DONT_NEED: + SWITCH_ENUM(new_status) { + case DRGN_MODULE_FILE_WANT: + case DRGN_MODULE_FILE_DONT_WANT: + case DRGN_MODULE_FILE_DONT_NEED: + return true; + case DRGN_MODULE_FILE_HAVE: + case DRGN_MODULE_FILE_WANT_SUPPLEMENTARY: + default: + return false; + } + case DRGN_MODULE_FILE_HAVE: + return new_status == DRGN_MODULE_FILE_HAVE; + case DRGN_MODULE_FILE_WANT_SUPPLEMENTARY: + SWITCH_ENUM(new_status) { + case DRGN_MODULE_FILE_WANT: + case DRGN_MODULE_FILE_DONT_WANT: + case DRGN_MODULE_FILE_DONT_NEED: + case DRGN_MODULE_FILE_WANT_SUPPLEMENTARY: + return true; + case DRGN_MODULE_FILE_HAVE: + default: + return false; + } + default: + UNREACHABLE(); + } +} + +LIBDRGN_PUBLIC +bool drgn_module_set_loaded_file_status(struct drgn_module *module, + enum drgn_module_file_status status) +{ + if (!drgn_can_change_module_file_status(module->loaded_file_status, + status)) + return false; + module->loaded_file_status = status; + return true; +} + +LIBDRGN_PUBLIC +bool drgn_module_wants_loaded_file(const struct drgn_module *module) +{ + SWITCH_ENUM(module->loaded_file_status) { + case DRGN_MODULE_FILE_WANT: + return true; + case DRGN_MODULE_FILE_HAVE: + case DRGN_MODULE_FILE_DONT_WANT: + case DRGN_MODULE_FILE_DONT_NEED: + return false; + case DRGN_MODULE_FILE_WANT_SUPPLEMENTARY: + default: + UNREACHABLE(); + } +} + +LIBDRGN_PUBLIC enum drgn_module_file_status +drgn_module_debug_file_status(const struct drgn_module *module) +{ + return module->debug_file_status; +} + +LIBDRGN_PUBLIC +bool drgn_module_set_debug_file_status(struct drgn_module *module, + enum drgn_module_file_status status) +{ + if (!drgn_can_change_module_file_status(module->debug_file_status, + status)) + return false; + if (module->debug_file_status == DRGN_MODULE_FILE_WANT_SUPPLEMENTARY + && status != DRGN_MODULE_FILE_WANT_SUPPLEMENTARY) + drgn_module_clear_wanted_supplementary_debug_file(module); + module->debug_file_status = status; + return true; +} + +LIBDRGN_PUBLIC +bool drgn_module_wants_debug_file(const struct drgn_module *module) +{ + SWITCH_ENUM(module->debug_file_status) { + case DRGN_MODULE_FILE_WANT: + case DRGN_MODULE_FILE_WANT_SUPPLEMENTARY: + return true; + case DRGN_MODULE_FILE_HAVE: + case DRGN_MODULE_FILE_DONT_WANT: + case DRGN_MODULE_FILE_DONT_NEED: + return false; + default: + UNREACHABLE(); + } +} + +LIBDRGN_PUBLIC +const char *drgn_module_loaded_file_path(const struct drgn_module *module) +{ + return module->loaded_file ? module->loaded_file->path : NULL; +} + +LIBDRGN_PUBLIC +uint64_t drgn_module_loaded_file_bias(const struct drgn_module *module) +{ + return module->loaded_file_bias; +} + +LIBDRGN_PUBLIC +const char *drgn_module_debug_file_path(const struct drgn_module *module) +{ + return module->debug_file ? module->debug_file->path : NULL; +} + +LIBDRGN_PUBLIC +uint64_t drgn_module_debug_file_bias(const struct drgn_module *module) +{ + return module->debug_file_bias; +} + +LIBDRGN_PUBLIC enum drgn_supplementary_file_kind +drgn_module_supplementary_debug_file_kind(const struct drgn_module *module) +{ + return module->supplementary_debug_file + ? DRGN_SUPPLEMENTARY_FILE_GNU_DEBUGALTLINK + : DRGN_SUPPLEMENTARY_FILE_NONE; +} + +LIBDRGN_PUBLIC const char * +drgn_module_supplementary_debug_file_path(const struct drgn_module *module) +{ + return module->supplementary_debug_file + ? module->supplementary_debug_file->path : NULL; +} + +LIBDRGN_PUBLIC enum drgn_supplementary_file_kind +drgn_module_wanted_supplementary_debug_file(struct drgn_module *module, + const char **debug_file_path_ret, + const char **supplementary_path_ret, + const void **checksum_ret, + size_t *checksum_len_ret) +{ + struct drgn_module_wanted_supplementary_file *wanted = + module->wanted_supplementary_debug_file; + if (debug_file_path_ret) + *debug_file_path_ret = wanted ? wanted->file->path : NULL; + if (supplementary_path_ret) + *supplementary_path_ret = wanted ? wanted->supplementary_path : NULL; + if (checksum_ret) + *checksum_ret = wanted ? wanted->checksum : NULL; + if (checksum_len_ret) + *checksum_len_ret = wanted ? wanted->checksum_len : 0; + return wanted + ? DRGN_SUPPLEMENTARY_FILE_GNU_DEBUGALTLINK + : DRGN_SUPPLEMENTARY_FILE_NONE; +} + +static struct drgn_error * +drgn_program_register_debug_info_finder_impl(struct drgn_program *prog, + struct drgn_debug_info_finder *finder, + const char *name, + const struct drgn_debug_info_finder_ops *ops, + void *arg, size_t enable_index) +{ + struct drgn_error *err; + bool should_free = !finder; + if (finder) { + finder->handler.name = name; + } else { + finder = malloc(sizeof(*finder)); + if (!finder) + return &drgn_enomem; + finder->handler.name = strdup(name); + if (!finder->handler.name) { + free(finder); + return &drgn_enomem; + } + } + finder->handler.free = should_free; + finder->ops = *ops; + finder->arg = arg; + err = drgn_handler_list_register(&prog->dbinfo.debug_info_finders, + &finder->handler, enable_index, + "module debug info finder"); + if (err && should_free) { + free((char *)finder->handler.name); + free(finder); + } + return err; +} + +LIBDRGN_PUBLIC struct drgn_error * +drgn_program_register_debug_info_finder(struct drgn_program *prog, + const char *name, + const struct drgn_debug_info_finder_ops *ops, + void *arg, size_t enable_index) +{ + return drgn_program_register_debug_info_finder_impl(prog, NULL, name, + ops, arg, + enable_index); +} + +LIBDRGN_PUBLIC struct drgn_error * +drgn_program_registered_debug_info_finders(struct drgn_program *prog, + const char ***names_ret, + size_t *count_ret) +{ + return drgn_handler_list_registered(&prog->dbinfo.debug_info_finders, + names_ret, count_ret); +} + +LIBDRGN_PUBLIC struct drgn_error * +drgn_program_set_enabled_debug_info_finders(struct drgn_program *prog, + const char * const *names, + size_t count) +{ + return drgn_handler_list_set_enabled(&prog->dbinfo.debug_info_finders, + names, count, + "module debug info finder"); +} + +LIBDRGN_PUBLIC struct drgn_error * +drgn_program_enabled_debug_info_finders(struct drgn_program *prog, + const char ***names_ret, + size_t *count_ret) +{ + return drgn_handler_list_enabled(&prog->dbinfo.debug_info_finders, + names_ret, count_ret); +} + +static const char *drgn_default_debug_info_path = ":.debug:/usr/lib/debug"; + +LIBDRGN_PUBLIC +const char *drgn_program_debug_info_path(struct drgn_program *prog) +{ + return prog->dbinfo.debug_info_path; +} + +LIBDRGN_PUBLIC +struct drgn_error *drgn_program_set_debug_info_path(struct drgn_program *prog, + const char *path) +{ + char *new_path; + if (path) { + new_path = strdup(path); + if (!new_path) + return &drgn_enomem; + } else { + new_path = NULL; + } + if (prog->dbinfo.debug_info_path != drgn_default_debug_info_path) + free((char *)prog->dbinfo.debug_info_path); + prog->dbinfo.debug_info_path = new_path; + return NULL; +} + +static struct drgn_error * +drgn_module_set_wanted_gnu_debugaltlink(struct drgn_module *module, + struct drgn_elf_file *file) +{ + struct drgn_error *err; + struct drgn_program *prog = module->prog; + + // We don't cache .gnu_debugaltlink, and it doesn't need relocation, so + // don't use drgn_elf_file_read_section(). + Elf_Data *data; + err = read_elf_section(file->scns[DRGN_SCN_GNU_DEBUGALTLINK], &data); + if (err) { + if (!drgn_error_is_fatal(err)) { + drgn_error_log_debug(prog, err, + "%s: couldn't read .gnu_debugaltlink; ignoring debug info: ", + file->path); + drgn_error_destroy(err); + err = NULL; + } + return err; + } + + const char *debugaltlink = data->d_buf; + const char *nul = memchr(debugaltlink, 0, data->d_size); + if (!nul || nul + 1 == debugaltlink + data->d_size) { + drgn_log_debug(prog, + "%s: couldn't parse .gnu_debugaltlink; ignoring debug info", + file->path); + return NULL; + } + const void *build_id = nul + 1; + size_t build_id_len = debugaltlink + data->d_size - (nul + 1); + _cleanup_free_ char *build_id_str = ahexlify(build_id, build_id_len); + if (!build_id_str) + return &drgn_enomem; + drgn_log_debug(prog, "%s has gnu_debugaltlink %s build ID %s", + file->path, debugaltlink, build_id_str); + + struct drgn_module_wanted_supplementary_file *wanted = + malloc(sizeof(*wanted)); + if (!wanted) + return &drgn_enomem; + *wanted = (struct drgn_module_wanted_supplementary_file){ + .file = file, + .supplementary_path = debugaltlink, + .checksum = build_id, + .checksum_len = build_id_len, + .checksum_str = no_cleanup_ptr(build_id_str), + .generation = ++prog->dbinfo.supplementary_file_generation, + }; + drgn_module_clear_wanted_supplementary_debug_file(module); + module->wanted_supplementary_debug_file = wanted; + module->debug_file_status = DRGN_MODULE_FILE_WANT_SUPPLEMENTARY; + return NULL; +} + +static bool +drgn_module_copy_section_addresses(struct drgn_module *module, Elf *elf) +{ + if (drgn_module_section_address_map_empty(&module->section_addresses)) + return true; + + size_t shstrndx; + if (elf_getshdrstrndx(elf, &shstrndx)) + return false; + + Elf_Scn *scn = NULL; + while ((scn = elf_nextscn(elf, scn))) { + GElf_Shdr *shdr, shdr_mem; + shdr = gelf_getshdr(scn, &shdr_mem); + if (!shdr) + return false; + + char *scnname = elf_strptr(elf, shstrndx, shdr->sh_name); + if (!scnname) + return false; + + struct drgn_module_section_address_map_iterator it = + drgn_module_section_address_map_search(&module->section_addresses, + &scnname); + if (!it.entry) + continue; + + shdr->sh_addr = it.entry->value; + if (!gelf_update_shdr(scn, shdr)) + return false; + } + return true; +} + +static bool elf_main_bias(struct drgn_program *prog, Elf *elf, uint64_t *ret) +{ + GElf_Ehdr ehdr_mem, *ehdr = gelf_getehdr(elf, &ehdr_mem); + if (!ehdr) { + drgn_log_debug(prog, "gelf_getehdr: %s", elf_errmsg(-1)); + return false; + } + + size_t phnum; + if (elf_getphdrnum(elf, &phnum) != 0) { + drgn_log_debug(prog, "elf_getphdrnum: %s", elf_errmsg(-1)); + return false; + } + + uint64_t phdr_vaddr; + bool have_phdr_vaddr = false; + for (size_t i = 0; i < phnum; i++) { + GElf_Phdr phdr_mem, *phdr = gelf_getphdr(elf, i, &phdr_mem); + if (!phdr) { + drgn_log_debug(prog, "gelf_getphdr: %s", + elf_errmsg(-1)); + return false; + } + if (phdr->p_type == PT_LOAD && + phdr->p_offset <= ehdr->e_phoff && + ehdr->e_phoff < phdr->p_offset + phdr->p_filesz) { + phdr_vaddr = ehdr->e_phoff - phdr->p_offset + phdr->p_vaddr; + have_phdr_vaddr = true; + } + } + if (!have_phdr_vaddr) { + drgn_log_debug(prog, + "file does not have loadable segment containing e_phoff"); + return false; + } + *ret = prog->auxv.at_phdr - phdr_vaddr; + return true; +} + +static bool elf_dso_bias(struct drgn_program *prog, Elf *elf, + uint64_t dynamic_address, uint64_t *ret) +{ + size_t phnum; + if (elf_getphdrnum(elf, &phnum) != 0) { + drgn_log_debug(prog, "elf_getphdrnum: %s", elf_errmsg(-1)); + return false; + } + + for (size_t i = 0; i < phnum; i++) { + GElf_Phdr phdr_mem, *phdr = gelf_getphdr(elf, i, &phdr_mem); + if (!phdr) { + drgn_log_debug(prog, "gelf_getphdr: %s", + elf_errmsg(-1)); + return false; + } + if (phdr->p_type == PT_DYNAMIC) { + *ret = dynamic_address - phdr->p_vaddr; + drgn_log_debug(prog, + "got bias 0x%" PRIx64 " from PT_DYNAMIC program header", + *ret); + return true; + } + } + drgn_log_debug(prog, "file does not have PT_DYNAMIC program header"); + return false; +} + +static bool drgn_module_elf_file_bias(struct drgn_module *module, + struct drgn_elf_file *file, uint64_t *ret) +{ + struct drgn_program *prog = module->prog; + SWITCH_ENUM(module->kind) { + case DRGN_MODULE_MAIN: + if (prog->flags & DRGN_PROGRAM_IS_LINUX_KERNEL) { + *ret = prog->vmcoreinfo.kaslr_offset; + drgn_log_debug(prog, + "got bias 0x%" PRIx64 " from VMCOREINFO", + *ret); + return true; + } else { + return elf_main_bias(prog, file->elf, ret); + } + case DRGN_MODULE_SHARED_LIBRARY: + return elf_dso_bias(prog, file->elf, + module->shared_library.dynamic_address, + ret); + case DRGN_MODULE_VDSO: + return elf_dso_bias(prog, file->elf, + module->vdso.dynamic_address, ret); + case DRGN_MODULE_EXTRA: + if (module->start != UINT64_MAX) { + uint64_t elf_start, elf_end; + if (!drgn_elf_file_address_range(file, &elf_start, + &elf_end)) + return false; + if (elf_start < elf_end) { + *ret = module->start - elf_start; + drgn_log_debug(prog, + "got bias 0x%" PRIx64 " from ELF start address", + *ret); + return true; + } + } + fallthrough; + case DRGN_MODULE_RELOCATABLE: + default: + *ret = 0; + return true; + } +} + +static bool +drgn_module_should_set_address_range_from_elf_file(struct drgn_module *module) +{ + if (module->start != UINT64_MAX) + return false; + + SWITCH_ENUM(module->kind) { + case DRGN_MODULE_MAIN: + case DRGN_MODULE_SHARED_LIBRARY: + case DRGN_MODULE_VDSO: + return true; + case DRGN_MODULE_RELOCATABLE: + case DRGN_MODULE_EXTRA: + default: + return false; + } +} + +// Takes ownership of file unless it is already owned by module. +static struct drgn_error * +drgn_module_maybe_use_elf_file(struct drgn_module *module, + struct drgn_elf_file *file, + bool is_gnu_debugaltlink_file) +{ + struct drgn_error *err; + struct drgn_program *prog = module->prog; + struct drgn_elf_file *gnu_debugaltlink_file = NULL; + + bool use_loaded, has_dwarf, use_debug; + if (is_gnu_debugaltlink_file) { + assert(module->debug_file_status + == DRGN_MODULE_FILE_WANT_SUPPLEMENTARY); + gnu_debugaltlink_file = file; + file = module->wanted_supplementary_debug_file->file; + use_loaded = false; + has_dwarf = use_debug = true; + } else { + // We should only be here if we want a file. + assert(drgn_module_wants_file(module)); + use_loaded = module->loaded_file_status == DRGN_MODULE_FILE_WANT + && file->is_loadable; + has_dwarf = drgn_elf_file_has_dwarf(file); + use_debug = drgn_module_wants_debug_file(module) && has_dwarf; + } + + _cleanup_free_ void *build_id_buf = NULL; + + if (!is_gnu_debugaltlink_file + && use_debug && file->scns[DRGN_SCN_GNU_DEBUGALTLINK]) { + // If we're trying to reuse a debug file that wants a + // supplementary file, then don't reset it, otherwise we'll free + // the file that we're trying to reuse. + if (!module->wanted_supplementary_debug_file + || module->wanted_supplementary_debug_file->file != file) { + err = drgn_module_set_wanted_gnu_debugaltlink(module, file); + if (err) + goto unused; + } + if (!use_loaded && module->wanted_supplementary_debug_file + && module->wanted_supplementary_debug_file->file == file) + return NULL; + use_debug = false; + } + + if (!use_loaded && !use_debug) { + if (file->is_loadable) { + drgn_log_debug(prog, + "%s is loadable, but don't want loaded file; ignoring", + file->path); + } else if (has_dwarf) { + drgn_log_debug(prog, + "%s has debug info, but don't want debug info; ignoring", + file->path); + } else { + drgn_log_debug(prog, + "%s is not loadable and no debug info; ignoring", + file->path); + } + err = NULL; + goto unused; + } + + // Get everything that might fail before we commit to using the file. + const void *elf_build_id; + ssize_t elf_build_id_len = 0; + if (module->build_id_len == 0) { + elf_build_id_len = drgn_elf_gnu_build_id(file->elf, + &elf_build_id); + if (elf_build_id_len < 0) { + drgn_log_debug(prog, "%s: %s", file->path, + elf_errmsg(-1)); + err = NULL; + goto unused; + } + if (elf_build_id_len > 0) { + build_id_buf = + drgn_module_alloc_build_id(elf_build_id_len); + if (!build_id_buf) { + err = &drgn_enomem; + goto unused; + } + } + } + + if (file != module->loaded_file && file != module->debug_file + && !drgn_module_copy_section_addresses(module, file->elf)) { + drgn_log_debug(prog, "%s: %s", file->path, elf_errmsg(-1)); + err = NULL; + goto unused; + } + + uint64_t bias; + if (!drgn_module_elf_file_bias(module, file, &bias)) { + err = NULL; + goto unused; + } + uint64_t elf_start = 0, elf_end = 0; + if (drgn_module_should_set_address_range_from_elf_file(module)) { + if (!drgn_elf_file_address_range(file, &elf_start, &elf_end)) { + drgn_log_debug(prog, "%s: %s", file->path, + elf_errmsg(-1)); + err = NULL; + goto unused; + } + elf_start += bias; + elf_end += bias; + if (elf_start >= elf_end) { + drgn_log_debug(prog, "%s: address range is invalid", + file->path); + } + } + + // At this point, we've committed to using the file. Nothing after this + // is allowed to fail. + + if (use_loaded && use_debug) { + drgn_log_info(prog, + "%s: using loadable file with debug info %s", + module->name, file->path); + } else if (use_loaded) { + drgn_log_info(prog, "%s: using loadable file %s", module->name, + file->path); + } else if (is_gnu_debugaltlink_file) { + drgn_log_info(prog, + "%s: using debug info file %s with supplementary file %s", + module->name, file->path, gnu_debugaltlink_file->path); + } else { + drgn_log_info(prog, "%s: using debug info file %s", + module->name, file->path); + } + + // If we got a build ID or address range earlier, install them. + if (elf_build_id_len > 0) { + drgn_module_set_build_id_impl(module, elf_build_id, + elf_build_id_len, + no_cleanup_ptr(build_id_buf)); + drgn_log_debug(prog, "%s: set build ID %s from file", + module->name, module->build_id_str); + } + if (elf_start < elf_end) { + drgn_log_debug(prog, + "%s: set address range 0x%" PRIx64 + "-0x%" PRIx64 " from file", module->name, + elf_start, elf_end); + err = drgn_module_set_address_range(module, elf_start, elf_end); + // This can only fail if the address range is invalid, which we + // just checked for. + assert(!err); + } + + if (use_loaded) { + module->loaded_file = file; + module->loaded_file_bias = bias; + module->loaded_file_status = DRGN_MODULE_FILE_HAVE; + module->elf_symtab_pending_files |= + DRGN_MODULE_FILE_MASK_LOADED; + } + if (use_debug) { + module->debug_file = file; + module->debug_file_bias = bias; + module->supplementary_debug_file = gnu_debugaltlink_file; + drgn_module_clear_wanted_supplementary_debug_file(module); + module->debug_file_status = DRGN_MODULE_FILE_HAVE; + module->pending_indexing_next = + prog->dbinfo.modules_pending_indexing; + prog->dbinfo.modules_pending_indexing = module; + prog->tried_main_language = false; + module->elf_symtab_pending_files |= + DRGN_MODULE_FILE_MASK_DEBUG; + } + if (!prog->has_platform) { + drgn_log_debug(prog, "setting program platform from %s", + file->path); + drgn_program_set_platform(prog, &file->platform); + } + return NULL; + +unused: + drgn_elf_file_destroy(gnu_debugaltlink_file); + if (module->wanted_supplementary_debug_file + && file == module->wanted_supplementary_debug_file->file) { + module->wanted_supplementary_debug_file->file = NULL; + drgn_module_clear_wanted_supplementary_debug_file(module); + module->debug_file_status = DRGN_MODULE_FILE_WANT; + } + if (file != module->loaded_file && file != module->debug_file) + drgn_elf_file_destroy(file); + return err; +} + +// Always takes ownership of fd_. Attempts to resolve the real path of path. +static struct drgn_error * +drgn_module_try_file_internal(struct drgn_module *module, const char *path, + int fd_, bool check_build_id, + const uint32_t *expected_crc) +{ + struct drgn_error *err; + struct drgn_program *prog = module->prog; + + _cleanup_close_ int fd = fd_; + if (fd >= 0) { + if (path) { + drgn_log_debug(prog, "%s: trying %s with fd %d", + module->name, path, fd); + } else { + drgn_log_debug(prog, "%s: trying fd %d", module->name, + fd); + } + } else { + fd = open(path, O_RDONLY); + if (fd < 0) { + drgn_log_debug(prog, "%s: %m", path); + return NULL; + } + drgn_log_debug(prog, "%s: trying %s", module->name, path); + } + + // Try to canonicalize the path, first via + // readlink("/proc/self/fd/$fd"), then via realpath(). +#define FORMAT "/proc/self/fd/%d" + char fd_path[sizeof(FORMAT) + - (sizeof("%d") - 1) + + max_decimal_length(int)]; + snprintf(fd_path, sizeof(fd_path), FORMAT, fd); +#undef FORMAT + + size_t link_buf_size = PATH_MAX; + _cleanup_free_ char *link_buf = malloc(link_buf_size); + if (!link_buf) + return &drgn_enomem; + + for (;;) { + ssize_t r = readlink(fd_path, link_buf, link_buf_size); + if (r < 0) { + drgn_log_debug(prog, "readlink: %s: %m", fd_path); + if (path) { + free(link_buf); + link_buf = realpath(path, NULL); + if (link_buf) { + drgn_log_debug(prog, + "canonical path is %s", + link_buf); + path = link_buf; + } else { + drgn_log_debug(prog, "realpath: %s: %m", + path); + } + } else { + path = fd_path; + } + break; + } + + if (r < link_buf_size) { + link_buf[r] = '\0'; + if (drgn_log_is_enabled(prog, DRGN_LOG_DEBUG) + && (!path || strcmp(path, link_buf) != 0)) { + drgn_log_debug(prog, "canonical path is %s", + link_buf); + } + path = link_buf; + break; + } + + if (__builtin_mul_overflow(link_buf_size, 2U, &link_buf_size)) + return &drgn_enomem; + free(link_buf); + link_buf = malloc(link_buf_size); + if (!link_buf) + return &drgn_enomem; + } + + _cleanup_elf_end_ Elf *elf = dwelf_elf_begin(fd); + if (!elf) { + drgn_log_debug(prog, "%s: %s", path, elf_errmsg(-1)); + return NULL; + } + if (elf_kind(elf) != ELF_K_ELF) { + drgn_log_debug(prog, "%s: not an ELF file", path); + return NULL; + } + + // This code assumes that DRGN_SUPPLEMENTARY_FILE_GNU_DEBUGALTLINK is + // the only kind of supplementary file, which is currently true. + bool log_build_id = check_build_id + || drgn_log_is_enabled(prog, DRGN_LOG_DEBUG); + const void *elf_build_id; + ssize_t elf_build_id_len; + if (module->debug_file_status == DRGN_MODULE_FILE_WANT_SUPPLEMENTARY + || (log_build_id && module->build_id_len > 0)) { + elf_build_id_len = drgn_elf_gnu_build_id(elf, &elf_build_id); + if (elf_build_id_len < 0) { + drgn_log_debug(prog, "%s: %s%s", path, elf_errmsg(-1), + check_build_id ? "" : "; ignoring build ID"); + } + } + + bool is_gnu_debugaltlink_file = false; + if (module->debug_file_status == DRGN_MODULE_FILE_WANT_SUPPLEMENTARY + && elf_build_id_len >= 0 + && elf_build_id_len + == module->wanted_supplementary_debug_file->checksum_len + && memcmp(elf_build_id, + module->wanted_supplementary_debug_file->checksum, + elf_build_id_len) == 0) { + drgn_log_debug(prog, "%s: %s build ID matches gnu_debugaltlink", + module->name, path); + is_gnu_debugaltlink_file = true; + } else if (log_build_id && module->build_id_len > 0) { + if (elf_build_id_len < 0) { + if (check_build_id) + return NULL; + } else if (elf_build_id_len == module->build_id_len + && memcmp(elf_build_id, module->build_id, + elf_build_id_len) == 0) { + drgn_log_debug(prog, "%s: %s build ID matches", + module->name, path); + } else { + if (elf_build_id_len == 0) { + drgn_log_debug(prog, + "%s: %s is missing build ID%s", + module->name, path, + check_build_id ? "" : "; forcing"); + } else { + drgn_log_debug(prog, + "%s: %s build ID does not match%s", + module->name, path, + check_build_id ? "" : "; forcing"); + } + if (check_build_id) + return NULL; + } + } + if (expected_crc) { + size_t size; + const void *rawfile = elf_rawfile(elf, &size); + if (!rawfile) { + drgn_log_debug(prog, "%s: %s", path, elf_errmsg(-1)); + return NULL; + } + uint32_t crc = ~crc32_update(-1, rawfile, size); + if (crc != *expected_crc) { + drgn_log_debug(prog, + "%s: %s CRC 0x%08" PRIx32 " does not match", + module->name, path, crc); + return NULL; + } + drgn_log_debug(prog, "%s: %s CRC matches", module->name, path); + } + + struct drgn_elf_file *file; + err = drgn_elf_file_create(module, path, fd, NULL, elf, &file); + if (err) { + if (!drgn_error_is_fatal(err)) { + drgn_error_log_debug(prog, err, ""); + drgn_error_destroy(err); + err = NULL; + } + return err; + } + // fd and elf are owned by the drgn_elf_file now. + fd = -1; + elf = NULL; + return drgn_module_maybe_use_elf_file(module, file, + is_gnu_debugaltlink_file); +} + +// Arbitrary limit on the number of bytes we'll allocate and read from the +// program's memory at once when finding modules/debug info. +static const uint64_t MAX_MEMORY_READ_FOR_DEBUG_INFO = UINT64_C(1048576); + +#define drgn_module_try_files_log(module, how_format, ...) \ +({ \ + struct drgn_module *_module = (module); \ + bool _want_loaded = _module->loaded_file_status == DRGN_MODULE_FILE_WANT;\ + bool _want_debug = _module->debug_file_status == DRGN_MODULE_FILE_WANT; \ + bool _want_supplementary_debug = _module->debug_file_status \ + == DRGN_MODULE_FILE_WANT_SUPPLEMENTARY;\ + drgn_log_debug(_module->prog, \ + "%s (%s%s): " how_format " %s%s%s file%s", _module->name,\ + _module->build_id_str ? "build ID " : "no build ID", \ + _module->build_id_str ?: "", \ + ## __VA_ARGS__, \ + _want_loaded ? "loaded" : "", \ + _want_loaded && (_want_debug || _want_supplementary_debug)\ + ? " and " : "", \ + _want_debug ? "debug" \ + : _want_supplementary_debug ? "supplementary debug" : "",\ + _want_loaded && (_want_debug || _want_supplementary_debug)\ + ? "s" : ""); \ +}) + +static struct drgn_error * +drgn_module_try_vdso_in_core(struct drgn_module *module) +{ + struct drgn_error *err; + struct drgn_program *prog = module->prog; + + // The Linux kernel has included the entire vDSO in core dumps since + // Linux kernel commit f47aef55d9a1 ("[PATCH] i386 vDSO: use + // VM_ALWAYSDUMP") (in v2.6.20). Try to read it from program memory. + + // The vDSO in memory is always stripped. + if (module->loaded_file_status != DRGN_MODULE_FILE_WANT) + return NULL; + + uint64_t start, end; + if (!drgn_module_address_range(module, &start, &end)) { + drgn_log_debug(prog, + "vDSO address range is not known; " + "can't read from program"); + return NULL; + } + if (start >= end) { + drgn_log_debug(prog, + "vDSO address range is empty; " + "can't read from program"); + return NULL; + } + uint64_t size = end - start; + if (size > MAX_MEMORY_READ_FOR_DEBUG_INFO) { + drgn_log_debug(prog, + "vDSO is unreasonably large (%" PRIu64 " bytes); " + "not reading from program", + size); + return NULL; + } + + _cleanup_free_ char *image = malloc(size); + if (!image) + return &drgn_enomem; + err = drgn_program_read_memory(prog, image, start, size, false); + if (err) { + if (!drgn_error_is_fatal(err)) { + drgn_error_log_debug(prog, err, "couldn't read vDSO: "); + drgn_error_destroy(err); + err = NULL; + } + return err; + } + + _cleanup_elf_end_ Elf *elf = elf_memory(image, size); + if (!elf) { + drgn_log_debug(prog, "couldn't read vDSO: %s", elf_errmsg(-1)); + return NULL; + } + struct drgn_elf_file *file; + err = drgn_elf_file_create(module, "[vdso]", -1, image, elf, &file); + if (err) { + if (!drgn_error_is_fatal(err)) { + drgn_error_log_debug(prog, err, ""); + drgn_error_destroy(err); + err = NULL; + } + return err; + } + // image and elf are owned by the drgn_elf_file now. + image = NULL; + elf = NULL; + + drgn_log_debug(prog, "trying vDSO in %s", + (module->prog->flags & DRGN_PROGRAM_IS_LIVE) + ? "memory" : "core"); + return drgn_module_maybe_use_elf_file(module, file, false); +} + +static void +drgn_module_try_supplementary_debug_file_log(struct drgn_module *module, + const char *how) +{ + const char *debug_file_path; + const char *debugaltlink_path; + if (drgn_module_wanted_supplementary_debug_file(module, + &debug_file_path, + &debugaltlink_path, + NULL, NULL) + != DRGN_SUPPLEMENTARY_FILE_GNU_DEBUGALTLINK) + return; + const char *debugaltlink_build_id_str = + module->wanted_supplementary_debug_file->checksum_str; + drgn_log_debug(module->prog, + "%s: %s gnu_debugaltlink %s build ID %s in file %s", + module->name, how, debugaltlink_path, + debugaltlink_build_id_str, debug_file_path); +} + +static struct drgn_error * +drgn_module_try_standard_supplementary_files(struct drgn_module *module) +{ + struct drgn_error *err; + struct drgn_program *prog = module->prog; + + const char *debug_file_path; + const char *debugaltlink_path; + if (drgn_module_wanted_supplementary_debug_file(module, + &debug_file_path, + &debugaltlink_path, + NULL, NULL) + != DRGN_SUPPLEMENTARY_FILE_GNU_DEBUGALTLINK) + return NULL; + + drgn_module_try_supplementary_debug_file_log(module, + "trying standard paths for"); + + STRING_BUILDER(sb); + const char *slash; + if (debugaltlink_path[0] == '/' + || !(slash = strrchr(debug_file_path, '/'))) { + // debugaltlink is absolute, or the debug file doesn't have a + // directory component and is therefore in the current working + // directory. Try debugaltlink directly. + err = drgn_module_try_file_internal(module, debugaltlink_path, + -1, true, NULL); + } else { + // Try $(dirname $path)/$debugaltlink. + if (!string_builder_appendn(&sb, debug_file_path, + slash + 1 - debug_file_path) + || !string_builder_append(&sb, debugaltlink_path) + || !string_builder_null_terminate(&sb)) + return &drgn_enomem; + err = drgn_module_try_file_internal(module, sb.str, -1, true, + NULL); + } + if (err + || module->debug_file_status != DRGN_MODULE_FILE_WANT_SUPPLEMENTARY) + return err; + + // All of the Linux distributions that use gnu_debugaltlink that I'm + // aware of (Debian, Fedora, SUSE, and their derivatives) put + // gnu_debugaltlink files in a ".dwz" subdirectory under the debug + // directory (e.g., "/usr/lib/debug/.dwz"). Try the path starting with + // the ".dwz" directory under all of the configured debug directories. + // This can help in a couple of cases: + // + // 1. When the gnu_debugaltlink path is absolute (which is the case on + // Debian and its derivatives as of Debian 12/Ubuntu 23.10) and the + // debug directory has been copied to a different path. See + // https://bugs.launchpad.net/ubuntu/+source/gdb/+bug/1818918. + // 2. When the gnu_debugaltlink path is relative (which is the case on + // Fedora, SUSE, and their derivatives) and the debug file was found + // outside of the debug directory. + const char *dwz = strstr(debugaltlink_path, "/.dwz/"); + if (dwz) { + const char *debug_dir; + size_t debug_dir_len; + drgn_program_for_each_debug_dir(prog, debug_dir, debug_dir_len) { + if (debug_dir_len == 0 || debug_dir[0] != '/') + continue; + + sb.len = 0; + if (!string_builder_appendn(&sb, debug_dir, + debug_dir_len) + || !string_builder_append(&sb, dwz) + || !string_builder_null_terminate(&sb)) + return &drgn_enomem; + + // Don't bother trying debugaltlink directly again. + if (strcmp(sb.str, debugaltlink_path) == 0) + continue; + + err = drgn_module_try_file_internal(module, sb.str, -1, + true, NULL); + if (err + || module->debug_file_status + != DRGN_MODULE_FILE_WANT_SUPPLEMENTARY) + return err; + } + } + return NULL; +} + +static bool +drgn_module_wanted_supplementary_debug_file_is_new(struct drgn_module *module, + uint64_t orig_supplementary_file_generation) +{ + return module->wanted_supplementary_debug_file + && module->wanted_supplementary_debug_file->generation + > orig_supplementary_file_generation; +} + +struct drgn_error * +drgn_module_try_standard_file(struct drgn_module *module, const char *path, + int fd, bool check_build_id, + const uint32_t *expected_crc) +{ + struct drgn_error *err; + uint64_t orig_supplementary_file_generation = + module->prog->dbinfo.supplementary_file_generation; + err = drgn_module_try_file_internal(module, path, fd, check_build_id, + expected_crc); + if (err) + return err; + // If the wanted supplementary debug file changed, try finding it again. + if (drgn_module_wanted_supplementary_debug_file_is_new(module, + orig_supplementary_file_generation)) { + err = drgn_module_try_standard_supplementary_files(module); + if (err) + return err; + } + return NULL; +} + +// An entry in /proc/$pid/map_files. +struct drgn_map_files_segment { + uint64_t start; + uint64_t end; +}; + +DEFINE_VECTOR(drgn_map_files_segment_vector, struct drgn_map_files_segment); + +static inline int drgn_map_files_segment_compare(const void *_a, const void *_b) +{ + const struct drgn_map_files_segment *a = _a; + const struct drgn_map_files_segment *b = _b; + return (a->start > b->start) - (a->start < b->start); +} + +static void +drgn_debug_info_set_map_files_segments(struct drgn_debug_info *dbinfo, + struct drgn_map_files_segment_vector *segments, + bool sorted) +{ + free(dbinfo->map_files_segments); + drgn_map_files_segment_vector_shrink_to_fit(segments); + drgn_map_files_segment_vector_steal(segments, + &dbinfo->map_files_segments, + &dbinfo->num_map_files_segments); + // The Linux kernel always returns these entries in order, but sort it + // just in case. + if (!sorted) { + qsort(dbinfo->map_files_segments, + dbinfo->num_map_files_segments, + sizeof(dbinfo->map_files_segments[0]), + drgn_map_files_segment_compare); + } +} + +static struct drgn_error * +drgn_module_try_proc_files_for_shared_library(struct drgn_module *module, + bool *tried) +{ + struct drgn_error *err; + struct drgn_program *prog = module->prog; + const uint64_t address = module->shared_library.dynamic_address; + +#define DIR_FORMAT "/proc/%ld/map_files" +#define ENTRY_FORMAT "/%" PRIx64 "-%" PRIx64 + char path[sizeof(DIR_FORMAT ENTRY_FORMAT) + - (sizeof("%ld") - 1) + + max_decimal_length(long) + - 2 * (sizeof("%" PRIx64) - 1) + + 2 * 16]; + int dir_len = sprintf(path, DIR_FORMAT, (long)prog->pid); + + // Check the cache first. + #define less_than_start(a, b) (*(a) < (b)->start) + size_t cache_index = binary_search_gt(prog->dbinfo.map_files_segments, + prog->dbinfo.num_map_files_segments, + &address, less_than_start); + #undef less_than_start + if (cache_index > 0 + && address < prog->dbinfo.map_files_segments[cache_index - 1].end) { + struct drgn_map_files_segment *cache = + &prog->dbinfo.map_files_segments[cache_index - 1]; + sprintf(path + dir_len, ENTRY_FORMAT, cache->start, cache->end); + drgn_log_debug(prog, + "found %s containing dynamic section 0x%" PRIx64 " in map_files cache", + path, address); + int fd = open(path, O_RDONLY); + if (fd >= 0) { + *tried = true; + return drgn_module_try_standard_file(module, path, fd, + false, NULL); + } else { + // We found a match in the cache, but we couldn't open + // it. If it doesn't exist anymore, then we need to + // rebuild the cache. If it failed for any other reason, + // ignore it like we do in the cache miss case. + bool rebuild_cache = errno == ENOENT; + drgn_log_debug(prog, "%s: %m", path); + if (!rebuild_cache) + return NULL; + } + drgn_log_debug(prog, "rebuilding map_files cache"); + path[dir_len] = '\0'; + } +#undef ENTRY_FORMAT +#undef DIR_FORMAT + + // Walk /proc/$pid/map_files, caching it while looking for a match. + _cleanup_closedir_ DIR *dir = opendir(path); + if (!dir) { + if (errno != ENOENT) + return drgn_error_create_os("opendir", errno, path); + drgn_log_debug(prog, "%s: %m", path); + return NULL; + } + _cleanup_(drgn_map_files_segment_vector_deinit) + struct drgn_map_files_segment_vector segments = VECTOR_INIT; + bool sorted = true; + bool found = false; + struct dirent *ent; + while ((errno = 0, ent = readdir(dir))) { + struct drgn_map_files_segment segment; + if (sscanf(ent->d_name, "%" SCNx64 "-%" SCNx64, &segment.start, + &segment.end) != 2) + continue; + + if (!drgn_map_files_segment_vector_empty(&segments) + && segment.start + < drgn_map_files_segment_vector_last(&segments)->start) + sorted = false; + if (!drgn_map_files_segment_vector_append(&segments, &segment)) + return &drgn_enomem; + + if (segment.start <= address && address < segment.end + && !found + && strlen(ent->d_name) + 1 < sizeof(path) - dir_len) { + found = true; + path[dir_len] = '/'; + memcpy(path + dir_len + 1, ent->d_name, + strlen(ent->d_name) + 1); + drgn_log_debug(prog, + "found %s containing dynamic section 0x%" PRIx64, + path, address); + int fd = openat(dirfd(dir), ent->d_name, O_RDONLY); + if (fd >= 0) { + *tried = true; + err = drgn_module_try_standard_file(module, + path, fd, + false, + NULL); + if (err) + return err; + } else { + drgn_log_debug(prog, "%s: %m", path); + } + path[dir_len] = '\0'; + } + } + if (errno) + return drgn_error_create_os("readdir", errno, path); + + drgn_debug_info_set_map_files_segments(&prog->dbinfo, &segments, + sorted); + + if (!found) { + drgn_log_debug(prog, + "didn't find entry in %s containing dynamic section 0x%" PRIx64, + path, address); + } + return NULL; +} + +static struct drgn_error *drgn_module_try_proc_files(struct drgn_module *module, + bool *tried) +{ + struct drgn_program *prog = module->prog; + + *tried = false; + if (module->kind == DRGN_MODULE_MAIN) { +#define FORMAT "/proc/%ld/exe" + char path[sizeof(FORMAT) + - (sizeof("%ld") - 1) + + max_decimal_length(long)]; + snprintf(path, sizeof(path), FORMAT, (long)prog->pid); +#undef FORMAT + int fd = open(path, O_RDONLY); + if (fd < 0) { + drgn_log_debug(prog, "%s: %m", path); + return NULL; + } + *tried = true; + return drgn_module_try_standard_file(module, path, fd, false, + NULL); + } else if (module->kind == DRGN_MODULE_SHARED_LIBRARY) { + return drgn_module_try_proc_files_for_shared_library(module, + tried); + } else { + return NULL; + } +} + +static struct drgn_error * +drgn_module_try_files_by_build_id(struct drgn_module *module) +{ + struct drgn_error *err; + + size_t build_id_len; + const char *build_id_str = + drgn_module_build_id(module, NULL, &build_id_len); + // We need at least 2 bytes (4 hex characters) to build the paths. + if (build_id_len < 2) + return NULL; + + STRING_BUILDER(sb); + const char *debug_dir; + size_t debug_dir_len; + drgn_program_for_each_debug_dir(module->prog, debug_dir, debug_dir_len) { + if (debug_dir_len == 0 || debug_dir[0] != '/') + continue; + if (!string_builder_appendn(&sb, debug_dir, debug_dir_len) + || !string_builder_appendf(&sb, "/.build-id/%c%c/%s.debug", + build_id_str[0], build_id_str[1], + &build_id_str[2]) + || !string_builder_null_terminate(&sb)) + return &drgn_enomem; + // We trust the build ID encoded in the path and don't check it + // again. + if (module->debug_file_status == DRGN_MODULE_FILE_WANT) { + err = drgn_module_try_standard_file(module, sb.str, -1, + false, NULL); + if (err || !drgn_module_wants_file(module)) + return err; + } + if (module->loaded_file_status == DRGN_MODULE_FILE_WANT) { + // Remove the ".debug" extension. + sb.str[sb.len - sizeof(".debug") + 1] = '\0'; + err = drgn_module_try_standard_file(module, sb.str, -1, + false, NULL); + if (err || !drgn_module_wants_file(module)) + return err; + } + sb.len = 0; + } + return NULL; +} + +static struct drgn_error * +drgn_module_try_files_by_gnu_debuglink(struct drgn_module *module) +{ + struct drgn_error *err; + struct drgn_program *prog = module->prog; + + struct drgn_elf_file *file = module->loaded_file; + if (!file || !file->scns[DRGN_SCN_GNU_DEBUGLINK]) + return NULL; + // We don't cache .gnu_debuglink, and it doesn't need relocation, so + // don't use drgn_elf_file_read_section(). + Elf_Data *data; + err = read_elf_section(file->scns[DRGN_SCN_GNU_DEBUGLINK], &data); + if (err) { + if (!drgn_error_is_fatal(err)) { + drgn_error_log_debug(prog, err, + "%s: couldn't read .gnu_debuglink: ", + file->path); + drgn_error_destroy(err); + err = NULL; + } + return err; + } + + struct drgn_elf_file_section_buffer buffer; + drgn_elf_file_section_buffer_init(&buffer, file, + file->scns[DRGN_SCN_GNU_DEBUGLINK], + data); + const char *debuglink; + size_t debuglink_len; + uint32_t crc; + if ((err = binary_buffer_next_string(&buffer.bb, &debuglink, + &debuglink_len)) + // Align up to 4-byte boundary. + || (err = binary_buffer_skip(&buffer.bb, -(debuglink_len + 1) & 3)) + || (err = binary_buffer_next_u32(&buffer.bb, &crc))) { + if (!drgn_error_is_fatal(err)) { + drgn_error_log_debug(prog, err, ""); + drgn_error_destroy(err); + err = NULL; + } + return err; + } + drgn_log_debug(prog, "%s has debuglink %s CRC 0x%08" PRIx32, file->path, + debuglink, crc); + + STRING_BUILDER(sb); + if (debuglink[0] == '/') { + // debuglink is absolute. Try it directly. + err = drgn_module_try_standard_file(module, debuglink, -1, + false, &crc); + if (err || !drgn_module_wants_file(module)) + return err; + } else if (file->path[0] && debuglink[0]) { + // debuglink is relative. Try it in the debug directories. + const char *slash = strrchr(file->path, '/'); + size_t dirslash_len = slash ? slash - file->path + 1 : 0; + const char *debug_dir; + size_t debug_dir_len; + drgn_program_for_each_debug_dir(prog, debug_dir, debug_dir_len) { + // If debug_dir is empty, then try: + // $(dirname $path)/$debuglink + // If debug_dir is relative, then try: + // $(dirname $path)/$debug_dir/$debuglink + // If debug_dir is absolute, then try: + // $debug_dir/$(dirname $path)/$debuglink + if (debug_dir_len > 0 && debug_dir[0] == '/') { + if (file->path[0] != '/') + continue; + if (!string_builder_appendn(&sb, debug_dir, + debug_dir_len)) + return &drgn_enomem; + } + if (!string_builder_appendn(&sb, file->path, + dirslash_len) + || (debug_dir_len > 0 && debug_dir[0] != '/' + && (!string_builder_appendn(&sb, debug_dir, + debug_dir_len) + || !string_builder_appendc(&sb, '/'))) + || !string_builder_appendn(&sb, debuglink, + debuglink_len) + || !string_builder_null_terminate(&sb)) + return &drgn_enomem; + err = drgn_module_try_standard_file(module, sb.str, -1, + false, &crc); + if (err || !drgn_module_wants_file(module)) + return err; + sb.len = 0; + } + } + return NULL; +} + +static struct drgn_error * +drgn_module_try_standard_files(struct drgn_module *module, + struct drgn_module_standard_files_state *state) +{ + struct drgn_error *err; + struct drgn_program *prog = module->prog; + + if (prog->dbinfo.debug_info_path) { + drgn_module_try_files_log(module, + "trying standard paths in \"%s\" for", + prog->dbinfo.debug_info_path); + } else { + drgn_module_try_files_log(module, "trying standard paths for"); + } + + // If we need a supplementary file, try that first. + err = drgn_module_try_standard_supplementary_files(module); + if (err || !drgn_module_wants_file(module)) + return err; + + // If a previous attempt used a loadable file with debug info but didn't + // want both, we might be able to reuse it. + if (module->loaded_file_status == DRGN_MODULE_FILE_WANT) { + struct drgn_elf_file *reuse_file = NULL; + if (module->debug_file && module->debug_file->is_loadable) + reuse_file = module->debug_file; + else if (module->wanted_supplementary_debug_file + && module->wanted_supplementary_debug_file->file->is_loadable) + reuse_file = module->wanted_supplementary_debug_file->file; + if (reuse_file) { + drgn_log_debug(prog, + "reusing loadable debug file %s as loaded file", + reuse_file->path); + err = drgn_module_maybe_use_elf_file(module, reuse_file, + false); + if (err || !drgn_module_wants_file(module)) + return err; + } + } + if (module->debug_file_status == DRGN_MODULE_FILE_WANT + && module->loaded_file + && drgn_elf_file_has_dwarf(module->loaded_file)) { + drgn_log_debug(prog, + "reusing loaded file with debug info %s as debug file", + module->loaded_file->path); + err = drgn_module_maybe_use_elf_file(module, + module->loaded_file, + false); + if (err || !drgn_module_wants_file(module)) + return err; + } + + // First, try methods that are guaranteed to find the right file: + // reading a vDSO from the core dump and opening a file via a magic + // symlink in /proc. + bool tried_proc_symlink = false; + if (module->kind == DRGN_MODULE_VDSO) { + err = drgn_module_try_vdso_in_core(module); + if (err || !drgn_module_wants_file(module)) + return err; + } else if (drgn_program_is_userspace_process(prog)) { + err = drgn_module_try_proc_files(module, &tried_proc_symlink); + if (err || !drgn_module_wants_file(module)) + return err; + } + + // If we already have the build ID, try it now before wasting time with + // the expected paths. If this is a Linux kernel loadable module, this + // can save us from needing the depmod index. If not, it can still save + // us from trying a file with the wrong build ID. + const bool had_build_id = module->build_id_len > 0; + if (had_build_id) { + err = drgn_module_try_files_by_build_id(module); + if (err || !drgn_module_wants_file(module)) + return err; + } + + // Next, try opening things at their expected paths. If this is the + // Linux kernel or a Linux kernel loadable module, try some well-known + // paths. + if (module->kind == DRGN_MODULE_MAIN + && (module->prog->flags & DRGN_PROGRAM_IS_LINUX_KERNEL)) { + err = drgn_module_try_vmlinux_files(module, state); + if (err || !drgn_module_wants_file(module)) + return err; + } else if (module->kind == DRGN_MODULE_RELOCATABLE + && (module->prog->flags & DRGN_PROGRAM_IS_LINUX_KERNEL)) { + err = drgn_module_try_linux_kmod_files(module, state); + if (err || !drgn_module_wants_file(module)) + return err; + // Otherwise, if the module name looks like a path (i.e., it contains a + // slash), try it. The vDSO is embedded in the kernel and isn't on disk, + // so there's no point in trying it. Additionally, if we already tried a + // /proc symlink, then we already tried the file that the path is + // supposed to refer to, so don't try again. + } else if (module->kind != DRGN_MODULE_VDSO + && !tried_proc_symlink + && strchr(module->name, '/')) { + err = drgn_module_try_standard_file(module, module->name, -1, + true, NULL); + if (err || !drgn_module_wants_file(module)) + return err; + } + + // If we didn't have the build ID before, we might have found the loaded + // file and gotten a build ID from it. Try to find the debug file by + // build ID now. + if (!had_build_id) { + err = drgn_module_try_files_by_build_id(module); + if (err || !drgn_module_wants_file(module)) + return err; + } + + // We might have a loaded file with a .gnu_debuglink. Try to find the + // corresponding debug file. + return drgn_module_try_files_by_gnu_debuglink(module); +} + +static void +drgn_module_standard_files_state_deinit(struct drgn_module_standard_files_state *state) +{ + depmod_index_deinit(&state->modules_dep); +} + +static struct drgn_error * +drgn_standard_module_file_find(struct drgn_module * const *modules, + size_t num_modules, void *arg) +{ + struct drgn_error *err; + + _cleanup_(drgn_module_standard_files_state_deinit) + struct drgn_module_standard_files_state state = {}; + for (size_t i = 0; i < num_modules; i++) { + err = drgn_module_try_standard_files(modules[i], &state); + if (err) + return err; + } + return NULL; +} + +#if WITH_DEBUGINFOD +static int count_columns(const char *s, size_t n) +{ + int columns = 0; + while (n > 0) { + mbstate_t ps; + memset(&ps, 0, sizeof(ps)); + do { + wchar_t wc; + size_t r = mbrtowc(&wc, s, n, &ps); + if (r == (size_t)-1) // Invalid multibyte sequence. + return -1; + if (r == (size_t)-2) // Incomplete multibyte character. + return -2; + if (r == 0) // Null wide character. + r = 1; + + int w = wcwidth(wc); + if (w < 0) // Nonprintable wide character. + return -3; + s += r; + n -= r; + columns += w; + } while (!mbsinit(&ps)); + } + return columns; +} + +static int truncate_columns(struct string_builder *sb, size_t start, size_t end, + int max_columns) +{ + int columns = 0; + + size_t truncate_len = start; + int truncate_column = 0; + mbstate_t truncate_ps; + memset(&truncate_ps, 0, sizeof(truncate_ps)); + + while (start < end) { + mbstate_t ps; + memset(&ps, 0, sizeof(ps)); + do { + wchar_t wc; + size_t r = mbrtowc(&wc, &sb->str[start], end - start, + &ps); + if (r == (size_t)-1) // Invalid multibyte sequence. + return -1; + if (r == (size_t)-2) // Incomplete multibyte character. + return -2; + if (r == 0) // Null wide character. + r = 1; + + int w = wcwidth(wc); + if (w < 0) // Nonprintable wide character. + return -3; + + if (w > max_columns - columns) { + int dots = min(max_columns, 3); + char reset[MB_LEN_MAX]; + size_t reset_len = 0; + if (!mbsinit(&truncate_ps)) { + reset_len = wcrtomb(reset, L'\0', + &truncate_ps) - 1; + } + size_t new_len = (truncate_len + + reset_len + + dots + + (sb->len - end)); + if (!string_builder_reserve(sb, new_len)) + return INT_MIN; + memmove(&sb->str[truncate_len + reset_len + dots], + &sb->str[end], sb->len - end); + memset(&sb->str[truncate_len + reset_len], '.', + dots); + memcpy(&sb->str[truncate_len], reset, + reset_len); + sb->len = new_len; + return truncate_column + dots; + } + + start += r; + columns += w; + if (columns <= max_columns - 3) { + truncate_len = start; + truncate_column = columns; + memcpy(&truncate_ps, &ps, sizeof(ps)); + } + } while (!mbsinit(&ps)); + } + return columns; +} + +static void reset_shift_state(struct string_builder *sb, mbstate_t *ps) +{ + if (!mbsinit(ps)) + sb->len += wcrtomb(&sb->str[sb->len], L'\0', ps) - 1; +} + +static bool write_unicode_progress_bar(struct string_builder *sb, int columns, + double ratio) +{ + size_t orig_len = sb->len; + + mbstate_t ps; + memset(&ps, 0, sizeof(ps)); + + // "Right one eighth block" character. + size_t r = wcrtomb(&sb->str[sb->len], L'\u2595', &ps); + if (r == (size_t)-1) + return false; + sb->len += r; + + // + 0.25 so that we round up if the piece would be at least 75% full. + int eighths = columns * ratio * 8.0 + 0.25; + int blocks = eighths / 8; + int i; + for (i = 0; i < blocks; i++) { + // "Full block" character. + r = wcrtomb(&sb->str[sb->len], L'\u2588', &ps); + if (r == (size_t)-1) + goto undo; + sb->len += r; + } + // "Left one eighth block" through "left seven eighths block" + // characters. + static const wchar_t eighths_blocks[7] = + L"\u258f\u258e\u258d\u258c\u258b\u258a\u2589"; + if (eighths % 8 != 0) { + r = wcrtomb(&sb->str[sb->len], eighths_blocks[eighths % 8 - 1], + &ps); + if (r == (size_t)-1) + goto undo; + sb->len += r; + i++; + } + + for (; i < columns; i++) { + r = wcrtomb(&sb->str[sb->len], L' ', &ps); + if (r == (size_t)-1) + goto undo; + sb->len += r; + } + + // "Left one eighth block" character. + r = wcrtomb(&sb->str[sb->len], L'\u258f', &ps); + if (r == (size_t)-1) + goto undo; + sb->len += r; + + reset_shift_state(sb, &ps); + return true; + +undo: + sb->len = orig_len; + return false; +} + +static void write_ascii_progress_bar(struct string_builder *sb, int columns, + double ratio) +{ + sb->str[sb->len++] = '['; + // + 0.25 so that we round up if the block would be at least 75% full. + int blocks = columns * ratio + 0.25; + memset(&sb->str[sb->len], '#', blocks); + sb->len += blocks; + memset(&sb->str[sb->len], ' ', columns - blocks); + sb->len += columns - blocks; + sb->str[sb->len++] = ']'; +} + +static bool write_unicode_spinner(struct string_builder *sb, int pos) +{ + static const wchar_t spinner[] = { + L'\u2596', // Quadrant lower left + L'\u2598', // Quadrant upper left + L'\u259d', // Quadrant upper right + L'\u2597', // Quadrant lower right + }; + mbstate_t ps; + memset(&ps, 0, sizeof(ps)); + size_t r = wcrtomb(&sb->str[sb->len], + spinner[pos % array_size(spinner)], &ps); + if (r == (size_t)-1) + return false; + sb->len += r; + reset_shift_state(sb, &ps); + return true; +} + +static void write_ascii_spinner(struct string_builder *sb, int pos) +{ + static const char spinner[] = { '|', '/', '-', '\\' }; + sb->str[sb->len++] = spinner[pos % array_size(spinner)]; +} + +// debuginfod_set_user_data() and debuginfod_get_user_data() were added in +// elfutils 0.179. Before that, we emulate them with a thread-local variable. +#if !_ELFUTILS_PREREQ(0, 179) +static _Thread_local void *drgn_debuginfod_user_data; +#endif + +// This is called with: +// - a >= 0 && b == 0 while cleaning the debuginfod cache, where a is the number +// of files in the cache that have been checked. +// - a >= 0 && b == 0 while waiting to read the first chunk of data from a +// debuginfod server, where a is an increasing counter. Note that this cannot +// be distinguished from the previous case. +// - a >= 0 && b > 0 while downloading, where a is the number of bytes +// downloaded and b is the total size to download in bytes. +// - a >= 0 && b <= 0 while downloading, where a is the number of bytes +// downloaded and the total size is not known. This can be distinguished from +// the first two cases because debuginfod_get_url() will return non-NULL. +// - a < 0 && b >= 0 when the download has finished successfully. b is the +// downloaded file descriptor. +// - a < 0 && b < 0 when the download failed. b is a negative errno. +static void drgn_log_debuginfod_progress(debuginfod_client *client, long a, + long b) +{ +#if _ELFUTILS_PREREQ(0, 179) + struct drgn_program *prog = drgn_debuginfod_get_user_data(client); +#else + struct drgn_program *prog = drgn_debuginfod_user_data; +#endif + + const bool done = a < 0; + + // If we already started logging progress for this download when it + // failed, we log the error like progress below. Otherwise, the download + // failed very early, so we only log a debug message. + if (done && b < 0 && !prog->dbinfo.logged_debuginfod_progress) { + if (b != -ENOSYS) { + errno = -b; + drgn_log_debug(prog, + "%s: couldn't download%s from debuginfod: %m", + prog->dbinfo.debuginfod_current_name, + prog->dbinfo.debuginfod_current_type); + } else if (!prog->dbinfo.logged_no_debuginfod) { + drgn_log_debug(prog, + "no debuginfod servers configured; " + "try setting the DEBUGINFOD_URLS environment variable"); + prog->dbinfo.logged_no_debuginfod = true; + } + return; + } + prog->dbinfo.logged_debuginfod_progress = true; + + int columns; + FILE *file = drgn_program_get_progress_file(prog, &columns); + + // ANSI escape sequence to clear the current line and return the cursor + // to the beginning of the line. + static const char ansi_erase_line[] = "\33[2K\r"; + + // Once we know what URL we are downloading from, log it. + if (!prog->dbinfo.debuginfod_have_url) { + // debuginfod_get_url() was added in elfutils 0.179. Before + // that, we have to assume that we have a URL. +#if _ELFUTILS_PREREQ(0, 179) + const char *url = drgn_debuginfod_get_url(client); + if (url) { + prog->dbinfo.debuginfod_have_url = true; + // Erase the current line since we may have logged + // progress. + if (columns >= 0) { + fwrite(ansi_erase_line, 1, + sizeof(ansi_erase_line) - 1, file); + fflush(file); + } + drgn_log_debug(prog, "downloading from debuginfod at %s", url); + } +#else + prog->dbinfo.debuginfod_have_url = true; +#endif + } + + // If we succeeded without ever getting a URL, it must have been cached. + if (done && b >= 0 && !prog->dbinfo.debuginfod_have_url) { + // We may have logged download progress when we were actually + // cleaning the cache. Clear it to avoid confusion. + if (columns >= 0) { + fwrite(ansi_erase_line, 1, sizeof(ansi_erase_line) - 1, + file); + fflush(file); + } + drgn_log_debug(prog, "%s: found%s in debuginfod cache", + prog->dbinfo.debuginfod_current_name, + prog->dbinfo.debuginfod_current_type); + return; + } + + if (!file) + return; + + // We only do the progress animation if we would have at least one + // column for a progress bar. Using the calculation for bar_columns + // below: + // + // columns - (floor(columns / 2) - 10) - 2 - 4 >= 1 + // => columns - floor(columns / 2) >= 17 + // => ceil(columns / 2) >= 17 + // => columns >= 33 + bool animate = columns >= 33; + const bool orig_animate = animate; + + STRING_BUILDER(sb); + + if (animate && !string_builder_appendc(&sb, '\r')) + return; + + int fill_columns = 0; + int bar_columns = 0; + if (animate) { + if (done) { + // We need to erase anything left in the line with + // spaces. + fill_columns = columns; + } else if (b > 0) { + // Use half of the line plus a bit for the name and + // download size so that it doesn't get too short in + // small terminals. + fill_columns = columns / 2 + 10; + // Use the rest for the progress bar. + bar_columns = (columns - fill_columns + - 2 // Ends of progress bar + - 4 // " XX%" + ); + } else { + // Use the whole line, minus the spinner, for the name + // and download size + fill_columns = columns - 1; + } + } + + if (!string_builder_append(&sb, + done && b >= 0 + ? "Downloaded " : "Downloading ") + || !string_builder_append(&sb, + prog->dbinfo.debuginfod_current_name) + || !string_builder_append(&sb, + prog->dbinfo.debuginfod_current_type)) + return; + + size_t download_size_start = sb.len; + if (done && b < 0) { + errno = -b; + if (!string_builder_appendf(&sb, " failed: %m")) + return; + } else if (prog->dbinfo.debuginfod_have_url) { + intmax_t download_size; + if (done) { + struct stat st; + if (fstat(b, &st) < 0) { + drgn_log_warning(prog, "fstat: %m"); + return; + } + download_size = st.st_size; + } else { + download_size = a; + } + if (download_size < 2048) { + if (!string_builder_appendf(&sb, " (%" PRIdMAX " B)", + download_size)) + return; + } else { + static const char prefixes[] = "KMGTPEZY"; + int i = 1; + while (i < sizeof(prefixes) - 1 + && (download_size >> (10 * i)) >= 2048) + i++; + double unit = INTMAX_C(1) << (10 * i); + if (!string_builder_appendf(&sb, " (%.1f %ciB)", + download_size / unit, + prefixes[i - 1])) + return; + } + } + + if (animate) { + int current_column; + if (done) { + // Start at byte 1 to skip the "\r". + current_column = count_columns(&sb.str[1], sb.len - 1); + } else { + int download_size_len = sb.len - download_size_start; + // Leave room for the download size and an extra space. + int max_columns = + max(fill_columns - download_size_len - 1, 0); + // Start at byte 1 to skip the "\r". + current_column = truncate_columns(&sb, 1, + download_size_start, + max_columns); + if (current_column == INT_MIN) + return; // Memory allocation failed. + if (current_column >= 0) + current_column += download_size_len; + } + if (current_column < 0) { + // We either couldn't decode the string or the string + // contained a nonprintable character. Give up on the + // animation. + animate = false; + } else if (current_column < fill_columns) { + if (!string_builder_reserve_for_append(&sb, + fill_columns + - current_column)) + return; + memset(&sb.str[sb.len], ' ', + fill_columns - current_column); + sb.len += fill_columns - current_column; + } + } + + // If we can't encode any of the following Unicode characters in the + // current locale, we fall back to ASCII. + if (!done && b > 0) { + // Clamp the ratio in case we get bogus sizes. + double ratio = a < b ? (double)a / (double)b : 1.0; + if (animate) { + // One multibyte character for each bar column, one for + // each end, and one to reset the shift state. + if (!string_builder_reserve_for_append(&sb, + (bar_columns + 3) + * MB_CUR_MAX)) + return; + if (!write_unicode_progress_bar(&sb, bar_columns, + ratio)) { + write_ascii_progress_bar(&sb, bar_columns, + ratio); + } + } + unsigned int percent = 100.0 * ratio; + // We're not 100% done until we're called with done = true. + if (percent > 99) + percent = 99; + if (!string_builder_appendf(&sb, " %*u%%", animate ? 2 : 0, + percent)) + return; + } else if (!done && animate) { + // One multibyte character for the spinner, one to reset the + // shift state. + if (!string_builder_reserve_for_append(&sb, 2 * MB_CUR_MAX)) + return; + unsigned int pos = prog->dbinfo.debuginfod_spinner_position++; + if (!write_unicode_spinner(&sb, pos)) + write_ascii_spinner(&sb, pos); + } + + if ((done || !animate) && !string_builder_appendc(&sb, '\n')) + return; + + // If we were originally animating but gave up, we need to skip the + // "\r". + fwrite(sb.str + (orig_animate && !animate ? 1 : 0), 1, + sb.len - (orig_animate && !animate ? 1 : 0), file); +} + +static struct sigaction drgn_cancel_debuginfod_oldact; +static volatile sig_atomic_t drgn_cancel_debuginfod; +static void drgn_cancel_debuginfod_handler(int sig) +{ + drgn_cancel_debuginfod = 1; + drgn_cancel_debuginfod_oldact.sa_handler(sig); +} +static void drgn_cancel_debuginfod_sigaction(int sig, siginfo_t *info, + void *ucontext) +{ + drgn_cancel_debuginfod = 1; + drgn_cancel_debuginfod_oldact.sa_sigaction(sig, info, ucontext); +} +static bool drgn_prepare_debuginfod_find(struct drgn_program *prog) +{ +#if !_ELFUTILS_PREREQ(0, 179) + drgn_debuginfod_user_data = prog; +#endif + // If the application has a signal handler for SIGINT, temporarily wrap + // it with our own signal handler that sets a flag for the debuginfod + // progressfn. This allows Ctrl+C to interrupt a download in + // applications that handle SIGINT (like the Python interpreter). + drgn_cancel_debuginfod = 0; + if (sigaction(SIGINT, NULL, &drgn_cancel_debuginfod_oldact) != 0) + return false; + struct sigaction act = drgn_cancel_debuginfod_oldact; + if ((act.sa_flags & SA_SIGINFO) + // SIG_DFL and SIG_IGN are meant to be assigned to sa_handler, but + // the Linux kernel treats them the same for sa_sigaction. + && act.sa_sigaction != (void *)SIG_DFL + && act.sa_sigaction != (void *)SIG_IGN) + act.sa_sigaction = drgn_cancel_debuginfod_sigaction; + else if (!(act.sa_flags & SA_SIGINFO) + && act.sa_handler != SIG_DFL && act.sa_handler != SIG_IGN) + act.sa_handler = drgn_cancel_debuginfod_handler; + else + return false; + return sigaction(SIGINT, &act, NULL) == 0; +} +static void drgn_finish_debuginfod_find(bool restore_sigaction) +{ + if (restore_sigaction) + sigaction(SIGINT, &drgn_cancel_debuginfod_oldact, NULL); +} + +static int drgn_debuginfod_progressfn(debuginfod_client *client, long a, long b) +{ + if (drgn_cancel_debuginfod) + return 1; + if (a >= 0) + drgn_log_debuginfod_progress(client, a, b); + return 0; +} + +static struct drgn_error * +drgn_module_try_file_from_debuginfod(struct drgn_module *module, + const char *build_id_str, + bool debug, bool supplementary, + struct string_builder *cache_sb) +{ + struct drgn_program *prog = module->prog; + + if (!string_builder_appendf(cache_sb, "/%s/%s", build_id_str, + debug ? "debuginfo" : "executable") + || !string_builder_null_terminate(cache_sb)) + return &drgn_enomem; + + prog->dbinfo.debuginfod_current_name = module->name; + if (supplementary) + prog->dbinfo.debuginfod_current_type = " supplementary debug info"; + else if (debug) + prog->dbinfo.debuginfod_current_type = " debug info"; + else + prog->dbinfo.debuginfod_current_type = ""; + prog->dbinfo.debuginfod_have_url = false; + prog->dbinfo.logged_debuginfod_progress = false; + bool restore_sigaction = drgn_prepare_debuginfod_find(prog); + char *path; + auto find = debug + ? drgn_debuginfod_find_debuginfo + : drgn_debuginfod_find_executable; + int fd = find(prog->dbinfo.debuginfod_client, + (const unsigned char *)build_id_str, 0, &path); + drgn_finish_debuginfod_find(restore_sigaction); + if (fd == -ENOENT && drgn_cancel_debuginfod) { + // Before elfutils commit 5527216460c6 ("debuginfod-client.c: + // Skip empty file creation for cancelled queries") (in elfutils + // 0.190), libdebuginfod has a nasty bug that causes it to cache + // a cancelled download as a negative hit. Work around it by + // deleting the cache file. + unlink(cache_sb->str); + return drgn_error_create_os("download cancelled", EINTR, NULL); + } + drgn_log_debuginfod_progress(prog->dbinfo.debuginfod_client, -1, fd); + if (fd >= 0) { + struct drgn_error *err = + drgn_module_try_file(module, path, fd, true); + free(path); + if (err) + return err; + } + return NULL; +} + +static struct drgn_error * +drgn_module_try_supplementary_file_from_debuginfod(struct drgn_module *module, + struct string_builder *cache_sb) +{ + if (drgn_module_wanted_supplementary_debug_file(module, NULL, NULL, + NULL, NULL) + != DRGN_SUPPLEMENTARY_FILE_GNU_DEBUGALTLINK) + return NULL; + const char *gnu_debugaltlink_build_id_str = + module->wanted_supplementary_debug_file->checksum_str; + return drgn_module_try_file_from_debuginfod(module, + gnu_debugaltlink_build_id_str, + true, true, cache_sb); +} + +static struct drgn_error * +drgn_debuginfod_find(struct drgn_module * const *modules, size_t num_modules, + void *arg) +{ + struct drgn_error *err; + struct drgn_program *prog = arg; + + if (!prog->dbinfo.debuginfod_client) { + prog->dbinfo.debuginfod_client = drgn_debuginfod_begin(); + if (!prog->dbinfo.debuginfod_client) { + return drgn_error_create(DRGN_ERROR_OTHER, + "couldn't create debuginfod client session"); + } + drgn_debuginfod_set_progressfn(prog->dbinfo.debuginfod_client, + drgn_debuginfod_progressfn); +#if _ELFUTILS_PREREQ(0, 179) + drgn_debuginfod_set_user_data(prog->dbinfo.debuginfod_client, + prog); +#endif + } + + STRING_BUILDER(sb); + const char *env; + if ((env = getenv("DEBUGINFOD_CACHE_PATH"))) { + if (!string_builder_append(&sb, env)) + return &drgn_enomem; + } else { + env = getenv("HOME") ?: "/"; + if (!string_builder_append(&sb, env) + || !string_builder_append(&sb, "/.debuginfod_client_cache") + || !string_builder_null_terminate(&sb)) + return &drgn_enomem; + struct stat st; + if (stat(sb.str, &st) < 0) { + sb.len = 0; + if ((env = getenv("XDG_CACHE_HOME"))) { + if (!string_builder_append(&sb, env) + || !string_builder_append(&sb, + "/debuginfod_client")) + return &drgn_enomem; + } else if (!string_builder_append(&sb, + getenv("HOME") ?: "/") + || !string_builder_append(&sb, + "/.cache/debuginfod_client")) { + return &drgn_enomem; + } + } + } + + size_t cache_dir_len = sb.len; + for (size_t i = 0; i < num_modules; i++) { + struct drgn_module *module = modules[i]; + const char *build_id_str = + drgn_module_build_id(module, NULL, NULL); + if (!build_id_str) { + drgn_module_try_files_log(module, "can't query debuginfod for"); + continue; + } + + drgn_module_try_files_log(module, "querying debuginfod for"); + + // If we need a supplementary file, try that first. + err = drgn_module_try_supplementary_file_from_debuginfod(module, + &sb); + if (err) + return err; + sb.len = cache_dir_len; + + // If we need the debug file (including if we needed a + // gnu_debugaltlink file and didn't find it), try that next. + if (drgn_module_wants_debug_file(module)) { + uint64_t orig_supplementary_file_generation = + prog->dbinfo.supplementary_file_generation; + err = drgn_module_try_file_from_debuginfod(module, + build_id_str, + true, false, + &sb); + if (err) + return err; + sb.len = cache_dir_len; + // If the wanted supplementary debug file changed, try + // finding it again. + if (drgn_module_wanted_supplementary_debug_file_is_new(module, + orig_supplementary_file_generation)) { + err = drgn_module_try_supplementary_file_from_debuginfod(module, + &sb); + if (err) + return err; + sb.len = cache_dir_len; + } + } + + if (drgn_module_wants_loaded_file(module)) { + err = drgn_module_try_file_from_debuginfod(module, + build_id_str, + false, false, + &sb); + if (err) + return err; + sb.len = cache_dir_len; + } + } + return NULL; +} +#endif // WITH_DEBUGINFOD + +LIBDRGN_PUBLIC +struct drgn_error *drgn_module_try_file(struct drgn_module *module, + const char *path, int fd, bool force) +{ + if (!drgn_module_wants_file(module)) { + drgn_log_debug(module->prog, "%s: ignoring unwanted file %s", + module->name, path); + if (fd >= 0) + close(fd); + return NULL; + } + drgn_module_try_files_log(module, "trying provided file as"); + return drgn_module_try_file_internal(module, path, fd, !force, NULL); +} + +LIBDRGN_PUBLIC +void drgn_module_iterator_destroy(struct drgn_module_iterator *it) +{ + if (it) { + if (it->destroy) + it->destroy(it); + else + free(it); + } +} + +LIBDRGN_PUBLIC struct drgn_program * +drgn_module_iterator_program(const struct drgn_module_iterator *it) +{ + return it->prog; +} + +LIBDRGN_PUBLIC +struct drgn_error *drgn_module_iterator_next(struct drgn_module_iterator *it, + struct drgn_module **ret, + bool *new_ret) +{ + if (!it->next) { + *ret = NULL; + return NULL; + } + struct drgn_error *err = it->next(it, ret, new_ret); + if (err || !*ret) + it->next = NULL; + return err; +} + +struct drgn_created_module_iterator { + struct drgn_module_iterator it; + struct drgn_module_table_iterator table_it; + uint64_t generation; + bool yielded_main; +}; + +static struct drgn_error * +drgn_created_module_iterator_next(struct drgn_module_iterator *_it, + struct drgn_module **ret, + bool *new_ret) +{ + struct drgn_created_module_iterator *it = + container_of(_it, struct drgn_created_module_iterator, it); + struct drgn_debug_info *dbinfo = &it->it.prog->dbinfo; + if (!it->yielded_main) { + it->yielded_main = true; + it->table_it = drgn_module_table_first(&dbinfo->modules); + it->generation = dbinfo->modules_generation; + if (dbinfo->main_module) { + *ret = dbinfo->main_module; + if (new_ret) + *new_ret = false; + return NULL; + } + } + if (it->generation != dbinfo->modules_generation) { + return drgn_error_create(DRGN_ERROR_OTHER, + "modules changed during iteration"); + } + if (it->table_it.entry) { + *ret = *it->table_it.entry; + if (new_ret) + *new_ret = false; + it->table_it = drgn_module_table_next(it->table_it); + } else { + *ret = NULL; + } + return NULL; +} + +LIBDRGN_PUBLIC struct drgn_error * +drgn_created_module_iterator_create(struct drgn_program *prog, + struct drgn_module_iterator **ret) +{ + struct drgn_created_module_iterator *it = calloc(1, sizeof(*it)); + if (!it) + return &drgn_enomem; + drgn_module_iterator_init(&it->it, prog, NULL, + drgn_created_module_iterator_next); + *ret = &it->it; + return NULL; +} - GElf_Ehdr ehdr; - struct core_get_phdr_arg arg; - read_ehdr(&ehdr_buf, &ehdr, &arg.is_64_bit, &arg.bswap); - if (ehdr.e_type == ET_CORE || - ehdr.e_phnum == 0 || - ehdr.e_phentsize != - (arg.is_64_bit ? sizeof(Elf64_Phdr) : sizeof(Elf32_Phdr))) { - ret->ignore = true; +struct drgn_mapped_file { + const char *path; + // Mapped address range containing file offset 0. This is used to find + // the file header. + uint64_t offset0_vaddr, offset0_size; +}; + +static struct drgn_mapped_file *drgn_mapped_file_create(const char *path) +{ + struct drgn_mapped_file *file = calloc(1, sizeof(*file)); + if (file) + file->path = path; + return file; +} + +static void drgn_mapped_file_destroy(struct drgn_mapped_file *file) +{ + free(file); +} + +struct drgn_mapped_file_segment { + uint64_t start; + uint64_t end; + uint64_t file_offset; + struct drgn_mapped_file *file; +}; + +DEFINE_VECTOR(drgn_mapped_file_segment_vector, struct drgn_mapped_file_segment); + +struct drgn_mapped_file_segments { + struct drgn_mapped_file_segment_vector vector; + // Whether the segments are already sorted by start address. This should + // always be true for both /proc/$pid/maps and NT_FILE, but we check and + // sort afterwards if not just in case. + bool sorted; +}; + +#define DRGN_MAPPED_FILE_SEGMENTS_INIT { VECTOR_INIT, true } + +static void drgn_mapped_file_segments_abort(struct drgn_mapped_file_segments *segments) +{ + drgn_mapped_file_segment_vector_deinit(&segments->vector); +} + +static struct drgn_error * +drgn_add_mapped_file_segment(struct drgn_mapped_file_segments *segments, + uint64_t start, uint64_t end, uint64_t file_offset, + struct drgn_mapped_file *file) +{ + assert(start < end); + if (file_offset == 0 && file->offset0_size == 0) { + file->offset0_vaddr = start; + file->offset0_size = end - start; + } + if (!drgn_mapped_file_segment_vector_empty(&segments->vector)) { + struct drgn_mapped_file_segment *last = + drgn_mapped_file_segment_vector_last(&segments->vector); + // If the last segment is from the same file and contiguous with + // this one, merge into that one. + if (file == last->file && start == last->end + && file_offset == last->file_offset + (last->end - last->start)) { + last->end = end; + return NULL; + } + if (start < last->start) + segments->sorted = false; + } + struct drgn_mapped_file_segment *entry = + drgn_mapped_file_segment_vector_append_entry(&segments->vector); + if (!entry) + return &drgn_enomem; + entry->start = start; + entry->end = end; + entry->file_offset = file_offset; + entry->file = file; + return NULL; +} + +enum { + // Yield main module next. + USERSPACE_LOADED_MODULE_ITERATOR_STATE_MAIN, + // Yield vDSO module next. + USERSPACE_LOADED_MODULE_ITERATOR_STATE_VDSO, + // Get first link_map from r_debug next. + USERSPACE_LOADED_MODULE_ITERATOR_STATE_R_DEBUG, + // Yield module from link_map list next. + USERSPACE_LOADED_MODULE_ITERATOR_STATE_LINK_MAP, + // States after this are the same as + // USERSPACE_LOADED_MODULE_ITERATOR_STATE_LINK_MAP but also count how + // many link_map entries we've iterated. +}; + +// Arbitrary limit on the number iterations to make through the link_map list in +// order to avoid getting stuck in a cycle. +static const int MAX_LINK_MAP_LIST_ITERATIONS = 10000; + +struct userspace_loaded_module_iterator { + struct drgn_module_iterator it; + int state; + bool read_main_phdrs; + bool have_main_dyn; + bool have_vdso_dyn; + + struct drgn_mapped_file_segment *file_segments; + size_t num_file_segments; + + uint64_t main_phoff; + uint64_t main_bias; + uint64_t main_dyn_vaddr; + uint64_t main_dyn_memsz; + uint64_t vdso_dyn_vaddr; + uint64_t link_map; + + // Temporary buffer for reading program headers. + void *phdrs_buf; + size_t phdrs_buf_capacity; + + // Temporary buffer for reading segment contents. + void *segment_buf; + size_t segment_buf_capacity; +}; + +static void +userspace_loaded_module_iterator_deinit(struct userspace_loaded_module_iterator *it) +{ + free(it->segment_buf); + free(it->phdrs_buf); + free(it->file_segments); +} + +static inline int drgn_mapped_file_segment_compare(const void *_a, + const void *_b) +{ + const struct drgn_mapped_file_segment *a = _a; + const struct drgn_mapped_file_segment *b = _b; + return (a->start > b->start) - (a->start < b->start); +} + +static void +userspace_loaded_module_iterator_set_file_segments(struct userspace_loaded_module_iterator *it, + struct drgn_mapped_file_segments *segments) +{ + // Don't bother shrinking to fit since this is short-lived. + drgn_mapped_file_segment_vector_steal(&segments->vector, + &it->file_segments, + &it->num_file_segments); + if (!segments->sorted) { + qsort(it->file_segments, it->num_file_segments, + sizeof(it->file_segments[0]), + drgn_mapped_file_segment_compare); + } +} + +static struct drgn_mapped_file_segment * +find_mapped_file_segment(struct userspace_loaded_module_iterator *it, + uint64_t address) +{ + #define less_than_start(a, b) (*(a) < (b)->start) + size_t i = binary_search_gt(it->file_segments, it->num_file_segments, + &address, less_than_start); + #undef less_than_start + if (i == 0 || address >= it->file_segments[i - 1].end) return NULL; + return &it->file_segments[i - 1]; +} + +static struct drgn_error * +userspace_loaded_module_iterator_read_ehdr(struct userspace_loaded_module_iterator *it, + uint64_t address, GElf_Ehdr *ret) +{ + struct drgn_error *err; + struct drgn_program *prog = it->it.prog; + + err = drgn_program_read_memory(prog, ret, address, sizeof(*ret), false); + if (err && err->code == DRGN_ERROR_FAULT) { + drgn_log_debug(prog, + "couldn't read ELF header at 0x%" PRIx64 ": %s", + err->address, err->message); + drgn_error_destroy(err); + return &drgn_not_found; + } else if (err) { + return err; + } + if (memcmp(ret->e_ident, ELFMAG, SELFMAG) != 0) { + drgn_log_debug(prog, "invalid ELF header magic"); + return &drgn_not_found; + } + if (ret->e_ident[EI_CLASS] != + (drgn_platform_is_64_bit(&prog->platform) + ? ELFCLASS64 : ELFCLASS32)) { + drgn_log_debug(prog, + "ELF header class (%u) does not match program", + ret->e_ident[EI_CLASS]); + return &drgn_not_found; } + if (ret->e_ident[EI_DATA] != + (drgn_platform_is_little_endian(&prog->platform) + ? ELFDATA2LSB : ELFDATA2MSB)) { + drgn_log_debug(prog, + "ELF header data encoding (%u) does not match program", + ret->e_ident[EI_DATA]); + return &drgn_not_found; + } +#define visit_elf_ehdr_members(visit_scalar_member, visit_raw_member) do { \ + visit_raw_member(e_ident); \ + visit_scalar_member(e_type); \ + visit_scalar_member(e_machine); \ + visit_scalar_member(e_version); \ + visit_scalar_member(e_entry); \ + visit_scalar_member(e_phoff); \ + visit_scalar_member(e_shoff); \ + visit_scalar_member(e_flags); \ + visit_scalar_member(e_ehsize); \ + visit_scalar_member(e_phentsize); \ + visit_scalar_member(e_phnum); \ + visit_scalar_member(e_shentsize); \ + visit_scalar_member(e_shnum); \ + visit_scalar_member(e_shstrndx); \ +} while (0) + deserialize_struct64_inplace(ret, Elf32_Ehdr, visit_elf_ehdr_members, + drgn_platform_is_64_bit(&prog->platform), + drgn_platform_bswap(&prog->platform)); +#undef visit_elf_ehdr_members + if (ret->e_phentsize != + (drgn_platform_is_64_bit(&prog->platform) + ? sizeof(Elf64_Phdr) : sizeof(Elf32_Phdr))) { + drgn_log_debug(prog, + "ELF program header entry size (%u) does not match class", + ret->e_phentsize); + return &drgn_not_found; + } + return NULL; +} - if (ehdr.e_phnum > SIZE_MAX / ehdr.e_phentsize || - !alloc_or_reuse(&core->phdr_buf, &core->phdr_buf_capacity, - ehdr.e_phnum * ehdr.e_phentsize)) +static struct drgn_error * +userspace_loaded_module_iterator_read_phdrs(struct userspace_loaded_module_iterator *it, + uint64_t address, uint16_t phnum) +{ + struct drgn_error *err; + struct drgn_program *prog = it->it.prog; + uint32_t phentsize = + (drgn_platform_is_64_bit(&prog->platform) + ? sizeof(Elf64_Phdr) : sizeof(Elf32_Phdr)); + uint32_t phdrs_size = (uint32_t)phnum * phentsize; + if (phdrs_size > MAX_MEMORY_READ_FOR_DEBUG_INFO) { + drgn_log_debug(prog, + "program header table is unreasonably large (%" PRIu32 " bytes); ignoring", + phdrs_size); + return &drgn_not_found; + } + if (!alloc_or_reuse(&it->phdrs_buf, &it->phdrs_buf_capacity, + phdrs_size)) return &drgn_enomem; + err = drgn_program_read_memory(prog, it->phdrs_buf, address, phdrs_size, + false); + if (err && err->code == DRGN_ERROR_FAULT) { + drgn_log_debug(prog, + "couldn't read program header table at 0x%" PRIx64 ": %s", + err->address, err->message); + drgn_error_destroy(err); + return &drgn_not_found; + } + return err; +} + +static void +userspace_loaded_module_iterator_phdr(struct userspace_loaded_module_iterator *it, + size_t i, GElf_Phdr *ret) +{ + struct drgn_program *prog = it->it.prog; + size_t phentsize = + (drgn_platform_is_64_bit(&prog->platform) + ? sizeof(Elf64_Phdr) : sizeof(Elf32_Phdr)); +#define visit_phdr_members(visit_scalar_member, visit_raw_member) do { \ + visit_scalar_member(p_type); \ + visit_scalar_member(p_flags); \ + visit_scalar_member(p_offset); \ + visit_scalar_member(p_vaddr); \ + visit_scalar_member(p_paddr); \ + visit_scalar_member(p_filesz); \ + visit_scalar_member(p_memsz); \ + visit_scalar_member(p_align); \ +} while (0) + deserialize_struct64(ret, Elf32_Phdr, visit_phdr_members, + (char *)it->phdrs_buf + i * phentsize, + drgn_platform_is_64_bit(&prog->platform), + drgn_platform_bswap(&prog->platform)); +#undef visit_phdr_members +} - /* - * Check whether the mapped segment containing the file header also - * contains the program headers. This seems to be the case in practice. - */ - uint64_t ehdr_segment_file_end = - (ehdr_segment->file_offset + - (ehdr_segment->end - ehdr_segment->start)); - if (ehdr_segment_file_end < ehdr.e_phoff || - ehdr_segment_file_end - ehdr.e_phoff < - ehdr.e_phnum * ehdr.e_phentsize) +static struct drgn_error * +userspace_loaded_module_iterator_read_dynamic(struct userspace_loaded_module_iterator *it, + uint64_t address, uint64_t size, + size_t *num_dyn_ret) +{ + struct drgn_error *err; + struct drgn_program *prog = it->it.prog; + + if (size > MAX_MEMORY_READ_FOR_DEBUG_INFO) { + drgn_log_debug(prog, + "dynamic section is unreasonably large (%" PRIu64 " bytes); ignoring", + size); + return &drgn_not_found; + } + size_t dyn_size = + (drgn_platform_is_64_bit(&prog->platform) + ? sizeof(Elf64_Dyn) : sizeof(Elf32_Dyn)); + uint64_t num_dyn = size / dyn_size; + *num_dyn_ret = num_dyn; + if (num_dyn == 0) return NULL; - err = drgn_program_read_memory(prog, core->phdr_buf, - ehdr_segment->start + ehdr.e_phoff, - ehdr.e_phnum * ehdr.e_phentsize, false); - if (err) { - if (err->code == DRGN_ERROR_FAULT) { - drgn_error_destroy(err); - err = NULL; - } - return err; + if (!alloc_or_reuse(&it->segment_buf, &it->segment_buf_capacity, + num_dyn * dyn_size)) + return &drgn_enomem; + err = drgn_program_read_memory(prog, it->segment_buf, address, + num_dyn * dyn_size, false); + if (err && err->code == DRGN_ERROR_FAULT) { + drgn_log_debug(prog, + "couldn't read dynamic section at 0x%" PRIx64 ": %s", + err->address, err->message); + drgn_error_destroy(err); + return &drgn_not_found; } - arg.phdr_buf = core->phdr_buf; - - /* - * In theory, if the program has a huge number of program headers, they - * may not all be dumped. However, the largest binary I was able to find - * still had all program headers within 1k. - * - * It'd be more reliable to determine the bias based on the headers that - * were saved, use that to read the build ID, use that to find the ELF - * file, and then determine the address range directly from the ELF - * file. However, we need the address range to report the build ID to - * libdwfl, so we do it this way. - */ - uint64_t bias; - err = userspace_core_elf_address_range(ehdr.e_type, ehdr.e_phnum, - core_get_phdr, &arg, segments, - num_segments, ehdr_segment, - &bias, &ret->start, &ret->end); - if (err) + return err; +} + +static void +userspace_loaded_module_iterator_dyn(struct userspace_loaded_module_iterator *it, + size_t i, GElf_Dyn *ret) +{ + struct drgn_program *prog = it->it.prog; + size_t dyn_size = + (drgn_platform_is_64_bit(&prog->platform) + ? sizeof(Elf64_Dyn) : sizeof(Elf32_Dyn)); +#define visit_elf_dyn_members(visit_scalar_member, visit_raw_member) do { \ + visit_scalar_member(d_tag); \ + visit_scalar_member(d_un.d_val); \ +} while (0) + deserialize_struct64(ret, Elf32_Dyn, visit_elf_dyn_members, + (char *)it->segment_buf + i * dyn_size, + drgn_platform_is_64_bit(&prog->platform), + drgn_platform_bswap(&prog->platform)); +#undef visit_elf_dyn_members +} + +static struct drgn_error * +userspace_loaded_module_iterator_read_main_phdrs(struct userspace_loaded_module_iterator *it) +{ + struct drgn_error *err; + struct drgn_program *prog = it->it.prog; + + // The main bias is the difference between AT_PHDR and the virtual + // address of the program headers in the ELF file. We determine the + // latter by finding the PT_LOAD segment containing e_phoff. We would + // use PT_PHDR instead, but static binaries usually don't have it, and + // we can't assume a bias of 0 for static PIE binaries. + // + // If we couldn't find the file offset of the program headers, we can't + // find anything else. + if (it->main_phoff == 0) + return NULL; + + drgn_log_debug(prog, "reading program header table from AT_PHDR"); + + err = userspace_loaded_module_iterator_read_phdrs(it, + prog->auxv.at_phdr, + prog->auxv.at_phnum); + if (err == &drgn_not_found) + return NULL; + else if (err) return err; - if (ret->start >= ret->end) { - ret->ignore = true; + + // Silence -Wmaybe-uninitialized false positives on dyn_vaddr and + // dyn_memsz last seen with GCC 9. + uint64_t phdr_vaddr, dyn_vaddr = 0, dyn_memsz = 0; + bool have_phdr_vaddr = false, have_dyn = false; + for (uint16_t i = 0; i < prog->auxv.at_phnum; i++) { + GElf_Phdr phdr; + userspace_loaded_module_iterator_phdr(it, i, &phdr); + if (phdr.p_type == PT_LOAD && phdr.p_offset <= it->main_phoff + && it->main_phoff < phdr.p_offset + phdr.p_filesz) { + drgn_log_debug(prog, + "found PT_LOAD containing program headers with p_vaddr 0x%" PRIx64 + " and p_offset 0x%" PRIx64, + phdr.p_vaddr, phdr.p_offset); + phdr_vaddr = it->main_phoff - phdr.p_offset + phdr.p_vaddr; + have_phdr_vaddr = true; + } else if (phdr.p_type == PT_DYNAMIC) { + drgn_log_debug(prog, + "found PT_DYNAMIC with p_vaddr 0x%" PRIx64 + " and p_memsz 0x%" PRIx64, + phdr.p_vaddr, phdr.p_memsz); + have_dyn = true; + dyn_vaddr = phdr.p_vaddr; + dyn_memsz = phdr.p_memsz; + } + } + if (have_phdr_vaddr) { + it->main_bias = prog->auxv.at_phdr - phdr_vaddr; + drgn_log_debug(prog, "main bias is 0x%" PRIx64, it->main_bias); + } else { + drgn_log_debug(prog, + "didn't find PT_LOAD containing program headers"); return NULL; } - ret->have_address_range = true; + if (have_dyn) { + it->have_main_dyn = true; + it->main_dyn_vaddr = dyn_vaddr + it->main_bias; + it->main_dyn_memsz = dyn_memsz; + drgn_log_debug(prog, "main dynamic section is at 0x%" PRIx64, + it->main_dyn_vaddr); + } else { + drgn_log_debug(prog, + "didn't find PT_DYNAMIC program header; probably statically linked"); + } + it->read_main_phdrs = true; + return NULL; +} + +static struct drgn_error * +identify_module_from_phdrs(struct userspace_loaded_module_iterator *it, + struct drgn_module *module, size_t phnum, + uint64_t bias) +{ + struct drgn_error *err; + struct drgn_program *prog = it->it.prog; - for (uint16_t i = 0; i < ehdr.e_phnum; i++) { + uint64_t start = UINT64_MAX, end = 0; + for (size_t i = 0; i < phnum; i++) { GElf_Phdr phdr; - core_get_phdr(&arg, i, &phdr); - if (phdr.p_type == PT_NOTE) { - if (phdr.p_filesz > SIZE_MAX || - !alloc_or_reuse(&core->segment_buf, - &core->segment_buf_capacity, - phdr.p_filesz)) + userspace_loaded_module_iterator_phdr(it, i, &phdr); + if (phdr.p_type == PT_LOAD) { + // Like elf_address_range_from_min_and_max_phdr(). + start = min(start, phdr.p_vaddr + bias); + end = max(end, phdr.p_vaddr + phdr.p_memsz + bias); + } else if (phdr.p_type == PT_NOTE + && module->build_id_len == 0) { + uint64_t note_size = min(phdr.p_filesz, phdr.p_memsz); + if (!note_size) + continue; + if (note_size > MAX_MEMORY_READ_FOR_DEBUG_INFO) { + drgn_log_debug(prog, + "note is unreasonably large (%" PRIu64 " bytes); ignoring", + note_size); + continue; + } + if (!alloc_or_reuse(&it->segment_buf, + &it->segment_buf_capacity, + note_size)) return &drgn_enomem; - err = drgn_program_read_memory(prog, core->segment_buf, + err = drgn_program_read_memory(prog, it->segment_buf, phdr.p_vaddr + bias, - phdr.p_filesz, false); - if (err) { - if (err->code == DRGN_ERROR_FAULT) { - drgn_error_destroy(err); - continue; - } else { + note_size, false); + if (err && err->code == DRGN_ERROR_FAULT) { + drgn_log_debug(prog, + "couldn't read note at 0x%" PRIx64 ": %s" + "; ignoring", + err->address, err->message); + drgn_error_destroy(err); + continue; + } else if (err) { + return err; + } + const void *build_id; + size_t build_id_len = + parse_gnu_build_id_from_notes(it->segment_buf, + note_size, + phdr.p_align == 8 ? + 8 : 4, + drgn_platform_bswap(&prog->platform), + &build_id); + if (build_id_len > 0) { + err = drgn_module_set_build_id(module, build_id, + build_id_len); + if (err) return err; - } + drgn_log_debug(prog, + "found build ID %s in note at 0x%" PRIx64, + module->build_id_str, + phdr.p_vaddr + bias + + ((char *)build_id + - (char *)it->segment_buf)); } - ret->build_id_len = - parse_gnu_build_id_from_notes(core->segment_buf, - phdr.p_filesz, - phdr.p_align == 8 - ? 8 : 4, - arg.bswap, - &ret->build_id); - if (ret->build_id_len) - break; } } - return NULL; -} + if (module->build_id_len == 0) { + drgn_log_debug(prog, + "couldn't find build ID from mapped program headers"); + } + if (start < end) { + err = drgn_module_set_address_range(module, start, end); + if (err) + return err; + drgn_log_debug(prog, + "got address range 0x%" PRIx64 "-0x%" PRIx64 " from mapped program headers", + start, end); + } else { + drgn_log_debug(prog, + "couldn't find address range from mapped program headers"); + } + return NULL; +} + +static struct drgn_error * +userspace_loaded_module_iterator_yield_main(struct userspace_loaded_module_iterator *it, + struct drgn_module **ret, + bool *new_ret) +{ + struct drgn_error *err; + struct drgn_program *prog = it->it.prog; + + struct drgn_mapped_file_segment *segment = + find_mapped_file_segment(it, prog->auxv.at_phdr); + if (segment) { + // We don't need to read the file header to get e_phoff. Instead, + // determine it from the file mapping. + it->main_phoff = + segment->file_offset + (prog->auxv.at_phdr - segment->start); + drgn_log_debug(prog, + "AT_PHDR is mapped from file %s at offset 0x%" PRIx64, + segment->file->path, it->main_phoff); + } else { + drgn_log_debug(prog, + "couldn't find mapped file segment containing AT_PHDR"); + } + + _cleanup_(drgn_module_deletep) struct drgn_module *module = NULL; + bool new; + err = drgn_module_find_or_create_main(prog, + segment ? segment->file->path : "", + &module, &new); + if (err) + return err; + if (!new) { + *ret = no_cleanup_ptr(module); + if (new_ret) + *new_ret = new; + return NULL; + } + err = userspace_loaded_module_iterator_read_main_phdrs(it); + if (err) + return err; + if (it->read_main_phdrs) { + err = identify_module_from_phdrs(it, module, + prog->auxv.at_phnum, + it->main_bias); + if (err) + return err; + } + *ret = no_cleanup_ptr(module); + if (new_ret) + *new_ret = new; + return NULL; +} + +static struct drgn_error * +userspace_loaded_module_iterator_yield_vdso(struct userspace_loaded_module_iterator *it, + struct drgn_module **ret, + bool *new_ret) +{ + struct drgn_error *err; + struct drgn_program *prog = it->it.prog; + + if (!prog->auxv.at_sysinfo_ehdr) { + drgn_log_debug(prog, "no vDSO"); +no_vdso: + *ret = NULL; + return NULL; + } + + drgn_log_debug(prog, "reading vDSO ELF header from AT_SYSINFO_EHDR"); + GElf_Ehdr ehdr; + err = userspace_loaded_module_iterator_read_ehdr(it, + prog->auxv.at_sysinfo_ehdr, + &ehdr); + if (err == &drgn_not_found) + goto no_vdso; + else if (err) + return err; + + drgn_log_debug(prog, + "reading %" PRIu16 " program headers at 0x%" PRIx64, + ehdr.e_phnum, prog->auxv.at_sysinfo_ehdr + ehdr.e_phoff); + + // It is effectively part of the ABI that the vDSO program headers are + // mapped at AT_SYSINFO_EHDR + e_phoff (see the Linux kernel's reference + // vDSO parser: vdso_init_from_sysinfo_ehdr() in + // tools/testing/selftests/vDSO/parse_vdso.c, glibc: setup_vdso() in + // elf/setup-vdso.h, and musl: __vdsosym() in src/internal/vdso.c). + err = userspace_loaded_module_iterator_read_phdrs(it, + prog->auxv.at_sysinfo_ehdr + ehdr.e_phoff, + ehdr.e_phnum); + if (err == &drgn_not_found) + goto no_vdso; + else if (err) + return err; + + // This is based on the Linux kernel's reference vDSO parser. + uint64_t bias = prog->auxv.at_sysinfo_ehdr; + // Silence -Wmaybe-uninitialized false positives on dyn_vaddr and + // dyn_memsz last seen with GCC 12. + uint64_t dyn_vaddr = 0, dyn_memsz = 0; + bool have_load = false, have_dyn = false; + for (size_t i = 0; i < ehdr.e_phnum; i++) { + GElf_Phdr phdr; + userspace_loaded_module_iterator_phdr(it, i, &phdr); + if (phdr.p_type == PT_LOAD && !have_load) { + drgn_log_debug(prog, + "found PT_LOAD with p_offset 0x%" PRIx64 + " and p_vaddr 0x%" PRIx64, + phdr.p_offset, phdr.p_vaddr); + have_load = true; + bias = prog->auxv.at_sysinfo_ehdr + phdr.p_offset - phdr.p_vaddr; + } else if (phdr.p_type == PT_DYNAMIC) { + drgn_log_debug(prog, + "found PT_DYNAMIC with p_offset 0x%" PRIx64 + " and p_memsz 0x%" PRIx64, + phdr.p_offset, phdr.p_memsz); + dyn_vaddr = prog->auxv.at_sysinfo_ehdr + phdr.p_offset; + dyn_memsz = phdr.p_memsz; + have_dyn = true; + } + } + if (!have_load) { + drgn_log_warning(prog, + "can't find vDSO: " + "no PT_LOAD header in vDSO program headers"); + goto no_vdso; + } + drgn_log_debug(prog, "vDSO bias is 0x%" PRIx64, bias); + if (!have_dyn) { + drgn_log_warning(prog, + "can't find vDSO: " + "no PT_DYNAMIC header in vDSO program headers"); + goto no_vdso; + } + it->vdso_dyn_vaddr = dyn_vaddr; + it->have_vdso_dyn = true; + + drgn_log_debug(prog, "reading vDSO dynamic section at 0x%" PRIx64, + dyn_vaddr); + size_t num_dyn; + err = userspace_loaded_module_iterator_read_dynamic(it, dyn_vaddr, + dyn_memsz, + &num_dyn); + if (err == &drgn_not_found) + goto no_vdso; + else if (err) + return err; + + // Silence -Wmaybe-uninitialized false positives on dt_strtab and + // dt_soname last seen with GCC 12. + uint64_t dt_strtab = 0, dt_soname = 0; + bool have_dt_strtab = false, have_dt_soname = false; + for (size_t i = 0; i < num_dyn; i++) { + GElf_Dyn dyn; + userspace_loaded_module_iterator_dyn(it, i, &dyn); + if (dyn.d_tag == DT_STRTAB) { + dt_strtab = dyn.d_un.d_ptr; + have_dt_strtab = true; + drgn_log_debug(prog, "found DT_STRTAB 0x%" PRIx64, + dt_strtab); + } else if (dyn.d_tag == DT_SONAME) { + dt_soname = dyn.d_un.d_val; + have_dt_soname = true; + drgn_log_debug(prog, "found DT_SONAME 0x%" PRIx64, + dt_soname); + } else if (dyn.d_tag == DT_NULL) { + break; + } + } + if (!have_dt_strtab || !have_dt_soname) { + drgn_log_warning(prog, + "can't find vDSO: " + "no %s%s%s entr%s in vDSO dynamic section", + have_dt_strtab ? "" : "DT_STRTAB", + have_dt_strtab || have_dt_soname ? "" : " or ", + have_dt_soname ? "" : "DT_SONAME", + have_dt_strtab || have_dt_soname ? "y" : "ies"); + goto no_vdso; + } + + _cleanup_free_ char *name = NULL; + err = drgn_program_read_c_string(prog, dt_strtab + bias + dt_soname, + false, SIZE_MAX, &name); + if (err && err->code == DRGN_ERROR_FAULT) { + drgn_log_warning(prog, + "can't find vDSO: " + "couldn't read soname at 0x%" PRIx64 ": %s", + err->address, err->message); + drgn_error_destroy(err); + goto no_vdso; + } else if (err) { + return err; + } + drgn_log_debug(prog, "read vDSO soname \"%s\"", name); + + _cleanup_(drgn_module_deletep) struct drgn_module *module = NULL; + bool new; + err = drgn_module_find_or_create_vdso(prog, name, dyn_vaddr, &module, + &new); + if (err) + return err; + if (!new) { + *ret = no_cleanup_ptr(module); + if (new_ret) + *new_ret = new; + return NULL; + } + + err = identify_module_from_phdrs(it, module, ehdr.e_phnum, bias); + if (err) + return err; + *ret = no_cleanup_ptr(module); + if (new_ret) + *new_ret = new; + return NULL; +} + +#define read_struct64(prog, struct64p, address, type32, visit_members) \ + read_struct64_impl(prog, struct64p, address, type32, visit_members, \ + PP_UNIQUE(prog), PP_UNIQUE(struct64p), \ + PP_UNIQUE(is_64_bit), PP_UNIQUE(err)) +#define read_struct64_impl(prog, struct64p, address, type32, visit_members, \ + unique_prog, unique_struct64, unique_is_64_bit, \ + unique_err) ({ \ + struct drgn_program *unique_prog = (prog); \ + __auto_type unique_struct64p = (struct64p); \ + static_assert(sizeof(*unique_struct64p) >= sizeof(type32), \ + "64-bit type is smaller than 32-bit type"); \ + const bool unique_is_64_bit = \ + drgn_platform_is_64_bit(&unique_prog->platform); \ + struct drgn_error *unique_err = \ + drgn_program_read_memory(unique_prog, unique_struct64p, \ + (address), \ + unique_is_64_bit \ + ? sizeof(*unique_struct64p) \ + : sizeof(type32), false); \ + if (!unique_err) { \ + deserialize_struct64_inplace(unique_struct64p, type32, \ + visit_members, unique_is_64_bit, \ + drgn_platform_bswap(&unique_prog->platform));\ + } \ + unique_err; \ +}) + +static struct drgn_error * +userspace_get_link_map(struct userspace_loaded_module_iterator *it) +{ + struct drgn_error *err; + struct drgn_program *prog = it->it.prog; + + if (!it->read_main_phdrs) { + err = userspace_loaded_module_iterator_read_main_phdrs(it); + if (err) + return err; + } + if (!it->have_main_dyn) + return NULL; + + drgn_log_debug(prog, "reading main dynamic section"); + size_t num_dyn; + err = userspace_loaded_module_iterator_read_dynamic(it, + it->main_dyn_vaddr, + it->main_dyn_memsz, + &num_dyn); + if (err == &drgn_not_found) { + drgn_log_warning(prog, + "can't find shared libraries: " + "couldn't read main dynamic section"); + return NULL; + } else if (err) { + return err; + } + + GElf_Dyn dyn; + size_t i; + for (i = 0; i < num_dyn; i++) { + userspace_loaded_module_iterator_dyn(it, i, &dyn); + if (dyn.d_tag == DT_NULL) { + i = num_dyn; + break; + } + if (dyn.d_tag == DT_DEBUG) { + drgn_log_debug(prog, "found DT_DEBUG 0x%" PRIx64, + dyn.d_un.d_ptr); + break; + } + } + if (i >= num_dyn) { + drgn_log_warning(prog, + "can't find shared libraries: " + "no DT_DEBUG entry in main dynamic section"); + return NULL; + } + + struct drgn_r_debug { + int32_t r_version; + alignas(8) uint64_t r_map; + } r_debug; + struct drgn_r_debug32 { + int32_t r_version; + uint32_t r_map; + }; +#define visit_r_debug_members(visit_scalar_member, visit_raw_member) do { \ + visit_scalar_member(r_version); \ + visit_scalar_member(r_map); \ +} while (0) + err = read_struct64(prog, &r_debug, dyn.d_un.d_ptr, + struct drgn_r_debug32, visit_r_debug_members); +#undef visit_r_debug_members + if (err && err->code == DRGN_ERROR_FAULT) { + // Note: musl doesn't update DT_DEBUG for static PIE binaries + // compiled with GCC (as of musl v1.2.3 and GCC 13), so that + // case is known to fail here. + drgn_log_warning(prog, + "can't find shared libraries: " + "couldn't read r_debug at 0x%" PRIx64 ": %s", + err->address, err->message); + drgn_error_destroy(err); + return NULL; + } else if (err) { + return err; + } + drgn_log_debug(prog, + "read r_debug = { .r_version = %" PRId32 ", .r_map = 0x%" PRIx64 " }", + r_debug.r_version, r_debug.r_map); + + if (r_debug.r_version < 1) { + drgn_log_warning(prog, + "can't find shared libraries: " + "invalid r_debug.r_version %" PRId32, + r_debug.r_version); + return NULL; + } + it->link_map = r_debug.r_map; + return NULL; +} + +static struct drgn_error * +identify_module_from_link_map(struct userspace_loaded_module_iterator *it, + struct drgn_module *module, + struct drgn_mapped_file *file, uint64_t l_addr) +{ + struct drgn_error *err; + struct drgn_program *prog = it->it.prog; + + // Even if it is a 32-bit file, segments should be at least a page, so + // we should be able to read the 64-bit size. + if (file->offset0_size < sizeof(Elf64_Ehdr)) { + drgn_log_debug(prog, "didn't find mapped ELF header"); + return NULL; + } + + drgn_log_debug(prog, "reading ELF header at 0x%" PRIx64, + file->offset0_vaddr); + GElf_Ehdr ehdr; + err = userspace_loaded_module_iterator_read_ehdr(it, + file->offset0_vaddr, + &ehdr); + if (err == &drgn_not_found) + return NULL; + else if (err) + return err; + + drgn_log_debug(prog, + "reading %" PRIu16 " program headers from 0x%" PRIx64, + ehdr.e_phnum, file->offset0_vaddr + ehdr.e_phoff); + // e_phnum and e_phentsize are uint16_t, so this can't overflow. + uint32_t phdrs_size = + (uint32_t)ehdr.e_phnum * (uint32_t)ehdr.e_phentsize; + if (ehdr.e_phoff > file->offset0_size || + phdrs_size > file->offset0_size - ehdr.e_phoff) { + drgn_log_debug(prog, + "program header table is not mapped with ELF header"); + return NULL; + } + err = userspace_loaded_module_iterator_read_phdrs(it, + file->offset0_vaddr + ehdr.e_phoff, + ehdr.e_phnum); + if (err == &drgn_not_found) + return NULL; + else if (err) + return err; + + return identify_module_from_phdrs(it, module, ehdr.e_phnum, l_addr); +} + +// This is the public definition of struct link_map from glibc's link.h: +// +// struct link_map +// { +// /* These first few members are part of the protocol with the debugger. +// This is the same format used in SVR4. */ +// +// ElfW(Addr) l_addr; /* Difference between the address in the ELF +// file and the addresses in memory. */ +// char *l_name; /* Absolute file name object was found in. */ +// ElfW(Dyn) *l_ld; /* Dynamic section of the shared object. */ +// struct link_map *l_next, *l_prev; /* Chain of loaded objects. */ +// }; +// +// We don't need l_prev, so we exclude it from our definition. +struct drgn_link_map { + uint64_t l_addr; + uint64_t l_name; + uint64_t l_ld; + uint64_t l_next; +}; +struct drgn_link_map32 { + uint32_t l_addr; + uint32_t l_name; + uint32_t l_ld; + uint32_t l_next; +}; + +static struct drgn_error * +userspace_next_link_map(struct userspace_loaded_module_iterator *it, + struct drgn_link_map *ret, char **name_ret) +{ + struct drgn_error *err; + struct drgn_program *prog = it->it.prog; + + if (!it->link_map) { + drgn_log_debug(prog, "found end of link_map list"); + return &drgn_stop; + } -static struct drgn_error *elf_file_get_phdr(void *arg, size_t i, - GElf_Phdr *phdr) -{ - if (!gelf_getphdr(arg, i, phdr)) - return drgn_error_libelf(); + if (it->state + >= USERSPACE_LOADED_MODULE_ITERATOR_STATE_LINK_MAP + + MAX_LINK_MAP_LIST_ITERATIONS) { + drgn_log_warning(prog, + "can't find remaining shared libraries: " + "too many entries or cycle in link_map list"); + return &drgn_stop; + } + it->state++; + +#define visit_link_map_members(visit_scalar_member, visit_raw_member) do { \ + visit_scalar_member(l_addr); \ + visit_scalar_member(l_name); \ + visit_scalar_member(l_ld); \ + visit_scalar_member(l_next); \ +} while (0) + err = read_struct64(prog, ret, it->link_map, struct drgn_link_map32, + visit_link_map_members); +#undef visit_link_map_members + if (err && err->code == DRGN_ERROR_FAULT) { + drgn_log_warning(prog, + "can't find remaining shared libraries: " + "couldn't read next link_map at 0x%" PRIx64 ": %s", + err->address, err->message); + drgn_error_destroy(err); + return &drgn_stop; + } else if (err) { + return err; + } + + it->link_map = ret->l_next; + + err = drgn_program_read_c_string(prog, ret->l_name, false, SIZE_MAX, + name_ret); + if (err && err->code == DRGN_ERROR_FAULT) + *name_ret = NULL; + else if (err) + return err; + drgn_log_debug(prog, + "read link_map = { .l_addr = 0x%" PRIx64 ", .l_name = 0x%" PRIx64 "%s%s%s, .l_ld = 0x%" PRIx64 ", .l_next = 0x%" PRIx64 " }", + ret->l_addr, ret->l_name, *name_ret ? " = \"" : "", + *name_ret ? *name_ret : "", *name_ret ? "\"" : "", + ret->l_ld, ret->l_next); + if (err) { + drgn_log_debug(prog, + "couldn't read l_name at 0x%" PRIx64 ": %s" + "; skipping", + err->address, err->message); + drgn_error_destroy(err); + } return NULL; } static struct drgn_error * -userspace_core_maybe_report_file(struct drgn_debug_info_load_state *load, - struct userspace_core_report_state *core, - const char *path, - const struct drgn_mapped_file_segment *segments, - size_t num_segments) +yield_from_link_map(struct userspace_loaded_module_iterator *it, + struct drgn_module **ret, bool *new_ret) { struct drgn_error *err; - struct drgn_program *prog = load->dbinfo->prog; - for (size_t ehdr_idx = 0; ehdr_idx < num_segments; ehdr_idx++) { - const struct drgn_mapped_file_segment *ehdr_segment = - &segments[ehdr_idx]; - /* - * There should always be a full page mapped, so even if it's a - * 32-bit file, we can read the 64-bit size. - */ - if (ehdr_segment->file_offset != 0 || - ehdr_segment->end - ehdr_segment->start < sizeof(Elf64_Ehdr)) + struct drgn_program *prog = it->it.prog; + + for (;;) { + struct drgn_link_map link_map; + _cleanup_free_ char *name = NULL; + err = userspace_next_link_map(it, &link_map, &name); + if (err == &drgn_stop) { + *ret = NULL; + return NULL; + } else if (err) { + return err; + } + + if (link_map.l_ld == it->main_dyn_vaddr) { + drgn_log_debug(prog, + "l_ld matches main dynamic section; skipping"); + continue; + } + if (it->have_vdso_dyn && link_map.l_ld == it->vdso_dyn_vaddr) { + drgn_log_debug(prog, + "l_ld matches vDSO dynamic section; skipping"); + continue; + } + if (!name) continue; - /* - * This logic is complicated because we're dealing with two data - * sources that we can't completely trust: the memory in the - * core dump and the file at the path found in the core dump. - * - * First, we try to identify the mapped file contents in the - * core dump. Ideally, this will find a build ID. However, this - * can fail for a few reasons: - * - * 1. The file is not an ELF file. - * 2. The ELF file is not an executable or library. - * 3. The ELF file does not have a build ID. - * 4. The file header was not dumped to the core dump, in which - * case we can't tell whether this is an ELF file. Dumping - * the first page of an executable file has been the default - * behavior since Linux kernel commit 895021552d6f - * ("coredump: default - * CONFIG_CORE_DUMP_DEFAULT_ELF_HEADERS=y") (in v2.6.37), but - * it can be disabled at kernel build time or toggled at - * runtime. - * 5. The build ID or the necessary ELF metadata were not dumped - * in the core dump. This can happen if the necessary program - * headers or note segment were not in the first page of the - * file. - * 6. The file is mapped but not actually loaded into the - * program (e.g., if the program is a tool like a profiler or - * a debugger that mmaps binaries [like drgn itself!]). - * - * In cases 1 and 2, we can simply ignore the file. In cases - * 3-5, we blindly trust the path in the core dump. We can - * sometimes detect case 6 in - * userspace_core_elf_address_range(). - * - * There is also the possibility that the program modified or - * corrupted the ELF metadata in memory (more likely if the file - * was explicitly mmap'd, since the metadata will usually be - * read-only if it was loaded properly). We don't deal with that - * yet. - */ - struct userspace_core_identified_file identity = {}; - err = userspace_core_identify_file(prog, core, segments, - num_segments, ehdr_segment, - &identity); + _cleanup_(drgn_module_deletep) struct drgn_module *module = NULL; + bool new; + err = drgn_module_find_or_create_shared_library(prog, name, + link_map.l_ld, + &module, &new); if (err) return err; - if (identity.ignore) - continue; - -#define CLEAR_ELF() do { \ - elf = NULL; \ - fd = -1; \ -} while (0) -#define CLOSE_ELF() do { \ - elf_end(elf); \ - close(fd); \ - CLEAR_ELF(); \ -} while (0) - int fd; - Elf *elf; - /* - * There are a few things that can go wrong here: - * - * 1. The path no longer exists. - * 2. The path refers to a different ELF file than was in the - * core dump. - * 3. The path refers to something which isn't a valid ELF file. - */ - err = open_elf_file(path, &fd, &elf); - if (err) { - drgn_error_destroy(err); - CLEAR_ELF(); - } else if (identity.build_id_len > 0) { - if (!build_id_matches(elf, identity.build_id, - identity.build_id_len)) - CLOSE_ELF(); - } - - if (elf && !identity.have_address_range) { - GElf_Ehdr ehdr_mem, *ehdr; - size_t phnum; - if ((ehdr = gelf_getehdr(elf, &ehdr_mem)) && - (elf_getphdrnum(elf, &phnum) == 0)) { - uint64_t bias; - err = userspace_core_elf_address_range(ehdr->e_type, - phnum, - elf_file_get_phdr, - elf, - segments, - num_segments, - ehdr_segment, - &bias, - &identity.start, - &identity.end); - if (err || identity.start >= identity.end) { - drgn_error_destroy(err); - CLOSE_ELF(); - } else { - identity.have_address_range = true; - } - } else { - CLOSE_ELF(); - } + if (!new) { + *ret = no_cleanup_ptr(module); + if (new_ret) + *new_ret = new; + return NULL; } - if (elf) { - err = drgn_debug_info_report_elf(load, path, fd, elf, - identity.start, - identity.end, NULL, - NULL); + struct drgn_mapped_file_segment *segment = + find_mapped_file_segment(it, link_map.l_ld); + if (segment) { + err = identify_module_from_link_map(it, module, + segment->file, + link_map.l_addr); if (err) return err; } else { - if (!identity.have_address_range) - identity.start = identity.end = 0; - Dwfl_Module *dwfl_module = - dwfl_report_module(load->dbinfo->dwfl, path, - identity.start, - identity.end); - if (!dwfl_module) - return drgn_error_libdwfl(); - if (identity.build_id_len > 0 && - dwfl_module_report_build_id(dwfl_module, - identity.build_id, - identity.build_id_len, - 0)) - return drgn_error_libdwfl(); - } -#undef CLOSE_ELF -#undef CLEAR_ELF + drgn_log_debug(prog, + "couldn't find mapped file segment containing l_ld"); + } + *ret = no_cleanup_ptr(module); + if (new_ret) + *new_ret = new; + return NULL; } - return NULL; } static struct drgn_error * -userspace_core_report_mapped_files(struct drgn_debug_info_load_state *load, - struct userspace_core_report_state *core) +userspace_loaded_module_iterator_next(struct drgn_module_iterator *_it, + struct drgn_module **ret, + bool *new_ret) { - struct drgn_error *err; - for (struct drgn_mapped_files_iterator it = - drgn_mapped_files_first(&core->files); - it.entry; it = drgn_mapped_files_next(it)) { - err = userspace_core_maybe_report_file(load, core, - it.entry->key, - drgn_mapped_file_segment_vector_begin(&it.entry->value), - drgn_mapped_file_segment_vector_size(&it.entry->value)); + struct userspace_loaded_module_iterator *it = + container_of(_it, struct userspace_loaded_module_iterator, it); + switch (it->state) { + case USERSPACE_LOADED_MODULE_ITERATOR_STATE_MAIN: + err = drgn_program_cache_auxv(it->it.prog); + if (err) + return err; + it->state = USERSPACE_LOADED_MODULE_ITERATOR_STATE_VDSO; + return userspace_loaded_module_iterator_yield_main(it, ret, + new_ret); + case USERSPACE_LOADED_MODULE_ITERATOR_STATE_VDSO: + it->state = USERSPACE_LOADED_MODULE_ITERATOR_STATE_R_DEBUG; + err = userspace_loaded_module_iterator_yield_vdso(it, ret, + new_ret); + if (err || *ret) + return err; + fallthrough; + case USERSPACE_LOADED_MODULE_ITERATOR_STATE_R_DEBUG: + it->state = USERSPACE_LOADED_MODULE_ITERATOR_STATE_LINK_MAP; + err = userspace_get_link_map(it); if (err) return err; + fallthrough; + default: + return yield_from_link_map(it, ret, new_ret); } - return NULL; } +struct process_mapped_file_entry { + dev_t dev; + ino_t ino; + struct drgn_mapped_file *file; +}; + +struct process_mapped_file_key { + dev_t dev; + ino_t ino; + const char *path; +}; + +static struct process_mapped_file_key +process_mapped_file_entry_to_key(const struct process_mapped_file_entry *entry) +{ + return (struct process_mapped_file_key){ + .dev = entry->dev, + .ino = entry->ino, + .path = entry->file->path, + }; +} + +static struct hash_pair +process_mapped_file_key_hash_pair(const struct process_mapped_file_key *key) +{ + size_t hash = hash_combine(key->dev, key->ino); + hash = hash_combine(hash, hash_c_string(key->path)); + return hash_pair_from_avalanching_hash(hash); +} + +static bool process_mapped_file_key_eq(const struct process_mapped_file_key *a, + const struct process_mapped_file_key *b) +{ + return (a->dev == b->dev + && a->ino == b->ino + && strcmp(a->path, b->path) == 0); +} + +DEFINE_HASH_TABLE(process_mapped_files, struct process_mapped_file_entry, + process_mapped_file_entry_to_key, + process_mapped_file_key_hash_pair, + process_mapped_file_key_eq); + +struct process_loaded_module_iterator { + struct userspace_loaded_module_iterator u; + struct process_mapped_files files; +}; + static struct drgn_error * -userspace_core_report_debug_info(struct drgn_debug_info_load_state *load, - const char *nt_file, size_t nt_file_len) +process_add_mapping(struct process_loaded_module_iterator *it, + const char *maps_path, const char *map_files_path, + int map_files_fd, bool *logged_readlink_eperm, + bool *logged_stat_eperm, + struct drgn_map_files_segment_vector *map_files_segments, + struct drgn_mapped_file_segments *segments, + char *line, size_t line_len) { - struct drgn_error *err; + struct drgn_program *prog = it->u.it.prog; + + struct drgn_map_files_segment segment; + uint64_t segment_file_offset; + unsigned int dev_major, dev_minor; + uint64_t ino; + int map_name_len, path_index; + if (sscanf(line, + "%" SCNx64 "-%" SCNx64 "%n %*s %" SCNx64 " %x:%x %" SCNu64 " %n", + &segment.start, &segment.end, &map_name_len, + &segment_file_offset, &dev_major, &dev_minor, &ino, + &path_index) != 6) { + return drgn_error_format(DRGN_ERROR_OTHER, "couldn't parse %s", + maps_path); + } + // Skip anonymous mappings. + if (ino == 0) + return NULL; + + if (!drgn_map_files_segment_vector_append(map_files_segments, &segment)) + return &drgn_enomem; - struct userspace_core_report_state core = { - .files = HASH_TABLE_INIT, + struct process_mapped_file_key key = { + .dev = makedev(dev_major, dev_minor), + .ino = ino, + .path = line + path_index, }; - err = userspace_core_get_mapped_files(load, &core, nt_file, - nt_file_len); - if (err) - goto out; - err = userspace_core_report_mapped_files(load, &core); -out: - free(core.segment_buf); - free(core.phdr_buf); - for (struct drgn_mapped_files_iterator it = - drgn_mapped_files_first(&core.files); - it.entry; it = drgn_mapped_files_next(it)) - drgn_mapped_file_segment_vector_deinit(&it.entry->value); - drgn_mapped_files_deinit(&core.files); - return err; + _cleanup_free_ char *real_path = NULL; + + // /proc/$pid/maps has a couple of ambiguities that + // /proc/$pid/map_files/
can help with: + // + // 1. Newlines in the file path from /proc/$pid/maps are escaped as + // \012. However, \ is not escaped, so it is ambiguous whether \012 + // is a newline or appeared literally in the path. We can read the + // map_files link to get the unescaped path. + // 2. The device number in /proc/$pid/maps is incorrect for some + // filesystems. Specifically, for Btrfs as of Linux 6.5, it refers to + // a filesystem-wide device number rather than the subvolume-specific + // device numbers returned by stat. We can stat the map_files link to + // get the correct device number. + if (map_files_fd >= 0) { + char map_files_name[34]; + snprintf(map_files_name, sizeof(map_files_name), + "%" PRIx64 "-%" PRIx64, segment.start, segment.end); + + // The escaped path must be at least as long as the original + // path, so use that as the readlink buffer size. + size_t bufsiz = line_len - path_index + 1; + real_path = malloc(bufsiz); + if (!real_path) + return &drgn_enomem; + // Before Linux kernel commit bdb4d100afe9 ("procfs: always + // expose /proc//map_files/ and make it readable") (in + // v4.3), reading these links required CAP_SYS_ADMIN. Since that + // commit, it only requires PTRACE_MODE_READ, which we must have + // since we opened /proc/$pid/maps. + // + // If we can't read this link, we have to fall back to the + // escaped path. Newlines and the literal sequence \012 are + // unlikely to appear in a path, so it's not a big deal. + ssize_t r = readlinkat(map_files_fd, map_files_name, real_path, + bufsiz); + if (r < 0) { + if (errno == EPERM) { + free(real_path); + real_path = NULL; + if (!*logged_readlink_eperm) { + drgn_log_debug(prog, + "don't have permission to read symlinks in %s", + map_files_path); + } + *logged_readlink_eperm = true; + } else if (errno == ENOENT) { + // We raced with a change to the mapping. + drgn_log_debug(prog, "mapping %s disappeared", + map_files_name); + return NULL; + } else { + return drgn_error_format_os("readlink", errno, + "%s/%s", + map_files_path, + map_files_name); + } + } else if (r >= bufsiz) { + // We didn't allocate enough for the link contents. The + // only way this is possible is if we raced with the + // mapping being replaced by a different path. + drgn_log_debug(prog, + "mapping %s path changed; skipping", + map_files_name); + return NULL; + } else { + real_path[r] = '\0'; + key.path = real_path; + } + + // Following these links requires CAP_SYS_ADMIN. If we can't, we + // have to fall back to using the device number from + // /proc/$pid/maps. Mapping files with the same path and inode + // number in different Btrfs subvolumes is unlikely, so this is + // also not a big deal. + struct stat st; + if (fstatat(map_files_fd, map_files_name, &st, 0) < 0) { + if (errno == EPERM) { + if (!*logged_stat_eperm) { + drgn_log_debug(prog, + "don't have permission to follow symlinks in %s", + map_files_path); + } + *logged_stat_eperm = true; + } else if (errno == ENOENT) { + // We raced with a change to the mapping. + drgn_log_debug(prog, "mapping %s disappeared", + map_files_name); + return NULL; + } else { + return drgn_error_format_os("stat", errno, + "%s/%s", + map_files_path, + map_files_name); + } + } else { + key.dev = st.st_dev; + } + } + + struct hash_pair hp = process_mapped_files_hash(&key); + struct process_mapped_files_iterator files_it = + process_mapped_files_search_hashed(&it->files, &key, hp); + if (!files_it.entry) { + if (!real_path) { + real_path = strdup(key.path); + if (!real_path) + return &drgn_enomem; + } + struct drgn_mapped_file *file = + drgn_mapped_file_create(real_path); + if (!file) + return &drgn_enomem; + struct process_mapped_file_entry entry = { + .dev = key.dev, + .ino = key.ino, + .file = file, + }; + if (process_mapped_files_insert_searched(&it->files, &entry, hp, + &files_it) < 0) { + drgn_mapped_file_destroy(file); + return &drgn_enomem; + } + // real_path is owned by the iterator now. + real_path = NULL; + } + return drgn_add_mapped_file_segment(segments, segment.start, segment.end, + segment_file_offset, + files_it.entry->file); } static struct drgn_error * -userspace_report_elf_file(struct drgn_debug_info_load_state *load, - const char *path) +process_get_mapped_files(struct process_loaded_module_iterator *it) { struct drgn_error *err; + struct drgn_program *prog = it->u.it.prog; + +#define FORMAT "/proc/%ld/maps" + char maps_path[sizeof(FORMAT) + - sizeof("%ld") + + max_decimal_length(long) + + 1]; + snprintf(maps_path, sizeof(maps_path), FORMAT, (long)prog->pid); +#undef FORMAT + _cleanup_fclose_ FILE *maps_file = fopen(maps_path, "r"); + if (!maps_file) + return drgn_error_create_os("fopen", errno, maps_path); + drgn_log_debug(prog, "parsing %s", maps_path); + +#define FORMAT "/proc/%ld/map_files" + char map_files_path[sizeof(FORMAT) + - sizeof("%ld") + + max_decimal_length(long) + + 1]; + snprintf(map_files_path, sizeof(map_files_path), FORMAT, + (long)prog->pid); +#undef FORMAT + // Since Linux kernel commit bdb4d100afe9 ("procfs: always expose + // /proc//map_files/ and make it readable") (in v4.3), + // /proc/$pid/map_files always exists. Before that, it only exists if + // CONFIG_CHECKPOINT_RESTORE is enabled. + // + // If it exists, we should always have permission to open it since we + // were able to open /proc/$pid/maps. + _cleanup_close_ int map_files_fd = + open(map_files_path, O_RDONLY | O_DIRECTORY); + if (map_files_fd < 0) { + if (errno != ENOENT) { + return drgn_error_create_os("open", errno, + map_files_path); + } + drgn_log_debug(prog, "%s: %m", map_files_path); + } - int fd; - Elf *elf; - err = open_elf_file(path, &fd, &elf); - if (err) - goto err; - - GElf_Ehdr ehdr_mem, *ehdr; - ehdr = gelf_getehdr(elf, &ehdr_mem); - if (!ehdr) { - err = drgn_error_libelf(); - goto err_close; - } - /* - * We haven't implemented a way to get the load address for dynamically - * loaded or relocatable files, so for now we report those as unloaded. - */ - uint64_t start = 0, end = 0; - if (ehdr->e_type == ET_EXEC || ehdr->e_type == ET_CORE) { - err = elf_address_range(elf, 0, &start, &end); + _cleanup_free_ char *line = NULL; + size_t n = 0; + bool logged_readlink_eperm = false, logged_stat_eperm = false; + // While we're reading /proc/$pid/maps, we might as well cache the + // segments for drgn_module_try_proc_files_for_shared_library(). + _cleanup_(drgn_map_files_segment_vector_deinit) + struct drgn_map_files_segment_vector map_files_segments = VECTOR_INIT; + struct drgn_mapped_file_segments segments = DRGN_MAPPED_FILE_SEGMENTS_INIT; + for (;;) { + errno = 0; + ssize_t len; + if ((len = getline(&line, &n, maps_file)) == -1) { + if (errno) { + err = drgn_error_create_os("getline", errno, + maps_path); + } else { + err = NULL; + } + break; + } + // Remove the newline. + if (len > 0 && line[len - 1] == '\n') + line[--len] = '\0'; + + drgn_log_debug(prog, "read %s", line); + err = process_add_mapping(it, maps_path, map_files_path, + map_files_fd, &logged_readlink_eperm, + &logged_stat_eperm, + &map_files_segments, &segments, line, + len); if (err) - goto err_close; + break; } + if (err) { + drgn_mapped_file_segments_abort(&segments); + } else { + drgn_debug_info_set_map_files_segments(&prog->dbinfo, + &map_files_segments, + segments.sorted); + userspace_loaded_module_iterator_set_file_segments(&it->u, + &segments); + } + return err; +} - return drgn_debug_info_report_elf(load, path, fd, elf, start, end, NULL, - NULL); +static void +process_loaded_module_iterator_destroy(struct drgn_module_iterator *_it) +{ + struct process_loaded_module_iterator *it = + container_of(_it, struct process_loaded_module_iterator, u.it); + for (struct process_mapped_files_iterator files_it = + process_mapped_files_first(&it->files); + files_it.entry; files_it = process_mapped_files_next(files_it)) { + free((char *)files_it.entry->file->path); + drgn_mapped_file_destroy(files_it.entry->file); + } + process_mapped_files_deinit(&it->files); + userspace_loaded_module_iterator_deinit(&it->u); + free(it); +} -err_close: - elf_end(elf); - close(fd); -err: - return drgn_debug_info_report_error(load, path, NULL, err); +static struct drgn_error * +process_loaded_module_iterator_create(struct drgn_program *prog, + struct drgn_module_iterator **ret) +{ + struct drgn_error *err; + struct process_loaded_module_iterator *it = calloc(1, sizeof(*it)); + if (!it) + return &drgn_enomem; + drgn_module_iterator_init(&it->u.it, prog, + process_loaded_module_iterator_destroy, + userspace_loaded_module_iterator_next); + process_mapped_files_init(&it->files); + err = process_get_mapped_files(it); + if (err) { + process_loaded_module_iterator_destroy(&it->u.it); + return err; + } + *ret = &it->u.it; + return NULL; +} + +static const char * +core_mapped_file_entry_to_key(struct drgn_mapped_file * const *entry) +{ + return (*entry)->path; +} + +DEFINE_HASH_TABLE(core_mapped_files, struct drgn_mapped_file *, + core_mapped_file_entry_to_key, c_string_key_hash_pair, + c_string_key_eq); + +struct core_loaded_module_iterator { + struct userspace_loaded_module_iterator u; + struct core_mapped_files files; +}; + +static struct drgn_error *parse_nt_file_error(struct binary_buffer *bb, + const char *pos, + const char *message) +{ + return drgn_error_create(DRGN_ERROR_OTHER, "couldn't parse NT_FILE"); } static struct drgn_error * -userspace_report_debug_info(struct drgn_debug_info_load_state *load) +core_get_mapped_files(struct core_loaded_module_iterator *it) { struct drgn_error *err; + struct drgn_program *prog = it->u.it.prog; - for (size_t i = 0; i < load->num_paths; i++) { - err = userspace_report_elf_file(load, load->paths[i]); - if (err) + const void *note; + size_t note_size; + if (find_elf_note(prog->core, "CORE", NT_FILE, ¬e, ¬e_size)) + return drgn_error_libelf(); + if (!note) { + drgn_log_debug(prog, "core doesn't have NT_FILE note"); + return NULL; + } + + drgn_log_debug(prog, "parsing NT_FILE"); + + bool is_64_bit = drgn_platform_is_64_bit(&prog->platform); + bool little_endian = drgn_platform_is_little_endian(&prog->platform); + + struct binary_buffer bb; + binary_buffer_init(&bb, note, note_size, little_endian, + parse_nt_file_error); + + // fs/binfmt_elf.c in the Linux kernel source code documents the format + // of NT_FILE as: + // + // long count -- how many files are mapped + // long page_size -- units for file_ofs + // array of [COUNT] elements of + // long start + // long end + // long file_ofs + // followed by COUNT filenames in ASCII: "FILE1" NUL "FILE2" NUL... + struct nt_file_segment64 { + uint64_t start; + uint64_t end; + uint64_t file_offset; + }; + struct nt_file_segment32 { + uint32_t start; + uint32_t end; + uint32_t file_offset; + }; + uint64_t count, page_size; + if (is_64_bit) { + if ((err = binary_buffer_next_u64(&bb, &count))) + return err; + if (count > UINT64_MAX / sizeof(struct nt_file_segment64)) + return binary_buffer_error(&bb, "count is too large"); + if ((err = binary_buffer_next_u64(&bb, &page_size)) || + (err = binary_buffer_skip(&bb, + count * sizeof(struct nt_file_segment64)))) + return err; + } else { + if ((err = binary_buffer_next_u32_into_u64(&bb, &count))) + return err; + if (count > UINT64_MAX / sizeof(struct nt_file_segment32)) + return binary_buffer_error(&bb, "count is too large"); + if ((err = binary_buffer_next_u32_into_u64(&bb, &page_size)) || + (err = binary_buffer_skip(&bb, + count * sizeof(struct nt_file_segment32)))) return err; } - if (load->load_default) { - Dwfl *dwfl = load->dbinfo->dwfl; - struct drgn_program *prog = load->dbinfo->prog; - if (prog->flags & DRGN_PROGRAM_IS_LIVE) { - int ret = dwfl_linux_proc_report(dwfl, prog->pid); - if (ret == -1) { - return drgn_error_libdwfl(); - } else if (ret) { - return drgn_error_create_os("dwfl_linux_proc_report", - ret, NULL); - } + struct drgn_mapped_file_segments segments = + DRGN_MAPPED_FILE_SEGMENTS_INIT; + for (uint64_t i = 0; i < count; i++) { + struct nt_file_segment64 segment; +#define visit_nt_file_segment_members(visit_scalar_member, visit_raw_member) do { \ + visit_scalar_member(start); \ + visit_scalar_member(end); \ + visit_scalar_member(file_offset); \ +} while (0) + deserialize_struct64(&segment, struct nt_file_segment32, + visit_nt_file_segment_members, + (char *)note + + (is_64_bit + ? 16 + i * sizeof(struct nt_file_segment64) + : 8 + i * sizeof(struct nt_file_segment32)), + is_64_bit, bb.bswap); +#undef visit_nt_file_segment_members + segment.file_offset *= page_size; + const char *path = bb.pos; + if ((err = binary_buffer_skip_string(&bb))) + goto err; + drgn_log_debug(prog, + "found 0x%" PRIx64 "-0x%" PRIx64 " 0x%" PRIx64 " %s", + segment.start, segment.end, segment.file_offset, + path); + if (segment.start >= segment.end) + continue; + + struct hash_pair hp = core_mapped_files_hash(&path); + struct core_mapped_files_iterator files_it = + core_mapped_files_search_hashed(&it->files, &path, hp); + struct drgn_mapped_file *file; + if (files_it.entry) { + file = *files_it.entry; } else { - const char *nt_file; - size_t nt_file_len; - char *env = getenv("DRGN_USE_LIBDWFL_REPORT"); - if (env && atoi(env)) { - nt_file = NULL; - nt_file_len = 0; - } else { - err = drgn_get_nt_file(prog->core, &nt_file, - &nt_file_len); - if (err) - return err; + file = drgn_mapped_file_create(path); + if (!file) { + err = &drgn_enomem; + goto err; } - if (nt_file) { - err = userspace_core_report_debug_info(load, - nt_file, - nt_file_len); - if (err) - return err; - } else if (dwfl_core_file_report(dwfl, prog->core, - NULL) == -1) { - return drgn_error_libdwfl(); + if (core_mapped_files_insert_searched(&it->files, &file, + hp, NULL) < 0) { + drgn_mapped_file_destroy(file); + err = &drgn_enomem; + goto err; } } + err = drgn_add_mapped_file_segment(&segments, segment.start, + segment.end, + segment.file_offset, file); + if (err) + goto err; } + userspace_loaded_module_iterator_set_file_segments(&it->u, &segments); return NULL; + +err: + drgn_mapped_file_segments_abort(&segments); + return err; } -static int should_apply_relocation_section(Elf *elf, size_t shstrndx, - const GElf_Shdr *shdr) +static void +core_loaded_module_iterator_destroy(struct drgn_module_iterator *_it) { - if (shdr->sh_type != SHT_RELA && shdr->sh_type != SHT_REL) - return 0; + struct core_loaded_module_iterator *it = + container_of(_it, struct core_loaded_module_iterator, u.it); + for (struct core_mapped_files_iterator files_it = + core_mapped_files_first(&it->files); + files_it.entry; + files_it = core_mapped_files_next(files_it)) + drgn_mapped_file_destroy(*files_it.entry); + core_mapped_files_deinit(&it->files); + userspace_loaded_module_iterator_deinit(&it->u); + free(it); +} - const char *scnname = elf_strptr(elf, shstrndx, shdr->sh_name); - if (!scnname) - return -1; - if (shdr->sh_type == SHT_RELA) { - if (!strstartswith(scnname, ".rela.")) - return 0; - scnname += sizeof(".rela.") - 1; - } else { - if (!strstartswith(scnname, ".rel.")) - return 0; - scnname += sizeof(".rel.") - 1; +static struct drgn_error * +core_loaded_module_iterator_create(struct drgn_program *prog, + struct drgn_module_iterator **ret) +{ + struct drgn_error *err; + struct core_loaded_module_iterator *it = calloc(1, sizeof(*it)); + if (!it) + return &drgn_enomem; + drgn_module_iterator_init(&it->u.it, prog, + core_loaded_module_iterator_destroy, + userspace_loaded_module_iterator_next); + core_mapped_files_init(&it->files); + err = core_get_mapped_files(it); + if (err) { + core_loaded_module_iterator_destroy(&it->u.it); + return err; } - return (strstartswith(scnname, "debug_") || - strstartswith(scnname, "orc_")); + *ret = &it->u.it; + return NULL; +} + +static struct drgn_error * +null_module_iterator_create(struct drgn_program *prog, + struct drgn_module_iterator **ret) +{ + struct drgn_module_iterator *it = calloc(1, sizeof(*it)); + if (!it) + return &drgn_enomem; + drgn_module_iterator_init(it, prog, NULL, NULL); + *ret = it; + return NULL; } -static inline struct drgn_error *get_reloc_sym_value(const void *syms, - size_t num_syms, - const uint64_t *sh_addrs, - size_t shdrnum, - bool is_64_bit, - bool bswap, - uint32_t r_sym, - uint64_t *ret) +LIBDRGN_PUBLIC struct drgn_error * +drgn_loaded_module_iterator_create(struct drgn_program *prog, + struct drgn_module_iterator **ret) { - if (r_sym >= num_syms) { - return drgn_error_create(DRGN_ERROR_OTHER, - "invalid ELF relocation symbol"); + if (prog->flags & DRGN_PROGRAM_IS_LINUX_KERNEL) + return linux_kernel_loaded_module_iterator_create(prog, ret); + else if (drgn_program_is_userspace_process(prog)) + return process_loaded_module_iterator_create(prog, ret); + else if (drgn_program_is_userspace_core(prog)) + return core_loaded_module_iterator_create(prog, ret); + else + return null_module_iterator_create(prog, ret); +} + +struct load_debug_info_file { + const char *path; + // We only keep this to keep load_debug_info_provided::build_id alive + // without needing to copy it. If we add a drgn_module_try_file API that + // allows providing an Elf handle, we could pass it down. + Elf *elf; + // This may be consumed and set to -1. + int fd; +}; + +DEFINE_VECTOR(load_debug_info_file_vector, struct load_debug_info_file); + +struct load_debug_info_provided { + const void *build_id; + size_t build_id_len; + struct load_debug_info_file_vector files; + bool matched; +}; + +static struct nstring +load_debug_info_provided_key(const struct load_debug_info_provided *provided) +{ + return (struct nstring){ provided->build_id, provided->build_id_len }; +} + +DEFINE_HASH_TABLE(load_debug_info_provided_table, + struct load_debug_info_provided, + load_debug_info_provided_key, nstring_hash_pair, nstring_eq); + +struct load_debug_info_state { + // Provided files grouped by build ID. + struct load_debug_info_provided_table provided; + // Number of entries in the provided table that haven't matched any + // modules. + size_t unmatched_provided; +}; + +static struct drgn_error * +load_debug_info_add_provided_file(struct drgn_program *prog, + struct load_debug_info_state *state, + const char *path) +{ + _cleanup_close_ int fd = open(path, O_RDONLY); + if (fd < 0) { + drgn_log_warning(prog, "%s: %m; ignoring", path); + return NULL; } - uint16_t st_shndx; - uint64_t st_value; - if (is_64_bit) { - const Elf64_Sym *sym = (Elf64_Sym *)syms + r_sym; - memcpy(&st_shndx, &sym->st_shndx, sizeof(st_shndx)); - memcpy(&st_value, &sym->st_value, sizeof(st_value)); - if (bswap) { - st_shndx = bswap_16(st_shndx); - st_value = bswap_64(st_value); - } - } else { - const Elf32_Sym *sym = (Elf32_Sym *)syms + r_sym; - memcpy(&st_shndx, &sym->st_shndx, sizeof(st_shndx)); - uint32_t st_value32; - memcpy(&st_value32, &sym->st_value, sizeof(st_value32)); - if (bswap) { - st_shndx = bswap_16(st_shndx); - st_value32 = bswap_32(st_value32); + _cleanup_elf_end_ Elf *elf = dwelf_elf_begin(fd); + if (!elf) { + drgn_log_warning(prog, "%s: %s; ignoring", path, + elf_errmsg(-1)); + return NULL; + } + if (elf_kind(elf) != ELF_K_ELF) { + drgn_log_warning(prog, "%s: not an ELF file; ignoring", path); + return NULL; + } + const void *build_id; + ssize_t build_id_len = drgn_elf_gnu_build_id(elf, &build_id); + if (build_id_len <= 0) { + if (build_id_len < 0) { + drgn_log_warning(prog, "%s: %s; ignoring", path, + elf_errmsg(-1)); + } else { + drgn_log_warning(prog, "%s: no build ID; ignoring", + path); } - st_value = st_value32; + return NULL; } - if (st_shndx >= shdrnum) { - return drgn_error_create(DRGN_ERROR_OTHER, - "invalid ELF symbol section index"); + + if (drgn_log_is_enabled(prog, DRGN_LOG_DEBUG)) { + _cleanup_free_ char *build_id_str = + ahexlify(build_id, build_id_len); + if (!build_id_str) + return &drgn_enomem; + drgn_log_debug(prog, "provided file %s build ID %s", + path, build_id_str); + } + + struct load_debug_info_provided provided = { + .build_id = build_id, + .build_id_len = build_id_len, + }; + struct load_debug_info_provided_table_iterator it; + int r = load_debug_info_provided_table_insert(&state->provided, + &provided, &it); + if (r < 0) + return &drgn_enomem; + if (r > 0) { + load_debug_info_file_vector_init(&it.entry->files); + state->unmatched_provided++; + } + + struct load_debug_info_file file = { + .path = path, + .fd = fd, + .elf = elf, + }; + if (!load_debug_info_file_vector_append(&it.entry->files, &file)) { + if (load_debug_info_file_vector_empty(&it.entry->files)) { + // The key will no longer be valid once we free the Elf + // handle, so we need to delete the entry. + load_debug_info_provided_table_delete_iterator(&state->provided, + it); + } + return &drgn_enomem; } - *ret = sh_addrs[st_shndx] + st_value; + // fd and elf are owned by state now. + fd = -1; + elf = NULL; return NULL; } +static void load_debug_info_state_deinit(struct load_debug_info_state *state) +{ + for (struct load_debug_info_provided_table_iterator it = + load_debug_info_provided_table_first(&state->provided); + it.entry; + it = load_debug_info_provided_table_next(it)) { + vector_for_each(load_debug_info_file_vector, file, + &it.entry->files) { + elf_end(file->elf); + if (file->fd >= 0) + close(file->fd); + } + load_debug_info_file_vector_deinit(&it.entry->files); + } + load_debug_info_provided_table_deinit(&state->provided); +} + +static struct load_debug_info_provided * +load_debug_info_find_provided(struct load_debug_info_state *state, + const void *build_id, size_t build_id_len) +{ + struct nstring key = { build_id, build_id_len }; + struct load_debug_info_provided *provided = + load_debug_info_provided_table_search(&state->provided, + &key).entry; + if (provided && !provided->matched) { + state->unmatched_provided--; + provided->matched = true; + } + return provided; +} + static struct drgn_error * -apply_elf_relas(const struct drgn_relocating_section *relocating, - Elf_Data *reloc_data, Elf_Data *symtab_data, - const uint64_t *sh_addrs, size_t shdrnum, - const struct drgn_platform *platform) +load_debug_info_try_provided(struct drgn_module *module, + struct load_debug_info_provided *provided, + enum drgn_module_file_status not_status) { struct drgn_error *err; - - bool is_64_bit = drgn_platform_is_64_bit(platform); - bool bswap = drgn_platform_bswap(platform); - apply_elf_reloc_fn *apply_elf_reloc = platform->arch->apply_elf_reloc; - - const void *relocs = reloc_data->d_buf; - size_t reloc_size = is_64_bit ? sizeof(Elf64_Rela) : sizeof(Elf32_Rela); - size_t num_relocs = reloc_data->d_size / reloc_size; - - const void *syms = symtab_data->d_buf; - size_t sym_size = is_64_bit ? sizeof(Elf64_Sym) : sizeof(Elf32_Sym); - size_t num_syms = symtab_data->d_size / sym_size; - - for (size_t i = 0; i < num_relocs; i++) { - uint64_t r_offset; - uint32_t r_sym; - uint32_t r_type; - int64_t r_addend; - if (is_64_bit) { - const Elf64_Rela *rela = (Elf64_Rela *)relocs + i; - uint64_t r_info; - memcpy(&r_offset, &rela->r_offset, sizeof(r_offset)); - memcpy(&r_info, &rela->r_info, sizeof(r_info)); - memcpy(&r_addend, &rela->r_addend, sizeof(r_addend)); - if (bswap) { - r_offset = bswap_64(r_offset); - r_info = bswap_64(r_info); - r_addend = bswap_64(r_addend); - } - r_sym = ELF64_R_SYM(r_info); - r_type = ELF64_R_TYPE(r_info); - } else { - const Elf32_Rela *rela32 = (Elf32_Rela *)relocs + i; - uint32_t r_offset32; - uint32_t r_info32; - int32_t r_addend32; - memcpy(&r_offset32, &rela32->r_offset, sizeof(r_offset32)); - memcpy(&r_info32, &rela32->r_info, sizeof(r_info32)); - memcpy(&r_addend32, &rela32->r_addend, sizeof(r_addend32)); - if (bswap) { - r_offset32 = bswap_32(r_offset32); - r_info32 = bswap_32(r_info32); - r_addend32 = bswap_32(r_addend32); - } - r_offset = r_offset32; - r_sym = ELF32_R_SYM(r_info32); - r_type = ELF32_R_TYPE(r_info32); - r_addend = r_addend32; - } - uint64_t sym_value; - err = get_reloc_sym_value(syms, num_syms, sh_addrs, shdrnum, - is_64_bit, bswap, r_sym, &sym_value); + vector_for_each(load_debug_info_file_vector, file, &provided->files) { + // No need to check build ID again. + err = drgn_module_try_file_internal(module, file->path, + file->fd, false, NULL); + // drgn_module_try_file_internal took ownership of file->fd. In + // the unlikely scenario that another module has the same build + // ID, we'll just have to reopen it by path. + file->fd = -1; if (err) return err; - err = apply_elf_reloc(relocating, r_offset, r_type, &r_addend, - sym_value); - if (err) - return err; + if (module->loaded_file_status != not_status + && module->debug_file_status != not_status) + break; } return NULL; } static struct drgn_error * -apply_elf_rels(const struct drgn_relocating_section *relocating, - Elf_Data *reloc_data, Elf_Data *symtab_data, - const uint64_t *sh_addrs, size_t shdrnum, - const struct drgn_platform *platform) +load_debug_info_try_provided_supplementary_files(struct drgn_module *module, + struct load_debug_info_state *state) +{ + const void *checksum; + size_t checksum_len; + if (drgn_module_wanted_supplementary_debug_file(module, NULL, NULL, + &checksum, + &checksum_len) + != DRGN_SUPPLEMENTARY_FILE_GNU_DEBUGALTLINK) + return NULL; + struct load_debug_info_provided *provided = + load_debug_info_find_provided(state, checksum, checksum_len); + if (!provided) + return NULL; + drgn_module_try_supplementary_debug_file_log(module, + "trying provided files for"); + return load_debug_info_try_provided(module, provided, + DRGN_MODULE_FILE_WANT_SUPPLEMENTARY); +} + +static struct drgn_error * +load_debug_info_try_provided_files(struct drgn_module *module, + struct load_debug_info_state *state) { struct drgn_error *err; - bool is_64_bit = drgn_platform_is_64_bit(platform); - bool bswap = drgn_platform_bswap(platform); - apply_elf_reloc_fn *apply_elf_reloc = platform->arch->apply_elf_reloc; - - const void *relocs = reloc_data->d_buf; - size_t reloc_size = is_64_bit ? sizeof(Elf64_Rel) : sizeof(Elf32_Rel); - size_t num_relocs = reloc_data->d_size / reloc_size; - - const void *syms = symtab_data->d_buf; - size_t sym_size = is_64_bit ? sizeof(Elf64_Sym) : sizeof(Elf32_Sym); - size_t num_syms = symtab_data->d_size / sym_size; - - for (size_t i = 0; i < num_relocs; i++) { - uint64_t r_offset; - uint32_t r_sym; - uint32_t r_type; - if (is_64_bit) { - const Elf64_Rel *rel = (Elf64_Rel *)relocs + i; - uint64_t r_info; - memcpy(&r_offset, &rel->r_offset, sizeof(r_offset)); - memcpy(&r_info, &rel->r_info, sizeof(r_info)); - if (bswap) { - r_offset = bswap_64(r_offset); - r_info = bswap_64(r_info); - } - r_sym = ELF64_R_SYM(r_info); - r_type = ELF64_R_TYPE(r_info); - } else { - const Elf32_Rel *rel32 = (Elf32_Rel *)relocs + i; - uint32_t r_offset32; - uint32_t r_info32; - memcpy(&r_offset32, &rel32->r_offset, sizeof(r_offset32)); - memcpy(&r_info32, &rel32->r_info, sizeof(r_info32)); - if (bswap) { - r_offset32 = bswap_32(r_offset32); - r_info32 = bswap_32(r_info32); + err = load_debug_info_try_provided_supplementary_files(module, state); + if (err) + return err; + + const void *build_id; + size_t build_id_len; + drgn_module_build_id(module, &build_id, &build_id_len); + if (build_id_len != 0) { + // Look up the provided file even if we don't need it so that it + // counts as matched. + struct load_debug_info_provided *provided = + load_debug_info_find_provided(state, build_id, + build_id_len); + if (provided && drgn_module_wants_file(module)) { + uint64_t orig_supplementary_file_generation = + module->prog->dbinfo.supplementary_file_generation; + drgn_module_try_files_log(module, + "trying provided files for"); + err = load_debug_info_try_provided(module, provided, + DRGN_MODULE_FILE_WANT); + if (err) + return err; + // If the wanted supplementary debug file changed, try + // finding it again. + if (drgn_module_wanted_supplementary_debug_file_is_new(module, + orig_supplementary_file_generation)) { + err = load_debug_info_try_provided_supplementary_files(module, + state); + if (err) + return err; } - r_offset = r_offset32; - r_sym = ELF32_R_SYM(r_info32); - r_type = ELF32_R_TYPE(r_info32); } - uint64_t sym_value; - err = get_reloc_sym_value(syms, num_syms, sh_addrs, shdrnum, - is_64_bit, bswap, r_sym, &sym_value); - if (err) - return err; - - err = apply_elf_reloc(relocating, r_offset, r_type, NULL, - sym_value); - if (err) - return err; } return NULL; } -/* - * Before the debugging information in a relocatable ELF file (e.g., Linux - * kernel module) can be used, it must have ELF relocations applied. This is - * usually done by libdwfl. However, libdwfl is relatively slow at it. This is a - * much faster implementation. - */ -static struct drgn_error *relocate_elf_file(Elf *elf) +static void load_debug_info_log_missing(struct drgn_module *module, + unsigned int max_warnings, + unsigned int *num_warnings) { - struct drgn_error *err; - - GElf_Ehdr ehdr_mem, *ehdr; - ehdr = gelf_getehdr(elf, &ehdr_mem); - if (!ehdr) - return drgn_error_libelf(); - - if (ehdr->e_type != ET_REL) { - /* Not a relocatable file. */ - return NULL; + if (++(*num_warnings) > max_warnings) + return; + const char *missing_loaded = ""; + if (drgn_module_loaded_file_status(module) == DRGN_MODULE_FILE_WANT) { + switch (drgn_module_kind(module)) { + case DRGN_MODULE_MAIN: + missing_loaded = "executable file"; + break; + case DRGN_MODULE_SHARED_LIBRARY: + case DRGN_MODULE_VDSO: + missing_loaded = "shared object file"; + break; + default: + missing_loaded = "loaded file"; + break; + } } - - struct drgn_platform platform; - drgn_platform_from_elf(ehdr, &platform); - if (!platform.arch->apply_elf_reloc) { - /* Unsupported; fall back to libdwfl. */ - return NULL; + const char *missing_debug; + switch (drgn_module_debug_file_status(module)) { + case DRGN_MODULE_FILE_WANT: + missing_debug = "debugging symbols"; + break; + case DRGN_MODULE_FILE_WANT_SUPPLEMENTARY: + missing_debug = "supplementary debugging symbols"; + break; + default: + missing_debug = ""; + break; } + drgn_log_warning(module->prog, "missing %s%s%s for %s", missing_loaded, + missing_loaded[0] && missing_debug[0] ? " and ": "", + missing_debug, module->name); +} - size_t shdrnum; - if (elf_getshdrnum(elf, &shdrnum)) - return drgn_error_libelf(); - _cleanup_free_ uint64_t *sh_addrs = - calloc(shdrnum, sizeof(sh_addrs[0])); - if (!sh_addrs && shdrnum > 0) - return &drgn_enomem; - - Elf_Scn *scn = NULL; - while ((scn = elf_nextscn(elf, scn))) { - GElf_Shdr *shdr, shdr_mem; - shdr = gelf_getshdr(scn, &shdr_mem); - if (!shdr) - return drgn_error_libelf(); - sh_addrs[elf_ndxscn(scn)] = shdr->sh_addr; - } +static inline void drgn_module_iterator_destroyp(struct drgn_module_iterator **itp) +{ + drgn_module_iterator_destroy(*itp); +} - size_t shstrndx; - if (elf_getshdrstrndx(elf, &shstrndx)) - return drgn_error_libelf(); +LIBDRGN_PUBLIC struct drgn_error * +drgn_program_load_debug_info(struct drgn_program *prog, const char **paths, + size_t n, bool load_default, bool load_main) +{ + struct drgn_error *err; - Elf_Scn *reloc_scn = NULL; - while ((reloc_scn = elf_nextscn(elf, reloc_scn))) { - GElf_Shdr *reloc_shdr, reloc_shdr_mem; - reloc_shdr = gelf_getshdr(reloc_scn, &reloc_shdr_mem); - if (!reloc_shdr) - return drgn_error_libelf(); - - int r = should_apply_relocation_section(elf, shstrndx, - reloc_shdr); - if (r < 0) - return drgn_error_libelf(); - if (r) { - scn = elf_getscn(elf, reloc_shdr->sh_info); - if (!scn) - return drgn_error_libelf(); - GElf_Shdr *shdr, shdr_mem; - shdr = gelf_getshdr(scn, &shdr_mem); - if (!shdr) - return drgn_error_libelf(); - if (shdr->sh_type == SHT_NOBITS) - continue; + if (n == 0 && !load_default && !load_main) { + // We don't have any files to try. Don't create any modules. + return NULL; + } - Elf_Scn *symtab_scn = elf_getscn(elf, - reloc_shdr->sh_link); - if (!symtab_scn) - return drgn_error_libelf(); + drgn_blocking_guard(prog); - Elf_Data *data, *reloc_data, *symtab_data; - if ((err = read_elf_section(scn, &data)) || - (err = read_elf_section(reloc_scn, &reloc_data)) || - (err = read_elf_section(symtab_scn, &symtab_data))) - return err; + const char *env = getenv("DRGN_MAX_DEBUG_INFO_ERRORS"); + unsigned int max_warnings = env ? atoi(env) : 5; + unsigned int num_warnings = 0; - struct drgn_relocating_section relocating = { - .buf = data->d_buf, - .buf_size = data->d_size, - .addr = sh_addrs[elf_ndxscn(scn)], - .bswap = drgn_platform_bswap(&platform), - }; + drgn_log_debug(prog, "loading %sdebugging symbols", + load_default ? "default " : load_main ? "main " : ""); - if (reloc_shdr->sh_type == SHT_RELA) { - err = apply_elf_relas(&relocating, reloc_data, - symtab_data, sh_addrs, - shdrnum, &platform); - } else { - err = apply_elf_rels(&relocating, reloc_data, - symtab_data, sh_addrs, - shdrnum, &platform); - } - if (err) - return err; + _cleanup_(load_debug_info_state_deinit) + struct load_debug_info_state state = { + .provided = HASH_TABLE_INIT, + }; + for (size_t i = 0; i < n; i++) { + err = load_debug_info_add_provided_file(prog, &state, paths[i]); + if (err) + return err; + } - /* - * Mark the relocation section as empty so that libdwfl - * doesn't try to apply it again. - */ - reloc_shdr->sh_size = 0; - if (!gelf_update_shdr(reloc_scn, reloc_shdr)) - return drgn_error_libelf(); - reloc_data->d_size = 0; - } + if (load_debug_info_provided_table_empty(&state.provided) + && !load_default && !load_main) { + drgn_log_debug(prog, "no usable provided files"); + return NULL; } - return NULL; -} -static struct drgn_error * -drgn_module_find_files(struct drgn_debug_info_load_state *load, - struct drgn_module *module) -{ - struct drgn_error *err; + uint64_t old_generation = prog->dbinfo.load_debug_info_generation; - if (module->elf) { - err = relocate_elf_file(module->elf); + _cleanup_(drgn_module_iterator_destroyp) + struct drgn_module_iterator *it = NULL; + err = drgn_loaded_module_iterator_create(prog, &it); + if (err) + return err; + _cleanup_(drgn_module_vector_deinit) + struct drgn_module_vector modules = VECTOR_INIT; + struct drgn_module *module; + while (!(err = drgn_module_iterator_next(it, &module, NULL)) && module) { + // Reset DONT_WANT to WANT. + if (module->loaded_file_status == DRGN_MODULE_FILE_DONT_WANT) + module->loaded_file_status = DRGN_MODULE_FILE_WANT; + if (module->debug_file_status == DRGN_MODULE_FILE_DONT_WANT) + module->debug_file_status = DRGN_MODULE_FILE_WANT; + + err = load_debug_info_try_provided_files(module, &state); if (err) return err; - } - GElf_Addr loaded_file_bias; - Elf *loaded_elf = NULL; - Dwarf_Addr debug_file_bias; - Dwarf *dwarf; - err = NULL; - #pragma omp critical(drgn_module_find_files) - { - // We don't need the loaded file for the Linux kernel, and we - // always report the debug file as the main file to libdwfl. - if (!(load->dbinfo->prog->flags & DRGN_PROGRAM_IS_LINUX_KERNEL)) { - loaded_elf = dwfl_module_getelf(module->dwfl_module, - &loaded_file_bias); - if (!loaded_elf) - err = drgn_error_libdwfl(); - } - if (!err) { - dwarf = dwfl_module_getdwarf(module->dwfl_module, - &debug_file_bias); - if (!dwarf) - err = drgn_error_libdwfl(); + if (drgn_module_wants_file(module) + && (load_default + || (load_main + && drgn_module_kind(module) == DRGN_MODULE_MAIN)) + && !drgn_module_vector_append(&modules, &module)) + return &drgn_enomem; + + // If we are only trying files for the main module (i.e., if + // we're not loading all default debug info and any provided + // files were all for the main module), then we only want to + // create the main module. + if (!load_default + && drgn_module_kind(module) == DRGN_MODULE_MAIN + && state.unmatched_provided == 0) { + err = NULL; + break; } } if (err) return err; - const char *loaded_file_path; - const char *debug_file_path; - dwfl_module_info(module->dwfl_module, NULL, NULL, NULL, NULL, NULL, - &loaded_file_path, &debug_file_path); - // If the loaded file also has debugging information, debug_file_path is - // NULL. (debug_file_path is also NULL if libdwfl got the debug file - // from debuginfod, so this isn't 100% correct, but it'll at least - // identify the module.) - if (!debug_file_path) - debug_file_path = loaded_file_path; - - module->debug_file_bias = debug_file_bias; - err = drgn_elf_file_create(module, debug_file_path, dwarf_getelf(dwarf), - &module->debug_file); - if (err) { - module->debug_file = NULL; - return err; - } - module->debug_file->dwarf = dwarf; - if (!module->debug_file->scns[DRGN_SCN_DEBUG_INFO] || - !module->debug_file->scns[DRGN_SCN_DEBUG_ABBREV]) { - return drgn_error_create(DRGN_ERROR_OTHER, - "missing debugging information sections"); - } - - Dwarf *altdwarf = dwarf_getalt(dwarf); - if (altdwarf) { - Elf *altelf = dwarf_getelf(altdwarf); - if (!altelf) - return drgn_error_libdw(); - size_t shstrndx; - if (elf_getshdrstrndx(altelf, &shstrndx)) - return drgn_error_libelf(); - - Elf_Scn *scn = NULL; - while ((scn = elf_nextscn(altelf, scn))) { - GElf_Shdr shdr_mem; - GElf_Shdr *shdr = gelf_getshdr(scn, &shdr_mem); - if (!shdr) - return drgn_error_libelf(); - - if (shdr->sh_type != SHT_PROGBITS) - continue; - const char *scnname = elf_strptr(altelf, shstrndx, - shdr->sh_name); - if (!scnname) - return drgn_error_libelf(); - - /* - * TODO: save more sections and support imported units. - */ - if (strcmp(scnname, ".debug_info") == 0 && - !module->debug_file->alt_debug_info_data) { - err = read_elf_section(scn, - &module->debug_file->alt_debug_info_data); - if (err) - return err; - } else if (strcmp(scnname, ".debug_str") == 0 && - !module->debug_file->alt_debug_str_data) { - err = read_elf_section(scn, - &module->debug_file->alt_debug_str_data); - if (err) - return err; + struct drgn_module **wanted_modules = + drgn_module_vector_begin(&modules); + size_t num_wanted_modules = drgn_module_vector_size(&modules); + bool iterator_tried_missing = false; + + // The module iterator may have tried to load debug info, so we need to + // check each module again. + if (num_wanted_modules > 0) { + uint64_t new_generation = + ++prog->dbinfo.load_debug_info_generation; + size_t new_num_wanted_modules = 0; + for (size_t i = 0; i < num_wanted_modules; i++) { + module = wanted_modules[i]; + if (module->load_debug_info_generation <= old_generation) { + // Reset DONT_WANT to WANT. + if (module->loaded_file_status == DRGN_MODULE_FILE_DONT_WANT) + module->loaded_file_status = DRGN_MODULE_FILE_WANT; + if (module->debug_file_status == DRGN_MODULE_FILE_DONT_WANT) + module->debug_file_status = DRGN_MODULE_FILE_WANT; + if (drgn_module_wants_file(module)) { + wanted_modules[new_num_wanted_modules++] = module; + module->load_debug_info_generation = new_generation; + } + } else if (drgn_module_wants_file(module)) { + load_debug_info_log_missing(module, + max_warnings, + &num_warnings); + iterator_tried_missing = true; } } + num_wanted_modules = new_num_wanted_modules; } - err = drgn_elf_file_precache_sections(module->debug_file); - if (err) - return err; - if (loaded_elf) { - module->loaded_file_bias = loaded_file_bias; - if (loaded_elf == module->debug_file->elf) { - module->loaded_file = module->debug_file; - } else { - err = drgn_elf_file_create(module, loaded_file_path, - loaded_elf, - &module->loaded_file); - if (err) { - module->loaded_file = NULL; + if (num_wanted_modules > 0) { + uint64_t orig_supplementary_file_generation = + prog->dbinfo.supplementary_file_generation; + drgn_handler_list_for_each_enabled(struct drgn_debug_info_finder, + finder, + &prog->dbinfo.debug_info_finders) { + err = finder->ops.find(wanted_modules, + num_wanted_modules, finder->arg); + if (err) return err; + size_t new_num_wanted_modules = 0; + for (size_t i = 0; i < num_wanted_modules; i++) { + module = wanted_modules[i]; + // If there are no more finders to try after + // this and a finder changed the wanted + // supplementary debug file, try to find a + // provided file for it one last time. + if (drgn_handler_is_last_enabled(&finder->handler) + && drgn_module_wanted_supplementary_debug_file_is_new(module, + orig_supplementary_file_generation)) { + err = load_debug_info_try_provided_supplementary_files(module, + &state); + if (err) + return err; + } + if (drgn_module_wants_file(module)) { + wanted_modules[new_num_wanted_modules++] = + module; + } } + num_wanted_modules = new_num_wanted_modules; + if (num_wanted_modules == 0) + break; } } - return NULL; -} -static struct drgn_error * -drgn_debug_info_read_module(struct drgn_debug_info_load_state *load, - struct drgn_dwarf_index_state *index, - struct drgn_module *head) -{ - struct drgn_error *err; - struct drgn_module *module; - for (module = head; module; module = module->next) { - err = drgn_module_find_files(load, module); - if (err) { - module->err = err; - continue; - } - module->state = DRGN_DEBUG_INFO_MODULE_INDEXING; - return drgn_dwarf_index_read_file(index, module->debug_file); - } - /* - * We checked all of the files and didn't find debugging information. - * Report why for each one. - * - * (If we did find debugging information, we discard errors on the - * unused files.) - */ - err = NULL; - #pragma omp critical(drgn_debug_info_read_module_error) - for (module = head; module; module = module->next) { - const char *name = - dwfl_module_info(module->dwfl_module, NULL, NULL, NULL, - NULL, NULL, NULL, NULL); - if (module->err) { - err = drgn_debug_info_report_error(load, name, NULL, - module->err); - module->err = NULL; - } else { - err = drgn_debug_info_report_error(load, name, - "no debugging information", - NULL); + if (state.unmatched_provided != 0) { + for (struct load_debug_info_provided_table_iterator pit = + load_debug_info_provided_table_first(&state.provided); + pit.entry; + pit = load_debug_info_provided_table_next(pit)) { + if (!pit.entry->matched) { + vector_for_each(load_debug_info_file_vector, + file, &pit.entry->files) { + drgn_log_warning(prog, + "provided file %s did not match any loaded modules; ignoring", + file->path); + } + } } - if (err) - break; } - return err; -} - -static struct drgn_error * -drgn_debug_info_update_index(struct drgn_debug_info_load_state *load) -{ - if (drgn_module_vector_empty(&load->new_modules)) - return NULL; - struct drgn_debug_info *dbinfo = load->dbinfo; - if (!c_string_set_reserve(&dbinfo->module_names, - c_string_set_size(&dbinfo->module_names) - + drgn_module_vector_size(&load->new_modules))) - return &drgn_enomem; - struct drgn_dwarf_index_state index; - if (!drgn_dwarf_index_state_init(&index, dbinfo)) - return &drgn_enomem; - struct drgn_error *err = NULL; - #pragma omp parallel for schedule(dynamic) num_threads(drgn_num_threads) - for (size_t i = 0; i < drgn_module_vector_size(&load->new_modules); i++) { - if (err) - continue; - struct drgn_module *module = - *drgn_module_vector_at(&load->new_modules, i); - struct drgn_error *module_err = - drgn_debug_info_read_module(load, &index, module); - if (module_err) { - #pragma omp critical(drgn_debug_info_update_index_error) - if (err) - drgn_error_destroy(module_err); - else - err = module_err; - } + for (size_t i = 0; i < num_wanted_modules; i++) { + load_debug_info_log_missing(wanted_modules[i], max_warnings, + &num_warnings); } - if (!err) { - drgn_debug_info_free_modules(dbinfo, true, false); - err = drgn_dwarf_info_update_index(&index); + if (num_warnings > max_warnings) { + drgn_log_warning(prog, "... missing %u more", + num_warnings - max_warnings); } - drgn_dwarf_index_state_deinit(&index); - return err; -} -struct drgn_error * -drgn_debug_info_report_flush(struct drgn_debug_info_load_state *load) -{ - struct drgn_debug_info *dbinfo = load->dbinfo; - my_dwfl_report_end(dbinfo, NULL, NULL); - struct drgn_error *err = drgn_debug_info_update_index(load); - dwfl_report_begin_add(dbinfo->dwfl); + // Update the DWARF index eagerly, mostly because that's what we did + // back when we used libdwfl. We may want to remove this in the future. + err = drgn_dwarf_info_update_index(&prog->dbinfo); if (err) return err; - drgn_module_vector_clear(&load->new_modules); - return NULL; -} - -static struct drgn_error * -drgn_debug_info_report_finalize_errors(struct drgn_debug_info_load_state *load) -{ - if (load->num_errors > load->max_errors && - (!string_builder_line_break(&load->errors) || - !string_builder_appendf(&load->errors, "... %u more", - load->num_errors - load->max_errors))) { - string_builder_deinit(&load->errors); - return &drgn_enomem; - } - if (load->num_errors) { - return drgn_error_from_string_builder(DRGN_ERROR_MISSING_DEBUG_INFO, - &load->errors); - } else { - return NULL; - } -} -struct drgn_error *drgn_debug_info_load(struct drgn_debug_info *dbinfo, - const char **paths, size_t n, - bool load_default, bool load_main) -{ - struct drgn_program *prog = dbinfo->prog; - struct drgn_error *err; - - if (load_default) - load_main = true; - - const char *max_errors = getenv("DRGN_MAX_DEBUG_INFO_ERRORS"); - struct drgn_debug_info_load_state load = { - .dbinfo = dbinfo, - .paths = paths, - .num_paths = n, - .load_default = load_default, - .load_main = load_main, - .new_modules = VECTOR_INIT, - .errors = STRING_BUILDER_INIT, - .max_errors = max_errors ? atoi(max_errors) : 5, - }; - dwfl_report_begin_add(dbinfo->dwfl); - if (prog->flags & DRGN_PROGRAM_IS_LINUX_KERNEL) - err = linux_kernel_report_debug_info(&load); - else - err = userspace_report_debug_info(&load); - my_dwfl_report_end(dbinfo, NULL, NULL); - if (err) - goto err; - - /* - * userspace_report_debug_info() reports the main debugging information - * directly with libdwfl, so we need to report it to dbinfo. - */ - if (!(prog->flags & DRGN_PROGRAM_IS_LINUX_KERNEL) && load_main && - dwfl_getmodules(dbinfo->dwfl, drgn_debug_info_report_dwfl_module, - &load, 0)) { - err = &drgn_enomem; - goto err; + if (num_wanted_modules > 0 || iterator_tried_missing) { + return drgn_error_create(DRGN_ERROR_MISSING_DEBUG_INFO, + "missing some debugging symbols; see https://drgn.readthedocs.io/en/latest/getting_debugging_symbols.html"); } - err = drgn_debug_info_update_index(&load); - if (err) - goto err; - - /* - * TODO: for core dumps, we need to add memory reader segments for - * read-only segments of the loaded binaries since those aren't saved in - * the core dump. - */ - - err = drgn_debug_info_report_finalize_errors(&load); -out: - drgn_module_vector_deinit(&load.new_modules); - return err; - -err: - drgn_debug_info_free_modules(dbinfo, false, false); - string_builder_deinit(&load.errors); - goto out; + return NULL; } -struct elf_symbols_search_arg { - const char *name; - uint64_t address; - enum drgn_find_symbol_flags flags; +LIBDRGN_PUBLIC struct drgn_error * +drgn_load_module_debug_info(struct drgn_module **modules, size_t *num_modulesp) +{ struct drgn_error *err; - struct drgn_symbol_result_builder *builder; -}; -static bool elf_symbol_match(struct elf_symbols_search_arg *arg, GElf_Addr addr, - const GElf_Sym *sym, const char *name) -{ - if ((arg->flags & DRGN_FIND_SYMBOL_NAME) && strcmp(name, arg->name) != 0) - return false; - if ((arg->flags & DRGN_FIND_SYMBOL_ADDR) && - (arg->address < addr || arg->address >= addr + sym->st_size)) - return false; - return true; -} + const size_t orig_num_modules = *num_modulesp; + if (orig_num_modules == 0) + return NULL; -static bool elf_symbol_store_match(struct elf_symbols_search_arg *arg, - GElf_Sym *elf_sym, GElf_Addr addr, - const char *name) -{ - struct drgn_symbol *sym; - if (arg->flags == (DRGN_FIND_SYMBOL_ONE | DRGN_FIND_SYMBOL_NAME)) { - int binding = GELF_ST_BIND(elf_sym->st_info); - /* - * The order of precedence is - * GLOBAL = UNIQUE > WEAK > LOCAL = everything else - * - * If we found a global or unique symbol, return it - * immediately. If we found a weak symbol, then save it, - * which may overwrite a previously found weak or local - * symbol. Otherwise, save the symbol only if we haven't - * found another symbol. - */ - if (binding != STB_GLOBAL - && binding != STB_GNU_UNIQUE - && binding != STB_WEAK - && drgn_symbol_result_builder_count(arg->builder) > 0) - return false; - sym = malloc(sizeof(*sym)); - if (!sym) { - arg->err = &drgn_enomem; - return true; - } - drgn_symbol_from_elf(name, addr, elf_sym, sym); - if (!drgn_symbol_result_builder_add(arg->builder, sym)) { - arg->err = &drgn_enomem; - drgn_symbol_destroy(sym); - } + struct drgn_program *prog = modules[0]->prog; + drgn_log_debug(prog, "loading debugging symbols for %zu modules", + orig_num_modules); - /* Abort on error, or short-circuit if we found a global or - * unique symbol */ - return (arg->err || sym->binding == DRGN_SYMBOL_BINDING_GLOBAL - || sym->binding == DRGN_SYMBOL_BINDING_UNIQUE); - } else { - sym = malloc(sizeof(*sym)); - if (!sym) { - arg->err = &drgn_enomem; - return true; + size_t num_wanted_modules = 0; + for (size_t i = 0; i < orig_num_modules; i++) { + if (modules[i]->prog != prog) { + return drgn_error_create(DRGN_ERROR_INVALID_ARGUMENT, + "modules are from different programs"); } - drgn_symbol_from_elf(name, addr, elf_sym, sym); - if (!drgn_symbol_result_builder_add(arg->builder, sym)) { - arg->err = &drgn_enomem; - drgn_symbol_destroy(sym); + if (drgn_module_wants_file(modules[i])) { + modules[num_wanted_modules++] = modules[i]; + } else if (modules[i]->loaded_file_status == DRGN_MODULE_FILE_DONT_WANT + || modules[i]->loaded_file_status == DRGN_MODULE_FILE_DONT_WANT) { + drgn_log_debug(prog, + "debugging symbols not wanted for %s", + modules[i]->name); + } else { + drgn_log_debug(prog, + "debugging symbols already loaded for %s", + modules[i]->name); } - /* Abort on error, or short-circuit for single lookup */ - return (arg->err || (arg->flags & DRGN_FIND_SYMBOL_ONE)); } -} + if (num_wanted_modules == 0) { + *num_modulesp = 0; + return NULL; + } -static int elf_symbols_search_cb(Dwfl_Module *dwfl_module, void **userdatap, - const char *module_name, Dwarf_Addr base, - void *cb_arg) -{ - struct elf_symbols_search_arg *arg = cb_arg; + uint64_t generation = ++prog->dbinfo.load_debug_info_generation; + for (size_t i = 0; i < num_wanted_modules; i++) + modules[i]->load_debug_info_generation = generation; - int symtab_len = dwfl_module_getsymtab(dwfl_module); - if (symtab_len == -1) - return DWARF_CB_OK; + drgn_blocking_guard(prog); - /* Ignore the zeroth null symbol */ - for (int i = 1; i < symtab_len; i++) { - GElf_Sym elf_sym; - GElf_Addr elf_addr; - const char *name = dwfl_module_getsym_info(dwfl_module, i, - &elf_sym, &elf_addr, - NULL, NULL, NULL); - if (!name || !elf_symbol_match(arg, elf_addr, &elf_sym, name)) - continue; - if (elf_symbol_store_match(arg, &elf_sym, elf_addr, name)) - return DWARF_CB_ABORT; + const size_t orig_num_wanted_modules = num_wanted_modules; + drgn_handler_list_for_each_enabled(struct drgn_debug_info_finder, + finder, + &prog->dbinfo.debug_info_finders) { + err = finder->ops.find(modules, num_wanted_modules, + finder->arg); + if (err) + return err; + size_t new_num_wanted_modules = 0; + for (size_t i = 0; i < num_wanted_modules; i++) { + if (drgn_module_wants_file(modules[i])) + modules[new_num_wanted_modules++] = modules[i]; + } + num_wanted_modules = new_num_wanted_modules; + if (num_wanted_modules == 0) + break; } - return DWARF_CB_OK; + drgn_log_debug(prog, "debugging symbols loaded for %zu/%zu modules", + orig_num_wanted_modules - num_wanted_modules, + orig_num_wanted_modules); + *num_modulesp = num_wanted_modules; + return NULL; } static struct drgn_error * -elf_symbols_search(const char *name, uint64_t addr, enum drgn_find_symbol_flags flags, - void *data, struct drgn_symbol_result_builder *builder) +elf_symbols_search(const char *name, uint64_t addr, + enum drgn_find_symbol_flags flags, void *data, + struct drgn_symbol_result_builder *builder) { - Dwfl_Module *dwfl_module = NULL; + struct drgn_error *err; struct drgn_program *prog = data; - struct elf_symbols_search_arg arg = { - .name = name, - .address = addr, - .flags = flags, - .err = NULL, - .builder = builder, - }; - - if (arg.flags & DRGN_FIND_SYMBOL_ADDR) { - dwfl_module = dwfl_addrmodule(prog->dbinfo.dwfl, arg.address); - if (!dwfl_module) - return NULL; - } - if ((arg.flags & (DRGN_FIND_SYMBOL_ADDR | DRGN_FIND_SYMBOL_ONE)) - == (DRGN_FIND_SYMBOL_ADDR | DRGN_FIND_SYMBOL_ONE)) { - GElf_Off offset; - GElf_Sym elf_sym; - const char *sym_name = dwfl_module_addrinfo(dwfl_module, addr, - &offset, &elf_sym, - NULL, NULL, NULL); - if (!sym_name) + if (flags & DRGN_FIND_SYMBOL_ADDR) { + struct drgn_module *module = + drgn_module_find_by_address(prog, addr); + if (!module) return NULL; - struct drgn_symbol *sym = malloc(sizeof(*sym)); - if (!sym) - return &drgn_enomem; - drgn_symbol_from_elf(sym_name, addr - offset, &elf_sym, sym); - if (!drgn_symbol_result_builder_add(builder, sym)) { - arg.err = &drgn_enomem; - drgn_symbol_destroy(sym); - } - } else if (dwfl_module) { - elf_symbols_search_cb(dwfl_module, NULL, NULL, 0, &arg); + return drgn_module_elf_symbols_search(module, name, addr, flags, + builder); } else { - dwfl_getmodules(prog->dbinfo.dwfl, elf_symbols_search_cb, &arg, 0); + if (prog->dbinfo.main_module) { + err = drgn_module_elf_symbols_search(prog->dbinfo.main_module, + name, addr, flags, + builder); + if (err == &drgn_stop) + return NULL; + if (err) + return err; + } + for (auto it = drgn_module_table_first(&prog->dbinfo.modules); + it.entry; it = drgn_module_table_next(it)) { + err = drgn_module_elf_symbols_search(*it.entry, name, + addr, flags, + builder); + if (err == &drgn_stop) + break; + if (err) + return err; + } + return NULL; } - return arg.err; -} - -bool drgn_debug_info_is_indexed(struct drgn_debug_info *dbinfo, - const char *name) -{ - return c_string_set_search(&dbinfo->module_names, &name).entry != NULL; } void drgn_debug_info_init(struct drgn_debug_info *dbinfo, struct drgn_program *prog) { + elf_version(EV_CURRENT); dbinfo->prog = prog; - dbinfo->dwfl = dwfl_begin(&drgn_dwfl_callbacks); - // This is temporary until we stop using libdwfl, and is extremely - // unlikely to fail anwyays, so don't bother propagating an error up. - if (!dbinfo->dwfl) - abort(); + drgn_module_table_init(&dbinfo->modules); + drgn_module_address_tree_init(&dbinfo->modules_by_address); const struct drgn_type_finder_ops type_finder_ops = { .find = drgn_debug_info_find_type, }; @@ -2215,26 +5372,53 @@ void drgn_debug_info_init(struct drgn_debug_info *dbinfo, drgn_program_register_symbol_finder_impl(prog, &dbinfo->symbol_finder, "elf", &symbol_finder_ops, prog, 0); + const struct drgn_debug_info_finder_ops + standard_debug_info_finder_ops = { + .find = drgn_standard_module_file_find, + }; + drgn_program_register_debug_info_finder_impl(prog, + &dbinfo->standard_debug_info_finder, + "standard", + &standard_debug_info_finder_ops, + prog, 0); + dbinfo->debug_info_path = drgn_default_debug_info_path; #if WITH_DEBUGINFOD dbinfo->debuginfod_client = NULL; + if (drgn_have_debuginfod()) { + const struct drgn_debug_info_finder_ops + debuginfod_debug_info_finder_ops = { + .find = drgn_debuginfod_find, + }; + drgn_program_register_debug_info_finder_impl(prog, + &dbinfo->debuginfod_debug_info_finder, + "debuginfod", + &debuginfod_debug_info_finder_ops, + prog, + DRGN_HANDLER_REGISTER_ENABLE_LAST); + } #endif - drgn_module_table_init(&dbinfo->modules); - c_string_set_init(&dbinfo->module_names); drgn_dwarf_info_init(dbinfo); } void drgn_debug_info_deinit(struct drgn_debug_info *dbinfo) { - drgn_dwarf_info_deinit(dbinfo); - c_string_set_deinit(&dbinfo->module_names); - drgn_debug_info_free_modules(dbinfo, false, true); - assert(drgn_module_table_empty(&dbinfo->modules)); - drgn_module_table_deinit(&dbinfo->modules); + free(dbinfo->map_files_segments); + if (dbinfo->debug_info_path != drgn_default_debug_info_path) + free((char *)dbinfo->debug_info_path); #if WITH_DEBUGINFOD if (dbinfo->debuginfod_client) drgn_debuginfod_end(dbinfo->debuginfod_client); #endif - dwfl_end(dbinfo->dwfl); + drgn_handler_list_deinit(struct drgn_debug_info_finder, finder, + &dbinfo->debug_info_finders, + if (finder->ops.destroy) + finder->ops.destroy(finder->arg); + ); + drgn_dwarf_info_deinit(dbinfo); + for (auto it = drgn_module_table_first(&dbinfo->modules); it.entry; + it = drgn_module_table_next(it)) + drgn_module_destroy(*it.entry); + drgn_module_table_deinit(&dbinfo->modules); } struct drgn_elf_file *drgn_module_find_dwarf_file(struct drgn_module *module, @@ -2242,7 +5426,7 @@ struct drgn_elf_file *drgn_module_find_dwarf_file(struct drgn_module *module, { if (!module->debug_file) return NULL; - if (dwarf == module->debug_file->dwarf) + if (dwarf == module->debug_file->_dwarf) return module->debug_file; struct drgn_elf_file_dwarf_table_iterator it = drgn_elf_file_dwarf_table_search(&module->split_dwarf_files, @@ -2256,15 +5440,11 @@ drgn_module_create_split_dwarf_file(struct drgn_module *module, struct drgn_elf_file **ret) { struct drgn_error *err; - err = drgn_elf_file_create(module, name, dwarf_getelf(dwarf), ret); + err = drgn_elf_file_create(module, name, -1, NULL, dwarf_getelf(dwarf), + ret); if (err) return err; - err = drgn_elf_file_precache_sections(*ret); - if (err) { - drgn_elf_file_destroy(*ret); - return err; - } - (*ret)->dwarf = dwarf; + (*ret)->_dwarf = dwarf; int r = drgn_elf_file_dwarf_table_insert(&module->split_dwarf_files, ret, NULL); if (r < 0) { @@ -2350,135 +5530,3 @@ drgn_module_find_cfi(struct drgn_program *prog, struct drgn_module *module, } return &drgn_not_found; } - -#if !_ELFUTILS_PREREQ(0, 175) -static Elf *dwelf_elf_begin(int fd) -{ - return elf_begin(fd, ELF_C_READ_MMAP_PRIVATE, NULL); -} -#endif - -struct drgn_error *open_elf_file(const char *path, int *fd_ret, Elf **elf_ret) -{ - struct drgn_error *err; - - *fd_ret = open(path, O_RDONLY); - if (*fd_ret == -1) - return drgn_error_create_os("open", errno, path); - *elf_ret = dwelf_elf_begin(*fd_ret); - if (!*elf_ret) { - err = drgn_error_libelf(); - goto err_fd; - } - if (elf_kind(*elf_ret) != ELF_K_ELF) { - err = drgn_error_create(DRGN_ERROR_OTHER, "not an ELF file"); - goto err_elf; - } - return NULL; - -err_elf: - elf_end(*elf_ret); -err_fd: - close(*fd_ret); - return err; -} - -struct drgn_error *find_elf_file(char **path_ret, int *fd_ret, Elf **elf_ret, - const char * const *path_formats, ...) -{ - struct drgn_error *err; - size_t i; - - for (i = 0; path_formats[i]; i++) { - va_list ap; - int ret; - char *path; - int fd; - Elf *elf; - - va_start(ap, path_formats); - ret = vasprintf(&path, path_formats[i], ap); - va_end(ap); - if (ret == -1) - return &drgn_enomem; - fd = open(path, O_RDONLY); - if (fd == -1) { - free(path); - continue; - } - elf = dwelf_elf_begin(fd); - if (!elf) { - close(fd); - free(path); - continue; - } - if (elf_kind(elf) != ELF_K_ELF) { - err = drgn_error_format(DRGN_ERROR_OTHER, - "%s: not an ELF file", path); - elf_end(elf); - close(fd); - free(path); - return err; - } - *path_ret = path; - *fd_ret = fd; - *elf_ret = elf; - return NULL; - } - *path_ret = NULL; - *fd_ret = -1; - *elf_ret = NULL; - return NULL; -} - -/* - * Get the start address from the first loadable segment and the end address - * from the last loadable segment. - * - * The ELF specification states that loadable segments are sorted on p_vaddr. - * However, vmlinux on x86-64 has an out of order segment for .data..percpu, and - * Arm has a couple for .vector and .stubs. Thankfully, those are placed in the - * middle by the vmlinux linker script, so we can still rely on the first and - * last loadable segments. - */ -struct drgn_error *elf_address_range(Elf *elf, uint64_t bias, - uint64_t *start_ret, uint64_t *end_ret) -{ - size_t phnum; - if (elf_getphdrnum(elf, &phnum) != 0) - return drgn_error_libelf(); - - GElf_Phdr phdr_mem, *phdr; - size_t i; - for (i = 0; i < phnum; i++) { - phdr = gelf_getphdr(elf, i, &phdr_mem); - if (!phdr) - return drgn_error_libelf(); - if (phdr->p_type == PT_LOAD) { - uint64_t align = phdr->p_align ? phdr->p_align : 1; - *start_ret = (phdr->p_vaddr & -align) + bias; - break; - } - } - if (i >= phnum) { - /* There were no loadable segments. */ - *start_ret = *end_ret = 0; - return NULL; - } - - for (i = phnum; i-- > 0;) { - phdr = gelf_getphdr(elf, i, &phdr_mem); - if (!phdr) - return drgn_error_libelf(); - if (phdr->p_type == PT_LOAD) { - *end_ret = (phdr->p_vaddr + phdr->p_memsz) + bias; - if (*start_ret >= *end_ret) - *start_ret = *end_ret = 0; - return NULL; - } - } - /* We found a loadable segment earlier, so this shouldn't happen. */ - assert(!"PT_LOAD segment disappeared"); - *end_ret = 0; - return NULL; -} diff --git a/libdrgn/debug_info.h b/libdrgn/debug_info.h index 614b4233b..2241ef3a8 100644 --- a/libdrgn/debug_info.h +++ b/libdrgn/debug_info.h @@ -16,12 +16,13 @@ #include #endif #include -#include #include +#include "binary_search_tree.h" #include "cfi.h" #include "drgn_internal.h" #include "dwarf_info.h" +#include "elf_symtab.h" #include "hash_table.h" #include "object.h" #include "orc_info.h" @@ -45,121 +46,84 @@ struct drgn_elf_file; * @{ */ -/** State of a @ref drgn_module. */ -enum drgn_module_state { - /** Reported but not indexed. */ - DRGN_DEBUG_INFO_MODULE_NEW, - /** Reported and will be indexed on success. */ - DRGN_DEBUG_INFO_MODULE_INDEXING, - /** Indexed. Must not be freed until @ref drgn_debug_info_destroy(). */ - DRGN_DEBUG_INFO_MODULE_INDEXED, -} __attribute__((__packed__)); - DEFINE_HASH_TABLE_TYPE(drgn_elf_file_dwarf_table, struct drgn_elf_file *); +DEFINE_HASH_TABLE_TYPE(drgn_module_table, struct drgn_module *); +DEFINE_BINARY_SEARCH_TREE_TYPE(drgn_module_address_tree, struct drgn_module); -/** - * A module reported to a @ref drgn_debug_info. - * - * Conceptually, a module is an ELF file loaded at a specific address range (or - * not loaded). - * - * Files are identified by canonical path and, if present, build ID. Each (path, - * address range) is uniquely represented by a @ref drgn_module. - */ -struct drgn_module { +struct drgn_debug_info_finder { + struct drgn_handler handler; + struct drgn_debug_info_finder_ops ops; + void *arg; +}; + +/** Cache of debugging information. */ +struct drgn_debug_info { + /** Program owning this cache. */ struct drgn_program *prog; - /** @c NULL if the module does not have a build ID. */ - const void *build_id; - /** Zero if the module does not have a build ID. */ - size_t build_id_len; - /** Load address range, or both 0 if not loaded. */ - uint64_t start, end; - /** Optional module name allocated with @c malloc(). */ - char *name; + struct drgn_type_finder type_finder; + struct drgn_object_finder object_finder; + struct drgn_symbol_finder symbol_finder; - Dwfl_Module *dwfl_module; - /** File that is loaded into the program. */ - struct drgn_elf_file *loaded_file; - /** File containing debugging information. */ - struct drgn_elf_file *debug_file; + /** Main module. @c NULL if not created yet. */ + struct drgn_module *main_module; + /** Table of non-main modules indexed on @ref drgn_module_key. */ + struct drgn_module_table modules; /** - * Difference between addresses in program and addresses in @ref - * drgn_module::loaded_file. + * Counter used to detect when @ref modules is modified during iteration + * of a @ref drgn_created_module_iterator. */ - uint64_t loaded_file_bias; + uint64_t modules_generation; + /** Tree of modules sorted by start address. */ + struct drgn_module_address_tree modules_by_address; /** - * Difference between addresses in program and addresses in @ref - * drgn_module::debug_file. + * Singly-linked list of modules that need to have their DWARF + * information indexed. */ - uint64_t debug_file_bias; - - struct drgn_elf_file_dwarf_table split_dwarf_files; - + struct drgn_module *modules_pending_indexing; /** DWARF debugging information. */ - struct drgn_module_dwarf_info dwarf; - /** ORC unwinder information. */ - struct drgn_module_orc_info orc; - - /** Whether DWARF CFI from .debug_frame has been parsed. */ - bool parsed_debug_frame; - /** Whether EH CFI from .eh_frame has been parsed. */ - bool parsed_eh_frame; - /** Whether ORC unwinder data has been parsed. */ - bool parsed_orc; + struct drgn_dwarf_info dwarf; - /* - * path, elf, and fd are used when an ELF file was reported with - * drgn_debug_info_report_elf() so we can report the file to libdwfl - * later. They are not valid after loading. + struct drgn_handler_list debug_info_finders; + struct drgn_debug_info_finder standard_debug_info_finder; + /** See @ref drgn_program_debug_info_path(). */ + const char *debug_info_path; + /** + * Counter used to detect when loading debugging information is + * attempted. + * + * @sa drgn_module::load_debug_info_generation */ - char *path; - Elf *elf; - int fd; - enum drgn_module_state state; - /** Error while loading. */ - struct drgn_error *err; + uint64_t load_debug_info_generation; /** - * Next module with same build ID and address range. + * Counter used to detect when the wanted supplementary file for a + * module has changed. * - * There may be multiple files with the same build ID (e.g., a stripped - * binary and its corresponding separate debug info file). While - * loading, all files with the same build ID and address range are - * linked in a list. Only one is indexed; the rest are destroyed. + * @sa drgn_module_wanted_supplementary_file::generation */ - struct drgn_module *next; -}; - -DEFINE_HASH_TABLE_TYPE(drgn_module_table, struct drgn_module *); - -DEFINE_HASH_SET_TYPE(c_string_set, const char *); - -/** Cache of debugging information. */ -struct drgn_debug_info { - /** Program owning this cache. */ - struct drgn_program *prog; + uint64_t supplementary_file_generation; - struct drgn_type_finder type_finder; - struct drgn_object_finder object_finder; - struct drgn_symbol_finder symbol_finder; - - /** DWARF frontend library handle. */ - Dwfl *dwfl; #if WITH_DEBUGINFOD + struct drgn_debug_info_finder debuginfod_debug_info_finder; /** debuginfod-client session. */ debuginfod_client *debuginfod_client; + const char *debuginfod_current_name; + const char *debuginfod_current_type; + unsigned int debuginfod_spinner_position; + bool debuginfod_have_url; + bool logged_debuginfod_progress; #endif - /** Modules keyed by build ID and address range. */ - struct drgn_module_table modules; + bool logged_no_debuginfod; + /** - * Names of indexed modules. - * - * The entries in this set are @ref drgn_module::name, so they should - * not be freed. + * Cache of entries in /proc/$pid/map_files used for finding loaded + * files. Populated the first time we need it or opportunistically when + * we parse /proc/$pid/maps. Rebuilt whenever we try to open an entry + * that no longer exists. */ - struct c_string_set module_names; - /** DWARF debugging information. */ - struct drgn_dwarf_info dwarf; + struct drgn_map_files_segment *map_files_segments; + /** Number of segments in @ref map_files_segments. */ + size_t num_map_files_segments; }; /** Initialize a @ref drgn_debug_info. */ @@ -169,96 +133,188 @@ void drgn_debug_info_init(struct drgn_debug_info *dbinfo, /** Deinitialize a @ref drgn_debug_info. */ void drgn_debug_info_deinit(struct drgn_debug_info *dbinfo); -DEFINE_VECTOR_TYPE(drgn_module_vector, struct drgn_module *); - -/** State tracked while loading debugging information. */ -struct drgn_debug_info_load_state { - struct drgn_debug_info * const dbinfo; - const char ** const paths; - const size_t num_paths; - const bool load_default; - const bool load_main; - /** Newly added modules to be indexed. */ - struct drgn_module_vector new_modules; - /** Formatted errors reported by @ref drgn_debug_info_report_error(). */ - struct string_builder errors; - /** Number of errors reported by @ref drgn_debug_info_report_error(). */ - unsigned int num_errors; - /** Maximum number of errors to report before truncating. */ - unsigned int max_errors; +typedef void drgn_module_iterator_destroy_fn(struct drgn_module_iterator *); +typedef struct drgn_error * +drgn_module_iterator_next_fn(struct drgn_module_iterator *, + struct drgn_module **, bool *); + +struct drgn_module_iterator { + struct drgn_program *prog; + drgn_module_iterator_destroy_fn *destroy; + drgn_module_iterator_next_fn *next; }; -/** - * Report a non-fatal error while loading debugging information. - * - * The error will be included in a @ref DRGN_ERROR_MISSING_DEBUG_INFO error - * returned by @ref drgn_debug_info_load(). - * - * @param[name] name An optional module name to prefix to the error message. - * @param[message] message An optional message with additional context to prefix - * to the error message. - * @param[err] err The error to report. This may be @c NULL if @p name and @p - * message provide sufficient information. This is destroyed on either success - * or failure. - * @return @c NULL on success, @ref drgn_enomem if the error could not be - * reported. - */ -struct drgn_error * -drgn_debug_info_report_error(struct drgn_debug_info_load_state *load, - const char *name, const char *message, - struct drgn_error *err); +static inline void +drgn_module_iterator_init(struct drgn_module_iterator *it, + struct drgn_program *prog, + drgn_module_iterator_destroy_fn *destroy, + drgn_module_iterator_next_fn *next) +{ + it->prog = prog; + it->destroy = destroy; + it->next = next; +} + +/** Bitmask of files in a @ref drgn_module. */ +enum drgn_module_file_mask { + DRGN_MODULE_FILE_MASK_LOADED = 1 << 0, + DRGN_MODULE_FILE_MASK_DEBUG = 1 << 1, +} __attribute__((__packed__)); -/** - * Report a module to a @ref drgn_debug_info from an ELF file. - * - * This takes ownership of @p fd and @p elf on either success or failure. They - * should not be used (including closed or freed) after this returns. - * - * @param[in] path The path to the file. - * @param[in] fd A file descriptor referring to the file. - * @param[in] elf The Elf handle of the file. - * @param[in] start The (inclusive) start address of the loaded file, or 0 if - * the file is not loaded. - * @param[in] end The (exclusive) end address of the loaded file, or 0 if the - * file is not loaded. - * @param[in] name An optional name for the module. This is only used for @ref - * drgn_debug_info_is_indexed(). - * @param[out] new_ret Whether the module was newly created and reported. This - * is @c false if a module with the same build ID and address range was already - * loaded or a file with the same path and address range was already reported. - */ -struct drgn_error * -drgn_debug_info_report_elf(struct drgn_debug_info_load_state *load, - const char *path, int fd, Elf *elf, uint64_t start, - uint64_t end, const char *name, bool *new_ret); +DEFINE_HASH_MAP_TYPE(drgn_module_section_address_map, char *, uint64_t); -/** Index new debugging information and continue reporting. */ -struct drgn_error * -drgn_debug_info_report_flush(struct drgn_debug_info_load_state *load); +struct drgn_module { + struct drgn_program *prog; + enum drgn_module_kind kind; -/** - * Load debugging information. - * - * @sa drgn_program_load_debug_info - */ -struct drgn_error *drgn_debug_info_load(struct drgn_debug_info *dbinfo, - const char **paths, size_t n, - bool load_default, bool load_main); + /** Module name. */ + char *name; + /** Kind-specific information. */ + union { + struct { + uint64_t dynamic_address; + } shared_library; + struct { + uint64_t dynamic_address; + } vdso; + struct { + uint64_t address; + } relocatable; + struct { + uint64_t id; + } extra; + }; + /** + * Raw binary build ID. @c NULL if the module does not have a build ID. + */ + void *build_id; + /** + * Length of @ref drgn_module::build_id in bytes. Zero if the module + * does not have a build ID. + */ + size_t build_id_len; + /** + * Build ID as a null-terminated hexadecimal string. @c NULL if the + * module does not have a build ID. + * + * Used for logging and finding debugging information. + * + * This is allocated together with @ref drgn_module::build_id. + */ + char *build_id_str; + /** Node in @ref drgn_debug_info::modules_by_address. */ + struct binary_tree_node node; + /** + * Load address range. Both 0 if not loaded. Both @c UINT64_MAX if not + * known yet. + */ + uint64_t start, end; + + struct drgn_elf_file *loaded_file; + struct drgn_elf_file *debug_file; + struct drgn_elf_file *supplementary_debug_file; + /** Table mapping libdw handle to corresponding @ref drgn_elf_file. */ + struct drgn_elf_file_dwarf_table split_dwarf_files; + uint64_t loaded_file_bias; + uint64_t debug_file_bias; + enum drgn_module_file_status loaded_file_status; + enum drgn_module_file_status debug_file_status; + enum drgn_supplementary_file_kind supplementary_debug_file_kind; + + /** DWARF debugging information. */ + struct drgn_module_dwarf_info dwarf; + /** ORC unwinder information. */ + struct drgn_module_orc_info orc; + /** ELF symbol table. */ + struct drgn_elf_symbol_table elf_symtab; + + /** Whether .debug_frame has been parsed. */ + bool parsed_debug_frame; + /** Whether .eh_frame has been parsed. */ + bool parsed_eh_frame; + /** Whether ORC unwinder data has been parsed. */ + bool parsed_orc; + /** Which files need to be checked for an ELF symbol table. */ + enum drgn_module_file_mask elf_symtab_pending_files; + /** + * Whether a full symbol table has been found (as opposed to a dynamic + * symbol table, which only contains a subset of symbols). + */ + bool have_full_symtab; + + /** Mapping from section name to address. */ + struct drgn_module_section_address_map section_addresses; + /** + * Counter used to detect when @ref section_addresses is modified during + * iteration of a @ref drgn_module_section_address_iterator. + */ + uint64_t section_addresses_generation; + + /** + * Counter used to detect when loading debugging information is + * attempted. + * + * @sa drgn_debug_info::load_debug_info_generation + */ + uint64_t load_debug_info_generation; + struct drgn_module_wanted_supplementary_file *wanted_supplementary_debug_file; + /** Node in @ref drgn_debug_info::modules_pending_indexing. */ + struct drgn_module *pending_indexing_next; +}; + +struct drgn_error *drgn_module_find_or_create(struct drgn_program *prog, + const struct drgn_module_key *key, + const char *name, + struct drgn_module **ret, + bool *new_ret); /** - * Return whether a @ref drgn_debug_info has indexed a module with the given - * name. + * Delete a partially-initialized module. This can only be called before the + * module is returned from public API. */ -bool drgn_debug_info_is_indexed(struct drgn_debug_info *dbinfo, - const char *name); +void drgn_module_delete(struct drgn_module *module); + +static inline void drgn_module_deletep(struct drgn_module **modulep) +{ + if (*modulep) + drgn_module_delete(*modulep); +} + +struct depmod_index { + char *path; + void *addr; + size_t len; +}; + +struct drgn_module_standard_files_state { + struct depmod_index modules_dep; +}; + +// Always takes ownership of fd. Attempts to resolve the real path of path. +struct drgn_error * +drgn_module_try_standard_file(struct drgn_module *module, const char *path, + int fd, bool check_build_id, + const uint32_t *expected_crc); + +#define drgn_program_for_each_debug_dir(prog, debug_dir, debug_dir_len) \ + for (debug_dir = (prog)->dbinfo.debug_info_path; \ + debug_dir \ + && (debug_dir_len = strchrnul(debug_dir, ':') - debug_dir, 1); \ + debug_dir = debug_dir[debug_dir_len] == '\0' \ + ? NULL : debug_dir + debug_dir_len + 1) + +static inline bool drgn_module_wants_file(struct drgn_module *module) +{ + return drgn_module_wants_loaded_file(module) + || drgn_module_wants_debug_file(module); +} /** * Get the language of the program's `main` function or `NULL` if it could not * be found. */ -struct drgn_error * -drgn_debug_info_main_language(struct drgn_debug_info *dbinfo, - const struct drgn_language **ret); +const struct drgn_language * +drgn_debug_info_main_language(struct drgn_debug_info *dbinfo); /** @ref drgn_type_finder_ops::find() that uses debugging information. */ struct drgn_error *drgn_debug_info_find_type(uint64_t kinds, const char *name, diff --git a/libdrgn/drgn.h b/libdrgn/drgn.h index 59224c333..59502fc8e 100644 --- a/libdrgn/drgn.h +++ b/libdrgn/drgn.h @@ -819,19 +819,6 @@ struct drgn_error *drgn_program_set_kernel(struct drgn_program *prog); */ struct drgn_error *drgn_program_set_pid(struct drgn_program *prog, pid_t pid); -/** - * Load debugging information for a list of executable or library files. - * - * @param[in] load_default Whether to also load debugging information which can - * automatically be determined from the program. This implies @p load_main. - * @param[in] load_main Whether to also load information for the main - * executable. - */ -struct drgn_error *drgn_program_load_debug_info(struct drgn_program *prog, - const char **paths, size_t n, - bool load_default, - bool load_main); - /** * Create a @ref drgn_program from a core dump file. * @@ -1199,6 +1186,546 @@ struct drgn_error *drgn_program_element_info(struct drgn_program *prog, /** @} */ +/** + * @defgroup Modules Modules + * + * Modules in a program and debugging information. + * + * @{ + */ + +/** An executable, library, or other binary file used by a program. */ +struct drgn_module; + +/** Kinds of modules. */ +enum drgn_module_kind { + /** + * Main module. For userspace programs, this is the executable. For the + * Linux kernel, this is `vmlinux`. + */ + DRGN_MODULE_MAIN, + /** Shared library (a.k.a. dynamic library or dynamic shared object). */ + DRGN_MODULE_SHARED_LIBRARY, + /** Virtual dynamic shared object (vDSO). */ + DRGN_MODULE_VDSO, + /** Relocatable object (e.g., Linux kernel loadable module). */ + DRGN_MODULE_RELOCATABLE, + /** Extra debugging information. */ + DRGN_MODULE_EXTRA, +} __attribute__((__packed__)); + +/** Unique key for a @ref drgn_module. */ +struct drgn_module_key { + /** Kind of module. */ + enum drgn_module_kind kind; + /** Kind-specific key. */ + union { + struct { + /** Name of module. */ + const char *name; + /** Address of dynamic section. */ + uint64_t dynamic_address; + } shared_library; + struct { + /** Name of module. */ + const char *name; + /** Address of dynamic section. */ + uint64_t dynamic_address; + } vdso; + struct { + /** Name of module. */ + const char *name; + /** + * Address identifying the module (e.g., for Linux + * kernel loadable modules, the base address). + */ + uint64_t address; + } relocatable; + struct { + /** Name of module. */ + const char *name; + /** Arbitrary identification number. */ + uint64_t id; + } extra; + }; +}; + +/** + * Find the created @ref drgn_module matching the given @p key. + * + * @return Module, or @c NULL if not found. + */ +struct drgn_module *drgn_module_find(struct drgn_program *prog, + const struct drgn_module_key *key); + +/** + * Find the created @ref drgn_module containing the given @p address. + * + * @return Module, or @c NULL if not found. + */ +struct drgn_module *drgn_module_find_by_address(struct drgn_program *prog, + uint64_t address); + +/** + * Find the main module, creating it if it doesn't already exist. + * + * @param[out] new_ret @c true if the module was newly created, @c false if it + * was found. + */ +struct drgn_error *drgn_module_find_or_create_main(struct drgn_program *prog, + const char *name, + struct drgn_module **ret, + bool *new_ret); + +/** + * Find a shared library module, creating it if it doesn't already exist. + * + * @param[out] new_ret @c true if the module was newly created, @c false if it + * was found. + */ +struct drgn_error * +drgn_module_find_or_create_shared_library(struct drgn_program *prog, + const char *name, + uint64_t dynamic_address, + struct drgn_module **ret, + bool *new_ret); + +/** + * Find a vDSO module, creating it if it doesn't already exist. + * + * @param[out] new_ret @c true if the module was newly created, @c false if it + * was found. + */ +struct drgn_error *drgn_module_find_or_create_vdso(struct drgn_program *prog, + const char *name, + uint64_t dynamic_address, + struct drgn_module **ret, + bool *new_ret); + +/** + * Find a relocatable module, creating it if it doesn't already exist. + * + * @param[out] new_ret @c true if the module was newly created, @c false if it + * was found. + */ +struct drgn_error * +drgn_module_find_or_create_relocatable(struct drgn_program *prog, + const char *name, uint64_t address, + struct drgn_module **ret, bool *new_ret); + +/** + * Find a created Linux kernel loadable module from a ``struct module`` object. + * + * @param[out] new_ret @c true if the module was newly created, @c false if it + * was found. + */ +struct drgn_error * +drgn_module_find_linux_kernel_loadable(const struct drgn_object *module_obj, + struct drgn_module **ret); + +/** + * Find a Linux kernel loadable module from a ``struct module`` object, creating + * it if it doesn't already exist. + * + * @param[out] new_ret @c true if the module was newly created, @c false if it + * was found. + */ +struct drgn_error * +drgn_module_find_or_create_linux_kernel_loadable(const struct drgn_object *module_obj, + struct drgn_module **ret, + bool *new_ret); + +/** + * Find an extra module, creating it if it doesn't already exist. + * + * @param[out] new_ret @c true if the module was newly created, @c false if it + * was found. + */ +struct drgn_error *drgn_module_find_or_create_extra(struct drgn_program *prog, + const char *name, + uint64_t id, + struct drgn_module **ret, + bool *new_ret); + +/** Get the program that a module is from. */ +struct drgn_program *drgn_module_program(const struct drgn_module *module); + +/** Get the unique key for a module. */ +struct drgn_module_key drgn_module_key(const struct drgn_module *module); + +/** Get the kind of a module. */ +enum drgn_module_kind drgn_module_kind(const struct drgn_module *module); + +/** Get the name of a module. */ +const char *drgn_module_name(const struct drgn_module *module); + +/** + * Get the address range where a module is loaded. + * + * If the module is not loaded in memory, then the start and end are both 0 + * + * @param[out] start_ret Minimum address (inclusive). + * @param[out] end_ret Maximum address (exclusive). + * @return @c true on success, @c false if the address range is not known yet. + */ +bool drgn_module_address_range(const struct drgn_module *module, + uint64_t *start_ret, uint64_t *end_ret); + +/** + * Set the address range of a module. + * + * @p start and @p end may both be 0 to indicate that the module is not loaded + * in memory. They may both be @c UINT64_MAX to unset the range. Otherwise, @p + * start must be less than @p end. + */ +struct drgn_error *drgn_module_set_address_range(struct drgn_module *module, + uint64_t start, uint64_t end); + +/** + * Get the unique byte string (e.g., GNU build ID) identifying files used by + * a module. + * + * @param[out] raw_ret Returned raw build ID. @c NULL if not known. Valid until + * the build ID is changed. + * @param[out] raw_len_ret Size of returned build ID, in bytes. 0 if not known. + * @return Lowercase hexadecimal representation of build ID. @c NULL if not + * known. Valid until the build ID is changed. + */ +const char *drgn_module_build_id(const struct drgn_module *module, + const void **raw_ret, size_t *raw_len_ret); + +/** + * Set the unique byte string (e.g., GNU build ID) identifying files used by a + * module. + * + * @param[in] build_id New build ID. + * @param[in] build_id_len New size of build ID, in bytes. May be 0 to unset the + * build ID. + */ +struct drgn_error *drgn_module_set_build_id(struct drgn_module *module, + const void *build_id, + size_t build_id_len); + +/** Get the address of a section with the given name in a relocatable module. */ +struct drgn_error *drgn_module_get_section_address(struct drgn_module *module, + const char *name, + uint64_t *ret); + +/** + * Set the address of a section with the given name in a relocatable module. + * + * This is not allowed after a file has been assigned to the module. + */ +struct drgn_error *drgn_module_set_section_address(struct drgn_module *module, + const char *name, + uint64_t address); + +/** + * Unset the address of a section with the given name in a relocatable module. + * + * This is not allowed after a file has been assigned to the module. + */ +struct drgn_error *drgn_module_delete_section_address(struct drgn_module *module, + const char *name); + +/** + * Get the number of section addresses currently set in a relocatable module. + */ +struct drgn_error *drgn_module_num_section_addresses(struct drgn_module *module, + size_t *ret); + +/** Iterator over set section addresses in a relocatable module. */ +struct drgn_module_section_address_iterator; + +/** Create a @ref drgn_module_section_address_iterator. */ +struct drgn_error * +drgn_module_section_address_iterator_create(struct drgn_module *module, + struct drgn_module_section_address_iterator **ret); + +/** Destroy a @ref drgn_module_section_address_iterator. */ +void +drgn_module_section_address_iterator_destroy(struct drgn_module_section_address_iterator *it); + +/** Get the module that a @ref drgn_module_section_address_iterator is for. */ +struct drgn_module * +drgn_module_section_address_iterator_module(struct drgn_module_section_address_iterator *it); + +/** + * Get the next section name and address from a @ref + * drgn_module_section_address_iterator. + * + * @param[out] name_ret Returned name. Valid until the the next call to @ref + * drgn_module_section_address_iterator_next() or @ref + * drgn_module_section_address_iterator_destroy() on @it. + * @param[out] address_ret Returned address. + */ +struct drgn_error * +drgn_module_section_address_iterator_next(struct drgn_module_section_address_iterator *it, + const char **name_ret, + uint64_t *address_ret); + +/** Status of a file in a @ref drgn_module. */ +enum drgn_module_file_status { + /** File has not been found and should be searched for. */ + DRGN_MODULE_FILE_WANT, + /** File has already been found and assigned. */ + DRGN_MODULE_FILE_HAVE, + /** File has not been found, but it should not be searched for. */ + DRGN_MODULE_FILE_DONT_WANT, + /** File has not been found and is not needed. */ + DRGN_MODULE_FILE_DONT_NEED, + /** + * File has been found, but it requires a supplementary file before it + * can be used. + */ + DRGN_MODULE_FILE_WANT_SUPPLEMENTARY, +}; + +/** Kind of supplementary file. */ +enum drgn_supplementary_file_kind { + /** Not known or not needed. */ + DRGN_SUPPLEMENTARY_FILE_NONE, + /** + * GNU-style supplementary debug file referred to by a + * ``.gnu_debugaltlink`` section. + */ + DRGN_SUPPLEMENTARY_FILE_GNU_DEBUGALTLINK, +}; + +/** Get the status of a module's loaded file. */ +enum drgn_module_file_status +drgn_module_loaded_file_status(const struct drgn_module *module); + +/** Set the status of a module's loaded file. */ +bool drgn_module_set_loaded_file_status(struct drgn_module *module, + enum drgn_module_file_status status); + +/** + * Get whether a module wants a loaded file. + * + * For future-proofness, debug info finders should prefer this over comparing + * @ref drgn_module_loaded_file_status() directly. + */ +bool drgn_module_wants_loaded_file(const struct drgn_module *module); + +/** Get the absolute path of a module's loaded file, or @c NULL if not known. */ +const char *drgn_module_loaded_file_path(const struct drgn_module *module); + +/** + * Get the difference between the load address in the program and addresses in a + * module's loaded file. + */ +uint64_t drgn_module_loaded_file_bias(const struct drgn_module *module); + +enum drgn_module_file_status +drgn_module_debug_file_status(const struct drgn_module *module); + +bool drgn_module_set_debug_file_status(struct drgn_module *module, + enum drgn_module_file_status status); + +/** + * Get whether a module wants a debug file. + * + * For future-proofness, debug info finders should prefer this over comparing + * @ref drgn_module_debug_file_status() directly. + */ +bool drgn_module_wants_debug_file(const struct drgn_module *module); + +/** Get the absolute path of a module's debug file, or @c NULL if not known. */ +const char *drgn_module_debug_file_path(const struct drgn_module *module); + +/** + * Get the difference between the load address in the program and addresses in a + * module's debug file. + */ +uint64_t drgn_module_debug_file_bias(const struct drgn_module *module); + +/** Get the kind of a module's supplementary debug file. */ +enum drgn_supplementary_file_kind +drgn_module_supplementary_debug_file_kind(const struct drgn_module *module); + +/** + * Get the absolute path of a module's supplementary debug file, or @c NULL if + * not known or not needed. + */ +const char * +drgn_module_supplementary_debug_file_path(const struct drgn_module *module); + +/** + * Get information about the supplementary debug file that a module currently + * wants. + * + * @param[out] debug_file_path_ret Path of main file that wants the + * supplementary file. + * @param[out] supplementary_path_ret Path to supplementary file. This may be + * absolute or relative to @p debug_file_path_ret. + * @param[out] checksum_ret Unique identifier of the supplementary file. + * @param[out] checksum_len_ret Size of unique identifier, in bytes. + * @return Kind of supplementary file. + */ +enum drgn_supplementary_file_kind +drgn_module_wanted_supplementary_debug_file(struct drgn_module *module, + const char **debug_file_path_ret, + const char **supplementary_path_ret, + const void **checksum_ret, + size_t *checksum_len_ret); + +/** Debugging information finder callback table. */ +struct drgn_debug_info_finder_ops { + /** + * Callback to destroy the debug info finder. + * + * This may be @c NULL. + * + * @param[in] arg Argument passed to @ref + * drgn_program_register_debug_info_finder(). + */ + void (*destroy)(void *arg); + /** + * Callback for finding debug info. + * + * @param[in] modules Array of modules that want debugging information. + * @param[in] num_modules Number of modules in @p modules. + * @param[in] arg Argument passed to @ref + * drgn_program_register_debug_info_finder(). + * @return @c NULL on success, non-@c NULL on error. It is not an error + * for some debugging information to not be found. + */ + struct drgn_error *(*find)(struct drgn_module * const *modules, + size_t num_modules, void *arg); +}; + +/** + * Register a debugging information finding callback. + * + * @param[in] name Finder name. This is copied. + * @param[in] ops Callback table. This is copied. + * @param[in] arg Argument to pass to callbacks. + * @param[in] enable_index Insert the finder into the list of enabled finders at + * the given index. If @ref DRGN_HANDLER_REGISTER_ENABLE_LAST or greater than + * the number of enabled finders, insert it at the end. If @ref + * DRGN_HANDLER_REGISTER_DONT_ENABLE, don’t enable the finder. + */ +struct drgn_error * +drgn_program_register_debug_info_finder(struct drgn_program *prog, + const char *name, + const struct drgn_debug_info_finder_ops *ops, + void *arg, size_t enable_index); + +/** + * Get the names of all registered debugging information finders. + * + * The order of the names is arbitrary. + * + * @param[out] names_ret Returned array of names. + * @param[out] count_ret Returned number of names in @p names_ret. + */ +struct drgn_error * +drgn_program_registered_debug_info_finders(struct drgn_program *prog, + const char ***names_ret, + size_t *count_ret); + +/** + * Set the list of enabled debugging information finders. + * + * Finders are called in the same order as the list until all wanted files have + * been found. + * + * @param[in] names Names of finders to enable, in order. + * @param[in] count Number of names in @p names. + */ +struct drgn_error * +drgn_program_set_enabled_debug_info_finders(struct drgn_program *prog, + const char * const *names, + size_t count); + +/** + * Get the names of enabled debugging information finders, in order. + * + * @param[out] names_ret Returned array of names. + * @param[out] count_ret Returned number of names in @p names_ret. + */ +struct drgn_error * +drgn_program_enabled_debug_info_finders(struct drgn_program *prog, + const char ***names_ret, + size_t *count_ret); + +/** Colon-separated directories to search for debugging information files. */ +const char *drgn_program_debug_info_path(struct drgn_program *prog); + +/** Set the directories to search for debugging information files. */ +struct drgn_error *drgn_program_set_debug_info_path(struct drgn_program *prog, + const char *path); + +/** + * Try to use the given file for a module. + * + * @param[in] path Path to file. + * @param[in] fd If nonnegative, an open file descriptor referring to the file. + * This always takes ownership of the file descriptor even if the file is not + * used or on error. + * @param[in] force If @c true, don't check whether the file matches the module. + */ +struct drgn_error * +drgn_module_try_file(struct drgn_module *module, const char *path, int fd, + bool force); + +/** Iterator over a set of modules. */ +struct drgn_module_iterator; + +/** Destroy a @ref drgn_module_iterator. */ +void +drgn_module_iterator_destroy(struct drgn_module_iterator *it); + +/** Get the program that a module iterator is from. */ +struct drgn_program * +drgn_module_iterator_program(const struct drgn_module_iterator *it); + +/** + * Get the next module in a module iterator. + * + * @param[out] ret Returned module. + * @param[out] new_ret Whether the module was newly created. May be @c NULL. + */ +struct drgn_error *drgn_module_iterator_next(struct drgn_module_iterator *it, + struct drgn_module **ret, + bool *new_ret); + +/** Create an iterator over created modules. */ +struct drgn_error * +drgn_created_module_iterator_create(struct drgn_program *prog, + struct drgn_module_iterator **ret); + +/** + * Create an iterator that determines what executables, libraries, etc. are + * loaded in the program and creates modules to represent them. + */ +struct drgn_error * +drgn_loaded_module_iterator_create(struct drgn_program *prog, + struct drgn_module_iterator **ret); + +/** + * Load debugging information for the given set of files and/or modules. + * + * @param[in] load_default Whether to load all debugging information for all + * loaded modules. This implies @p load_main. + * @param[in] load_main Whether to load all debugging information for the main + * module. + */ +struct drgn_error *drgn_program_load_debug_info(struct drgn_program *prog, + const char **paths, size_t n, + bool load_default, + bool load_main); + +/** + * Load debugging information for the given modules using the enabled debugging + * information finders. + */ +struct drgn_error *drgn_load_module_debug_info(struct drgn_module **modules, + size_t *num_modules); + +/** @} */ + /** * @defgroup Logging Logging * diff --git a/libdrgn/dwarf_info.c b/libdrgn/dwarf_info.c index 7725e781c..939a1b663 100644 --- a/libdrgn/dwarf_info.c +++ b/libdrgn/dwarf_info.c @@ -138,6 +138,7 @@ struct drgn_dwarf_index_cu { }; DEFINE_VECTOR_FUNCTIONS(drgn_dwarf_index_cu_vector); +DEFINE_VECTOR(drgn_module_vector, struct drgn_module *); DEFINE_HASH_MAP_FUNCTIONS(drgn_dwarf_type_map, ptr_key_hash_pair, scalar_key_eq); @@ -372,26 +373,6 @@ drgn_dwarf_index_cu_buffer_init(struct drgn_dwarf_index_cu_buffer *buffer, buffer->cu = cu; } -bool drgn_dwarf_index_state_init(struct drgn_dwarf_index_state *state, - struct drgn_debug_info *dbinfo) -{ - state->dbinfo = dbinfo; - drgn_init_num_threads(); - state->cus = malloc_array(drgn_num_threads, sizeof(*state->cus)); - if (!state->cus) - return false; - for (int i = 0; i < drgn_num_threads; i++) - drgn_dwarf_index_cu_vector_init(&state->cus[i]); - return true; -} - -void drgn_dwarf_index_state_deinit(struct drgn_dwarf_index_state *state) -{ - for (int i = 0; i < drgn_num_threads; i++) - drgn_dwarf_index_cu_vector_deinit(&state->cus[i]); - free(state->cus); -} - static const char *drgn_dwarf_dwo_name(Dwarf_Die *die) { Dwarf_Attribute attr_mem, *attr; @@ -402,14 +383,20 @@ static const char *drgn_dwarf_dwo_name(Dwarf_Die *die) } static struct drgn_error * -drgn_dwarf_index_read_cus(struct drgn_dwarf_index_state *state, - struct drgn_elf_file *file, - enum drgn_section_index scn) +drgn_dwarf_index_read_file(struct drgn_elf_file *file, + struct drgn_dwarf_index_cu_vector *cus); + +static struct drgn_error * +drgn_dwarf_index_read_cus(struct drgn_elf_file *file, + enum drgn_section_index scn, + struct drgn_dwarf_index_cu_vector *cus) { struct drgn_error *err; - struct drgn_dwarf_index_cu_vector *cus = - &state->cus[omp_get_thread_num()]; + Dwarf *dwarf; + err = drgn_elf_file_get_dwarf(file, &dwarf); + if (err) + return err; Dwarf_Off off, next_off; size_t header_size; Dwarf_Half version; @@ -421,19 +408,18 @@ drgn_dwarf_index_read_cus(struct drgn_dwarf_index_state *state, scn == DRGN_SCN_DEBUG_TYPES ? &v4_type_signature : NULL; int ret; for (off = 0; - (ret = dwarf_next_unit(file->dwarf, off, &next_off, &header_size, + (ret = dwarf_next_unit(dwarf, off, &next_off, &header_size, &version, &abbrev_offset, &address_size, &offset_size, v4_type_signaturep, NULL)) == 0; off = next_off) { Dwarf_Die cudie; if (scn == DRGN_SCN_DEBUG_TYPES) { - if (!dwarf_offdie_types(file->dwarf, off + header_size, + if (!dwarf_offdie_types(dwarf, off + header_size, &cudie)) return drgn_error_libdw(); } else { - if (!dwarf_offdie(file->dwarf, off + header_size, - &cudie)) + if (!dwarf_offdie(dwarf, off + header_size, &cudie)) return drgn_error_libdw(); } uint8_t unit_type; @@ -459,18 +445,18 @@ drgn_dwarf_index_read_cus(struct drgn_dwarf_index_state *state, &split_file); if (err) return err; - err = drgn_dwarf_index_read_file(state, - split_file); + err = drgn_dwarf_index_read_file(split_file, + cus); if (err) return err; } continue; } else if (unit_type == DW_UT_skeleton) { - if (drgn_log_is_enabled(state->dbinfo->prog, + if (drgn_log_is_enabled(file->module->prog, DRGN_LOG_WARNING)) { const char *dwo_name = drgn_dwarf_dwo_name(&cudie); - drgn_log_warning(state->dbinfo->prog, + drgn_log_warning(file->module->prog, "%s: split DWARF file%s%s not found", file->path ?: "", dwo_name ? " " : "", @@ -592,15 +578,41 @@ drgn_dwarf_index_read_cus(struct drgn_dwarf_index_state *state, return NULL; } -struct drgn_error * -drgn_dwarf_index_read_file(struct drgn_dwarf_index_state *state, - struct drgn_elf_file *file) +static struct drgn_error * +drgn_dwarf_index_read_file(struct drgn_elf_file *file, + struct drgn_dwarf_index_cu_vector *cus) { struct drgn_error *err; - err = drgn_dwarf_index_read_cus(state, file, DRGN_SCN_DEBUG_INFO); - if (!err && file->scn_data[DRGN_SCN_DEBUG_TYPES]) { - err = drgn_dwarf_index_read_cus(state, file, - DRGN_SCN_DEBUG_TYPES); + + for (int scn = 0; scn < DRGN_SECTION_INDEX_NUM_DWARF_INDEX; scn++) { + if (file->scns[scn]) { + Elf_Data *data; + err = drgn_elf_file_read_section(file, scn, &data); + if (err) + return err; + } + } + struct drgn_elf_file *supplementary_file = + file->module->supplementary_debug_file; + if (supplementary_file) { + err = drgn_elf_file_read_section(supplementary_file, + DRGN_SCN_DEBUG_INFO, + &file->alt_debug_info_data); + if (err) + return err; + if (supplementary_file->scns[DRGN_SCN_DEBUG_STR]) { + err = drgn_elf_file_read_section(supplementary_file, + DRGN_SCN_DEBUG_STR, + &file->alt_debug_str_data); + if (err) + return err; + } + } + + err = drgn_dwarf_index_read_cus(file, DRGN_SCN_DEBUG_INFO, cus); + if (!err && file->scns[DRGN_SCN_DEBUG_TYPES]) { + err = drgn_dwarf_index_read_cus(file, DRGN_SCN_DEBUG_TYPES, + cus); } return err; } @@ -1798,24 +1810,33 @@ drgn_dwarf_base_type_map_merge(struct drgn_dwarf_base_type_map *dst, return err; } -struct drgn_error * -drgn_dwarf_info_update_index(struct drgn_dwarf_index_state *state) +static struct drgn_error * +drgn_dwarf_index_update(struct drgn_debug_info *dbinfo) { - struct drgn_debug_info *dbinfo = state->dbinfo; - struct drgn_dwarf_index_cu_vector *cus = &dbinfo->dwarf.index_cus; + if (!dbinfo->modules_pending_indexing) + return NULL; if (dbinfo->dwarf.global.saved_err) return drgn_error_copy(dbinfo->dwarf.global.saved_err); - size_t new_cus_size = drgn_dwarf_index_cu_vector_size(cus); - for (int i = 0; i < drgn_num_threads; i++) - new_cus_size += drgn_dwarf_index_cu_vector_size(&state->cus[i]); - if (new_cus_size == drgn_dwarf_index_cu_vector_size(cus)) - return NULL; + drgn_init_num_threads(); - // Per-thread array of maps to populate. Thread 0 uses the maps in the - // dbinfo directly. These are merged into the dbinfo and freed. + _cleanup_(drgn_module_vector_deinit) + struct drgn_module_vector modules = VECTOR_INIT; + { + struct drgn_module *module = dbinfo->modules_pending_indexing; + do { + if (!drgn_module_vector_append(&modules, &module)) + return &drgn_enomem; + module = module->pending_indexing_next; + } while (module); + } + + // Per-thread structures to populate. Thread 0 uses the structures in + // the dbinfo directly. These are merged into the dbinfo and freed. _cleanup_free_ union { + // For reading modules. + struct drgn_dwarf_index_cu_vector cus; // For first pass. struct drgn_dwarf_specification_map specifications; // For second pass. @@ -1823,19 +1844,69 @@ drgn_dwarf_info_update_index(struct drgn_dwarf_index_state *state) struct drgn_dwarf_index_die_map map[DRGN_DWARF_INDEX_MAP_SIZE]; struct drgn_dwarf_base_type_map base_types; }; - } *maps = NULL; + } *threads = NULL; if (drgn_num_threads > 1) { - maps = malloc_array(drgn_num_threads - 1, sizeof(maps[0])); - if (!maps) + threads = malloc_array(drgn_num_threads - 1, sizeof(threads[0])); + if (!threads) return &drgn_enomem; } - if (!drgn_dwarf_index_cu_vector_reserve(cus, new_cus_size)) - return &drgn_enomem; - for (int i = 0; i < drgn_num_threads; i++) - drgn_dwarf_index_cu_vector_extend(cus, &state->cus[i]); + size_t old_cus_size = + drgn_dwarf_index_cu_vector_size(&dbinfo->dwarf.index_cus); struct drgn_error *err = NULL; + #pragma omp parallel num_threads(drgn_num_threads) + { + struct drgn_dwarf_index_cu_vector *cus; + int thread_num = omp_get_thread_num(); + if (thread_num == 0) { + cus = &dbinfo->dwarf.index_cus; + } else { + cus = &threads[thread_num - 1].cus; + drgn_dwarf_index_cu_vector_init(cus); + } + + #pragma omp for schedule(dynamic) + for (size_t i = 0; i < drgn_module_vector_size(&modules); i++) { + struct drgn_module *module = + *drgn_module_vector_at(&modules, i); + if (err) + continue; + struct drgn_error *module_err = + drgn_dwarf_index_read_file(module->debug_file, + cus); + if (module_err) { + #pragma omp critical(drgn_dwarf_info_update_index_error) + if (err) + drgn_error_destroy(module_err); + else + err = module_err; + } + } + } + if (err) + goto err; + + struct drgn_dwarf_index_cu_vector *cus = &dbinfo->dwarf.index_cus; + + size_t new_cus_size = drgn_dwarf_index_cu_vector_size(cus); + for (int i = 0; i < drgn_num_threads - 1; i++) + new_cus_size += drgn_dwarf_index_cu_vector_size(&threads[i].cus); + if (new_cus_size == old_cus_size) + return NULL; + + if (!drgn_dwarf_index_cu_vector_reserve(cus, new_cus_size)) { + for (int i = 0; i < drgn_num_threads - 1; i++) + drgn_dwarf_index_cu_vector_deinit(&threads[i].cus); + err = &drgn_enomem; + goto err; + } + + for (int i = 0; i < drgn_num_threads - 1; i++) { + drgn_dwarf_index_cu_vector_extend(cus, &threads[i].cus); + drgn_dwarf_index_cu_vector_deinit(&threads[i].cus); + } + #pragma omp parallel num_threads(drgn_num_threads) { struct drgn_dwarf_specification_map *specifications; @@ -1843,7 +1914,7 @@ drgn_dwarf_info_update_index(struct drgn_dwarf_index_state *state) if (thread_num == 0) { specifications = &dbinfo->dwarf.specifications; } else { - specifications = &maps[thread_num - 1].specifications; + specifications = &threads[thread_num - 1].specifications; drgn_dwarf_specification_map_init(specifications); } @@ -1873,7 +1944,7 @@ drgn_dwarf_info_update_index(struct drgn_dwarf_index_state *state) } for (int i = 0; i < drgn_num_threads - 1; i++) { err = drgn_dwarf_specification_map_merge(&dbinfo->dwarf.specifications, - &maps[i].specifications, + &threads[i].specifications, err); } if (err) @@ -1890,10 +1961,10 @@ drgn_dwarf_info_update_index(struct drgn_dwarf_index_state *state) map = dbinfo->dwarf.global.map; base_types = &dbinfo->dwarf.base_types; } else { - array_for_each(tag_map, maps[thread_num - 1].map) + array_for_each(tag_map, threads[thread_num - 1].map) drgn_dwarf_index_die_map_init(tag_map); - map = maps[thread_num - 1].map; - base_types = &maps[thread_num - 1].base_types; + map = threads[thread_num - 1].map; + base_types = &threads[thread_num - 1].base_types; drgn_dwarf_base_type_map_init(base_types); } @@ -1926,14 +1997,14 @@ drgn_dwarf_info_update_index(struct drgn_dwarf_index_state *state) for (int j = 0; j < drgn_num_threads - 1; j++) { thread_err = drgn_dwarf_index_die_map_merge(&dbinfo->dwarf.global.map[i], - &maps[j].map[i], + &threads[j].map[i], thread_err); } } else { for (int j = 0; j < drgn_num_threads - 1; j++) { thread_err = drgn_dwarf_base_type_map_merge(&dbinfo->dwarf.base_types, - &maps[j].base_types, + &threads[j].base_types, thread_err); } } @@ -1955,13 +2026,16 @@ drgn_dwarf_info_update_index(struct drgn_dwarf_index_state *state) qsort(drgn_dwarf_index_cu_vector_begin(cus), drgn_dwarf_index_cu_vector_size(cus), sizeof(struct drgn_dwarf_index_cu), drgn_dwarf_index_cu_cmp); + dbinfo->modules_pending_indexing = NULL; dbinfo->dwarf.global.cus_indexed = drgn_dwarf_index_cu_vector_size(cus); return NULL; } -static struct drgn_error *index_namespace(struct drgn_namespace_dwarf_index *ns) +static struct drgn_error *index_namespace_impl(struct drgn_namespace_dwarf_index *ns) { + struct drgn_error *err; + size_t num_index_cus = drgn_dwarf_index_cu_vector_size(&ns->dbinfo->dwarf.index_cus); if (ns->cus_indexed >= num_index_cus) @@ -1972,12 +2046,10 @@ static struct drgn_error *index_namespace(struct drgn_namespace_dwarf_index *ns) // The parent namespace must be indexed first so that the DIEs for this // namespace are populated. - struct drgn_error *err = index_namespace(ns->parent); + err = index_namespace_impl(ns->parent); if (err) return err; - drgn_blocking_guard(ns->dbinfo->prog); - struct drgn_dwarf_index_die_vector *die_vectors_to_index[DRGN_DWARF_INDEX_NUM_NAMESPACE_TAGS]; int tags_to_index[DRGN_DWARF_INDEX_NUM_NAMESPACE_TAGS]; @@ -2086,6 +2158,26 @@ static struct drgn_error *index_namespace(struct drgn_namespace_dwarf_index *ns) return NULL; } +static struct drgn_error *index_namespace(struct drgn_namespace_dwarf_index *ns) +{ + if (!ns->dbinfo->modules_pending_indexing + && (ns->cus_indexed + >= drgn_dwarf_index_cu_vector_size(&ns->dbinfo->dwarf.index_cus))) + return NULL; + + drgn_blocking_guard(ns->dbinfo->prog); + + struct drgn_error *err = drgn_dwarf_index_update(ns->dbinfo); + if (err) + return err; + return index_namespace_impl(ns); +} + +struct drgn_error *drgn_dwarf_info_update_index(struct drgn_debug_info *dbinfo) +{ + return index_namespace(&dbinfo->dwarf.global); +} + /** * Iterator over DWARF debugging information. * @@ -2285,28 +2377,29 @@ static struct drgn_error *drgn_language_from_die(Dwarf_Die *die, bool fall_back, return NULL; } -struct drgn_error * -drgn_debug_info_main_language(struct drgn_debug_info *dbinfo, - const struct drgn_language **ret) +const struct drgn_language * +drgn_debug_info_main_language(struct drgn_debug_info *dbinfo) { struct drgn_error *err; struct drgn_dwarf_index_iterator it; const enum drgn_dwarf_index_tag tag = DRGN_DWARF_INDEX_subprogram; err = drgn_dwarf_index_iterator_init(&it, &dbinfo->dwarf.global, "main", strlen("main"), &tag, 1); - if (err) - return err; + if (err) { + drgn_error_destroy(err); + return NULL; + } Dwarf_Die die; while (drgn_dwarf_index_iterator_next(&it, &die, NULL)) { - err = drgn_language_from_die(&die, false, ret); + const struct drgn_language *lang; + err = drgn_language_from_die(&die, false, &lang); if (err) { drgn_error_destroy(err); continue; } - if (*ret) - return NULL; + if (lang) + return lang; } - *ret = NULL; return NULL; } @@ -2531,7 +2624,10 @@ struct drgn_error *drgn_module_find_dwarf_scopes(struct drgn_module *module, *length_ret = 0; return NULL; } - Dwarf *dwarf = module->debug_file->dwarf; + Dwarf *dwarf; + err = drgn_elf_file_get_dwarf(module->debug_file, &dwarf); + if (err) + return err; *bias_ret = module->debug_file_bias; pc -= module->debug_file_bias; @@ -2772,16 +2868,17 @@ static struct drgn_error *drgn_dwarf_next_addrx(struct binary_buffer *bb, return drgn_error_create(DRGN_ERROR_OTHER, "indirect address without .debug_addr section"); } - err = drgn_elf_file_cache_section(file, DRGN_SCN_DEBUG_ADDR); + Elf_Data *data; + err = drgn_elf_file_read_section(file, DRGN_SCN_DEBUG_ADDR, &data); if (err) return err; - if (base > file->scn_data[DRGN_SCN_DEBUG_ADDR]->d_size) { + if (base > data->d_size) { return drgn_error_create(DRGN_ERROR_OTHER, "DW_AT_addr_base is out of bounds"); } - *addr_base = (char *)file->scn_data[DRGN_SCN_DEBUG_ADDR]->d_buf + base; + *addr_base = (char *)data->d_buf + base; // In DWARF 5, there is a header immediately before addr_base, // which ends with a segment selector size. We don't support a // segment selector yet. In GNU Debug Fission, .debug_addr @@ -2804,6 +2901,7 @@ static struct drgn_error *drgn_dwarf_next_addrx(struct binary_buffer *bb, if ((err = binary_buffer_next_uleb128(bb, &index))) return err; + // The data must was cached when we cached addr_base. Elf_Data *data = file->scn_data[DRGN_SCN_DEBUG_ADDR]; if (index >= ((char *)data->d_buf + data->d_size - *addr_base) / address_size) { @@ -2849,10 +2947,10 @@ static struct drgn_error *drgn_dwarf_read_loclistx(struct drgn_elf_file *file, return drgn_error_create(DRGN_ERROR_OTHER, "DW_FORM_loclistx without .debug_loclists section"); } - err = drgn_elf_file_cache_section(file, DRGN_SCN_DEBUG_LOCLISTS); + Elf_Data *data; + err = drgn_elf_file_read_section(file, DRGN_SCN_DEBUG_LOCLISTS, &data); if (err) return err; - Elf_Data *data = file->scn_data[DRGN_SCN_DEBUG_LOCLISTS]; if (base > data->d_size) { return drgn_error_create(DRGN_ERROR_OTHER, @@ -2893,12 +2991,11 @@ static struct drgn_error *drgn_dwarf5_location_list(struct drgn_elf_file *file, return drgn_error_create(DRGN_ERROR_OTHER, "loclist without .debug_loclists section"); } - err = drgn_elf_file_cache_section(file, DRGN_SCN_DEBUG_LOCLISTS); - if (err) - return err; struct drgn_elf_file_section_buffer buffer; - drgn_elf_file_section_buffer_init_index(&buffer, file, + err = drgn_elf_file_section_buffer_read(&buffer, file, DRGN_SCN_DEBUG_LOCLISTS); + if (err) + return err; if (offset > buffer.bb.end - buffer.bb.pos) { return drgn_error_create(DRGN_ERROR_OTHER, "loclist is out of bounds"); @@ -3031,17 +3128,16 @@ drgn_dwarf4_split_location_list(struct drgn_elf_file *file, Dwarf_Word offset, return drgn_error_create(DRGN_ERROR_OTHER, "loclistptr without .debug_loc section"); } - err = drgn_elf_file_cache_section(file, DRGN_SCN_DEBUG_LOC); - if (err) - return err; Dwarf_Off dwp_offset; if (dwarf_cu_dwp_section_info(cu_die->cu, DW_SECT_LOCLISTS, &dwp_offset, NULL)) return drgn_error_libdw(); offset += dwp_offset; struct drgn_elf_file_section_buffer buffer; - drgn_elf_file_section_buffer_init_index(&buffer, file, + err = drgn_elf_file_section_buffer_read(&buffer, file, DRGN_SCN_DEBUG_LOC); + if (err) + return err; if (offset > buffer.bb.end - buffer.bb.pos) { return drgn_error_create(DRGN_ERROR_OTHER, "loclistptr is out of bounds"); @@ -3146,12 +3242,11 @@ static struct drgn_error *drgn_dwarf4_location_list(struct drgn_elf_file *file, return drgn_error_create(DRGN_ERROR_OTHER, "loclistptr without .debug_loc section"); } - err = drgn_elf_file_cache_section(file, DRGN_SCN_DEBUG_LOC); - if (err) - return err; struct drgn_elf_file_section_buffer buffer; - drgn_elf_file_section_buffer_init_index(&buffer, file, + err = drgn_elf_file_section_buffer_read(&buffer, file, DRGN_SCN_DEBUG_LOC); + if (err) + return err; if (offset > buffer.bb.end - buffer.bb.pos) { return drgn_error_create(DRGN_ERROR_OTHER, "loclistptr is out of bounds"); @@ -4539,17 +4634,17 @@ drgn_object_from_dwarf_location(struct drgn_program *prog, drgn_object_reinit(ret, &type, DRGN_OBJECT_ABSENT); err = NULL; } else if (bit_offset >= 0) { - Dwarf_Addr start, end, bias; - dwfl_module_info(file->module->dwfl_module, NULL, &start, &end, - &bias, NULL, NULL, NULL); + uint64_t biased_address = + address + file->module->debug_file_bias; /* * If the address is not in the module's address range, then * it's probably something special like a Linux per-CPU variable * (which isn't actually a variable address but an offset). * Don't apply the bias in that case. */ - if (start <= address + bias && address + bias < end) - address += bias; + if (file->module->start <= biased_address + && biased_address < file->module->end) + address = biased_address; err = drgn_object_set_reference_internal(ret, &type, address, bit_offset); } else if (type.encoding == DRGN_OBJECT_ENCODING_BUFFER) { @@ -6623,10 +6718,6 @@ static struct drgn_error *drgn_parse_dwarf_cfi(struct drgn_dwarf_cfi *cfi, &file->module->dwarf.datarel_base); } - err = drgn_elf_file_cache_section(file, scn); - if (err) - return err; - _cleanup_(drgn_dwarf_cie_vector_deinit) struct drgn_dwarf_cie_vector cies = VECTOR_INIT; _cleanup_(drgn_dwarf_fde_vector_deinit) @@ -6634,9 +6725,10 @@ static struct drgn_error *drgn_parse_dwarf_cfi(struct drgn_dwarf_cfi *cfi, _cleanup_(drgn_dwarf_cie_map_deinit) struct drgn_dwarf_cie_map cie_map = HASH_TABLE_INIT; - Elf_Data *data = file->scn_data[scn]; struct drgn_elf_file_section_buffer buffer; - drgn_elf_file_section_buffer_init_index(&buffer, file, scn); + err = drgn_elf_file_section_buffer_read(&buffer, file, scn); + if (err) + return err; while (binary_buffer_has_next(&buffer.bb)) { uint32_t tmp; if ((err = binary_buffer_next_u32(&buffer.bb, &tmp))) @@ -6688,13 +6780,13 @@ static struct drgn_error *drgn_parse_dwarf_cfi(struct drgn_dwarf_cfi *cfi, size_t pointer_offset = (buffer.bb.pos - (is_64_bit ? 8 : 4) - - (char *)data->d_buf); + - (char *)buffer.data->d_buf); if (cie_pointer > pointer_offset) { return binary_buffer_error(&buffer.bb, "CIE pointer is out of bounds"); } cie_pointer = pointer_offset - cie_pointer; - } else if (cie_pointer > data->d_size) { + } else if (cie_pointer > buffer.data->d_size) { return binary_buffer_error(&buffer.bb, "CIE pointer is out of bounds"); } @@ -6753,7 +6845,8 @@ static struct drgn_error *drgn_parse_dwarf_cfi(struct drgn_dwarf_cfi *cfi, } buffer.bb.pos = buffer.bb.end; - buffer.bb.end = (const char *)data->d_buf + data->d_size; + buffer.bb.end = (const char *)buffer.data->d_buf + + buffer.data->d_size; } drgn_dwarf_cie_vector_shrink_to_fit(&cies); diff --git a/libdrgn/dwarf_info.h b/libdrgn/dwarf_info.h index 81761efcd..50f51dfc6 100644 --- a/libdrgn/dwarf_info.h +++ b/libdrgn/dwarf_info.h @@ -216,39 +216,7 @@ struct drgn_dwarf_info { void drgn_dwarf_info_init(struct drgn_debug_info *dbinfo); void drgn_dwarf_info_deinit(struct drgn_debug_info *dbinfo); -/** - * State tracked while indexing new DWARF information in a @ref drgn_dwarf_info. - */ -struct drgn_dwarf_index_state { - struct drgn_debug_info *dbinfo; - /** Per-thread arrays of CUs to be indexed. */ - struct drgn_dwarf_index_cu_vector *cus; -}; - -/** - * Initialize state for indexing new DWARF information. - * - * @return @c true on success, @c false on failure to allocate memory. - */ -bool drgn_dwarf_index_state_init(struct drgn_dwarf_index_state *state, - struct drgn_debug_info *dbinfo); - -/** Deinitialize state for indexing new DWARF information. */ -void drgn_dwarf_index_state_deinit(struct drgn_dwarf_index_state *state); - -/** Read a @ref drgn_elf_file to index its DWARF information. */ -struct drgn_error * -drgn_dwarf_index_read_file(struct drgn_dwarf_index_state *state, - struct drgn_elf_file *file); - -/** - * Index new DWARF information. - * - * This should be called once all files have been read with @ref - * drgn_dwarf_index_read_file() to finish indexing those files. - */ -struct drgn_error * -drgn_dwarf_info_update_index(struct drgn_dwarf_index_state *state); +struct drgn_error *drgn_dwarf_info_update_index(struct drgn_debug_info *dbinfo); /** * Find the DWARF DIEs in a @ref drgn_module for the scope containing a given diff --git a/libdrgn/elf_file.c b/libdrgn/elf_file.c index bf5952b6b..4b3af027d 100644 --- a/libdrgn/elf_file.c +++ b/libdrgn/elf_file.c @@ -3,13 +3,16 @@ #include #include +#include #include #include #include #include #include +#include #include "array.h" +#include "debug_info.h" #include "drgn_internal.h" #include "elf_file.h" #include "error.h" @@ -35,6 +38,16 @@ struct drgn_error *read_elf_section(Elf_Scn *scn, Elf_Data **ret) return NULL; } +void truncate_elf_string_data(Elf_Data *data) +{ + const char *buf = data->d_buf; + const char *nul = memrchr(buf, '\0', data->d_size); + if (nul) + data->d_size = nul - buf + 1; + else + data->d_size = 0; +} + #include "drgn_section_name_to_index.inc" enum drgn_dwarf_file_type { @@ -45,163 +58,519 @@ enum drgn_dwarf_file_type { }; struct drgn_error *drgn_elf_file_create(struct drgn_module *module, - const char *path, Elf *elf, - struct drgn_elf_file **ret) + const char *path, int fd, char *image, + Elf *elf, struct drgn_elf_file **ret) { - struct drgn_error *err; + if (elf_kind(elf) != ELF_K_ELF) + return drgn_error_create(DRGN_ERROR_OTHER, "not an ELF file"); + GElf_Ehdr ehdr_mem, *ehdr = gelf_getehdr(elf, &ehdr_mem); if (!ehdr) return drgn_error_libelf(); - size_t shstrndx; - if (elf_getshdrstrndx(elf, &shstrndx)) - return drgn_error_libelf(); - struct drgn_elf_file *file = calloc(1, sizeof(*file)); + _cleanup_free_ struct drgn_elf_file *file = calloc(1, sizeof(*file)); if (!file) return &drgn_enomem; - file->module = module; - file->path = path; - file->elf = elf; - drgn_platform_from_elf(ehdr, &file->platform); - // We mimic libdw's logic for choosing debug sections: we either use all - // .debug_* or .zdebug_* sections (DRGN_DWARF_FILE_PLAIN), all - // .debug_*.dwo or .zdebug_*.dwo sections (DRGN_DWARF_FILE_DWO), or all - // .gnu.debuglto_.debug_* sections (DRGN_DWARF_FILE_GNU_LTO), in that - // order of preference. - enum drgn_dwarf_file_type dwarf_file_type = DRGN_DWARF_FILE_NONE; - Elf_Scn *scn = NULL; - while ((scn = elf_nextscn(elf, scn))) { - GElf_Shdr shdr_mem, *shdr = gelf_getshdr(scn, &shdr_mem); - if (!shdr) { - err = drgn_error_libelf(); - goto err; - } - const char *scnname = elf_strptr(elf, shstrndx, shdr->sh_name); - if (!scnname) { - err = drgn_error_libelf(); - goto err; - } + if (ehdr->e_type == ET_EXEC || + ehdr->e_type == ET_DYN || + ehdr->e_type == ET_REL) { + size_t shstrndx; + if (elf_getshdrstrndx(elf, &shstrndx)) + return drgn_error_libelf(); - enum drgn_dwarf_file_type dwarf_section_type; - if (strcmp(scnname, ".debug_cu_index") == 0 || - strcmp(scnname, ".debug_tu_index") == 0) { - dwarf_section_type = DRGN_DWARF_FILE_DWO; - } else if (strstartswith(scnname, ".debug_") || - strstartswith(scnname, ".zdebug_")) { - if (strcmp(scnname + strlen(scnname) - 4, ".dwo") == 0) + bool has_sections = false; + bool has_alloc_section = false; + // We mimic libdw's logic for choosing debug sections: we either + // use all .debug_* or .zdebug_* sections + // (DRGN_DWARF_FILE_PLAIN), all .debug_*.dwo or .zdebug_*.dwo + // sections (DRGN_DWARF_FILE_DWO), or all .gnu.debuglto_.debug_* + // sections (DRGN_DWARF_FILE_GNU_LTO), in that order of + // preference. + enum drgn_dwarf_file_type dwarf_file_type = DRGN_DWARF_FILE_NONE; + Elf_Scn *scn = NULL; + while ((scn = elf_nextscn(elf, scn))) { + GElf_Shdr shdr_mem, *shdr = gelf_getshdr(scn, &shdr_mem); + if (!shdr) + return drgn_error_libelf(); + + has_sections = true; + if (shdr->sh_type != SHT_NOBITS && + shdr->sh_type != SHT_NOTE && + (shdr->sh_flags & SHF_ALLOC)) + has_alloc_section = true; + + const char *scnname = elf_strptr(elf, shstrndx, shdr->sh_name); + if (!scnname) + return drgn_error_libelf(); + + enum drgn_dwarf_file_type dwarf_section_type; + if (strcmp(scnname, ".debug_cu_index") == 0 || + strcmp(scnname, ".debug_tu_index") == 0) { dwarf_section_type = DRGN_DWARF_FILE_DWO; - else - dwarf_section_type = DRGN_DWARF_FILE_PLAIN; - } else if (strstartswith(scnname, ".gnu.debuglto_.debug")) { - dwarf_section_type = DRGN_DWARF_FILE_GNU_LTO; - } else { - dwarf_section_type = DRGN_DWARF_FILE_NONE; + } else if (strstartswith(scnname, ".debug_") || + strstartswith(scnname, ".zdebug_")) { + if (strcmp(scnname + strlen(scnname) - 4, ".dwo") == 0) + dwarf_section_type = DRGN_DWARF_FILE_DWO; + else + dwarf_section_type = DRGN_DWARF_FILE_PLAIN; + } else if (strstartswith(scnname, ".gnu.debuglto_.debug")) { + dwarf_section_type = DRGN_DWARF_FILE_GNU_LTO; + } else { + dwarf_section_type = DRGN_DWARF_FILE_NONE; + } + dwarf_file_type = max(dwarf_file_type, dwarf_section_type); } - dwarf_file_type = max(dwarf_file_type, dwarf_section_type); - } - scn = NULL; - while ((scn = elf_nextscn(elf, scn))) { - GElf_Shdr shdr_mem, *shdr = gelf_getshdr(scn, &shdr_mem); - if (!shdr) { - err = drgn_error_libelf(); - goto err; - } + scn = NULL; + while ((scn = elf_nextscn(elf, scn))) { + GElf_Shdr shdr_mem, *shdr = gelf_getshdr(scn, &shdr_mem); + if (!shdr) + return drgn_error_libelf(); - if (shdr->sh_type != SHT_PROGBITS) - continue; + if (shdr->sh_type != SHT_PROGBITS) + continue; - const char *scnname = elf_strptr(elf, shstrndx, shdr->sh_name); - if (!scnname) { - err = drgn_error_libelf(); - goto err; - } + const char *scnname = elf_strptr(elf, shstrndx, shdr->sh_name); + if (!scnname) + return drgn_error_libelf(); - enum drgn_section_index index; - if (strstartswith(scnname, ".debug_") || - strstartswith(scnname, ".zdebug_")) { - const char *subname; - if (strstartswith(scnname, ".zdebug_")) - subname = scnname + sizeof(".zdebug_") - 1; - else - subname = scnname + sizeof(".debug_") - 1; - size_t len = strlen(subname); - if (len >= 4 - && strcmp(subname + len - 4, ".dwo") == 0) { - if (dwarf_file_type != DRGN_DWARF_FILE_DWO) + enum drgn_section_index index; + if (strstartswith(scnname, ".debug_") || + strstartswith(scnname, ".zdebug_")) { + const char *subname; + if (strstartswith(scnname, ".zdebug_")) + subname = scnname + sizeof(".zdebug_") - 1; + else + subname = scnname + sizeof(".debug_") - 1; + size_t len = strlen(subname); + if (len >= 4 + && strcmp(subname + len - 4, ".dwo") == 0) { + if (dwarf_file_type != DRGN_DWARF_FILE_DWO) + continue; + len -= 4; + } else if (dwarf_file_type != DRGN_DWARF_FILE_PLAIN) { continue; - len -= 4; - } else if (dwarf_file_type != DRGN_DWARF_FILE_PLAIN) { - continue; + } + index = drgn_debug_section_name_to_index(subname, len); + } else if (strstartswith(scnname, ".gnu.debuglto_.debug_")) { + if (dwarf_file_type != DRGN_DWARF_FILE_GNU_LTO) + continue; + const char *subname = + scnname + sizeof(".gnu.debuglto_.debug_") - 1; + index = drgn_debug_section_name_to_index(subname, + strlen(subname)); + } else if (strcmp(scnname, ".init.text") == 0) { + // We consider a file to be vmlinux if it has an + // .init.text section and is not relocatable + // (which excludes kernel modules). + file->is_vmlinux = ehdr->e_type != ET_REL; + index = DRGN_SECTION_INDEX_NUM; + } else { + index = drgn_non_debug_section_name_to_index(scnname); } - index = drgn_debug_section_name_to_index(subname, len); - } else if (strstartswith(scnname, ".gnu.debuglto_.debug_")) { - if (dwarf_file_type != DRGN_DWARF_FILE_GNU_LTO) - continue; - const char *subname = - scnname + sizeof(".gnu.debuglto_.debug_") - 1; - index = drgn_debug_section_name_to_index(subname, - strlen(subname)); + if (index < DRGN_SECTION_INDEX_NUM && !file->scns[index]) + file->scns[index] = scn; + } + + if (ehdr->e_type == ET_REL) { + // We consider a relocatable file "loadable" if it has + // any allocated sections. + file->is_loadable = has_alloc_section; + file->is_relocatable = file->needs_relocation = true; } else { - index = drgn_non_debug_section_name_to_index(scnname); + // We consider executable and shared object files + // loadable if they have any loadable segments, and + // either no sections or at least one allocated section. + bool has_loadable_segment = false; + size_t phnum; + if (elf_getphdrnum(elf, &phnum) != 0) + return drgn_error_libelf(); + for (size_t i = 0; i < phnum; i++) { + GElf_Phdr phdr_mem, *phdr = + gelf_getphdr(elf, i, &phdr_mem); + if (!phdr) + return drgn_error_libelf(); + if (phdr->p_type == PT_LOAD) { + has_loadable_segment = true; + break; + } + } + file->is_loadable = + has_loadable_segment && + (!has_sections || has_alloc_section); } - if (index < DRGN_SECTION_INDEX_NUM && !file->scns[index]) - file->scns[index] = scn; } - *ret = file; - return NULL; -err: - free(file); - return err; + file->module = module; + file->path = strdup(path); + if (!file->path) + return &drgn_enomem; + file->image = image; + file->fd = fd; + file->elf = elf; + drgn_platform_from_elf(ehdr, &file->platform); + *ret = no_cleanup_ptr(file); + return NULL; } void drgn_elf_file_destroy(struct drgn_elf_file *file) { - free(file); + if (file) { + dwarf_end(file->_dwarf); + elf_end(file->elf); + if (file->fd >= 0) + close(file->fd); + free(file->image); + free(file->path); + free(file); + } } -static void truncate_null_terminated_section(Elf_Data *data) +static int should_apply_relocation_section(Elf *elf, size_t shstrndx, + const GElf_Shdr *shdr) { - if (data) { - const char *buf = data->d_buf; - const char *nul = memrchr(buf, '\0', data->d_size); - if (nul) - data->d_size = nul - buf + 1; - else - data->d_size = 0; + if (shdr->sh_type != SHT_RELA && shdr->sh_type != SHT_REL) + return 0; + + const char *scnname = elf_strptr(elf, shstrndx, shdr->sh_name); + if (!scnname) + return -1; + if (shdr->sh_type == SHT_RELA) { + if (!strstartswith(scnname, ".rela.")) + return 0; + scnname += sizeof(".rela.") - 1; + } else { + if (!strstartswith(scnname, ".rel.")) + return 0; + scnname += sizeof(".rel.") - 1; } + return (strstartswith(scnname, "debug_") + || strstartswith(scnname, "orc_")); } -struct drgn_error *drgn_elf_file_precache_sections(struct drgn_elf_file *file) +static inline struct drgn_error *get_reloc_sym_value(const void *syms, + size_t num_syms, + const uint64_t *sh_addrs, + size_t shdrnum, + bool is_64_bit, + bool bswap, + uint32_t r_sym, + uint64_t *ret) +{ + if (r_sym >= num_syms) { + return drgn_error_create(DRGN_ERROR_OTHER, + "invalid ELF relocation symbol"); + } + uint16_t st_shndx; + uint64_t st_value; + if (is_64_bit) { + const Elf64_Sym *sym = (Elf64_Sym *)syms + r_sym; + memcpy(&st_shndx, &sym->st_shndx, sizeof(st_shndx)); + memcpy(&st_value, &sym->st_value, sizeof(st_value)); + if (bswap) { + st_shndx = bswap_16(st_shndx); + st_value = bswap_64(st_value); + } + } else { + const Elf32_Sym *sym = (Elf32_Sym *)syms + r_sym; + memcpy(&st_shndx, &sym->st_shndx, sizeof(st_shndx)); + uint32_t st_value32; + memcpy(&st_value32, &sym->st_value, sizeof(st_value32)); + if (bswap) { + st_shndx = bswap_16(st_shndx); + st_value32 = bswap_32(st_value32); + } + st_value = st_value32; + } + if (st_shndx >= shdrnum) { + return drgn_error_create(DRGN_ERROR_OTHER, + "invalid ELF symbol section index"); + } + *ret = sh_addrs[st_shndx] + st_value; + return NULL; +} + +static struct drgn_error * +apply_elf_relas(const struct drgn_relocating_section *relocating, + Elf_Data *reloc_data, Elf_Data *symtab_data, + const uint64_t *sh_addrs, size_t shdrnum, + const struct drgn_platform *platform) { struct drgn_error *err; - for (size_t i = 0; i < DRGN_SECTION_INDEX_NUM_PRECACHE; i++) { - if (file->scns[i]) { - err = read_elf_section(file->scns[i], - &file->scn_data[i]); - if (err) - return err; + bool is_64_bit = drgn_platform_is_64_bit(platform); + bool bswap = drgn_platform_bswap(platform); + apply_elf_reloc_fn *apply_elf_reloc = platform->arch->apply_elf_reloc; + + const void *relocs = reloc_data->d_buf; + size_t reloc_size = is_64_bit ? sizeof(Elf64_Rela) : sizeof(Elf32_Rela); + size_t num_relocs = reloc_data->d_size / reloc_size; + + const void *syms = symtab_data->d_buf; + size_t sym_size = is_64_bit ? sizeof(Elf64_Sym) : sizeof(Elf32_Sym); + size_t num_syms = symtab_data->d_size / sym_size; + + for (size_t i = 0; i < num_relocs; i++) { + uint64_t r_offset; + uint32_t r_sym; + uint32_t r_type; + int64_t r_addend; + if (is_64_bit) { + const Elf64_Rela *rela = (Elf64_Rela *)relocs + i; + uint64_t r_info; + memcpy(&r_offset, &rela->r_offset, sizeof(r_offset)); + memcpy(&r_info, &rela->r_info, sizeof(r_info)); + memcpy(&r_addend, &rela->r_addend, sizeof(r_addend)); + if (bswap) { + r_offset = bswap_64(r_offset); + r_info = bswap_64(r_info); + r_addend = bswap_64(r_addend); + } + r_sym = ELF64_R_SYM(r_info); + r_type = ELF64_R_TYPE(r_info); + } else { + const Elf32_Rela *rela32 = (Elf32_Rela *)relocs + i; + uint32_t r_offset32; + uint32_t r_info32; + int32_t r_addend32; + memcpy(&r_offset32, &rela32->r_offset, sizeof(r_offset32)); + memcpy(&r_info32, &rela32->r_info, sizeof(r_info32)); + memcpy(&r_addend32, &rela32->r_addend, sizeof(r_addend32)); + if (bswap) { + r_offset32 = bswap_32(r_offset32); + r_info32 = bswap_32(r_info32); + r_addend32 = bswap_32(r_addend32); + } + r_offset = r_offset32; + r_sym = ELF32_R_SYM(r_info32); + r_type = ELF32_R_TYPE(r_info32); + r_addend = r_addend32; } + uint64_t sym_value; + err = get_reloc_sym_value(syms, num_syms, sh_addrs, shdrnum, + is_64_bit, bswap, r_sym, &sym_value); + if (err) + return err; + + err = apply_elf_reloc(relocating, r_offset, r_type, &r_addend, + sym_value); + if (err) + return err; } + return NULL; +} + +static struct drgn_error * +apply_elf_rels(const struct drgn_relocating_section *relocating, + Elf_Data *reloc_data, Elf_Data *symtab_data, + const uint64_t *sh_addrs, size_t shdrnum, + const struct drgn_platform *platform) +{ + struct drgn_error *err; + + bool is_64_bit = drgn_platform_is_64_bit(platform); + bool bswap = drgn_platform_bswap(platform); + apply_elf_reloc_fn *apply_elf_reloc = platform->arch->apply_elf_reloc; + + const void *relocs = reloc_data->d_buf; + size_t reloc_size = is_64_bit ? sizeof(Elf64_Rel) : sizeof(Elf32_Rel); + size_t num_relocs = reloc_data->d_size / reloc_size; + + const void *syms = symtab_data->d_buf; + size_t sym_size = is_64_bit ? sizeof(Elf64_Sym) : sizeof(Elf32_Sym); + size_t num_syms = symtab_data->d_size / sym_size; + + for (size_t i = 0; i < num_relocs; i++) { + uint64_t r_offset; + uint32_t r_sym; + uint32_t r_type; + if (is_64_bit) { + const Elf64_Rel *rel = (Elf64_Rel *)relocs + i; + uint64_t r_info; + memcpy(&r_offset, &rel->r_offset, sizeof(r_offset)); + memcpy(&r_info, &rel->r_info, sizeof(r_info)); + if (bswap) { + r_offset = bswap_64(r_offset); + r_info = bswap_64(r_info); + } + r_sym = ELF64_R_SYM(r_info); + r_type = ELF64_R_TYPE(r_info); + } else { + const Elf32_Rel *rel32 = (Elf32_Rel *)relocs + i; + uint32_t r_offset32; + uint32_t r_info32; + memcpy(&r_offset32, &rel32->r_offset, sizeof(r_offset32)); + memcpy(&r_info32, &rel32->r_info, sizeof(r_info32)); + if (bswap) { + r_offset32 = bswap_32(r_offset32); + r_info32 = bswap_32(r_info32); + } + r_offset = r_offset32; + r_sym = ELF32_R_SYM(r_info32); + r_type = ELF32_R_TYPE(r_info32); + } + uint64_t sym_value; + err = get_reloc_sym_value(syms, num_syms, sh_addrs, shdrnum, + is_64_bit, bswap, r_sym, &sym_value); + if (err) + return err; - /* - * Truncate any extraneous bytes so that we can assume that a pointer - * within .debug_{,line_}str is always null-terminated. - */ - truncate_null_terminated_section(file->scn_data[DRGN_SCN_DEBUG_STR]); - truncate_null_terminated_section(file->alt_debug_str_data); + err = apply_elf_reloc(relocating, r_offset, r_type, NULL, + sym_value); + if (err) + return err; + } return NULL; } struct drgn_error * -drgn_elf_file_cache_section(struct drgn_elf_file *file, enum drgn_section_index scn) +drgn_elf_file_apply_relocations(struct drgn_elf_file *file) { - if (file->scn_data[scn]) + struct drgn_error *err; + + if (!file->needs_relocation) return NULL; - return read_elf_section(file->scns[scn], &file->scn_data[scn]); + + if (!file->platform.arch->apply_elf_reloc) { + return drgn_error_format(DRGN_ERROR_NOT_IMPLEMENTED, + "relocation support is not implemented for %s architecture", + file->platform.arch->name); + } + + Elf *elf = file->elf; + size_t shdrnum; + if (elf_getshdrnum(elf, &shdrnum)) + return drgn_error_libelf(); + _cleanup_free_ uint64_t *sh_addrs = + calloc(shdrnum, sizeof(sh_addrs[0])); + if (!sh_addrs && shdrnum > 0) + return &drgn_enomem; + + Elf_Scn *scn = NULL; + while ((scn = elf_nextscn(elf, scn))) { + GElf_Shdr *shdr, shdr_mem; + shdr = gelf_getshdr(scn, &shdr_mem); + if (!shdr) + return drgn_error_libelf(); + sh_addrs[elf_ndxscn(scn)] = shdr->sh_addr; + } + + size_t shstrndx; + if (elf_getshdrstrndx(elf, &shstrndx)) + return drgn_error_libelf(); + + Elf_Scn *reloc_scn = NULL; + while ((reloc_scn = elf_nextscn(elf, reloc_scn))) { + GElf_Shdr *reloc_shdr, reloc_shdr_mem; + reloc_shdr = gelf_getshdr(reloc_scn, &reloc_shdr_mem); + if (!reloc_shdr) + return drgn_error_libelf(); + + int r = should_apply_relocation_section(elf, shstrndx, + reloc_shdr); + if (r < 0) + return drgn_error_libelf(); + if (r) { + scn = elf_getscn(elf, reloc_shdr->sh_info); + if (!scn) + return drgn_error_libelf(); + GElf_Shdr *shdr, shdr_mem; + shdr = gelf_getshdr(scn, &shdr_mem); + if (!shdr) + return drgn_error_libelf(); + if (shdr->sh_type == SHT_NOBITS) + continue; + + Elf_Scn *symtab_scn = elf_getscn(elf, + reloc_shdr->sh_link); + if (!symtab_scn) + return drgn_error_libelf(); + shdr = gelf_getshdr(symtab_scn, &shdr_mem); + if (!shdr) + return drgn_error_libelf(); + if (shdr->sh_type == SHT_NOBITS) { + return drgn_error_create(DRGN_ERROR_OTHER, + "relocation symbol table has no data"); + } + + Elf_Data *data, *reloc_data, *symtab_data; + if ((err = read_elf_section(scn, &data)) + || (err = read_elf_section(reloc_scn, &reloc_data)) + || (err = read_elf_section(symtab_scn, &symtab_data))) + return err; + + struct drgn_relocating_section relocating = { + .buf = data->d_buf, + .buf_size = data->d_size, + .addr = sh_addrs[elf_ndxscn(scn)], + .bswap = drgn_platform_bswap(&file->platform), + }; + + if (reloc_shdr->sh_type == SHT_RELA) { + err = apply_elf_relas(&relocating, reloc_data, + symtab_data, sh_addrs, + shdrnum, &file->platform); + } else { + err = apply_elf_rels(&relocating, reloc_data, + symtab_data, sh_addrs, + shdrnum, &file->platform); + } + if (err) + return err; + } + } + file->needs_relocation = false; + return NULL; +} + +struct drgn_error *drgn_elf_file_read_section(struct drgn_elf_file *file, + enum drgn_section_index scn, + Elf_Data **ret) +{ + struct drgn_error *err; + if (!file->scn_data[scn]) { + err = drgn_elf_file_apply_relocations(file); + if (err) + return err; + err = read_elf_section(file->scns[scn], &file->scn_data[scn]); + if (err) + return err; + if (scn == DRGN_SCN_DEBUG_STR) + truncate_elf_string_data(file->scn_data[scn]); + } + *ret = file->scn_data[scn]; + return NULL; +} + +struct drgn_error *drgn_elf_file_get_dwarf(struct drgn_elf_file *file, + Dwarf **ret) +{ + struct drgn_error *err; + if (!file->_dwarf) { + struct drgn_elf_file *supplementary_file = + file->module->supplementary_debug_file; + if (supplementary_file) { + supplementary_file->_dwarf = + dwarf_begin_elf(supplementary_file->elf, + DWARF_C_READ, NULL); + if (!supplementary_file->_dwarf) + return drgn_error_libdw(); + } + + err = drgn_elf_file_apply_relocations(file); + if (err) + return err; + + file->_dwarf = dwarf_begin_elf(file->elf, DWARF_C_READ, NULL); + if (!file->_dwarf) + return drgn_error_libdw(); + + if (supplementary_file) + dwarf_setalt(file->_dwarf, supplementary_file->_dwarf); + } + *ret = file->_dwarf; + return NULL; } struct drgn_error * @@ -281,3 +650,114 @@ struct drgn_error *drgn_elf_file_section_buffer_error(struct binary_buffer *bb, return drgn_elf_file_section_error(buffer->file, buffer->scn, buffer->data, ptr, message); } + +static bool elf_address_range_from_first_and_last_segment(Elf *elf, + uint64_t *start_ret, + uint64_t *end_ret) +{ + size_t phnum; + if (elf_getphdrnum(elf, &phnum)) + return false; + + uint64_t start; + GElf_Phdr phdr_mem, *phdr; + size_t i; + for (i = 0; i < phnum; i++) { + phdr = gelf_getphdr(elf, i, &phdr_mem); + if (!phdr) + return false; + if (phdr->p_type == PT_LOAD) { + start = phdr->p_vaddr; + break; + } + } + if (i >= phnum) { + *start_ret = *end_ret = 0; + return true; + } + + for (i = phnum; i-- > 0;) { + phdr = gelf_getphdr(elf, i, &phdr_mem); + if (!phdr) + return false; + + if (phdr->p_type == PT_LOAD) { + uint64_t end = phdr->p_vaddr + phdr->p_memsz; + if (start < end) { + *start_ret = start; + *end_ret = end; + return true; + } + break; + } + } + *start_ret = *end_ret = 0; + return true; +} + +static bool elf_address_range_from_min_and_max_segment(Elf *elf, + uint64_t *start_ret, + uint64_t *end_ret) +{ + size_t phnum; + if (elf_getphdrnum(elf, &phnum)) + return false; + + uint64_t start = UINT64_MAX, end = 0; + for (size_t i = 0; i < phnum; i++) { + GElf_Phdr phdr_mem, *phdr = gelf_getphdr(elf, i, &phdr_mem); + if (!phdr) + return false; + if (phdr->p_type == PT_LOAD) { + start = min(start, phdr->p_vaddr); + end = max(end, phdr->p_vaddr + phdr->p_memsz); + } + } + if (start < end) { + *start_ret = start; + *end_ret = end; + } else { + *start_ret = *end_ret = 0; + } + return true; +} + +bool drgn_elf_file_address_range(struct drgn_elf_file *file, + uint64_t *start_ret, uint64_t *end_ret) +{ + // The ELF specification says that "loadable segment entries in the + // program header table appear in ascending order, sorted on the p_vaddr + // member." However, this is not the case in practice. + // + // vmlinux on some architectures contains special segments whose + // addresses are not meaningful and break the sorted order (e.g., + // segments corresponding to the .data..percpu section on x86-64 and the + // .vectors and .stubs sections on Arm). It appears that segments in + // vmlinux are sorted other than those special segments, and the special + // segments are never the first or last segment. + // + // Userspace ELF loaders disagree about whether to assume sorted order: + // + // - As of Linux kernel commit 10b19249192a ("ELF: fix overflow in total + // mapping size calculation") (in v5.18), the Linux kernel DOES NOT + // assume sorting. Before that, it DOES. + // - glibc as of v2.40 DOES assume sorting; see _dl_map_object_from_fd() + // in elf/dl-load.c and _dl_map_segments() in elf/dl-map-segments.h. + // - musl as of v1.2.5 DOES NOT assume sorting; see map_library() in + // ldso/dynlink.c. + // + // So, we use a heuristic: if the file has an .init.text section, then + // it is probably a vmlinux file, so we assume the sorted order, which + // allows us to ignore the special segments in the middle. + // + // Otherwise, we don't assume the sorted order. + if (file->is_vmlinux) { + return elf_address_range_from_first_and_last_segment(file->elf, + start_ret, + end_ret); + } else { + return elf_address_range_from_min_and_max_segment(file->elf, + start_ret, + end_ret); + } +} diff --git a/libdrgn/elf_file.h b/libdrgn/elf_file.h index ca2b6f3eb..386d6409c 100644 --- a/libdrgn/elf_file.h +++ b/libdrgn/elf_file.h @@ -43,6 +43,14 @@ struct drgn_module; */ struct drgn_error *read_elf_section(Elf_Scn *scn, Elf_Data **ret); +/** + * Truncate any bytes beyond the last null character in an ELF string table. + * + * This sets `data->d_size` so that any string table index less than + * `data->d_size` is guaranteed to be valid. + */ +void truncate_elf_string_data(Elf_Data *data); + static inline bool elf_data_contains_ptr(Elf_Data *data, const void *ptr) { uintptr_t bufi = (uintptr_t)data->d_buf; @@ -55,11 +63,38 @@ struct drgn_elf_file { /** Module using this file. */ struct drgn_module *module; /** Filesystem path to this file. */ - const char *path; + char *path; + /** + * Memory image backing @ref elf. + * + * @c NULL if not backed by a memory image. + */ + char *image; + /** + * File descriptor backing @ref elf. + * + * -1 if not backed by a file. + */ + int fd; + /** Whether the file is loadable. */ + bool is_loadable; + /** Whether the file is relocatable. */ + bool is_relocatable; + /** Whether the file still need to have relocations applied. */ + bool needs_relocation; + /** Whether the file is a Linux kernel image (`vmlinux`). */ + bool is_vmlinux; /** libelf handle. */ Elf *elf; - /** libdw handle if we're using DWARF information from this file. */ - Dwarf *dwarf; + /** + * libdw handle. + * + * @c NULL if not yet created. + * + * Don't access this directly. Get it with @ref + * drgn_elf_file_get_dwarf() instead. + */ + Dwarf *_dwarf; /** * Platform of this file. * @@ -86,16 +121,33 @@ struct drgn_elf_file { Elf_Data *alt_debug_str_data; }; +/** + * Create a @ref drgn_elf_file. + * + * On success, this takes ownership of @p fd, @p image, and @p elf. @p path is + * copied. + */ struct drgn_error *drgn_elf_file_create(struct drgn_module *module, - const char *path, Elf *elf, - struct drgn_elf_file **ret); + const char *path, int fd, char *image, + Elf *elf, struct drgn_elf_file **ret); void drgn_elf_file_destroy(struct drgn_elf_file *file); -struct drgn_error *drgn_elf_file_precache_sections(struct drgn_elf_file *file); - +/** Apply ELF relocations to the file if needed. */ struct drgn_error * -drgn_elf_file_cache_section(struct drgn_elf_file *file, enum drgn_section_index scn); +drgn_elf_file_apply_relocations(struct drgn_elf_file *file); + +/** + * Read an indexed ELF section. + * + * This applies ELF relocations to the file first if needed. + */ +struct drgn_error *drgn_elf_file_read_section(struct drgn_elf_file *file, + enum drgn_section_index scn, + Elf_Data **ret); + +struct drgn_error *drgn_elf_file_get_dwarf(struct drgn_elf_file *file, + Dwarf **ret); static inline bool drgn_elf_file_is_little_endian(const struct drgn_elf_file *file) @@ -108,6 +160,12 @@ static inline bool drgn_elf_file_bswap(const struct drgn_elf_file *file) return drgn_platform_bswap(&file->platform); } +static inline bool +drgn_elf_file_is_64_bit(const struct drgn_elf_file *file) +{ + return drgn_platform_is_64_bit(&file->platform); +} + static inline uint8_t drgn_elf_file_address_size(const struct drgn_elf_file *file) { @@ -120,6 +178,12 @@ drgn_elf_file_address_mask(const struct drgn_elf_file *file) return drgn_platform_address_mask(&file->platform); } +static inline bool drgn_elf_file_has_dwarf(const struct drgn_elf_file *file) +{ + return (file->scns[DRGN_SCN_DEBUG_INFO] + && file->scns[DRGN_SCN_DEBUG_ABBREV]); +} + struct drgn_error * drgn_elf_file_section_error(struct drgn_elf_file *file, Elf_Scn *scn, Elf_Data *data, const char *ptr, @@ -156,6 +220,10 @@ drgn_elf_file_section_buffer_init(struct drgn_elf_file_section_buffer *buffer, buffer->data = data; } +/** + * Initialize a @ref binary_buffer for an indexed ELF section that has already + * been read. + */ static inline void drgn_elf_file_section_buffer_init_index(struct drgn_elf_file_section_buffer *buffer, struct drgn_elf_file *file, @@ -165,6 +233,32 @@ drgn_elf_file_section_buffer_init_index(struct drgn_elf_file_section_buffer *buf file->scn_data[scn]); } +/** + * Read an indexed ELF section (applying ELF relocations if needed) and + * initialize a @ref binary_buffer for it. + */ +static inline struct drgn_error * +drgn_elf_file_section_buffer_read(struct drgn_elf_file_section_buffer *buffer, + struct drgn_elf_file *file, + enum drgn_section_index scn) +{ + Elf_Data *data; + struct drgn_error *err = drgn_elf_file_read_section(file, scn, &data); + if (err) + return err; + drgn_elf_file_section_buffer_init(buffer, file, file->scns[scn], data); + return NULL; +} + +/** + * Return the virtual address range of an ELF file. + * + * @param[out] start_ret Minimum virtual address (inclusive). + * @param[out] end_ret Maximum virtual address (exclusive). + */ +bool drgn_elf_file_address_range(struct drgn_elf_file *file, + uint64_t *start_ret, uint64_t *end_ret); + /** @} */ #endif /* DRGN_ELF_FILE_H */ diff --git a/libdrgn/elf_symtab.c b/libdrgn/elf_symtab.c new file mode 100644 index 000000000..0c0dd3ae9 --- /dev/null +++ b/libdrgn/elf_symtab.c @@ -0,0 +1,450 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include +#include +#include + +#include "debug_info.h" +#include "elf_file.h" +#include "elf_symtab.h" +#include "error.h" +#include "log.h" +#include "minmax.h" +#include "serialize.h" +#include "util.h" + +static struct drgn_error *find_elf_file_symtab(struct drgn_elf_file *file, + uint64_t bias, + struct drgn_elf_file **file_ret, + uint64_t *bias_ret, + Elf_Scn **scn_ret, + GElf_Word *strtab_idx_ret, + GElf_Word *num_local_symbols_ret, + bool *full_symtab_ret) +{ + Elf_Scn *scn = NULL; + while ((scn = elf_nextscn(file->elf, scn))) { + GElf_Shdr shdr_mem, *shdr = gelf_getshdr(scn, &shdr_mem); + if (!shdr) + return drgn_error_libelf(); + + if (shdr->sh_type == SHT_SYMTAB + || shdr->sh_type == SHT_DYNSYM) { + *file_ret = file; + *bias_ret = bias; + *scn_ret = scn; + *strtab_idx_ret = shdr->sh_link; + *num_local_symbols_ret = shdr->sh_info; + if (shdr->sh_type == SHT_SYMTAB) { + *full_symtab_ret = true; + return NULL; + } + } + } + return NULL; +} + +static struct drgn_error * +find_module_elf_symtab(struct drgn_module *module) +{ + struct drgn_error *err; + + if (!module->elf_symtab_pending_files) + return NULL; + + if (module->elf_symtab.num_symbols > 0 && !module->have_full_symtab) { + module->elf_symtab_pending_files = 0; + return NULL; + } + + struct drgn_elf_file *file = NULL; + uint64_t bias; + Elf_Scn *symtab_scn; + GElf_Word strtab_idx, num_local_symbols; + bool full_symtab = false; + + if (module->elf_symtab_pending_files & DRGN_MODULE_FILE_MASK_DEBUG) { + err = find_elf_file_symtab(module->debug_file, + module->debug_file_bias, &file, + &bias, &symtab_scn, &strtab_idx, + &num_local_symbols, &full_symtab); + if (err) + return err; + } + + if (!full_symtab && + (module->elf_symtab_pending_files & DRGN_MODULE_FILE_MASK_LOADED)) { + err = find_elf_file_symtab(module->loaded_file, + module->loaded_file_bias, &file, + &bias, &symtab_scn, &strtab_idx, + &num_local_symbols, &full_symtab); + if (err) + return err; + } + + if (!file) { + drgn_log_debug(module->prog, "%s: no ELF symbol table", + module->name); + module->elf_symtab_pending_files = 0; + return NULL; + } + + Elf_Scn *strtab_scn = elf_getscn(file->elf, strtab_idx); + if (!strtab_scn) + return drgn_error_libelf(); + + Elf_Data *data, *strtab_data; + if ((err = read_elf_section(symtab_scn, &data)) + || (err = read_elf_section(strtab_scn, &strtab_data))) + if (err) + return err; + + truncate_elf_string_data(strtab_data); + + Elf_Data *shndx_data = NULL; + int shndx_idx = elf_scnshndx(symtab_scn); + if (shndx_idx > 0) { + Elf_Scn *shndx_scn = elf_getscn(file->elf, shndx_idx); + if (!shndx_scn) + return drgn_error_libelf(); + err = read_elf_section(shndx_scn, &shndx_data); + if (err) + return err; + } + + module->elf_symtab.file = file; + module->elf_symtab.bias = bias; + module->elf_symtab.data = data->d_buf; + module->elf_symtab.num_symbols = + data->d_size + / (drgn_elf_file_is_64_bit(file) + ? sizeof(Elf64_Sym) : sizeof(Elf32_Sym)); + if (num_local_symbols < 1) + num_local_symbols = 1; + if (num_local_symbols > module->elf_symtab.num_symbols) + num_local_symbols = module->elf_symtab.num_symbols; + module->elf_symtab.num_local_symbols = num_local_symbols; + module->elf_symtab.strtab = strtab_data; + module->elf_symtab.shndx = shndx_data; + module->elf_symtab_pending_files = 0; + module->have_full_symtab = full_symtab; + + drgn_log_debug(module->prog, + "%s: found ELF %ssymbol table with %zu symbols", + module->name, full_symtab ? "" : "dynamic ", + module->elf_symtab.num_symbols); + + return NULL; +} + +static size_t elf_symbol_shndx(struct drgn_module *module, size_t sym_idx, + const GElf_Sym *sym) +{ + if (sym->st_shndx < SHN_LORESERVE) + return sym->st_shndx; + if (sym->st_shndx == SHN_XINDEX + && module->elf_symtab.shndx + && sym_idx < + module->elf_symtab.shndx->d_size / sizeof(uint32_t)) { + uint32_t tmp; + memcpy(&tmp, + (const char *)module->elf_symtab.shndx->d_buf + + sym_idx * sizeof(uint32_t), + sizeof(uint32_t)); + if (drgn_elf_file_bswap(module->elf_symtab.file)) + tmp = bswap_32(tmp); + return tmp; + } + return SHN_UNDEF; +} + +static bool elf_symbol_address(struct drgn_module *module, size_t sym_idx, + const GElf_Sym *sym, uint64_t *ret) +{ + uint64_t addr = sym->st_value; + + // On 32-bit Arm, the least significant bit of st_value in an STT_FUNC + // symbol indicates whether it addresses a Thumb instruction. Clear it. + // + // P.S. If we need any more architecture-specific hacks, then we should + // add a callback to drgn_architecture_info. Note that we don't + // currently support V1 of the 64-bit PowerPC ELF ABI where st_value is + // the address of a "function descriptor" instead of the function entry + // point. + if (module->elf_symtab.file->platform.arch->arch == DRGN_ARCH_ARM + && GELF_ST_TYPE(sym->st_info) == STT_FUNC) + addr &= ~1; + + addr += module->elf_symtab.bias; + if (module->elf_symtab.file->is_relocatable) { + size_t shndx = elf_symbol_shndx(module, sym_idx, sym); + if (shndx == SHN_UNDEF) + return false; + Elf_Scn *scn = elf_getscn(module->elf_symtab.file->elf, shndx); + if (!scn) + return false; + GElf_Shdr shdr_mem, *shdr = gelf_getshdr(scn, &shdr_mem); + if (!shdr) + return false; + addr += shdr->sh_addr; + } + *ret = addr; + return true; +} + +// When searching for one symbol, if there are multiple matches, we break ties +// based on the symbol binding. The order of precedence is: +// GLOBAL = UNIQUE > WEAK > LOCAL = everything else +static int drgn_symbol_binding_precedence(const struct drgn_symbol *sym) +{ + SWITCH_ENUM(sym->binding) { + case DRGN_SYMBOL_BINDING_GLOBAL: + case DRGN_SYMBOL_BINDING_UNIQUE: + return 3; + case DRGN_SYMBOL_BINDING_WEAK: + return 2; + case DRGN_SYMBOL_BINDING_LOCAL: + case DRGN_SYMBOL_BINDING_UNKNOWN: + return 1; + default: + UNREACHABLE(); + } +} + +static int elf_symbol_binding_precedence(const GElf_Sym *sym) +{ + switch (GELF_ST_BIND(sym->st_info)) { + case STB_GLOBAL: + case STB_GNU_UNIQUE: + return 3; + case STB_WEAK: + return 2; + default: + return 1; + } +} + +// This assumes that both symbols contain the search address. +static bool better_addr_match(const GElf_Sym *a, uint64_t a_addr, + const struct drgn_symbol *b) +{ + // Prefer the symbol that starts closer to the search address. + if (a_addr > b->address) + return true; + if (a_addr < b->address) + return false; + + // If the symbols have the same start address, prefer the one that ends + // closer to the search address. + if (a->st_size < b->size) + return true; + if (a->st_size > b->size) + return false; + + // If the symbols have the same start and end addresses, prefer the one + // with the higher binding precedence. + return elf_symbol_binding_precedence(a) + > drgn_symbol_binding_precedence(b); +} + +// This assumes that both symbols start before the search address and have size +// 0. +static bool better_sizeless_addr_match(const GElf_Sym *a, uint64_t a_addr, + const GElf_Sym *b, uint64_t b_addr) +{ + // Prefer the symbol that starts closer to the search address. + if (a_addr > b_addr) + return true; + if (a_addr < b_addr) + return false; + + // If the symbols have the same start address, prefer the one with the + // higher binding precedence. + return elf_symbol_binding_precedence(a) + > elf_symbol_binding_precedence(b); +} + +static bool addr_in_sym_section(struct drgn_module *module, size_t sym_idx, + const GElf_Sym *sym, uint64_t unbiased_addr) +{ + size_t shndx = elf_symbol_shndx(module, sym_idx, sym); + if (shndx == SHN_UNDEF) + return false; + Elf_Scn *scn = elf_getscn(module->elf_symtab.file->elf, shndx); + if (!scn) + return false; + GElf_Shdr shdr_mem, *shdr = gelf_getshdr(scn, &shdr_mem); + if (!shdr) + return false; + return unbiased_addr >= shdr->sh_addr + && (unbiased_addr - shdr->sh_addr < shdr->sh_size); +} + +struct drgn_error * +drgn_module_elf_symbols_search(struct drgn_module *module, const char *name, + uint64_t addr, enum drgn_find_symbol_flags flags, + struct drgn_symbol_result_builder *builder) +{ + struct drgn_error *err; + + err = find_module_elf_symtab(module); + if (err) + return err; + if (module->elf_symtab.num_symbols == 0) + return NULL; + + const bool is_64_bit = drgn_elf_file_is_64_bit(module->elf_symtab.file); + const bool bswap = drgn_elf_file_bswap(module->elf_symtab.file); + const size_t sym_size = + is_64_bit ? sizeof(Elf64_Sym) : sizeof(Elf32_Sym); + + // Handwritten assembly functions may have a symbol size of 0 even + // though logically they have a size. The best we can do is assume that + // such a symbol extends until the next symbol. If we're searching by + // address and we don't find any symbols containing the address, then we + // will return a symbol with size 0 that could contain it based on this + // assumption. + const char *sizeless_name = NULL; + // Silence -Wmaybe-uninitialized false positives on sizeless_addr and + // sizeless_sym_idx last seen with GCC 12. + uint64_t sizeless_addr = 0; + size_t sizeless_sym_idx = 0; + Elf64_Sym sizeless_sym; + // The maximum end address of any symbol starting before the given + // address. Any symbol with size 0 starting before this is either + // contained within another symbol or is assumed to end before this, so + // it should be ignored. + uint64_t max_end_addr = 0; + + // If we're searching for one symbol, then we may already have a match, + // but we still need to search for a better match. This is only possible + // if we're not searching by address, because address searches only + // search one module. + struct drgn_symbol *best_sym = NULL; + if (flags & DRGN_FIND_SYMBOL_ONE) + best_sym = drgn_symbol_result_builder_single(builder); + + // If we already have a match, then we will never prefer a local symbol + // over that match, so we can skip local symbols. + // + // Otherwise, skip the undefined symbol at index 0. + for (size_t i = best_sym ? module->elf_symtab.num_local_symbols : 1; + i < module->elf_symtab.num_symbols; i++) { + Elf64_Sym elf_sym; +#define visit_elf_sym_members(visit_scalar_member, visit_raw_member) do { \ + visit_scalar_member(st_name); \ + visit_scalar_member(st_info); \ + visit_scalar_member(st_other); \ + visit_scalar_member(st_shndx); \ + visit_scalar_member(st_value); \ + visit_scalar_member(st_size); \ +} while (0) + deserialize_struct64(&elf_sym, Elf32_Sym, visit_elf_sym_members, + module->elf_symtab.data + i * sym_size, + is_64_bit, bswap); +#undef visit_elf_sym_members + + // Ignore undefined symbols. + if (elf_sym.st_shndx == SHN_UNDEF) + continue; + + // Ignore symbols with an out-of-bounds name. + if (elf_sym.st_name >= module->elf_symtab.strtab->d_size) + continue; + const char *elf_sym_name = + (const char *)module->elf_symtab.strtab->d_buf + + elf_sym.st_name; + + if ((flags & DRGN_FIND_SYMBOL_NAME) + && strcmp(elf_sym_name, name) != 0) + continue; + + if (flags & DRGN_FIND_SYMBOL_ADDR) { + // Ignore these special symbol types for address + // searches (before we bother computing the address). + switch (GELF_ST_TYPE(elf_sym.st_info)) { + case STT_SECTION: + case STT_FILE: + case STT_TLS: + continue; + default: + break; + } + } else if (best_sym + // This is a non-address search for one symbol. + // Prefer the symbol with the higher binding + // precedence. + && elf_symbol_binding_precedence(&elf_sym) + <= drgn_symbol_binding_precedence(best_sym)) { + continue; + } + + uint64_t elf_sym_addr; + if (!elf_symbol_address(module, i, &elf_sym, &elf_sym_addr)) + continue; + + if (flags & DRGN_FIND_SYMBOL_ADDR) { + if (elf_sym_addr > addr) + continue; + + max_end_addr = max(max_end_addr, + elf_sym_addr + elf_sym.st_size); + + if (elf_sym.st_size == 0) { + if (!sizeless_name + || better_sizeless_addr_match(&elf_sym, + elf_sym_addr, + &sizeless_sym, + sizeless_addr)) { + sizeless_name = elf_sym_name; + sizeless_addr = elf_sym_addr; + sizeless_sym_idx = i; + sizeless_sym = elf_sym; + } + continue; + } else if (addr - elf_sym_addr >= elf_sym.st_size + || (best_sym + && !better_addr_match(&elf_sym, + elf_sym_addr, + best_sym))) { + continue; + } + } + + if (!drgn_symbol_result_builder_add_from_elf(builder, + elf_sym_name, + elf_sym_addr, + &elf_sym)) + return &drgn_enomem; + + if (flags & DRGN_FIND_SYMBOL_ONE) { + best_sym = drgn_symbol_result_builder_single(builder); + if (!(flags & DRGN_FIND_SYMBOL_ADDR)) { + // If we're not searching by address and we find + // a matching global symbol, then we don't need + // to search anymore. + if (best_sym->binding == DRGN_SYMBOL_BINDING_GLOBAL + || best_sym->binding == DRGN_SYMBOL_BINDING_UNIQUE) + return &drgn_stop; + // Otherwise, if we're searching by address and + // we find a matching local symbol, then we can + // skip past the remaining local symbols. + if (i < module->elf_symtab.num_local_symbols) + i = module->elf_symtab.num_local_symbols - 1; + } + } + } + + if (sizeless_name + && drgn_symbol_result_builder_count(builder) == 0 + && sizeless_addr >= max_end_addr + && addr_in_sym_section(module, sizeless_sym_idx, &sizeless_sym, + addr - module->elf_symtab.bias) + && !drgn_symbol_result_builder_add_from_elf(builder, sizeless_name, + sizeless_addr, + &sizeless_sym)) + return &drgn_enomem; + + return NULL; +} diff --git a/libdrgn/elf_symtab.h b/libdrgn/elf_symtab.h new file mode 100644 index 000000000..298f93a84 --- /dev/null +++ b/libdrgn/elf_symtab.h @@ -0,0 +1,55 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// SPDX-License-Identifier: LGPL-2.1-or-later + +/** + * @file + * + * ELF symbol tables. + * + * See @ref ElfSymtab. + */ + +#ifndef DRGN_ELF_SYMBOL_H +#define DRGN_ELF_SYMBOL_H + +#include "drgn_internal.h" + +struct drgn_elf_file; + +/** + * @ingroup Internals + * + * @defgroup ElfSymtab ELF symbol tables + * + * ELF symbol table lookups. + * + * @{ + */ + +/** Symbol table from an ELF file. */ +struct drgn_elf_symbol_table { + /** File containing symbol table. @c NULL if not found yet. */ + struct drgn_elf_file *file; + /** Bias to apply to addresses from the file. */ + uint64_t bias; + /** Symbol table section data. */ + const char *data; + /** Number of symbols in table. */ + size_t num_symbols; + /** Number of local symbols in table. */ + size_t num_local_symbols; + /** String table section used by symbol table. */ + Elf_Data *strtab; + /** Optional `SHT_SYMTAB_SHNDX` section used by symbol table. */ + Elf_Data *shndx; +}; + +/** Find matching ELF symbols in a specific module. */ +struct drgn_error * +drgn_module_elf_symbols_search(struct drgn_module *module, const char *name, + uint64_t addr, enum drgn_find_symbol_flags flags, + struct drgn_symbol_result_builder *builder); + +/** @} */ + +#endif /* DRGN_ELF_SYMBOL_H */ diff --git a/libdrgn/error.c b/libdrgn/error.c index 95174abbe..27d9a2dc5 100644 --- a/libdrgn/error.c +++ b/libdrgn/error.c @@ -2,7 +2,6 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include -#include #include #include #include @@ -158,16 +157,6 @@ drgn_error_format_fault(uint64_t address, const char *format, ...) return err; } -struct drgn_error *drgn_error_from_string_builder(enum drgn_error_code code, - struct string_builder *sb) -{ - if (!string_builder_null_terminate(sb)) { - string_builder_deinit(sb); - return &drgn_enomem; - } - return drgn_error_create_nodup(code, sb->str); -} - LIBDRGN_PUBLIC struct drgn_error *drgn_error_copy(struct drgn_error *src) { if (!src->needs_destroy) @@ -274,9 +263,3 @@ struct drgn_error *drgn_error_libdw(void) return drgn_error_format(DRGN_ERROR_OTHER, "libdw error: %s", dwarf_errmsg(-1)); } - -struct drgn_error *drgn_error_libdwfl(void) -{ - return drgn_error_format(DRGN_ERROR_OTHER, "libdwfl error: %s", - dwfl_errmsg(-1)); -} diff --git a/libdrgn/error.h b/libdrgn/error.h index d9ee35368..40f57ca96 100644 --- a/libdrgn/error.h +++ b/libdrgn/error.h @@ -33,15 +33,16 @@ extern struct drgn_error drgn_stop; /** Global @ref DRGN_ERROR_OBJECT_ABSENT error. */ extern struct drgn_error drgn_error_object_absent; -struct string_builder; - /** - * Create a @ref drgn_error with a message from a @ref string_builder. - * - * This deinitializes the string builder. + * Return whether an error is fatal, meaning that it should usually be returned + * to the caller instead of being handled or logged. */ -struct drgn_error *drgn_error_from_string_builder(enum drgn_error_code code, - struct string_builder *sb); +static inline bool drgn_error_is_fatal(struct drgn_error *err) +{ + return err == &drgn_enomem; +} + +struct string_builder; /** * Append a formatted @ref drgn_error to a @ref string_builder. @@ -60,10 +61,6 @@ struct drgn_error *drgn_error_libelf(void) struct drgn_error *drgn_error_libdw(void) __attribute__((__returns_nonnull__)); -/** Create a @ref drgn_error from the libdwfl error indicator. */ -struct drgn_error *drgn_error_libdwfl(void) - __attribute__((__returns_nonnull__)); - /** * Create a @ref drgn_error with a type name. * diff --git a/libdrgn/examples/load_debug_info.c b/libdrgn/examples/load_debug_info.c index 953771df1..284c8b67e 100644 --- a/libdrgn/examples/load_debug_info.c +++ b/libdrgn/examples/load_debug_info.c @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -91,6 +92,8 @@ noreturn static void usage(bool error) int main(int argc, char **argv) { + setlocale(LC_ALL, ""); + struct option long_options[] = { {"kernel", no_argument, NULL, 'k'}, {"core", required_argument, NULL, 'c'}, diff --git a/libdrgn/handler.h b/libdrgn/handler.h index 3b7d8b7cb..c73b9f2cf 100644 --- a/libdrgn/handler.h +++ b/libdrgn/handler.h @@ -51,6 +51,11 @@ struct drgn_error *drgn_handler_list_enabled(struct drgn_handler_list *list, const char ***names_ret, size_t *count_ret); +static inline bool drgn_handler_is_last_enabled(struct drgn_handler *handler) +{ + return handler->enabled && (!handler->next || !handler->next->enabled); +} + // Helper to simplify the casting and naming in drgn_handler_list_deinit(). static inline struct drgn_handler * drgn_handler_free_and_next(struct drgn_handler *handler) diff --git a/libdrgn/linux_kernel.c b/libdrgn/linux_kernel.c index 30f167d47..61d5e5e52 100644 --- a/libdrgn/linux_kernel.c +++ b/libdrgn/linux_kernel.c @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -16,6 +17,7 @@ #include #include +#include "array.h" #include "binary_buffer.h" #include "cleanup.h" #include "debug_info.h" @@ -28,6 +30,7 @@ #include "hexlify.h" #include "io.h" #include "linux_kernel.h" +#include "log.h" #include "platform.h" #include "program.h" #include "type.h" @@ -410,15 +413,11 @@ struct drgn_error *drgn_program_finish_set_kernel(struct drgn_program *prog) * changes in the future, we can reevaluate this. */ -struct depmod_index { - void *addr; - size_t len; - char path[256]; -}; - -static void depmod_index_deinit(struct depmod_index *depmod) +void depmod_index_deinit(struct depmod_index *depmod) { - munmap(depmod->addr, depmod->len); + if (depmod->len > 0) + munmap(depmod->addr, depmod->len); + free(depmod->path); } struct depmod_index_buffer { @@ -469,33 +468,41 @@ static struct drgn_error *depmod_index_validate(struct depmod_index *depmod) return NULL; } +__attribute__((__format__(__printf__, 2, 3))) static struct drgn_error *depmod_index_init(struct depmod_index *depmod, - const char *osrelease) + const char *path_format, + ...) { struct drgn_error *err; - snprintf(depmod->path, sizeof(depmod->path), - "/lib/modules/%s/modules.dep.bin", osrelease); + va_list ap; + va_start(ap, path_format); + int r = vasprintf(&depmod->path, path_format, ap); + va_end(ap); + if (r < 0) + return &drgn_enomem; int fd = open(depmod->path, O_RDONLY); - if (fd == -1) - return drgn_error_create_os("open", errno, depmod->path); + if (fd == -1) { + err = drgn_error_create_os("open", errno, depmod->path); + goto out_path; + } struct stat st; if (fstat(fd, &st) == -1) { err = drgn_error_create_os("fstat", errno, depmod->path); - goto out; + goto out_fd; } - if (st.st_size < 0 || st.st_size > SIZE_MAX) { + if (st.st_size > SIZE_MAX) { err = &drgn_enomem; - goto out; + goto out_fd; } void *addr = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0); if (addr == MAP_FAILED) { err = drgn_error_create_os("mmap", errno, depmod->path); - goto out; + goto out_fd; } depmod->addr = addr; @@ -504,8 +511,11 @@ static struct drgn_error *depmod_index_init(struct depmod_index *depmod, err = depmod_index_validate(depmod); if (err) depmod_index_deinit(depmod); -out: +out_fd: close(fd); +out_path: + if (err) + free(depmod->path); return err; } @@ -610,312 +620,393 @@ static struct drgn_error *depmod_index_find(struct depmod_index *depmod, return NULL; } -struct kernel_module_iterator { - char *name; - uint64_t start, end; - void *build_id_buf; - size_t build_id_buf_capacity; - /* `struct module` type. */ - struct drgn_qualified_type module_type; - /* Current `struct module` (not a pointer). */ - struct drgn_object mod; - /* `struct list_head *` in next module to return. */ - struct drgn_object node; - /* Temporary objects reused for various purposes. */ - struct drgn_object tmp1, tmp2, tmp3; - /* Address of `struct list_head modules`. */ - uint64_t head; - bool use_sys_module; - bool use_sys_module_sections; -}; - -static void kernel_module_iterator_deinit(struct kernel_module_iterator *it) +struct drgn_error * +drgn_module_try_vmlinux_files(struct drgn_module *module, + struct drgn_module_standard_files_state *state) { - drgn_object_deinit(&it->tmp3); - drgn_object_deinit(&it->tmp2); - drgn_object_deinit(&it->tmp1); - drgn_object_deinit(&it->node); - drgn_object_deinit(&it->mod); - free(it->build_id_buf); - free(it->name); + struct drgn_error *err; + struct drgn_program *prog = module->prog; + const char *osrelease = prog->vmcoreinfo.osrelease; + + // Paths relative to the debug directory where vmlinux might be + // installed. + static const char * const debug_dir_paths[] = { + // Debian, Ubuntu: + "/boot/vmlinux-%s", + // Fedora, CentOS: + "/lib/modules/%s/vmlinux", + // SUSE: + "/lib/modules/%s/vmlinux.debug", + }; + STRING_BUILDER(sb); + const char *debug_dir; + size_t debug_dir_len; + drgn_program_for_each_debug_dir(prog, debug_dir, debug_dir_len) { + if (debug_dir_len == 0 || debug_dir[0] != '/') + continue; + array_for_each(format, debug_dir_paths) { + if (!string_builder_appendn(&sb, debug_dir, + debug_dir_len) + || !string_builder_appendf(&sb, *format, osrelease) + || !string_builder_null_terminate(&sb)) + return &drgn_enomem; + err = drgn_module_try_standard_file(module, sb.str, -1, + true, NULL); + if (err || !drgn_module_wants_file(module)) + return err; + sb.len = 0; + } + } + + // Absolute paths where vmlinux might be installed. + static const char * const paths[] = { + "/boot/vmlinux-%s", + "/lib/modules/%s/build/vmlinux", + "/lib/modules/%s/vmlinux", + }; + array_for_each(format, paths) { + if (!string_builder_appendf(&sb, *format, osrelease) + || !string_builder_null_terminate(&sb)) + return &drgn_enomem; + err = drgn_module_try_standard_file(module, sb.str, -1, true, + NULL); + if (err || !drgn_module_wants_file(module)) + return err; + sb.len = 0; + } + return NULL; } -static struct drgn_error * -kernel_module_iterator_init(struct kernel_module_iterator *it, - struct drgn_program *prog, bool use_sys_module) +struct drgn_error * +drgn_module_try_linux_kmod_files(struct drgn_module *module, + struct drgn_module_standard_files_state *state) { struct drgn_error *err; + struct drgn_program *prog = module->prog; + struct depmod_index *modules_dep = &state->modules_dep; - it->name = NULL; - it->build_id_buf = NULL; - it->build_id_buf_capacity = 0; - it->use_sys_module = use_sys_module; - it->use_sys_module_sections = use_sys_module; - err = drgn_program_find_type(prog, "struct module", NULL, - &it->module_type); - if (err) - return err; + if (!modules_dep->addr) { + err = depmod_index_init(modules_dep, + "/lib/modules/%s/modules.dep.bin", + prog->vmcoreinfo.osrelease); + if (err) { + if (drgn_error_is_fatal(err)) + return err; + drgn_error_log_debug(prog, err, + "couldn't open depmod index: "); + drgn_error_destroy(err); + modules_dep->path = NULL; + modules_dep->addr = MAP_FAILED; + modules_dep->len = 0; + } else { + drgn_log_debug(prog, "opened depmod index %s", + modules_dep->path); + } + } + if (modules_dep->len == 0) + return NULL; - drgn_object_init(&it->mod, prog); - drgn_object_init(&it->node, prog); - drgn_object_init(&it->tmp1, prog); - drgn_object_init(&it->tmp2, prog); - drgn_object_init(&it->tmp3, prog); + const char *depmod_path; + size_t depmod_path_len; + err = depmod_index_find(modules_dep, module->name, &depmod_path, + &depmod_path_len); + if (err) { + drgn_error_log_debug(prog, err, + "couldn't parse depmod index: "); + drgn_error_destroy(err); + return NULL; + } + if (!depmod_path) { + drgn_log_debug(prog, "couldn't find %s in depmod index", + module->name); + return NULL; + } + drgn_log_debug(prog, "found %.*s in depmod index", + depmod_path_len > INT_MAX + ? INT_MAX : (int)depmod_path_len, + depmod_path); + + // Get the length of the path with one extension after ".ko" removed if + // present (e.g., ".gz", ".xz", or ".zst"). + const char *name = memrchr(depmod_path, '/', depmod_path_len); + if (name) + name = name + 1; + else + name = depmod_path; + const char *name_end = depmod_path + depmod_path_len; + size_t ko_len = depmod_path_len; + for (int j = 0; j < 2; j++) { + char *dot = memrchr(name, '.', name_end - name); + if (!dot) + break; + if (name_end - dot == 3 + && dot[1] == 'k' && dot[2] == 'o') { + ko_len = name_end - depmod_path; + break; + } + name_end = dot; + } - err = drgn_program_find_object(prog, "modules", NULL, - DRGN_FIND_OBJECT_VARIABLE, &it->node); - if (err) - goto err; - if (it->node.kind != DRGN_OBJECT_REFERENCE) { - err = drgn_error_create(DRGN_ERROR_OTHER, - "can't get address of modules list"); - goto err; + const char *osrelease = prog->vmcoreinfo.osrelease; + STRING_BUILDER(sb); + const char *debug_dir; + size_t debug_dir_len; + drgn_program_for_each_debug_dir(prog, debug_dir, debug_dir_len) { + if (debug_dir_len == 0 || debug_dir[0] != '/') + continue; + // Debian, Ubuntu: + // $debug_dir/lib/modules/$(uname -r)/$ko_name + if (!string_builder_appendn(&sb, debug_dir, debug_dir_len) + || !string_builder_appendn(&sb, depmod_path, ko_len) + || !string_builder_null_terminate(&sb)) + return &drgn_enomem; + err = drgn_module_try_standard_file(module, sb.str, -1, true, + NULL); + if (err || !drgn_module_wants_file(module)) + return err; + + // Fedora, CentOS, SUSE: + // $debug_dir/lib/modules/$(uname -r)/$ko_name.debug + if (!string_builder_append(&sb, ".debug") + || !string_builder_null_terminate(&sb)) + return &drgn_enomem; + err = drgn_module_try_standard_file(module, sb.str, -1, true, + NULL); + if (err || !drgn_module_wants_file(module)) + return err; } - it->head = it->node.address; - err = drgn_object_member(&it->node, &it->node, "next"); - if (err) - goto err; - err = drgn_object_read(&it->node, &it->node); - if (err) - goto err; + sb.len = 0; + if (!string_builder_appendf(&sb, "/lib/modules/%s/", osrelease) || + !string_builder_appendn(&sb, depmod_path, depmod_path_len) || + !string_builder_null_terminate(&sb)) + return &drgn_enomem; + return drgn_module_try_standard_file(module, sb.str, -1, true, NULL); +} + +// This has a weird calling convention so that the caller can call +// drgn_error_format_os() itself. +static const char *get_gnu_build_id_from_note_file(int fd, + void **bufp, + size_t *buf_capacityp, + const void **build_id_ret, + size_t *build_id_len_ret) +{ + struct stat st; + if (fstat(fd, &st) < 0) + return "fstat"; + + if (st.st_size > SSIZE_MAX + || !alloc_or_reuse(bufp, buf_capacityp, st.st_size)) + return ""; + + ssize_t r = read_all(fd, *bufp, st.st_size); + if (r < 0) + return "read"; + *build_id_len_ret = parse_gnu_build_id_from_notes(*bufp, r, 4, false, + build_id_ret); return NULL; +} -err: - kernel_module_iterator_deinit(it); - return err; +static struct drgn_error * +get_build_id_from_sys_kernel_notes(void **buf_ret, + const void **build_id_ret, + size_t *build_id_len_ret) +{ + static const char path[] = "/sys/kernel/notes"; + _cleanup_close_ int fd = open(path, O_RDONLY); + if (fd == -1) + return drgn_error_create_os("open", errno, path); + + _cleanup_free_ void *buf = NULL; + size_t buf_capacity = 0; + const char *message = get_gnu_build_id_from_note_file(fd, &buf, + &buf_capacity, + build_id_ret, + build_id_len_ret); + if (message && message[0]) + return drgn_error_create_os(message, errno, path); + else if (message) + return &drgn_enomem; + *buf_ret = no_cleanup_ptr(buf); + return NULL; +} + +// Arbitrary limit on the number iterations to make through the modules list in +// order to avoid getting stuck in a cycle. +static const int MAX_MODULE_LIST_ITERATIONS = 10000; + +struct linux_kernel_loaded_module_iterator { + struct drgn_module_iterator it; + bool yielded_vmlinux; + int module_list_iterations_remaining; + // `struct module` type. + struct drgn_qualified_type module_type; + // `struct list_head *` in next module to yield. + struct drgn_object node; + // Address of `struct list_head modules`. + uint64_t modules_head; +}; + +static void +linux_kernel_loaded_module_iterator_destroy(struct drgn_module_iterator *_it) +{ + struct linux_kernel_loaded_module_iterator *it = + container_of(_it, struct linux_kernel_loaded_module_iterator, it); + drgn_object_deinit(&it->node); + free(it); } -/** - * Get the the next loaded kernel module. - * - * After this is called, @c it->name is set to the name of the kernel module, - * and @c it->start and @c it->end are set to the address range of the kernel - * module. These are valid until the next time this is called or the iterator is - * destroyed. - * - * @return @c NULL on success, non-@c NULL on error. In particular, when there - * are no more modules, returns &@ref drgn_stop. - */ static struct drgn_error * -kernel_module_iterator_next(struct kernel_module_iterator *it) +yield_vmlinux(struct linux_kernel_loaded_module_iterator *it, + struct drgn_module **ret, bool *new_ret) { struct drgn_error *err; - struct drgn_program *prog = drgn_object_program(&it->mod); - - uint64_t addr; - err = drgn_object_read_unsigned(&it->node, &addr); - if (err) - return err; - if (addr == it->head) - return &drgn_stop; + struct drgn_program *prog = it->it.prog; - err = drgn_object_container_of(&it->mod, &it->node, it->module_type, - "list"); - if (err) - return err; - err = drgn_object_dereference(&it->mod, &it->mod); + _cleanup_(drgn_module_deletep) struct drgn_module *module = NULL; + bool new; + err = drgn_module_find_or_create_main(prog, "kernel", &module, &new); if (err) return err; - // We need several fields from the `struct module`. Especially for - // /proc/kcore, it is faster to read the entire structure (which is <1kB - // as of Linux 6.0) from the core dump all at once than it is to read - // each field individually. - err = drgn_object_read(&it->mod, &it->mod); - if (err) - return err; - err = drgn_object_member(&it->node, &it->mod, "list"); - if (err) - return err; - err = drgn_object_member(&it->node, &it->node, "next"); - if (err) - return err; - - // Set tmp1 to the module base address and tmp2 to the size. - err = drgn_object_member(&it->tmp1, &it->mod, "mem"); - if (!err) { - // Since Linux kernel commit ac3b43283923 ("module: replace - // module_layout with module_memory") (in v6.4), the base and - // size are in the `struct module_memory mem[MOD_TEXT]` member - // of `struct module`. - if (!prog->mod_text_cached) { - err = drgn_program_find_object(drgn_object_program(&it->mod), - "MOD_TEXT", NULL, - DRGN_FIND_OBJECT_CONSTANT, - &it->tmp2); - if (err) - return err; - union drgn_value mod_text_value; - err = drgn_object_read_integer(&it->tmp2, - &mod_text_value); - if (err) - return err; - prog->mod_text = mod_text_value.uvalue; - prog->mod_text_cached = true; - } + if (!new) { + *ret = no_cleanup_ptr(module); + if (new_ret) + *new_ret = new; + return NULL; + } - err = drgn_object_subscript(&it->tmp1, &it->tmp1, - prog->mod_text); + if (prog->vmcoreinfo.build_id_len > 0) { + // Since Linux kernel commit 0935288c6e00 ("kdump: append kernel + // build-id string to VMCOREINFO") (in v5.9), we can get the + // build ID from VMCOREINFO. + err = drgn_module_set_build_id(module, prog->vmcoreinfo.build_id, + prog->vmcoreinfo.build_id_len); if (err) return err; - err = drgn_object_member(&it->tmp2, &it->tmp1, "size"); + drgn_log_debug(prog, + "found kernel build ID %s in VMCOREINFO", + module->build_id_str); + } else if (prog->flags & DRGN_PROGRAM_IS_LIVE) { + // Before that, on the live kernel, we can get the build ID from + // /sys/kernel/notes. + _cleanup_free_ void *build_id_buf = NULL; + const void *build_id; + size_t build_id_len; + err = get_build_id_from_sys_kernel_notes(&build_id_buf, + &build_id, + &build_id_len); if (err) return err; - err = drgn_object_member(&it->tmp1, &it->tmp1, "base"); - if (err) - return err; - } else if (err->code == DRGN_ERROR_LOOKUP) { - // Since Linux kernel commit 7523e4dc5057 ("module: use a - // structure to encapsulate layout.") (in v4.5), the base and - // size are in the `struct module_layout core_layout` member of - // `struct module`. - drgn_error_destroy(err); - - err = drgn_object_member(&it->tmp1, &it->mod, "core_layout"); - if (!err) { - err = drgn_object_member(&it->tmp2, &it->tmp1, "size"); - if (err) - return err; - err = drgn_object_member(&it->tmp1, &it->tmp1, "base"); - if (err) - return err; - } else if (err->code == DRGN_ERROR_LOOKUP) { - // Before that, they are directly in the `struct - // module`. - drgn_error_destroy(err); - - err = drgn_object_member(&it->tmp2, &it->mod, - "core_size"); - if (err) - return err; - err = drgn_object_member(&it->tmp1, &it->mod, - "module_core"); + if (build_id_len > 0) { + err = drgn_module_set_build_id(module, build_id, + build_id_len); if (err) return err; + drgn_log_debug(prog, + "found kernel build ID %s in /sys/kernel/notes", + module->build_id_str); } else { - return err; + drgn_log_debug(prog, + "couldn't find kernel build ID in /sys/kernel/notes"); } } else { - return err; + // Otherwise, we can't get the build ID. + drgn_log_debug(prog, "couldn't find kernel build ID"); } - err = drgn_object_read_unsigned(&it->tmp1, &it->start); - if (err) - return err; - err = drgn_object_read_unsigned(&it->tmp2, &it->end); - if (err) - return err; - it->end += it->start; - - err = drgn_object_member(&it->tmp2, &it->mod, "name"); - if (err) - return err; - char *name; - err = drgn_object_read_c_string(&it->tmp2, &name); - if (err) - return err; - free(it->name); - it->name = name; + *ret = no_cleanup_ptr(module); + if (new_ret) + *new_ret = new; return NULL; } static struct drgn_error * -kernel_module_iterator_gnu_build_id_live(struct kernel_module_iterator *it, - const void **build_id_ret, - size_t *build_id_len_ret) +kernel_module_set_build_id_live(struct drgn_module *module) { struct drgn_error *err; + struct drgn_program *prog = module->prog; - char *path; - if (asprintf(&path, "/sys/module/%s/notes", it->name) == -1) + _cleanup_free_ char *path; + if (asprintf(&path, "/sys/module/%s/notes", module->name) < 0) { + path = NULL; return &drgn_enomem; - DIR *dir = opendir(path); + } + _cleanup_closedir_ DIR *dir = opendir(path); if (!dir) { - err = drgn_error_create_os("opendir", errno, path); - goto out_path; + if (errno == ENOENT) { + drgn_log_debug(prog, "opendir: %s: %m", path); + return NULL; + } else { + return drgn_error_create_os("opendir", errno, path); + } } + _cleanup_free_ void *buf = NULL; + size_t capacity = 0; + struct dirent *ent; while ((errno = 0, ent = readdir(dir))) { if (ent->d_type == DT_DIR) continue; - int fd = openat(dirfd(dir), ent->d_name, O_RDONLY); - if (fd == -1) { - err = drgn_error_format_os("openat", errno, "%s/%s", - path, ent->d_name); - goto out; - } - - struct stat st; - if (fstat(fd, &st) < 0) { - err = drgn_error_format_os("fstat", errno, "%s/%s", - path, ent->d_name); - close(fd); - goto out; - } - - if (st.st_size > SIZE_MAX || - !alloc_or_reuse(&it->build_id_buf, - &it->build_id_buf_capacity, st.st_size)) { - err = &drgn_enomem; - close(fd); - goto out; + _cleanup_close_ int fd = openat(dirfd(dir), ent->d_name, + O_RDONLY); + if (fd < 0) { + return drgn_error_format_os("openat", errno, "%s/%s", + path, ent->d_name); } - ssize_t r = read_all(fd, it->build_id_buf, st.st_size); - if (r < 0) { - err = drgn_error_format_os("read", errno, "%s/%s", path, - ent->d_name); - close(fd); - goto out; + const void *build_id; + size_t build_id_len; + const char *message = + get_gnu_build_id_from_note_file(fd, &buf, &capacity, + &build_id, + &build_id_len); + if (message && message[0]) { + return drgn_error_format_os(message, errno, "%s/%s", + path, ent->d_name); + } else if (message) { + return &drgn_enomem; } - close(fd); - - *build_id_len_ret = - parse_gnu_build_id_from_notes(it->build_id_buf, r, 4, - false, build_id_ret); - if (*build_id_len_ret) { - err = NULL; - goto out; + if (build_id_len > 0) { + err = drgn_module_set_build_id(module, build_id, + build_id_len); + if (!err) { + drgn_log_debug(prog, + "found build ID %s in %s/%s", + module->build_id_str, path, + ent->d_name); + } + return err; } } - if (errno) { - err = drgn_error_create_os("readdir", errno, path); - } else { - *build_id_ret = NULL; - *build_id_len_ret = 0; - err = NULL; - } - -out: - closedir(dir); -out_path: - free(path); - return err; + if (errno) + return drgn_error_create_os("readdir", errno, path); + drgn_log_debug(prog, "couldn't find build ID in %s", path); + return NULL; } static struct drgn_error * -kernel_module_iterator_gnu_build_id(struct kernel_module_iterator *it, - const void **build_id_ret, - size_t *build_id_len_ret) +kernel_module_set_build_id(struct drgn_module *module, + const struct drgn_object *module_obj, + bool use_sys_module) { - if (it->use_sys_module) { - return kernel_module_iterator_gnu_build_id_live(it, - build_id_ret, - build_id_len_ret); - } + if (use_sys_module) + return kernel_module_set_build_id_live(module); struct drgn_error *err; - struct drgn_program *prog = drgn_object_program(&it->mod); + struct drgn_program *prog = module->prog; const bool bswap = drgn_platform_bswap(&prog->platform); DRGN_OBJECT(attrs, prog); DRGN_OBJECT(attr, prog); DRGN_OBJECT(tmp, prog); + _cleanup_free_ void *buf = NULL; + size_t capacity = 0; // n = mod->notes_attrs->notes uint64_t n; - err = drgn_object_member(&attrs, &it->mod, "notes_attrs"); + err = drgn_object_member(&attrs, module_obj, "notes_attrs"); if (err) return err; err = drgn_object_member_dereference(&tmp, &attrs, "notes"); @@ -954,158 +1045,91 @@ kernel_module_iterator_gnu_build_id(struct kernel_module_iterator *it, if (err) return err; - if (size > SIZE_MAX || - !alloc_or_reuse(&it->build_id_buf, - &it->build_id_buf_capacity, size)) + if (size > SIZE_MAX || !alloc_or_reuse(&buf, &capacity, size)) return &drgn_enomem; - err = drgn_program_read_memory(prog, it->build_id_buf, address, - size, false); + err = drgn_program_read_memory(prog, buf, address, size, false); if (err) return err; - *build_id_len_ret = - parse_gnu_build_id_from_notes(it->build_id_buf, size, 4, - bswap, build_id_ret); - if (*build_id_len_ret) - return NULL; + const void *build_id; + size_t build_id_len = + parse_gnu_build_id_from_notes(buf, size, 4, bswap, + &build_id); + if (build_id_len > 0) { + err = drgn_module_set_build_id(module, build_id, + build_id_len); + if (!err) { + drgn_log_debug(prog, + "found build ID %s in notes_attrs", + module->build_id_str); + } + return err; + } } - *build_id_ret = NULL; - *build_id_len_ret = 0; + drgn_log_debug(prog, + "couldn't find build ID in notes_attrs"); return NULL; } -struct kernel_module_section_iterator { - struct kernel_module_iterator *kmod_it; - bool yielded_percpu; - /* /sys/module/$module/sections directory or NULL. */ - DIR *sections_dir; - /* If not using /sys/module/$module/sections. */ - uint64_t i; - uint64_t nsections; - char *name; -}; - static struct drgn_error * -kernel_module_section_iterator_init_no_sys_module(struct kernel_module_section_iterator *it, - struct kernel_module_iterator *kmod_it) +kernel_module_set_section_addresses_live(struct drgn_module *module) { struct drgn_error *err; + struct drgn_program *prog = module->prog; - it->sections_dir = NULL; - it->i = 0; - it->name = NULL; - /* it->nsections = mod->sect_attrs->nsections */ - err = drgn_object_member(&kmod_it->tmp1, &kmod_it->mod, "sect_attrs"); - if (err) - return err; - err = drgn_object_member_dereference(&kmod_it->tmp2, &kmod_it->tmp1, - "nsections"); - if (err) - return err; - err = drgn_object_read_unsigned(&kmod_it->tmp2, &it->nsections); - if (err) - return err; - /* kmod_it->tmp1 = mod->sect_attrs->attrs */ - return drgn_object_member_dereference(&kmod_it->tmp1, &kmod_it->tmp1, - "attrs"); -} - -static struct drgn_error * -kernel_module_section_iterator_init(struct kernel_module_section_iterator *it, - struct kernel_module_iterator *kmod_it) -{ - it->kmod_it = kmod_it; - it->yielded_percpu = false; - if (kmod_it->use_sys_module_sections) { - char *path; - if (asprintf(&path, "/sys/module/%s/sections", - kmod_it->name) == -1) - return &drgn_enomem; - it->sections_dir = opendir(path); - free(path); - if (!it->sections_dir) { - return drgn_error_format_os("opendir", errno, - "/sys/module/%s/sections", - kmod_it->name); - } - return NULL; - } else { - return kernel_module_section_iterator_init_no_sys_module(it, kmod_it); + _cleanup_free_ char *path; + if (asprintf(&path, "/sys/module/%s/sections", module->name) < 0) { + path = NULL; + return &drgn_enomem; } -} - -static void -kernel_module_section_iterator_deinit(struct kernel_module_section_iterator *it) -{ - if (it->sections_dir) - closedir(it->sections_dir); - else - free(it->name); -} + _cleanup_closedir_ DIR *dir = opendir(path); + if (!dir) + return drgn_error_create_os("opendir", errno, path); -static struct drgn_error * -kernel_module_section_iterator_next_live(struct kernel_module_section_iterator *it, - const char **name_ret, - uint64_t *address_ret) -{ struct dirent *ent; - while ((errno = 0, ent = readdir(it->sections_dir))) { + while ((errno = 0, ent = readdir(dir))) { if (ent->d_type == DT_DIR) continue; - if (ent->d_type == DT_UNKNOWN) { - struct stat st; - - if (fstatat(dirfd(it->sections_dir), ent->d_name, &st, - 0) == -1) { - return drgn_error_format_os("fstatat", errno, - "/sys/module/%s/sections/%s", - it->kmod_it->name, - ent->d_name); - } - if (S_ISDIR(st.st_mode)) - continue; - } - int fd = openat(dirfd(it->sections_dir), ent->d_name, O_RDONLY); - if (fd == -1) { - return drgn_error_format_os("openat", errno, - "/sys/module/%s/sections/%s", - it->kmod_it->name, - ent->d_name); + _cleanup_close_ int fd = openat(dirfd(dir), ent->d_name, + O_RDONLY); + if (fd < 0) { + return drgn_error_format_os("openat", errno, "%s/%s", + path, ent->d_name); } - FILE *file = fdopen(fd, "r"); - if (!file) { - close(fd); + + _cleanup_fclose_ FILE *file = fdopen(fd, "r"); + if (!file) return drgn_error_create_os("fdopen", errno, NULL); - } - int ret = fscanf(file, "%" SCNx64, address_ret); - fclose(file); - if (ret != 1) { + uint64_t address; + if (fscanf(file, "%" SCNx64, &address) != 1) { return drgn_error_format(DRGN_ERROR_OTHER, - "could not parse /sys/module/%s/sections/%s", - it->kmod_it->name, - ent->d_name); + "could not parse %s/%s", + path, ent->d_name); } - *name_ret = ent->d_name; - return NULL; - } - if (errno) { - return drgn_error_format_os("readdir", errno, - "/sys/module/%s/sections", - it->kmod_it->name); - } else { - return &drgn_stop; + + drgn_log_debug(prog, "found section %s@0x%" PRIx64 " in %s", + ent->d_name, address, path); + err = drgn_module_set_section_address(module, ent->d_name, + address); + if (err) + return err; } + if (errno) + return drgn_error_create_os("readdir", errno, path); + return NULL; } static struct drgn_error * -kernel_module_section_iterator_next(struct kernel_module_section_iterator *it, - const char **name_ret, - uint64_t *address_ret) +kernel_module_set_section_addresses(struct drgn_module *module, + const struct drgn_object *module_obj, + bool use_sys_module) { struct drgn_error *err; - struct kernel_module_iterator *kmod_it = it->kmod_it; + struct drgn_program *prog = module->prog; + + DRGN_OBJECT(tmp, prog); // As of Linux 6.0, the .data..percpu section is not included in the // section attributes. (kernel/module/sysfs.c:add_sect_attrs() only @@ -1114,656 +1138,479 @@ kernel_module_section_iterator_next(struct kernel_module_section_iterator *it, // for the .data..percpu section.) However, we need this address so that // global per-CPU variables will be relocated correctly. Get it from // `struct module`. - if (!it->yielded_percpu) { - it->yielded_percpu = true; - err = drgn_object_member(&kmod_it->tmp2, &kmod_it->mod, - "percpu"); - if (!err) { - err = drgn_object_read_unsigned(&kmod_it->tmp2, address_ret); + err = drgn_object_member(&tmp, module_obj, "percpu"); + if (!err) { + uint64_t address; + err = drgn_object_read_unsigned(&tmp, &address); + if (err) + return err; + drgn_log_debug(prog, "module percpu is 0x%" PRIx64, address); + // struct module::percpu is NULL if the module doesn't have any + // per-CPU data. + if (address) { + err = drgn_module_set_section_address(module, + ".data..percpu", + address); if (err) return err; - // struct module::percpu is NULL if the module doesn't - // have any per-CPU data. - if (*address_ret) { - *name_ret = ".data..percpu"; - return NULL; - } - } else if (err->code == DRGN_ERROR_LOOKUP) { - // struct module::percpu doesn't exist if !SMP. - drgn_error_destroy(err); - } else { - return err; } + } else if (err->code == DRGN_ERROR_LOOKUP) { + // struct module::percpu doesn't exist if !SMP. + drgn_error_destroy(err); + } else { + return err; } - if (it->sections_dir) { - err = kernel_module_section_iterator_next_live(it, name_ret, - address_ret); - if (err && err->code == DRGN_ERROR_OS && err->errnum == EACCES) { - closedir(it->sections_dir); - drgn_error_destroy(err); - it->kmod_it->use_sys_module_sections = false; - err = kernel_module_section_iterator_init_no_sys_module(it, it->kmod_it); - if (err) - return err; - } else { + if (use_sys_module) { + err = kernel_module_set_section_addresses_live(module); + // We could be debugging /proc/kcore without root privileges via + // an fd that we were passed. If we didn't have permission to + // access the files in /sys/module/$module/sections, fall back + // to the non-live path. + if (!err || err->code != DRGN_ERROR_OS || err->errnum != EACCES) return err; - } + drgn_error_log_debug(prog, err, "falling back to sect_attrs: "); + drgn_error_destroy(err); } - if (it->i >= it->nsections) - return &drgn_stop; - err = drgn_object_subscript(&kmod_it->tmp2, &kmod_it->tmp1, it->i++); + DRGN_OBJECT(attrs, prog); + DRGN_OBJECT(attr, prog); + + err = drgn_object_member(&attrs, module_obj, "sect_attrs"); if (err) return err; - err = drgn_object_member(&kmod_it->tmp3, &kmod_it->tmp2, "address"); + + // i = mod->sect_attrs->nsections + err = drgn_object_member_dereference(&tmp, &attrs, "nsections"); if (err) return err; - err = drgn_object_read_unsigned(&kmod_it->tmp3, address_ret); + uint64_t i; + err = drgn_object_read_unsigned(&tmp, &i); if (err) return err; - /* - * Since Linux kernel commit ed66f991bb19 ("module: Refactor section - * attr into bin attribute") (in v5.8), the section name is - * module_sect_attr.battr.attr.name. Before that, it is simply - * module_sect_attr.name. - */ - err = drgn_object_member(&kmod_it->tmp2, &kmod_it->tmp2, "battr"); - if (!err) { - err = drgn_object_member(&kmod_it->tmp2, &kmod_it->tmp2, - "attr"); + + // attrs = mod->sect_attrs->attrs + err = drgn_object_member_dereference(&attrs, &attrs, "attrs"); + if (err) + return err; + + while (i-- > 0) { + // attr = attrs[i] + err = drgn_object_subscript(&attr, &attrs, i); if (err) return err; - } else { - if (err->code != DRGN_ERROR_LOOKUP) + + // address = attr.address + err = drgn_object_member(&tmp, &attr, "address"); + if (err) + return err; + uint64_t address; + err = drgn_object_read_unsigned(&tmp, &address); + if (err) return err; - drgn_error_destroy(err); - } - err = drgn_object_member(&kmod_it->tmp3, &kmod_it->tmp2, "name"); - if (err) - return err; - char *name; - err = drgn_object_read_c_string(&kmod_it->tmp3, &name); - if (err) - return err; - free(it->name); - *name_ret = it->name = name; - return NULL; -} -/* - * Identify an ELF file as a kernel module, vmlinux, or neither. We classify a - * file as a kernel module if it has a section named .gnu.linkonce.this_module. - * If it doesn't, but it does have a section named .init.text, we classify it as - * vmlinux. - */ -static struct drgn_error *identify_kernel_elf(Elf *elf, - bool *is_vmlinux_ret, - bool *is_module_ret) -{ - size_t shstrndx; - if (elf_getshdrstrndx(elf, &shstrndx)) - return drgn_error_libelf(); - - Elf_Scn *scn = NULL; - bool have_init_text = false; - while ((scn = elf_nextscn(elf, scn))) { - GElf_Shdr *shdr, shdr_mem; - const char *scnname; - - shdr = gelf_getshdr(scn, &shdr_mem); - if (!shdr) - continue; + // Since Linux kernel commit ed66f991bb19 ("module: Refactor + // section attr into bin attribute") (in v5.8), the section name + // is module_sect_attr.battr.attr.name. Before that, it is + // simply module_sect_attr.name. - scnname = elf_strptr(elf, shstrndx, shdr->sh_name); - if (!scnname) - return drgn_error_libelf(); - if (strcmp(scnname, ".gnu.linkonce.this_module") == 0) { - *is_vmlinux_ret = false; - *is_module_ret = true; - return NULL; - } else if (strcmp(scnname, ".init.text") == 0) { - have_init_text = true; + // attr = attr.battr.attr + err = drgn_object_member(&attr, &attr, "battr"); + if (!err) { + err = drgn_object_member(&attr, &attr, "attr"); + if (err) + return err; + } else { + if (err->code != DRGN_ERROR_LOOKUP) + return err; + drgn_error_destroy(err); } + err = drgn_object_member(&tmp, &attr, "name"); + if (err) + return err; + _cleanup_free_ char *name = NULL; + err = drgn_object_read_c_string(&tmp, &name); + if (err) + return err; + + drgn_log_debug(prog, + "found section %s@0x%" PRIx64 " in sect_attrs", + name, address); + err = drgn_module_set_section_address(module, name, address); + if (err) + return err; } - *is_vmlinux_ret = have_init_text; - *is_module_ret = false; return NULL; } -DEFINE_HASH_MAP(elf_scn_name_map, const char *, Elf_Scn *, - c_string_key_hash_pair, c_string_key_eq); - static struct drgn_error * -cache_kernel_module_sections(struct kernel_module_iterator *kmod_it, Elf *elf) +kernel_module_find_or_create_internal(const struct drgn_object *module_obj, + struct drgn_module **ret, bool *new_ret, + bool create, bool log) { struct drgn_error *err; + struct drgn_program *prog = drgn_object_program(module_obj); - size_t shstrndx; - if (elf_getshdrstrndx(elf, &shstrndx)) - return drgn_error_libelf(); - - struct elf_scn_name_map scn_map = HASH_TABLE_INIT; - Elf_Scn *scn = NULL; - while ((scn = elf_nextscn(elf, scn))) { - GElf_Shdr shdr_mem; - GElf_Shdr *shdr = gelf_getshdr(scn, &shdr_mem); - if (!shdr) { - err = drgn_error_libelf(); - goto out_scn_map; - } - - if (!(shdr->sh_flags & SHF_ALLOC)) - continue; + struct drgn_module_key key; + key.kind = DRGN_MODULE_RELOCATABLE; + uint64_t name_offset; + err = drgn_type_offsetof(module_obj->type, "name", &name_offset); + if (err) + return err; + if (name_offset >= drgn_object_size(module_obj) + || !memchr(drgn_object_buffer(module_obj) + name_offset, '\0', + drgn_object_size(module_obj) - name_offset)) { + return drgn_error_create(DRGN_ERROR_OTHER, + "couldn't read module name"); + } + key.relocatable.name = drgn_object_buffer(module_obj) + name_offset; - struct elf_scn_name_map_entry entry = { - .key = elf_strptr(elf, shstrndx, shdr->sh_name), - .value = scn, - }; - if (!entry.key) { - err = drgn_error_libelf(); - goto out_scn_map; + DRGN_OBJECT(mem, prog); + DRGN_OBJECT(val, prog); + bool layout_in_module = false; + err = drgn_object_member(&mem, module_obj, "mem"); + if (!err) { + // Since Linux kernel commit ac3b43283923 ("module: replace + // module_layout with module_memory") (in v6.4), the base and + // size are in the `struct module_memory mem[MOD_TEXT]` member + // of `struct module`. + if (!prog->mod_text_cached) { + err = drgn_program_find_object(prog, "MOD_TEXT", NULL, + DRGN_FIND_OBJECT_CONSTANT, + &val); + if (err) + return err; + union drgn_value mod_text_value; + err = drgn_object_read_integer(&val, &mod_text_value); + if (err) + return err; + prog->mod_text = mod_text_value.uvalue; + prog->mod_text_cached = true; } - - if (elf_scn_name_map_insert(&scn_map, &entry, NULL) == -1) { - err = &drgn_enomem; - goto out_scn_map; + err = drgn_object_subscript(&mem, &mem, prog->mod_text); + if (err) + return err; + } else { + if (err->code != DRGN_ERROR_LOOKUP) + return err; + drgn_error_destroy(err); + // Between that and Linux kernel commit 7523e4dc5057 ("module: + // use a structure to encapsulate layout.") (in v4.5), the base + // and size are in the `struct module_layout core_layout` member + // of `struct module`. + err = drgn_object_member(&mem, module_obj, "core_layout"); + if (err) { + if (err->code != DRGN_ERROR_LOOKUP) + return err; + drgn_error_destroy(err); + // Before that, they are directly in the `struct + // module`. + layout_in_module = true; } } - - struct kernel_module_section_iterator section_it; - err = kernel_module_section_iterator_init(§ion_it, kmod_it); + if (layout_in_module) + err = drgn_object_member(&val, module_obj, "module_core"); + else + err = drgn_object_member(&val, &mem, "base"); if (err) - goto out_scn_map; - const char *name; - uint64_t address; - while (!(err = kernel_module_section_iterator_next(§ion_it, &name, - &address))) { - struct elf_scn_name_map_iterator it = - elf_scn_name_map_search(&scn_map, &name); - if (it.entry) { - GElf_Shdr shdr_mem; - GElf_Shdr *shdr = gelf_getshdr(it.entry->value, - &shdr_mem); - if (!shdr) { - err = drgn_error_libelf(); - break; - } - shdr->sh_addr = address; - if (!gelf_update_shdr(it.entry->value, shdr)) { - err = drgn_error_libelf(); - break; - } - } + return err; + err = drgn_object_read_unsigned(&val, &key.relocatable.address); + if (err) + return err; + + if (log) { + drgn_log_debug(prog, "found loaded kernel module %s@0x%" PRIx64, + key.relocatable.name, key.relocatable.address); } - if (err && err != &drgn_stop) - goto out_section_it; - err = NULL; -out_section_it: - kernel_module_section_iterator_deinit(§ion_it); -out_scn_map: - elf_scn_name_map_deinit(&scn_map); - return err; -} -struct kernel_module_file { - const char *path; - int fd; - Elf *elf; - /* - * Kernel module build ID. This is owned by the Elf handle. Because we - * use this as the key in the kernel_module_table, the file must always - * be removed from the table before it is reported to the DWARF index - * (which takes ownership of the Elf handle). - */ - const void *gnu_build_id; - size_t gnu_build_id_len; - /* Next file with the same build ID. */ - struct kernel_module_file *next; -}; + if (!create) { + *ret = drgn_module_find(prog, &key); + if (new_ret) + *new_ret = false; + return NULL; + } -static struct nstring -kernel_module_table_key(struct kernel_module_file * const *entry) -{ - return (struct nstring){ - (*entry)->gnu_build_id, (*entry)->gnu_build_id_len - }; -} + _cleanup_(drgn_module_deletep) struct drgn_module *module = NULL; + bool new; + err = drgn_module_find_or_create(prog, &key, key.relocatable.name, + &module, &new); + if (err) + return err; + if (!new) { + *ret = no_cleanup_ptr(module); + if (new_ret) + *new_ret = new; + return NULL; + } -DEFINE_HASH_TABLE(kernel_module_table, struct kernel_module_file *, - kernel_module_table_key, nstring_hash_pair, nstring_eq); + if (layout_in_module) + err = drgn_object_member(&val, module_obj, "core_size"); + else + err = drgn_object_member(&val, &mem, "size"); + if (err) + return err; + uint64_t size; + err = drgn_object_read_unsigned(&val, &size); + if (err) + return err; -static struct drgn_error * -report_loaded_kernel_module(struct drgn_debug_info_load_state *load, - struct kernel_module_iterator *kmod_it, - struct kernel_module_table *kmod_table) -{ - struct drgn_error *err; + drgn_log_debug(prog, "module size is %" PRIu64, size); + err = drgn_module_set_address_range(module, key.relocatable.address, + key.relocatable.address + size); + if (err) + return err; - struct nstring key; - err = kernel_module_iterator_gnu_build_id(kmod_it, - (const void **)&key.str, - &key.len); - if (err || key.len == 0) { - return drgn_debug_info_report_error(load, kmod_it->name, - "could not find GNU build ID", - err); + // If we're debugging the running kernel, we can use + // /sys/module/$module/notes and /sys/module/$module/sections instead of + // getting the equivalent information from the core dump. This fast path + // can be disabled via an environment variable for testing. It may also + // be disabled if we encounter permission issues using + // /sys/module/$module/sections. + bool use_sys_module = false; + if (prog->flags & DRGN_PROGRAM_IS_LOCAL) { + char *env = getenv("DRGN_USE_SYS_MODULE"); + use_sys_module = !env || atoi(env); } + err = kernel_module_set_build_id(module, module_obj, use_sys_module); + if (err) + return err; + err = kernel_module_set_section_addresses(module, module_obj, + use_sys_module); + if (err) + return err; - struct hash_pair hp = kernel_module_table_hash(&key); - struct kernel_module_table_iterator it = - kernel_module_table_search_hashed(kmod_table, &key, hp); - if (!it.entry) - return &drgn_not_found; - - struct kernel_module_file *kmod = *it.entry; - kernel_module_table_delete_iterator_hashed(kmod_table, it, hp); - do { - err = cache_kernel_module_sections(kmod_it, kmod->elf); - if (err) { - err = drgn_debug_info_report_error(load, kmod->path, - "could not get section addresses", - err); - if (err) - return err; - goto next; - } - - err = drgn_debug_info_report_elf(load, kmod->path, kmod->fd, - kmod->elf, kmod_it->start, - kmod_it->end, kmod_it->name, - NULL); - kmod->elf = NULL; - kmod->fd = -1; - if (err) - return err; -next: - kmod = kmod->next; - } while (kmod); + *ret = no_cleanup_ptr(module); + if (new_ret) + *new_ret = new; return NULL; } static struct drgn_error * -report_default_kernel_module(struct drgn_debug_info_load_state *load, - struct kernel_module_iterator *kmod_it, - struct depmod_index *depmod) +drgn_module_find_or_create_linux_kernel_loadable_internal(const struct drgn_object *module_obj, + struct drgn_module **ret, + bool *new_ret, + bool create) { - static const char * const module_paths[] = { - "/usr/lib/debug/lib/modules/%s/%.*s", - "/usr/lib/debug/lib/modules/%s/%.*s.debug", - "/lib/modules/%s/%.*s%.*s", - NULL, - }; struct drgn_error *err; - const char *depmod_path; - size_t depmod_path_len; - err = depmod_index_find(depmod, kmod_it->name, &depmod_path, - &depmod_path_len); - if (err) { - return drgn_debug_info_report_error(load, - "kernel modules", - "could not parse depmod", - err); - } else if (!depmod_path) { - return drgn_debug_info_report_error(load, kmod_it->name, - "could not find module in depmod", - NULL); + // kernel_module_find_or_create_internal() expects a `struct module` + // value. + struct drgn_object mod; + if (drgn_type_kind(drgn_underlying_type(module_obj->type)) + == DRGN_TYPE_POINTER) { + drgn_object_init(&mod, drgn_object_program(module_obj)); + err = drgn_object_dereference(&mod, module_obj); + if (!err) + err = drgn_object_read(&mod, &mod); + module_obj = &mod; + if (err) + goto out; + } else if (module_obj->kind != DRGN_OBJECT_VALUE) { + drgn_object_init(&mod, drgn_object_program(module_obj)); + err = drgn_object_read(&mod, module_obj); + module_obj = &mod; + if (err) + goto out; } - size_t extension_len; - if (depmod_path_len >= 3 && - (memcmp(depmod_path + depmod_path_len - 3, ".gz", 3) == 0 || - memcmp(depmod_path + depmod_path_len - 3, ".xz", 3) == 0)) - extension_len = 3; - else - extension_len = 0; - char *path; - int fd; - Elf *elf; - err = find_elf_file(&path, &fd, &elf, module_paths, - load->dbinfo->prog->vmcoreinfo.osrelease, - depmod_path_len - extension_len, depmod_path, - extension_len, - depmod_path + depmod_path_len - extension_len); - if (err) - return drgn_debug_info_report_error(load, NULL, NULL, err); - if (!elf) { - return drgn_debug_info_report_error(load, kmod_it->name, - "could not find .ko", - NULL); - } + err = kernel_module_find_or_create_internal(module_obj, ret, new_ret, + create, false); +out: + if (module_obj == &mod) + drgn_object_deinit(&mod); + return err; +} - err = cache_kernel_module_sections(kmod_it, elf); - if (err) { - err = drgn_debug_info_report_error(load, path, - "could not get section addresses", - err); - elf_end(elf); - close(fd); - free(path); - return err; - } +LIBDRGN_PUBLIC struct drgn_error * +drgn_module_find_linux_kernel_loadable(const struct drgn_object *module_obj, + struct drgn_module **ret) +{ + return drgn_module_find_or_create_linux_kernel_loadable_internal(module_obj, + ret, + NULL, + false); +} - err = drgn_debug_info_report_elf(load, path, fd, elf, kmod_it->start, - kmod_it->end, kmod_it->name, NULL); - free(path); - return err; +LIBDRGN_PUBLIC struct drgn_error * +drgn_module_find_or_create_linux_kernel_loadable(const struct drgn_object *module_obj, + struct drgn_module **ret, + bool *new_ret) +{ + return drgn_module_find_or_create_linux_kernel_loadable_internal(module_obj, + ret, + new_ret, + true); } static struct drgn_error * -report_loaded_kernel_modules(struct drgn_debug_info_load_state *load, - struct kernel_module_table *kmod_table, - struct depmod_index *depmod, bool use_sys_module) +yield_kernel_module(struct linux_kernel_loaded_module_iterator *it, + struct drgn_module **ret, bool *new_ret) { - struct drgn_program *prog = load->dbinfo->prog; struct drgn_error *err; + struct drgn_program *prog = it->it.prog; - struct kernel_module_iterator kmod_it; - err = kernel_module_iterator_init(&kmod_it, prog, use_sys_module); - if (err) { -kernel_module_iterator_error: - return drgn_debug_info_report_error(load, "kernel modules", - "could not find loaded kernel modules", - err); - } + DRGN_OBJECT(mod, prog); for (;;) { - err = kernel_module_iterator_next(&kmod_it); - if (err == &drgn_stop) { - err = NULL; - break; - } else if (err) { - kernel_module_iterator_deinit(&kmod_it); - goto kernel_module_iterator_error; + uint64_t addr; + err = drgn_object_read_unsigned(&it->node, &addr); + if (err) { +list_walk_err: + if (!drgn_error_is_fatal(err)) { + drgn_error_log_warning(prog, err, + "can't find remaining kernel modules: " + "couldn't read next module: "); + drgn_error_destroy(err); + *ret = NULL; + err = NULL; + } + return err; } - - /* Look for an explicitly-reported file first. */ - if (kmod_table) { - err = report_loaded_kernel_module(load, &kmod_it, - kmod_table); - if (!err) - continue; - else if (err != &drgn_not_found) - break; + if (addr == it->modules_head) { + drgn_log_debug(prog, + "found end of loaded kernel module list"); + *ret = NULL; + return NULL; } - /* - * If it was not reported explicitly and we're also reporting the - * defaults, look for the module at the standard locations unless we've - * already indexed that module. - */ - if (depmod && - !drgn_debug_info_is_indexed(load->dbinfo, kmod_it.name)) { - if (!depmod->addr) { - err = depmod_index_init(depmod, - prog->vmcoreinfo.osrelease); - if (err) { - depmod->addr = NULL; - err = drgn_debug_info_report_error(load, - "kernel modules", - "could not read depmod", - err); - if (err) - break; - depmod = NULL; - continue; - } - } - err = report_default_kernel_module(load, &kmod_it, - depmod); - if (err) - break; + if (it->module_list_iterations_remaining == 0) { + drgn_log_warning(prog, + "can't find remaining kernel modules: " + "too many entries or cycle in modules list"); + *ret = NULL; + return NULL; } - } - kernel_module_iterator_deinit(&kmod_it); - return err; -} + it->module_list_iterations_remaining--; -static struct drgn_error * -report_kernel_modules(struct drgn_debug_info_load_state *load, - struct kernel_module_file *kmods, size_t num_kmods, - bool vmlinux_is_pending) -{ - struct drgn_program *prog = load->dbinfo->prog; - struct drgn_error *err; + err = drgn_object_container_of(&mod, &it->node, it->module_type, + "list"); + if (err) + goto list_walk_err; - if (!num_kmods && !load->load_default) - return NULL; + err = drgn_object_dereference(&mod, &mod); + if (err) + goto list_walk_err; + // We need several fields from the `struct module`. Especially + // for /proc/kcore, it is faster to read the entire structure + // (which is <2kB as of Linux 6.5) from the core dump all at + // once than it is to read each field individually. + err = drgn_object_read(&mod, &mod); + if (err) + goto list_walk_err; - /* - * If we're debugging the running kernel, we can use - * /sys/module/$module/notes and /sys/module/$module/sections instead of - * getting the equivalent information from the core dump. This fast path - * can be disabled via an environment variable for testing. It may also - * be disabled if we encounter permission issues using - * /sys/module/$module/sections. - */ - bool use_sys_module = false; - if (prog->flags & DRGN_PROGRAM_IS_LOCAL) { - char *env = getenv("DRGN_USE_SYS_MODULE"); - use_sys_module = !env || atoi(env); - } - /* - * We need to index vmlinux now so that we can walk the list of modules - * in the kernel. - */ - if (vmlinux_is_pending) { - err = drgn_debug_info_report_flush(load); + err = drgn_object_member(&it->node, &mod, "list"); if (err) - return err; - } + goto list_walk_err; + err = drgn_object_member(&it->node, &it->node, "next"); + if (err) + goto list_walk_err; - struct kernel_module_table kmod_table = HASH_TABLE_INIT; - struct depmod_index depmod; - depmod.addr = NULL; - struct kernel_module_table_iterator it; - for (size_t i = 0; i < num_kmods; i++) { - struct kernel_module_file *kmod = &kmods[i]; - - ssize_t build_id_len = - drgn_elf_gnu_build_id(kmod->elf, &kmod->gnu_build_id); - if (build_id_len < 0) { - err = drgn_debug_info_report_error(load, kmod->path, - NULL, - drgn_error_libelf()); - if (err) - goto out; + err = kernel_module_find_or_create_internal(&mod, ret, new_ret, + true, true); + if (err && !drgn_error_is_fatal(err)) { + drgn_error_log_warning(prog, err, "ignoring module: "); + drgn_error_destroy(err); continue; } - kmod->gnu_build_id_len = build_id_len; - - struct nstring key = kernel_module_table_key(&kmod); - struct hash_pair hp = kernel_module_table_hash(&key); - it = kernel_module_table_search_hashed(&kmod_table, &key, hp); - if (it.entry) { - kmod->next = *it.entry; - *it.entry = kmod; - } else { - if (kernel_module_table_insert_searched(&kmod_table, - &kmod, hp, - NULL) == -1) { - err = &drgn_enomem; - goto out; - } - kmod->next = NULL; - } - } - - err = report_loaded_kernel_modules(load, num_kmods ? &kmod_table : NULL, - load->load_default ? &depmod : NULL, - use_sys_module); - if (err) - goto out; - - /* Anything left over was not loaded. */ - for (it = kernel_module_table_first(&kmod_table); it.entry; ) { - struct kernel_module_file *kmod = *it.entry; - it = kernel_module_table_delete_iterator(&kmod_table, it); - do { - err = drgn_debug_info_report_elf(load, kmod->path, - kmod->fd, kmod->elf, 0, - 0, kmod->path, NULL); - kmod->elf = NULL; - kmod->fd = -1; - if (err) - goto out; - kmod = kmod->next; - } while (kmod); - } - err = NULL; -out: - if (depmod.addr) - depmod_index_deinit(&depmod); - kernel_module_table_deinit(&kmod_table); - return err; -} - -static struct drgn_error * -report_vmlinux(struct drgn_debug_info_load_state *load, - bool *vmlinux_is_pending) -{ - static const char * const vmlinux_paths[] = { - /* - * The files under /usr/lib/debug should always have debug - * information, so check for those first. - */ - "/usr/lib/debug/boot/vmlinux-%s", - "/usr/lib/debug/lib/modules/%s/vmlinux", - "/boot/vmlinux-%s", - "/lib/modules/%s/build/vmlinux", - "/lib/modules/%s/vmlinux", - NULL, - }; - struct drgn_program *prog = load->dbinfo->prog; - struct drgn_error *err; - - char *path; - int fd; - Elf *elf; - err = find_elf_file(&path, &fd, &elf, vmlinux_paths, - prog->vmcoreinfo.osrelease); - if (err) - return drgn_debug_info_report_error(load, NULL, NULL, err); - if (!elf) { - err = drgn_error_format(DRGN_ERROR_OTHER, - "could not find vmlinux for %s", - prog->vmcoreinfo.osrelease); - return drgn_debug_info_report_error(load, "kernel", NULL, err); - } - - uint64_t start, end; - err = elf_address_range(elf, prog->vmcoreinfo.kaslr_offset, &start, - &end); - if (err) { - err = drgn_debug_info_report_error(load, path, NULL, err); - elf_end(elf); - close(fd); - free(path); return err; } - - err = drgn_debug_info_report_elf(load, path, fd, elf, start, end, - "kernel", vmlinux_is_pending); - free(path); - return err; } -struct drgn_error * -linux_kernel_report_debug_info(struct drgn_debug_info_load_state *load) +static struct drgn_error * +linux_kernel_loaded_module_iterator_next(struct drgn_module_iterator *_it, + struct drgn_module **ret, + bool *new_ret) { - struct drgn_program *prog = load->dbinfo->prog; struct drgn_error *err; + struct linux_kernel_loaded_module_iterator *it = + container_of(_it, struct linux_kernel_loaded_module_iterator, it); + struct drgn_program *prog = it->it.prog; - struct kernel_module_file *kmods; - if (load->num_paths) { - kmods = malloc_array(load->num_paths, sizeof(*kmods)); - if (!kmods) - return &drgn_enomem; - } else { - kmods = NULL; + if (!it->yielded_vmlinux) { + it->yielded_vmlinux = true; + return yield_vmlinux(it, ret, new_ret); } - /* - * We may need to index vmlinux before we can properly report kernel - * modules. So, this sets aside kernel modules and reports everything - * else. - */ - size_t num_kmods = 0; - bool vmlinux_is_pending = false; - for (size_t i = 0; i < load->num_paths; i++) { - const char *path = load->paths[i]; - int fd; - Elf *elf; - err = open_elf_file(path, &fd, &elf); - if (err) { - err = drgn_debug_info_report_error(load, path, NULL, - err); - if (err) - goto out; - continue; + // Start the module list walk if we haven't yet. + if (!it->module_type.type) { + for (int attempt = 1; attempt <= 2; attempt++) { + err = drgn_program_find_type(prog, "struct module", + NULL, &it->module_type); + if (!err) { + err = drgn_program_find_object(prog, "modules", + NULL, + DRGN_FIND_OBJECT_VARIABLE, + &it->node); + } + if (err && err->code == DRGN_ERROR_LOOKUP) { + drgn_error_destroy(err); + if (attempt == 1 && prog->dbinfo.main_module) { + struct drgn_module *module = + prog->dbinfo.main_module; + if (module->debug_file_status + == DRGN_MODULE_FILE_DONT_WANT) { + module->debug_file_status = + DRGN_MODULE_FILE_WANT; + } + if (drgn_module_wants_debug_file(module)) { + err = drgn_load_module_debug_info(&module, + &(size_t){1}); + if (err) + return err; + continue; + } + } + if (!prog->dbinfo.main_module + || drgn_module_wants_debug_file(prog->dbinfo.main_module)) { + drgn_log_warning(prog, + "can't find loaded modules without kernel debug info"); + } else { + drgn_log_debug(prog, + "kernel does not have loadable module support"); + } + *ret = NULL; + return NULL; + } else if (err) { + return err; + } } - - bool is_vmlinux, is_module; - err = identify_kernel_elf(elf, &is_vmlinux, &is_module); - if (err) { - err = drgn_debug_info_report_error(load, path, NULL, - err); - elf_end(elf); - close(fd); - if (err) - goto out; - continue; + if (it->node.kind != DRGN_OBJECT_REFERENCE) { + drgn_log_warning(prog, + "can't find kernel modules: " + "can't get address of modules list"); + *ret = NULL; + return NULL; } - if (is_module) { - struct kernel_module_file *kmod = &kmods[num_kmods++]; - kmod->path = path; - kmod->fd = fd; - kmod->elf = elf; - } else if (is_vmlinux) { - uint64_t start, end; - err = elf_address_range(elf, - prog->vmcoreinfo.kaslr_offset, - &start, &end); - if (err) { - elf_end(elf); - close(fd); - err = drgn_debug_info_report_error(load, path, - NULL, err); - if (err) - goto out; - continue; - } - - bool is_new; - err = drgn_debug_info_report_elf(load, path, fd, elf, - start, end, "kernel", - &is_new); - if (err) - goto out; - if (is_new) - vmlinux_is_pending = true; - } else { - err = drgn_debug_info_report_elf(load, path, fd, elf, 0, - 0, NULL, NULL); - if (err) - goto out; + it->modules_head = it->node.address; + err = drgn_object_member(&it->node, &it->node, "next"); + if (!err) + err = drgn_object_read(&it->node, &it->node); + if (err) { + if (drgn_error_is_fatal(err)) + return err; + drgn_error_log_warning(prog, err, + "can't find kernel modules: " + "couldn't read modules list: "); + drgn_error_destroy(err); + *ret = NULL; + return NULL; } } - if (load->load_main && !vmlinux_is_pending && - !drgn_debug_info_is_indexed(load->dbinfo, "kernel")) { - err = report_vmlinux(load, &vmlinux_is_pending); - if (err) - goto out; - } + return yield_kernel_module(it, ret, new_ret); +} - err = report_kernel_modules(load, kmods, num_kmods, vmlinux_is_pending); -out: - for (size_t i = 0; i < num_kmods; i++) { - elf_end(kmods[i].elf); - if (kmods[i].fd != -1) - close(kmods[i].fd); - } - free(kmods); - return err; +struct drgn_error * +linux_kernel_loaded_module_iterator_create(struct drgn_program *prog, + struct drgn_module_iterator **ret) +{ + struct linux_kernel_loaded_module_iterator *it = calloc(1, sizeof(*it)); + if (!it) + return &drgn_enomem; + drgn_module_iterator_init(&it->it, prog, + linux_kernel_loaded_module_iterator_destroy, + linux_kernel_loaded_module_iterator_next); + it->module_list_iterations_remaining = MAX_MODULE_LIST_ITERATIONS; + drgn_object_init(&it->node, prog); + *ret = &it->it; + return NULL; } diff --git a/libdrgn/linux_kernel.h b/libdrgn/linux_kernel.h index 78002f5aa..16e6f2866 100644 --- a/libdrgn/linux_kernel.h +++ b/libdrgn/linux_kernel.h @@ -6,7 +6,8 @@ #include "drgn_internal.h" -struct drgn_debug_info_load_state; +struct depmod_index; +struct drgn_module_standard_files_state; struct drgn_error *drgn_program_finish_set_kernel(struct drgn_program *prog); @@ -23,8 +24,19 @@ struct drgn_error *proc_kallsyms_symbol_addr(const char *name, struct drgn_error *read_vmcoreinfo_fallback(struct drgn_program *prog); +void depmod_index_deinit(struct depmod_index *depmod); + +struct drgn_error * +linux_kernel_loaded_module_iterator_create(struct drgn_program *prog, + struct drgn_module_iterator **ret); + +struct drgn_error * +drgn_module_try_vmlinux_files(struct drgn_module *module, + struct drgn_module_standard_files_state *state); + struct drgn_error * -linux_kernel_report_debug_info(struct drgn_debug_info_load_state *load); +drgn_module_try_linux_kmod_files(struct drgn_module *module, + struct drgn_module_standard_files_state *state); #define KDUMP_SIGNATURE "KDUMP " #define KDUMP_SIG_LEN (sizeof(KDUMP_SIGNATURE) - 1) diff --git a/libdrgn/orc_info.c b/libdrgn/orc_info.c index 809fd6b50..d30b4f6da 100644 --- a/libdrgn/orc_info.c +++ b/libdrgn/orc_info.c @@ -302,6 +302,10 @@ static struct drgn_error *drgn_read_orc_sections(struct drgn_module *module) return NULL; } + err = drgn_elf_file_apply_relocations(module->debug_file); + if (err) + return err; + // Since Linux kernel b9f174c811e3 ("x86/unwind/orc: Add ELF section // with ORC version identifier") (in v6.4), which was also backported to // Linux 6.3.10, vmlinux and kernel modules have a .orc_header ELF diff --git a/libdrgn/program.c b/libdrgn/program.c index 0f3e4db1b..c06c55419 100644 --- a/libdrgn/program.c +++ b/libdrgn/program.c @@ -6,7 +6,6 @@ #include #include #include -#include #include #include #include @@ -28,10 +27,12 @@ #include "language.h" #include "log.h" #include "linux_kernel.h" +#include "log.h" #include "memory_reader.h" #include "minmax.h" #include "object.h" #include "program.h" +#include "serialize.h" #include "symbol.h" #include "util.h" #include "vector.h" @@ -77,7 +78,27 @@ drgn_program_platform(struct drgn_program *prog) LIBDRGN_PUBLIC const struct drgn_language * drgn_program_language(struct drgn_program *prog) { - return prog->lang ? prog->lang : &drgn_default_language; + if (prog->lang) + return prog->lang; + if (prog->flags & DRGN_PROGRAM_IS_LINUX_KERNEL) { + prog->lang = &drgn_language_c; + return prog->lang; + } + if (!prog->tried_main_language) { + prog->tried_main_language = true; + prog->lang = drgn_debug_info_main_language(&prog->dbinfo); + if (prog->lang) { + drgn_log_debug(prog, + "set default language to %s from main()", + prog->lang->name); + return prog->lang; + } else { + drgn_log_debug(prog, + "couldn't find language of main(); defaulting to %s", + drgn_default_language.name); + } + } + return &drgn_default_language; } LIBDRGN_PUBLIC void drgn_program_set_language(struct drgn_program *prog, @@ -760,63 +781,90 @@ drgn_program_set_pid(struct drgn_program *prog, pid_t pid) return err; } -/* Set the default language from the language of "main". */ -static void drgn_program_set_language_from_main(struct drgn_program *prog) +struct drgn_error *drgn_program_cache_auxv(struct drgn_program *prog) { - struct drgn_error *err; + if (prog->auxv_cached) + return NULL; - if (prog->flags & DRGN_PROGRAM_IS_LINUX_KERNEL) - return; - const struct drgn_language *lang; - err = drgn_debug_info_main_language(&prog->dbinfo, &lang); - if (err) { - drgn_error_destroy(err); - return; + _cleanup_close_ int fd = -1; + const void *note; + size_t note_size; +#define FORMAT "/proc/%ld/auxv" + char path[sizeof(FORMAT) + - sizeof("%ld") + + max_decimal_length(long) + + 1]; + if (drgn_program_is_userspace_process(prog)) { + snprintf(path, sizeof(path), FORMAT, (long)prog->pid); +#undef FORMAT + fd = open(path, O_RDONLY); + if (fd < 0) + return drgn_error_create_os("open", errno, path); + drgn_log_debug(prog, "parsing %s", path); + } else { + assert(drgn_program_is_userspace_core(prog)); + if (find_elf_note(prog->core, "CORE", NT_AUXV, ¬e, + ¬e_size)) + return drgn_error_libelf(); + if (!note) { + return drgn_error_create(DRGN_ERROR_OTHER, + "core file is missing NT_AUXV"); + } + drgn_log_debug(prog, "parsing NT_AUXV"); } - if (lang) - prog->lang = lang; -} -static int drgn_set_platform_from_dwarf(Dwfl_Module *module, void **userdatap, - const char *name, Dwarf_Addr base, - Dwarf *dwarf, Dwarf_Addr bias, - void *arg) -{ - Elf *elf; - GElf_Ehdr ehdr_mem, *ehdr; - struct drgn_platform platform; + memset(&prog->auxv, 0, sizeof(prog->auxv)); - elf = dwarf_getelf(dwarf); - if (!elf) - return DWARF_CB_OK; - ehdr = gelf_getehdr(elf, &ehdr_mem); - if (!ehdr) - return DWARF_CB_OK; - drgn_platform_from_elf(ehdr, &platform); - drgn_program_set_platform(arg, &platform); - return DWARF_CB_ABORT; -} - -LIBDRGN_PUBLIC struct drgn_error * -drgn_program_load_debug_info(struct drgn_program *prog, const char **paths, - size_t n, bool load_default, bool load_main) -{ - struct drgn_error *err; - - if (!n && !load_default && !load_main) - return NULL; - - drgn_blocking_guard(prog); - err = drgn_debug_info_load(&prog->dbinfo, paths, n, load_default, load_main); - if ((!err || err->code == DRGN_ERROR_MISSING_DEBUG_INFO)) { - if (!prog->lang) - drgn_program_set_language_from_main(prog); - if (!prog->has_platform) { - dwfl_getdwarf(prog->dbinfo.dwfl, - drgn_set_platform_from_dwarf, prog, 0); + bool is_64_bit = drgn_platform_is_64_bit(&prog->platform); + bool bswap = drgn_platform_bswap(&prog->platform); + size_t aux_size = is_64_bit ? 16 : 8; +#define visit_aux_members(visit_scalar_member, visit_raw_member) do { \ + visit_scalar_member(a_type); \ + visit_scalar_member(a_un.a_val); \ +} while (0) + for (;;) { + Elf64_auxv_t auxv; + if (fd >= 0) { + ssize_t r = read_all(fd, &auxv, aux_size); + if (r < 0) + return drgn_error_create_os("read", errno, path); + if (r < aux_size) + break; + deserialize_struct64_inplace(&auxv, Elf32_auxv_t, + visit_aux_members, + is_64_bit, bswap); + } else { + if (note_size < aux_size) + break; + deserialize_struct64(&auxv, Elf32_auxv_t, + visit_aux_members, note, is_64_bit, + bswap); + note = (char *)note + aux_size; + note_size -= aux_size; + } + if (auxv.a_type == 0 && auxv.a_un.a_val == 0) + break; + switch (auxv.a_type) { + case AT_PHDR: + drgn_log_debug(prog, "found AT_PHDR 0x%" PRIx64, + auxv.a_un.a_val); + prog->auxv.at_phdr = auxv.a_un.a_val; + break; + case AT_PHNUM: + drgn_log_debug(prog, "found AT_PHNUM %" PRIu64, + auxv.a_un.a_val); + prog->auxv.at_phnum = auxv.a_un.a_val; + break; + case AT_SYSINFO_EHDR: + drgn_log_debug(prog, "found AT_SYSINFO_EHDR 0x%" PRIx64, + auxv.a_un.a_val); + prog->auxv.at_sysinfo_ehdr = auxv.a_un.a_val; + break; } } - return err; +#undef visit_aux_members + prog->auxv_cached = true; + return NULL; } static struct drgn_error *get_prstatus_pid(struct drgn_program *prog, const char *data, diff --git a/libdrgn/program.h b/libdrgn/program.h index 3f89baf4c..55c335104 100644 --- a/libdrgn/program.h +++ b/libdrgn/program.h @@ -30,7 +30,9 @@ #include "vector.h" struct drgn_object_finder; +struct drgn_symbol; struct drgn_symbol_finder; +struct drgn_type_finder; /** * @defgroup Internals Internals @@ -118,6 +120,11 @@ struct drgn_program { /* Default language of the program. */ const struct drgn_language *lang; struct drgn_platform platform; + /** + * Whether we have tried determining the default language from "main" + * since the last time that debug info was added. + */ + bool tried_main_language; bool has_platform; enum drgn_program_flags flags; @@ -147,6 +154,13 @@ struct drgn_program { struct { /** Cached `pr_fname` from `NT_PRPSINFO` note. */ const char *core_dump_fname_cached; + /** Cache of important parts of auxiliary vector. */ + struct { + uint64_t at_phdr; + uint64_t at_phnum; + uint64_t at_sysinfo_ehdr; + } auxv; + bool auxv_cached; }; /* @@ -293,6 +307,8 @@ struct drgn_error *drgn_program_init_kernel(struct drgn_program *prog); */ struct drgn_error *drgn_program_init_pid(struct drgn_program *prog, pid_t pid); +struct drgn_error *drgn_program_cache_auxv(struct drgn_program *prog); + /** * Return whether a @ref drgn_program is a userspace process running on the * local machine. diff --git a/libdrgn/python/drgnpy.h b/libdrgn/python/drgnpy.h index 97ace8b76..41a2ba6d3 100644 --- a/libdrgn/python/drgnpy.h +++ b/libdrgn/python/drgnpy.h @@ -45,6 +45,10 @@ static inline PyObject *PyObject_CallNoArgs(PyObject *func) { return PyObject_CallFunctionObjArgs(func, NULL); } +static inline PyObject *PyObject_CallOneArg(PyObject *callable, PyObject *arg) +{ + return PyObject_CallFunctionObjArgs(callable, arg, NULL); +} #endif #if PY_VERSION_HEX < 0x030d00a1 @@ -136,6 +140,26 @@ typedef struct { const struct drgn_language *language; } Language; +typedef struct { + PyObject_HEAD + struct drgn_module *module; +} Module; + +typedef struct { + PyObject_HEAD + struct drgn_module_iterator *it; +} ModuleIterator; + +typedef struct { + PyObject_HEAD + struct drgn_module *module; +} ModuleSectionAddresses; + +typedef struct { + PyObject_HEAD + struct drgn_module_section_address_iterator *it; +} ModuleSectionAddressesIterator; + typedef struct { PyObject_HEAD DrgnObject *obj; @@ -241,33 +265,45 @@ typedef struct { extern PyObject *Architecture_class; extern PyObject *FindObjectFlags_class; +extern PyObject *ModuleFileStatus_class; +extern PyObject *ModuleSectionAddresses_class; extern PyObject *PlatformFlags_class; extern PyObject *PrimitiveType_class; extern PyObject *ProgramFlags_class; extern PyObject *Qualifiers_class; +extern PyObject *SupplementaryFileKind_class; extern PyObject *SymbolBinding_class; extern PyObject *SymbolKind_class; extern PyObject *TypeKind_class; extern PyTypeObject DrgnObject_type; extern PyTypeObject DrgnType_type; +extern PyTypeObject ExtraModule_type; extern PyTypeObject FaultError_type; extern PyTypeObject Language_type; +extern PyTypeObject MainModule_type; +extern PyTypeObject ModuleIteratorWithNew_type; +extern PyTypeObject ModuleIterator_type; +extern PyTypeObject ModuleSectionAddressesIterator_type; +extern PyTypeObject Module_type; extern PyTypeObject ObjectIterator_type; extern PyTypeObject Platform_type; extern PyTypeObject Program_type; extern PyTypeObject Register_type; +extern PyTypeObject RelocatableModule_type; +extern PyTypeObject SharedLibraryModule_type; extern PyTypeObject StackFrame_type; extern PyTypeObject StackTrace_type; -extern PyTypeObject Symbol_type; extern PyTypeObject SymbolIndex_type; -extern PyTypeObject Thread_type; +extern PyTypeObject Symbol_type; extern PyTypeObject ThreadIterator_type; +extern PyTypeObject Thread_type; extern PyTypeObject TypeEnumerator_type; -extern PyTypeObject TypeKindSet_type; extern PyTypeObject TypeKindSetIterator_type; +extern PyTypeObject TypeKindSet_type; extern PyTypeObject TypeMember_type; extern PyTypeObject TypeParameter_type; extern PyTypeObject TypeTemplateParameter_type; +extern PyTypeObject VdsoModule_type; extern PyObject *MissingDebugInfoError; extern PyObject *ObjectAbsentError; extern PyObject *OutOfBoundsError; @@ -284,6 +320,11 @@ void *set_error_type_name(const char *format, #define call_tp_alloc(type) ((type *)type##_type.tp_alloc(&type##_type, 0)) +PyObject *Module_wrap(struct drgn_module *module); +PyObject *Module_and_bool_wrap(struct drgn_module *module, bool b); +int add_WantedSupplementaryFile(PyObject *m); +int init_module_section_addresses(void); + PyObject *Language_wrap(const struct drgn_language *language); int language_converter(PyObject *o, void *p); int add_languages(void); @@ -348,7 +389,9 @@ DrgnType *Program_array_type(Program *self, PyObject *args, PyObject *kwds); DrgnType *Program_function_type(Program *self, PyObject *args, PyObject *kwds); int append_string(PyObject *parts, const char *s); +int append_u64_hex(PyObject *parts, uint64_t value); int append_format(PyObject *parts, const char *format, ...); +int append_attr_repr(PyObject *parts, PyObject *obj, const char *attr_name); PyObject *join_strings(PyObject *parts); // Implementation of _repr_pretty_() for IPython/Jupyter that just calls str(). PyObject *repr_pretty_from_str(PyObject *self, PyObject *args, PyObject *kwds); diff --git a/libdrgn/python/main.c b/libdrgn/python/main.c index c103f0dec..34a0aa037 100644 --- a/libdrgn/python/main.c +++ b/libdrgn/python/main.c @@ -293,6 +293,17 @@ DRGNPY_PUBLIC PyMODINIT_FUNC PyInit__drgn(void) if (add_module_constants(m) || add_type(m, &Language_type) || add_languages() || add_type(m, &DrgnObject_type) || + add_type(m, &Module_type) || + add_type(m, &MainModule_type) || + add_type(m, &SharedLibraryModule_type) || + add_type(m, &VdsoModule_type) || + add_type(m, &RelocatableModule_type) || + add_type(m, &ExtraModule_type) || + PyType_Ready(&ModuleIterator_type) || + PyType_Ready(&ModuleIteratorWithNew_type) || + add_WantedSupplementaryFile(m) || + init_module_section_addresses() || + PyType_Ready(&ModuleSectionAddressesIterator_type) || PyType_Ready(&ObjectIterator_type) || add_type(m, &Platform_type) || add_type(m, &Program_type) || diff --git a/libdrgn/python/module.c b/libdrgn/python/module.c new file mode 100644 index 000000000..06067f845 --- /dev/null +++ b/libdrgn/python/module.c @@ -0,0 +1,593 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "drgnpy.h" +#include "../util.h" + +static PyObject *WantedSupplementaryFile_class; + +int add_WantedSupplementaryFile(PyObject *m) +{ + _cleanup_pydecref_ PyObject *collections = + PyImport_ImportModule("collections"); + _cleanup_pydecref_ PyObject *namedtuple = + PyObject_GetAttrString(collections, "namedtuple"); + if (!namedtuple) + return -1; + WantedSupplementaryFile_class = + PyObject_CallFunction(namedtuple, "s[ssss]", + "WantedSupplementaryFile", "kind", "path", + "supplementary_path", "checksum"); + if (!WantedSupplementaryFile_class) + return -1; + Py_INCREF(WantedSupplementaryFile_class); + if (PyModule_AddObject(m, "WantedSupplementaryFile", + WantedSupplementaryFile_class) == -1) { + Py_DECREF(WantedSupplementaryFile_class); + Py_DECREF(WantedSupplementaryFile_class); + return -1; + } + return 0; +} + +PyObject *Module_wrap(struct drgn_module *module) +{ + PyTypeObject *type; + SWITCH_ENUM(drgn_module_kind(module)) { + case DRGN_MODULE_MAIN: + type = &MainModule_type; + break; + case DRGN_MODULE_SHARED_LIBRARY: + type = &SharedLibraryModule_type; + break; + case DRGN_MODULE_VDSO: + type = &VdsoModule_type; + break; + case DRGN_MODULE_RELOCATABLE: + type = &RelocatableModule_type; + break; + case DRGN_MODULE_EXTRA: + type = &ExtraModule_type; + break; + default: + UNREACHABLE(); + } + Module *ret = (Module *)type->tp_alloc(type, 0); + if (ret) { + struct drgn_program *prog = drgn_module_program(module); + Py_INCREF(container_of(prog, Program, prog)); + ret->module = module; + } + return (PyObject *)ret; +} + +PyObject *Module_and_bool_wrap(struct drgn_module *module, bool b) +{ + return Py_BuildValue("NO", Module_wrap(module), b ? Py_True : Py_False); +} + +static void Module_dealloc(Module *self) +{ + if (self->module) { + struct drgn_program *prog = drgn_module_program(self->module); + Py_DECREF(container_of(prog, Program, prog)); + } + Py_TYPE(self)->tp_free((PyObject *)self); +} + +static int append_module_repr_common(PyObject *parts, Module *self, + const char *method_name) +{ + if (append_format(parts, "prog.%s_module(name=", method_name) < 0 || + append_attr_repr(parts, (PyObject *)self, "name") < 0) + return -1; + return 0; +} + +static PyObject *Module_repr(Module *self) +{ + struct drgn_module_key key = drgn_module_key(self->module); + + _cleanup_pydecref_ PyObject *parts = PyList_New(0); + if (!parts) + return NULL; + + SWITCH_ENUM(key.kind) { + case DRGN_MODULE_MAIN: + if (append_module_repr_common(parts, self, "main") < 0) + return NULL; + break; + case DRGN_MODULE_SHARED_LIBRARY: + if (append_module_repr_common(parts, self, + "shared_library") + || append_string(parts, ", dynamic_address=") + || append_u64_hex(parts, + key.shared_library.dynamic_address)) + return NULL; + break; + case DRGN_MODULE_VDSO: + if (append_module_repr_common(parts, self, "vdso") + || append_string(parts, ", dynamic_address=") + || append_u64_hex(parts, key.vdso.dynamic_address)) + return NULL; + break; + case DRGN_MODULE_RELOCATABLE: + if (append_module_repr_common(parts, self, "relocatable") + || append_string(parts, ", address=") + || append_u64_hex(parts, key.relocatable.address)) + return NULL; + break; + case DRGN_MODULE_EXTRA: + if (append_module_repr_common(parts, self, "extra") + || append_string(parts, ", id=") + || append_u64_hex(parts, key.extra.id)) + return NULL; + break; + default: + UNREACHABLE(); + } + if (append_string(parts, ")")) + return NULL; + return join_strings(parts); +} + +static PyObject *Module_richcompare(Module *self, PyObject *other, int op) +{ + if ((op != Py_EQ && op != Py_NE) || + !PyObject_TypeCheck(other, &Module_type)) + Py_RETURN_NOTIMPLEMENTED; + int ret = self->module == ((Module *)other)->module; + if (op == Py_NE) + ret = !ret; + Py_RETURN_BOOL(ret); +} + +static Py_hash_t Module_hash(Module *self) +{ + return _Py_HashPointer(self->module); +} + +static PyObject *Module_wanted_supplementary_debug_file(Module *self) +{ + const char *debug_file_path, *supplementary_path; + const void *checksum; + size_t checksum_len; + enum drgn_supplementary_file_kind kind = + drgn_module_wanted_supplementary_debug_file(self->module, + &debug_file_path, + &supplementary_path, + &checksum, + &checksum_len); + if (kind == DRGN_SUPPLEMENTARY_FILE_NONE) { + return PyErr_Format(PyExc_ValueError, + "module does not want supplementary debug file"); + } + return PyObject_CallFunction(WantedSupplementaryFile_class, + "NO&O&y#", + PyObject_CallFunction(SupplementaryFileKind_class, + "k", + (unsigned long)kind), + PyUnicode_DecodeFSDefault, debug_file_path, + PyUnicode_DecodeFSDefault, + supplementary_path, checksum, + (Py_ssize_t)checksum_len); +} + +static PyObject *Module_try_file(Module *self, PyObject *args, PyObject *kwds) +{ + struct drgn_error *err; + static char *keywords[] = { "path", "fd", "force", NULL }; + struct path_arg path = {}; + int fd = -1; + int force = 0; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&|$ip:try_file", keywords, + path_converter, &path, &fd, &force)) + return NULL; + err = drgn_module_try_file(self->module, path.path, fd, force); + path_cleanup(&path); + if (err) + return set_drgn_error(err); + Py_RETURN_NONE; +} + +static Program *Module_get_prog(Module *self, void *arg) +{ + Program *prog = + container_of(drgn_module_program(self->module), Program, prog); + Py_INCREF(prog); + return prog; +} + +static PyObject *Module_get_name(Module *self, void *arg) +{ + return PyUnicode_DecodeFSDefault(drgn_module_name(self->module)); +} + +static PyObject *Module_get_address_range(Module *self, void *arg) +{ + uint64_t start, end; + if (!drgn_module_address_range(self->module, &start, &end)) + Py_RETURN_NONE; + return Py_BuildValue("KK", (unsigned long long)start, + (unsigned long long)end); +} + +static int Module_set_address_range(Module *self, PyObject *value, void *arg) +{ + struct drgn_error *err; + if (value == Py_None) { + err = drgn_module_set_address_range(self->module, -1, -1); + } else { + if (!PyTuple_Check(value) || PyTuple_GET_SIZE(value) != 2) { + PyErr_SetString(PyExc_TypeError, + "address_range must be (int, int) or None"); + return -1; + } + _cleanup_pydecref_ PyObject *start_obj = + PyNumber_Index(PyTuple_GET_ITEM(value, 0)); + if (!start_obj) + return -1; + _cleanup_pydecref_ PyObject *end_obj = + PyNumber_Index(PyTuple_GET_ITEM(value, 1)); + if (!end_obj) + return -1; + uint64_t start = PyLong_AsUint64(start_obj); + uint64_t end = PyLong_AsUint64(end_obj); + if (start == UINT64_MAX && end == UINT64_MAX) { + PyErr_SetString(PyExc_ValueError, + "invalid module address range"); + return -1; + } + err = drgn_module_set_address_range(self->module, start, end); + } + if (err) { + set_drgn_error(err); + return -1; + } + return 0; +} + +static PyObject *Module_get_build_id(Module *self, void *arg) +{ + const void *build_id; + size_t build_id_len; + if (!drgn_module_build_id(self->module, &build_id, &build_id_len)) + Py_RETURN_NONE; + return PyBytes_FromStringAndSize(build_id, build_id_len); +} + +static int Module_set_build_id(Module *self, PyObject *value, void *arg) +{ + struct drgn_error *err; + if (value == Py_None) { + err = drgn_module_set_build_id(self->module, NULL, 0); + } else { + Py_buffer buffer; + int ret = PyObject_GetBuffer(value, &buffer, PyBUF_SIMPLE); + if (ret) + return ret; + + if (buffer.len == 0) { + PyErr_SetString(PyExc_ValueError, + "build ID cannot be empty"); + PyBuffer_Release(&buffer); + return -1; + } + + err = drgn_module_set_build_id(self->module, buffer.buf, + buffer.len); + PyBuffer_Release(&buffer); + } + if (err) { + set_drgn_error(err); + return -1; + } + return 0; +} + +#define MODULE_FILE_STATUS_GETSET(which) \ +static PyObject *Module_wants_##which##_file(Module *self) \ +{ \ + Py_RETURN_BOOL(drgn_module_wants_##which##_file(self->module)); \ +} \ + \ +static PyObject *Module_get_##which##_file_status(Module *self, void *arg) \ +{ \ + return PyObject_CallFunction(ModuleFileStatus_class, "i", \ + (int)drgn_module_##which##_file_status(self->module));\ +} \ + \ +static int Module_set_##which##_file_status(Module *self, PyObject *value, \ + void *arg) \ +{ \ + if (!PyObject_TypeCheck(value, \ + (PyTypeObject *)ModuleFileStatus_class)) { \ + PyErr_SetString(PyExc_TypeError, \ + #which "_file_status must be ModuleFileStatus");\ + return -1; \ + } \ + _cleanup_pydecref_ PyObject *value_obj = \ + PyObject_GetAttrString(value, "value"); \ + if (!value_obj) \ + return -1; \ + long status = PyLong_AsLong(value_obj); \ + if (status == -1 && PyErr_Occurred()) \ + return -1; \ + \ + if (drgn_module_set_##which##_file_status(self->module, status)) \ + return 0; \ + \ + _cleanup_pydecref_ PyObject *old_status = \ + Module_get_##which##_file_status(self, NULL); \ + if (!old_status) \ + return -1; \ + PyErr_Format(PyExc_ValueError, \ + "cannot change " #which "_file_status from %S to %S", \ + old_status, value); \ + return -1; \ +} +MODULE_FILE_STATUS_GETSET(loaded) +MODULE_FILE_STATUS_GETSET(debug) + +static PyObject *Module_get_loaded_file_path(Module *self, void *arg) +{ + const char *path = drgn_module_loaded_file_path(self->module); + if (!path) + Py_RETURN_NONE; + return PyUnicode_DecodeFSDefault(path); +} + +static PyObject *Module_get_loaded_file_bias(Module *self, void *arg) +{ + if (!drgn_module_loaded_file_path(self->module)) + Py_RETURN_NONE; + return PyLong_FromUint64(drgn_module_loaded_file_bias(self->module)); +} + +static PyObject *Module_get_debug_file_path(Module *self, void *arg) +{ + const char *path = drgn_module_debug_file_path(self->module); + if (!path) + Py_RETURN_NONE; + return PyUnicode_DecodeFSDefault(path); +} + +static PyObject *Module_get_debug_file_bias(Module *self, void *arg) +{ + if (!drgn_module_debug_file_path(self->module)) + Py_RETURN_NONE; + return PyLong_FromUint64(drgn_module_debug_file_bias(self->module)); +} + +static PyObject *Module_get_supplementary_debug_file_kind(Module *self, + void *arg) +{ + enum drgn_supplementary_file_kind kind = + drgn_module_supplementary_debug_file_kind(self->module); + if (kind == DRGN_SUPPLEMENTARY_FILE_NONE) + Py_RETURN_NONE; + return PyObject_CallFunction(SupplementaryFileKind_class, "k", + (unsigned long)kind); +} + +static PyObject *Module_get_supplementary_debug_file_path(Module *self, + void *arg) +{ + const char *path = + drgn_module_supplementary_debug_file_path(self->module); + if (!path) + Py_RETURN_NONE; + return PyUnicode_DecodeFSDefault(path); +} + +static PyMethodDef Module_methods[] = { + {"wants_loaded_file", (PyCFunction)Module_wants_loaded_file, + METH_NOARGS, drgn_Module_wants_loaded_file_DOC}, + {"wants_debug_file", (PyCFunction)Module_wants_debug_file, METH_NOARGS, + drgn_Module_wants_debug_file_DOC}, + {"wanted_supplementary_debug_file", + (PyCFunction)Module_wanted_supplementary_debug_file, METH_NOARGS, + drgn_Module_wanted_supplementary_debug_file_DOC}, + {"try_file", (PyCFunction)Module_try_file, + METH_VARARGS | METH_KEYWORDS, drgn_Module_try_file_DOC}, + {}, +}; + +static PyGetSetDef Module_getset[] = { + {"prog", (getter)Module_get_prog, NULL, drgn_Module_prog_DOC}, + {"name", (getter)Module_get_name, NULL, drgn_Module_name_DOC}, + {"address_range", (getter)Module_get_address_range, + (setter)Module_set_address_range, drgn_Module_address_range_DOC}, + {"build_id", (getter)Module_get_build_id, (setter)Module_set_build_id, + drgn_Module_build_id_DOC}, + {"loaded_file_status", (getter)Module_get_loaded_file_status, + (setter)Module_set_loaded_file_status, + drgn_Module_loaded_file_status_DOC}, + {"loaded_file_path", (getter)Module_get_loaded_file_path, NULL, + drgn_Module_loaded_file_path_DOC}, + {"loaded_file_bias", (getter)Module_get_loaded_file_bias, NULL, + drgn_Module_loaded_file_bias_DOC}, + {"debug_file_status", (getter)Module_get_debug_file_status, + (setter)Module_set_debug_file_status, + drgn_Module_debug_file_status_DOC}, + {"debug_file_path", (getter)Module_get_debug_file_path, NULL, + drgn_Module_debug_file_path_DOC}, + {"debug_file_bias", (getter)Module_get_debug_file_bias, NULL, + drgn_Module_debug_file_bias_DOC}, + {"supplementary_debug_file_kind", + (getter)Module_get_supplementary_debug_file_kind, NULL, + drgn_Module_supplementary_debug_file_kind_DOC}, + {"supplementary_debug_file_path", + (getter)Module_get_supplementary_debug_file_path, NULL, + drgn_Module_supplementary_debug_file_path_DOC}, + {}, +}; + +PyTypeObject Module_type = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "_drgn.Module", + .tp_basicsize = sizeof(Module), + .tp_dealloc = (destructor)Module_dealloc, + .tp_repr = (reprfunc)Module_repr, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, + .tp_doc = drgn_Module_DOC, + .tp_richcompare = (richcmpfunc)Module_richcompare, + .tp_hash = (hashfunc)Module_hash, + .tp_methods = Module_methods, + .tp_getset = Module_getset, +}; + +PyTypeObject MainModule_type = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "_drgn.MainModule", + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_doc = drgn_MainModule_DOC, + .tp_base = &Module_type, +}; + +static PyObject *SharedLibraryModule_get_dynamic_address(Module *self, void *arg) +{ + struct drgn_module_key key = drgn_module_key(self->module); + return PyLong_FromUint64(key.shared_library.dynamic_address); +} + +static PyGetSetDef SharedLibraryModule_getset[] = { + {"dynamic_address", (getter)SharedLibraryModule_get_dynamic_address, + NULL, drgn_SharedLibraryModule_dynamic_address_DOC}, + {}, +}; + +PyTypeObject SharedLibraryModule_type = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "_drgn.SharedLibraryModule", + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_doc = drgn_SharedLibraryModule_DOC, + .tp_getset = SharedLibraryModule_getset, + .tp_base = &Module_type, +}; + +static PyObject *VdsoModule_get_dynamic_address(Module *self, void *arg) +{ + struct drgn_module_key key = drgn_module_key(self->module); + return PyLong_FromUint64(key.vdso.dynamic_address); +} + +static PyGetSetDef VdsoModule_getset[] = { + {"dynamic_address", (getter)VdsoModule_get_dynamic_address, NULL, + drgn_VdsoModule_dynamic_address_DOC}, + {}, +}; + +PyTypeObject VdsoModule_type = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "_drgn.VdsoModule", + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_doc = drgn_VdsoModule_DOC, + .tp_getset = VdsoModule_getset, + .tp_base = &Module_type, +}; + +static PyObject *RelocatableModule_get_address(Module *self, void *arg) +{ + struct drgn_module_key key = drgn_module_key(self->module); + return PyLong_FromUint64(key.relocatable.address); +} + +static PyObject *RelocatableModule_get_section_addresses(PyObject *self, + void *arg) +{ + return PyObject_CallOneArg(ModuleSectionAddresses_class, self); +} + +static PyGetSetDef RelocatableModule_getset[] = { + {"address", (getter)RelocatableModule_get_address, NULL, + drgn_RelocatableModule_address_DOC}, + {"section_addresses", RelocatableModule_get_section_addresses, + NULL, drgn_RelocatableModule_section_addresses_DOC}, + {}, +}; + +PyTypeObject RelocatableModule_type = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "_drgn.RelocatableModule", + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_doc = drgn_RelocatableModule_DOC, + .tp_getset = RelocatableModule_getset, + .tp_base = &Module_type, +}; + +static PyObject *ExtraModule_get_id(Module *self, void *arg) +{ + struct drgn_module_key key = drgn_module_key(self->module); + return PyLong_FromUint64(key.extra.id); +} + +static PyGetSetDef ExtraModule_getset[] = { + {"id", (getter)ExtraModule_get_id, NULL, drgn_ExtraModule_id_DOC}, + {}, +}; + +PyTypeObject ExtraModule_type = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "_drgn.ExtraModule", + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_doc = drgn_ExtraModule_DOC, + .tp_getset = ExtraModule_getset, + .tp_base = &Module_type, +}; + +static void ModuleIterator_dealloc(ModuleIterator *self) +{ + if (self->it) { + struct drgn_program *prog = + drgn_module_iterator_program(self->it); + Py_DECREF(container_of(prog, Program, prog)); + drgn_module_iterator_destroy(self->it); + } + Py_TYPE(self)->tp_free((PyObject *)self); +} + +static PyObject *ModuleIterator_next(ModuleIterator *self) +{ + struct drgn_error *err; + struct drgn_module *module; + err = drgn_module_iterator_next(self->it, &module, NULL); + if (err) + return set_drgn_error(err); + if (!module) + return NULL; + return Module_wrap(module); +} + +static PyObject *ModuleIteratorWithNew_next(ModuleIterator *self) +{ + struct drgn_error *err; + struct drgn_module *module; + bool new; + err = drgn_module_iterator_next(self->it, &module, &new); + if (err) + return set_drgn_error(err); + if (!module) + return NULL; + return Module_and_bool_wrap(module, new); +} + +PyTypeObject ModuleIterator_type = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "_drgn._ModuleIterator", + .tp_basicsize = sizeof(ModuleIterator), + .tp_dealloc = (destructor)ModuleIterator_dealloc, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_iter = PyObject_SelfIter, + .tp_iternext = (iternextfunc)ModuleIterator_next, +}; + +PyTypeObject ModuleIteratorWithNew_type = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "_drgn._ModuleIteratorWithNew", + .tp_basicsize = sizeof(ModuleIterator), + .tp_dealloc = (destructor)ModuleIterator_dealloc, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_iter = PyObject_SelfIter, + .tp_iternext = (iternextfunc)ModuleIteratorWithNew_next, +}; diff --git a/libdrgn/python/module_section_addresses.c b/libdrgn/python/module_section_addresses.c new file mode 100644 index 000000000..76d75fc6d --- /dev/null +++ b/libdrgn/python/module_section_addresses.c @@ -0,0 +1,260 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "drgnpy.h" +#include "../cleanup.h" +#include "../util.h" + +PyObject *ModuleSectionAddresses_class; + +static ModuleSectionAddresses *ModuleSectionAddresses_new(PyTypeObject *subtype, + PyObject *args, + PyObject *kwds) +{ + static char *keywords[] = {"module", NULL}; + Module *module; + if (!PyArg_ParseTupleAndKeywords(args, kwds, + "O!:_ModuleSectionAddresses", keywords, + &Module_type, &module)) + return NULL; + ModuleSectionAddresses *ret = + (ModuleSectionAddresses *)subtype->tp_alloc(subtype, 0); + if (ret) { + struct drgn_program *prog = drgn_module_program(module->module); + Py_INCREF(container_of(prog, Program, prog)); + ret->module = module->module; + } + return ret; +} + +static void ModuleSectionAddresses_dealloc(ModuleSectionAddresses *self) +{ + if (self->module) { + struct drgn_program *prog = drgn_module_program(self->module); + Py_DECREF(container_of(prog, Program, prog)); + } + Py_TYPE(self)->tp_free((PyObject *)self); +} + +static inline void +drgn_module_section_address_iterator_destroyp(struct drgn_module_section_address_iterator **itp) +{ + drgn_module_section_address_iterator_destroy(*itp); +} + +static PyObject *ModuleSectionAddresses_repr(ModuleSectionAddresses *self) +{ + struct drgn_error *err; + + _cleanup_(drgn_module_section_address_iterator_destroyp) + struct drgn_module_section_address_iterator *it = NULL; + err = drgn_module_section_address_iterator_create(self->module, &it); + if (err) + return set_drgn_error(err); + + _cleanup_pydecref_ PyObject *parts = PyList_New(0); + if (!parts) + return NULL; + if (append_string(parts, "ModuleSectionAddresses(")) + return NULL; + bool first = true; + for (;;) { + const char *name; + uint64_t address; + err = drgn_module_section_address_iterator_next(it, &name, + &address); + if (err) + return set_drgn_error(err); + if (!name) + break; + + _cleanup_pydecref_ PyObject *name_obj = + PyUnicode_FromString(name); + if (!name_obj) + return NULL; + if (append_format(parts, "%s%R: ", first ? "{" : ", ", name_obj) + || append_u64_hex(parts, address)) + return NULL; + first = false; + } + if (append_string(parts, first ? ")" : "})")) + return NULL; + return join_strings(parts); +} + +static Py_ssize_t ModuleSectionAddresses_length(ModuleSectionAddresses *self) +{ + size_t ret; + struct drgn_error *err = + drgn_module_num_section_addresses(self->module, &ret); + if (err) { + set_drgn_error(err); + return -1; + } + return ret; +} + +static PyObject *ModuleSectionAddresses_subscript(ModuleSectionAddresses *self, + PyObject *key) +{ + if (!PyUnicode_Check(key)) { + PyErr_SetObject(PyExc_KeyError, key); + return NULL; + } + const char *name = PyUnicode_AsUTF8(key); + if (!name) + return NULL; + uint64_t address; + struct drgn_error *err = drgn_module_get_section_address(self->module, + name, + &address); + if (err && err->code == DRGN_ERROR_LOOKUP) { + drgn_error_destroy(err); + PyErr_SetObject(PyExc_KeyError, key); + return NULL; + } else if (err) { + return set_drgn_error(err); + } + return PyLong_FromUint64(address); +} + +static int ModuleSectionAddresses_ass_subscript(ModuleSectionAddresses *self, + PyObject *key, + PyObject *value) +{ + struct drgn_error *err; + if (value) { + if (!PyUnicode_Check(key)) { + PyErr_SetString(PyExc_TypeError, + "section_addresses key must be str"); + return -1; + } + const char *name = PyUnicode_AsUTF8(key); + if (!name) + return -1; + uint64_t address = PyLong_AsUint64(value); + if (address == (uint64_t)-1 && PyErr_Occurred()) + return -1; + err = drgn_module_set_section_address(self->module, name, + address); + } else { + if (!PyUnicode_Check(key)) { + PyErr_SetObject(PyExc_KeyError, key); + return -1; + } + const char *name = PyUnicode_AsUTF8(key); + if (!name) + return -1; + err = drgn_module_delete_section_address(self->module, name); + if (err && err->code == DRGN_ERROR_LOOKUP) { + drgn_error_destroy(err); + PyErr_SetObject(PyExc_KeyError, key); + return -1; + } + } + if (err) { + set_drgn_error(err); + return -1; + } + return 0; +} + +static ModuleSectionAddressesIterator * +ModuleSectionAddresses_iter(ModuleSectionAddresses *self) +{ + struct drgn_error *err; + _cleanup_pydecref_ ModuleSectionAddressesIterator *it = + call_tp_alloc(ModuleSectionAddressesIterator); + if (!it) + return NULL; + err = drgn_module_section_address_iterator_create(self->module, + &it->it); + if (err) + return set_drgn_error(err); + struct drgn_program *prog = drgn_module_program(self->module); + Py_INCREF(container_of(prog, Program, prog)); + return_ptr(it); +} + +// We only define the bare minimum for collections.abc.MutableMapping, +// which gives us naive implementations of the remaining methods. We can +// define performance-sensitive ones as needed. +static PyMappingMethods ModuleSectionAddressesMixin_as_mapping = { + .mp_length = (lenfunc)ModuleSectionAddresses_length, + .mp_subscript = (binaryfunc)ModuleSectionAddresses_subscript, + .mp_ass_subscript = (objobjargproc)ModuleSectionAddresses_ass_subscript, +}; + +static PyTypeObject ModuleSectionAddressesMixin_type = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "_drgn.ModuleSectionAddressesMixin", + .tp_dealloc = (destructor)ModuleSectionAddresses_dealloc, + .tp_basicsize = sizeof(ModuleSectionAddresses), + .tp_repr = (reprfunc)ModuleSectionAddresses_repr, + .tp_as_mapping = &ModuleSectionAddressesMixin_as_mapping, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, + .tp_iter = (getiterfunc)ModuleSectionAddresses_iter, + .tp_new = (newfunc)ModuleSectionAddresses_new, +}; + +static void +ModuleSectionAddressesIterator_dealloc(ModuleSectionAddressesIterator *self) +{ + if (self->it) { + struct drgn_module *module = + drgn_module_section_address_iterator_module(self->it); + struct drgn_program *prog = drgn_module_program(module); + Py_DECREF(container_of(prog, Program, prog)); + drgn_module_section_address_iterator_destroy(self->it); + } + Py_TYPE(self)->tp_free((PyObject *)self); +} + +static PyObject * +ModuleSectionAddressesIterator_next(ModuleSectionAddressesIterator *self) +{ + struct drgn_error *err; + const char *name; + err = drgn_module_section_address_iterator_next(self->it, &name, NULL); + if (err) + return set_drgn_error(err); + if (!name) + return NULL; + return PyUnicode_FromString(name); +} + +PyTypeObject ModuleSectionAddressesIterator_type = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "_drgn._ModuleSectionAddressesIterator", + .tp_basicsize = sizeof(ModuleSectionAddressesIterator), + .tp_dealloc = (destructor)ModuleSectionAddressesIterator_dealloc, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_iter = PyObject_SelfIter, + .tp_iternext = (iternextfunc)ModuleSectionAddressesIterator_next, +}; + +int init_module_section_addresses(void) +{ + if (PyType_Ready(&ModuleSectionAddressesMixin_type)) + return -1; + _cleanup_pydecref_ PyObject *collections_abc = + PyImport_ImportModule("collections.abc"); + if (!collections_abc) + return -1; + _cleanup_pydecref_ PyObject *MutableMapping = + PyObject_GetAttrString(collections_abc, "MutableMapping"); + if (!MutableMapping) + return -1; + // We can't create a direct subclass of MutableMapping from C (see + // https://github.com/python/cpython/issues/103968). Use this multiple + // inheritance trick taken from cpython/Modules/_decimal/_decimal.c + // instead. + ModuleSectionAddresses_class = + PyObject_CallFunction((PyObject *)&PyType_Type, "s(OO){}", + "ModuleSectionAddresses", + &ModuleSectionAddressesMixin_type, + MutableMapping); + if (!ModuleSectionAddresses_class) + return -1; + return 0; +} diff --git a/libdrgn/python/program.c b/libdrgn/python/program.c index eea999cbf..90b972fe7 100644 --- a/libdrgn/python/program.c +++ b/libdrgn/python/program.c @@ -492,6 +492,28 @@ static PyObject *Program_add_memory_segment(Program *self, PyObject *args, Py_RETURN_NONE; } +static struct drgn_error * +py_debug_info_find_fn(struct drgn_module * const *modules, size_t num_modules, + void *arg) +{ + PyGILState_guard(); + + _cleanup_pydecref_ PyObject *modules_list = PyList_New(num_modules); + if (!modules_list) + return drgn_error_from_python(); + for (size_t i = 0; i < num_modules; i++) { + PyObject *module_obj = Module_wrap(modules[i]); + if (!module_obj) + return drgn_error_from_python(); + PyList_SET_ITEM(modules_list, i, module_obj); + } + _cleanup_pydecref_ PyObject *obj = + PyObject_CallOneArg(arg, modules_list); + if (!obj) + return drgn_error_from_python(); + return NULL; +} + static inline struct drgn_error * py_type_find_fn_common(PyObject *type_obj, void *arg, struct drgn_qualified_type *ret) @@ -682,6 +704,7 @@ py_symbol_find_fn(const char *name, uint64_t addr, return NULL; } +#define debug_info_finder_arg(self, fn) PyObject *arg = fn; #define type_finder_arg(self, fn) \ _cleanup_pydecref_ PyObject *arg = Py_BuildValue("OO", self, fn); \ if (!arg) \ @@ -829,6 +852,7 @@ static PyObject *Program_enabled_##which##_finders(Program *self) \ return_ptr(res); \ } +DEFINE_PROGRAM_FINDER_METHODS(debug_info) DEFINE_PROGRAM_FINDER_METHODS(type) DEFINE_PROGRAM_FINDER_METHODS(object) DEFINE_PROGRAM_FINDER_METHODS(symbol) @@ -968,6 +992,322 @@ static PyObject *Program_set_pid(Program *self, PyObject *args, PyObject *kwds) Py_RETURN_NONE; } +static ModuleIterator *Program_modules(Program *self) +{ + struct drgn_error *err; + ModuleIterator *it = call_tp_alloc(ModuleIterator); + if (!it) + return NULL; + err = drgn_created_module_iterator_create(&self->prog, &it->it); + if (err) { + it->it = NULL; + Py_DECREF(it); + return set_drgn_error(err); + } + Py_INCREF(self); + return it; +} + +static ModuleIterator *Program_loaded_modules(Program *self) +{ + struct drgn_error *err; + ModuleIterator *it = + (ModuleIterator *)ModuleIteratorWithNew_type.tp_alloc( + &ModuleIteratorWithNew_type, 0); + if (!it) + return NULL; + err = drgn_loaded_module_iterator_create(&self->prog, &it->it); + if (err) { + it->it = NULL; + Py_DECREF(it); + return set_drgn_error(err); + } + Py_INCREF(self); + return it; +} + +static PyObject *Program_main_module(Program *self, PyObject *args, + PyObject *kwds) +{ + struct drgn_error *err; + static char *keywords[] = {"name", "create", NULL}; + PATH_ARG(name); + int create = 0; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O&$p:main_module", + keywords, path_converter, &name, + &create)) + return NULL; + + if (create) { + if (!name.path) { + PyErr_SetString(PyExc_TypeError, + "name must be given if create=True"); + return NULL; + } + struct drgn_module *module; + bool new; + err = drgn_module_find_or_create_main(&self->prog, name.path, + &module, &new); + if (err) { + set_drgn_error(err); + return NULL; + } + return Module_and_bool_wrap(module, new); + } else { + struct drgn_module_key key = { .kind = DRGN_MODULE_MAIN }; + struct drgn_module *module = drgn_module_find(&self->prog, &key); + if (!module + || (name.path + && strcmp(drgn_module_name(module), name.path) != 0)) { + PyErr_SetString(PyExc_LookupError, "module not found"); + return NULL; + } + return Module_wrap(module); + } +} + +static PyObject *Program_find_module(Program *self, const struct drgn_module_key *key) +{ + struct drgn_module *module = drgn_module_find(&self->prog, key); + if (!module) { + PyErr_SetString(PyExc_LookupError, "module not found"); + return NULL; + } + return Module_wrap(module); +} + +static PyObject *Program_shared_library_module(Program *self, PyObject *args, + PyObject *kwds) +{ + struct drgn_error *err; + static char *keywords[] = {"name", "dynamic_address", "create", NULL}; + PATH_ARG(name); + struct index_arg dynamic_address = {}; + int create = 0; + if (!PyArg_ParseTupleAndKeywords(args, kwds, + "O&O&|$p:shared_library_module", + keywords, path_converter, &name, + index_converter, &dynamic_address, + &create)) + return NULL; + + if (create) { + struct drgn_module *module; + bool new; + err = drgn_module_find_or_create_shared_library(&self->prog, + name.path, + dynamic_address.uvalue, + &module, &new); + if (err) { + set_drgn_error(err); + return NULL; + } + return Module_and_bool_wrap(module, new); + } else { + struct drgn_module_key key = { + .kind = DRGN_MODULE_SHARED_LIBRARY, + .shared_library.name = name.path, + .shared_library.dynamic_address = + dynamic_address.uvalue, + }; + return Program_find_module(self, &key); + } +} + +static PyObject *Program_vdso_module(Program *self, PyObject *args, + PyObject *kwds) +{ + struct drgn_error *err; + static char *keywords[] = {"name", "dynamic_address", "create", NULL}; + PATH_ARG(name); + struct index_arg dynamic_address = {}; + int create = 0; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&O&|$p:vdso_module", + keywords, path_converter, &name, + index_converter, &dynamic_address, + &create)) + return NULL; + + if (create) { + struct drgn_module *module; + bool new; + err = drgn_module_find_or_create_vdso(&self->prog, name.path, + dynamic_address.uvalue, + &module, &new); + if (err) { + set_drgn_error(err); + return NULL; + } + return Module_and_bool_wrap(module, new); + } else { + struct drgn_module_key key = { + .kind = DRGN_MODULE_VDSO, + .vdso.name = name.path, + .vdso.dynamic_address = dynamic_address.uvalue, + }; + return Program_find_module(self, &key); + } +} + +static PyObject *Program_relocatable_module(Program *self, PyObject *args, + PyObject *kwds) +{ + struct drgn_error *err; + static char *keywords[] = {"name", "address", "create", NULL}; + PATH_ARG(name); + struct index_arg address = {}; + int create = 0; + if (!PyArg_ParseTupleAndKeywords(args, kwds, + "O&O&|$p:relocatable_module", keywords, + path_converter, &name, index_converter, + &address, &create)) + return NULL; + + if (create) { + struct drgn_module *module; + bool new; + err = drgn_module_find_or_create_relocatable(&self->prog, + name.path, + address.uvalue, + &module, &new); + if (err) { + set_drgn_error(err); + return NULL; + } + return Module_and_bool_wrap(module, new); + } else { + struct drgn_module_key key = { + .kind = DRGN_MODULE_RELOCATABLE, + .relocatable.name = name.path, + .relocatable.address = address.uvalue, + }; + return Program_find_module(self, &key); + } +} + +static PyObject *Program_linux_kernel_loadable_module(Program *self, + PyObject *args, + PyObject *kwds) +{ + struct drgn_error *err; + static char *keywords[] = {"module_obj", "create", NULL}; + DrgnObject *module_obj; + int create = 0; + if (!PyArg_ParseTupleAndKeywords(args, kwds, + "O!|$p:linux_kernel_loadable_module", + keywords, &DrgnObject_type, + &module_obj, &create)) + return NULL; + + if (DrgnObject_prog(module_obj) != self) { + PyErr_SetString(PyExc_ValueError, + "object is from different program"); + return NULL; + } + + struct drgn_module *module; + if (create) { + bool new; + err = drgn_module_find_or_create_linux_kernel_loadable(&module_obj->obj, + &module, + &new); + if (err) { + set_drgn_error(err); + return NULL; + } + return Module_and_bool_wrap(module, new); + } else { + err = drgn_module_find_linux_kernel_loadable(&module_obj->obj, + &module); + if (err) { + set_drgn_error(err); + return NULL; + } + if (!module) { + PyErr_SetString(PyExc_LookupError, "module not found"); + return NULL; + } + return Module_wrap(module); + } +} + +static PyObject *Program_extra_module(Program *self, PyObject *args, + PyObject *kwds) +{ + struct drgn_error *err; + static char *keywords[] = {"name", "id", "create", NULL}; + PATH_ARG(name); + struct index_arg id = {}; + int create = 0; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&|O&$p:extra_module", + keywords, path_converter, &name, + index_converter, &id, &create)) + return NULL; + + if (create) { + struct drgn_module *module; + bool new; + err = drgn_module_find_or_create_extra(&self->prog, name.path, + id.uvalue, &module, + &new); + if (err) { + set_drgn_error(err); + return NULL; + } + return Module_and_bool_wrap(module, new); + } else { + struct drgn_module_key key = { + .kind = DRGN_MODULE_EXTRA, + .extra.name = name.path, + .extra.id = id.uvalue, + }; + return Program_find_module(self, &key); + } +} + +static PyObject *Program_module(Program *self, PyObject *arg) +{ + struct index_arg address = {}; + if (!index_converter(arg, &address)) + return NULL; + struct drgn_module *module = + drgn_module_find_by_address(&self->prog, address.uvalue); + if (!module) { + PyErr_SetString(PyExc_LookupError, "module not found"); + return NULL; + } + return Module_wrap(module); +} + +static PyObject *Program_get_debug_info_path(Program *self, void *arg) +{ + return PyUnicode_FromString(drgn_program_debug_info_path(&self->prog)); +} + +static int Program_set_debug_info_path(Program *self, PyObject *value, void *arg) +{ + const char *path; + if (value == Py_None) { + path = NULL; + } else { + if (!PyUnicode_Check(value)) { + PyErr_SetString(PyExc_TypeError, + "debug_info_path must be str or None"); + return -1; + } + path = PyUnicode_AsUTF8(value); + if (!path) + return -1; + } + struct drgn_error *err = + drgn_program_set_debug_info_path(&self->prog, path); + if (err) { + set_drgn_error(err); + return -1; + } + return 0; +} + DEFINE_VECTOR(path_arg_vector, struct path_arg); static void path_arg_vector_cleanup(struct path_arg_vector *path_args) @@ -1055,6 +1395,40 @@ static PyObject *Program_load_default_debug_info(Program *self) Py_RETURN_NONE; } +DEFINE_VECTOR(drgn_module_vector, struct drgn_module *); + +static PyObject *Program_load_module_debug_info(Program *self, PyObject *args) +{ + size_t num_modules = PyTuple_GET_SIZE(args); + _cleanup_free_ struct drgn_module **modules = + malloc_array(num_modules, sizeof(*modules)); + if (!modules) { + PyErr_NoMemory(); + return NULL; + } + + for (size_t i = 0; i < num_modules; i++) { + PyObject *item = PyTuple_GET_ITEM(args, i); + if (!PyObject_TypeCheck(item, &Module_type)) { + return PyErr_Format(PyExc_TypeError, + "expected Module, not %s", + Py_TYPE(item)->tp_name); + } + modules[i] = ((Module *)item)->module; + if (modules[i]->prog != &self->prog) { + PyErr_SetString(PyExc_ValueError, + "module from wrong program"); + return NULL; + } + } + + struct drgn_error *err = + drgn_load_module_debug_info(modules, &num_modules); + if (err) + return set_drgn_error(err); + Py_RETURN_NONE; +} + static PyObject *Program_read(Program *self, PyObject *args, PyObject *kwds) { static char *keywords[] = {"address", "size", "physical", NULL}; @@ -1562,6 +1936,7 @@ static int Program_set_language(Program *self, PyObject *value, void *arg) static PyMethodDef Program_methods[] = { {"add_memory_segment", (PyCFunction)Program_add_memory_segment, METH_VARARGS | METH_KEYWORDS, drgn_Program_add_memory_segment_DOC}, + PROGRAM_FINDER_METHOD_DEFS(debug_info), PROGRAM_FINDER_METHOD_DEFS(type), PROGRAM_FINDER_METHOD_DEFS(object), PROGRAM_FINDER_METHOD_DEFS(symbol), @@ -1575,11 +1950,33 @@ static PyMethodDef Program_methods[] = { drgn_Program_set_kernel_DOC}, {"set_pid", (PyCFunction)Program_set_pid, METH_VARARGS | METH_KEYWORDS, drgn_Program_set_pid_DOC}, + {"modules", (PyCFunction)Program_modules, METH_NOARGS, + drgn_Program_modules_DOC}, + {"loaded_modules", (PyCFunction)Program_loaded_modules, METH_NOARGS, + drgn_Program_loaded_modules_DOC}, + {"main_module", (PyCFunction)Program_main_module, + METH_VARARGS | METH_KEYWORDS, drgn_Program_main_module_DOC}, + {"shared_library_module", (PyCFunction)Program_shared_library_module, + METH_VARARGS | METH_KEYWORDS, drgn_Program_shared_library_module_DOC}, + {"vdso_module", (PyCFunction)Program_vdso_module, + METH_VARARGS | METH_KEYWORDS, drgn_Program_vdso_module_DOC}, + {"relocatable_module", (PyCFunction)Program_relocatable_module, + METH_VARARGS | METH_KEYWORDS, drgn_Program_relocatable_module_DOC}, + {"linux_kernel_loadable_module", + (PyCFunction)Program_linux_kernel_loadable_module, + METH_VARARGS | METH_KEYWORDS, + drgn_Program_linux_kernel_loadable_module_DOC}, + {"extra_module", (PyCFunction)Program_extra_module, + METH_VARARGS | METH_KEYWORDS, drgn_Program_extra_module_DOC}, + {"module", (PyCFunction)Program_module, METH_O, + drgn_Program_module_DOC}, {"load_debug_info", (PyCFunction)Program_load_debug_info, METH_VARARGS | METH_KEYWORDS, drgn_Program_load_debug_info_DOC}, {"load_default_debug_info", (PyCFunction)Program_load_default_debug_info, METH_NOARGS, drgn_Program_load_default_debug_info_DOC}, + {"load_module_debug_info", (PyCFunction)Program_load_module_debug_info, + METH_VARARGS, drgn_Program_load_module_debug_info_DOC}, {"__getitem__", (PyCFunction)Program_subscript, METH_O | METH_COEXIST, drgn_Program___getitem___DOC}, {"__contains__", (PyCFunction)Program_contains, METH_O | METH_COEXIST, @@ -1661,6 +2058,8 @@ static PyGetSetDef Program_getset[] = { drgn_Program_platform_DOC}, {"language", (getter)Program_get_language, (setter)Program_set_language, drgn_Program_language_DOC}, + {"debug_info_path", (getter)Program_get_debug_info_path, + (setter)Program_set_debug_info_path, drgn_Program_debug_info_path_DOC}, {}, }; diff --git a/libdrgn/python/util.c b/libdrgn/python/util.c index 40b09f36f..c2270b02b 100644 --- a/libdrgn/python/util.c +++ b/libdrgn/python/util.c @@ -1,6 +1,7 @@ // Copyright (c) Meta Platforms, Inc. and affiliates. // SPDX-License-Identifier: LGPL-2.1-or-later +#include #include #include "drgnpy.h" @@ -13,6 +14,13 @@ int append_string(PyObject *parts, const char *s) return PyList_Append(parts, str); } +int append_u64_hex(PyObject *parts, uint64_t value) +{ + char buf[19]; + snprintf(buf, sizeof(buf), "0x%" PRIx64, value); + return append_string(parts, buf); +} + static int append_formatv(PyObject *parts, const char *format, va_list ap) { _cleanup_pydecref_ PyObject *str = PyUnicode_FromFormatV(format, ap); @@ -32,6 +40,18 @@ int append_format(PyObject *parts, const char *format, ...) return ret; } +int append_attr_repr(PyObject *parts, PyObject *obj, const char *attr_name) +{ + _cleanup_pydecref_ PyObject *attr = + PyObject_GetAttrString(obj, attr_name); + if (!attr) + return -1; + _cleanup_pydecref_ PyObject *str = PyObject_Repr(attr); + if (!str) + return -1; + return PyList_Append(parts, str); +} + PyObject *join_strings(PyObject *parts) { _cleanup_pydecref_ PyObject *sep = PyUnicode_New(0, 0); diff --git a/libdrgn/register_state.c b/libdrgn/register_state.c index d6c6d3c55..281157b86 100644 --- a/libdrgn/register_state.c +++ b/libdrgn/register_state.c @@ -1,7 +1,6 @@ // Copyright (c) Meta Platforms, Inc. and affiliates. // SPDX-License-Identifier: LGPL-2.1-or-later -#include #include #include "debug_info.h" @@ -105,14 +104,8 @@ void drgn_register_state_set_pc(struct drgn_program *prog, pc &= drgn_platform_address_mask(&prog->platform); regs->_pc = pc; drgn_register_state_set_known(regs, 0); - Dwfl_Module *dwfl_module = dwfl_addrmodule(prog->dbinfo.dwfl, + regs->module = drgn_module_find_by_address(prog, pc - !regs->interrupted); - if (dwfl_module) { - void **userdatap; - dwfl_module_info(dwfl_module, &userdatap, NULL, NULL, - NULL, NULL, NULL, NULL); - regs->module = *userdatap; - } } struct optional_uint64 diff --git a/libdrgn/symbol.c b/libdrgn/symbol.c index 51177deb0..5b7b3779f 100644 --- a/libdrgn/symbol.c +++ b/libdrgn/symbol.c @@ -34,26 +34,6 @@ LIBDRGN_PUBLIC void drgn_symbols_destroy(struct drgn_symbol **syms, free(syms); } -void drgn_symbol_from_elf(const char *name, uint64_t address, - const GElf_Sym *elf_sym, struct drgn_symbol *ret) -{ - ret->name = name; - ret->name_lifetime = DRGN_LIFETIME_STATIC; - ret->lifetime = DRGN_LIFETIME_OWNED; - ret->address = address; - ret->size = elf_sym->st_size; - int binding = GELF_ST_BIND(elf_sym->st_info); - if (binding <= STB_WEAK || binding == STB_GNU_UNIQUE) - ret->binding = binding + 1; - else - ret->binding = DRGN_SYMBOL_BINDING_UNKNOWN; - int type = GELF_ST_TYPE(elf_sym->st_info); - if (type <= STT_TLS || type == STT_GNU_IFUNC) - ret->kind = type; - else - ret->kind = DRGN_SYMBOL_KIND_UNKNOWN; -} - struct drgn_error * drgn_symbol_copy(struct drgn_symbol *dst, struct drgn_symbol *src) { @@ -142,6 +122,57 @@ drgn_symbol_result_builder_add(struct drgn_symbol_result_builder *builder, return true; } +static void drgn_symbol_from_elf(const char *name, uint64_t address, + const GElf_Sym *elf_sym, + struct drgn_symbol *ret) +{ + ret->name = name; + ret->name_lifetime = DRGN_LIFETIME_STATIC; + ret->lifetime = DRGN_LIFETIME_OWNED; + ret->address = address; + ret->size = elf_sym->st_size; + int binding = GELF_ST_BIND(elf_sym->st_info); + if (binding <= STB_WEAK || binding == STB_GNU_UNIQUE) + ret->binding = binding + 1; + else + ret->binding = DRGN_SYMBOL_BINDING_UNKNOWN; + int type = GELF_ST_TYPE(elf_sym->st_info); + if (type <= STT_TLS || type == STT_GNU_IFUNC) + ret->kind = type; + else + ret->kind = DRGN_SYMBOL_KIND_UNKNOWN; +} + +bool +drgn_symbol_result_builder_add_from_elf(struct drgn_symbol_result_builder *builder, + const char *name, uint64_t address, + const GElf_Sym *elf_sym) +{ + if (builder->one) { + // As an optimization, reuse the existing symbol allocation if + // we can. + if (!builder->single + || builder->single->lifetime == DRGN_LIFETIME_STATIC) { + builder->single = malloc(sizeof(*builder->single)); + if (!builder->single) + return false; + } else if (builder->single->name_lifetime == DRGN_LIFETIME_OWNED) { + free((char *)builder->single->name); + } + drgn_symbol_from_elf(name, address, elf_sym, builder->single); + } else { + struct drgn_symbol *sym = malloc(sizeof(*sym)); + if (!sym) + return false; + drgn_symbol_from_elf(name, address, elf_sym, sym); + if (!symbolp_vector_append(&builder->vector, &sym)) { + free(sym); + return false; + } + } + return true; +} + LIBDRGN_PUBLIC size_t drgn_symbol_result_builder_count(const struct drgn_symbol_result_builder *builder) { diff --git a/libdrgn/symbol.h b/libdrgn/symbol.h index c3dd75ca7..3bd0c508c 100644 --- a/libdrgn/symbol.h +++ b/libdrgn/symbol.h @@ -46,10 +46,6 @@ static inline void drgn_symbol_cleanup(struct drgn_symbol **p) drgn_symbol_destroy(*p); } -/** Initialize a @ref drgn_symbol from an ELF symbol. */ -void drgn_symbol_from_elf(const char *name, uint64_t address, - const GElf_Sym *elf_sym, struct drgn_symbol *ret); - /** Destroy the contents of the result builder */ void drgn_symbol_result_builder_abort(struct drgn_symbol_result_builder *builder); @@ -57,6 +53,16 @@ void drgn_symbol_result_builder_abort(struct drgn_symbol_result_builder *builder void drgn_symbol_result_builder_init(struct drgn_symbol_result_builder *builder, bool one); +/** + * Convert an ELF symbol to a @ref drgn_symbol and add it to a result builder. + * + * @return @c true on success, @c false on failure to allocate memory. + */ +bool +drgn_symbol_result_builder_add_from_elf(struct drgn_symbol_result_builder *builder, + const char *name, uint64_t address, + const GElf_Sym *elf_sym); + /** Return single result */ struct drgn_symbol * drgn_symbol_result_builder_single(struct drgn_symbol_result_builder *builder); diff --git a/libdrgn/util.h b/libdrgn/util.h index 333ff2f58..7cd890598 100644 --- a/libdrgn/util.h +++ b/libdrgn/util.h @@ -18,6 +18,8 @@ #include #include +#define _unused_ __attribute__((__unused__)) + #ifndef LIBDRGN_PUBLIC #define LIBDRGN_PUBLIC __attribute__((__visibility__("default"))) #endif diff --git a/scripts/crashme/Makefile b/scripts/crashme/Makefile new file mode 100644 index 000000000..b9b5c5a8c --- /dev/null +++ b/scripts/crashme/Makefile @@ -0,0 +1,65 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# SPDX-License-Identifier: LGPL-2.1-or-later + +# Makefile used to generate tests/resources/crashme* + +.PHONY: all cores clean + +.DELETE_ON_ERROR: + +EXECUTABLES := crashme crashme_pie crashme_static crashme_static_pie +CORES := $(addsuffix .core, $(EXECUTABLES)) $(addsuffix _no_headers.core, $(EXECUTABLES)) +BINARIES := crashme.so $(EXECUTABLES) crashme.dwz crashme.so.dwz crashme.alt +ZSTD_BINARIES := $(addsuffix .zst, $(BINARIES)) +ZSTD_CORES := $(addsuffix .zst, $(CORES)) + +all: $(BINARIES) cores $(ZSTD_BINARIES) $(ZSTD_CORES) + +clean: + rm -f $(BINARIES) $(CORES) $(ZSTD_BINARIES) $(ZSTD_CORES) + +crashme.so: crashme.c common.c + gcc -g -Os -fpic -shared $^ -o $@ + +crashme: main.c common.c crashme.so + gcc -g -Os -fno-pie -no-pie $(filter-out crashme.so,$^) -o $@ -L . -l:crashme.so -Wl,-rpath,$(CURDIR) + +crashme_pie: main.c common.c crashme.so + gcc -g -Os -fpie -pie $(filter-out crashme.so,$^) -o $@ -L . -l:crashme.so -Wl,-rpath,$(CURDIR) + +crashme_static: main.c common.c crashme.c + musl-gcc -g -Os -fno-pie -static $^ -o $@ + +crashme_static_pie: main.c common.c crashme.c + musl-gcc -g -Os -fpie -static-pie $^ -o $@ + +crashme.dwz crashme.so.dwz crashme.alt &: crashme crashme.so + cp crashme crashme.dwz + cp crashme.so crashme.so.dwz + dwz -m crashme.alt -r crashme.dwz crashme.so.dwz + +cores: $(CORES) + +.NOTPARALLEL: cores + +define CORE_COMMAND +flock /proc/sys/kernel/core_pattern sh -e -c '\ +ulimit -c unlimited; \ +echo "$$COREDUMP_FILTER" > /proc/$$$$/coredump_filter; \ +old_pattern=$$(cat /proc/sys/kernel/core_pattern); \ +restore_core_pattern() { \ + echo "$$old_pattern" > /proc/sys/kernel/core_pattern; \ +}; \ +trap restore_core_pattern EXIT; \ +echo "$$PWD/core.%p" > /proc/sys/kernel/core_pattern; \ +su "$$SUDO_USER" -c "env -i sh -l -c \"exec ./$<\" & wait; mv core.\$$! $@"' +endef + +%.core: % + sudo env COREDUMP_FILTER=0x33 $(CORE_COMMAND) + +%_no_headers.core: % + sudo env COREDUMP_FILTER=0x23 $(CORE_COMMAND) + +%.zst: % + zstd -19 $< -o $@ diff --git a/scripts/crashme/common.c b/scripts/crashme/common.c new file mode 100644 index 000000000..98b13c615 --- /dev/null +++ b/scripts/crashme/common.c @@ -0,0 +1,10 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "crashme.h" + +__attribute__((__visibility__("hidden"))) +int *crashme_ptr(void) +{ + return (int *)0xabc; +} diff --git a/scripts/crashme/crashme.c b/scripts/crashme/crashme.c new file mode 100644 index 000000000..8edf2f5d9 --- /dev/null +++ b/scripts/crashme/crashme.c @@ -0,0 +1,25 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "crashme.h" + +__attribute__((__noipa__)) static int c(struct crashme *cm) +{ + *cm->ptr = 0xdeadbeef; + return 3; +} + +__attribute__((__noipa__)) static int b(struct crashme *cm) +{ + return c(cm) - 1; +} + +__attribute__((__noipa__)) static int a(struct crashme *cm) +{ + return b(cm) - 1; +} + +int crashme(struct crashme *cm) +{ + return cm->ptr == crashme_ptr() ? a(cm) - 1 : 1; +} diff --git a/scripts/crashme/crashme.h b/scripts/crashme/crashme.h new file mode 100644 index 000000000..75ab6e1cb --- /dev/null +++ b/scripts/crashme/crashme.h @@ -0,0 +1,15 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// SPDX-License-Identifier: LGPL-2.1-or-later + +#ifndef CRASHME_H +#define CRASHME_H + +int *crashme_ptr(void); + +struct crashme { + int *ptr; +}; + +int crashme(struct crashme *cm); + +#endif /* CRASHME_H */ diff --git a/scripts/crashme/main.c b/scripts/crashme/main.c new file mode 100644 index 000000000..06c65a758 --- /dev/null +++ b/scripts/crashme/main.c @@ -0,0 +1,10 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "crashme.h" + +int main(void) +{ + struct crashme cm = { crashme_ptr() }; + return !!crashme(&cm); +} diff --git a/tests/linux_kernel/test_debug_info.py b/tests/linux_kernel/test_debug_info.py index 75ccabf50..f50afbdfb 100644 --- a/tests/linux_kernel/test_debug_info.py +++ b/tests/linux_kernel/test_debug_info.py @@ -2,49 +2,89 @@ # SPDX-License-Identifier: LGPL-2.1-or-later import os -from pathlib import Path -import unittest -from drgn import Program +from drgn import Program, RelocatableModule +from drgn.helpers.linux.module import find_module from tests import modifyenv from tests.linux_kernel import LinuxKernelTestCase, skip_unless_have_test_kmod -KALLSYMS_PATH = Path("/proc/kallsyms") - - -@unittest.skipUnless( - KALLSYMS_PATH.exists(), "kernel does not have kallsyms (CONFIG_KALLSYMS)" -) -@skip_unless_have_test_kmod -class TestModuleDebugInfo(LinuxKernelTestCase): - # Arbitrary symbol that we can use to check that the module debug info was - # loaded. - SYMBOL = "drgn_test_function" - - def setUp(self): - super().setUp() - with KALLSYMS_PATH.open() as f: - for line in f: - tokens = line.split() - if tokens[2] == self.SYMBOL: - self.symbol_address = int(tokens[0], 16) - break - else: - self.fail(f"{self.SYMBOL!r} symbol not found") - def _test_module_debug_info(self, use_sys_module): - old_use_sys_module = int(os.environ.get("DRGN_USE_SYS_MODULE", "1")) != 0 - with modifyenv({"DRGN_USE_SYS_MODULE": "1" if use_sys_module else "0"}): - if old_use_sys_module == use_sys_module: - prog = self.prog +def iter_proc_modules(): + try: + f = open("/proc/modules", "r") + except FileNotFoundError: + return + with f: + for line in f: + tokens = line.split() + yield tokens[0], int(tokens[5], 16) + + +class TestModule(LinuxKernelTestCase): + def test_loaded_modules(self): + expected = [("kernel", None), *iter_proc_modules()] + + loaded_modules = [] + for module, _ in self.prog.loaded_modules(): + if isinstance(module, RelocatableModule): + loaded_modules.append((module.name, module.address)) else: - prog = Program() - prog.set_kernel() - self._load_debug_info(prog) - self.assertEqual(prog.symbol(self.SYMBOL).address, self.symbol_address) + loaded_modules.append((module.name, None)) + + self.assertCountEqual(loaded_modules, expected) + + @skip_unless_have_test_kmod + def test_find(self): + self.assertEqual(self.prog.main_module().name, "kernel") + for name, address in iter_proc_modules(): + if name == "drgn_test": + self.assertEqual( + self.prog.relocatable_module(name, address).name, "drgn_test" + ) + break + else: + self.fail("test module not found") + + @skip_unless_have_test_kmod + def test_find_by_obj(self): + for module in self.prog.modules(): + if module.name == "drgn_test": + break + else: + self.fail("test module not found") - def test_module_debug_info_use_proc_and_sys(self): - self._test_module_debug_info(True) + module_obj = find_module(self.prog, "drgn_test") + self.assertEqual(self.prog.linux_kernel_loadable_module(module_obj), module) + self.assertEqual( + self.prog.linux_kernel_loadable_module(module_obj, create=True), + (module, False), + ) + + def test_no_sys_module(self): + # Test that we get the same modules with and without using /sys/module. + + def module_dict(prog): + return { + (module.name, module.address): ( + module.address_range, + module.build_id, + dict(module.section_addresses), + ) + for module, _ in prog.loaded_modules() + if isinstance(module, RelocatableModule) + } + + use_sys_module = int(os.environ.get("DRGN_USE_SYS_MODULE", "1")) != 0 + + with modifyenv({"DRGN_USE_SYS_MODULE": str(int(not use_sys_module))}): + prog = Program() + prog.set_kernel() + + if use_sys_module: + with_sys_module = module_dict(self.prog) + without_sys_module = module_dict(prog) + else: + with_sys_module = module_dict(prog) + without_sys_module = module_dict(self.prog) - def test_module_debug_info_use_core_dump(self): - self._test_module_debug_info(False) + self.assertEqual(with_sys_module, without_sys_module) diff --git a/tests/resources/crashme.alt.zst b/tests/resources/crashme.alt.zst new file mode 100644 index 0000000000000000000000000000000000000000..1aab40796933e144f210591d017fc3e0480ac349 GIT binary patch literal 409 zcmV;K0cQRvwJ-f-j{;o`0LIivI52Uw2Ef3;paq364ig0tN+b*rJAMK7`dgl~hLBoW z%>BctC9U@t8^0g^Ty#5ertHVMlI?CHQ#AlM05<@k4#0MA|MtJb)_#Z)nwaf?C4z*| z;!=h$sshVQ4a_1Q8$b4Y{_Jh;(WY`cxZT=Iqa$M_CGYfE*kB)vzwEtJmvHA`e@EWw zQrwD5>aYc_H|W@s{q;1pjPnj{oc+6O0Q)(#4{-tZbTzpG<2Pg=)7*bi+slt+m1>x# z=3Jkk;~A=`h-9IqXix=ZEM1+ocH$1a;Jt&m_w2nJ%oc3l(f$*oX)0t*p62R=Ohqw7 zOH?h99KTb@@ibB5sQMmFhjC@L#IJR#K5|9RZ!uicRV~$T6~mbIggks)5MYqVNLY|B zph28f5>${R2}{~xA)1~JyV37#o9AxE$jE D{pHCc literal 0 HcmV?d00001 diff --git a/tests/resources/crashme.core.zst b/tests/resources/crashme.core.zst new file mode 100644 index 0000000000000000000000000000000000000000..215fe38e8c62ae0e771e793770c2d2f3199003f7 GIT binary patch literal 18351 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 KTG$qhn+!R3E42~; literal 0 HcmV?d00001 diff --git a/tests/resources/crashme.dwz.zst b/tests/resources/crashme.dwz.zst new file mode 100755 index 0000000000000000000000000000000000000000..79dd123fd667fa9e2adab09a8e8daafe08fd3d58 GIT binary patch literal 2716 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 W!?iC_Z_GSa!jO^#zrY?c{UdGl&p0*! literal 0 HcmV?d00001 diff --git a/tests/resources/crashme.so.dwz.zst b/tests/resources/crashme.so.dwz.zst new file mode 100755 index 0000000000000000000000000000000000000000..df14e0fd997e0b449692919f359e45df744ef655 GIT binary patch literal 2482 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 literal 0 HcmV?d00001 diff --git a/tests/resources/crashme.so.zst b/tests/resources/crashme.so.zst new file mode 100755 index 0000000000000000000000000000000000000000..a31a002adf7fa8170b3fafb2ac02bebd6c5ff6ed GIT binary patch literal 2561 zcmV+c3jXydwJ-f-Kt!ES09tO47gu0b9TyI@2Pw_pr+M}(RlYLZy|VZm9K059@|quf zSmP~UXmcYpvN2?agKZP=O}cS3YYj|8;ii#^I&hiHWx!ztKer!>mJzmTuIpHJU;KCJ zX=xLg0&M|X0ayVM_+VwE*fi;C$c%sy#T!=tNh%;xfI*4Zz>;Eh6tUDUOd5Jc9L3+Z z!sH=*uwSvmgG~~E_@bStKd422mGcR*qeH*b3obQ``PC zGnT`ClJqhZqtpBFBdK+>Z;nstp+?Cq$!o%lssj{5tbB+_HQ)h)lPLdKR`vTyMm4VX z)Bot{PXlzW^WW&nLm<68jFI2|ATN)06%V?iAJwt8b7i|aTP@qY+3C&N%a^x!9p^ex z$Gy*U@_+X@L?lE(ubdZ+cA0^aH~Z_!>XNiU$EyFOnfL*Hc1&jU;_UNGo_lPifjx&B z!M-z_geG4bS=5|aldhlqH=5lw|F7kL`liUFCSSPFnDiBPDgaU*-xV->Cd^EZ6v0c6ttNjVQ}?08q}p>2%JDTE6ZdW86(w(i9{mS74?~`Ej3d zYZ}I<52MB=yR6!Kk787snVg#q&5cH#S!q_A85Ks1Is*u(MtZ}8p1hLB_|^M!i8C*0 zJxHl!sz4cEJo(yRAKvwtlT78DkOTSMVYe0P;#CIA{MdLD)9c*zK;#fH+@)p=H;iUsCxASesA(&*yDyyY*$V{ZjBJ%L2iopjf#r|(8qA&q+dw4|> z{+iyM9P|D{On~Tp>zLp5@ujDDSc<;-$9^Qx(}ehq@cbw58)mwje4OEG6wN{kCHy1& zBK#n5aP?Hj)g71xy>aMM-Z(1!#pyEGN0ZM;UbYGfiY$b(1q&7`zNt}XQzo@di}E@` zI2eR^_vL_lLU#zuyg}F}bKc7PIWgJPp$(hbWK(T!Y&SShJ3e0Q@_z9ve4BP&AyE$6 z)B$FI)VDO1S<%bO7{H0Te~vJw1xhHU(pS?06cfj*#VONfOPAwiCXO~;TITF`(#|%g z&e81iWzt5oHn!g8YbRx6OOO9u-O~U2CNU_w`@hW5oBv&SN&aJhG-JQXSy->HUqvTI zs`Pwy{V4h3dA$zP`d>IN6SQA5UauJ0G*;TO(n7vyF&8~MTV+QL$2#d5EvNG0g-fqy z4t055R{pgKYU6X{PotnEXmEaT$v|$avh{SAd(Fq@N5T zw-b=syudR+O^;jWa4HxEW|ZD3`ZpW~Qr6t-%?b2&9Ezm)Vo{ZO&yWt{so7OWkDfpL z>v`TplE{Rhwv(|Hs*XG1<3BK&={oqEhMP)LwZ}?&wp5Cm`Tx9v*MgKvyB0UImoo;dyq#=l}YDWUYJ%l*8AT3*?Pbq zaR(*GuM-?Qtwdqd`pU4-7t1?W4nhrA+}{cj{rKowoe5QU zMLtG2f?A-jg@=FJ1{n#pLPb7DLp|Z^ekZq3!632c#!QEbW#QXGxpC~azuVe-49mS1=xo1dFZ#- ziL>$e2{^LB4%kWTyodS}8(O=bv5<3PxMy671GF`aB~Y&`O6GRq;Z|ZK?(3tk6a|Wc zetkx(^6XzKh->vp;CN}8i#@fPjhqx(Rw!TZoRr9@UPSYJBJ^uA4(Z1tTfQ(rT6g21vpR;a;Y#iqFsyVFDQsA!s`3uB8iAjw2v{O6;)E**XMkMf~zeh5@5bxe~?5;)sr-rhZnU zosNJ)6(S7$`(7(fXV6Msi}rwrcBVTVBa~|UgRQlp7lVf z?o`5t;6RQ*pHHReY(2%)K!|^tWiQbdWk0HlY&g(LVzBSNL@oxjD z1KDc8%ga?`N04S6FbK(>Bw{$JNkh(la&rVHbo$La(DiZ!Y7$IQ??P(?0wqTTBNzCN zB-Ge#o3a{tGr=~iVM{e;SryuLk-u}q+YKsjsSJKYfC>@tEl-S)7gQ4WS)Yb(Ah}Vo zw>Fu)#R-IP&p8kK$P_y-lo<^d;3;tS1<(nT(PyEe4Huz@%Ir>ZAUHWd)?3E8=p^4H XALf$k|3Z^UE*W?f`WGbwW^tL08S)41 literal 0 HcmV?d00001 diff --git a/tests/resources/crashme.zst b/tests/resources/crashme.zst new file mode 100755 index 0000000000000000000000000000000000000000..670858840ea50fda9422ad9f59d43cfbfbd5cc2f GIT binary patch literal 2727 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 FQ45a{gh>DZ literal 0 HcmV?d00001 diff --git a/tests/resources/crashme_pie_no_headers.core.zst b/tests/resources/crashme_pie_no_headers.core.zst new file mode 100644 index 0000000000000000000000000000000000000000..db67b91361cc1f7a1d67b29630f17080c1b8f8d2 GIT binary patch literal 14787 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 dE%OB$iXZ?0 literal 0 HcmV?d00001 diff --git a/tests/resources/crashme_static.core.zst b/tests/resources/crashme_static.core.zst new file mode 100644 index 0000000000000000000000000000000000000000..db207fbc6b408052ad796b85f33a39b117d4232a GIT binary patch literal 5234 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 FpLUkHAM5}C literal 0 HcmV?d00001 diff --git a/tests/resources/crashme_static_pie.core.zst b/tests/resources/crashme_static_pie.core.zst new file mode 100644 index 0000000000000000000000000000000000000000..2e2ef463b5078bc3578908b0a46ce4139dbb6de2 GIT binary patch literal 5482 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 gmhji_@% literal 0 HcmV?d00001 diff --git a/tests/resources/crashme_static_pie.zst b/tests/resources/crashme_static_pie.zst new file mode 100755 index 0000000000000000000000000000000000000000..03fc71cac07f70564934818348aa30c97bb05905 GIT binary patch literal 8036 zcmV-qADiGPwJ-f-2xhhV07@cBLr@@6Lel}3dTC~ry+u?+R7C75p>CqOh`6{{)Yj~D z&CJx^!EJ4@0oQXn8vm&az-fKwwa;IPm1PcLgS2*zp>^IQG#T>{T zG6ehsXRbnFoDimAkH9c~W{md(l061ZWd$Hwu{>m~v3P@Z=XHP^RDg_nu2yC1HKQ@e zNk+}Z9?hDwQ+qVJdmyV{@G<`Igpq(1**ZIvJzf#hV2!phZEb-OVY*Z@2*W4nS=NMP zflk<(-eCh|q-U{7u28Pn+6xb9(5}b{+l9vP@FF>(UENw=@Di?EQ>xDDMbK(mQmdq9-iVpi} zMPvpak4Geo z6t==MW@kcZEZrEmHY*NSUf^PDA|%9U=>iaji+q?05FoIy#G~1=HO>kZB5FeT4?WA0 z+wG!JixI53jgV1==~`dOuzqI}JM+?=eRmof>MXp5*IcM|DB(h#M#!i}(wUd;ZHrdp z+-pf&ea17sso}r`g;l4mZDjvv1^N$djTXjBcgqaeV;ZbZ*Vbp&f4;UoVw|tccoz2+ z?2$`D{pIFRp9C683sr8X^%4SdMhk)i`jaIUyMC!;YNCxJ?>=C zGZ0oWNjP3};WeGDZ`S$DwGJEN$c)U2_3*Cl8jiHJ7OwR`7wZ;g3)V>2GHMsF$1&Cy z>AD1!A91b4@cMgrFv4sR7HxQReQ1qbGBGBsX|qQt>lU(Eo830O@U!;R!&ADqNUia+ zMy~Zr(%KNMrsuUQEm}QzcLOkG= zYyArG%$nV9nlv(P-R(w^_JFqDF2QTgSB~16jJ0m&+&`lkaIGb%`Sxnd{&Olw9I0m@ z+FA_O_&?Ue3pW3s1y@WFLo?LS)?l!862kgydjju=0lDI)&&A3}(2i$*~1>6~lYmEk5WdydqI>Un3#H(=)dw4#)AmX7$_Y4cEql*@e z<@=vurG_pE9~t;;dxZFj`l5qa7?xZP*!SMH{HjM3kaH2J=$C!G6<`f=c^gp`|4)}M zf)fACr#ZOZIEZg^1d%dq28?TJI)B*7$1eUS_ecWFKcve($HUM03w11p1v0F|i^EBW zBQb@%pYg^&-0z`G>dnX)6(0p#@(S20fouNDn&df|a?APWt$-slNYL%50>(q&04d^G z95=hL;!nB{j*FmYNI0$TXcrtw2gU-P1FJ!RMc8YTtt$qQzlil?*qAey$42jOnVp}q z-1`H|nSqUnXAOo3v*b_0##PZpwt*g?Gl@U!gP4bT(1M3WX@G}8!Sb*U{yBi+VH-r@ zVH$+-uqQt7uncf`7zSEA%n1@atjUvyF(H75T~PBd3l`u{q=P2&#E5-8rxW1!iZkVW zkyVZ!RMmkE@Lnph{__z*rrP2N>ox%GtFHj!79Nu;D!tDaw^RIGX6|EoE|CtAifX{BVm$}(2b!xSp%0$svw}S=tj|zQIQc)7!(GNgg~Jn zppuhQ)W5(STpye1qPc0oLz@mRjPuKJ98gB;u(X?|3=fuyc}IumrlV_o$Ti>;0N&hF z2QYjszDC`y6ZF^&A+LPxWm2)WNQHnDtRSxUXf`~8uf!y@e98D)!(~lKPqh#9vtOU$ zGAURKHATKY^|E5Dje-|wl66owy4^B!*&ScuGACK*^kk6WszYMhgA1nI-d12yplGFM14U@k1j94i!3HY$lT9N&R+E^zM}q zV+GXhj#&=r|6CL9WdN3SQZEAZuJ}gjc9-bF1DtI&JDmRFPgR8b(7;RGt{Irdg2WLN zGE0hpE_t#7cDqmb$lTxs3k*=a%%ERe%PKNF*1Fv$mN!f}3iAKLaP-e>;uKVHz&jiY zwML1&m`kEFCiL4v8f<-r#>%CuLBDN7TsNqjfYn^6X|QtX)*95@Hnb;v)HQ5vCQRq0 zVQaIcDjf`!u5x%pt=hM0yZ|E;M2(y2S$;;q!O{@LGjb98Pl33+@@V*pL6?0B`Xb|< zIr0gpZL!9j)%L>Y1qnb9OfdFA5*{CJA8S;@RkmoxuJA^sMHQltSDGWYwJ%m&fT=R0 zfJ4N#c1=U85rmlxJ~a?e)ZJQJ=fGe{KfJsSCz^^)!(5ERL)FhYErkb*!(YKn4vN|G_HZ-Zcnbl@f>SR1lHSx%* zy5d4ItI4e|_L%BnMB_;OVLMe7pPYJJLETa2O_g$0-)qUNMy<>#*zMKD>{?Q#K&DZ} z9(Iu`6Z&i#RiR7UexNfdg}zm>O8H&ZDA&_Rolj`e{$_Kk2`wJku>Nf`i#)4OT`rSe z_Vl5krjSF;UyUxR=^O)qAMfXWj zlKT!^-yyZ+>ry&j@9hxay`|_pSx5K7M4o$(#%p%#J*KMMtvR`?So#6WZ%4A!12)>R z)QRMHNv8*wux@N_R$R~O-D>M*FJSqVGT%t<=FFv-`M@&&Kz>uu-6_K{ix^94iSAi7 zQc_F5u7#)HQn-uGbsYd)zrI~ciVW{`Bte=6%zJzIi`9c^s7CJLx*ZzMseb+#fZy{R?l#+GARiv=G5mJ^ADmzQ zI$qasBVWI`+y4jUu8VU!nD^kf?{;=@&Q%WmHZEEm`Q(6MIvfc%!s{N6!vbf84iL`{ z5XVuWlgs7c@vrCbT#olr{LeTvx5>Hc+3A6Ez1<6L<0JCv^^Ba~@SIJ~70=N*KD>F? zbwz$d+YPtT0n+7axXx0}=he}{gSi8K?;COR?Kgk7%|5y3<&Zj#SeHw&gL64@x&O&& zLLukv8FNWF9Nb6gNXN5-KZwT&NWo9YJDv@s)ERFuVQP%gn&0XiKH$Y+ha?*g$>U1^1P`{Ro1 ze1z3Nr#UVquT7T{X91VX++^DWa}1}m>t0#PhI^lSPvhczduJrr%1fC@SuuRkoQd%n zIWs*3P~$e_|LJcJQ}5>gia&v^I8MvO8^Pj8kk%!oC5Y2_Xeh5d7`J_pz*k!O@9z!zT+^G3p&@5re!~smBQ~m`$B;qW6@W#C20SnU1r}*u zVz-?Znp^?gfCOw{Y1R+E6}1raAp!pikWWBiGid^N9gaZ9xP+*?h$urD!cZf=Eu8_` zh&b}HLvy4Vuc>J)ZGG9Ccw>OVU;^Mk0I_J`yhG7&wgISv%Y`P=(d7lY^C_2X<+XSS zSiC25Fb8rM!K+34Zx{~vMnni0=C%xn&_G%Sn3kD8EqE9Q;V=?h#rqEi=61|H@Joro z-*HB%Ri%^}^_dUt_Uga5YG$+V@DJMplZDu0?(65$$R zL+#?RiD+dLo2HIY_ESC8b&4n5KO&`weeu@R?FZ3xO1c2?j5% z#MT0klWY?$K2GQZxU`5o{k|b+%Xt{UjaA$& z1q>kYOB>rUL)GF*8?2+x%g{yH>x_61M-qWNMP7LCHPYd5nHyRU#4rcQO-R$hx4rVk zk!)=zfPnnn0!Sn|u$~uW2z;C=ADxs2qvz!3XM^$Zo{A1X(TJYpKjUY71pdT;FR1`ax0@%K^N-=R z5;x%lz^|3JN|jfH*av?QZRotZLweb@p`OgoW^Bx6;BF5q{ z6xC{!?j-I`dXwIyU(he0x3U$CxXBj*Fbtd6irX}afm*~@yv4^dY=X3_OZr~Ys+5S? zCFS}d(I>PCWjvl>=-^Us_#4iGsnX&3smG2$@~Nqo5J6idaK9 z1g%=Th@Otdt0p4qbGd{*ty@H24L*sEY}jA?VU;dF3pwR9O?KVSEh1Q6PHK{9Q*9%r zv+8I>V~8pGbXuWPCnK^6g<6|TO(j<~2~}3DQZ8i_teTWkrMGm5a+|JgCrS9G#C#SH7p(!s|ZG9XE6?DFr4U! zfdCju00I!A03ZMW0Z9PPdCELcdO<=|hJ9GJF$~2veyKGPlQx$XYNS3x5UJokSV$=N zaJ@ISN+J^Ym>pf;cEyz8N6{5*r3XB^)f$G8@}INMz_8Le(UsuCJLn(S_ii(w_LN-$ z=nj8jFy*0o(Od?h?Z=@V49o2SZ90U8m%hLzg96G=Pmy-A7T(lAb{!USrZi|s$HMmt zX^{rtOx(Tz4&eeR)y9f$(VGRXZgH_N?TSO`yqJL*^?iW*8s?}1X+ z9p`vQ9eF`X_l|Py*+WqdJ@ddbH3t<{Ca@yJEpjD6byZ;I)yB&lw7h#7g zC_z!GbXSs3Bm>6f0+Jef?L^9h5n&`fN@#?LQ4TMhR1Y2iXA~?jt}9L@TYm=+Pb%t} zIfR@$Y`QOWrP4H1Nv1~fnA6U;a~V<1X{FNzc=P(2ub*Fla4Cxtm7SJ`V&?qURl-l4 zu+-U5SWNlp@RAGLMnM3@$f zl3Ns>1j;<&&i4{%LkbfKl!MNGhk)9%xI9O=@Jn+zu~D|TX3>K1ovw^0f5yfJmqwpp z8R$-iOobBP_9oTcXK?k}Hsmmpg5_edFo^=9^%utnywTYkfiu za=)xH>aF{&I*hW4#zTP5tZ64_+lg4(zoTSywQA)O}bPk)ij}lqkb({Fam_VjAa5~>zq#$IRT%l;R&#KJV^q1%3!ey3bO#jc8b_g?aqUWK+LID zIanjS4*i>g1>pWW7q@IBTqfIj@!biNW&6*tUXX32ty?O~<;HYf`h>ukE&>EU5zs+E z9eULB7H6~xeBmWYg@Sef<3M7JT+G5ii8&Q(<%O*?BfawHx0MGG-Nxi(tcGGmf-f4g zG62;^@I%liGjbzG_JHj(3zvNW3jicqk6yc2p4m3&7V3_b?Xl`5BMCu6l4*(|bP^1X zV@LS{28fVpdfeEA-(V@#L#a|tbuhbiWFL4F zBLxwr*&*mG;eT4+-vLh?Q7=?GyB*9)eLTp&lf0x!x!)YMzhLwX?W+X0tdY`*%>jNFf+2QGo499| zF?r&e&`F5!NNNR5!1;k|pGaX3HT2{AXXyed57fmV+_xiIn1bi_<#MBeFuW)#dBOSU zd(f2iG%@Cw?ZVIrqFw}GC?goE_W!zff*}D4q_q?Q@(~txh8zK<2wO|IeSNQ`T^4%i zzBT>#Rs|xa-5x$-Er0_5J7qyO41i76(*ef88Z0>*2L9ZHvz_1F2W=0iX% zwP#!@eX>%JWw9_9e?ON z8MXyTMrgpZKBbqFqW~ppGwll{Vsa#)V~;SQ?#>m5L)|LBp;Kjtf4|~j3{jvHd@*5^ znzEXB&xU_b#)4R8%1A~Y;#}a~Tni_047YL%V6kD#e^DLk$PA54oJZ;o6ITVeUL4o>h#sI=Wwzocj5V1nq)8(sw zpAxpE|7{N3_RqmCzdOI-$V6aS=6rOgZyrQvuk6lRjo8OYzA zER0{1bW{|l@rl?4E20ie1|c&^f0*Kq0VAxxr2RC1ZdQjG3X9Q?;eQ#?e3HqO<#=2} z)uF~K!~4O#oiMcn$)QIDv+Vlz@mc^AzS~61g{rs*PVu;oK)*fxk642ow!ff{Ld2z^ z!n~+mVbpJu41X+XHAQyF;3s_2)ryrpy3yQX-}*-Ds)*mJPMFfZz&Yw^F45ktMy zLf-1HmJ6B_Uc`e4>A_P?s@u%NCN{&8^`U-~Oi4PzcNdOwPr&8BI~mM;`-8v>vxQ%k zb`=>-&x^-?+my1ljBt$Qx&Q}oM!E!toU>{PgZ6sgmDm*_?JgGK3(`E~uOismgRrOs zG;pSHBHpcUw&D=(KzDk{vxu*Wdn|tN3pfuszHurPCF@zhe}R%PQqvx zbnlIS3oU0b{->-g`X1e<89h>XD({5_6glJ^DgGIVJ^73kwLiNseFbZ$ zZW6rW%zg)6$aAuZ>2(Fr%+3)PD_cLxZe_O+)$X+XRMM+yB^>o9fHeUXyk=w##XC4Y zBeYeN38ozSWF)_FAgzQtYxrL3Ya{3}N=CSh25;=)O1_tTXGWIx7RaEei5$KVgGVnU z|6>F{EtsTW&8&rF;jBh9B*Gwh98ZR(Qut+&5xtm2owWCv#N_b6i)Na3s=E%lso(Xqig5_ zOof4^Z9qcBxeU-(VhqY!NOQE|(86+gQdSgOe)49A=z~x#k1j3yf*n-l-Hzv>90D$3 zR5oDTF=^X7`N{K?kuD<9m3x)|IZgCSykPcQ`aIrX2u-7Bia$c<`Xmz6$;dq=In_CU zR*_Jjd22;D5+F1tr|b40Gj6LaqLP+WuxzvdO{`V+{IVSLe|g`MWM#4jlq{Sgsa2vA z5j=tASPxyy4l>A#QbV!a0-ya(){*Tmmh`x71nYYLbE_&3bmRSovnopSK zRc{2c+MoU?SOFiAi5qLD%i%0OX{^Q3aX!oEPd`Sv4_jS}bnxs!=&|*hpW=S64S$su z=`ErTz>(9~#Mkna~f`zp@<2ePwc3 z`O-Dd+1IxC^Qd6PeFUmgDRoa=M;o`x#?o!i20DEN$y&$0A6xc^TsqE-Yyg_-u@h-J zj(QmJT$0|Hi**Fe;J(9~wBJD{-;08yvELdinH(dGy6BWhBKV}^)1gZYbak#mHiC=k z_LpnB76+1}^@>ntiDzJq6po-il0oz!p2%Hh!1wIt&qHk!K;ot}CBQT8G}hzI^aPc~ zyt{ajUFfHtFWz!5mI0QMnigtwCJrPOO7 zcU)w19V1O%?GT3p>K1T-6!~iyQBefRaf#SARDci_21igLVxV1x(TZVo&Yz>bZaVLV z<0gmo!WoYwtEL=TuBPn2YOlV=Bs@n^V1;1Fe4=#f)}g-wEBhO~9OjlK_gEk7_1}UER*=D$}n6Rb4WkiXmV#9gho}l$}D+xc3Eq3#Y!LF_(+?)(%UdK zrE;HFq&oHNhG76=KpmPOe{sNkWmL+1YW0?ykd$0mnT%RKtoW)~&qzmLRm;&Vzi}r9 zvBH*O)LyNq&N)sr>r69o#Qar)X_c6OQ^HFWQ{rt2V*R^MBtAsb4`Ir$8Q|_ov7T=j zLl8amvhe+S{ume1JFrzDFdB{YT$0u|+R|_z#CA7liF4&FSO?z3!ZyS`n>l9E^QR*? z4hg#3?rGy$9-Z1RS2C%8-TtqY_uD|-nExdMvWn4xUw&hPdX+m$gv>k@G?Uj)qOCA1 z4R)lt$)C`*`?4KOY~v_dy#BlhMN&p@mLQI z1Z>|9o+9H8u^EKH@)#85C=C_=E>gk|jJ7+AS#mDzD=vP?$LP%i9^NgQeMb223r-t! z(_qDX61Gv-pjvbJSAlDR=^Aty`D}#yf^GE{NPl9<8v&JX%_9|@VXV{Ziqr>NgDLOd zmBd8S=dGUu;6wrQi1AaRD0z6*XedT-jS(thO*x$37N_1R^E(^FV7|Lt=NFr#lN|2e zbTi!O>26AckSvY|Wv`eGaagF{k2KL#=3|lc!OGl#3}!1LrvIYI1kLbIqalUB#&s=& mn3(VG%Z^w$qqEz64HQzq`xfT&YZ$?#qAoy=_lwf!=a;@HUUNJE literal 0 HcmV?d00001 diff --git a/tests/test_debug_info.py b/tests/test_debug_info.py new file mode 100644 index 000000000..76849c960 --- /dev/null +++ b/tests/test_debug_info.py @@ -0,0 +1,2671 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# SPDX-License-Identifier: LGPL-2.1-or-later + + +import binascii +import contextlib +import http.server +import os +import os.path +from pathlib import Path +import re +import shutil +import socket +import socketserver +import tempfile +import threading +import unittest +import unittest.mock + +from _drgn_util.elf import ET, PT, SHF, SHT +from drgn import ( + MainModule, + MissingDebugInfoError, + ModuleFileStatus, + Program, + SharedLibraryModule, + SupplementaryFileKind, + VdsoModule, +) +from tests import TestCase, modifyenv +from tests.dwarfwriter import compile_dwarf +from tests.elfwriter import ElfSection, create_elf_file +from tests.resources import get_resource + + +def gnu_debuglink_section(path, crc): + path = os.fsencode(path) + return ElfSection( + name=".gnu_debuglink", + sh_type=SHT.PROGBITS, + data=path + bytes(4 - len(path) % 4) + crc.to_bytes(4, "little"), + ) + + +def gnu_debugaltlink_section(path, build_id): + return ElfSection( + name=".gnu_debugaltlink", + sh_type=SHT.PROGBITS, + data=os.fsencode(path) + b"\0" + build_id, + ) + + +ALLOCATED_SECTION = ElfSection( + name=".bss", + sh_type=SHT.PROGBITS, + sh_flags=SHF.ALLOC, + p_type=PT.LOAD, + vaddr=0x10000000, + memsz=0x1000, +) + + +@contextlib.contextmanager +def NamedTemporaryElfFile(*, loadable=True, debug=True, build_id=None, sections=()): + if loadable: + sections = (ALLOCATED_SECTION,) + sections + with tempfile.NamedTemporaryFile() as f: + if debug: + f.write(compile_dwarf((), sections=sections, build_id=build_id)) + else: + f.write(create_elf_file(ET.EXEC, sections=sections, build_id=build_id)) + f.flush() + yield f + + +class TestModuleTryFile(TestCase): + def setUp(self): + self.prog = Program() + self.prog.set_enabled_debug_info_finders([]) + + def test_want_both(self): + module = self.prog.extra_module("/foo/bar", create=True)[0] + with NamedTemporaryElfFile() as f: + module.try_file(f.name) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.loaded_file_path, f.name) + self.assertEqual(module.debug_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.debug_file_path, f.name) + + for status in set(ModuleFileStatus) - {ModuleFileStatus.HAVE}: + for file in ("loaded", "debug"): + with self.subTest(file=file): + self.assertEqual(getattr(module, f"wants_{file}_file")(), False) + # Test that we can't unset the file once it's set. + status_attr = file + "_file_status" + with self.subTest(from_=ModuleFileStatus.HAVE, to=status): + self.assertRaises( + ValueError, setattr, module, status_attr, status + ) + self.assertEqual( + getattr(module, status_attr), ModuleFileStatus.HAVE + ) + + def test_want_both_not_loadable(self): + module = self.prog.extra_module("/foo/bar", create=True)[0] + with NamedTemporaryElfFile(loadable=False) as f: + module.try_file(f.name) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.WANT) + self.assertIsNone(module.loaded_file_path) + self.assertEqual(module.debug_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.debug_file_path, f.name) + + def test_want_both_no_debug(self): + module = self.prog.extra_module("/foo/bar", create=True)[0] + with NamedTemporaryElfFile(debug=False) as f: + module.try_file(f.name) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.loaded_file_path, f.name) + self.assertEqual(module.debug_file_status, ModuleFileStatus.WANT) + self.assertIsNone(module.debug_file_path) + + def test_want_both_is_neither(self): + module = self.prog.extra_module("/foo/bar", create=True)[0] + with NamedTemporaryElfFile(loadable=False, debug=False) as f: + module.try_file(f.name) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.WANT) + self.assertEqual(module.debug_file_status, ModuleFileStatus.WANT) + self.assertIsNone(module.loaded_file_path) + self.assertIsNone(module.debug_file_path) + + def test_only_want_loaded(self): + module = self.prog.extra_module("/foo/bar", create=True)[0] + module.debug_file_status = ModuleFileStatus.DONT_WANT + with NamedTemporaryElfFile() as f: + module.try_file(f.name) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.loaded_file_path, f.name) + self.assertEqual(module.debug_file_status, ModuleFileStatus.DONT_WANT) + self.assertIsNone(module.debug_file_path) + + def test_only_want_loaded_not_loadable(self): + module = self.prog.extra_module("/foo/bar", create=True)[0] + module.debug_file_status = ModuleFileStatus.DONT_WANT + with NamedTemporaryElfFile(loadable=False) as f: + module.try_file(f.name) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.WANT) + self.assertIsNone(module.loaded_file_path) + self.assertEqual(module.debug_file_status, ModuleFileStatus.DONT_WANT) + self.assertIsNone(module.debug_file_path) + + def test_only_want_loaded_no_debug(self): + module = self.prog.extra_module("/foo/bar", create=True)[0] + module.debug_file_status = ModuleFileStatus.DONT_WANT + with NamedTemporaryElfFile(debug=False) as f: + module.try_file(f.name) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.loaded_file_path, f.name) + self.assertEqual(module.debug_file_status, ModuleFileStatus.DONT_WANT) + self.assertIsNone(module.debug_file_path) + + def test_only_want_loaded_is_neither(self): + module = self.prog.extra_module("/foo/bar", create=True)[0] + module.debug_file_status = ModuleFileStatus.DONT_WANT + with NamedTemporaryElfFile(loadable=False, debug=False) as f: + module.try_file(f.name) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.WANT) + self.assertIsNone(module.loaded_file_path) + self.assertEqual(module.debug_file_status, ModuleFileStatus.DONT_WANT) + self.assertIsNone(module.debug_file_path) + + def test_only_want_debug(self): + module = self.prog.extra_module("/foo/bar", create=True)[0] + module.loaded_file_status = ModuleFileStatus.DONT_WANT + with NamedTemporaryElfFile() as f: + module.try_file(f.name) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.DONT_WANT) + self.assertIsNone(module.loaded_file_path) + self.assertEqual(module.debug_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.debug_file_path, f.name) + + def test_only_want_debug_not_loadable(self): + module = self.prog.extra_module("/foo/bar", create=True)[0] + module.loaded_file_status = ModuleFileStatus.DONT_WANT + with NamedTemporaryElfFile(loadable=False) as f: + module.try_file(f.name) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.DONT_WANT) + self.assertIsNone(module.loaded_file_path) + self.assertEqual(module.debug_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.debug_file_path, f.name) + + def test_only_want_debug_no_debug(self): + module = self.prog.extra_module("/foo/bar", create=True)[0] + module.loaded_file_status = ModuleFileStatus.DONT_WANT + with NamedTemporaryElfFile(debug=False) as f: + module.try_file(f.name) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.DONT_WANT) + self.assertIsNone(module.loaded_file_path) + self.assertEqual(module.debug_file_status, ModuleFileStatus.WANT) + self.assertIsNone(module.debug_file_path) + + def test_only_want_debug_is_neither(self): + module = self.prog.extra_module("/foo/bar", create=True)[0] + module.loaded_file_status = ModuleFileStatus.DONT_WANT + with NamedTemporaryElfFile(loadable=False, debug=False) as f: + module.try_file(f.name) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.DONT_WANT) + self.assertIsNone(module.loaded_file_path) + self.assertEqual(module.debug_file_status, ModuleFileStatus.WANT) + self.assertIsNone(module.debug_file_path) + + def test_want_neither(self): + module = self.prog.extra_module("/foo/bar", create=True)[0] + module.loaded_file_status = ModuleFileStatus.DONT_WANT + module.debug_file_status = ModuleFileStatus.DONT_WANT + with NamedTemporaryElfFile() as f: + module.try_file(f.name) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.DONT_WANT) + self.assertIsNone(module.loaded_file_path) + self.assertEqual(module.debug_file_status, ModuleFileStatus.DONT_WANT) + self.assertIsNone(module.debug_file_path) + + def test_separate_files_loaded_first(self): + module = self.prog.extra_module("/foo/bar", create=True)[0] + with NamedTemporaryElfFile(debug=False) as f1: + module.try_file(f1.name) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.loaded_file_path, f1.name) + self.assertEqual(module.debug_file_status, ModuleFileStatus.WANT) + self.assertIsNone(module.debug_file_path) + + with NamedTemporaryElfFile(loadable=False) as f2: + module.try_file(f2.name) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.loaded_file_path, f1.name) + self.assertEqual(module.debug_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.debug_file_path, f2.name) + + def test_separate_files_debug_first(self): + module = self.prog.extra_module("/foo/bar", create=True)[0] + with NamedTemporaryElfFile(loadable=False) as f1: + module.try_file(f1.name) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.WANT) + self.assertIsNone(module.loaded_file_path) + self.assertEqual(module.debug_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.debug_file_path, f1.name) + + with NamedTemporaryElfFile(debug=False) as f2: + module.try_file(f2.name) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.loaded_file_path, f2.name) + self.assertEqual(module.debug_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.debug_file_path, f1.name) + + def test_loadable_then_both(self): + module = self.prog.extra_module("/foo/bar", create=True)[0] + with NamedTemporaryElfFile(debug=False) as f1: + module.try_file(f1.name) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.loaded_file_path, f1.name) + self.assertEqual(module.debug_file_status, ModuleFileStatus.WANT) + self.assertIsNone(module.debug_file_path) + + with NamedTemporaryElfFile() as f2: + module.try_file(f2.name) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.loaded_file_path, f1.name) + self.assertEqual(module.debug_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.debug_file_path, f2.name) + + def test_debug_then_both(self): + module = self.prog.extra_module("/foo/bar", create=True)[0] + with NamedTemporaryElfFile(loadable=False) as f1: + module.try_file(f1.name) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.WANT) + self.assertIsNone(module.loaded_file_path) + self.assertEqual(module.debug_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.debug_file_path, f1.name) + + with NamedTemporaryElfFile() as f2: + module.try_file(f2.name) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.loaded_file_path, f2.name) + self.assertEqual(module.debug_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.debug_file_path, f1.name) + + def test_no_build_id_force(self): + module = self.prog.extra_module("/foo/bar", create=True)[0] + with NamedTemporaryElfFile() as f: + module.try_file(f.name, force=True) + self.assertEqual(module.debug_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.debug_file_path, f.name) + + def test_no_build_id_file_has_build_id(self): + module = self.prog.extra_module("/foo/bar", create=True)[0] + with NamedTemporaryElfFile(build_id=b"\x01\x23\x45\x67\x89\xab\xcd\xef") as f: + module.try_file(f.name) + self.assertEqual(module.debug_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.debug_file_path, f.name) + self.assertEqual(module.build_id, b"\x01\x23\x45\x67\x89\xab\xcd\xef") + + def test_no_build_id_file_has_build_id_force(self): + module = self.prog.extra_module("/foo/bar", create=True)[0] + with NamedTemporaryElfFile(build_id=b"\x01\x23\x45\x67\x89\xab\xcd\xef") as f: + module.try_file(f.name, force=True) + self.assertEqual(module.debug_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.debug_file_path, f.name) + self.assertEqual(module.build_id, b"\x01\x23\x45\x67\x89\xab\xcd\xef") + + def test_build_id_match(self): + module = self.prog.extra_module("/foo/bar", create=True)[0] + module.build_id = b"\x01\x23\x45\x67\x89\xab\xcd\xef" + with NamedTemporaryElfFile(build_id=b"\x01\x23\x45\x67\x89\xab\xcd\xef") as f: + module.try_file(f.name) + self.assertEqual(module.debug_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.debug_file_path, f.name) + self.assertEqual(module.build_id, b"\x01\x23\x45\x67\x89\xab\xcd\xef") + + def test_build_id_match_force(self): + module = self.prog.extra_module("/foo/bar", create=True)[0] + module.build_id = b"\x01\x23\x45\x67\x89\xab\xcd\xef" + with NamedTemporaryElfFile(build_id=b"\x01\x23\x45\x67\x89\xab\xcd\xef") as f: + module.try_file(f.name, force=True) + self.assertEqual(module.debug_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.debug_file_path, f.name) + self.assertEqual(module.build_id, b"\x01\x23\x45\x67\x89\xab\xcd\xef") + + def test_build_id_mismatch(self): + module = self.prog.extra_module("/foo/bar", create=True)[0] + module.build_id = b"\x01\x23\x45\x67\x89\xab\xcd\xef" + with NamedTemporaryElfFile(build_id=b"\xff\xff\xff\xff") as f: + module.try_file(f.name) + self.assertEqual(module.debug_file_status, ModuleFileStatus.WANT) + self.assertIsNone(module.debug_file_path) + self.assertEqual(module.build_id, b"\x01\x23\x45\x67\x89\xab\xcd\xef") + + def test_build_id_mismatch_force(self): + module = self.prog.extra_module("/foo/bar", create=True)[0] + module.build_id = b"\x01\x23\x45\x67\x89\xab\xcd\xef" + with NamedTemporaryElfFile(build_id=b"\xff\xff\xff\xff") as f: + module.try_file(f.name, force=True) + self.assertEqual(module.debug_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.debug_file_path, f.name) + self.assertEqual(module.build_id, b"\x01\x23\x45\x67\x89\xab\xcd\xef") + + def test_build_id_missing(self): + module = self.prog.extra_module("/foo/bar", create=True)[0] + module.build_id = b"\x01\x23\x45\x67\x89\xab\xcd\xef" + with NamedTemporaryElfFile() as f: + module.try_file(f.name) + self.assertEqual(module.debug_file_status, ModuleFileStatus.WANT) + self.assertIsNone(module.debug_file_path) + self.assertEqual(module.build_id, b"\x01\x23\x45\x67\x89\xab\xcd\xef") + + def test_build_id_missing_force(self): + module = self.prog.extra_module("/foo/bar", create=True)[0] + module.build_id = b"\x01\x23\x45\x67\x89\xab\xcd\xef" + with NamedTemporaryElfFile() as f: + module.try_file(f.name, force=True) + self.assertEqual(module.debug_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.debug_file_path, f.name) + self.assertEqual(module.build_id, b"\x01\x23\x45\x67\x89\xab\xcd\xef") + + def test_gnu_debugaltlink(self): + build_id = b"\x01\x23\x45\x67\x89\xab\xcd\xef" + alt_build_id = b"\xfe\xdc\xba\x98\x76\x54\x32\x10" + + with tempfile.TemporaryDirectory( + prefix="bin-" + ) as bin_dir, tempfile.TemporaryDirectory(prefix="debug-") as debug_dir: + bin_dir = Path(bin_dir) + debug_dir = Path(debug_dir) + + alt_path = debug_dir / "alt.debug" + alt_path.write_bytes(compile_dwarf((), build_id=alt_build_id)) + + binary_path = bin_dir / "binary" + binary_path.write_bytes( + compile_dwarf( + (), + sections=( + ALLOCATED_SECTION, + gnu_debugaltlink_section(alt_path, alt_build_id), + ), + build_id=build_id, + ) + ) + + module = self.prog.extra_module(bin_dir / "binary", create=True)[0] + module.build_id = build_id + + self.assertEqual(module.debug_file_status, ModuleFileStatus.WANT) + self.assertRaises(ValueError, module.wanted_supplementary_debug_file) + + module.try_file(binary_path) + self.assertEqual( + module.debug_file_status, ModuleFileStatus.WANT_SUPPLEMENTARY + ) + self.assertEqual(module.wants_debug_file(), True) + self.assertIsNone(module.debug_file_path) + self.assertIsNone(module.supplementary_debug_file_kind) + self.assertIsNone(module.supplementary_debug_file_path) + self.assertEqual( + module.wanted_supplementary_debug_file(), + ( + SupplementaryFileKind.GNU_DEBUGALTLINK, + str(binary_path), + str(alt_path), + alt_build_id, + ), + ) + + with self.assertRaises(ValueError): + module.debug_file_status = ModuleFileStatus.HAVE + self.assertEqual( + module.debug_file_status, ModuleFileStatus.WANT_SUPPLEMENTARY + ) + module.debug_file_status = ModuleFileStatus.WANT_SUPPLEMENTARY + self.assertEqual( + module.debug_file_status, ModuleFileStatus.WANT_SUPPLEMENTARY + ) + + module.try_file(alt_path) + self.assertEqual(module.debug_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.debug_file_path, str(binary_path)) + self.assertEqual( + module.supplementary_debug_file_kind, + SupplementaryFileKind.GNU_DEBUGALTLINK, + ) + self.assertEqual(module.supplementary_debug_file_path, str(alt_path)) + self.assertRaises(ValueError, module.wanted_supplementary_debug_file) + + def test_gnu_debugaltlink_build_id_mismatch(self): + build_id = b"\x01\x23\x45\x67\x89\xab\xcd\xef" + alt_build_id = b"\xfe\xdc\xba\x98\x76\x54\x32\x10" + + with tempfile.TemporaryDirectory( + prefix="bin-" + ) as bin_dir, tempfile.TemporaryDirectory(prefix="debug-") as debug_dir: + bin_dir = Path(bin_dir) + debug_dir = Path(debug_dir) + + alt_path = debug_dir / "alt.debug" + alt_path.write_bytes(compile_dwarf((), build_id=alt_build_id[::-1])) + + binary_path = bin_dir / "binary" + binary_path.write_bytes( + compile_dwarf( + (), + sections=( + ALLOCATED_SECTION, + gnu_debugaltlink_section(alt_path, alt_build_id), + ), + build_id=build_id, + ) + ) + + module = self.prog.extra_module(bin_dir / "binary", create=True)[0] + module.build_id = build_id + + self.assertEqual(module.debug_file_status, ModuleFileStatus.WANT) + self.assertRaises(ValueError, module.wanted_supplementary_debug_file) + + module.try_file(binary_path) + self.assertEqual( + module.debug_file_status, ModuleFileStatus.WANT_SUPPLEMENTARY + ) + self.assertIsNone(module.debug_file_path) + self.assertIsNone(module.supplementary_debug_file_kind) + self.assertIsNone(module.supplementary_debug_file_path) + self.assertEqual( + module.wanted_supplementary_debug_file(), + ( + SupplementaryFileKind.GNU_DEBUGALTLINK, + str(binary_path), + str(alt_path), + alt_build_id, + ), + ) + + module.try_file(alt_path) + self.assertEqual( + module.debug_file_status, ModuleFileStatus.WANT_SUPPLEMENTARY + ) + self.assertIsNone(module.debug_file_path) + self.assertIsNone(module.supplementary_debug_file_kind) + self.assertIsNone(module.supplementary_debug_file_path) + self.assertEqual( + module.wanted_supplementary_debug_file(), + ( + SupplementaryFileKind.GNU_DEBUGALTLINK, + str(binary_path), + str(alt_path), + alt_build_id, + ), + ) + + def test_gnu_debugaltlink_then_both(self): + build_id = b"\x01\x23\x45\x67\x89\xab\xcd\xef" + alt_build_id = b"\xfe\xdc\xba\x98\x76\x54\x32\x10" + + with tempfile.TemporaryDirectory( + prefix="bin-" + ) as bin_dir, tempfile.TemporaryDirectory(prefix="debug-") as debug_dir: + bin_dir = Path(bin_dir) + debug_dir = Path(debug_dir) + + alt_path = debug_dir / "alt.debug" + alt_path.write_bytes(compile_dwarf((), build_id=alt_build_id)) + + module = self.prog.extra_module(bin_dir / "binary", create=True)[0] + module.build_id = build_id + with NamedTemporaryElfFile( + sections=(gnu_debugaltlink_section(alt_path, alt_build_id),), + build_id=build_id, + ) as f1: + module.try_file(f1.name) + self.assertEqual( + module.debug_file_status, ModuleFileStatus.WANT_SUPPLEMENTARY + ) + + with NamedTemporaryElfFile(build_id=build_id) as f2: + module.try_file(f2.name) + + self.assertEqual(module.loaded_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.loaded_file_path, f1.name) + self.assertEqual(module.debug_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.debug_file_path, f2.name) + + def test_gnu_debugaltlink_cancel(self): + build_id = b"\x01\x23\x45\x67\x89\xab\xcd\xef" + alt_build_id = b"\xfe\xdc\xba\x98\x76\x54\x32\x10" + + with tempfile.TemporaryDirectory( + prefix="bin-" + ) as bin_dir, tempfile.TemporaryDirectory(prefix="debug-") as debug_dir: + bin_dir = Path(bin_dir) + debug_dir = Path(debug_dir) + + alt_path = debug_dir / "alt.debug" + alt_path.write_bytes(compile_dwarf((), build_id=alt_build_id)) + + module = self.prog.extra_module(bin_dir / "binary", create=True)[0] + module.build_id = build_id + with NamedTemporaryElfFile( + sections=(gnu_debugaltlink_section(alt_path, alt_build_id),), + build_id=build_id, + ) as f: + module.try_file(f.name) + self.assertEqual( + module.debug_file_status, ModuleFileStatus.WANT_SUPPLEMENTARY + ) + + module.debug_file_status = ModuleFileStatus.WANT + self.assertEqual(module.debug_file_status, ModuleFileStatus.WANT) + self.assertEqual(module.wants_debug_file(), True) + self.assertRaises(ValueError, module.wanted_supplementary_debug_file) + + def test_extra_module_no_address_range(self): + module = self.prog.extra_module("/foo/bar", create=True)[0] + with NamedTemporaryElfFile() as f: + module.try_file(f.name) + self.assertIsNone(module.address_range) + self.assertEqual(module.loaded_file_bias, 0) + self.assertEqual(module.debug_file_bias, 0) + + def test_extra_module_address_range(self): + module = self.prog.extra_module("/foo/bar", create=True)[0] + module.address_range = (0x40000000, 0x40001000) + with NamedTemporaryElfFile() as f: + module.try_file(f.name) + self.assertEqual(module.address_range, (0x40000000, 0x40001000)) + self.assertEqual(module.loaded_file_bias, 0x30000000) + self.assertEqual(module.debug_file_bias, 0x30000000) + + +class TestLinuxUserspaceCoreDump(TestCase): + def setUp(self): + self.prog = Program() + self.prog.debug_info_path = None + self.prog.set_enabled_debug_info_finders(["standard"]) + + def test_loaded_modules(self): + self.prog.set_core_dump(get_resource("crashme.core")) + + loaded_modules = [] + for module, new in self.prog.loaded_modules(): + self.assertEqual(new, True) + loaded_modules.append(module) + found_modules = [] + + with self.subTest(module="main"): + module = self.prog.main_module() + found_modules.append(module) + self.assertEqual(module.name, "/home/osandov/crashme") + self.assertEqual(module.address_range, (0x400000, 0x404010)) + self.assertEqual( + module.build_id.hex(), "99a6524c4df01fbff9b43a6ead3d8e8e6201568b" + ) + + with self.subTest(module="crashme"): + module = self.prog.shared_library_module( + "/home/osandov/crashme.so", 0x7F6112CACE08 + ) + found_modules.append(module) + self.assertEqual(module.address_range, (0x7F6112CA9000, 0x7F6112CAD010)) + self.assertEqual( + module.build_id.hex(), "7bd58f10e741c3c8fbcf2031aa65f830f933d616" + ) + + with self.subTest(module="libc"): + module = self.prog.shared_library_module("/lib64/libc.so.6", 0x7F6112C94960) + found_modules.append(module) + self.assertEqual(module.address_range, (0x7F6112AAE000, 0x7F6112C9EB70)) + self.assertEqual( + module.build_id.hex(), "77c77fee058b19c6f001cf2cb0371ce3b8341211" + ) + + with self.subTest(module="ld-linux"): + module = self.prog.shared_library_module( + "/lib64/ld-linux-x86-64.so.2", 0x7F6112CEAE68 + ) + found_modules.append(module) + self.assertEqual(module.address_range, (0x7F6112CB6000, 0x7F6112CEC2D8)) + self.assertEqual( + module.build_id.hex(), "91dcd0244204201b616bbf59427771b3751736ce" + ) + + with self.subTest(module="vdso"): + module = self.prog.vdso_module("linux-vdso.so.1", 0x7F6112CB4438) + found_modules.append(module) + self.assertEqual(module.address_range, (0x7F6112CB4000, 0x7F6112CB590F)) + self.assertEqual( + module.build_id.hex(), "fdc3e4d463911345fbc6d9cc432e5ebc276e8e03" + ) + + self.assertCountEqual(loaded_modules, found_modules) + + loaded_modules = [] + for module, new in self.prog.loaded_modules(): + self.assertEqual(new, False) + loaded_modules.append(module) + self.assertCountEqual(loaded_modules, found_modules) + + def _try_vdso_in_core(self, module): + module.debug_file_status = ModuleFileStatus.DONT_WANT + self.prog.load_module_debug_info(module) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.HAVE) + + def test_bias(self): + self.prog.set_core_dump(get_resource("crashme.core")) + + for _ in self.prog.loaded_modules(): + pass + + with self.subTest(module="main"): + module = self.prog.main_module() + module.try_file(get_resource("crashme")) + self.assertEqual(module.loaded_file_bias, 0) + self.assertEqual(module.debug_file_bias, 0) + + with self.subTest(module="crashme"): + module = self.prog.shared_library_module( + "/home/osandov/crashme.so", 0x7F6112CACE08 + ) + module.try_file(get_resource("crashme.so")) + self.assertEqual(module.loaded_file_bias, 0x7F6112CA9000) + self.assertEqual(module.debug_file_bias, 0x7F6112CA9000) + + with self.subTest(module="vdso"): + module = self.prog.vdso_module("linux-vdso.so.1", 0x7F6112CB4438) + self._try_vdso_in_core(module) + self.assertEqual(module.loaded_file_bias, 0x7F6112CB4000) + self.assertIsNone(module.debug_file_bias) + + def test_loaded_modules_pie(self): + self.prog.set_core_dump(get_resource("crashme_pie.core")) + + loaded_modules = [] + for module, new in self.prog.loaded_modules(): + self.assertEqual(new, True) + loaded_modules.append(module) + found_modules = [] + + with self.subTest(module="main"): + module = self.prog.main_module() + found_modules.append(module) + self.assertEqual(module.name, "/home/osandov/crashme_pie") + self.assertEqual(module.address_range, (0x557ED343D000, 0x557ED3441018)) + self.assertEqual( + module.build_id.hex(), "eb4ad7aaded3815ab133a6d7784a2c95a4e52998" + ) + + with self.subTest(module="crashme"): + module = self.prog.shared_library_module( + "/home/osandov/crashme.so", 0x7FAB2C38DE08 + ) + found_modules.append(module) + self.assertEqual(module.address_range, (0x7FAB2C38A000, 0x7FAB2C38E010)) + self.assertEqual( + module.build_id.hex(), "7bd58f10e741c3c8fbcf2031aa65f830f933d616" + ) + + with self.subTest(module="libc"): + module = self.prog.shared_library_module("/lib64/libc.so.6", 0x7FAB2C375960) + found_modules.append(module) + self.assertEqual(module.address_range, (0x7FAB2C18F000, 0x7FAB2C37FB70)) + self.assertEqual( + module.build_id.hex(), "77c77fee058b19c6f001cf2cb0371ce3b8341211" + ) + + with self.subTest(module="ld-linux"): + module = self.prog.shared_library_module( + "/lib64/ld-linux-x86-64.so.2", 0x7FAB2C3CBE68 + ) + found_modules.append(module) + self.assertEqual(module.address_range, (0x7FAB2C397000, 0x7FAB2C3CD2D8)) + self.assertEqual( + module.build_id.hex(), "91dcd0244204201b616bbf59427771b3751736ce" + ) + + with self.subTest(module="vdso"): + module = self.prog.vdso_module("linux-vdso.so.1", 0x7FAB2C395438) + found_modules.append(module) + self.assertEqual(module.address_range, (0x7FAB2C395000, 0x7FAB2C39690F)) + self.assertEqual( + module.build_id.hex(), "fdc3e4d463911345fbc6d9cc432e5ebc276e8e03" + ) + + self.assertCountEqual(loaded_modules, found_modules) + + loaded_modules = [] + for module, new in self.prog.loaded_modules(): + self.assertEqual(new, False) + loaded_modules.append(module) + self.assertCountEqual(loaded_modules, found_modules) + + def test_bias_pie(self): + self.prog.set_core_dump(get_resource("crashme_pie.core")) + + for _ in self.prog.loaded_modules(): + pass + + with self.subTest(module="main"): + module = self.prog.main_module() + module.try_file(get_resource("crashme_pie")) + self.assertEqual(module.loaded_file_bias, 0x557ED343D000) + self.assertEqual(module.debug_file_bias, 0x557ED343D000) + + with self.subTest(module="crashme"): + module = self.prog.shared_library_module( + "/home/osandov/crashme.so", 0x7FAB2C38DE08 + ) + module.try_file(get_resource("crashme.so")) + self.assertEqual(module.loaded_file_bias, 0x7FAB2C38A000) + self.assertEqual(module.debug_file_bias, 0x7FAB2C38A000) + + with self.subTest(module="vdso"): + module = self.prog.vdso_module("linux-vdso.so.1", 0x7FAB2C395438) + self._try_vdso_in_core(module) + self.assertEqual(module.loaded_file_bias, 0x7FAB2C395000) + self.assertIsNone(module.debug_file_bias) + + def test_loaded_modules_static(self): + self.prog.set_core_dump(get_resource("crashme_static.core")) + + loaded_modules = [] + for module, new in self.prog.loaded_modules(): + self.assertEqual(new, True) + loaded_modules.append(module) + found_modules = [] + + with self.subTest(module="main"): + module = self.prog.main_module() + found_modules.append(module) + self.assertEqual(module.name, "/home/osandov/crashme_static") + self.assertEqual(module.address_range, (0x400000, 0x4042B8)) + self.assertEqual( + module.build_id.hex(), "a0b6befad9f0883c52c475ba3cee9c549cd082cf" + ) + + with self.subTest(module="vdso"): + module = self.prog.vdso_module("linux-vdso.so.1", 0x7FBC73A66438) + found_modules.append(module) + self.assertEqual(module.address_range, (0x7FBC73A66000, 0x7FBC73A6790F)) + self.assertEqual( + module.build_id.hex(), "fdc3e4d463911345fbc6d9cc432e5ebc276e8e03" + ) + + self.assertCountEqual(loaded_modules, found_modules) + + loaded_modules = [] + for module, new in self.prog.loaded_modules(): + self.assertEqual(new, False) + loaded_modules.append(module) + self.assertCountEqual(loaded_modules, found_modules) + + def test_bias_static(self): + self.prog.set_core_dump(get_resource("crashme_static.core")) + + for _ in self.prog.loaded_modules(): + pass + + with self.subTest(module="main"): + module = self.prog.main_module() + module.try_file(get_resource("crashme_static")) + self.assertEqual(module.loaded_file_bias, 0x0) + self.assertEqual(module.debug_file_bias, 0x0) + + with self.subTest(module="vdso"): + module = self.prog.vdso_module("linux-vdso.so.1", 0x7FBC73A66438) + self._try_vdso_in_core(module) + self.assertEqual(module.loaded_file_bias, 0x7FBC73A66000) + self.assertIsNone(module.debug_file_bias) + + def test_loaded_modules_static_pie(self): + self.prog.set_core_dump(get_resource("crashme_static_pie.core")) + + loaded_modules = [] + for module, new in self.prog.loaded_modules(): + self.assertEqual(new, True) + loaded_modules.append(module) + found_modules = [] + + with self.subTest(module="main"): + module = self.prog.main_module() + found_modules.append(module) + self.assertEqual(module.name, "/home/osandov/crashme_static_pie") + self.assertEqual(module.address_range, (0x7FD981DC9000, 0x7FD981DCD278)) + self.assertEqual( + module.build_id.hex(), "3e0bc47f80d7e64724e11fc021a251ed0d35bc2c" + ) + + with self.subTest(module="vdso"): + module = self.prog.vdso_module("linux-vdso.so.1", 0x7FD981DC7438) + found_modules.append(module) + self.assertEqual(module.address_range, (0x7FD981DC7000, 0x7FD981DC890F)) + self.assertEqual( + module.build_id.hex(), "fdc3e4d463911345fbc6d9cc432e5ebc276e8e03" + ) + + self.assertCountEqual(loaded_modules, found_modules) + + loaded_modules = [] + for module, new in self.prog.loaded_modules(): + self.assertEqual(new, False) + loaded_modules.append(module) + self.assertCountEqual(loaded_modules, found_modules) + + def test_bias_static_pie(self): + self.prog.set_core_dump(get_resource("crashme_static_pie.core")) + + for _ in self.prog.loaded_modules(): + pass + + with self.subTest(module="main"): + module = self.prog.main_module() + module.try_file(get_resource("crashme_static_pie")) + self.assertEqual(module.loaded_file_bias, 0x7FD981DC9000) + self.assertEqual(module.debug_file_bias, 0x7FD981DC9000) + + with self.subTest(module="vdso"): + module = self.prog.vdso_module("linux-vdso.so.1", 0x7FD981DC7438) + self._try_vdso_in_core(module) + self.assertEqual(module.loaded_file_bias, 0x7FD981DC7000) + self.assertIsNone(module.debug_file_bias) + + def test_loaded_modules_pie_no_headers(self): + self.prog.set_core_dump(get_resource("crashme_pie_no_headers.core")) + + loaded_modules = [] + for module, new in self.prog.loaded_modules(): + self.assertEqual(new, True) + loaded_modules.append(module) + found_modules = [] + + # Without ELF headers saved in the core dump, and without the main ELF + # file, only the main module (with limited information) and vDSO can be + # found. + with self.subTest(module="main"): + module = self.prog.main_module() + found_modules.append(module) + self.assertEqual(module.name, "/home/osandov/crashme_pie") + self.assertIsNone(module.address_range) + self.assertIsNone(module.build_id) + + with self.subTest(module="vdso"): + module = self.prog.vdso_module("linux-vdso.so.1", 0x7F299F607438) + found_modules.append(module) + self.assertEqual(module.address_range, (0x7F299F607000, 0x7F299F60890F)) + self.assertEqual( + module.build_id.hex(), "fdc3e4d463911345fbc6d9cc432e5ebc276e8e03" + ) + + self.assertCountEqual(loaded_modules, found_modules) + + loaded_modules = [] + for module, new in self.prog.loaded_modules(): + self.assertEqual(new, False) + loaded_modules.append(module) + self.assertCountEqual(loaded_modules, found_modules) + + # If we can read the file headers (specifically, the program header + # table and the interpreter path), then we should be able to get all of + # the modules (with limited information). + exe_file = self.enterContext(open(get_resource("crashme_pie"), "rb")) + + def read_headers(address, count, offset, physical): + exe_file.seek(offset) + return exe_file.read(count) + + self.prog.add_memory_segment(0x5623363D6000, 4096, read_headers, False) + + old_loaded_modules = [] + new_loaded_modules = [] + for module, new in self.prog.loaded_modules(): + (new_loaded_modules if new else old_loaded_modules).append(module) + new_found_modules = [] + + with self.subTest(module="main2"): + module = self.prog.main_module() + self.assertIsNone(module.address_range) + self.assertIsNone(module.build_id) + + with self.subTest(module="crashme"): + module = self.prog.shared_library_module( + "/home/osandov/crashme.so", 0x7F299F5FFE08 + ) + new_found_modules.append(module) + self.assertIsNone(module.address_range) + self.assertIsNone(module.build_id) + + with self.subTest(module="libc"): + module = self.prog.shared_library_module("/lib64/libc.so.6", 0x7F299F5E7960) + new_found_modules.append(module) + self.assertIsNone(module.address_range) + self.assertIsNone(module.build_id) + + with self.subTest(module="ld-linux"): + module = self.prog.shared_library_module( + "/lib64/ld-linux-x86-64.so.2", 0x7F299F63DE68 + ) + new_found_modules.append(module) + self.assertIsNone(module.address_range) + self.assertIsNone(module.build_id) + + self.assertCountEqual(old_loaded_modules, loaded_modules) + self.assertCountEqual(new_loaded_modules, new_found_modules) + + +class TestLoadDebugInfo(TestCase): + def setUp(self): + self.prog = Program() + self.prog.set_core_dump(get_resource("crashme.core")) + self.prog.set_enabled_debug_info_finders([]) + self.finder = unittest.mock.Mock() + self.prog.register_debug_info_finder("mock", self.finder, enable_index=0) + + def test_nothing(self): + self.prog.load_debug_info(None, default=False, main=False) + self.assertFalse(list(self.prog.modules())) + self.finder.assert_not_called() + + def test_empty_list(self): + self.prog.load_debug_info([], default=False, main=False) + self.assertFalse(list(self.prog.modules())) + self.finder.assert_not_called() + + def test_no_such_file(self): + with tempfile.TemporaryDirectory() as tmp_dir: + self.prog.load_debug_info([Path(tmp_dir) / "file"]) + self.assertFalse(list(self.prog.modules())) + self.finder.assert_not_called() + + def test_not_elf(self): + with tempfile.NamedTemporaryFile() as f: + f.write(b"hello, world\n") + f.flush() + self.prog.load_debug_info([f.name]) + self.assertFalse(list(self.prog.modules())) + self.finder.assert_not_called() + + def test_no_build_id(self): + with NamedTemporaryElfFile() as f: + self.prog.load_debug_info([f.name]) + self.assertFalse(list(self.prog.modules())) + self.finder.assert_not_called() + + def test_only_main_path(self): + crashme_path = get_resource("crashme") + + self.prog.load_debug_info([crashme_path]) + + # Only the main module should be created. + self.assertEqual(list(self.prog.modules()), [self.prog.main_module()]) + # The provided path should be used for the main module. + self.assertEqual( + self.prog.main_module().loaded_file_path, + str(crashme_path), + ) + self.assertEqual( + self.prog.main_module().debug_file_path, + str(crashme_path), + ) + # Finders shouldn't be called. + self.finder.assert_not_called() + + def test_only_paths(self): + crashme_path = get_resource("crashme") + crashme_so_path = get_resource("crashme.so") + + self.prog.load_debug_info([crashme_path, crashme_so_path]) + + modules = list(self.prog.modules()) + # All loaded modules should be created. + self.assertEqual(len(modules), 5) + # The provided files should be used for their respective modules. + self.assertEqual( + self.prog.main_module().loaded_file_path, + str(crashme_path), + ) + self.assertEqual( + self.prog.main_module().debug_file_path, + str(crashme_path), + ) + crashme_so_module = next( + module for module in modules if module.name == "/home/osandov/crashme.so" + ) + self.assertEqual( + crashme_so_module.loaded_file_path, + str(crashme_so_path), + ) + self.assertEqual( + crashme_so_module.debug_file_path, + str(crashme_so_path), + ) + # The rest should not have a file. + for module in modules: + if module.name not in ("/home/osandov/crashme", "/home/osandov/crashme.so"): + self.assertEqual(module.loaded_file_status, ModuleFileStatus.WANT) + self.assertEqual(module.debug_file_status, ModuleFileStatus.WANT) + # Finders shouldn't be called. + self.finder.assert_not_called() + + def test_main_by_path(self): + crashme_path = get_resource("crashme") + + self.prog.load_debug_info([crashme_path], main=True) + + # Only the main module should be created. + self.assertEqual(list(self.prog.modules()), [self.prog.main_module()]) + # The provided path should be used for the main module. + self.assertEqual( + self.prog.main_module().loaded_file_path, + str(crashme_path), + ) + self.assertEqual( + self.prog.main_module().debug_file_path, + str(crashme_path), + ) + # Finders shouldn't be called. + self.finder.assert_not_called() + + def test_main_by_finder(self): + crashme_path = get_resource("crashme") + + def finder(modules): + for module in modules: + if module.name == "/home/osandov/crashme": + module.try_file(crashme_path) + + self.finder.side_effect = finder + + self.prog.load_debug_info(main=True) + + # Only the main module should be created. + self.assertEqual(list(self.prog.modules()), [self.prog.main_module()]) + # The finder should be called and set the file for the main module. + self.assertEqual( + self.prog.main_module().loaded_file_path, + str(crashme_path), + ) + self.assertEqual( + self.prog.main_module().debug_file_path, + str(crashme_path), + ) + self.finder.assert_called_once_with([self.prog.main_module()]) + + def test_default_by_paths(self): + crashme_path = get_resource("crashme") + crashme_so_path = get_resource("crashme.so") + + self.assertRaises( + MissingDebugInfoError, + self.prog.load_debug_info, + [crashme_path, crashme_so_path], + default=True, + ) + + # All loaded modules should be created. + modules = list(self.prog.modules()) + self.assertEqual(len(modules), 5) + # The provided files should be used for their respective modules. + self.assertEqual( + self.prog.main_module().loaded_file_path, + str(crashme_path), + ) + self.assertEqual( + self.prog.main_module().debug_file_path, + str(crashme_path), + ) + crashme_so_module = next( + module for module in modules if module.name == "/home/osandov/crashme.so" + ) + self.assertEqual( + crashme_so_module.loaded_file_path, + str(crashme_so_path), + ) + self.assertEqual( + crashme_so_module.debug_file_path, + str(crashme_so_path), + ) + # The rest should not have a file. + missing_modules = set() + for module in modules: + if module.name not in ("/home/osandov/crashme", "/home/osandov/crashme.so"): + self.assertEqual(module.loaded_file_status, ModuleFileStatus.WANT) + self.assertEqual(module.debug_file_status, ModuleFileStatus.WANT) + missing_modules.add(module) + self.assertEqual(len(missing_modules), 3) + # The finder should be called for the rest. + self.finder.assert_called_once() + self.assertEqual(set(self.finder.call_args.args[0]), missing_modules) + + def test_default_by_finder(self): + crashme_path = get_resource("crashme") + crashme_so_path = get_resource("crashme.so") + + def finder(modules): + for module in modules: + if module.name == "/home/osandov/crashme": + module.try_file(crashme_path) + elif module.name == "/home/osandov/crashme.so": + module.try_file(crashme_so_path) + + self.finder.side_effect = finder + + self.assertRaises( + MissingDebugInfoError, self.prog.load_debug_info, default=True + ) + + # All loaded modules should be created. + modules = list(self.prog.modules()) + self.assertEqual(len(modules), 5) + # The finder should be called and set the files for the matching + # modules. + self.assertEqual( + self.prog.main_module().loaded_file_path, + str(crashme_path), + ) + self.assertEqual( + self.prog.main_module().debug_file_path, + str(crashme_path), + ) + crashme_so_module = next( + module for module in modules if module.name == "/home/osandov/crashme.so" + ) + self.assertEqual( + crashme_so_module.loaded_file_path, + str(crashme_so_path), + ) + self.assertEqual( + crashme_so_module.debug_file_path, + str(crashme_so_path), + ) + # The rest should not have a file. + for module in modules: + if module.name not in ("/home/osandov/crashme", "/home/osandov/crashme.so"): + self.assertEqual(module.loaded_file_status, ModuleFileStatus.WANT) + self.assertEqual(module.debug_file_status, ModuleFileStatus.WANT) + # The finder should be called for all loaded modules. + self.finder.assert_called_once() + self.assertEqual(set(self.finder.call_args.args[0]), set(modules)) + + def test_main_gnu_debugaltlink_by_path(self): + crashme_dwz_path = get_resource("crashme.dwz") + crashme_alt_path = get_resource("crashme.alt") + + self.prog.load_debug_info([crashme_dwz_path, crashme_alt_path], main=True) + + # Only the main module should be created. + self.assertEqual(list(self.prog.modules()), [self.prog.main_module()]) + # The provided paths should be used for the main module. + self.assertEqual( + self.prog.main_module().loaded_file_path, + str(crashme_dwz_path), + ) + self.assertEqual( + self.prog.main_module().debug_file_path, + str(crashme_dwz_path), + ) + self.assertEqual( + self.prog.main_module().supplementary_debug_file_path, + str(crashme_alt_path), + ) + # Finders shouldn't be called. + self.finder.assert_not_called() + + def test_main_gnu_debugaltlink_by_finder(self): + crashme_dwz_path = get_resource("crashme.dwz") + crashme_alt_path = get_resource("crashme.alt") + + def finder(modules): + for module in modules: + if module.name == "/home/osandov/crashme": + module.try_file(crashme_dwz_path) + module.try_file(crashme_alt_path) + + self.finder.side_effect = finder + + self.prog.load_debug_info(main=True) + + # Only the main module should be created. + self.assertEqual(list(self.prog.modules()), [self.prog.main_module()]) + # The finder should be called and set the files for the main module. + self.assertEqual( + self.prog.main_module().loaded_file_path, + str(crashme_dwz_path), + ) + self.assertEqual( + self.prog.main_module().debug_file_path, + str(crashme_dwz_path), + ) + self.assertEqual( + self.prog.main_module().supplementary_debug_file_path, + str(crashme_alt_path), + ) + self.finder.assert_called_once_with([self.prog.main_module()]) + + def test_main_by_path_gnu_debugaltlink_not_found(self): + crashme_dwz_path = get_resource("crashme.dwz") + + def finder(modules): + for module in modules: + if module.name == "/home/osandov/crashme": + self.assertEqual( + module.debug_file_status, ModuleFileStatus.WANT_SUPPLEMENTARY + ) + + self.finder.side_effect = finder + + self.assertRaises( + MissingDebugInfoError, + self.prog.load_debug_info, + [crashme_dwz_path], + main=True, + ) + + # Only the main module should be created. + self.assertEqual(list(self.prog.modules()), [self.prog.main_module()]) + # The provided path should be used for the loaded file. + self.assertEqual( + self.prog.main_module().loaded_file_path, + str(crashme_dwz_path), + ) + # The finder should be called and fail to find the supplementary file + # for the main module. + self.assertEqual( + self.prog.main_module().debug_file_status, + ModuleFileStatus.WANT_SUPPLEMENTARY, + ) + self.assertEqual( + self.prog.main_module().wanted_supplementary_debug_file()[:3], + ( + SupplementaryFileKind.GNU_DEBUGALTLINK, + str(crashme_dwz_path), + "crashme.alt", + ), + ) + self.finder.assert_called_once_with([self.prog.main_module()]) + + def test_main_by_finder_gnu_debugaltlink_not_found(self): + crashme_dwz_path = get_resource("crashme.dwz") + + def finder(modules): + for module in modules: + if module.name == "/home/osandov/crashme": + self.assertEqual(module.debug_file_status, ModuleFileStatus.WANT) + module.try_file(crashme_dwz_path) + self.assertEqual( + module.debug_file_status, ModuleFileStatus.WANT_SUPPLEMENTARY + ) + + self.finder.side_effect = finder + + self.assertRaises(MissingDebugInfoError, self.prog.load_debug_info, main=True) + + # Only the main module should be created. + self.assertEqual(list(self.prog.modules()), [self.prog.main_module()]) + # The finder should be called and set the loaded file for the main + # module but fail to find the supplementary file. + self.assertEqual( + self.prog.main_module().loaded_file_path, + str(crashme_dwz_path), + ) + self.assertEqual( + self.prog.main_module().debug_file_status, + ModuleFileStatus.WANT_SUPPLEMENTARY, + ) + self.assertEqual( + self.prog.main_module().wanted_supplementary_debug_file()[:3], + ( + SupplementaryFileKind.GNU_DEBUGALTLINK, + str(crashme_dwz_path), + "crashme.alt", + ), + ) + self.finder.assert_called_once_with([self.prog.main_module()]) + + def test_main_by_path_gnu_debugaltlink_by_finder(self): + crashme_dwz_path = get_resource("crashme.dwz") + crashme_alt_path = get_resource("crashme.alt") + + def finder(modules): + for module in modules: + if ( + module.name == "/home/osandov/crashme" + and module.debug_file_status == ModuleFileStatus.WANT_SUPPLEMENTARY + ): + module.try_file(crashme_alt_path) + + self.finder.side_effect = finder + + self.prog.load_debug_info([crashme_dwz_path], main=True) + + # Only the main module should be created. + self.assertEqual(list(self.prog.modules()), [self.prog.main_module()]) + # The provided path should be used for the main module. + self.assertEqual( + self.prog.main_module().loaded_file_path, + str(crashme_dwz_path), + ) + self.assertEqual( + self.prog.main_module().debug_file_path, + str(crashme_dwz_path), + ) + # The finder should be called and set the supplementary file for the + # main module. + self.assertEqual( + self.prog.main_module().supplementary_debug_file_path, + str(crashme_alt_path), + ) + self.finder.assert_called_once_with([self.prog.main_module()]) + + def test_main_by_finder_gnu_debugaltlink_by_path(self): + crashme_dwz_path = get_resource("crashme.dwz") + crashme_alt_path = get_resource("crashme.alt") + + def finder(modules): + for module in modules: + if module.name == "/home/osandov/crashme": + module.try_file(crashme_dwz_path) + + self.finder.side_effect = finder + + self.prog.load_debug_info([crashme_alt_path], main=True) + + # The provided path should be used for the supplementary file for the + # main module. + self.assertEqual( + self.prog.main_module().supplementary_debug_file_path, + str(crashme_alt_path), + ) + # The finder should be called and set the file for the main module. + self.assertEqual( + self.prog.main_module().loaded_file_path, + str(crashme_dwz_path), + ) + self.assertEqual( + self.prog.main_module().debug_file_path, + str(crashme_dwz_path), + ) + self.finder.assert_called_once_with([self.prog.main_module()]) + + def test_main_wants_gnu_debugaltlink_by_path(self): + crashme_dwz_path = get_resource("crashme.dwz") + crashme_alt_path = get_resource("crashme.alt") + + for module, _ in self.prog.loaded_modules(): + if isinstance(module, MainModule): + module.try_file(crashme_dwz_path) + break + self.assertEqual( + self.prog.main_module().loaded_file_path, + str(crashme_dwz_path), + ) + self.assertEqual( + self.prog.main_module().debug_file_status, + ModuleFileStatus.WANT_SUPPLEMENTARY, + ) + + self.prog.load_debug_info([crashme_alt_path], main=True) + + # Only the main module should be created. + self.assertEqual(list(self.prog.modules()), [self.prog.main_module()]) + # The provided path should be used for the supplementary file. + self.assertEqual( + self.prog.main_module().supplementary_debug_file_path, + str(crashme_alt_path), + ) + # Finders shouldn't be called. + self.finder.assert_not_called() + + def test_main_wants_gnu_debugaltlink_by_finder(self): + crashme_dwz_path = get_resource("crashme.dwz") + crashme_alt_path = get_resource("crashme.alt") + + for module, _ in self.prog.loaded_modules(): + if isinstance(module, MainModule): + module.try_file(crashme_dwz_path) + break + self.assertEqual( + self.prog.main_module().loaded_file_path, + str(crashme_dwz_path), + ) + self.assertEqual( + self.prog.main_module().debug_file_status, + ModuleFileStatus.WANT_SUPPLEMENTARY, + ) + + def finder(modules): + for module in modules: + if ( + module.name == "/home/osandov/crashme" + and module.debug_file_status == ModuleFileStatus.WANT_SUPPLEMENTARY + ): + module.try_file(crashme_alt_path) + + self.finder.side_effect = finder + + self.prog.load_debug_info(main=True) + + # Only the main module should be created. + self.assertEqual(list(self.prog.modules()), [self.prog.main_module()]) + # The finder should be called and set the supplementary file for the + # main module. + self.assertEqual( + self.prog.main_module().supplementary_debug_file_path, + str(crashme_alt_path), + ) + self.finder.assert_called_once_with([self.prog.main_module()]) + + def test_main_wants_gnu_debugaltlink_not_found(self): + crashme_dwz_path = get_resource("crashme.dwz") + + for module, _ in self.prog.loaded_modules(): + if isinstance(module, MainModule): + module.try_file(crashme_dwz_path) + break + self.assertEqual( + self.prog.main_module().loaded_file_path, + str(crashme_dwz_path), + ) + self.assertEqual( + self.prog.main_module().debug_file_status, + ModuleFileStatus.WANT_SUPPLEMENTARY, + ) + + self.assertRaises(MissingDebugInfoError, self.prog.load_debug_info, main=True) + + # Only the main module should be created. + self.assertEqual(list(self.prog.modules()), [self.prog.main_module()]) + # The finder should be called and fail to find the supplementary file + # for the main module, but the supplementary file should still be + # wanted. + self.assertEqual( + self.prog.main_module().debug_file_status, + ModuleFileStatus.WANT_SUPPLEMENTARY, + ) + self.finder.assert_called_once_with([self.prog.main_module()]) + + def test_default_gnu_debugaltlink_by_paths(self): + crashme_dwz_path = get_resource("crashme.dwz") + crashme_so_dwz_path = get_resource("crashme.so.dwz") + crashme_alt_path = get_resource("crashme.alt") + + self.assertRaises( + MissingDebugInfoError, + self.prog.load_debug_info, + [crashme_dwz_path, crashme_so_dwz_path, crashme_alt_path], + default=True, + ) + + # All loaded modules should be created. + modules = list(self.prog.modules()) + self.assertEqual(len(modules), 5) + # The provided files should be used for their respective modules. + self.assertEqual( + self.prog.main_module().loaded_file_path, + str(crashme_dwz_path), + ) + self.assertEqual( + self.prog.main_module().debug_file_path, + str(crashme_dwz_path), + ) + self.assertEqual( + self.prog.main_module().supplementary_debug_file_path, + str(crashme_alt_path), + ) + crashme_so_module = next( + module for module in modules if module.name == "/home/osandov/crashme.so" + ) + self.assertEqual( + crashme_so_module.loaded_file_path, + str(crashme_so_dwz_path), + ) + self.assertEqual( + crashme_so_module.debug_file_path, + str(crashme_so_dwz_path), + ) + self.assertEqual( + crashme_so_module.supplementary_debug_file_path, + str(crashme_alt_path), + ) + # The rest should not have a file. + missing_modules = set() + for module in modules: + if module.name not in ("/home/osandov/crashme", "/home/osandov/crashme.so"): + self.assertEqual(module.loaded_file_status, ModuleFileStatus.WANT) + self.assertEqual(module.debug_file_status, ModuleFileStatus.WANT) + missing_modules.add(module) + self.assertEqual(len(missing_modules), 3) + # The finder should be called for the rest. + self.finder.assert_called_once() + self.assertEqual(set(self.finder.call_args.args[0]), missing_modules) + + def test_dont_want(self): + for module, _ in self.prog.loaded_modules(): + if isinstance(module, MainModule): + module.loaded_file_status = ModuleFileStatus.DONT_WANT + module.debug_file_status = ModuleFileStatus.DONT_WANT + break + # DONT_WANT should be reset to WANT. + self.assertRaises(MissingDebugInfoError, self.prog.load_debug_info, main=True) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.WANT) + self.assertEqual(module.debug_file_status, ModuleFileStatus.WANT) + self.finder.assert_called_once_with([self.prog.main_module()]) + + def test_dont_need(self): + for module, _ in self.prog.loaded_modules(): + if isinstance(module, MainModule): + module.loaded_file_status = ModuleFileStatus.DONT_NEED + module.debug_file_status = ModuleFileStatus.DONT_NEED + break + # DONT_NEED should be preserved. + self.prog.load_debug_info(main=True) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.DONT_NEED) + self.assertEqual(module.debug_file_status, ModuleFileStatus.DONT_NEED) + self.finder.assert_not_called() + + def test_unmatched(self): + self.prog.load_debug_info([get_resource("crashme_static")]) + modules = list(self.prog.modules()) + # All loaded modules should be created. + self.assertEqual(len(modules), 5) + # None of them should have files. + for module in modules: + self.assertEqual(module.loaded_file_status, ModuleFileStatus.WANT) + self.assertEqual(module.debug_file_status, ModuleFileStatus.WANT) + self.finder.assert_not_called() + + +class TestLoadDebugInfoCoreNoHeaders(TestCase): + def setUp(self): + self.prog = Program() + self.prog.set_core_dump(get_resource("crashme_pie_no_headers.core")) + self.prog.set_enabled_debug_info_finders([]) + self.finder = unittest.mock.Mock() + self.prog.register_debug_info_finder("mock", self.finder, enable_index=0) + + def test_main_by_finder(self): + crashme_pie_path = get_resource("crashme_pie") + + def finder(modules): + for module in modules: + if module.name == "/home/osandov/crashme_pie": + module.try_file(crashme_pie_path) + + self.finder.side_effect = finder + + self.prog.load_debug_info(main=True) + + # Only the main module should be created. + self.assertEqual(list(self.prog.modules()), [self.prog.main_module()]) + # The finder should be called and set the files, address range, and + # build ID for the main module. + self.assertEqual( + self.prog.main_module().loaded_file_path, + str(crashme_pie_path), + ) + self.assertEqual( + self.prog.main_module().debug_file_path, + str(crashme_pie_path), + ) + self.assertEqual( + self.prog.main_module().address_range, (0x5623363D6000, 0x5623363DA018) + ) + self.assertEqual( + self.prog.main_module().build_id.hex(), + "eb4ad7aaded3815ab133a6d7784a2c95a4e52998", + ) + self.finder.assert_called_once_with([self.prog.main_module()]) + + @unittest.expectedFailure # Issue #291 + def test_default_by_finder(self): + crashme_pie_path = get_resource("crashme_pie") + crashme_so_path = get_resource("crashme.so") + + def finder(modules): + for module in modules: + if module.name == "/home/osandov/crashme_pie": + module.try_file(crashme_pie_path) + elif module.name == "/home/osandov/crashme.so": + module.try_file(crashme_so_path) + else: + module.loaded_file_status = ModuleFileStatus.DONT_NEED + module.debug_file_status = ModuleFileStatus.DONT_NEED + + self.finder.side_effect = finder + + self.prog.load_debug_info(default=True) + + # All loaded modules should be created (except ld-linux.so; see + # tests.test_module.TestLinuxUserspaceCoreDump.test_loaded_modules_pie_no_headers). + self.assertCountEqual( + list(self.prog.modules()), + [ + self.prog.main_module(), + self.prog.vdso_module("linux-vdso.so.1", 0x7F299F607438), + self.prog.shared_library_module( + "/home/osandov/crashme.so", 0x7F299F5FFE08 + ), + self.prog.shared_library_module("/lib64/libc.so.6", 0x7F299F5E7960), + self.prog.shared_library_module( + "/lib64/ld-linux-x86-64.so.2", 0x7F299F63DE68 + ), + ], + ) + # The finder should be called and set the files, address range, and + # build ID for the main and crashme.so modules. + self.assertEqual( + self.prog.main_module().loaded_file_path, + str(crashme_pie_path), + ) + self.assertEqual( + self.prog.main_module().debug_file_path, + str(crashme_pie_path), + ) + self.assertEqual( + self.prog.main_module().address_range, (0x5623363D6000, 0x5623363DA018) + ) + self.assertEqual( + self.prog.main_module().build_id.hex(), + "eb4ad7aaded3815ab133a6d7784a2c95a4e52998", + ) + self.assertEqual( + self.prog.shared_library_module( + "/home/osandov/crashme.so", 0x7F299F5FFE08 + ).loaded_file_path, + str(crashme_so_path), + ) + self.assertEqual( + self.prog.shared_library_module( + "/home/osandov/crashme.so", 0x7F299F5FFE08 + ).debug_file_path, + str(crashme_so_path), + ) + self.assertEqual( + self.prog.shared_library_module( + "/home/osandov/crashme.so", 0x7F299F5FFE08 + ).address_range, + (0x7F299F5FC000, 0x7F299F600010), + ) + self.assertEqual( + self.prog.shared_library_module( + "/home/osandov/crashme.so", 0x7F299F5FFE08 + ).build_id.hex(), + "7bd58f10e741c3c8fbcf2031aa65f830f933d616", + ) + self.finder.assert_called() + + +class TestLoadModuleDebugInfo(TestCase): + def setUp(self): + self.prog = Program() + self.prog.set_enabled_debug_info_finders([]) + self.finder = unittest.mock.Mock() + self.prog.register_debug_info_finder("mock", self.finder, enable_index=0) + + def test_empty(self): + self.prog.load_module_debug_info() + self.finder.assert_not_called() + + def test_multiple(self): + self.prog.load_module_debug_info( + self.prog.extra_module("/foo/bar", create=True)[0], + self.prog.extra_module("/foo/baz", create=True)[0], + ) + self.finder.assert_called_once() + self.assertCountEqual( + self.finder.call_args.args[0], + [ + self.prog.extra_module("/foo/bar"), + self.prog.extra_module("/foo/baz"), + ], + ) + + def test_wrong_program(self): + self.assertRaisesRegex( + ValueError, + "module from wrong program", + self.prog.load_module_debug_info, + self.prog.extra_module("/foo/bar", create=True)[0], + Program().extra_module("/foo/baz", create=True)[0], + ) + + def test_type_error(self): + self.assertRaises( + TypeError, + self.prog.load_module_debug_info, + self.prog.extra_module("/foo/bar", create=True)[0], + None, + ) + + +class TestStandardDebugInfoFinder(TestCase): + def setUp(self): + self.prog = Program() + self.prog.debug_info_path = None + self.prog.set_enabled_debug_info_finders(["standard"]) + + def test_by_module_name(self): + with NamedTemporaryElfFile() as f: + module = self.prog.extra_module(f.name, create=True)[0] + self.prog.load_module_debug_info(module) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.debug_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.loaded_file_path, f.name) + self.assertEqual(module.debug_file_path, f.name) + + def test_by_module_name_with_build_id(self): + build_id = b"\x01\x23\x45\x67\x89\xab\xcd\xef" + + with NamedTemporaryElfFile(build_id=build_id) as f: + module = self.prog.extra_module(f.name, create=True)[0] + module.build_id = build_id + self.prog.load_module_debug_info(module) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.debug_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.loaded_file_path, f.name) + self.assertEqual(module.debug_file_path, f.name) + + def test_by_module_name_missing_build_id(self): + build_id = b"\x01\x23\x45\x67\x89\xab\xcd\xef" + + with NamedTemporaryElfFile() as f: + module = self.prog.extra_module(f.name, create=True)[0] + module.build_id = build_id + self.prog.load_module_debug_info(module) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.WANT) + self.assertEqual(module.debug_file_status, ModuleFileStatus.WANT) + + def test_by_module_name_build_id_mismatch(self): + build_id = b"\x01\x23\x45\x67\x89\xab\xcd\xef" + + with NamedTemporaryElfFile(build_id=build_id[::-1]) as f: + module = self.prog.extra_module(f.name, create=True)[0] + module.build_id = build_id + self.prog.load_module_debug_info(module) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.WANT) + self.assertEqual(module.debug_file_status, ModuleFileStatus.WANT) + + def test_reuse_loaded_file(self): + with NamedTemporaryElfFile() as f: + module = self.prog.extra_module(f.name, create=True)[0] + module.debug_file_status = ModuleFileStatus.DONT_WANT + self.prog.load_module_debug_info(module) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.loaded_file_path, f.name) + self.assertEqual(module.debug_file_status, ModuleFileStatus.DONT_WANT) + + module.debug_file_status = ModuleFileStatus.WANT + self.prog.load_module_debug_info(module) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.loaded_file_path, f.name) + self.assertEqual(module.debug_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.debug_file_path, f.name) + + def test_reuse_debug_file(self): + with NamedTemporaryElfFile() as f: + module = self.prog.extra_module(f.name, create=True)[0] + module.loaded_file_status = ModuleFileStatus.DONT_WANT + self.prog.load_module_debug_info(module) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.DONT_WANT) + self.assertEqual(module.debug_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.debug_file_path, f.name) + + module.loaded_file_status = ModuleFileStatus.WANT + self.prog.load_module_debug_info(module) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.loaded_file_path, f.name) + self.assertEqual(module.debug_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.debug_file_path, f.name) + + def test_reuse_wanted_supplementary_debug_file(self): + alt_build_id = b"\xfe\xdc\xba\x98\x76\x54\x32\x10" + + with NamedTemporaryElfFile( + sections=(gnu_debugaltlink_section("alt.debug", alt_build_id),), + ) as f: + module = self.prog.extra_module(f.name, create=True)[0] + module.loaded_file_status = ModuleFileStatus.DONT_WANT + self.prog.load_module_debug_info(module) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.DONT_WANT) + self.assertEqual(module.debug_file_status, ModuleFileStatus.WANT_SUPPLEMENTARY) + + module.loaded_file_status = ModuleFileStatus.WANT + self.prog.load_module_debug_info(module) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.loaded_file_path, f.name) + self.assertEqual(module.debug_file_status, ModuleFileStatus.WANT_SUPPLEMENTARY) + + def test_vdso_in_core(self): + self.prog.set_core_dump(get_resource("crashme.core")) + for module, _ in self.prog.loaded_modules(): + if isinstance(module, VdsoModule): + break + else: + self.fail("vDSO module not found") + self.prog.load_module_debug_info(module) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.loaded_file_path, "[vdso]") + self.assertEqual(module.debug_file_status, ModuleFileStatus.WANT) + + def test_main_by_proc(self): + self.prog.set_pid(os.getpid()) + for module, _ in self.prog.loaded_modules(): + if isinstance(module, MainModule): + break + else: + self.fail("main module not found") + self.prog.load_module_debug_info(module) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.HAVE) + + def test_vdso_by_proc(self): + self.prog.set_pid(os.getpid()) + for module, _ in self.prog.loaded_modules(): + if isinstance(module, VdsoModule): + break + else: + self.skipTest("vDSO module not found") + self.prog.load_module_debug_info(module) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.loaded_file_path, "[vdso]") + self.assertEqual(module.debug_file_status, ModuleFileStatus.WANT) + + def test_shared_library_by_proc(self): + self.prog.set_pid(os.getpid()) + for module, _ in self.prog.loaded_modules(): + if isinstance(module, SharedLibraryModule): + break + else: + self.skipTest("shared library module not found") + self.prog.load_module_debug_info(module) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.HAVE) + + def test_by_build_id(self): + build_id = b"\x01\x23\x45\x67\x89\xab\xcd\xef" + + with tempfile.TemporaryDirectory( + prefix="bin-" + ) as bin_dir, tempfile.TemporaryDirectory(prefix="debug-") as debug_dir: + bin_dir = Path(bin_dir) + debug_dir = Path(debug_dir) + + build_id_dir = debug_dir / ".build-id" / build_id.hex()[:2] + build_id_dir.mkdir(parents=True) + binary_path = build_id_dir / build_id.hex()[2:] + binary_path.write_bytes(compile_dwarf((), sections=(ALLOCATED_SECTION,))) + + module = self.prog.extra_module(bin_dir / "binary", create=True)[0] + module.build_id = build_id + + self.prog.debug_info_path = ":.debug:" + str(debug_dir) + self.prog.load_module_debug_info(module) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.debug_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.loaded_file_path, str(binary_path)) + self.assertEqual(module.debug_file_path, str(binary_path)) + + def test_by_build_id_separate(self): + build_id = b"\x01\x23\x45\x67\x89\xab\xcd\xef" + + with tempfile.TemporaryDirectory( + prefix="bin-" + ) as bin_dir, tempfile.TemporaryDirectory(prefix="debug-") as debug_dir: + bin_dir = Path(bin_dir) + debug_dir = Path(debug_dir) + + build_id_dir = debug_dir / ".build-id" / build_id.hex()[:2] + build_id_dir.mkdir(parents=True) + loadable_path = build_id_dir / build_id.hex()[2:] + loadable_path.write_bytes( + create_elf_file(ET.EXEC, sections=(ALLOCATED_SECTION,)) + ) + debug_path = build_id_dir / (build_id.hex()[2:] + ".debug") + debug_path.write_bytes(compile_dwarf(())) + + module = self.prog.extra_module(bin_dir / "binary", create=True)[0] + module.build_id = build_id + + self.prog.debug_info_path = ":.debug:" + str(debug_dir) + self.prog.load_module_debug_info(module) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.debug_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.loaded_file_path, str(loadable_path)) + self.assertEqual(module.debug_file_path, str(debug_path)) + + def test_by_build_id_from_loaded(self): + build_id = b"\x01\x23\x45\x67\x89\xab\xcd\xef" + + with tempfile.TemporaryDirectory( + prefix="bin-" + ) as bin_dir, tempfile.TemporaryDirectory(prefix="debug-") as debug_dir: + bin_dir = Path(bin_dir) + debug_dir = Path(debug_dir) + + loadable_path = bin_dir / "binary" + loadable_path.write_bytes( + create_elf_file( + ET.EXEC, sections=(ALLOCATED_SECTION,), build_id=build_id + ) + ) + build_id_dir = debug_dir / ".build-id" / build_id.hex()[:2] + build_id_dir.mkdir(parents=True) + debug_path = build_id_dir / (build_id.hex()[2:] + ".debug") + debug_path.write_bytes(compile_dwarf(())) + + module = self.prog.extra_module(bin_dir / "binary", create=True)[0] + + self.prog.debug_info_path = ":.debug:" + str(debug_dir) + self.prog.load_module_debug_info(module) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.debug_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.loaded_file_path, str(loadable_path)) + self.assertEqual(module.debug_file_path, str(debug_path)) + + def test_by_gnu_debuglink(self): + with tempfile.TemporaryDirectory( + prefix="bin-" + ) as bin_dir, tempfile.TemporaryDirectory(prefix="debug-") as debug_dir: + bin_dir = Path(bin_dir) + debug_dir = Path(debug_dir) + + debug_file_contents = compile_dwarf(()) + crc = binascii.crc32(debug_file_contents) + + loadable_path = bin_dir / "binary" + loadable_path.write_bytes( + create_elf_file( + ET.EXEC, + sections=( + ALLOCATED_SECTION, + gnu_debuglink_section("binary.debug", crc), + ), + ) + ) + + self.prog.debug_info_path = ":.debug:" + str(debug_dir) + for i, debug_path in enumerate( + ( + bin_dir / "binary.debug", + bin_dir / ".debug" / "binary.debug", + debug_dir / bin_dir.relative_to("/") / "binary.debug", + ) + ): + with self.subTest(debug_path=debug_path): + try: + debug_path.parent.mkdir(parents=True, exist_ok=True) + debug_path.write_bytes(debug_file_contents) + + module = self.prog.extra_module( + bin_dir / "binary", i, create=True + )[0] + + self.prog.load_module_debug_info(module) + self.assertEqual( + module.loaded_file_status, ModuleFileStatus.HAVE + ) + self.assertEqual( + module.debug_file_status, ModuleFileStatus.HAVE + ) + self.assertEqual(module.loaded_file_path, str(loadable_path)) + self.assertEqual(module.debug_file_path, str(debug_path)) + finally: + try: + debug_path.unlink() + except FileNotFoundError: + pass + + def test_by_gnu_debuglink_absolute(self): + with tempfile.TemporaryDirectory( + prefix="bin-" + ) as bin_dir, tempfile.TemporaryDirectory(prefix="debug-") as debug_dir: + bin_dir = Path(bin_dir) + debug_dir = Path(debug_dir) + + debug_file_contents = compile_dwarf(()) + crc = binascii.crc32(debug_file_contents) + debug_path = debug_dir / "binary.debug" + + loadable_path = bin_dir / "binary" + loadable_path.write_bytes( + create_elf_file( + ET.EXEC, + sections=( + ALLOCATED_SECTION, + gnu_debuglink_section(debug_path, crc), + ), + ) + ) + + debug_path.parent.mkdir(parents=True, exist_ok=True) + debug_path.write_bytes(debug_file_contents) + + module = self.prog.extra_module(bin_dir / "binary", create=True)[0] + + self.prog.load_module_debug_info(module) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.debug_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.loaded_file_path, str(loadable_path)) + self.assertEqual(module.debug_file_path, str(debug_path)) + + def test_by_gnu_debuglink_crc_mismatch(self): + with tempfile.TemporaryDirectory(prefix="bin-") as bin_dir: + bin_dir = Path(bin_dir) + + debug_file_contents = compile_dwarf(()) + crc = binascii.crc32(debug_file_contents) + + loadable_path = bin_dir / "binary" + loadable_path.write_bytes( + create_elf_file( + ET.EXEC, + sections=( + ALLOCATED_SECTION, + gnu_debuglink_section("binary.debug", crc ^ 1), + ), + ) + ) + + debug_path = bin_dir / "binary.debug" + debug_path.write_bytes(debug_file_contents) + + module = self.prog.extra_module(bin_dir / "binary", create=True)[0] + self.prog.debug_info_path = "" + self.prog.load_module_debug_info(module) + self.assertEqual(module.debug_file_status, ModuleFileStatus.WANT) + + def test_invalid_gnu_debuglink(self): + with tempfile.TemporaryDirectory(prefix="bin-") as bin_dir: + bin_dir = Path(bin_dir) + + loadable_path = bin_dir / "binary" + loadable_path.write_bytes( + create_elf_file( + ET.EXEC, + sections=( + ALLOCATED_SECTION, + ElfSection( + name=".gnu_debuglink", sh_type=SHT.PROGBITS, data=b"foo" + ), + ), + ) + ) + + module = self.prog.extra_module(bin_dir / "binary", create=True)[0] + self.prog.load_module_debug_info(module) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.debug_file_status, ModuleFileStatus.WANT) + self.assertEqual(module.loaded_file_path, str(loadable_path)) + + def test_gnu_debugaltlink_absolute(self): + alt_build_id = b"\xfe\xdc\xba\x98\x76\x54\x32\x10" + + with tempfile.TemporaryDirectory( + prefix="bin-" + ) as bin_dir, tempfile.TemporaryDirectory(prefix="debug-") as debug_dir: + bin_dir = Path(bin_dir) + debug_dir = Path(debug_dir) + + alt_path = debug_dir / "alt.debug" + alt_path.write_bytes(compile_dwarf((), build_id=alt_build_id)) + + binary_path = bin_dir / "binary" + binary_path.write_bytes( + compile_dwarf( + (), + sections=( + ALLOCATED_SECTION, + gnu_debugaltlink_section(alt_path, alt_build_id), + ), + ) + ) + + module = self.prog.extra_module(bin_dir / "binary", create=True)[0] + self.prog.load_module_debug_info(module) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.debug_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.loaded_file_path, str(binary_path)) + self.assertEqual(module.debug_file_path, str(binary_path)) + self.assertEqual(module.supplementary_debug_file_path, str(alt_path)) + + def test_gnu_debugaltlink_not_found(self): + alt_build_id = b"\xfe\xdc\xba\x98\x76\x54\x32\x10" + + with tempfile.TemporaryDirectory( + prefix="bin-" + ) as bin_dir, tempfile.TemporaryDirectory(prefix="debug-") as debug_dir: + bin_dir = Path(bin_dir) + debug_dir = Path(debug_dir) + + binary_path = bin_dir / "binary" + binary_path.write_bytes( + compile_dwarf( + (), + sections=( + ALLOCATED_SECTION, + gnu_debugaltlink_section(debug_dir / "alt.debug", alt_build_id), + ), + ) + ) + + module = self.prog.extra_module(bin_dir / "binary", create=True)[0] + self.prog.load_module_debug_info(module) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.HAVE) + self.assertEqual( + module.debug_file_status, ModuleFileStatus.WANT_SUPPLEMENTARY + ) + self.assertEqual( + module.wanted_supplementary_debug_file(), + ( + SupplementaryFileKind.GNU_DEBUGALTLINK, + str(binary_path), + str(debug_dir / "alt.debug"), + alt_build_id, + ), + ) + self.assertEqual(module.loaded_file_path, str(binary_path)) + + def test_only_gnu_debugaltlink_absolute(self): + alt_build_id = b"\xfe\xdc\xba\x98\x76\x54\x32\x10" + + with tempfile.TemporaryDirectory( + prefix="bin-" + ) as bin_dir, tempfile.TemporaryDirectory(prefix="debug-") as debug_dir: + bin_dir = Path(bin_dir) + debug_dir = Path(debug_dir) + + alt_path = debug_dir / "alt.debug" + alt_path.write_bytes(compile_dwarf((), build_id=alt_build_id)) + + binary_path = bin_dir / "binary" + binary_path.write_bytes( + compile_dwarf( + (), + sections=( + ALLOCATED_SECTION, + gnu_debugaltlink_section(alt_path, alt_build_id), + ), + ) + ) + + module = self.prog.extra_module(bin_dir / "binary", create=True)[0] + module.try_file(binary_path) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.HAVE) + self.assertEqual( + module.debug_file_status, ModuleFileStatus.WANT_SUPPLEMENTARY + ) + self.assertEqual(module.loaded_file_path, str(binary_path)) + + self.prog.load_module_debug_info(module) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.debug_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.debug_file_path, str(binary_path)) + self.assertEqual(module.supplementary_debug_file_path, str(alt_path)) + + def test_only_gnu_debugaltlink_not_found(self): + alt_build_id = b"\xfe\xdc\xba\x98\x76\x54\x32\x10" + + with tempfile.TemporaryDirectory( + prefix="bin-" + ) as bin_dir, tempfile.TemporaryDirectory(prefix="debug-") as debug_dir: + bin_dir = Path(bin_dir) + debug_dir = Path(debug_dir) + + binary_path = bin_dir / "binary" + binary_path.write_bytes( + compile_dwarf( + (), + sections=( + ALLOCATED_SECTION, + gnu_debugaltlink_section(debug_dir / "alt.debug", alt_build_id), + ), + ) + ) + + module = self.prog.extra_module(bin_dir / "binary", create=True)[0] + module.try_file(binary_path) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.HAVE) + self.assertEqual( + module.debug_file_status, ModuleFileStatus.WANT_SUPPLEMENTARY + ) + self.assertEqual(module.loaded_file_path, str(binary_path)) + + self.prog.load_module_debug_info(module) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.HAVE) + self.assertEqual( + module.debug_file_status, ModuleFileStatus.WANT_SUPPLEMENTARY + ) + self.assertEqual( + module.wanted_supplementary_debug_file(), + ( + SupplementaryFileKind.GNU_DEBUGALTLINK, + str(binary_path), + str(debug_dir / "alt.debug"), + alt_build_id, + ), + ) + + def test_gnu_debugaltlink_relative(self): + alt_build_id = b"\xfe\xdc\xba\x98\x76\x54\x32\x10" + + with tempfile.TemporaryDirectory( + prefix="bin-" + ) as bin_dir, tempfile.TemporaryDirectory(prefix="debug-") as debug_dir: + bin_dir = Path(bin_dir) + debug_dir = Path(debug_dir) + + alt_path = debug_dir / "alt.debug" + alt_path.write_bytes(compile_dwarf((), build_id=alt_build_id)) + + binary_path = bin_dir / "binary" + binary_path.write_bytes( + compile_dwarf( + (), + sections=( + ALLOCATED_SECTION, + gnu_debugaltlink_section( + Path(os.path.relpath(alt_path, bin_dir)), alt_build_id + ), + ), + ) + ) + + module = self.prog.extra_module(bin_dir / "binary", create=True)[0] + self.prog.load_module_debug_info(module) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.debug_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.loaded_file_path, str(binary_path)) + self.assertEqual(module.debug_file_path, str(binary_path)) + self.assertEqual(module.supplementary_debug_file_path, str(alt_path)) + + def test_gnu_debugaltlink_debug_directories(self): + alt_build_id = b"\xfe\xdc\xba\x98\x76\x54\x32\x10" + + with tempfile.TemporaryDirectory( + prefix="bin-" + ) as bin_dir, tempfile.TemporaryDirectory(prefix="debug-") as debug_dir: + bin_dir = Path(bin_dir) + debug_dir = Path(debug_dir) + + alt_path = debug_dir / ".dwz/alt.debug" + alt_path.parent.mkdir() + alt_path.write_bytes(compile_dwarf((), build_id=alt_build_id)) + + binary_path = bin_dir / "binary" + + self.prog.debug_info_path = ":.debug:" + str(debug_dir) + for i, debugaltlink in enumerate( + ( + bin_dir / "debug/.dwz/alt.debug", + Path("debug/.dwz/alt.debug"), + ) + ): + with self.subTest(debugaltlink=debugaltlink): + binary_path.write_bytes( + compile_dwarf( + (), + sections=( + ALLOCATED_SECTION, + gnu_debugaltlink_section(debugaltlink, alt_build_id), + ), + ) + ) + + module = self.prog.extra_module(bin_dir / "binary", i, create=True)[ + 0 + ] + self.prog.load_module_debug_info(module) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.debug_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.loaded_file_path, str(binary_path)) + self.assertEqual(module.debug_file_path, str(binary_path)) + self.assertEqual( + module.supplementary_debug_file_path, str(alt_path) + ) + + def test_gnu_debugaltlink_build_id_mismatch(self): + build_id = b"\x01\x23\x45\x67\x89\xab\xcd\xef" + alt_build_id = b"\xfe\xdc\xba\x98\x76\x54\x32\x10" + + with tempfile.TemporaryDirectory( + prefix="bin-" + ) as bin_dir, tempfile.TemporaryDirectory(prefix="debug-") as debug_dir: + bin_dir = Path(bin_dir) + debug_dir = Path(debug_dir) + + alt_path = debug_dir / "alt.debug" + alt_path.write_bytes(compile_dwarf((), build_id=alt_build_id[::-1])) + + binary_path = bin_dir / "binary" + binary_path.write_bytes( + compile_dwarf( + (), + sections=( + ALLOCATED_SECTION, + gnu_debugaltlink_section(alt_path, alt_build_id), + ), + build_id=build_id, + ) + ) + + module = self.prog.extra_module(bin_dir / "binary", create=True)[0] + self.prog.load_module_debug_info(module) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.HAVE) + self.assertEqual( + module.debug_file_status, ModuleFileStatus.WANT_SUPPLEMENTARY + ) + self.assertEqual( + module.wanted_supplementary_debug_file(), + ( + SupplementaryFileKind.GNU_DEBUGALTLINK, + str(binary_path), + str(alt_path), + alt_build_id, + ), + ) + self.assertEqual(module.loaded_file_path, str(binary_path)) + + def test_invalid_gnu_debugaltlink(self): + build_id = b"\x01\x23\x45\x67\x89\xab\xcd\xef" + + with tempfile.TemporaryDirectory(prefix="bin-") as bin_dir: + bin_dir = Path(bin_dir) + + binary_path = bin_dir / "binary" + binary_path.write_bytes( + compile_dwarf( + (), + sections=( + ALLOCATED_SECTION, + ElfSection( + name=".gnu_debugaltlink", + sh_type=SHT.PROGBITS, + data=b"foo", + ), + ), + build_id=build_id, + ) + ) + + module = self.prog.extra_module(bin_dir / "binary", create=True)[0] + self.prog.load_module_debug_info(module) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.debug_file_status, ModuleFileStatus.WANT) + self.assertEqual(module.loaded_file_path, str(binary_path)) + + +class _DebuginfodHTTPHandler(http.server.BaseHTTPRequestHandler): + def do_GET(self): + match = re.fullmatch( + r"/buildid/((?:[0-9a-fA-F][0-9a-fA-F])+)/(executable|debuginfo)", self.path + ) + if not match: + self.send_error(http.HTTPStatus.BAD_REQUEST) + return + + build_id = bytes.fromhex(match.group(1)) + type = match.group(2) + + try: + file_path = self.server.build_ids[build_id][type] + except KeyError: + self.send_error(http.HTTPStatus.NOT_FOUND) + return + + try: + f = open(file_path, "rb") + except OSError: + self.send_error(http.HTTPStatus.INTERNAL_SERVER_ERROR) + return + + with f: + self.send_response(http.HTTPStatus.OK) + st = os.fstat(f.fileno()) + self.send_header("Content-Type", "application/octet-stream") + self.send_header("Content-Length", str(st.st_size)) + self.send_header("X-Debuginfod-Size", str(st.st_size)) + self.send_header("Last-Modified", self.date_time_string(st.st_mtime)) + self.end_headers() + shutil.copyfileobj(f, self.wfile) + + +class TestDebuginfodDebugInfoFinder(TestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.server = socketserver.TCPServer(("localhost", 0), _DebuginfodHTTPHandler) + cls.server.build_ids = {} + cls.server_thread = threading.Thread( + target=cls.server.serve_forever, daemon=True + ) + cls.server_thread.start() + + @classmethod + def tearDownClass(cls): + # By default, serve_forever() only checks if it should shut down every + # 0.5 seconds. Shutting down the socket makes it check immediately. + cls.server.socket.shutdown(socket.SHUT_RD) + cls.server.shutdown() + cls.server_thread.join() + + def setUp(self): + self.prog = Program() + try: + self.prog.set_enabled_debug_info_finders(["debuginfod"]) + except ValueError: + self.skipTest("no debuginfod support") + + self.server.build_ids.clear() + self.cache_dir = Path( + self.enterContext(tempfile.TemporaryDirectory(prefix="debuginfod-cache-")) + ) + self.enterContext( + modifyenv( + { + "DEBUGINFOD_URLS": "http://{}:{}/".format( + *self.server.server_address + ), + "DEBUGINFOD_CACHE_PATH": str(self.cache_dir), + } + ) + ) + + def test_no_build_id(self): + module = self.prog.extra_module("foo", create=True)[0] + self.prog.load_module_debug_info(module) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.WANT) + self.assertEqual(module.debug_file_status, ModuleFileStatus.WANT) + + def test_separate(self): + build_id = b"\x01\x23\x45\x67\x89\xab\xcd\xef" + + with NamedTemporaryElfFile( + loadable=True, debug=False, build_id=build_id + ) as loadable_file, NamedTemporaryElfFile( + loadable=False, debug=True, build_id=build_id + ) as debug_file: + self.server.build_ids[build_id] = { + "executable": loadable_file.name, + "debuginfo": debug_file.name, + } + + module = self.prog.extra_module("foo", create=True)[0] + module.build_id = build_id + self.prog.load_module_debug_info(module) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.debug_file_status, ModuleFileStatus.HAVE) + self.assertEqual( + module.loaded_file_path, + str(self.cache_dir / build_id.hex() / "executable"), + ) + self.assertEqual( + module.debug_file_path, + str(self.cache_dir / build_id.hex() / "debuginfo"), + ) + + def test_no_servers(self): + build_id = b"\x01\x23\x45\x67\x89\xab\xcd\xef" + + with NamedTemporaryElfFile( + loadable=True, debug=False, build_id=build_id + ) as loadable_file, NamedTemporaryElfFile( + loadable=False, debug=True, build_id=build_id + ) as debug_file, modifyenv( + {"DEBUGINFOD_URLS": None} + ): + self.server.build_ids[build_id] = { + "executable": loadable_file.name, + "debuginfo": debug_file.name, + } + + module = self.prog.extra_module("foo", create=True)[0] + module.build_id = build_id + self.prog.load_module_debug_info(module) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.WANT) + self.assertEqual(module.debug_file_status, ModuleFileStatus.WANT) + + def test_cache_hit(self): + build_id = b"\x01\x23\x45\x67\x89\xab\xcd\xef" + + with NamedTemporaryElfFile( + loadable=False, debug=True, build_id=build_id + ) as debug_file: + self.server.build_ids[build_id] = {"debuginfo": debug_file.name} + + for i in range(2): + module = self.prog.extra_module("foo", i, create=True)[0] + module.build_id = build_id + self.prog.load_module_debug_info(module) + self.assertEqual(module.debug_file_status, ModuleFileStatus.HAVE) + self.assertEqual( + module.debug_file_path, + str(self.cache_dir / build_id.hex() / "debuginfo"), + ) + + def test_gnu_debugaltlink(self): + build_id = b"\x01\x23\x45\x67\x89\xab\xcd\xef" + alt_build_id = b"\xfe\xdc\xba\x98\x76\x54\x32\x10" + + with NamedTemporaryElfFile( + loadable=True, debug=False, build_id=build_id + ) as loadable_file, NamedTemporaryElfFile( + loadable=False, + debug=True, + build_id=build_id, + sections=(gnu_debugaltlink_section("alt.debug", alt_build_id),), + ) as debug_file, NamedTemporaryElfFile( + loadable=False, debug=True, build_id=alt_build_id + ) as alt_f: + self.server.build_ids[build_id] = { + "executable": loadable_file.name, + "debuginfo": debug_file.name, + } + self.server.build_ids[alt_build_id] = {"debuginfo": alt_f.name} + + module = self.prog.extra_module("foo", create=True)[0] + module.build_id = build_id + self.prog.load_module_debug_info(module) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.debug_file_status, ModuleFileStatus.HAVE) + self.assertEqual( + module.loaded_file_path, + str(self.cache_dir / build_id.hex() / "executable"), + ) + self.assertEqual( + module.debug_file_path, + str(self.cache_dir / build_id.hex() / "debuginfo"), + ) + self.assertEqual( + module.supplementary_debug_file_path, + str(self.cache_dir / alt_build_id.hex() / "debuginfo"), + ) + + def test_gnu_debugaltlink_not_found(self): + build_id = b"\x01\x23\x45\x67\x89\xab\xcd\xef" + alt_build_id = b"\xfe\xdc\xba\x98\x76\x54\x32\x10" + + with NamedTemporaryElfFile( + loadable=True, debug=False, build_id=build_id + ) as loadable_file, NamedTemporaryElfFile( + loadable=False, + debug=True, + build_id=build_id, + sections=(gnu_debugaltlink_section("alt.debug", alt_build_id),), + ) as debug_file: + self.server.build_ids[build_id] = { + "executable": loadable_file.name, + "debuginfo": debug_file.name, + } + + module = self.prog.extra_module("foo", create=True)[0] + module.build_id = build_id + self.prog.load_module_debug_info(module) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.HAVE) + self.assertEqual( + module.debug_file_status, ModuleFileStatus.WANT_SUPPLEMENTARY + ) + self.assertEqual( + module.wanted_supplementary_debug_file(), + ( + SupplementaryFileKind.GNU_DEBUGALTLINK, + str(self.cache_dir / build_id.hex() / "debuginfo"), + "alt.debug", + alt_build_id, + ), + ) + self.assertEqual( + module.loaded_file_path, + str(self.cache_dir / build_id.hex() / "executable"), + ) + + def test_only_gnu_debugaltlink(self): + build_id = b"\x01\x23\x45\x67\x89\xab\xcd\xef" + alt_build_id = b"\xfe\xdc\xba\x98\x76\x54\x32\x10" + + with NamedTemporaryElfFile( + build_id=build_id, + sections=(gnu_debugaltlink_section("alt.debug", alt_build_id),), + ) as f, NamedTemporaryElfFile( + loadable=False, debug=True, build_id=alt_build_id + ) as alt_f: + self.server.build_ids[alt_build_id] = {"debuginfo": alt_f.name} + + module = self.prog.extra_module("foo", create=True)[0] + module.try_file(f.name) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.HAVE) + self.assertEqual( + module.debug_file_status, ModuleFileStatus.WANT_SUPPLEMENTARY + ) + self.assertEqual(module.loaded_file_path, f.name) + + self.prog.load_module_debug_info(module) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.debug_file_status, ModuleFileStatus.HAVE) + self.assertEqual(module.debug_file_path, f.name) + self.assertEqual( + module.supplementary_debug_file_path, + str(self.cache_dir / alt_build_id.hex() / "debuginfo"), + ) + + def test_only_gnu_debugaltlink_not_found(self): + build_id = b"\x01\x23\x45\x67\x89\xab\xcd\xef" + alt_build_id = b"\xfe\xdc\xba\x98\x76\x54\x32\x10" + + with NamedTemporaryElfFile( + build_id=build_id, + sections=(gnu_debugaltlink_section("alt.debug", alt_build_id),), + ) as f: + module = self.prog.extra_module("foo", create=True)[0] + module.try_file(f.name) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.HAVE) + self.assertEqual( + module.debug_file_status, ModuleFileStatus.WANT_SUPPLEMENTARY + ) + self.assertEqual( + module.wanted_supplementary_debug_file(), + ( + SupplementaryFileKind.GNU_DEBUGALTLINK, + f.name, + "alt.debug", + alt_build_id, + ), + ) + self.assertEqual(module.loaded_file_path, f.name) + + self.prog.load_module_debug_info(module) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.HAVE) + self.assertEqual( + module.debug_file_status, ModuleFileStatus.WANT_SUPPLEMENTARY + ) diff --git a/tests/test_dwarf.py b/tests/test_dwarf.py index 7658cd67f..f2f0e7187 100644 --- a/tests/test_dwarf.py +++ b/tests/test_dwarf.py @@ -202,12 +202,16 @@ labeled_float_die = (DwarfLabel("float_die"), float_die) +def add_extra_dwarf(prog, path): + prog.extra_module(path, create=True)[0].try_file(path, force=True) + + def dwarf_program(*args, segments=None, **kwds): prog = Program() with tempfile.NamedTemporaryFile() as f: f.write(compile_dwarf(*args, **kwds)) f.flush() - prog.load_debug_info([f.name]) + add_extra_dwarf(prog, f.name) if segments is not None: add_mock_memory_segments(prog, segments) @@ -6909,7 +6913,7 @@ def test_dwo4(self): ) ) ) - prog.load_debug_info([f.name]) + add_extra_dwarf(prog, f.name) self.assertIdentical(prog.type("TEST").type, prog.int_type("int", 4, True)) def test_dwo4_not_found(self): @@ -6937,7 +6941,12 @@ def test_dwo4_not_found(self): ) ) with self.assertLogs(logging.getLogger("drgn"), "WARNING") as log: - prog.load_debug_info([f.name]) + add_extra_dwarf(prog, f.name) + # Force debug info to be indexed. + try: + prog["foo"] + except KeyError: + pass self.assertTrue( any( "split DWARF file split.dwo not found" in output @@ -6989,7 +6998,12 @@ def test_dwo4_id_mismatch(self): ) ) with self.assertLogs(logging.getLogger("drgn"), "WARNING") as log: - prog.load_debug_info([f.name]) + add_extra_dwarf(prog, f.name) + # Force debug info to be indexed. + try: + prog["foo"] + except KeyError: + pass self.assertTrue( any( "split DWARF file split.dwo not found" in output @@ -7034,7 +7048,7 @@ def test_dwo5(self): version=5, ) ) - prog.load_debug_info([f.name]) + add_extra_dwarf(prog, f.name) self.assertIdentical(prog.type("TEST").type, prog.int_type("int", 4, True)) def test_dwo5_not_found(self): @@ -7059,7 +7073,12 @@ def test_dwo5_not_found(self): ) ) with self.assertLogs(logging.getLogger("drgn"), "WARNING") as log: - prog.load_debug_info([f.name]) + add_extra_dwarf(prog, f.name) + # Force debug info to be indexed. + try: + prog["foo"] + except KeyError: + pass self.assertTrue( any( "split DWARF file split.dwo not found" in output @@ -7105,7 +7124,12 @@ def test_dwo5_id_mismatch(self): ) ) with self.assertLogs(logging.getLogger("drgn"), "WARNING") as log: - prog.load_debug_info([f.name]) + add_extra_dwarf(prog, f.name) + # Force debug info to be indexed. + try: + prog["foo"] + except KeyError: + pass self.assertTrue( any( "split DWARF file split.dwo not found" in output diff --git a/tests/test_module.py b/tests/test_module.py new file mode 100644 index 000000000..2ff5c1c89 --- /dev/null +++ b/tests/test_module.py @@ -0,0 +1,489 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# SPDX-License-Identifier: LGPL-2.1-or-later + +from pathlib import Path + +from drgn import ( + ExtraModule, + MainModule, + ModuleFileStatus, + Program, + RelocatableModule, + SharedLibraryModule, + VdsoModule, +) +from tests import TestCase + + +class IntWrapper: + def __init__(self, value): + self._value = value + + def __index__(self): + return self._value + + +class TestModule(TestCase): + def _test_module_init_common(self, module): + self.assertIsNone(module.address_range) + self.assertIsNone(module.build_id) + self.assertEqual(module.loaded_file_status, ModuleFileStatus.WANT) + self.assertIsNone(module.loaded_file_path) + self.assertIsNone(module.loaded_file_bias) + self.assertEqual(module.debug_file_status, ModuleFileStatus.WANT) + self.assertIsNone(module.debug_file_path) + self.assertIsNone(module.debug_file_bias) + self.assertIsNone(module.supplementary_debug_file_kind) + self.assertIsNone(module.supplementary_debug_file_path) + + def test_main_module(self): + prog = Program() + + self.assertRaises(LookupError, prog.main_module) + self.assertRaises(LookupError, prog.main_module, "/foo/bar") + + module, new = prog.main_module("/foo/bar", create=True) + self.assertIsInstance(module, MainModule) + self.assertEqual(new, True) + + self.assertEqual(prog.main_module(), module) + self.assertEqual(prog.main_module(create=False), module) + self.assertEqual(prog.main_module("/foo/bar"), module) + self.assertEqual(prog.main_module(b"/foo/bar"), module) + self.assertEqual(prog.main_module(Path("/foo/bar")), module) + self.assertEqual(prog.main_module("/foo/bar", create=True), (module, False)) + + self.assertRaises(LookupError, prog.main_module, "/foo/baz") + self.assertRaises(LookupError, prog.main_module, "/foo/baz", create=True) + + self.assertIs(module.prog, prog) + self.assertEqual(module.name, "/foo/bar") + self._test_module_init_common(module) + + def test_main_module_invalid(self): + prog = Program() + self.assertRaises(TypeError, prog.main_module, None) + self.assertRaises(TypeError, prog.main_module, create=True) + self.assertRaises(TypeError, prog.main_module, "/foo/bar", True) + + def test_shared_library_module(self): + prog = Program() + + self.assertRaises( + LookupError, prog.shared_library_module, "/foo/bar", 0x10000000 + ) + + module, new = prog.shared_library_module("/foo/bar", 0x10000000, create=True) + self.assertIsInstance(module, SharedLibraryModule) + self.assertEqual(new, True) + + self.assertEqual(prog.shared_library_module("/foo/bar", 0x10000000), module) + self.assertEqual(prog.shared_library_module(b"/foo/bar", 0x10000000), module) + self.assertEqual( + prog.shared_library_module(Path("/foo/bar"), IntWrapper(0x10000000)), module + ) + self.assertEqual( + prog.shared_library_module("/foo/bar", 0x10000000, create=True), + (module, False), + ) + + self.assertRaises( + LookupError, prog.shared_library_module, "/foo/bar", 0x20000000 + ) + self.assertRaises( + LookupError, prog.shared_library_module, "/foo/baz", 0x10000000 + ) + + self.assertNotEqual( + prog.shared_library_module("/foo/bar", 0x20000000, create=True)[0], module + ) + self.assertNotEqual( + prog.shared_library_module("/foo/baz", 0x10000000, create=True)[0], module + ) + self.assertNotEqual( + prog.vdso_module("/foo/bar", 0x10000000, create=True)[0], module + ) + + self.assertIs(module.prog, prog) + self.assertEqual(module.name, "/foo/bar") + self.assertEqual(module.dynamic_address, 0x10000000) + self._test_module_init_common(module) + + def test_shared_library_module_invalid(self): + prog = Program() + self.assertRaises(TypeError, prog.shared_library_module) + self.assertRaises(TypeError, prog.shared_library_module, "/foo/bar") + self.assertRaises(TypeError, prog.shared_library_module, "/foo/bar", None) + self.assertRaises(TypeError, prog.shared_library_module, None, 0) + self.assertRaises( + TypeError, prog.shared_library_module, "/foo/bar", 0x10000000, True + ) + + def test_vdso_module(self): + prog = Program() + + self.assertRaises(LookupError, prog.vdso_module, "/foo/bar", 0x10000000) + + module, new = prog.vdso_module("/foo/bar", 0x10000000, create=True) + self.assertIsInstance(module, VdsoModule) + self.assertEqual(new, True) + + self.assertEqual(prog.vdso_module("/foo/bar", 0x10000000), module) + self.assertEqual(prog.vdso_module(b"/foo/bar", 0x10000000), module) + self.assertEqual( + prog.vdso_module(Path("/foo/bar"), IntWrapper(0x10000000)), module + ) + self.assertEqual( + prog.vdso_module("/foo/bar", 0x10000000, create=True), (module, False) + ) + + self.assertRaises(LookupError, prog.vdso_module, "/foo/bar", 0x20000000) + self.assertRaises(LookupError, prog.vdso_module, "/foo/baz", 0x10000000) + + self.assertNotEqual( + prog.vdso_module("/foo/bar", 0x20000000, create=True)[0], module + ) + self.assertNotEqual( + prog.vdso_module("/foo/baz", 0x10000000, create=True)[0], module + ) + self.assertNotEqual( + prog.shared_library_module("/foo/bar", 0x10000000, create=True)[0], module + ) + + self.assertIs(module.prog, prog) + self.assertEqual(module.name, "/foo/bar") + self.assertEqual(module.dynamic_address, 0x10000000) + self._test_module_init_common(module) + + def test_vdso_module_invalid(self): + prog = Program() + self.assertRaises(TypeError, prog.vdso_module) + self.assertRaises(TypeError, prog.vdso_module, "/foo/bar") + self.assertRaises(TypeError, prog.vdso_module, "/foo/bar", None) + self.assertRaises(TypeError, prog.vdso_module, None, 0) + self.assertRaises(TypeError, prog.vdso_module, "/foo/bar", 0x10000000, True) + + def test_relocatable_module(self): + prog = Program() + + self.assertRaises(LookupError, prog.relocatable_module, "/foo/bar", 0x10000000) + + module, new = prog.relocatable_module("/foo/bar", 0x10000000, create=True) + self.assertIsInstance(module, RelocatableModule) + self.assertEqual(new, True) + + self.assertEqual(prog.relocatable_module("/foo/bar", 0x10000000), module) + self.assertEqual(prog.relocatable_module(b"/foo/bar", 0x10000000), module) + self.assertEqual( + prog.relocatable_module(Path("/foo/bar"), IntWrapper(0x10000000)), module + ) + self.assertEqual( + prog.relocatable_module("/foo/bar", 0x10000000, create=True), + (module, False), + ) + + self.assertRaises(LookupError, prog.relocatable_module, "/foo/bar", 0x20000000) + self.assertRaises(LookupError, prog.relocatable_module, "/foo/baz", 0x10000000) + + self.assertNotEqual( + prog.relocatable_module("/foo/bar", 0x20000000, create=True)[0], module + ) + self.assertNotEqual( + prog.relocatable_module("/foo/baz", 0x10000000, create=True)[0], module + ) + self.assertNotEqual( + prog.shared_library_module("/foo/bar", 0x10000000, create=True)[0], module + ) + + self.assertIs(module.prog, prog) + self.assertEqual(module.name, "/foo/bar") + self.assertEqual(module.address, 0x10000000) + self._test_module_init_common(module) + + def test_section_addresses(self): + prog = Program() + module = prog.relocatable_module("/foo/bar", 0x10000000, create=True)[0] + + self.assertNotIn(".text", module.section_addresses) + self.assertNotIn(1, module.section_addresses) + + with self.assertRaises(KeyError): + module.section_addresses[".text"] + with self.assertRaises(KeyError): + module.section_addresses[1] + + with self.assertRaises(KeyError): + del module.section_addresses[".text"] + with self.assertRaises(KeyError): + del module.section_addresses[1] + + module.section_addresses[".text"] = 0x10000000 + self.assertIn(".text", module.section_addresses) + self.assertEqual(module.section_addresses[".text"], 0x10000000) + + self.assertEqual(len(module.section_addresses), 1) + self.assertCountEqual(list(module.section_addresses), [".text"]) + self.assertCountEqual(list(module.section_addresses.keys()), [".text"]) + self.assertCountEqual(list(module.section_addresses.values()), [0x10000000]) + self.assertCountEqual( + list(module.section_addresses.items()), [(".text", 0x10000000)] + ) + + module.section_addresses[".data"] = 0x10001000 + + self.assertEqual(len(module.section_addresses), 2) + self.assertCountEqual(list(module.section_addresses), [".text", ".data"]) + self.assertCountEqual(list(module.section_addresses.keys()), [".text", ".data"]) + self.assertCountEqual( + list(module.section_addresses.values()), [0x10000000, 0x10001000] + ) + self.assertCountEqual( + list(module.section_addresses.items()), + [(".text", 0x10000000), (".data", 0x10001000)], + ) + + del module.section_addresses[".data"] + + self.assertEqual(len(module.section_addresses), 1) + self.assertCountEqual(list(module.section_addresses), [".text"]) + self.assertCountEqual(list(module.section_addresses.keys()), [".text"]) + self.assertCountEqual(list(module.section_addresses.values()), [0x10000000]) + self.assertCountEqual( + list(module.section_addresses.items()), [(".text", 0x10000000)] + ) + + def test_relocatable_module_invalid(self): + prog = Program() + self.assertRaises(TypeError, prog.relocatable_module) + self.assertRaises(TypeError, prog.relocatable_module, "/foo/bar") + self.assertRaises(TypeError, prog.relocatable_module, "/foo/bar", None) + self.assertRaises(TypeError, prog.relocatable_module, None, 0) + self.assertRaises( + TypeError, prog.relocatable_module, "/foo/bar", 0x10000000, True + ) + + def test_extra_module(self): + prog = Program() + + self.assertRaises(LookupError, prog.extra_module, "/foo/bar", 1234) + + module, new = prog.extra_module("/foo/bar", 1234, create=True) + self.assertIsInstance(module, ExtraModule) + self.assertEqual(new, True) + + self.assertEqual(prog.extra_module("/foo/bar", 1234), module) + self.assertEqual(prog.extra_module(b"/foo/bar", 1234), module) + self.assertEqual(prog.extra_module(Path("/foo/bar"), IntWrapper(1234)), module) + self.assertEqual( + prog.extra_module("/foo/bar", 1234, create=True), (module, False) + ) + + self.assertRaises(LookupError, prog.extra_module, "/foo/bar", 5678) + self.assertRaises(LookupError, prog.extra_module, "/foo/baz", 1234) + + self.assertNotEqual(prog.extra_module("/foo/bar", 5678, create=True)[0], module) + self.assertNotEqual(prog.extra_module("/foo/baz", 1234, create=True)[0], module) + self.assertNotEqual( + prog.shared_library_module("/foo/bar", 1234, create=True)[0], module + ) + self.assertEqual(prog.extra_module("/foo/bar", create=True)[0].id, 0) + + self.assertIs(module.prog, prog) + self.assertEqual(module.name, "/foo/bar") + self.assertEqual(module.id, 1234) + self._test_module_init_common(module) + + def test_extra_module_invalid(self): + prog = Program() + self.assertRaises(TypeError, prog.extra_module) + self.assertRaises(TypeError, prog.extra_module, "/foo/bar", None) + self.assertRaises(TypeError, prog.extra_module, None, 0) + self.assertRaises(TypeError, prog.extra_module, "/foo/bar", 1234, True) + + def test_address_range(self): + module = Program().extra_module("/foo/bar", create=True)[0] + + module.address_range = (0x10000000, 0x10010000) + self.assertEqual(module.address_range, (0x10000000, 0x10010000)) + + module.address_range = (0x20000000, 0x20020000) + self.assertEqual(module.address_range, (0x20000000, 0x20020000)) + + module.address_range = None + self.assertIsNone(module.address_range) + + module.address_range = None + self.assertIsNone(module.address_range) + + def test_address_range_empty(self): + module = Program().extra_module("/foo/bar", create=True)[0] + + module.address_range = (0, 0) + self.assertEqual(module.address_range, (0, 0)) + + def test_address_range_type_error(self): + module = Program().extra_module("/foo/bar", create=True)[0] + + with self.assertRaises(TypeError): + module.address_range = 1 + + with self.assertRaises(TypeError): + module.address_range = (1,) + + with self.assertRaises(TypeError): + module.address_range = ("foo", 1) + + with self.assertRaises(TypeError): + module.address_range = (1, "bar") + + def test_address_range_invalid(self): + module = Program().extra_module("/foo/bar", create=True)[0] + + with self.assertRaisesRegex(ValueError, "invalid module address range"): + module.address_range = (0x10010000, 0x10000000) + + with self.assertRaisesRegex(ValueError, "invalid module address range"): + module.address_range = (1, 1) + + with self.assertRaisesRegex(ValueError, "invalid module address range"): + module.address_range = (2**64 - 1, 1) + + with self.assertRaisesRegex(ValueError, "invalid module address range"): + module.address_range = (2**64 - 1, 2**64 - 1) + + def test_build_id(self): + module = Program().extra_module("/foo/bar", create=True)[0] + + module.build_id = b"\x01\x23\x45\x67\x89\xab\xcd\xef" + self.assertEqual(module.build_id, b"\x01\x23\x45\x67\x89\xab\xcd\xef") + + module.build_id = b"\xfe\xdc\xba\x98\x76\x54\x32\x10" + self.assertEqual(module.build_id, b"\xfe\xdc\xba\x98\x76\x54\x32\x10") + + module.build_id = None + self.assertIsNone(module.build_id) + + module.build_id = None + self.assertIsNone(module.build_id) + + def test_build_id_type_error(self): + module = Program().extra_module("/foo/bar", create=True)[0] + with self.assertRaises(TypeError): + module.build_id = "abcd" + + def test_build_id_invalid_empty(self): + module = Program().extra_module("/foo/bar", create=True)[0] + with self.assertRaisesRegex(ValueError, "build ID cannot be empty"): + module.build_id = b"" + + def test_find_by_address(self): + prog = Program() + module1 = prog.extra_module("/foo/bar", create=True)[0] + module1.address_range = (0x10000000, 0x10010000) + module2 = prog.extra_module("/asdf/jkl", create=True)[0] + module2.address_range = (0x20000000, 0x20020000) + + self.assertRaises(LookupError, prog.module, 0x0FFFFFFF) + self.assertEqual(prog.module(0x10000000), module1) + self.assertEqual(prog.module(0x10000001), module1) + self.assertEqual(prog.module(0x1000FFFF), module1) + self.assertRaises(LookupError, prog.module, 0x10010000) + + self.assertRaises(LookupError, prog.module, 0x1FFFFFFF) + self.assertEqual(prog.module(0x20000000), module2) + self.assertEqual(prog.module(0x20000001), module2) + self.assertEqual(prog.module(0x2001FFFF), module2) + self.assertRaises(LookupError, prog.module, 0x20020000) + + # Test all of the state transitions that we can without setting a file. + def _test_file_status(self, which): + module = Program().extra_module("/foo/bar", create=True)[0] + + status_attr = which + "_file_status" + wants_file = getattr(module, f"wants_{which}_file") + + self.assertRaises(TypeError, setattr, module, status_attr, 1) + + setattr(module, status_attr, ModuleFileStatus.WANT) + self.assertEqual(getattr(module, status_attr), ModuleFileStatus.WANT) + self.assertEqual(wants_file(), True) + for status in set(ModuleFileStatus) - { + ModuleFileStatus.WANT, + ModuleFileStatus.DONT_WANT, + ModuleFileStatus.DONT_NEED, + }: + with self.subTest(from_=ModuleFileStatus.WANT, to=status): + self.assertRaises(ValueError, setattr, module, status_attr, status) + self.assertEqual(getattr(module, status_attr), ModuleFileStatus.WANT) + + setattr(module, status_attr, ModuleFileStatus.DONT_WANT) + self.assertEqual(getattr(module, status_attr), ModuleFileStatus.DONT_WANT) + self.assertEqual(wants_file(), False) + for status in set(ModuleFileStatus) - { + ModuleFileStatus.WANT, + ModuleFileStatus.DONT_WANT, + ModuleFileStatus.DONT_NEED, + }: + with self.subTest(from_=ModuleFileStatus.DONT_WANT, to=status): + self.assertRaises(ValueError, setattr, module, status_attr, status) + self.assertEqual( + getattr(module, status_attr), ModuleFileStatus.DONT_WANT + ) + + setattr(module, status_attr, ModuleFileStatus.DONT_NEED) + self.assertEqual(getattr(module, status_attr), ModuleFileStatus.DONT_NEED) + self.assertEqual(wants_file(), False) + for status in set(ModuleFileStatus) - { + ModuleFileStatus.WANT, + ModuleFileStatus.DONT_WANT, + ModuleFileStatus.DONT_NEED, + }: + with self.subTest(from_=ModuleFileStatus.DONT_NEED, to=status): + self.assertRaises(ValueError, setattr, module, status_attr, status) + self.assertEqual( + getattr(module, status_attr), ModuleFileStatus.DONT_NEED + ) + + setattr(module, status_attr, ModuleFileStatus.DONT_WANT) + self.assertEqual(getattr(module, status_attr), ModuleFileStatus.DONT_WANT) + + setattr(module, status_attr, ModuleFileStatus.WANT) + self.assertEqual(getattr(module, status_attr), ModuleFileStatus.WANT) + + setattr(module, status_attr, ModuleFileStatus.DONT_NEED) + self.assertEqual(getattr(module, status_attr), ModuleFileStatus.DONT_NEED) + + setattr(module, status_attr, ModuleFileStatus.WANT) + self.assertEqual(getattr(module, status_attr), ModuleFileStatus.WANT) + + def test_loaded_file_status(self): + self._test_file_status("loaded") + + def test_debug_file_status(self): + self._test_file_status("debug") + + +class TestCreatedModules(TestCase): + def test_empty(self): + self.assertEqual(list(Program().modules()), []) + + def test_one(self): + module = Program().extra_module("/foo/bar", create=True)[0] + self.assertEqual(list(module.prog.modules()), [module]) + + def test_multiple(self): + prog = Program() + modules = [ + prog.extra_module("/foo/bar", create=True)[0], + prog.extra_module("/asdf/jkl", create=True)[0], + prog.extra_module("/123/456", create=True)[0], + ] + self.assertCountEqual(list(prog.modules()), modules) + + def test_change_during_iteration(self): + prog = Program() + prog.extra_module("/foo/bar", create=True) + with self.assertRaisesRegex(Exception, "modules changed during iteration"): + for module in prog.modules(): + prog.extra_module("/asdf/jkl", create=True) + prog.extra_module("/123/456", create=True) diff --git a/tests/test_symbol.py b/tests/test_symbol.py index d9cc3dd94..91fac06aa 100644 --- a/tests/test_symbol.py +++ b/tests/test_symbol.py @@ -1,8 +1,10 @@ # Copyright (c) Meta Platforms, Inc. and affiliates. # SPDX-License-Identifier: LGPL-2.1-or-later + +import itertools import tempfile -from _drgn_util.elf import ET, PT, SHT, STB, STT +from _drgn_util.elf import ET, PT, SHF, SHT, STB, STT from drgn import Program, Symbol, SymbolBinding, SymbolIndex, SymbolKind from tests import TestCase from tests.dwarfwriter import dwarf_sections @@ -10,19 +12,20 @@ def create_elf_symbol_file(symbols): - # We need some DWARF data so that libdwfl will load the file. sections = dwarf_sections(()) # Create a section for the symbols to reference and the corresponding # segment for address lookups. min_address = min(symbol.value for symbol in symbols) max_address = max(symbol.value + symbol.size for symbol in symbols) + size = max(max_address - min_address, 4096) sections.append( ElfSection( name=".foo", sh_type=SHT.NOBITS, + sh_flags=SHF.ALLOC, p_type=PT.LOAD, vaddr=min_address, - memsz=max_address - min_address, + memsz=size, ) ) symbols = [ @@ -31,16 +34,20 @@ def create_elf_symbol_file(symbols): ) for symbol in symbols ] - return create_elf_file(ET.EXEC, sections, symbols) + return create_elf_file(ET.EXEC, sections, symbols), min_address, min_address + size def elf_symbol_program(*modules): prog = Program() for symbols in modules: with tempfile.NamedTemporaryFile() as f: - f.write(create_elf_symbol_file(symbols)) + contents, start, end = create_elf_symbol_file(symbols) + f.write(contents) f.flush() - prog.load_debug_info([f.name]) + module = prog.extra_module(f.name, create=True)[0] + module.address_range = (start, end) + module.try_file(f.name, force=True) + print(module.loaded_file_path) return prog @@ -78,59 +85,167 @@ def test_by_address(self): self.assert_symbols_equal_unordered(prog.symbols(0xFFFF000C), [second]) self.assertRaises(LookupError, prog.symbol, 0xFFFF0010) - def test_by_address_precedence(self): - precedence = (STB.GLOBAL, STB.WEAK, STB.LOCAL) - drgn_precedence = ( - SymbolBinding.GLOBAL, - SymbolBinding.WEAK, - SymbolBinding.LOCAL, + def test_by_address_closest(self): + # If two symbols contain the given address, then the one whose start + # address is closest to the given address should be preferred + # (regardless of the binding of either symbol). + elf_closest = ElfSymbol("closest", 0xFFFF0008, 0x8, STT.OBJECT, STB.WEAK) + elf_furthest = ElfSymbol("furthest", 0xFFFF0000, 0xC, STT.OBJECT, STB.GLOBAL) + closest = Symbol( + "closest", 0xFFFF0008, 0x8, SymbolBinding.WEAK, SymbolKind.OBJECT + ) + furthest = Symbol( + "furthest", 0xFFFF0000, 0xC, SymbolBinding.GLOBAL, SymbolKind.OBJECT ) - def assert_find_higher(*modules): - self.assertEqual( - elf_symbol_program(*modules).symbol(0xFFFF0000).name, "foo" + def test(elf_symbols): + prog = elf_symbol_program(elf_symbols) + self.assertEqual(prog.symbol(0xFFFF000B), closest) + self.assert_symbols_equal_unordered( + prog.symbols(0xFFFF000B), [closest, furthest] ) - def assert_finds_both(symbols, *modules): + with self.subTest("closest first"): + test([elf_closest, elf_furthest]) + + with self.subTest("furthest first"): + test([elf_furthest, elf_closest]) + + def test_by_address_closest_end(self): + # If two symbols contain the given address and have the same start + # address, then the one whose end address is closest to the given + # address should be preferred (regardless of the binding of either + # symbol). + elf_closest = ElfSymbol("closest", 0xFFFF0000, 0xC, STT.OBJECT, STB.WEAK) + elf_furthest = ElfSymbol("furthest", 0xFFFF0000, 0x10, STT.OBJECT, STB.GLOBAL) + closest = Symbol( + "closest", 0xFFFF0000, 0xC, SymbolBinding.WEAK, SymbolKind.OBJECT + ) + furthest = Symbol( + "furthest", 0xFFFF0000, 0x10, SymbolBinding.GLOBAL, SymbolKind.OBJECT + ) + + def test(elf_symbols): + prog = elf_symbol_program(elf_symbols) + self.assertEqual(prog.symbol(0xFFFF000B), closest) self.assert_symbols_equal_unordered( - elf_symbol_program(*modules).symbols(0xFFFF0000), - symbols, + prog.symbols(0xFFFF000B), [closest, furthest] ) - for i in range(len(precedence) - 1): - higher_binding = precedence[i] - higher_binding_drgn = drgn_precedence[i] - for j in range(i + 1, len(precedence)): - lower_binding = precedence[j] - lower_binding_drgn = drgn_precedence[j] - with self.subTest(higher=higher_binding, lower=lower_binding): - higher = ElfSymbol( - "foo", 0xFFFF0000, 0x8, STT.OBJECT, higher_binding - ) - lower = ElfSymbol("bar", 0xFFFF0000, 0x8, STT.OBJECT, lower_binding) - symbols = [ - Symbol( - "foo", - 0xFFFF0000, - 0x8, - higher_binding_drgn, - SymbolKind.OBJECT, - ), - Symbol( - "bar", - 0xFFFF0000, - 0x8, - lower_binding_drgn, - SymbolKind.OBJECT, - ), - ] - # Local symbols must be before global symbols. - if lower_binding != STB.LOCAL: - with self.subTest("higher before lower"): - assert_find_higher((higher, lower)) - with self.subTest("lower before higher"): - assert_find_higher((lower, higher)) - assert_finds_both(symbols, (lower, higher)) + with self.subTest("closest first"): + test([elf_closest, elf_furthest]) + + with self.subTest("furthest first"): + test([elf_furthest, elf_closest]) + + def test_by_address_sizeless(self): + label = ElfSymbol("label", 0xFFFF0008, 0x0, STT.FUNC, STB.LOCAL) + less = ElfSymbol("less", 0xFFFF0000, 0x4, STT.FUNC, STB.LOCAL) + greater = ElfSymbol("greater", 0xFFFF0010, 0x4, STT.FUNC, STB.LOCAL) + + expected = Symbol( + "label", 0xFFFF0008, 0x0, SymbolBinding.LOCAL, SymbolKind.FUNC + ) + + # Test every permutation of every combination of symbols that includes + # "label". + for elf_symbols in itertools.chain.from_iterable( + itertools.permutations((label,) + extra_elf_symbols) + for r in range(3) + for extra_elf_symbols in itertools.combinations((less, greater), r) + ): + with self.subTest(elf_symbols=[sym.name for sym in elf_symbols]): + prog = elf_symbol_program(elf_symbols) + self.assertEqual(prog.symbol(0xFFFF0009), expected) + self.assertEqual(prog.symbols(0xFFFF0009), [expected]) + + def test_by_address_sizeless_subsumed(self): + import unittest.util + + unittest.util._MAX_LENGTH = 999999999 + label = ElfSymbol("label", 0xFFFF0008, 0x0, STT.FUNC, STB.LOCAL) + subsume = ElfSymbol("subsume", 0xFFFF0004, 0x8, STT.FUNC, STB.LOCAL) + less = ElfSymbol("less", 0xFFFF0000, 0x4, STT.FUNC, STB.LOCAL) + greater = ElfSymbol("greater", 0xFFFF0010, 0x4, STT.FUNC, STB.LOCAL) + + expected = Symbol( + "subsume", 0xFFFF0004, 0x8, SymbolBinding.LOCAL, SymbolKind.FUNC + ) + + # Test every permutation of every combination of symbols that includes + # "label" and "subsume". + for elf_symbols in itertools.chain.from_iterable( + itertools.permutations((label, subsume) + extra_elf_symbols) + for r in range(3) + for extra_elf_symbols in itertools.combinations((less, greater), r) + ): + with self.subTest(elf_symbols=[sym.name for sym in elf_symbols]): + prog = elf_symbol_program(elf_symbols) + self.assertEqual(prog.symbol(0xFFFF0009), expected) + self.assertEqual(prog.symbols(0xFFFF0009), [expected]) + + def test_by_address_sizeless_wrong_section(self): + prog = elf_symbol_program( + (ElfSymbol("label", 0xFFFF0008, 0x0, STT.FUNC, STB.LOCAL),) + ) + for module in prog.modules(): + start, end = module.address_range + module.address_range = (start, 0xFFFFFF00) + self.assertRaises(LookupError, prog.symbol, 0xFFFFFE00) + + def test_by_address_binding_precedence(self): + precedence = ( + (STB.GLOBAL, STB.GNU_UNIQUE), + (STB.WEAK,), + (STB.LOCAL, STB.HIPROC), + ) + + def assert_find_higher(*modules, both): + prog = elf_symbol_program(*modules) + self.assertEqual(prog.symbol(0xFFFF0000).name, "foo") + # Test that symbols() finds both if expected or either one if not. + if both: + self.assertCountEqual( + [sym.name for sym in prog.symbols(0xFFFF0000)], ["foo", "bar"] + ) + else: + self.assertIn( + [sym.name for sym in prog.symbols(0xFFFF0000)], (["foo"], ["bar"]) + ) + + for size in (8, 0): + with self.subTest(size=size): + for i in range(len(precedence) - 1): + for higher_binding in precedence[i]: + for j in range(i + 1, len(precedence)): + for lower_binding in precedence[j]: + with self.subTest( + higher=higher_binding, lower=lower_binding + ): + higher = ElfSymbol( + "foo", + 0xFFFF0000, + size, + STT.OBJECT, + higher_binding, + ) + lower = ElfSymbol( + "bar", + 0xFFFF0000, + size, + STT.OBJECT, + lower_binding, + ) + # Local symbols must be before global symbols. + if lower_binding not in precedence[-1]: + with self.subTest("higher before lower"): + assert_find_higher( + (higher, lower), both=size > 0 + ) + with self.subTest("lower before higher"): + assert_find_higher( + (lower, higher), both=size > 0 + ) def test_by_name(self): elf_first = ElfSymbol("first", 0xFFFF0000, 0x8, STT.OBJECT, STB.GLOBAL) @@ -156,7 +271,7 @@ def test_by_name(self): self.assert_symbols_equal_unordered(prog.symbols("second"), [second]) self.assertEqual(prog.symbols("third"), []) - def test_by_name_precedence(self): + def test_by_name_binding_precedence(self): precedence = ( (STB.GLOBAL, STB.GNU_UNIQUE), (STB.WEAK,), @@ -170,10 +285,9 @@ def assert_find_higher(*modules): prog = elf_symbol_program(*modules) self.assertEqual(prog.symbol("foo").address, expected) # assert symbols() always finds both - symbols = sorted(prog.symbols("foo"), key=lambda s: s.address) - self.assertEqual(len(symbols), 2) - self.assertEqual(symbols[0].address, other) - self.assertEqual(symbols[1].address, expected) + self.assertCountEqual( + [sym.address for sym in prog.symbols("foo")], [expected, other] + ) for i in range(len(precedence) - 1): for higher_binding in precedence[i]: