#!/usr/bin/env python3 import imp import os import signal import subprocess import sys import common defaults = { 'after': '', 'before': '', 'break_at': None, 'kgdb': False, 'no_continue': False, 'no_lxsymbols': False, 'test': False, 'sim': False, 'userland': None, } 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 common.print_cmd(cmd) cmd = common.strip_newlines(cmd) 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 = imp.load_source('test', test_script_path) test.test(self) self.child.sendcontrol('d') self.child.close() 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) def main(args, extra_args=None): ''' :param args: argparse parse_argument() output. Must contain all the common options, but does not need GDB specific ones. :type args: argparse.Namespace :param extra_args: extra arguments to be added to args :type extra_args: Dict[str,Any] :return: GDB exit status :rtype: int ''' global defaults args = common.resolve_args(defaults, args, extra_args) after = common.shlex_split(args.after) before = common.shlex_split(args.before) no_continue = args.no_continue if args.test: no_continue = True before.extend([ '-q', common.Newline, '-nh', common.Newline, '-ex', 'set confirm off', common.Newline ]) elif args.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', common.Newline, '-ex', 'set remotetimeout 99999', common.Newline, ]) if args.break_at is not None: break_at = ['-ex', 'break {}'.format(args.break_at), common.Newline] else: break_at = [] linux_full_system = (args.baremetal is None and args.userland is None) if args.userland: image = common.resolve_userland(args.userland) elif args.baremetal: image = common.image test_script_path = os.path.splitext(common.source_path)[0] + '.py' else: image = common.vmlinux if args.baremetal: allowed_toolchains = ['crosstool-ng', 'buildroot', 'host'] else: allowed_toolchains = ['buildroot', 'crosstool-ng', 'host'] cmd = ( [common.get_toolchain_tool('gdb', allowed_toolchains=allowed_toolchains), common.Newline] + before + ['-q', common.Newline] ) if linux_full_system: cmd.extend(['-ex', 'add-auto-load-safe-path {}'.format(common.linux_build_dir), common.Newline]) if args.sim: target = 'sim' else: if args.kgdb: port = common.extra_serial_port else: port = common.gdb_port target = 'remote localhost:{}'.format(port) cmd.extend([ '-ex', 'file {}'.format(image), common.Newline, '-ex', 'target {}'.format(target), common.Newline, ]) if not args.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', common.Newline]) if not args.no_lxsymbols and linux_full_system: cmd.extend(['-ex', 'lx-symbols {}'.format(common.kernel_modules_build_subdir), common.Newline]) cmd.extend(after) if args.test: GdbTestcase( common.source_path, test_script_path, cmd, verbose=args.verbose, ) 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 common.run_cmd( cmd, cmd_file=os.path.join(common.run_dir, 'run-gdb.sh'), cwd=common.linux_build_dir ) if __name__ == '__main__': parser = common.get_argparse(argparse_args={'description': 'Connect with GDB to an emulator to debug Linux itself'}) parser.add_argument( '-A', '--after', default=defaults['after'], help='Pass extra arguments to GDB, to be appended after all other arguments' ) parser.add_argument( '--before', default=defaults['before'], help='Pass extra arguments to GDB to be prepended before any of the arguments passed by this script' ) parser.add_argument( '-C', '--no-continue', default=defaults['no_continue'], action='store_true', help="Don't run continue after connecting" ) parser.add_argument( '-k', '--kgdb', default=defaults['kgdb'], action='store_true' ) parser.add_argument( '--sim', default=defaults['sim'], action='store_true', help='''Use the built-in GDB CPU simulator See: https://github.com/cirosantilli/linux-kernel-module-cheat#gdb-builtin-cpu-simulator ''' ) parser.add_argument( '-X', '--no-lxsymbols', default=defaults['no_lxsymbols'], action='store_true' ) parser.add_argument( '--test', default=defaults['test'], action='store_true', 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. ''' ) parser.add_argument( '--userland', default=defaults['userland'], ) parser.add_argument( 'break_at', nargs='?', help='Extra options to append at the end of the emulator command line' ) args = common.setup(parser) sys.exit(main(args))