123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256 |
- #!/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-<lineid> */ 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()
|