diff --git a/Makefile b/Makefile index 343208ea2e0..b729e85ce2d 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ endif all: clean build doc test doc-source-clean: - rm -f doc-source/crash/*.rst doc-source/kdump/*.rst + rm -f doc-source/crash/*.rst rm -f doc-source/commands/*.rst doc-clean: doc-source-clean @@ -54,8 +54,6 @@ textdir=$(docdir)/text doc-text-install: doc-help install -m 755 -d $(DESTDIR)$(textdir)/crash install -m 644 -t $(DESTDIR)$(textdir)/crash docs/text/crash/*.txt - install -m 755 -d $(DESTDIR)$(textdir)/kdump - install -m 644 -t $(DESTDIR)$(textdir)/kdump docs/text/kdump/*.txt install -m 644 -t $(DESTDIR)$(textdir) docs/text/*.txt htmldir=$(docdir)/html @@ -68,7 +66,7 @@ unit-tests: force-rebuild sh tests/run-tests.sh lint: force-rebuild - sh tests/run-pylint.sh $(PYLINT_ARGS) crash kdump + sh tests/run-pylint.sh $(PYLINT_ARGS) crash static-check: force-rebuild sh tests/run-static-checks.sh diff --git a/README.rst b/README.rst index 987c7a894cd..2f0325ed6e5 100644 --- a/README.rst +++ b/README.rst @@ -91,8 +91,8 @@ It requires the following components to work successfully: - `Python `_ 3.6 or newer - `pyelftools `_ -- `libkdumpfile `_ -- `GDB `_ with python extensions and built with Python 3.6 or newer. +- `libkdumpfile `_ with the kdumpfile_object_from_native API +- `GDB `_ with python extensions and built with Python 3.6 or newer. If you are using a SUSE or openSUSE release, pre-built packages are available on the `Open Build Service `_. diff --git a/crash.sh b/crash.sh index ed258f47748..09277b7ab75 100755 --- a/crash.sh +++ b/crash.sh @@ -163,6 +163,8 @@ if [ -z "$GDB" ]; then exit 1 fi +:> $GDBINIT + # If we're using crash.sh from the git repo, use the modules from the git repo DIR="$(dirname $0)" if [ -e "$DIR/setup.py" ]; then @@ -183,11 +185,10 @@ if [ -e "$DIR/setup.py" ]; then done else export CRASH_PYTHON_HELP="/usr/share/crash-python/help" - :> $GDBINIT TEST_GDBINIT="/usr/share/crash-python/test-gdb-compatibility.gdbinit" fi -if ! $GDB -nx -batch -x $GDBINIT -x $TEST_GDBINIT; then +if ! $GDB $GDB_CMDLINE -nx -batch -x $GDBINIT -x $TEST_GDBINIT; then echo "fatal: crash-python cannot initialize" >&2 exit 1 fi @@ -232,19 +233,14 @@ set prompt py-crash> set height 0 set print pretty on -python -from kdump.target import Target -target = Target(debug=False) -end - -target kdumpfile $KERNEL $VMCORE +file $KERNEL +core $VMCORE python import sys import traceback try: import crash.session - from crash.kernel import CrashKernel except RuntimeError as e: print("crash-python: {}, exiting".format(str(e)), file=sys.stderr) traceback.print_exc() @@ -274,10 +270,8 @@ if len(s) > 0: module_debuginfo_path = s.split(" ") try: - kernel = CrashKernel(roots, vmlinux_debuginfo, module_path, + x = crash.session.Session(roots, vmlinux_debuginfo, module_path, module_debuginfo_path, verbose, debug) - - x = crash.session.Session(kernel, verbose=verbose, debug=debug) print("The 'pyhelp' command will list the command extensions.") except gdb.error as e: print("crash-python: {}, exiting".format(str(e)), file=sys.stderr) @@ -289,18 +283,16 @@ except RuntimeError as e: traceback.print_exc() sys.exit(1) -target.unregister() -del target EOF # This is how we debug gdb problems when running crash if [ "$DEBUGMODE" = "gdb" ]; then - RUN="run -nx -q -x $GDBINIT" + RUN="run $GDB_CMDLINE -nx -q -x $GDBINIT" echo $RUN > $TMPDIR/gdbinit-debug gdb $GDB -nx -q -x $TMPDIR/gdbinit-debug elif [ "$DEBUGMODE" = "valgrind" ]; then valgrind --keep-stacktraces=alloc-and-free $GDB -nh -q -x $GDBINIT else - $GDB -nx -q -x $GDBINIT + $GDB $GDB_CMDLINE -nx -q -x $GDBINIT fi diff --git a/crash/__init__.py b/crash/__init__.py index 19ed28db6fe..e7e966536a9 100644 --- a/crash/__init__.py +++ b/crash/__init__.py @@ -3,14 +3,5 @@ import gdb -import kdump.target - -def current_target() -> kdump.target.Target: - target = gdb.current_target() - if target is None: - raise ValueError("No current target") - - if not isinstance(target, kdump.target.Target): - raise ValueError(f"Current target {type(target)} is not supported") - - return target +def archname() -> str: + return gdb.selected_inferior().architecture().name() diff --git a/crash/addrxlat.py b/crash/addrxlat.py index 0638e7da532..31a8867c5fa 100644 --- a/crash/addrxlat.py +++ b/crash/addrxlat.py @@ -1,10 +1,12 @@ # -*- coding: utf-8 -*- # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: +import addrxlat + import gdb -import addrxlat import crash +import crash.target from crash.cache.syscache import utsname from crash.util import offsetof @@ -49,15 +51,15 @@ def cb_read64(self, faddr: addrxlat.FullAddress) -> int: class CrashAddressTranslation: def __init__(self) -> None: try: - target = crash.current_target() - self.context = target.kdump.get_addrxlat_ctx() - self.system = target.kdump.get_addrxlat_sys() + target = crash.target.check_target() + self.context = target.kdumpfile.get_addrxlat_ctx() + self.system = target.kdumpfile.get_addrxlat_sys() except AttributeError: self.context = TranslationContext() self.system = addrxlat.System() self.system.os_init(self.context, arch=utsname.machine, - type=addrxlat.OS_LINUX) + os_type="linux") self.is_non_auto = False xlatmap = self.system.get_map(addrxlat.SYS_MAP_MACHPHYS_KPHYS) diff --git a/crash/arch/__init__.py b/crash/arch/__init__.py deleted file mode 100644 index 3046ea881a1..00000000000 --- a/crash/arch/__init__.py +++ /dev/null @@ -1,110 +0,0 @@ -# -*- coding: utf-8 -*- -# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: - -from typing import List, Iterator, Any, Optional, Type - -import gdb -from gdb.FrameDecorator import FrameDecorator - -import crash - -class FetchRegistersCallback: - """ - The base class from which to implement the fetch_registers callback. - - The architecture code must implement the :meth:`fetch_active` and - :meth:`fetch_scheduled` methods. - """ - def fetch_active(self, thread: gdb.InferiorThread, register: int) -> None: - raise NotImplementedError("Target has no fetch_active callback") - - def fetch_scheduled(self, thread: gdb.InferiorThread, - register: int) -> None: - raise NotImplementedError("Target has no fetch_scheduled callback") - - def __call__(self, thread: gdb.InferiorThread, - register: gdb.Register) -> None: - if register is None: - regnum = -1 - else: - regnum = register.regnum - - if thread.info.active: - return self.fetch_active(thread, regnum) - - return self.fetch_scheduled(thread, regnum) - -class CrashArchitecture: - ident = "base-class" - aliases: List[str] = list() - - _fetch_registers: Type[FetchRegistersCallback] - - def __init__(self) -> None: - target = crash.current_target() - try: - target.set_fetch_registers(self._fetch_registers()) - except AttributeError: - raise NotImplementedError("No fetch_registers callback defined") from None - - @classmethod - def set_fetch_registers(cls, - callback: Type[FetchRegistersCallback]) -> None: - """ - Set a fetch_regisers callback for the Target to use. - - Args: - callback: A Callable that accepts a :obj:`gdb.InferiorThread` and - :obj:`gdb.Register` and populates the requested registers for - the specified thread. A register with the seemingly invalid - register number of -1 is a request to populate all registers. - """ - cls._fetch_registers = callback - - def setup_thread_info(self, thread: gdb.InferiorThread) -> None: - raise NotImplementedError("setup_thread_info not implemented") - - def get_stack_pointer(self, thread_struct: gdb.Value) -> int: - raise NotImplementedError("get_stack_pointer is not implemented") - - def setup_scheduled_frame_offset(self, task: gdb.Value) -> None: - pass - -# This keeps stack traces from continuing into userspace and causing problems. -class KernelFrameFilter: - def __init__(self, address: int) -> None: - self.name = "KernelFrameFilter" - self.priority = 100 - self.enabled = True - self.address = address - gdb.frame_filters[self.name] = self - - def filter(self, frame_iter: Iterator[Any]) -> Any: - return KernelAddressIterator(frame_iter, self.address) - -class KernelAddressIterator: - def __init__(self, ii: Iterator, address: int) -> None: - self.input_iterator = ii - self.address = address - - def __iter__(self) -> Any: - return self - - def __next__(self) -> Any: - frame = next(self.input_iterator) - - if frame.inferior_frame().pc() < self.address: - raise StopIteration - - return frame - -architectures = {} -def register_arch(arch: Type[CrashArchitecture]) -> None: - architectures[arch.ident] = arch - for ident in arch.aliases: - architectures[ident] = arch - -def get_architecture(archname: str) -> Type[CrashArchitecture]: - if archname in architectures: - return architectures[archname] - raise RuntimeError(f"Couldn't locate helpers for arch: {archname}") diff --git a/crash/arch/ppc64.py b/crash/arch/ppc64.py deleted file mode 100644 index 07da586cad7..00000000000 --- a/crash/arch/ppc64.py +++ /dev/null @@ -1,31 +0,0 @@ -# -*- coding: utf-8 -*- -# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: - -import gdb - -from crash.arch import CrashArchitecture, KernelFrameFilter, register_arch -from crash.arch import FetchRegistersCallback - -class FR_Placeholder(FetchRegistersCallback): # pylint: disable=abstract-method - pass - -class Powerpc64Architecture(CrashArchitecture): - ident = "powerpc:common64" - aliases = ["ppc64", "elf64-powerpc"] - - _fetch_registers = FR_Placeholder - - def __init__(self) -> None: - super(Powerpc64Architecture, self).__init__() - # Stop stack traces with addresses below this - self.filter = KernelFrameFilter(0xffff000000000000) - - def setup_thread_info(self, thread: gdb.InferiorThread) -> None: - task = thread.info.task_struct - thread.info.set_thread_info(task['thread_info'].address) - - @classmethod - def get_stack_pointer(cls, thread_struct: gdb.Value) -> int: - return int(thread_struct['ksp']) - -register_arch(Powerpc64Architecture) diff --git a/crash/arch/x86_64.py b/crash/arch/x86_64.py deleted file mode 100644 index 58c6be880c8..00000000000 --- a/crash/arch/x86_64.py +++ /dev/null @@ -1,184 +0,0 @@ -# -*- coding: utf-8 -*- -# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: - -import gdb -import re - -from typing import Optional - -from crash.arch import CrashArchitecture, KernelFrameFilter, register_arch -from crash.arch import FetchRegistersCallback -from crash.util.symbols import Types, MinimalSymvals -from crash.util.symbols import TypeCallbacks, MinimalSymbolCallbacks - -types = Types(['struct inactive_task_frame *', 'struct thread_info *', - 'unsigned long *']) -msymvals = MinimalSymvals(['thread_return']) - -# pylint: disable=abstract-method -class _FetchRegistersBase(FetchRegistersCallback): - def fetch_active(self, thread: gdb.InferiorThread, register: int) -> None: - task = thread.info - for reg in task.regs: - if reg == "rip" and register not in (16, -1): - continue - try: - thread.registers[reg].value = task.regs[reg] - except KeyError: - pass - - def fetch_scheduled(self, thread: gdb.InferiorThread, - register: int) -> None: - pass - -# pylint: disable=abstract-method -class _FRC_inactive_task_frame(_FetchRegistersBase): - def fetch_scheduled(self, thread: gdb.InferiorThread, - register: int) -> None: - task = thread.info.task_struct - - rsp = task['thread']['sp'].cast(types.unsigned_long_p_type) - - rsp = thread.arch.adjust_scheduled_frame_offset(rsp) - - thread.registers['rsp'].value = rsp - - frame = rsp.cast(types.inactive_task_frame_p_type).dereference() - - # Only write rip when requested; It resets the frame cache - if register in (16, -1): - thread.registers['rip'].value = thread.arch.get_scheduled_rip() - if register == 16: - return - - thread.registers['rbp'].value = frame['bp'] - thread.registers['rbx'].value = frame['bx'] - thread.registers['r12'].value = frame['r12'] - thread.registers['r13'].value = frame['r13'] - thread.registers['r14'].value = frame['r14'] - thread.registers['r15'].value = frame['r15'] - thread.registers['cs'].value = 2*8 - thread.registers['ss'].value = 3*8 - - thread.info.stack_pointer = rsp - thread.info.valid_stack = True - -class _FRC_thread_return(_FetchRegistersBase): - def fetch_scheduled(self, thread: gdb.InferiorThread, - register: int) -> None: - task = thread.info.task_struct - - # Only write rip when requested; It resets the frame cache - if register in (16, -1): - thread.registers['rip'].value = msymvals.thread_return - if register == 16: - return - - rsp = task['thread']['sp'].cast(types.unsigned_long_p_type) - rbp = rsp.dereference().cast(types.unsigned_long_p_type) - rbx = (rbp - 1).dereference() - r12 = (rbp - 2).dereference() - r13 = (rbp - 3).dereference() - r14 = (rbp - 4).dereference() - r15 = (rbp - 5).dereference() - - # The two pushes that don't have CFI info - # rsp += 2 - - # ex = in_exception_stack(rsp) - # if ex: - # print("EXCEPTION STACK: pid {:d}".format(task['pid'])) - - thread.registers['rsp'].value = rsp - thread.registers['rbp'].value = rbp - thread.registers['rbx'].value = rbx - thread.registers['r12'].value = r12 - thread.registers['r13'].value = r13 - thread.registers['r14'].value = r14 - thread.registers['r15'].value = r15 - thread.registers['cs'].value = 2*8 - thread.registers['ss'].value = 3*8 - - thread.info.stack_pointer = rsp - thread.info.valid_stack = True - -class x86_64Architecture(CrashArchitecture): - ident = "i386:x86-64" - aliases = ["x86_64"] - - _frame_offset : Optional[int] = None - - def __init__(self) -> None: - super(x86_64Architecture, self).__init__() - - # Stop stack traces with addresses below this - self.filter = KernelFrameFilter(0xffff000000000000) - - def setup_thread_info(self, thread: gdb.InferiorThread) -> None: - task = thread.info.task_struct - thread_info = task['stack'].cast(types.thread_info_p_type) - thread.info.set_thread_info(thread_info) - - # We don't have CFI for __switch_to_asm but we do know what it looks like. - # We push 6 registers and then swap rsp, so we can just rewind back - # to __switch_to_asm getting called and then populate the registers that - # were saved on the stack. - def setup_scheduled_frame_offset(self, task: gdb.Value) -> None: - if self._frame_offset: - return - - top = int(task['stack']) + 16*1024 - callq = re.compile("callq.*<(\w+)>") - - orig_rsp = rsp = task['thread']['sp'].cast(types.unsigned_long_p_type) - - count = 0 - while int(rsp) < top: - val = int(rsp.dereference()) - 5 - if val > self.filter.address: - try: - insn = gdb.execute(f"x/i {val:#x}", to_string=True) - except Exception as e: - rsp += 1 - count += 1 - continue - - m = callq.search(insn) - if m and m.group(1) == "__switch_to_asm": - self._frame_offset = rsp - orig_rsp + 1 - self._scheduled_rip = val - return - - rsp += 1 - count += 1 - - raise RuntimeError("Cannot locate stack frame offset for __schedule") - - def adjust_scheduled_frame_offset(self, rsp: gdb.Value) -> gdb.Value: - if self._frame_offset: - return rsp + self._frame_offset - return rsp - - def get_scheduled_rip(self) -> None: - return self._scheduled_rip - - @classmethod - # pylint: disable=unused-argument - def setup_inactive_task_frame_handler(cls, inactive: gdb.Type) -> None: - cls.set_fetch_registers(_FRC_inactive_task_frame) - - @classmethod - # pylint: disable=unused-argument - def setup_thread_return_handler(cls, inactive: gdb.Type) -> None: - cls.set_fetch_registers(_FRC_thread_return) - - @classmethod - def get_stack_pointer(cls, thread_struct: gdb.Value) -> int: - return int(thread_struct['sp']) - -type_cbs = TypeCallbacks([('struct inactive_task_frame', - x86_64Architecture.setup_inactive_task_frame_handler)]) -msymbol_cbs = MinimalSymbolCallbacks([('thread_return', - x86_64Architecture.setup_thread_return_handler)]) - -register_arch(x86_64Architecture) diff --git a/crash/commands/dev.py b/crash/commands/dev.py index 5a6ebdc1031..ced373ed364 100644 --- a/crash/commands/dev.py +++ b/crash/commands/dev.py @@ -17,14 +17,15 @@ class DevCommand(Command): """display character and block devices""" - def __init__(self, name : str) -> None: + def __init__(self, name: str) -> None: parser = ArgumentParser(prog=name) - parser.add_argument('-d', action='store_true', default=False) + parser.add_argument('-d', action='store_true', default=False, + required=True) super().__init__(name, parser) - def execute(self, args : argparse.Namespace) -> None: + def execute(self, args: argparse.Namespace) -> None: if args.d: print("{:^5} {:^16} {:^10} {:^16} {:^5} {:^5} {:^5} {:^5}" .format("MAJOR", "GENDISK", "NAME", "REQUEST_QUEUE", diff --git a/crash/commands/dmesg.py b/crash/commands/dmesg.py index 75348bb0437..c2d63868d2e 100644 --- a/crash/commands/dmesg.py +++ b/crash/commands/dmesg.py @@ -139,14 +139,9 @@ ... """ -from typing import Dict, Iterable, Any - import argparse -import gdb - from crash.commands import Command, ArgumentParser, CommandError -from crash.exceptions import DelayedAttributeError from crash.subsystem.printk import LogTypeException, LogInvalidOption from crash.subsystem.printk.lockless_ringbuffer import lockless_rb_show from crash.subsystem.printk.structured_ringbuffer import structured_rb_show diff --git a/crash/commands/kmem.py b/crash/commands/kmem.py index b7a1a573d4c..516fef814b2 100644 --- a/crash/commands/kmem.py +++ b/crash/commands/kmem.py @@ -75,6 +75,7 @@ def _find_kmem_cache(self, query: str) -> Optional[KmemCache]: pass return cache + # pylint: disable=too-many-return-statements def execute(self, args: argparse.Namespace) -> None: if args.z: self.print_zones() diff --git a/crash/exceptions.py b/crash/exceptions.py index d0c4d3b3cde..fe7e9d57ea3 100644 --- a/crash/exceptions.py +++ b/crash/exceptions.py @@ -17,6 +17,9 @@ class MissingSymbolError(RuntimeError): class MissingTypeError(RuntimeError): """The requested type cannot be located.""" +class MissingFieldError(RuntimeError): + """The requested field cannot be located.""" + class CorruptedError(RuntimeError): """A corrupted data structure has been encountered.""" diff --git a/crash/infra/__init__.py b/crash/infra/__init__.py index 3b15529054e..5e8f6119289 100644 --- a/crash/infra/__init__.py +++ b/crash/infra/__init__.py @@ -16,6 +16,8 @@ def autoload_submodules(caller: str, except KeyError: mod = importlib.import_module(caller) mods.append(caller) + if mod.__file__ is None: + return list() path = os.path.dirname(mod.__file__) modules = glob.glob("{}/[A-Za-z0-9_]*.py".format(path)) for modname in modules: diff --git a/crash/infra/callback.py b/crash/infra/callback.py index ddee8d5a5f2..ede7ec1daa0 100644 --- a/crash/infra/callback.py +++ b/crash/infra/callback.py @@ -1,7 +1,9 @@ # -*- coding: utf-8 -*- # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: -from typing import Callable, Any, Union, TypeVar, Optional +from typing import Any, Callable, List, Optional, TypeVar, Union + +import abc import gdb @@ -16,7 +18,7 @@ def __init__(self, callback_obj: 'ObjfileEventCallback') -> None: super().__init__(msg) self.callback_obj = callback_obj -class ObjfileEventCallback: +class ObjfileEventCallback(metaclass=abc.ABCMeta): """ A generic objfile callback class @@ -30,11 +32,61 @@ class ObjfileEventCallback: Consumers of this interface must also call :meth:`connect_callback` to connect the object to the callback infrastructure. """ - def __init__(self) -> None: + + _target_waitlist: List['ObjfileEventCallback'] = list() + _pending_list: List['ObjfileEventCallback'] = list() + _paused: bool = False + _connected_to_objfile_callback: bool = False + + def check_target(self) -> bool: + return isinstance(gdb.current_target(), gdb.LinuxKernelTarget) + + def __init__(self, wait_for_target: bool = True) -> None: self.completed = False self.connected = False + self._waiting_for_target = wait_for_target and not self.check_target() + + if not self._connected_to_objfile_callback: + # pylint: disable=no-member + gdb.events.new_objfile.connect(self._new_objfile_callback) + self._connected_to_objfile_callback = True + + # pylint: disable=unused-argument + @classmethod + def _new_objfile_callback(cls, event: gdb.NewObjFileEvent) -> None: + cls.evaluate_all() + + @classmethod + def target_ready(cls) -> None: + for callback in cls._target_waitlist: + callback.complete_wait_for_target() + + cls._target_waitlist[:] = list() + cls._update_pending() + + @classmethod + def evaluate_all(cls) -> None: + if not cls._paused: + for callback in cls._pending_list: + callback.evaluate(False) + cls._update_pending() - self._setup_symbol_cache_flush_callback() + @classmethod + def pause(cls) -> None: + cls._paused = True + + @classmethod + def unpause(cls) -> None: + cls._paused = False + cls.evaluate_all() + @classmethod + def dump_lists(cls) -> None: + print(f"Pending list: {[str(x) for x in ObjfileEventCallback._pending_list]}") + print(f"Target waitlist: {[str(x) for x in ObjfileEventCallback._target_waitlist]}") + + def complete_wait_for_target(self) -> None: + self._waiting_for_target = False + self.evaluate(False) def connect_callback(self) -> bool: """ @@ -49,27 +101,26 @@ def connect_callback(self) -> bool: if self.connected: return False - self.connected = True - - # We don't want to do lookups immediately if we don't have - # an objfile. It'll fail for any custom types but it can - # also return builtin types that are eventually changed. - objfiles = gdb.objfiles() - if objfiles: - result = self.check_ready() - if not (result is None or result is False): - completed = self.callback(result) - if completed is None: - completed = True - self.completed = completed + if not self._waiting_for_target: + # We don't want to do lookups immediately if we don't have + # an objfile. It'll fail for any custom types but it can + # also return builtin types that are eventually changed. + if gdb.objfiles(): + self.evaluate() + else: + self._target_waitlist.append(self) if self.completed is False: - # pylint: disable=no-member - gdb.events.new_objfile.connect(self._new_objfile_callback) + self.connected = True + self._pending_list.append(self) return self.completed - def complete(self) -> None: + @classmethod + def _update_pending(cls) -> None: + cls._pending_list[:] = [x for x in cls._pending_list if x.connected] + + def complete(self, update_now: bool = True) -> None: """ Complete and disconnect this callback from the event system. @@ -77,43 +128,26 @@ def complete(self) -> None: :obj:`CallbackCompleted`: This callback has already been completed. """ if not self.completed: - # pylint: disable=no-member - gdb.events.new_objfile.disconnect(self._new_objfile_callback) self.completed = True - self.connected = False + if self.connected: + self.connected = False + if update_now: + self._update_pending() else: raise CallbackCompleted(self) - _symbol_cache_flush_setup = False - @classmethod - def _setup_symbol_cache_flush_callback(cls) -> None: - if not cls._symbol_cache_flush_setup: - # pylint: disable=no-member - gdb.events.new_objfile.connect(cls._flush_symbol_cache_callback) - cls._symbol_cache_flush_setup = True - - - # GDB does this itself, but Python is initialized ahead of the - # symtab code. The symtab observer is behind the python observers - # in the execution queue so the cache flush executes /after/ us. - @classmethod - # pylint: disable=unused-argument - def _flush_symbol_cache_callback(cls, event: gdb.NewObjFileEvent) -> None: - gdb.execute("maint flush-symbol-cache") - - # pylint: disable=unused-argument - def _new_objfile_callback(self, event: gdb.NewObjFileEvent) -> None: - # GDB purposely copies the event list prior to calling the callbacks - # If we remove an event from another handler, it will still be sent - if self.completed: - return - - result = self.check_ready() - if not (result is None or result is False): - completed = self.callback(result) - if completed is True or completed is None: - self.complete() - + def evaluate(self, update_now: bool = True) -> None: + if not self._waiting_for_target: + try: + result = self.check_ready() + if not (result is None or result is False): + completed = self.callback(result) + if completed is True or completed is None: + self.complete(update_now) + except gdb.error: + pass + + @abc.abstractmethod def check_ready(self) -> Any: """ The method that derived classes implement for detecting when the @@ -124,8 +158,9 @@ def check_ready(self) -> Any: be passed untouched to :meth:`callback` if the result is anything other than :obj:`None` or :obj:`False`. """ - raise NotImplementedError("check_ready must be implemented by derived class.") + pass + @abc.abstractmethod def callback(self, result: Any) -> Optional[bool]: """ The callback that derived classes implement for handling the @@ -139,4 +174,19 @@ def callback(self, result: Any) -> Optional[bool]: the callback succeeded and will be completed and removed. Otherwise, the callback will stay connected for future completion. """ - raise NotImplementedError("callback must be implemented by derived class.") + pass + +def target_ready() -> None: + ObjfileEventCallback.target_ready() + +def evaluate_all() -> None: + ObjfileEventCallback.evaluate_all() + +def pause_objfile_callbacks() -> None: + ObjfileEventCallback.pause() + +def unpause_objfile_callbacks() -> None: + ObjfileEventCallback.unpause() + +def dump_lists() -> None: + ObjfileEventCallback.dump_lists() diff --git a/crash/infra/lookup.py b/crash/infra/lookup.py index ad50cdc0b0e..7d84ad47d77 100644 --- a/crash/infra/lookup.py +++ b/crash/infra/lookup.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: -from typing import Tuple, Any, Union, Optional +from typing import Any, Optional, Tuple, Type, Union import gdb @@ -9,6 +9,7 @@ from crash.infra.callback import Callback from crash.exceptions import DelayedAttributeError +# pylint: disable=abstract-method class NamedCallback(ObjfileEventCallback): """ A base class for Callbacks with names @@ -28,31 +29,14 @@ class NamedCallback(ObjfileEventCallback): attrname (:obj:`str`): The name of symbol or type being resolved translated for use as an attribute name. """ - def __init__(self, name: str, callback: Callback, - attrname: str = None) -> None: - super().__init__() + def __init__(self, name: str, callback: Callback, wait_for_target: bool = True, **kwargs: Any) -> None: + super().__init__(wait_for_target) self.name = name - self.attrname = self.name - - if attrname is not None: - self.attrname = attrname + self.attrname = kwargs.get('attrname', self.name) self._callback = callback - # This is silly but it avoids pylint abstract-method warnings - def check_ready(self) -> Any: - """ - The method that derived classes implement for detecting when the - conditions required to call the callback have been met. - - Returns: - :obj:`object`: This method can return an arbitrary object. It will - be passed untouched to :meth:`callback` if the result is anything - other than :obj:`None` or :obj:`False`. - """ - raise NotImplementedError("check_ready must be implemented by derived class.") - def callback(self, result: Any) -> Union[None, bool]: """ The callback for handling the sucessful result of :meth:`check_ready`. @@ -82,9 +66,9 @@ class MinimalSymbolCallback(NamedCallback): callback: The callback to execute when the minimal symbol is discovered symbol_file (optional): Name of the symbol file to use """ - def __init__(self, name: str, callback: Callback, + def __init__(self, name: str, callback: Callback, wait_for_target: bool = True, symbol_file: str = None) -> None: - super().__init__(name, callback) + super().__init__(name, callback, wait_for_target) self.symbol_file = symbol_file @@ -120,9 +104,9 @@ class SymbolCallback(NamedCallback): is assumed to be one of the value associated with :obj:`gdb.Symbol` constant, i.e. SYMBOL_*_DOMAIN. """ - def __init__(self, name: str, callback: Callback, + def __init__(self, name: str, callback: Callback, wait_for_target: bool = True, domain: int = gdb.SYMBOL_VAR_DOMAIN) -> None: - super().__init__(name, callback) + super().__init__(name, callback, wait_for_target) self.domain = domain @@ -183,11 +167,11 @@ class TypeCallback(NamedCallback): block (optional): The :obj:`gdb.Block` to search for the symbol """ - def __init__(self, name: str, callback: Callback, - block: gdb.Block = None) -> None: + def __init__(self, name: str, callback: Callback, wait_for_target: bool = True, + block: gdb.Block = None, **kwargs: Any) -> None: (name, attrname, self.pointer) = self.resolve_type(name) - super().__init__(name, callback, attrname) + super().__init__(name, callback, wait_for_target, attrname=attrname) self.block = block @@ -264,21 +248,23 @@ class DelayedValue: A generic class for making class attributes available that describe to-be-loaded symbols, minimal symbols, and types. """ - def __init__(self, name: str, attrname: str = None) -> None: + def __init__(self, name: str, wait_for_target: bool = True, **kwargs: Any) -> None: if name is None or not isinstance(name, str): raise ValueError("Name must be a valid string") self.name = name - - if attrname is None: - self.attrname = name - else: - self.attrname = attrname + self.wait_for_target = wait_for_target + self.attrname = kwargs.get('attrname', self.name) assert self.attrname is not None + self.cb: NamedCallback + self.value: Any = None + def attach_callback(self, cbcls: Type[NamedCallback], **kwargs: Any) -> None: + self.cb = cbcls(self.name, self.callback, self.wait_for_target, **kwargs) + def get(self) -> Any: if self.value is None: raise DelayedAttributeError(self.name) @@ -288,6 +274,13 @@ def callback(self, value: Any) -> None: if self.value is not None: return self.value = value + try: + del self.cb + except AttributeError: + pass + + def __str__(self) -> str: + return "{} attached with {}".format(self.__class__, str(self.cb)) class DelayedMinimalSymbol(DelayedValue): """ @@ -296,12 +289,9 @@ class DelayedMinimalSymbol(DelayedValue): Args: name: The name of the minimal symbol """ - def __init__(self, name: str) -> None: - super().__init__(name) - self.cb = MinimalSymbolCallback(name, self.callback) - - def __str__(self) -> str: - return "{} attached with {}".format(self.__class__, str(self.cb)) + def __init__(self, name: str, wait_for_target: bool = True) -> None: + super().__init__(name, wait_for_target) + self.attach_callback(MinimalSymbolCallback) class DelayedSymbol(DelayedValue): """ @@ -310,12 +300,9 @@ class DelayedSymbol(DelayedValue): Args: name: The name of the symbol """ - def __init__(self, name: str) -> None: - super().__init__(name) - self.cb = SymbolCallback(name, self.callback) - - def __str__(self) -> str: - return "{} attached with {}".format(self.__class__, str(self.cb)) + def __init__(self, name: str, wait_for_target: bool = True) -> None: + super().__init__(name, wait_for_target) + self.attach_callback(SymbolCallback) class DelayedType(DelayedValue): """ @@ -324,10 +311,11 @@ class DelayedType(DelayedValue): Args: name: The name of the type. """ - def __init__(self, name: str) -> None: + def __init__(self, name: str, wait_for_target: bool = True, + block: gdb.Block = None) -> None: (name, attrname, self.pointer) = TypeCallback.resolve_type(name) - super().__init__(name, attrname) - self.cb = TypeCallback(name, self.callback) + super().__init__(name, wait_for_target, attrname=attrname) + self.attach_callback(TypeCallback, block=block) def __str__(self) -> str: return "{} attached with {}".format(self.__class__, str(self.callback)) @@ -352,7 +340,7 @@ def callback(self, value: gdb.Symbol) -> None: self.value = symval def __str__(self) -> str: - return "{} attached with {}".format(self.__class__, str(self.cb)) + return "{} attached with {}".format(self.__class__, str(self.callback)) class DelayedMinimalSymval(DelayedMinimalSymbol): """ @@ -364,6 +352,3 @@ class DelayedMinimalSymval(DelayedMinimalSymbol): """ def callback(self, value: gdb.MinSymbol) -> None: self.value = int(value.value().address) - - def __str__(self) -> str: - return "{} attached with {}".format(self.__class__, str(self.cb)) diff --git a/crash/kernel.py b/crash/kernel.py index b5f1ada38a3..d997237c5a4 100644 --- a/crash/kernel.py +++ b/crash/kernel.py @@ -1,24 +1,29 @@ # -*- coding: utf-8 -*- # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: -from typing import Pattern, Union, List, Dict, Any, Optional +from typing import Pattern, Union, List, Dict, Any, Optional, BinaryIO import sys import re import fnmatch import os.path +import tempfile from elftools.elf.elffile import ELFFile import gdb +# This is from the C extension and published via __all__; pylint bug? +# pylint: disable=no-name-in-module +from zstd import decompress as zstd_decompress + import crash -import crash.arch -import crash.arch.x86_64 -import crash.arch.ppc64 +import crash.target from crash.types.module import for_each_module, for_each_module_section from crash.util import get_symbol_value -from crash.util.symbols import Types, Symvals, Symbols +from crash.util.symbols import Types from crash.exceptions import MissingSymbolError, InvalidArgumentError +from crash.infra.callback import pause_objfile_callbacks, unpause_objfile_callbacks +from crash.cache.syscache import utsname class CrashKernelError(RuntimeError): """Raised when an error occurs while initializing the debugging session""" @@ -125,8 +130,6 @@ class CrashKernel: """ types = Types(['char *']) - symvals = Symvals(['init_task']) - symbols = Symbols(['runqueues']) # pylint: disable=unused-argument def __init__(self, roots: PathSpecifier = None, @@ -134,6 +137,9 @@ def __init__(self, roots: PathSpecifier = None, module_path: PathSpecifier = None, module_debuginfo_path: PathSpecifier = None, verbose: bool = False, debug: bool = False) -> None: + + self.target = crash.target.check_target() + self.findmap: Dict[str, Dict[Any, Any]] = dict() self.modules_order: Dict[str, Dict[str, str]] = dict() obj = gdb.objfiles()[0] @@ -171,19 +177,6 @@ def __init__(self, roots: PathSpecifier = None, self.vermagic = self.extract_vermagic() - archname = obj.architecture.name() - try: - archclass = crash.arch.get_architecture(archname) - except RuntimeError as e: - raise CrashKernelError(str(e)) from e - - self.arch = archclass() - - self.target = crash.current_target() - self.vmcore = self.target.kdump - - self.crashing_thread: Optional[gdb.InferiorThread] = None - def _setup_roots(self, roots: PathSpecifier = None, verbose: bool = False) -> None: if roots is None: @@ -367,20 +360,19 @@ def extract_vermagic(self) -> str: return self._get_minsymbol_as_string('vermagic') - def extract_modinfo_from_module(self, modpath: str) -> Dict[str, str]: + def extract_modinfo_from_module(self, modfile: BinaryIO) -> Dict[str, str]: """ Returns the modinfo from a module file Args: - modpath: The path to the module file. + modpath: An open module file Returns: dict: A dictionary containing the names and values of the modinfo variables. """ - f = open(modpath, 'rb') - elf = ELFFile(f) + elf = ELFFile(modfile) modinfo = elf.get_section_by_name('.modinfo') d = {} @@ -391,7 +383,6 @@ def extract_modinfo_from_module(self, modpath: str) -> Dict[str, str]: d[val[0:eq]] = val[eq + 1:] del elf - f.close() return d def _get_module_sections(self, module: gdb.Value) -> str: @@ -400,8 +391,9 @@ def _get_module_sections(self, module: gdb.Value) -> str: out.append("-s {} {:#x}".format(name, addr)) return " ".join(out) - def _check_module_version(self, modpath: str, module: gdb.Value) -> None: - modinfo = self.extract_modinfo_from_module(modpath) + def _check_module_version(self, modfile: BinaryIO, module: gdb.Value) -> None: + modinfo = self.extract_modinfo_from_module(modfile) + modpath = modfile.name vermagic = modinfo.get('vermagic', None) @@ -418,6 +410,58 @@ def _check_module_version(self, modpath: str, module: gdb.Value) -> None: raise _ModSourceVersionMismatchError(modpath, mi_srcversion, mod_srcversion) + def _try_load_module(self, modname: str, module: gdb.Value, modfile: BinaryIO, + verbose: bool = False, debug: bool = False) -> gdb.Objfile: + self._check_module_version(modfile, module) + + modpath = modfile.name + + if 'module_core' in module.type: + addr = int(module['module_core']) + else: + addr = int(module['core_layout']['base']) + + if debug: + print(f"Loading {modpath} at {addr:#x} from {modname}") + elif verbose: + print(f"Loading {modname} at {addr:#x}") + else: + print(".", end='') + sys.stdout.flush() + + sections = self._get_module_sections(module) + + percpu = int(module['percpu']) + if percpu > 0: + sections += " -s .data..percpu {:#x}".format(percpu) + + sections += " -o 0xff000000" + + try: + result = gdb.execute("add-symbol-file {} {:#x} {}" + .format(modpath, addr, sections), + to_string=True) + except gdb.error as e: + raise CrashKernelError("Error while loading module `{}': {}" + .format(modname, str(e))) from e + if debug: + print(result) + + return gdb.lookup_objfile(modpath) + + def try_load_module(self, modname: str, module: gdb.Value, modpath: str, + tmpdirname: str, + verbose: bool = False, debug: bool = False) -> gdb.Objfile: + if modpath.endswith(".zst"): + with open(modpath, 'rb') as cmodfile: + with open(os.path.join(tmpdirname, modname + ".ko"), 'w+b') as modfile: + modfile.write(zstd_decompress(cmodfile.read())) + return self._try_load_module(modname, module, modfile, debug) + else: + with open(modpath, 'rb') as modfile: + return self._try_load_module(modname, module, modfile, debug) + + def load_modules(self, verbose: bool = False, debug: bool = False) -> None: """ Load modules (including debuginfo) into the crash session. @@ -435,82 +479,57 @@ def load_modules(self, verbose: bool = False, debug: bool = False) -> None: This does not include a failure to locate a module or its debuginfo. """ - import crash.cache.syscache # pylint: disable=redefined-outer-name - version = crash.cache.syscache.utsname.release - print("Loading modules for {}".format(version), end='') + count = 0 + for module in for_each_module(): + count += 1 + print(f"Loading {count} modules for {utsname.release}", end='') if verbose: print(":", flush=True) + else: + print(".", end='', flush=True) failed = 0 loaded = 0 - for module in for_each_module(): - modname = "{}".format(module['name'].string()) - modfname = "{}.ko".format(modname) - found = False - for path in self.module_path: - - try: - modpath = self._find_module_file(modfname, path) - except _NoMatchingFileError: - continue - - try: - self._check_module_version(modpath, module) - except _ModinfoMismatchError as e: - if verbose: - print(str(e)) - continue - found = True + pause_objfile_callbacks() + with tempfile.TemporaryDirectory() as tmpdirname: + for module in for_each_module(): + modname = module['name'].string() + modfname = f"{modname}.ko" + objfile = None + for path in self.module_path: + + try: + modpath = self._find_module_file(modfname, path) + except _NoMatchingFileError: + continue + + try: + objfile = self.try_load_module(modname, module, modpath, + tmpdirname, verbose, debug) + except (_ModinfoMismatchError, OSError) as e: + if verbose: + print(f"Module open failed: {str(e)}") + continue + + if not objfile.has_symbols(): + self._load_module_debuginfo(objfile, modpath, verbose) + elif debug: + print(" + has debug symbols") + break - if 'module_core' in module.type: - addr = int(module['module_core']) + if objfile: + if not objfile.has_symbols(): + print("Couldn't find debuginfo for {}".format(modname)) + loaded += 1 else: - addr = int(module['core_layout']['base']) + if failed == 0: + print() + print("Couldn't find module file for {}".format(modname)) + failed += 1 - if debug: - print("Loading {} at {:#x}".format(modpath, addr)) - elif verbose: - print("Loading {} at {:#x}".format(modname, addr)) - else: + if (loaded + failed) % 10 == 10: print(".", end='') sys.stdout.flush() - - sections = self._get_module_sections(module) - - percpu = int(module['percpu']) - if percpu > 0: - sections += " -s .data..percpu {:#x}".format(percpu) - - try: - result = gdb.execute("add-symbol-file {} {:#x} {}" - .format(modpath, addr, sections), - to_string=True) - except gdb.error as e: - raise CrashKernelError("Error while loading module `{}': {}" - .format(modname, str(e))) from e - if debug: - print(result) - - objfile = gdb.lookup_objfile(modpath) - if not objfile.has_symbols(): - self._load_module_debuginfo(objfile, modpath, verbose) - elif debug: - print(" + has debug symbols") - - break - - if not found: - if failed == 0: - print() - print("Couldn't find module file for {}".format(modname)) - failed += 1 - else: - if not objfile.has_symbols(): - print("Couldn't find debuginfo for {}".format(modname)) - loaded += 1 - if (loaded + failed) % 10 == 10: - print(".", end='') - sys.stdout.flush() print(" done. ({} loaded".format(loaded), end='') if failed: print(", {} failed)".format(failed)) @@ -520,6 +539,7 @@ def load_modules(self, verbose: bool = False, debug: bool = False) -> None: # We shouldn't need this again, so why keep it around? del self.findmap self.findmap = {} + unpause_objfile_callbacks() def _normalize_modname(self, mod: str) -> str: return mod.replace('-', '_') @@ -537,6 +557,8 @@ def _cache_modules_order(self, path: str) -> None: modpath = os.path.join(path, modpath) if os.path.exists(modpath): self.modules_order[path][modname] = modpath + if os.path.exists(modpath + ".zst"): + self.modules_order[path][modname] = modpath + ".zst" f.close() except OSError: pass @@ -641,7 +663,9 @@ def _load_module_debuginfo(self, objfile: gdb.Objfile, if modpath is None: raise RuntimeError("loaded objfile has no filename???") if ".gz" in modpath: - modpath = modpath.replace(".gz", "") + modpath = modpath[:-3] + elif ".zst" in modpath: + modpath = modpath[:-4] filename = "{}.debug".format(os.path.basename(modpath)) build_id_path = self.build_id_path(objfile) @@ -659,67 +683,3 @@ def _load_module_debuginfo(self, objfile: gdb.Objfile, if self._try_load_debuginfo(objfile, filepath, verbose): break - - def setup_tasks(self) -> None: - """ - Populate GDB's thread list using the kernel's task lists - - This method will iterate over the kernel's task lists, create a - LinuxTask object, and create a gdb thread for each one. The - threads will be built so that the registers are ready to be - populated, which allows symbolic stack traces to be made available. - """ - from crash.types.percpu import get_percpu_vars - from crash.types.task import LinuxTask, for_each_all_tasks - import crash.cache.tasks # pylint: disable=redefined-outer-name - gdb.execute('set print thread-events 0') - - rqs = get_percpu_vars(self.symbols.runqueues) - rqscurrs = {int(x["curr"]) : k for (k, x) in rqs.items()} - - print("Loading tasks...", end='') - sys.stdout.flush() - - task_count = 0 - try: - crashing_cpu = int(get_symbol_value('crashing_cpu')) - except MissingSymbolError: - crashing_cpu = -1 - - for task in for_each_all_tasks(): - ltask = LinuxTask(task) - - active = int(task.address) in rqscurrs - if active: - cpu = rqscurrs[int(task.address)] - regs = self.vmcore.attr.cpu[cpu].reg - ltask.set_active(cpu, regs) - else: - self.arch.setup_scheduled_frame_offset(task) - - ptid = (LINUX_KERNEL_PID, task['pid'], 0) - - try: - thread = gdb.selected_inferior().new_thread(ptid) - thread.info = ltask - thread.arch = self.arch - except gdb.error: - print("Failed to setup task @{:#x}".format(int(task.address))) - continue - thread.name = task['comm'].string() - if active and cpu == crashing_cpu: - self.crashing_thread = thread - - self.arch.setup_thread_info(thread) - ltask.attach_thread(thread) - ltask.set_get_stack_pointer(self.arch.get_stack_pointer) - - crash.cache.tasks.cache_task(ltask) - - task_count += 1 - if task_count % 100 == 0: - print(".", end='') - sys.stdout.flush() - print(" done. ({} tasks total)".format(task_count)) - - gdb.selected_inferior().executing = False diff --git a/crash/requirements/__init__.py b/crash/requirements/__init__.py index 5e1b217e31d..1468f05e0de 100644 --- a/crash/requirements/__init__.py +++ b/crash/requirements/__init__.py @@ -25,19 +25,13 @@ raise IncompatibleGDBError("gdb.MinSymbol") from e try: - x4 = gdb.Register + x4 = gdb.RegisterDescriptor del x4 except AttributeError as e: raise IncompatibleGDBError("gdb.Register") from e try: - x6 = gdb.Inferior.new_thread - del x6 + x5 = gdb.LinuxKernelTarget + del x5 except AttributeError as e: - raise IncompatibleGDBError("gdb.Inferior.new_thread") from e - -try: - x7 = gdb.Objfile.architecture - del x7 -except AttributeError as e: - raise IncompatibleGDBError("gdb.Objfile.architecture") from e + raise IncompatibleGDBError("gdb.LinuxKernelTarget") from e diff --git a/crash/requirements/test_target.py b/crash/requirements/test_target.py index 06e2c5e5acc..38bfd157777 100644 --- a/crash/requirements/test_target.py +++ b/crash/requirements/test_target.py @@ -1,12 +1,12 @@ # -*- coding: utf-8 -*- # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: -from typing import Tuple +from typing import Optional, Tuple import gdb PTID = Tuple[int, int, int] -class TestTarget(gdb.Target): +class TestTarget(gdb.LinuxKernelTarget): def __init__(self) -> None: super().__init__() @@ -21,13 +21,9 @@ def close(self) -> None: pass def fetch_registers(self, thread: gdb.InferiorThread, - register: gdb.Register) -> None: + register: Optional[gdb.RegisterDescriptor]) -> Optional[gdb.RegisterCollectionType]: pass # pylint: disable=unused-argument def thread_alive(self, ptid: PTID) -> bool: return True - - def setup_task(self) -> None: - ptid = (1, 1, 0) - gdb.selected_inferior().new_thread(ptid, self) diff --git a/crash/session.py b/crash/session.py index 9f912e8b847..e47a8cd7a0e 100644 --- a/crash/session.py +++ b/crash/session.py @@ -1,10 +1,16 @@ # -*- coding: utf-8 -*- # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: +from typing import List, Union + import gdb from crash.infra import autoload_submodules -from crash.kernel import CrashKernel, CrashKernelError +import crash.target +import crash.target.ppc64 +import crash.target.x86_64 + +PathSpecifier = Union[List[str], str] class Session: """ @@ -20,27 +26,38 @@ class Session: debug (optional, default=False): Whether to enable verbose debugging output """ - def __init__(self, kernel: CrashKernel, verbose: bool = False, - debug: bool = False) -> None: + def __init__(self, roots: PathSpecifier = None, + vmlinux_debuginfo: PathSpecifier = None, + module_path: PathSpecifier = None, + module_debuginfo_path: PathSpecifier = None, + verbose: bool = False, debug: bool = False) -> None: print("crash-python initializing...") - self.kernel = kernel + + self.debug = debug + self.verbose = verbose + + target = crash.target.setup_target() + from crash.kernel import CrashKernel, CrashKernelError + + self.kernel = CrashKernel(roots, vmlinux_debuginfo, module_path, + module_debuginfo_path, verbose, debug) autoload_submodules('crash.cache') autoload_submodules('crash.subsystem') autoload_submodules('crash.commands') try: - self.kernel.setup_tasks() + print("Loading modules") self.kernel.load_modules(verbose=verbose, debug=debug) except CrashKernelError as e: print(str(e)) print("Further debugging may not be possible.") return - if self.kernel.crashing_thread: + if target.crashing_thread: try: result = gdb.execute("thread {}" - .format(self.kernel.crashing_thread.num), + .format(target.crashing_thread.num), to_string=True) if debug: print(result) @@ -51,5 +68,5 @@ def __init__(self, kernel: CrashKernel, verbose: bool = False, return print("Backtrace from crashing task (PID {:d}):" - .format(self.kernel.crashing_thread.ptid[1])) + .format(target.crashing_thread.ptid[1])) gdb.execute("where") diff --git a/crash/subsystem/printk/lockless_ringbuffer.py b/crash/subsystem/printk/lockless_ringbuffer.py index 736281d3d33..bb6362c0675 100644 --- a/crash/subsystem/printk/lockless_ringbuffer.py +++ b/crash/subsystem/printk/lockless_ringbuffer.py @@ -1,15 +1,13 @@ # -*- coding: utf-8 -*- # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: -from typing import Dict, Iterable, Any - import argparse -import sys + import gdb from crash.util.symbols import Types, Symvals from crash.exceptions import DelayedAttributeError -from crash.subsystem.printk import LogTypeException, LogInvalidOption +from crash.subsystem.printk import LogTypeException types = Types(['struct printk_info *', 'struct prb_desc *', @@ -92,11 +90,11 @@ def desc_state(self) -> int: ''' Return state of the descriptor ''' return (self.state_var & self.sv_mask) >> self.sv_shift - def is_finalized(self): + def is_finalized(self) -> bool: ''' Finalized desriptor points to a valid (deta) message ''' return self.desc_state() == 0x2 - def is_reusable(self): + def is_reusable(self) -> bool: ''' Reusable descriptor still has a valid sequence number but the data are gone. @@ -131,10 +129,10 @@ def get_data_block(self, blk_lpos: PrbDataBlkLPos) -> PrbDataBlock: blk_p = self.data.cast(types.char_p_type) + begin_idx return PrbDataBlock(blk_p.cast(types.prb_data_block_p_type)) - def get_text(self, blk_lpos: PrbDataBlkLPos, len: int) -> str: + def get_text(self, blk_lpos: PrbDataBlkLPos, _len: int) -> str: ''' return string stored at the given blk_lpos ''' data_block = self.get_data_block(blk_lpos) - return data_block.data.cast(types.char_p_type).string(length=len) + return data_block.data.cast(types.char_p_type).string(length=_len) class PrbDescRing: @@ -154,20 +152,20 @@ def __init__(self, dr: gdb.Value) -> None: self.tail_id = atomic_long_read(dr['tail_id']) self.mask_id = (1 << self.count_bits) - 1 - def get_idx(self, id: int) -> int: + def get_idx(self, _id: int) -> int: ''' Return index to the desc ring for the given id ''' - return id & self.mask_id + return _id & self.mask_id - def get_desc(self, id: int) -> PrbDesc: + def get_desc(self, _id: int) -> PrbDesc: ''' Return prb_desc structure for the given id ''' - idx = self.get_idx(id) + idx = self.get_idx(_id) desc_p = (self.descs.cast(types.char_p_type) + types.prb_desc_p_type.target().sizeof * idx) return PrbDesc(desc_p.cast(types.prb_desc_p_type)) - def get_info(self, id: int) -> PrintkInfo: + def get_info(self, _id: int) -> PrintkInfo: ''' return printk_info structure for the given id ''' - idx = self.get_idx(id) + idx = self.get_idx(_id) info_p = (self.infos.cast(types.char_p_type) + types.printk_info_p_type.target().sizeof * idx) return PrintkInfo(info_p.cast(types.printk_info_p_type)) @@ -184,10 +182,10 @@ def __init__(self, prb: gdb.Value) -> None: def is_valid_desc(self, desc: PrbDesc, info: PrintkInfo, seq: int) -> bool: ''' Does the descritor constains consistent values? ''' - if (not (desc.is_finalized() or desc.is_reusable())): + if not (desc.is_finalized() or desc.is_reusable()): return False # Must match the expected seq number. Otherwise is being updated. - return (info.seq == seq) + return info.seq == seq def first_seq(self) -> int: ''' @@ -202,11 +200,11 @@ def first_seq(self) -> int: # As a result, the valid sequence number should be either in tail_id # or tail_id + 1 entry. for i in range(0, 1): - id = self.desc_ring.tail_id + i - desc = self.desc_ring.get_desc(id) + _id = self.desc_ring.tail_id + i + desc = self.desc_ring.get_desc(_id) - if (desc.is_finalized() or desc.is_reusable()): - info = self.desc_ring.get_info(id) + if desc.is_finalized() or desc.is_reusable(): + info = self.desc_ring.get_info(_id) return info.seq # Something went wrong. Do not continue with an invalid sequence number. @@ -230,13 +228,13 @@ def show_msg(self, desc: PrbDesc, info: PrintkInfo, level = '<{:d}>'.format(info.level) text = self.data_ring.get_text(desc.text_blk_lpos, info.text_len) - print('{}{}{}'.format(level,timestamp,text)) + print('{}{}{}'.format(level, timestamp, text)) - if (args.d): + if args.d: # Only two dev_info values are supported at the moment - if (len(info.dev_info.subsystem)): + if info.dev_info.subsystem: print(' SUBSYSTEM={}'.format(info.dev_info.subsystem)) - if (len(info.dev_info.device)): + if info.dev_info.device: print(' DEVICE={}'.format(info.dev_info.device)) def show_log(self, args: argparse.Namespace) -> None: @@ -247,7 +245,7 @@ def show_log(self, args: argparse.Namespace) -> None: while True: desc = self.desc_ring.get_desc(seq) info = self.desc_ring.get_info(seq) - if (not self.is_valid_desc(desc, info, seq)): + if not self.is_valid_desc(desc, info, seq): break seq += 1 @@ -255,7 +253,7 @@ def show_log(self, args: argparse.Namespace) -> None: # Sequence numbers are stored in separate ring buffer. # The descriptor ring might include valid sequence numbers # but the data might already be replaced. - if (desc.is_reusable()): + if desc.is_reusable(): continue self.show_msg(desc, info, args) @@ -273,10 +271,8 @@ def lockless_rb_show(args: argparse.Namespace) -> None: """ try: - test = symvals.prb + prb = PrbRingBuffer(symvals.prb) except DelayedAttributeError: raise LogTypeException('not lockless log') from None - prb = PrbRingBuffer(symvals.prb) - prb.show_log(args) diff --git a/crash/subsystem/printk/plain_ringbuffer.py b/crash/subsystem/printk/plain_ringbuffer.py index fd4174f3732..c8c8fc99528 100644 --- a/crash/subsystem/printk/plain_ringbuffer.py +++ b/crash/subsystem/printk/plain_ringbuffer.py @@ -4,10 +4,8 @@ import argparse import re -import gdb - from crash.util.symbols import Types, Symvals -from crash.subsystem.printk import LogTypeException, LogInvalidOption +from crash.subsystem.printk import LogInvalidOption types = Types(['char *']) symvals = Symvals(['log_buf', 'log_buf_len']) diff --git a/crash/subsystem/printk/structured_ringbuffer.py b/crash/subsystem/printk/structured_ringbuffer.py index 456672c10e4..bf389f68193 100644 --- a/crash/subsystem/printk/structured_ringbuffer.py +++ b/crash/subsystem/printk/structured_ringbuffer.py @@ -9,7 +9,7 @@ from crash.util.symbols import Types, Symvals from crash.exceptions import DelayedAttributeError -from crash.subsystem.printk import LogTypeException, LogInvalidOption +from crash.subsystem.printk import LogTypeException types = Types(['struct printk_log *', 'char *']) symvals = Symvals(['log_buf', 'log_buf_len', 'log_first_idx', 'log_next_idx', @@ -31,7 +31,7 @@ def log_from_idx(logbuf: gdb.Value, idx: int) -> Dict: dictval = (msg.cast(types.char_p_type) + types.printk_log_p_type.target().sizeof + textlen) - dict = dictval.string(length=dictlen) + msgdict = dictval.string(length=dictlen) msglen = int(msg['len']) @@ -41,16 +41,14 @@ def log_from_idx(logbuf: gdb.Value, idx: int) -> Dict: else: nextidx = idx + msglen - msgdict = { + return { 'text' : text[0:textlen], 'timestamp' : int(msg['ts_nsec']), 'level' : int(msg['level']), 'next' : nextidx, - 'dict' : dict[0:dictlen], + 'dict' : msgdict[0:dictlen], } - return msgdict - def get_log_msgs() -> Iterable[Dict[str, Any]]: try: idx = symvals.log_first_idx @@ -88,5 +86,5 @@ def structured_rb_show(args: argparse.Namespace) -> None: print('{}{}{}'.format(level, timestamp, line)) if (args.d and msg['dict']): - for dict in msg['dict'].split('\0'): - print(' {}'.format(dict)) + for entry in msg['dict'].split('\0'): + print(' {}'.format(entry)) diff --git a/crash/subsystem/storage/__init__.py b/crash/subsystem/storage/__init__.py index 1ed6ec90b4c..2ee6921a2c2 100644 --- a/crash/subsystem/storage/__init__.py +++ b/crash/subsystem/storage/__init__.py @@ -1,19 +1,19 @@ # -*- coding: utf-8 -*- # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: -from typing import Iterable +from typing import Callable, Iterable import gdb from gdb.types import get_basic_type -from crash.util import container_of, struct_has_member +from crash.util import container_of, struct_has_member, InvalidComponentError from crash.util.symbols import Types, Symvals, SymbolCallbacks, TypeCallbacks from crash.types.classdev import for_each_class_device from crash.exceptions import DelayedAttributeError, InvalidArgumentError from crash.cache.syscache import kernel, jiffies_to_msec types = Types(['struct gendisk', 'struct hd_struct', 'struct device', - 'struct device_type', 'struct bdev_inode', + 'struct device_type', 'struct bdev_inode', 'struct block_device', 'struct request_queue', 'struct request', 'enum req_flag_bits', 'enum mq_rq_state', 'enum rq_atomic_flags']) symvals = Symvals(['block_class', 'blockdev_superblock', 'disk_type', @@ -21,6 +21,28 @@ READ = 0 WRITE = 1 +# Values will be filled in via callback. These are declared here to honor +# imports for lint. +REQ_FUA: int +REQ_PREFLUSH: int +REQ_STARTED: int +REQ_SYNC: int + +def dev_to_bdev(dev: gdb.Value) -> gdb.Value: + """ + Converts a ``struct device'' that is embedded in a ``struct block_device`` + back to the ``struct block_device``. + + Args: + dev: A ``struct device'' contained within a ``struct block_device``. + The vlaue must be of type ``struct device``. + + Returns: + :obj:`gdb.Value`: The converted block device. The value is of type + ``struct block_device``. + """ + return container_of(dev, types.block_device_type, 'bd_device') + def dev_to_gendisk(dev: gdb.Value) -> gdb.Value: """ Converts a ``struct device`` that is embedded in a ``struct gendisk`` @@ -34,7 +56,10 @@ def dev_to_gendisk(dev: gdb.Value) -> gdb.Value: :obj:`gdb.Value`: The converted gendisk. The value is of type ``struct gendisk``. """ - return container_of(dev, types.gendisk_type, 'part0.__dev') + try: + return container_of(dev, types.gendisk_type, 'part0.__dev') + except InvalidComponentError: + return dev_to_bdev(dev)['bd_disk'] def dev_to_part(dev: gdb.Value) -> gdb.Value: """ @@ -66,6 +91,9 @@ def gendisk_to_dev(gendisk: gdb.Value) -> gdb.Value: of type ``struct device``. """ + if struct_has_member(gendisk['part0'], 'bd_device'): + return gendisk['part0']['bd_device'] + return gendisk['part0']['__dev'] def part_to_dev(part: gdb.Value) -> gdb.Value: @@ -295,12 +323,21 @@ def rq_is_sync(request: gdb.Value) -> bool: :obj:`bool`: True for synchronous requests, False otherwise. """ return (request['cmd_flags'] & 1 == 0 or - request['cmd_flags'] & (REQ_SYNC | REQ_FUA | REQ_PREFLUSH) != 0) # type: ignore + request['cmd_flags'] & (REQ_SYNC | REQ_FUA | REQ_PREFLUSH) != 0) + -# This is a stub to make static checker happy. It gets overridden once 'struct -# request' is resolved. -def _rq_in_flight(request: gdb.Value) -> bool: - raise RuntimeError("struct request type not resolved yet!") +_rq_in_flight: Callable[[gdb.Value], bool] + +def _rq_in_flight_rq_state(request: gdb.Value) -> bool: + return (request['rq_state'] != + types.enum_mq_rq_state_type['MQ_RQ_IDLE']) + +def _rq_in_flight_atomic_flags(request: gdb.Value) -> bool: + return (request['atomic_flags'] & + (1 << int(types.enum_rq_atomic_flags_type['REQ_ATOM_STARTED'].enumval)) != 0) + +def _rq_in_flight_cmd_flags(request: gdb.Value) -> bool: + return request['cmd_flags'] & REQ_STARTED != 0 def rq_in_flight(request: gdb.Value) -> bool: """ @@ -359,22 +396,16 @@ def _export_req_flags(req_flag_bits: gdb.Type) -> None: # Check struct request and define functions based on its current form in this # kernel def _check_struct_request(request_s: gdb.Type) -> None: - global _rq_in_flight if struct_has_member(request_s, 'rq_state'): - def _rq_in_flight(request: gdb.Value) -> bool: - return (request['rq_state'] != - types.enum_mq_rq_state_type['MQ_RQ_IDLE']) + impl = _rq_in_flight_rq_state elif struct_has_member(request_s, 'atomic_flags'): - def _rq_in_flight(request: gdb.Value) -> bool: - return (request['atomic_flags'] & - (1 << int(types.enum_rq_atomic_flags_type['REQ_ATOM_STARTED'].enumval)) != 0) + impl = _rq_in_flight_atomic_flags else: - def _rq_in_flight(request: gdb.Value) -> bool: - return request['cmd_flags'] & REQ_STARTED != 0 # type: ignore + impl = _rq_in_flight_cmd_flags + globals()['_rq_in_flight'] = impl symbol_cbs = SymbolCallbacks([('disk_type', _check_types), ('part_type', _check_types)]) type_cbs = TypeCallbacks([('struct device_type', _check_types), ('enum req_flag_bits', _export_req_flags), ('struct request', _check_struct_request)]) - diff --git a/crash/subsystem/storage/block.py b/crash/subsystem/storage/block.py index 2427a17a441..62abdf33ca2 100644 --- a/crash/subsystem/storage/block.py +++ b/crash/subsystem/storage/block.py @@ -5,7 +5,6 @@ import gdb -from crash.util.symbols import Types from crash.subsystem.storage import queue_is_mq from crash.subsystem.storage.blocksq import sq_for_each_request_in_queue, \ sq_requests_in_flight, sq_requests_queued diff --git a/crash/subsystem/storage/blockmq.py b/crash/subsystem/storage/blockmq.py index 407a3bd7903..b48455228f8 100644 --- a/crash/subsystem/storage/blockmq.py +++ b/crash/subsystem/storage/blockmq.py @@ -14,7 +14,7 @@ class NoQueueError(RuntimeError): pass types = Types(['struct request', 'struct request_queue', - 'struct sbitmap_queue', 'struct blk_mq_hw_ctx' ]) + 'struct sbitmap_queue', 'struct blk_mq_hw_ctx']) def _check_queue_type(queue: gdb.Value) -> None: if not queue_is_mq(queue): diff --git a/crash/target/__init__.py b/crash/target/__init__.py new file mode 100644 index 00000000000..320d002e7de --- /dev/null +++ b/crash/target/__init__.py @@ -0,0 +1,243 @@ +# -*- coding: utf-8 -*- +# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: + +from typing import Any, Iterator, List, Optional, Tuple, Type + +import abc +import sys + +import gdb + +from crash.exceptions import MissingSymbolError +import crash.infra.callback + +from crash.types.percpu import get_percpu_vars +from crash.util.symbols import Symbols, Symvals +from crash.util import get_typed_pointer + +symbols = Symbols(['runqueues']) +symvals = Symvals(['crashing_cpu']) + +class IncorrectTargetError(ValueError): + """Incorrect target implementation for this kernel""" + pass + +PTID = Tuple[int, int, int] + +# This keeps stack traces from continuing into userspace and causing problems. +class KernelFrameFilter: + def __init__(self, address: int) -> None: + self.name = "KernelFrameFilter" + self.priority = 100 + self.enabled = True + self.address = address + gdb.frame_filters[self.name] = self + + def filter(self, frame_iter: Iterator[Any]) -> Any: + return KernelAddressIterator(frame_iter, self.address) + +class KernelAddressIterator: + def __init__(self, ii: Iterator, address: int) -> None: + self.input_iterator = ii + self.address = address + + def __iter__(self) -> Any: + return self + + def __next__(self) -> Any: + frame = next(self.input_iterator) + + if frame.inferior_frame().pc() < self.address: + raise StopIteration + + return frame + +# A working target will be a mixin composed of a class derived from +# TargetBase and TargetFetchRegistersBase + +class TargetBase(gdb.LinuxKernelTarget, metaclass=abc.ABCMeta): + def __init__(self, debug: int = 0) -> None: + super().__init__() + + self.debug = debug + self.shortname = "Crash-Python Linux Target" + self.longname = "Use a Core file as a Linux Kernel Target" + self.ready = False + + self.crashing_thread: Optional[gdb.InferiorThread] = None + + def open(self, name: str, from_tty: bool) -> None: + if not self.fetch_registers_usable(): + raise IncorrectTargetError("Not usable") + + if not gdb.objfiles()[0].has_symbols(): + raise ValueError("Cannot debug kernel without symbol table") + + super().open(name, from_tty) + + crash.infra.callback.target_ready() + + self.setup_tasks() + + def setup_tasks(self) -> None: + # pylint complains about this. It's ugly but putting the import within + # setup_tasks breaks the cycle. + # pylint: disable=cyclic-import + from crash.types.task import LinuxTask, types as task_types + import crash.cache.tasks # pylint: disable=redefined-outer-name + print("Loading tasks...", end="") + sys.stdout.flush() + + rqs = get_percpu_vars(symbols.runqueues) + rqscurrs = {int(x["curr"]) : k for (k, x) in rqs.items()} + + task_count = 0 + try: + crashing_cpu = symvals.crashing_cpu + except MissingSymbolError: + crashing_cpu = -1 + + task_struct_p_type = task_types.task_struct_type.pointer() + for thread in gdb.selected_inferior().threads(): + task_address = thread.ptid[2] + + task = get_typed_pointer(task_address, task_struct_p_type) + ltask = LinuxTask(task.dereference()) + + active = task_address in rqscurrs + if active: + cpu = rqscurrs[task_address] + regs = self.kdumpfile.attr.cpu[cpu].reg + ltask.set_active(cpu, regs) + + thread.info = ltask + if active and cpu == crashing_cpu: + self.crashing_thread = thread + + self.arch_setup_thread(thread) + ltask.attach_thread(thread) + + crash.cache.tasks.cache_task(ltask) + + task_count += 1 + if task_count % 100 == 0: + print(".", end='') + sys.stdout.flush() + print(" done. ({} tasks total)".format(task_count)) + + def close(self) -> None: + pass + + # pylint: disable=unused-argument + def thread_alive(self, ptid: PTID) -> bool: + return True + + # pylint: disable=unused-argument + def prepare_to_store(self, thread: gdb.InferiorThread) -> None: + pass + + @abc.abstractmethod + def fetch_registers_usable(self) -> bool: + pass + + @abc.abstractmethod + def fetch_registers(self, thread: gdb.InferiorThread, + register: Optional[gdb.RegisterDescriptor]) -> Optional[gdb.RegisterCollectionType]: + pass + + # pylint: disable=unused-argument + def store_registers(self, thread: gdb.InferiorThread, registers: gdb.RegisterCollectionType) -> None: + raise TypeError("This target is read-only.") + + # pylint: disable=unused-argument + def has_execution(self, ptid: PTID) -> bool: + return False + + @abc.abstractmethod + def arch_setup_thread(self, thread: gdb.InferiorThread) -> None: + pass + + @abc.abstractmethod + def get_stack_pointer(self, thread: gdb.InferiorThread) -> int: + pass + +class TargetFetchRegistersBase(metaclass=abc.ABCMeta): + """ + The base class from which to implement the fetch_registers callback. + + The architecture code must implement the :meth:`fetch_active` and + :meth:`fetch_scheduled` methods. + """ + _enabled: bool = False + + def __init__(self) -> None: + super().__init__() + self.fetching: bool = False + + # pylint: disable=unused-argument + @classmethod + def enable(cls, unused: Optional[gdb.Type] = None) -> None: + cls._enabled = True + + @classmethod + def fetch_registers_usable(cls) -> bool: + return cls._enabled + + @abc.abstractmethod + def fetch_active(self, thread: gdb.InferiorThread, + register: Optional[gdb.RegisterDescriptor]) -> gdb.RegisterCollectionType: + pass + + @abc.abstractmethod + def fetch_scheduled(self, thread: gdb.InferiorThread, + register: Optional[gdb.RegisterDescriptor]) -> gdb.RegisterCollectionType: + pass + + def fetch_registers(self, thread: gdb.InferiorThread, + register: Optional[gdb.RegisterDescriptor]) -> Optional[gdb.RegisterCollectionType]: + ret: Optional[gdb.RegisterCollectionType] = None + + # Don't recurse, but don't fail either + if self.fetching: + return None + + self.fetching = True + try: + if thread.info.active: + ret = self.fetch_active(thread, register) + else: + ret = self.fetch_scheduled(thread, register) + except AttributeError: + # We still want to be able to list the threads even if we haven't + # setup tasks. + ret = None + + self.fetching = False + return ret + +_targets: List[Type[TargetBase]] = [] +def register_target(new_target: Type[TargetBase]) -> None: + _targets.append(new_target) + +def setup_target() -> TargetBase: + for target in _targets: + t = None + try: + t = target() + t.open("", False) + return t + except IncorrectTargetError: + del t + + raise IncorrectTargetError("Could not identify target implementation for this kernel") + +def check_target() -> TargetBase: + target = gdb.current_target() + + if target is None: + raise ValueError("No current target") + + if not isinstance(target, TargetBase): + raise ValueError(f"Current target {type(target)} is not supported") + + return target diff --git a/crash/target/ppc64.py b/crash/target/ppc64.py new file mode 100644 index 00000000000..ae54f2930d1 --- /dev/null +++ b/crash/target/ppc64.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: + +from typing import Optional + +import gdb + +import crash.target +from crash.target import register_target +from crash.target import KernelFrameFilter + +class _FetchRegistersBase(crash.target.TargetFetchRegistersBase): + def __init__(self) -> None: + super().__init__() + self.filter: KernelFrameFilter + + def fetch_active(self, thread: gdb.InferiorThread, + register: Optional[gdb.RegisterDescriptor]) -> gdb.RegisterCollectionType: + registers = {} + task = thread.info + for reg in task.regs: + if (reg == "pc" and register is not None and + register.name != "pc"): + continue + try: + registers[reg] = task.regs[reg] + except KeyError: + pass + + return registers + + def fetch_scheduled(self, thread: gdb.InferiorThread, + register: Optional[gdb.RegisterDescriptor]) -> gdb.RegisterCollectionType: + registers: gdb.RegisterCollectionType = {} + return registers + +# pylint: disable=abstract-method +class PPC64TargetBase(crash.target.TargetBase): + ident = "powerpc:common64" + aliases = ["ppc64", "elf64-powerpc"] + + def __init__(self) -> None: + super().__init__() + + # Stop stack traces with addresses below this + self.filter = KernelFrameFilter(0xffff000000000000) + + def arch_setup_thread(self, thread: gdb.InferiorThread) -> None: + task = thread.info.task_struct + thread.info.set_thread_info(task['thread_info'].address) + thread.info.set_thread_struct(task['thread']) + + def get_stack_pointer(self, thread: gdb.InferiorThread) -> int: + return int(thread.info.thread_struct['ksp']) + +class PPC64Target(_FetchRegistersBase, PPC64TargetBase): + pass + +register_target(PPC64Target) diff --git a/crash/target/x86_64.py b/crash/target/x86_64.py new file mode 100644 index 00000000000..9a6dc8eb100 --- /dev/null +++ b/crash/target/x86_64.py @@ -0,0 +1,183 @@ +# -*- coding: utf-8 -*- +# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: + +from typing import Optional +import re + +import gdb +import crash.target +from crash.target import IncorrectTargetError, register_target +from crash.target import KernelFrameFilter +from crash.util.symbols import Types, MinimalSymvals +from crash.util.symbols import TypeCallbacks, MinimalSymbolCallbacks + +types = Types(['struct inactive_task_frame *', 'struct thread_info *', + 'unsigned long *']) +msymvals = MinimalSymvals(['thread_return']) + +# pylint: disable=abstract-method +class _FetchRegistersBase(crash.target.TargetFetchRegistersBase): + def __init__(self) -> None: + super().__init__() + self.filter: KernelFrameFilter + + def fetch_active(self, thread: gdb.InferiorThread, + register: Optional[gdb.RegisterDescriptor]) -> gdb.RegisterCollectionType: + regmap = { + "rflags" : "eflags" + } + registers = {} + task = thread.info + for reg in task.regs: + if (reg == "rip" and register is not None and + register.name != "rip"): + continue + try: + # vmcore uses rflags, gdb uses eflags + if reg in regmap: + reg = regmap[reg] + registers[reg] = task.regs[reg] + except KeyError: + pass + + return registers + +class _FetchRegistersInactiveFrame(_FetchRegistersBase): + def __init__(self) -> None: + super().__init__() + + self._scheduled_rip: int = 0 + if not self._enabled: + raise IncorrectTargetError("Missing struct inactive_task_frame type") + + # We don't have CFI for __switch_to_asm but we do know what it looks like. + # We push 6 registers and then swap rsp, so we can just rewind back + # to __switch_to_asm getting called and then populate the registers that + # were saved on the stack. + def find_scheduled_rip(self, task: gdb.Value) -> None: + top = int(task['stack']) + 16*1024 + callq = re.compile(r"callq?.*<(\w+)>") + + rsp = task['thread']['sp'].cast(types.unsigned_long_p_type) + + count = 0 + while int(rsp) < top: + val = int(rsp.dereference()) - 5 + if val > self.filter.address: + try: + insn = gdb.execute(f"x/i {val:#x}", to_string=True) + except gdb.error: + insn = None + + if insn: + m = callq.search(insn) + if m and m.group(1) == "__switch_to_asm": + print("Set scheduled RIP") + self._scheduled_rip = val + return + + rsp += 1 + count += 1 + + raise RuntimeError("Cannot locate stack frame offset for __schedule") + + def get_scheduled_rip(self, task: gdb.Value) -> int: + if self._scheduled_rip == 0: + self.find_scheduled_rip(task) + + return self._scheduled_rip + + def fetch_scheduled(self, thread: gdb.InferiorThread, + register: Optional[gdb.RegisterDescriptor]) -> gdb.RegisterCollectionType: + registers: gdb.RegisterCollectionType = {} + task = thread.info.task_struct + + rsp = task['thread']['sp'].cast(types.unsigned_long_p_type) + registers['rsp'] = rsp + + frame = rsp.cast(types.inactive_task_frame_p_type).dereference() + + registers['rip'] = self.get_scheduled_rip(task) + registers['rbp'] = frame['bp'] + registers['rbx'] = frame['bx'] + registers['r12'] = frame['r12'] + registers['r13'] = frame['r13'] + registers['r14'] = frame['r14'] + registers['r15'] = frame['r15'] + registers['cs'] = 2*8 + registers['ss'] = 3*8 + + thread.info.stack_pointer = rsp + thread.info.valid_stack = True + + return registers + +class _FetchRegistersThreadReturn(_FetchRegistersBase): + def fetch_scheduled(self, thread: gdb.InferiorThread, + register: Optional[gdb.RegisterDescriptor]) -> gdb.RegisterCollectionType: + registers: gdb.RegisterCollectionType = {} + task = thread.info.task_struct + + rsp = task['thread']['sp'].cast(types.unsigned_long_p_type) + rbp = rsp.dereference().cast(types.unsigned_long_p_type) + rbx = (rbp - 1).dereference() + r12 = (rbp - 2).dereference() + r13 = (rbp - 3).dereference() + r14 = (rbp - 4).dereference() + r15 = (rbp - 5).dereference() + + # The two pushes that don't have CFI info + # rsp += 2 + + # ex = in_exception_stack(rsp) + # if ex: + # print("EXCEPTION STACK: pid {:d}".format(task['pid'])) + + registers['rip'] = msymvals.thread_return + registers['rsp'] = rsp + registers['rbp'] = rbp + registers['rbx'] = rbx + registers['r12'] = r12 + registers['r13'] = r13 + registers['r14'] = r14 + registers['r15'] = r15 + registers['cs'] = 2*8 + registers['ss'] = 3*8 + + thread.info.stack_pointer = rsp + thread.info.valid_stack = True + + return registers + +class X8664TargetBase(crash.target.TargetBase): + ident = "i386:x86-64" + aliases = ["x86_64"] + + def __init__(self) -> None: + super().__init__() + + # Stop stack traces with addresses below this + self.filter = KernelFrameFilter(0xffff000000000000) + + def arch_setup_thread(self, thread: gdb.InferiorThread) -> None: + task = thread.info.task_struct + thread_info = task['stack'].cast(types.thread_info_p_type) + thread.info.set_thread_info(thread_info) + thread.info.set_thread_struct(task['thread']) + + def get_stack_pointer(self, thread: gdb.InferiorThread) -> int: + return int(thread.info.thread_struct['sp']) + +class X8664ThreadReturnTarget(_FetchRegistersThreadReturn, X8664TargetBase): + pass + +class X8664InactiveFrameTarget(_FetchRegistersInactiveFrame, X8664TargetBase): + pass + +type_cbs = TypeCallbacks([('struct inactive_task_frame', _FetchRegistersInactiveFrame.enable)], + wait_for_target=False) +msymbol_cbs = MinimalSymbolCallbacks([('thread_return', _FetchRegistersThreadReturn.enable)], + wait_for_target=False) + +register_target(X8664ThreadReturnTarget) +register_target(X8664InactiveFrameTarget) diff --git a/crash/types/node.py b/crash/types/node.py index cc4fc71609b..7658f26f5aa 100644 --- a/crash/types/node.py +++ b/crash/types/node.py @@ -28,7 +28,7 @@ def numa_node_id(cpu: int) -> int: Returns: :obj:`int`: The NUMA node ID for the specified CPU. """ - if crash.current_target().arch.name() == "powerpc:common64": + if crash.archname() == "powerpc:common64": return int(symvals.numa_cpu_lookup_table[cpu]) return int(get_percpu_var(symbols.numa_node, cpu)) diff --git a/crash/types/page.py b/crash/types/page.py index 8648615fe92..a487975da55 100644 --- a/crash/types/page.py +++ b/crash/types/page.py @@ -60,7 +60,7 @@ class Page: def setup_page_type(cls, gdbtype: gdb.Type) -> None: # TODO: should check config, but that failed to work on ppc64, hardcode # 64k for now - if crash.current_target().arch.name() == "powerpc:common64": + if crash.archname() == "powerpc:common64": cls.PAGE_SHIFT = 16 # also a config cls.directmap_base = 0xc000000000000000 diff --git a/crash/types/sbitmap.py b/crash/types/sbitmap.py index 33f0ad439af..dacef415aeb 100644 --- a/crash/types/sbitmap.py +++ b/crash/types/sbitmap.py @@ -10,7 +10,6 @@ import gdb -from crash.exceptions import InvalidArgumentError from crash.util.symbols import Types from crash.util import struct_has_member diff --git a/crash/types/slab.py b/crash/types/slab.py index 2ca38a6e059..af50e1eae44 100644 --- a/crash/types/slab.py +++ b/crash/types/slab.py @@ -213,7 +213,7 @@ class SlabSLAB(Slab): BUFCTL_END = ~0 & 0xffffffff - kmem_cache : 'KmemCacheSLAB' + kmem_cache: 'KmemCacheSLAB' def __init__(self, gdb_obj: gdb.Value, kmem_cache: 'KmemCacheSLAB', error: bool = False) -> None: @@ -501,7 +501,7 @@ def check(self, slabtype: int, nid: int) -> int: class SlabSLUB(Slab): - kmem_cache : 'KmemCacheSLUB' + kmem_cache: 'KmemCacheSLUB' def __init__(self, gdb_obj: gdb.Value, kmem_cache: 'KmemCacheSLUB') -> None: super().__init__(gdb_obj, kmem_cache) @@ -770,7 +770,7 @@ class KmemCacheSLAB(KmemCache): slab_list_name = {0: "partial", 1: "full", 2: "free"} slab_list_fullname = {0: "slabs_partial", 1: "slabs_full", 2: "slabs_free"} - buffer_size : int + buffer_size: int def __init__(self, name: str, gdb_obj: gdb.Value) -> None: super().__init__(name, gdb_obj) @@ -1012,7 +1012,7 @@ def ___check_slabs(self, node: gdb.Value, slabtype: int, nid: int, count = errors['num_ok'] if (count and errors['first_ok'] is not None and - errors['last_ok'] is not None): + errors['last_ok'] is not None): print(f"{errors['num_ok']} slab objects were ok between " f"0x{errors['first_ok']:x} and 0x{errors['last_ok']:x}") diff --git a/crash/types/task.py b/crash/types/task.py index a0be1a6cdeb..6018cf7b4e2 100644 --- a/crash/types/task.py +++ b/crash/types/task.py @@ -5,8 +5,9 @@ import gdb +from crash.target import check_target from crash.exceptions import InvalidArgumentError, ArgumentTypeError -from crash.exceptions import UnexpectedGDBTypeError +from crash.exceptions import UnexpectedGDBTypeError, MissingFieldError from crash.util import array_size, struct_has_member from crash.util.symbols import Types, Symvals, SymbolCallbacks from crash.types.list import list_for_each_entry @@ -51,6 +52,8 @@ class TaskStateFlags: TASK_NEW: int = TASK_FLAG_UNINITIALIZED TASK_IDLE: int = TASK_FLAG_UNINITIALIZED + _state_field: str = 'state' + def __init__(self) -> None: raise NotImplementedError("This class is not meant to be instantiated") @@ -225,6 +228,8 @@ class LinuxTask: _get_rss: Callable[['LinuxTask'], int] _get_last_run: Callable[['LinuxTask'], int] + _state_field: str + def __init__(self, task_struct: gdb.Value) -> None: self._init_task_types(task_struct) @@ -241,6 +246,7 @@ def __init__(self, task_struct: gdb.Value) -> None: self.cpu = -1 self.regs: Dict[str, int] = dict() + self.thread_struct: gdb.Value self.thread_info: gdb.Value self.thread: gdb.InferiorThread @@ -263,8 +269,15 @@ def _init_task_types(cls, task: gdb.Value) -> None: # within gdb. Equality requires a deep comparison rather than # a simple pointer comparison. types.override('struct task_struct', task.type) - fields = types.task_struct_type.fields() + fields = [x.name for x in types.task_struct_type.fields()] cls._task_state_has_exit_state = 'exit_state' in fields + if 'state' in fields: + cls._state_field = 'state' + elif '__state' in fields: + cls._state_field = '__state' + else: + raise MissingFieldError("No way to resolve task_struct.state") + cls._pick_get_rss() cls._pick_last_run() cls._valid = True @@ -298,6 +311,33 @@ def attach_thread(self, thread: gdb.InferiorThread) -> None: raise TypeError("Expected gdb.InferiorThread") self.thread = thread + def set_thread_struct(self, thread_struct: gdb.Value) -> None: + """ + Set the thread struct for this task + + The thread struct structure is architecture specific. This method + allows the architecture code to assign its thread struct structure + to this task. + + Args: + thread_struct: The ``struct thread_struct`` to be associated with + this task. The value must be of type ``struct thread_struct``. + """ + self.thread_struct = thread_struct + + def get_thread_struct(self) -> gdb.Value: + """ + Get the thread struct for this task + + The thread struct structure is architecture specific and so this + method abstracts its retreival. + + Returns: + :obj:`gdb.Value`: The struct thread_struct associated with this + task. The type of the value is ``struct thread_struct``. + """ + return self.thread_struct + def set_thread_info(self, thread_info: gdb.Value) -> None: """ Set the thread info for this task @@ -348,7 +388,7 @@ def task_state(self) -> int: Returns: :obj:`int`: The state flags for this task. """ - state = int(self.task_struct['state']) + state = int(self.task_struct[self._state_field]) if self._task_state_has_exit_state: state |= int(self.task_struct['exit_state']) return state @@ -489,20 +529,6 @@ def is_kernel_task(self) -> bool: return False - @classmethod - def set_get_stack_pointer(cls, fn: Callable[[gdb.Value], int]) -> None: - """ - Set the stack pointer callback for this architecture - - The callback must accept a :obj:`gdb.Value` of type - ``struct thread`` and return a :obj:`int` containing the address - of the stack pointer. - - Args: - fn: The callback to use. It will be used by all tasks. - """ - setattr(cls, '_get_stack_pointer_fn', fn) - def get_stack_pointer(self) -> int: """ Get the stack pointer for this task @@ -514,12 +540,8 @@ def get_stack_pointer(self) -> int: :obj:`NotImplementedError`: The architecture hasn't provided a stack pointer callback. """ - try: - fn = getattr(self, '_get_stack_pointer_fn') - except AttributeError: - raise NotImplementedError("Architecture hasn't provided stack pointer callback") from None - - return fn(self.task_struct['thread']) + target = check_target() + return target.get_stack_pointer(self.thread) def _get_rss_field(self) -> int: return int(self.task_struct['mm']['rss'].value()) diff --git a/crash/util/symbols.py b/crash/util/symbols.py index 0c11a56bc33..075338cf626 100644 --- a/crash/util/symbols.py +++ b/crash/util/symbols.py @@ -48,14 +48,14 @@ class DelayedCollection: the container object *or* the contained object if it has been overridden via :meth:`override`. """ - def __init__(self, cls: Type[DelayedValue], names: Names) -> None: + def __init__(self, cls: Type[DelayedValue], names: Names, wait_for_target: bool) -> None: self.attrs: Dict[str, DelayedValue] = {} if isinstance(names, str): names = [names] for name in names: - t = cls(name) + t = cls(name, wait_for_target=wait_for_target) self.attrs[t.attrname] = t self.attrs[t.name] = t @@ -129,8 +129,8 @@ class Types(DelayedCollection): names: A :obj:`str` or :obj:`list` of :obj:`str` containing the names of the types to resolve. """ - def __init__(self, names: Names) -> None: - super(Types, self).__init__(DelayedType, names) + def __init__(self, names: Names, wait_for_target: bool = True) -> None: + super(Types, self).__init__(DelayedType, names, wait_for_target) def override(self, name: str, value: gdb.Type) -> None: # type: ignore """ @@ -171,8 +171,8 @@ class Symbols(DelayedCollection): names: A :obj:`str` or :obj:`list` of :obj:`str` containing the names of the symbols to resolve. """ - def __init__(self, names: Names) -> None: - super(Symbols, self).__init__(DelayedSymbol, names) + def __init__(self, names: Names, wait_for_target: bool = True) -> None: + super(Symbols, self).__init__(DelayedSymbol, names, wait_for_target) class Symvals(DelayedCollection): """ @@ -205,8 +205,8 @@ class Symvals(DelayedCollection): names: A :obj:`str` or :obj:`list` of :obj:`str` containing the names of the symbols to resolve. """ - def __init__(self, names: Names) -> None: - super(Symvals, self).__init__(DelayedSymval, names) + def __init__(self, names: Names, wait_for_target: bool = True) -> None: + super(Symvals, self).__init__(DelayedSymval, names, wait_for_target) class MinimalSymbols(DelayedCollection): """ @@ -239,8 +239,8 @@ class MinimalSymbols(DelayedCollection): names: A :obj:`str` or :obj:`list` of :obj:`str` containing the names of the minimal symbols to resolve. """ - def __init__(self, names: Names) -> None: - super(MinimalSymbols, self).__init__(DelayedMinimalSymbol, names) + def __init__(self, names: Names, wait_for_target: bool = True) -> None: + super().__init__(DelayedMinimalSymbol, names, wait_for_target) class MinimalSymvals(DelayedCollection): """ @@ -268,8 +268,8 @@ class MinimalSymvals(DelayedCollection): names: A :obj:`str` or :obj:`list` of :obj:`str` containing the names of the minimal symbols to resolve. """ - def __init__(self, names: Names) -> None: - super(MinimalSymvals, self).__init__(DelayedMinimalSymval, names) + def __init__(self, names: Names, wait_for_target: bool = True) -> None: + super().__init__(DelayedMinimalSymval, names, wait_for_target) class DelayedValues(DelayedCollection): """ @@ -303,30 +303,30 @@ class DelayedValues(DelayedCollection): Args: names: The names to use for the :obj:`.DelayedValue` objects. """ - def __init__(self, names: Names) -> None: - super(DelayedValues, self).__init__(DelayedValue, names) + def __init__(self, names: Names, wait_for_target: bool = True) -> None: + super().__init__(DelayedValue, names, wait_for_target) CallbackSpecifier = Tuple[str, Callable] CallbackSpecifiers = Union[List[CallbackSpecifier], CallbackSpecifier] class CallbackCollection: - def __init__(self, cls: Type[NamedCallback], - cbs: CallbackSpecifiers) -> None: + def __init__(self, cls: Type[NamedCallback], cbs: CallbackSpecifiers, + wait_for_target: bool) -> None: if isinstance(cbs, tuple): cbs = [cbs] for cb in cbs: - t = cls(cb[0], cb[1]) + t = cls(cb[0], cb[1], wait_for_target=wait_for_target) setattr(self, t.attrname, t) class TypeCallbacks(CallbackCollection): - def __init__(self, cbs: CallbackSpecifiers) -> None: - super().__init__(TypeCallback, cbs) + def __init__(self, cbs: CallbackSpecifiers, wait_for_target: bool = True) -> None: + super().__init__(TypeCallback, cbs, wait_for_target) class SymbolCallbacks(CallbackCollection): - def __init__(self, cbs: CallbackSpecifiers) -> None: - super().__init__(SymbolCallback, cbs) + def __init__(self, cbs: CallbackSpecifiers, wait_for_target: bool = True) -> None: + super().__init__(SymbolCallback, cbs, wait_for_target) class MinimalSymbolCallbacks(CallbackCollection): - def __init__(self, cbs: CallbackSpecifiers) -> None: - super().__init__(MinimalSymbolCallback, cbs) + def __init__(self, cbs: CallbackSpecifiers, wait_for_target: bool = True) -> None: + super().__init__(MinimalSymbolCallback, cbs, wait_for_target) diff --git a/doc-source/conf.py b/doc-source/conf.py index 0101071bebd..bf518b5602c 100644 --- a/doc-source/conf.py +++ b/doc-source/conf.py @@ -60,11 +60,6 @@ def run_apidoc(_): print(line, file=f, end='') f.close() - out_dir = os.path.join(cur_dir, "kdump") - mod_dir = os.path.join(cur_dir, "..", "kdump") - argv = [ '-M', '-e', '-H', 'Kdump Target API Reference', '-f', - '-o', out_dir, mod_dir ] - main(argv) print("*** Generating doc templates") @@ -247,3 +242,25 @@ def setup(app): 'Miscellaneous'), ] + + +# Temporary workaround for 5.1.0 bug +import sphinx +if sphinx.__version__ == '5.1.0': + # see https://github.com/sphinx-doc/sphinx/issues/10701 + # hope is it would get fixed for the next release + + # Although crash happens within NumpyDocstring, it is subclass of GoogleDocstring + # so we need to overload method there + from sphinx.ext.napoleon.docstring import GoogleDocstring + from functools import wraps + + @wraps(GoogleDocstring._consume_inline_attribute) + def _consume_inline_attribute_safe(self): + try: + return self._consume_inline_attribute_safe() + except: + return "", [] + + GoogleDocstring._consume_inline_attribute = _consume_inline_attribute_safe + diff --git a/doc-source/development.rst b/doc-source/development.rst index 9d26f65fd6c..06422a42f2c 100644 --- a/doc-source/development.rst +++ b/doc-source/development.rst @@ -6,7 +6,6 @@ Development api_changes testing - kdump/modules crash/modules gdb-internals diff --git a/doc-source/mock/gdb/__init__.py b/doc-source/mock/gdb/__init__.py index 525b50a2b1c..ab0b877815c 100644 --- a/doc-source/mock/gdb/__init__.py +++ b/doc-source/mock/gdb/__init__.py @@ -1,9 +1,13 @@ # -*- coding: utf-8 -*- # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: +from typing import Dict, Optional, Union + class Target(object): - class kdump(object): - pass + pass + +class LinuxKernelTarget(Target): + class kdumpfile(object): def get_addrxlat_ctx(): pass class get_addrxlat_sys(): @@ -76,5 +80,13 @@ class NewObjFileEvent(object): class Frame(object): pass +class RegisterDescriptor: + pass + +RegisterNameType = Union[RegisterDescriptor, str] +RegisterValueType = Optional[Union[int, bytearray]] +RegisterCollectionType = Dict[RegisterNameType, RegisterValueType] + + SYMBOL_VAR_DOMAIN = 0 COMMAND_USER = 0 diff --git a/doc-source/testing.rst b/doc-source/testing.rst index 47de9664c00..792ea650d4c 100644 --- a/doc-source/testing.rst +++ b/doc-source/testing.rst @@ -90,8 +90,7 @@ The ``lint`` target does allow several options: - ``E=1`` -- Only report errors - ``PYLINT_ARGS`` -- Override the default arguments. It will still operate - on the :py:mod:`crash` and :py:mod:`kdump` modules but no other default - arguments will be used. + on the :py:mod:`crash` modules but no other default arguments will be used. Testing with vmcores -------------------- diff --git a/kdump/__init__.py b/kdump/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/kdump/target.py b/kdump/target.py deleted file mode 100644 index 0949d21551b..00000000000 --- a/kdump/target.py +++ /dev/null @@ -1,145 +0,0 @@ -# -*- coding: utf-8 -*- -# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: - -from typing import Tuple, Callable - -import sys -import shlex - -from kdumpfile import kdumpfile, KDUMP_KVADDR -from kdumpfile.exceptions import AddressTranslationException, EOFException -from kdumpfile.exceptions import NoDataException -import addrxlat.exceptions - -import gdb - -TargetFetchRegisters = Callable[[gdb.InferiorThread, gdb.Register], None] - -PTID = Tuple[int, int, int] - -class Target(gdb.Target): - - _fetch_registers: TargetFetchRegisters - - def __init__(self, debug: bool = False) -> None: - super().__init__() - self.debug = debug - self.shortname = "kdumpfile" - self.longname = "Use a Linux kernel kdump file as a target" - self.kdump: kdumpfile - self.base_offset = 0 - - self.register() - - # pylint: disable=unused-argument - def open(self, args: str, from_tty: bool) -> None: - argv = shlex.split(args) - if len(argv) < 2: - raise gdb.GdbError("kdumpfile target requires kernel image and vmcore") - - vmlinux = argv[0] - filename = argv[1] - - try: - self.kdump = kdumpfile(file=filename) - except Exception as e: - raise gdb.GdbError("Failed to open `{}': {}" - .format(filename, str(e))) - - # pylint: disable=unsupported-assignment-operation - self.kdump.attr['addrxlat.ostype'] = 'linux' - - KERNELOFFSET = "linux.vmcoreinfo.lines.KERNELOFFSET" - try: - attr = self.kdump.attr.get(KERNELOFFSET, "0") # pylint: disable=no-member - self.base_offset = int(attr, base=16) - except (TypeError, ValueError): - pass - - # Load the kernel at the relocated address - # Unfortunately, the percpu section has an offset of 0 and - # ends up getting placed at the offset base. This is easy - # enough to handle in the percpu code. - result = gdb.execute("symbol-file {} -o {:#x}" - .format(vmlinux, self.base_offset), - to_string=True) - - if self.debug: - print(result) - - # We don't have an exec-file so we need to set the architecture - # explicitly. - arch = gdb.objfiles()[0].architecture.name() - result = gdb.execute("set architecture {}".format(arch), to_string=True) - if self.debug: - print(result) - - - def close(self) -> None: - try: - self.unregister() - except RuntimeError: - pass - del self.kdump - - @classmethod - def report_error(cls, addr: int, length: int, error: Exception) -> None: - print("Error while reading {:d} bytes from {:#x}: {}" - .format(length, addr, str(error)), - file=sys.stderr) - - # pylint: disable=unused-argument - def xfer_partial(self, obj: int, annex: str, readbuf: bytearray, - writebuf: bytearray, offset: int, ln: int) -> int: - ret = -1 - if obj == self.TARGET_OBJECT_MEMORY: - try: - r = self.kdump.read(KDUMP_KVADDR, offset, ln) - readbuf[:] = r - ret = ln - except EOFException as e: - if self.debug: - self.report_error(offset, ln, e) - raise gdb.TargetXferEOF(str(e)) - # pylint: disable=no-member - except (NoDataException, addrxlat.exceptions.NoDataError) as e: - if self.debug: - self.report_error(offset, ln, e) - raise gdb.TargetXferUnavailable(str(e)) - except AddressTranslationException as e: - if self.debug: - self.report_error(offset, ln, e) - raise gdb.TargetXferUnavailable(str(e)) - else: - raise IOError("Unknown obj type") - return ret - - # pylint: disable=unused-argument - def thread_alive(self, ptid: PTID) -> bool: - return True - - def pid_to_str(self, ptid: PTID) -> str: - return "pid {:d}".format(ptid[1]) - - def set_fetch_registers(self, callback: TargetFetchRegisters) -> None: - self._fetch_registers = callback # type: ignore - - def fetch_registers(self, thread: gdb.InferiorThread, - register: gdb.Register) -> None: - try: - return self._fetch_registers(thread, register) # type: ignore - except AttributeError: - raise NotImplementedError("Target did not define fetch_registers callback") from None - - def prepare_to_store(self, thread: gdb.InferiorThread) -> None: - pass - - # We don't need to store anything; The regcache is already written. - # pylint: disable=unused-argument - def store_registers(self, thread: gdb.InferiorThread, - register: gdb.Register) -> None: - pass - - # pylint: disable=unused-argument - def has_execution(self, ptid: PTID) -> bool: - return False diff --git a/setup.py b/setup.py index 3a8db1515ce..467adc843c7 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ }, python_requires='>=3.6', - install_requires = [ 'pyelftools' ], + install_requires = [ 'pyelftools', 'addrxlat', 'zstd' ], author = "Jeff Mahoney", author_email = "jeffm@suse.com", diff --git a/test-gdb-compatibility.gdbinit b/test-gdb-compatibility.gdbinit index 1bef4e030e6..0a5c64c8bca 100644 --- a/test-gdb-compatibility.gdbinit +++ b/test-gdb-compatibility.gdbinit @@ -16,15 +16,3 @@ except IncompatibleGDBError as e: end target testtarget foo - -python -try: - gdb.execute('set print thread-events 0') - target.setup_task() - gdb.execute("thread 1", to_string=True) - sys.exit(0) -except gdb.error as e: - print(e) - print("This version of gdb is not compatible with crash-python") - sys.exit(1) -end diff --git a/tests/gen-import-tests.sh b/tests/gen-import-tests.sh index f5ffe202bcf..d5ff37c076d 100755 --- a/tests/gen-import-tests.sh +++ b/tests/gen-import-tests.sh @@ -13,7 +13,7 @@ import unittest class TestImports(unittest.TestCase): END -for f in $(cd $DIR ; find crash kdump -name '*.py'); do +for f in $(cd $DIR ; find crash -name '*.py'); do path=$(echo $f | sed -e 's#/__init__.py##' -e 's#.py##') name=$(echo $path | tr / .) tname=$(echo $path | tr / _) diff --git a/tests/pylintrc b/tests/pylintrc index ba13178f0f4..b4358b2fb7e 100644 --- a/tests/pylintrc +++ b/tests/pylintrc @@ -65,7 +65,7 @@ confidence= # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use"--disable=all --enable=classes # --disable=W" -disable=missing-docstring,too-few-public-methods,invalid-name,too-many-locals,too-many-instance-attributes,too-many-public-methods,fixme,no-self-use,too-many-branches,too-many-statements,too-many-arguments,too-many-boolean-expressions,line-too-long,duplicate-code,bad-option-value +disable=missing-docstring,too-few-public-methods,invalid-name,too-many-locals,too-many-instance-attributes,too-many-public-methods,fixme,no-self-use,too-many-branches,too-many-statements,too-many-arguments,too-many-boolean-expressions,line-too-long,duplicate-code,bad-option-value,too-many-lines [REPORTS] diff --git a/tests/run-gdb.sh b/tests/run-gdb.sh index b09f53b7df4..26f8b8e3af2 100755 --- a/tests/run-gdb.sh +++ b/tests/run-gdb.sh @@ -1,5 +1,10 @@ #!/bin/bash DIR=$(dirname "$0") + +if test -z "$GDB"; then + GDB=crash-python-gdb +fi + echo "Starting gdb" -exec crash-python-gdb -nx -batch -x $DIR/gdbinit-boilerplate "$@" +exec $GDB $GDB_CMDLINE -nx -batch -x $DIR/gdbinit-boilerplate "$@" diff --git a/tests/run-mypy.py b/tests/run-mypy.py index ce164e140c4..f4df83c6091 100644 --- a/tests/run-mypy.py +++ b/tests/run-mypy.py @@ -15,13 +15,14 @@ "--disallow-untyped-globals"] try: - ret = main(None, stdout=sys.stdout, stderr=sys.stderr, args=["-p", "kdump"] + common_args) - ret2 = main(None, stdout=sys.stdout, stderr=sys.stderr, args=["-p", "crash"] + common_args) + ret = main(stdout=sys.stdout, stderr=sys.stderr, args=["-p", "crash"] + common_args) except TypeError: - ret = main(None, args=["-p", "kdump"] + common_args) - ret2 = main(None, args=["-p", "crash"] + common_args) + try: + ret = main(None, stdout=sys.stdout, stderr=sys.stderr, args=["-p", "crash"] + common_args) + except TypeError: + ret = main(None, args=["-p", "crash"] + common_args) -if ret or ret2: +if ret: print("static checking failed.", file=sys.stderr) sys.exit(1) diff --git a/tests/stubs/_gdb.pyi b/tests/stubs/_gdb.pyi index 375a5e2ef7c..07829021b81 100644 --- a/tests/stubs/_gdb.pyi +++ b/tests/stubs/_gdb.pyi @@ -2,6 +2,8 @@ # # NOTE: This dynamically typed stub was automatically generated by stubgen. +from kdumpfile import kdumpfile as kdumpfile_type + from typing import Any, Tuple, List, Optional, Dict, Iterator, Callable from typing import Union, Iterable, Sequence, NewType from typing import TypeVar, Generic @@ -258,7 +260,9 @@ class EventRegistry(Generic[EventType]): class ExitedEvent(Event): ... -class Field: ... +class Field: + enumval: int = ... + bitpos: int = ... class FinishBreakpoint(Breakpoint): return_value: Optional[Value] = ... @@ -291,17 +295,13 @@ class GdbError(Exception): ... IntValue = Union[Value, int] class Inferior: - executing: bool = ... num: int = ... pid: bool = ... progspace: Progspace = ... was_attached: bool = ... def appeared(self, pid: int) -> None: ... def architecture(self) -> Architecture: ... - def delete_thread(self, ptid: Tuple[int, int, int]) -> None: ... def is_valid(self) -> bool: ... - def new_thread(self, ptid: Tuple[int, int, int], - priv: Optional[Any] = ...) -> InferiorThread: ... def read_memory(self, address: IntValue, length: IntValue) -> Membuf: ... def search_memory(self, address: IntValue, length: IntValue, pattern: Buffer) -> int: ... @@ -318,14 +318,14 @@ class InferiorCallPreEvent(Event): ... class InferiorDeletedEvent(Event): ... class InferiorThread: - executing: bool = ... + arch: Any = ... + details: str = ... global_num: int = ... inferior: Inferior = ... info: Any = ... name: str = ... num: int = ... ptid: Tuple[int, int, int] = ... - registers: Dict[str, Register] = ... def handle(self) -> bytes: ... def is_exited(self) -> bool: ... def is_running(self) -> bool: ... @@ -418,12 +418,12 @@ class Progspace: def objfiles(self) -> List[Objfile]: ... def solib_name(self, name: int) -> Optional[str]: ... -class Register: - name: Optional[str] = ... - regnum: int = ... - size: int = ... - type: Type = ... - value: Union[Value, int] = ... +class RegisterDescriptor: + name: str = ... + +RegisterNameType = Union[str, RegisterDescriptor] +RegisterValueType = Optional[Union[int, bytearray]] +RegisterCollectionType = Dict[RegisterNameType, RegisterValueType] class RegisterChangedEvent(Event): ... @@ -505,7 +505,6 @@ class Target: def register(self) -> Any: ... def unregister(self) -> Any: ... - def stacked_target(self) -> bool: ... def open(self, argstring: str, from_tty: bool) -> None: ... def close(self) -> None: ... def info(self, thread: InferiorThread) -> str: ... @@ -516,12 +515,15 @@ class Target: def thread_alive(self, ptid: Tuple[int, int, int]) -> bool: ... def pid_to_str(self, ptid: Tuple[int, int,int]) -> str: ... def fetch_registers(self, thread: InferiorThread, - register: Register) -> None: ... + register: Optional[RegisterDescriptor]) -> Optional[RegisterCollectionType]: ... def prepare_to_store(self, thread: InferiorThread) -> None: ... def store_registers(self, thread: InferiorThread, - register: Register) -> None: ... + registers: RegisterCollectionType) -> None: ... def has_execution(self, ptid: Tuple[int, int, int]) -> bool: ... +class LinuxKernelTarget(Target): + kdumpfile: kdumpfile_type = ... + class TargetXferEOF(EOFError): ... class TargetXferUnavailable(LookupError): ... diff --git a/tests/test_infra_lookup.py b/tests/test_infra_lookup.py index 8f79ccdd7e8..ca180fbea08 100644 --- a/tests/test_infra_lookup.py +++ b/tests/test_infra_lookup.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: import unittest +from unittest.mock import patch import gdb from crash.exceptions import DelayedAttributeError -from crash.infra.callback import ObjfileEventCallback from crash.infra.lookup import SymbolCallback, TypeCallback from crash.infra.lookup import MinimalSymbolCallback from crash.infra.lookup import DelayedType, DelayedSymbol, DelayedSymval @@ -74,14 +74,18 @@ def setUp(self): def tearDown(self): gdb.execute("file") - def load_file(self): + def load_util_file(self): gdb.execute("file tests/test-util") + def load_list_file(self): + gdb.execute("file tests/test-list") + def get_test_class(self): class test_class(object): def __init__(self): self.found = False - cb = MinimalSymbolCallback('test_struct', self.callback) + with patch.object(MinimalSymbolCallback, 'check_target', return_value=True): + cb = MinimalSymbolCallback('test_struct', self.callback) def callback(self, result): self.found = True @@ -93,12 +97,12 @@ def test_minsymbol_no_symbol_found(self): test_class = self.get_test_class() x = test_class() self.assertFalse(x.found) - gdb.execute("file tests/test-list") + self.load_list_file() self.assertFalse(x.found) def test_minsymbol_found_immediately(self): test_class = self.get_test_class() - self.load_file() + self.load_util_file() x = test_class() self.assertTrue(x.found) self.assertTrue(isinstance(x.result, gdb.MinSymbol)) @@ -107,7 +111,7 @@ def test_minsymbol_found_after_load(self): test_class = self.get_test_class() x = test_class() self.assertFalse(x.found) - self.load_file() + self.load_util_file() self.assertTrue(x.found) self.assertTrue(isinstance(x.result, gdb.MinSymbol)) @@ -115,9 +119,9 @@ def test_minsymbol_not_found_in_early_load_then_found_after_load(self): test_class = self.get_test_class() x = test_class() self.assertFalse(x.found) - gdb.execute("file tests/test-list") + self.load_list_file() self.assertFalse(x.found) - self.load_file() + self.load_util_file() self.assertTrue(x.found) self.assertTrue(isinstance(x.result, gdb.MinSymbol)) @@ -132,7 +136,8 @@ def get_test_class(self): class test_class(object): def __init__(self): self.found = False - cb = SymbolCallback('test_struct', self.callback) + with patch.object(SymbolCallback, 'check_target', return_value=True): + cb = SymbolCallback('test_struct', self.callback) def callback(self, result): self.found = True @@ -183,7 +188,8 @@ def get_test_class(self): class test_class(object): def __init__(self): self.found = False - cb = TypeCallback('struct test', self.callback) + with patch.object(TypeCallback, 'check_target', return_value=True): + cb = TypeCallback('struct test', self.callback) def callback(self, result): self.found = True diff --git a/tests/test_list.py b/tests/test_list.py index 4a8dec2beab..51216083bba 100644 --- a/tests/test_list.py +++ b/tests/test_list.py @@ -2,8 +2,11 @@ # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: import unittest +from unittest.mock import patch import gdb +import crash.infra.callback + from crash.exceptions import ArgumentTypeError, UnexpectedGDBTypeError from crash.exceptions import InvalidArgumentError from crash.types.list import list_for_each, list_for_each_entry @@ -15,6 +18,7 @@ def get_symbol(name): class TestList(unittest.TestCase): def setUp(self): gdb.execute("file tests/test-list") + crash.infra.callback.target_ready() self.list_head = gdb.lookup_type("struct list_head") def tearDown(self): diff --git a/tests/test_objfile_callbacks.py b/tests/test_objfile_callbacks.py index ae1906e3dc8..2cba0c63d14 100644 --- a/tests/test_objfile_callbacks.py +++ b/tests/test_objfile_callbacks.py @@ -2,6 +2,7 @@ # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: import unittest +from unittest.mock import patch import gdb from crash.util import safe_get_symbol_value @@ -17,12 +18,13 @@ def tearDown(self): def load_file(self): gdb.execute("file tests/test-util") - def test_registering(self): + def get_test_class(self): class test_class(ObjfileEventCallback): - def __init__(self): + def __init__(self, *args, **kwargs): self.called = False self.checked = False - super(test_class, self).__init__() + self.result = None + super(test_class, self).__init__(*args, **kwargs) self.connect_callback() @@ -34,12 +36,67 @@ def callback(self, result): self.called = True self.result = result + return test_class + + def test_registering(self): + test_class = self.get_test_class() + with patch.object(test_class, 'check_target', return_value=True): + x = test_class() + + self.assertFalse(x.called) + self.assertFalse(x.completed) + self.assertFalse(x.checked) + self.assertTrue(x.result is None) + + self.load_file() + self.assertTrue(x.checked) + self.assertTrue(x.called) + self.assertTrue(x.completed) + + self.assertTrue(isinstance(x.result, gdb.Value)) + + def test_early_callback_with_target_wait(self): + test_class = self.get_test_class() + x = test_class() + self.assertFalse(x.called) self.assertFalse(x.completed) self.assertFalse(x.checked) + self.assertTrue(x.result is None) + self.load_file() + self.assertFalse(x.called) + self.assertFalse(x.completed) + self.assertFalse(x.checked) + self.assertTrue(x.result is None) + + x.target_ready() + self.assertTrue(x.checked) + self.assertTrue(x.called) + self.assertTrue(x.completed) + + self.assertTrue(isinstance(x.result, gdb.Value)) + + def test_early_callback_without_target_wait(self): + test_class = self.get_test_class() + + x = test_class(False) + + self.assertFalse(x.called) + self.assertFalse(x.completed) + self.assertFalse(x.checked) + self.assertTrue(x.result is None) + + self.load_file() + self.assertTrue(x.called) + self.assertTrue(x.completed) + self.assertTrue(x.checked) + self.assertTrue(isinstance(x.result, gdb.Value)) + + x.target_ready() self.assertTrue(x.checked) self.assertTrue(x.called) self.assertTrue(x.completed) + self.assertTrue(isinstance(x.result, gdb.Value)) diff --git a/tests/test_percpu.py b/tests/test_percpu.py index 9087fccb6c5..7e88038ea06 100644 --- a/tests/test_percpu.py +++ b/tests/test_percpu.py @@ -5,11 +5,13 @@ import gdb import crash +import crash.infra.callback import crash.types.percpu as percpu class TestPerCPU(unittest.TestCase): def setUp(self): gdb.execute("file tests/test-percpu", to_string=True) + crash.infra.callback.target_ready() try: print() diff --git a/tests/test_rbtree.py b/tests/test_rbtree.py index 5346ac7900a..5f0f17791d2 100644 --- a/tests/test_rbtree.py +++ b/tests/test_rbtree.py @@ -4,6 +4,7 @@ import unittest import gdb +import crash.infra.callback from crash.types.rbtree import rbtree_postorder_for_each, rbtree_postorder_for_each_entry def get_symbol(name): @@ -12,6 +13,7 @@ def get_symbol(name): class TestRbtree(unittest.TestCase): def setUp(self): gdb.execute("file tests/test-rbtree", to_string=True) + crash.infra.callback.target_ready() try: print() print("--- Unsuppressable gdb output ---", end='') diff --git a/tests/test_syscache.py b/tests/test_syscache.py index 1ee1089bf69..f3cdfc97f9e 100644 --- a/tests/test_syscache.py +++ b/tests/test_syscache.py @@ -2,10 +2,14 @@ # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: import unittest +from unittest.mock import patch import gdb import sys from importlib import reload +import crash.infra.callback +from crash.infra.callback import ObjfileEventCallback + from crash.exceptions import DelayedAttributeError fake_config = ( """ @@ -35,6 +39,7 @@ def cycle_namespace(self): self.utsname = crash.cache.syscache.utsname self.kernel = crash.cache.syscache.kernel self.config = crash.cache.syscache.config + crash.infra.callback.target_ready() def clear_namespace(self): gdb.execute("file") @@ -50,7 +55,7 @@ def _decompress_config_buffer(self): def test_utsname_no_sym(self): gdb.execute("file") - gdb.execute("maint flush-symbol-cache") + gdb.execute("maint flush symbol-cache") self.cycle_namespace() utsname = self.CrashUtsnameCache() with self.assertRaises(DelayedAttributeError): diff --git a/tests/test_syscmd.py b/tests/test_syscmd.py index 7387d18600c..d5a7d002df6 100644 --- a/tests/test_syscmd.py +++ b/tests/test_syscmd.py @@ -7,12 +7,14 @@ from io import StringIO from crash.exceptions import MissingSymbolError +import crash.infra.callback from crash.commands import CommandLineError from crash.commands.syscmd import SysCommand class TestSysCmd(unittest.TestCase): def setUp(self): gdb.execute("file tests/test-syscache", to_string=True) + crash.infra.callback.target_ready() self.cmd = SysCommand("pysys") def tearDown(self): diff --git a/tests/test_target.py b/tests/test_target.py deleted file mode 100644 index dd824992c64..00000000000 --- a/tests/test_target.py +++ /dev/null @@ -1,34 +0,0 @@ -# -*- coding: utf-8 -*- -# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: - -import unittest -import gdb -import os.path -from kdump.target import Target - -class TestTarget(unittest.TestCase): - def setUp(self): - gdb.execute("file") - self.do_real_tests = os.path.exists("tests/vmcore") - - def tearDown(self): - try: - x = gdb.current_target() - del x - except: - pass - gdb.execute('target exec') - - def test_bad_file(self): - x = Target() - with self.assertRaises(gdb.error): - gdb.execute('target kdumpfile /does/not/exist') - x.unregister() - - def test_real_open_with_no_kernel(self): - if self.do_real_tests: - x = Target() - with self.assertRaises(gdb.error): - gdb.execute('target kdumpfile tests/vmcore') - x.unregister() - diff --git a/tests/test_types_bitmap.py b/tests/test_types_bitmap.py index b11cf2d4f93..c5ffda9fd4c 100644 --- a/tests/test_types_bitmap.py +++ b/tests/test_types_bitmap.py @@ -4,6 +4,7 @@ import unittest import sys +import crash.infra.callback import crash.types.bitmap as bm import gdb @@ -11,6 +12,7 @@ class TestBitmap(unittest.TestCase): def setUp(self): gdb.execute("file tests/test-percpu") + crash.infra.callback.target_ready() ulong = gdb.lookup_type('unsigned long') ulong_array = ulong.array(0) diff --git a/tests/test_util.py b/tests/test_util.py index dd3fdf2ae32..797a31ca09f 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -3,6 +3,7 @@ import unittest import gdb +import crash.infra.callback from crash.exceptions import MissingTypeError, MissingSymbolError from crash.util import offsetof, container_of, resolve_type from crash.util import get_symbol_value, safe_get_symbol_value @@ -10,12 +11,14 @@ from crash.exceptions import NotStructOrUnionError from crash.util import InvalidComponentError + def getsym(sym): return gdb.lookup_symbol(sym, None)[0].value() class TestUtil(unittest.TestCase): def setUp(self): gdb.execute("file tests/test-util") + crash.infra.callback.target_ready() self.ulong = gdb.lookup_type('unsigned long') self.ulongsize = self.ulong.sizeof self.test_struct = gdb.lookup_type("struct test") diff --git a/tests/test_util_symbols.py b/tests/test_util_symbols.py index e32c6728e5f..4a2f10dce5c 100644 --- a/tests/test_util_symbols.py +++ b/tests/test_util_symbols.py @@ -1,11 +1,13 @@ # -*- coding: utf-8 -*- # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: import unittest +from unittest.mock import patch import platform import gdb from crash.exceptions import DelayedAttributeError +from crash.infra.lookup import NamedCallback from crash.util.symbols import MinimalSymbols, Symbols, Symvals, Types from crash.util.symbols import TypeCallbacks, SymbolCallbacks from crash.util.symbols import MinimalSymbolCallbacks @@ -19,7 +21,8 @@ def load_file(self): def msymbol_test(self): class Test(object): - msymbols = MinimalSymbols([ 'test_struct' ]) + with patch.object(NamedCallback, 'check_target', return_value=True): + msymbols = MinimalSymbols([ 'test_struct' ]) return Test def test_bad_msymbol_name(self): @@ -51,7 +54,8 @@ def test_msymbol_available_at_start(self): def symbol_test(self): class Test(object): - symbols = Symbols([ 'test_struct' ]) + with patch.object(NamedCallback, 'check_target', return_value=True): + symbols = Symbols([ 'test_struct' ]) return Test def test_bad_symbol_name(self): @@ -83,7 +87,8 @@ def test_symbol_available_at_start(self): def symval_test(self): class Test(object): - symvals = Symvals( [ 'test_struct' ] ) + with patch.object(NamedCallback, 'check_target', return_value=True): + symvals = Symvals( [ 'test_struct' ] ) return Test def test_bad_symval_name(self): @@ -115,7 +120,8 @@ def test_symval_available_at_start(self): def type_test(self): class Test(object): - types = Types( [ 'struct test' ] ) + with patch.object(NamedCallback, 'check_target', return_value=True): + types = Types( [ 'struct test' ] ) return Test def test_bad_type_name(self): @@ -149,7 +155,8 @@ def test_type_available_at_start(self): def ptype_test(self): class Test(object): - types = Types( [ 'struct test *' ]) + with patch.object(NamedCallback, 'check_target', return_value=True): + types = Types( [ 'struct test *' ]) return Test def test_bad_ptype_name(self): @@ -190,8 +197,9 @@ class nested(object): def check_ulong(cls, gdbtype): cls.ulong_valid = True - type_cbs = TypeCallbacks( [ ('unsigned long', - nested.check_ulong) ] ) + with patch.object(NamedCallback, 'check_target', return_value=True): + type_cbs = TypeCallbacks( [ ('unsigned long', + nested.check_ulong) ] ) return Test def test_type_callback_nofile(self): @@ -211,17 +219,19 @@ def test_type_callback(self): def type_callback_test_multi(self): class Test(object): - class nested(object): - types = Types( [ 'unsigned long' ] ) + with patch.object(NamedCallback, 'check_target', return_value=True): + class nested(object): + types = Types( [ 'unsigned long' ] ) - ulong_valid = False + ulong_valid = False - @classmethod - def check_ulong(cls, gdbtype): - cls.ulong_valid = True + @classmethod + def check_ulong(cls, gdbtype): + cls.ulong_valid = True - type_cbs = TypeCallbacks( [ ('unsigned long', - nested.check_ulong) ] ) + with patch.object(NamedCallback, 'check_target', return_value=True): + type_cbs = TypeCallbacks( [ ('unsigned long', + nested.check_ulong) ] ) return Test