#!/usr/bin/env python # # Copyright (c) 2010-2023 Antmicro # # This file is licensed under the MIT License. # Full license text is available in 'licenses/MIT.txt'. # # This script provides convenience functions for debugging user-space seL4 applications. # All of the commands are available under `sel4` prefix, e.g. `sel4 break`, `sel4 thread`, etc. # To use this script in GDB you have to source it first, either using `source` command or running # GDB with `-x ` argument. # # Example: break on main function in rootserver thread # # (gdb) sel4 wait-for-thread rootserver # (gdb) sel4 switch-symbols rootserver # (gdb) sel4 break rootserver main # (gdb) continue import os import os.path import sys from enum import IntEnum from glob import glob import pickle def get_envvar(name): if name not in os.environ: raise gdb.GdbError("{} environment variable is not set".format(name)) else: return os.environ[name] SYMBOL_AUTOSWITCHING = True SOURCE_DIR = get_envvar('SOURCE_DIR') sys.path.append(os.path.join(SOURCE_DIR, 'projects', 'capdl', 'python-capdl-tool')) import capdl class CapDLSource: def __init__(self, spec_pickle): with open(spec_pickle, 'rb') as f: cdl = pickle.load(f) self.threads = {obj.name for obj in cdl.obj_space if isinstance(obj, capdl.TCB)} # Add rootserver thread explicitly self.threads.add('rootserver') def get_threads(self): return self.threads class ExitUserspaceMode(IntEnum): Never = 0 Once = 1 Always = 2 def source(callable): """Convenience decorator for sourcing gdb commands""" callable() return callable @source class seL4(gdb.Command): """Utility functions for debugging seL4 applications""" def __init__(self): super(self.__class__, self).__init__('sel4', gdb.COMMAND_USER, prefix=True) @source class seL4Threads(gdb.Command): """Lists all seL4 threads""" def __init__(self): super(self.__class__, self).__init__('sel4 threads', gdb.COMMAND_STATUS) def invoke(self, arg, from_tty): print('\n'.join(get_threads())) @source class seL4Thread(gdb.Command): """Returns current seL4 thread""" def __init__(self): super(self.__class__, self).__init__('sel4 thread', gdb.COMMAND_STATUS) def invoke(self, arg, from_tty): print(get_current_thread()) @source class seL4WaitForThread(gdb.Command): """Continues until given thread is available to make operations on. This command waits until seL4_DebugNameThread syscall is handled by the kernel, which allows for the use of break and tbreak commands.""" def __init__(self): super(self.__class__, self).__init__('sel4 wait-for-thread', gdb.COMMAND_BREAKPOINTS) def complete(self, text, word): global THREADS return list(component for component in THREADS if component.startswith(text)) def invoke(self, arg, from_tty): threads = get_threads() thread_exists = next((thread for thread in threads if arg in thread), None) if thread_exists is not None: raise gdb.GdbError("'{}' thread is already loaded".format(thread_exists)) gdb.execute('mon seL4 BreakOnNamingThread "{}"'.format(arg)) gdb.execute('continue') @source class seL4SwitchSymbols(gdb.Command): """Forcibly switches symbols to ones of the given component sel4 switch-symbols [component] Forcibly sets symbol-file to file corresponding to given component. If no component name is given or thread is "default" then default binary is loaded instead.""" def __init__(self): super(self.__class__, self).__init__('sel4 switch-symbols', gdb.COMMAND_FILES) def complete(self, text, word): global BINARIES # List all known components, add 'kernel' as an option for DEFAULT_BINARY components = list(BINARIES.keys()) components.append('kernel') return list(component for component in components if component.startswith(text)) def invoke(self, arg, from_tty): if not arg: thread = get_current_thread() switch_symbols(thread, verbose=True) elif arg == 'kernel': switch_symbols(verbose=True) else: switch_symbols(arg, fallback=False, verbose=True) def _sel4_break_complete_helper(text, word): words = text.split(' ') if not text or len(words) <= 1: threads = get_threads() # Extend list of threads with additional special names threads.extend(['user', 'kernel']) return list(thread for thread in threads if thread.startswith(word)) elif len(words) == 2: return gdb.COMPLETE_SYMBOL else: return gdb.COMPLETE_NONE def _sel4_break_invoke_helper(command, arg): args = arg.split(' ') if command == 'break': exit_userspace_mode = ExitUserspaceMode.Always monitor_command = 'mon seL4 SetBreakpoint' elif command == 'tbreak': exit_userspace_mode = ExitUserspaceMode.Once monitor_command = 'mon seL4 SetTemporaryBreakpoint' else: raise ValueError("'{}' is invalid value for command".format(command)) if len(args) < 1 or not args[0]: raise gdb.GdbError("sel4 {}: thread argument is required".format(command)) thread = args[0] symbol_name = args[1] if len(args) >= 2 else None if thread == 'kernel': gdb.execute('mon seL4 BreakOnExittingUserspace {}'.format(exit_userspace_mode)) return if thread == 'user': thread = '' if symbol_name: _, hits = gdb.decode_line(symbol_name) for hit in hits: gdb.execute('{} "{}" {}'.format(monitor_command, thread, hex(hit.pc))) else: gdb.execute('{} "{}"'.format(monitor_command, thread)) @source class seL4Break(gdb.Command): """Creates a breakpoint sel4 break thread [address] Creates a breakpoint in on optional
If no address is given, the breakpoint will be set on first address after context-switch""" def __init__(self): super(self.__class__, self).__init__('sel4 break', gdb.COMMAND_BREAKPOINTS) def complete(self, text, word): return _sel4_break_complete_helper(text, word) def invoke(self, arg, from_tty): _sel4_break_invoke_helper('break', arg) @source class seL4TBreak(gdb.Command): """Creates temporary breakpoint sel4 tbreak thread [address] Creates temporary breakpoint in on optional
If no address is given, the breakpoint will be set on first address after context-switch""" def __init__(self): super(self.__class__, self).__init__('sel4 tbreak', gdb.COMMAND_BREAKPOINTS) def complete(self, text, word): return _sel4_break_complete_helper(text, word) def invoke(self, arg, from_tty): _sel4_break_invoke_helper('tbreak', arg) @source class seL4Delete(gdb.Command): """Removes a breakpoint sel4 delete thread [address] Removes given breakpoint. If no address is given, the context-switch breakpoint is removed""" def __init__(self): super(self.__class__, self).__init__('sel4 delete', gdb.COMMAND_BREAKPOINTS) def complete(self, text, word): words = text.split(' ') if not text or len(words) <= 1: threads = get_threads() # Extend list of threads with additional special names threads.extend(['user', 'kernel']) return list(thread for thread in threads if thread.startswith(word)) elif len(words) == 2: # Renode returns breakpoints as a table, so we have to parse it # in order to gather completion list chosen_thread = words[0] if chosen_thread == 'user': chosen_thread = '' completions = [] for thread, address in get_breakpoints(chosen_thread): if chosen_thread not in thread: continue if address == '' or not address.startswith(word): continue completions.append(address) return completions else: return gdb.COMPLETE_NONE def invoke(self, arg, from_tty): args = arg.split(' ') if len(args) < 1 or not args[0]: raise gdb.GdbError("sel4 delete: thread argument is required") thread = args[0] if len(args) >= 1 else None address = args[1] if len(args) >= 2 else None if thread == 'kernel': gdb.execute('mon seL4 BreakOnExittingUserspace {}'.format(ExitUserspaceMode.Never)) return if thread == 'user': thread = '' if address: gdb.execute('mon seL4 RemoveBreakpoint "{}" {}'.format(thread, address)) gdb.execute('mon seL4 RemoveTemporaryBreakpoint "{}" {}'.format(thread, address)) else: gdb.execute('mon seL4 RemoveBreakpoint "{}"'.format(thread)) gdb.execute('mon seL4 RemoveTemporaryBreakpoint "{}"'.format(thread)) @source class seL4DeleteAll(gdb.Command): """Removes all breakpoints assigned to the thread sel4 delete-all thread""" def __init__(self): super(self.__class__, self).__init__('sel4 delete-all', gdb.COMMAND_BREAKPOINTS) def complete(self, text, word): return list(thread for thread in get_threads() if thread.startswith(text)) def invoke(self, arg, from_tty): gdb.execute('mon seL4 BreakOnExittingUserspace {}'.format(ExitUserspaceMode.Never)) if arg: gdb.execute('mon seL4 RemoveAllBreakpoints "{}"'.format(arg)) else: gdb.execute('mon seL4 RemoveAllBreakpoints') @source class seL4ListBreakpoints(gdb.Command): """Lists all breakpoints sel4 list-breakpoints [thread] """ def __init__(self): super(self.__class__, self).__init__('sel4 list-breakpoints', gdb.COMMAND_BREAKPOINTS) def complete(self, text, word): return list(thread for thread in get_threads() if thread.startswith(text)) def invoke(self, arg, from_tty): if not arg: gdb.execute('mon seL4 GetBreakpoints') else: gdb.execute('mon seL4 GetBreakpoints "{}"'.format(arg)) @source class seL4Ready(gdb.Command): """Returns ready state of seL4 extensions""" def __init__(self): super(self.__class__, self).__init__('sel4 ready', gdb.COMMAND_STATUS) def invoke(self, arg, from_tty): gdb.execute('mon seL4 Ready') @source class seL4SymbolAutoswitching(gdb.Command): """Enables or disables symbol file autoswitching""" def __init__(self): super(self.__class__, self).__init__('sel4 symbol-autoswitching', gdb.COMMAND_USER) def invoke(self, arg, from_tty): global SYMBOL_AUTOSWITCHING if not arg: print('Symbol autoswitching is {}'.format('enabled' if SYMBOL_AUTOSWITCHING else 'disabled')) return if arg.isdigit(): arg = bool(int(arg)) elif arg in ['True', 'true']: arg = True elif arg in ['False', 'false']: arg = False else: raise gdb.GdbError('{} is invalid argument for sel4 symbol-autoswitching'.format(arg)) SYMBOL_AUTOSWITCHING = arg print('Symbol autoswitching is now {}'.format('enabled' if SYMBOL_AUTOSWITCHING else 'disabled')) @source def find_binaries(): global BINARIES, DEFAULT_BINARY BINARIES = {} build_dir = get_envvar('BUILD_DIR') # Default binary is kernel, because every other component has its own thread, # including rootserver. DEFAULT_BINARY = os.path.join(build_dir, 'kernel', 'kernel.elf') # Find all CAmkES components images_path = os.path.join(build_dir, '*.instance.bin') for image_path in glob(images_path): image_name = os.path.basename(image_path).split('.instance.bin')[0] BINARIES[image_name] = image_path # rootserver thread is just capdl-loader BINARIES['rootserver'] = os.path.join(build_dir, 'capdl-loader') @source def find_threads(): global THREADS build_dir = get_envvar('BUILD_DIR') cdl_pickle = os.path.join(build_dir, 'object.pickle') provider = CapDLSource(cdl_pickle) THREADS = provider.get_threads() def get_breakpoints(thread=None): if not thread: breakpoints = gdb.execute('mon seL4 GetBreakpointsPlain', to_string=True).strip() else: breakpoints = gdb.execute('mon seL4 GetBreakpointsPlain "{}"'.format(thread), to_string=True).strip() return [tuple(line.strip().split(':')) for line in breakpoints.splitlines()] def get_current_thread(): return gdb.execute('mon seL4 CurrentThread', to_string=True).strip() def get_threads(): command_output = gdb.execute('mon seL4 Threads', to_string=True) threads = [] for line in command_output.strip().split('\n')[1:-1]: splitted_line = (thread.strip() for thread in line.split(',')) threads.extend(thread for thread in splitted_line if thread) return threads def resolve_symbol_file(thread, fallback=True): if thread is None: image_path = DEFAULT_BINARY else: image_name = next((key for key in BINARIES.keys() if key in thread), None) if image_name is not None: image_path = BINARIES[image_name] elif image_name is None and fallback: image_path = DEFAULT_BINARY else: return None return image_path def switch_symbols(thread=None, fallback=True, verbose=False): """Switch symbol-file to the file corresponding to the given thread If fallback is set to true and the given thread is not found, it will set symbol-file to DEFAULT_BINARY (def. kernel.elf). If verbose is set to true, symbol-file command will be run in interactive mode and information of switching symbol file will be shown""" global BINARIES, DEFAULT_BINARY image_path = resolve_symbol_file(thread, fallback) if image_path is None: raise gdb.GdbError("no symbol-file found for thread/component {}".format(thread)) gdb.execute('symbol-file {}'.format(image_path), from_tty=verbose) def stop_handler(event): if not isinstance(event, gdb.StopEvent): return global SYMBOL_AUTOSWITCHING if SYMBOL_AUTOSWITCHING: thread = get_current_thread() switch_symbols(thread) gdb.events.stop.connect(stop_handler)