469 lines
14 KiB
Python
469 lines
14 KiB
Python
|
|
#!/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 <path/to/gdbscript.py>` 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 = '<any>'
|
||
|
|
|
||
|
|
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 <thread> on optional <address>
|
||
|
|
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 <thread> on optional <address>
|
||
|
|
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 = '<any>'
|
||
|
|
completions = []
|
||
|
|
for thread, address in get_breakpoints(chosen_thread):
|
||
|
|
if chosen_thread not in thread:
|
||
|
|
continue
|
||
|
|
if address == '<any>' 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 = '<any>'
|
||
|
|
|
||
|
|
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)
|