#!/usr/bin/env python3 import os import signal import subprocess import sys from shell_helpers import LF import common import lkmc.import_path import thread_pool class GdbTestcase: def __init__( self, source_path, test_script_path, cmd, verbose=False ): ''' :param verbose: if True, print extra debug information to help understand why a test is not working ''' self.prompt = '\(gdb\) ' self.source_path = source_path import pexpect self.child = pexpect.spawn( cmd[0], cmd[1:], encoding='utf-8' ) if verbose: self.child.logfile = sys.stdout self.child.setecho(False) self.child.waitnoecho() self.child.expect(self.prompt) test = lkmc.import_path.import_path(test_script_path) exception = None try: test.test(self) except Exception as e: exception = e self.child.sendcontrol('d') self.child.close() self.exception = exception def before(self): return self.child.before.rstrip() def continue_to(self, lineid): line_number = self.find_line(lineid) self.sendline('tbreak {}'.format(line_number)) self.sendline('continue') def get_int(self, int_id): self.sendline('printf "%d\\n", {}'.format(int_id)) return int(self.before()) def get_float(self, float_id): self.sendline('printf "%f\\n", {}'.format(float_id)) return float(self.before()) def find_line(self, lineid): ''' Search for the first line that contains a comment line that ends in /* test-gdb- */ and return the line number. ''' lineend = '/* test-gdb-' + lineid + ' */' with open(self.source_path, 'r') as f: for i, line in enumerate(f): if line.rstrip().endswith(lineend): return i + 1 return -1 def sendline(self, line): self.child.sendline(line) self.child.expect(self.prompt) class Main(common.LkmcCliFunction): def __init__(self): super().__init__(description='''\ Connect with GDB to an emulator to debug Linux itself ''') self.add_argument( '--after', default='', help='''Pass extra arguments to GDB, to be appended after all other arguments.''' ) self.add_argument( '--before', default='', help='''Pass extra arguments to GDB to be prepended before any of the arguments passed by this script.''' ) self.add_argument( '--continue', default=True, help='''\ Run `continue` in GDB after connecting. * https://cirosantilli.com/linux-kernel-module-cheat#gdb-step-debug-early-boot * https://cirosantilli.com/linux-kernel-module-cheat#freestanding-programs * https://cirosantilli.com/linux-kernel-module-cheat#baremetal-gdb-step-debug ''' ) self.add_argument( '--gdbserver', default=False, help='''https://cirosantilli.com/linux-kernel-module-cheat#gdbserver''' ) self.add_argument( '--kgdb', default=False, help='''https://cirosantilli.com/linux-kernel-module-cheat#kgdb''' ) self.add_argument( '--lxsymbols', default=True, help='''\ Use the Linux kernel lxsymbols GDB script. Only enabled by default when debugging the Linux kernel, not on userland or baremetal. * https://cirosantilli.com/linux-kernel-module-cheat#gdb-step-debug-kernel-module * https://cirosantilli.com/linux-kernel-module-cheat#bypass-lx-symbols ''' ) self.add_argument( '--sim', default=False, help='''\ Use the built-in GDB CPU simulator. https://cirosantilli.com/linux-kernel-module-cheat#gdb-builtin-cpu-simulator ''' ) self.add_argument( '--test', default=False, help='''\ Run an expect test case instead of interactive usage. For baremetal and userland, the script is a .py file next to the source code. ''' ) self.add_argument( 'break_at', nargs='?', help='''\ If given, break at the given expression, e.g. `main`. You will be left there automatically by default due to --continue if this breakpoint is reached. ''' ) def timed_main(self): after = self.sh.shlex_split(self.env['after']) before = self.sh.shlex_split(self.env['before']) no_continue = not self.env['continue'] if self.env['test']: no_continue = True before.extend([ '-q', LF, '-nh', LF, '-ex', 'set confirm off', LF ]) elif self.env['verbose']: # The output of this would affect the tests. # https://stackoverflow.com/questions/13496389/gdb-remote-protocol-how-to-analyse-packets # Also be opinionated and set remotetimeout to allow you to step debug the emulator at the same time. before.extend([ '-ex', 'set debug remote 1', LF, '-ex', 'set remotetimeout 99999', LF, ]) if self.env['break_at'] is not None: break_at = ['-ex', 'break {}'.format(self.env['break_at']), LF] else: break_at = [] if self.env['userland']: linux_full_system = False if self.env['gdbserver']: before.extend([ '-ex', 'set sysroot {}'.format(self.env['buildroot_staging_dir']), ]) elif self.env['baremetal']: linux_full_system = False else: linux_full_system = True cmd = ( [self.env['gdb_path'], LF] + before ) if linux_full_system: cmd.extend(['-ex', 'add-auto-load-safe-path {}'.format(self.env['linux_build_dir']), LF]) if self.env['sim']: target = 'sim' else: if self.env['gdbserver']: port = self.env['qemu_hostfwd_generic_port'] elif self.env['kgdb']: port = self.env['extra_serial_port'] else: port = self.env['gdb_port'] target = 'remote localhost:{}'.format(port) cmd.extend([ '-ex', 'file {}'.format(self.env['image_elf']), LF, '-ex', 'target {}'.format(target), LF, ]) if not self.env['kgdb']: cmd.extend(break_at) if not no_continue: # ## lx-symbols # # ### lx-symbols after continue # # lx symbols must be run after continue. # # running it immediately after the connect on the bootloader leads to failure, # likely because kernel structure on which it depends are not yet available. # # With this setup, continue runs, and lx-symbols only runs when a break happens, # either by hitting the breakpoint, or by entering Ctrl + C. # # Sure, if the user sets a break on a raw address of the bootloader, # problems will still arise, but let's think about that some other time. # # ### lx-symbols autoload # # The lx-symbols commands gets loaded through the file vmlinux-gdb.py # which gets put on the kernel build root when python debugging scripts are enabled. cmd.extend(['-ex', 'continue', LF]) if self.env['lxsymbols'] and linux_full_system: cmd.extend(['-ex', 'lx-symbols {}'.format(self.env['kernel_modules_build_subdir']), LF]) cmd.extend(after) if self.env['test']: self.sh.print_cmd(cmd) if not self.env['dry_run']: exception = GdbTestcase( self.env['source_path'], os.path.splitext(self.env['source_path'])[0] + '.py', self.sh.strip_newlines(cmd), verbose=self.env['verbose'], ).exception if exception is None: exit_status = 0 else: exit_status = 1 self.log_info(thread_pool.ThreadPool.exception_traceback_string(exception)) return exit_status else: # I would rather have cwd be out_rootfs_overlay_dir, # but then lx-symbols cannot fine the vmlinux and fails with: # vmlinux: No such file or directory. return self.sh.run_cmd( cmd, cmd_file=os.path.join(self.env['run_dir'], 'run-gdb.sh'), cwd=self.env['linux_build_dir'] ) if __name__ == '__main__': Main().cli()