diff --git a/README.adoc b/README.adoc index 9fc0f8d..09264a0 100644 --- a/README.adoc +++ b/README.adoc @@ -960,7 +960,7 @@ This automatically clears the GDB pane, and starts a new one. Pass extra GDB arguments with: .... -./run --wait-gdb --tmux=start_kernel +./run --wait-gdb --tmux --tmux-args start_kernel .... See the tmux manual for further details: @@ -2986,7 +2986,8 @@ Or alternatively, if you are using <>, do everything in one go with: ./run \ --arch aarch64 \ --userland print_argv \ - --tmux=main \ + --tmux \ + --tmux-args main \ --wait-gdb \ -- \ asdf qwer \ @@ -5056,7 +5057,7 @@ If `CONFIG_KALLSYMS=n`, then addresses are shown on traces instead of symbol plu In v4.16 it does not seem possible to configure that at runtime. GDB step debugging with: .... -./run --eval-after 'insmod /dump_stack.ko' --wait-gdb --tmux=dump_stack +./run --eval-after 'insmod /dump_stack.ko' --wait-gdb --tmux --tmux-args dump_stack .... shows that traces are printed at `arch/x86/kernel/dumpstack.c`: @@ -8168,7 +8169,7 @@ And in QEMU: Or for a faster development loop: .... -./run --debug-vm='-ex "break edu_mmio_read" -ex "run"' +./run --debug-vm --debug-vm-args '-ex "break edu_mmio_read" -ex "run"' .... When in <>, using `--debug-vm` makes Ctrl-C not get passed to the QEMU guest anymore: it is instead captured by GDB itself, so allow breaking. So e.g. you won't be able to easily quit from a guest program like: @@ -10259,13 +10260,13 @@ then on the second shell: Or if you are a <>, do everything in one go with: .... -./run --arch arm --baremetal interactive/prompt --wait-gdb --tmux=main +./run --arch arm --baremetal interactive/prompt --wait-gdb --tmux --tmux-args main .... Alternatively, to start from the very first executed instruction of our tiny <>: .... -./run --arch arm --baremetal interactive/prompt --wait-gdb --tmux=--no-continue +./run --arch arm --baremetal interactive/prompt --wait-gdb --tmux --tmux-args --no-continue .... Now you can just `stepi` to when jumping into main to go to the C code in link:baremetal/interactive/prompt.c[]. @@ -10273,7 +10274,7 @@ Now you can just `stepi` to when jumping into main to go to the C code in link:b This is specially interesting for the executables that don't use the bootloader from under `baremetal/arch//no_bootloader/*.S`, e.g.: .... -./run --arch arm --baremetal arch/arm/no_bootloader/semihost_exit --wait-gdb --tmux=--no-continue +./run --arch arm --baremetal arch/arm/no_bootloader/semihost_exit --wait-gdb --tmux --tmux-args --no-continue .... The cool thing about those examples is that you start at the very first instruction of your program, which gives more control. diff --git a/bisect-linux-boot-gem5 b/bisect-linux-boot-gem5 index 8ae557f..c204fac 100755 --- a/bisect-linux-boot-gem5 +++ b/bisect-linux-boot-gem5 @@ -9,7 +9,7 @@ import common build_linux = imp.load_source('build-linux', os.path.join(kwargs['root_dir'], 'build_linux')) run = imp.load_source('run', os.path.join(kwargs['root_dir'], 'run')) -parser = common.get_argparse( +parser = self.get_argparse( argparse_args={ 'description': '''Bisect the Linux kernel on gem5 boots. @@ -20,11 +20,11 @@ More information at: https://github.com/cirosantilli/linux-kernel-module-cheat#b 'linux_build_id': 'bisect', }, ) -args = common.setup(parser) +args = self.setup(parser) # We need a clean rebuild because rebuilds at different revisions: # - may fail # - may not actually rebuild all files, e.g. on header changes -common.rmrf(kwargs['linux_build_dir']) +self.rmrf(kwargs['linux_build_dir']) build_linux.LinuxComponent().do_build(args) status = run.main(args, { 'eval': 'm5 exit', diff --git a/bst-vs-heap b/bst-vs-heap index b8cbda5..69ec9b9 100755 --- a/bst-vs-heap +++ b/bst-vs-heap @@ -1,10 +1,10 @@ #!/usr/bin/env python3 import common -parser = common.get_argparse( +parser = self.get_argparse( argparse_args={'description':'Convert a BST vs heap stat file into a gnuplot input'} ) -args = common.setup(parser) -stats = common.get_stats() +args = self.setup(parser) +stats = self.get_stats() it = iter(stats) i = 1 for stat in it: diff --git a/build b/build index d7ad995..cafebc1 100755 --- a/build +++ b/build @@ -315,9 +315,9 @@ Extra args to pass to all scripts. parser.add_argument('components', choices=list(name_to_component_map.keys()) + [[]], default=[], nargs='*', help='''\ Which components to build. Default: qemu-buildroot '''.format(kwargs['default_arch'])) -common.add_dry_run_argument(parser) +self.add_dry_run_argument(parser) args = parser.parse_args() -common.setup_dry_run_arguments(args) +self.setup_dry_run_arguments(args) # Decide archs. if kwargs['arch'] == []: diff --git a/build-baremetal b/build-baremetal index 372aad4..a5626c1 100755 --- a/build-baremetal +++ b/build-baremetal @@ -4,9 +4,9 @@ import os import common -class BaremetalComponent(common.Component): +class BaremetalComponent(self.Component): def do_build(self, args): - common.assert_crosstool_ng_supports_arch(kwargs['arch']) + self.assert_crosstool_ng_supports_arch(kwargs['arch']) build_dir = self.get_build_dir(args) bootloader_obj = os.path.join(kwargs['baremetal_build_lib_dir'], 'bootloader{}'.format(kwargs['obj_ext'])) common_basename_noext = 'common' @@ -28,7 +28,7 @@ class BaremetalComponent(common.Component): gcc = 'arm-none-eabi-gcc' else: os.environ['PATH'] = kwargs['crosstool_ng_bin_dir'] + os.environ['PATH'] - gcc = common.get_toolchain_tool('gcc', allowed_toolchains=['crosstool-ng']) + gcc = self.get_toolchain_tool('gcc', allowed_toolchains=['crosstool-ng']) if kwargs['emulator'] == 'gem5': if kwargs['machine'] == 'VExpress_GEM5_V1': entry_address = 0x80000000 @@ -45,7 +45,7 @@ class BaremetalComponent(common.Component): os.makedirs(build_dir, exist_ok=True) os.makedirs(kwargs['baremetal_build_lib_dir'], exist_ok=True) src = os.path.join(kwargs['baremetal_src_lib_dir'], '{}{}'.format(kwargs['arch'], kwargs['asm_ext'])) - if common.need_rebuild([src], bootloader_obj): + if self.need_rebuild([src], bootloader_obj): self.sh.run_cmd( [gcc, LF] + cflags + @@ -59,7 +59,7 @@ class BaremetalComponent(common.Component): (common_src, common_obj), (syscalls_src, syscalls_obj), ]: - if common.need_rebuild([src], obj): + if self.need_rebuild([src], obj): self.sh.run_cmd( [gcc, LF] + cflags + @@ -149,7 +149,7 @@ Build the baremetal examples with crosstool-NG. in_name = os.path.splitext(in_basename)[0] main_obj = os.path.join(kwargs['baremetal_build_dir'], subpath, '{}{}'.format(in_name, kwargs['obj_ext'])) src = os.path.join(kwargs['baremetal_src_dir'], in_path) - if common.need_rebuild([src], main_obj): + if self.need_rebuild([src], main_obj): self.sh.run_cmd( [gcc, LF] + cflags + @@ -162,7 +162,7 @@ Build the baremetal examples with crosstool-NG. objs = common_objs + [main_obj] out = os.path.join(kwargs['baremetal_build_dir'], subpath, in_name + kwargs['baremetal_build_ext']) link_script = os.path.join(kwargs['baremetal_src_dir'], 'link.ld') - if common.need_rebuild(objs + [link_script], out): + if self.need_rebuild(objs + [link_script], out): self.sh.run_cmd( [gcc, LF] + cflags + diff --git a/build-buildroot b/build-buildroot index 88a6f54..4288498 100755 --- a/build-buildroot +++ b/build-buildroot @@ -10,10 +10,10 @@ import re import common -class BuildrootComponent(common.Component): +class BuildrootComponent(self.Component): def add_parser_arguments(self, parser): parser.add_argument( - '--build-linux', default=self._defaults['build_linux'], action='store_true', + '--build-linux', default=self._defaults['build_linux'], help='''\ Enable building the Linux kernel with Buildroot. This is done mostly to extract Buildroot's default kernel configurations when updating Buildroot. @@ -22,7 +22,7 @@ not currently supported, juse use ./build-linux script if you want to do that. ''' ) parser.add_argument( - '--baseline', default=self._defaults['baseline'], action='store_true', + '--baseline', default=self._defaults['baseline'], help='''Do a default-ish Buildroot defconfig build, without any of our extra options. Mostly to track how much slower we are than a basic build. ''' @@ -42,14 +42,14 @@ Pass multiple times to use multiple fragment files. ''' ) parser.add_argument( - '--no-all', default=self._defaults['no_all'], action='store_true', + '--no-all', default=self._defaults['no_all'], help='''\ Don't build the all target which normally gets build by default. That target builds the root filesystem and all its dependencies. ''' ) parser.add_argument( - '--no-overlay', default=self._defaults['no_all'], action='store_true', + '--no-overlay', default=self._defaults['no_all'], help='''\ Don't add our overlay which contains all files we build without going through Buildroot. This prevents us from overwriting certain Buildroot files. Remember however that you must @@ -140,7 +140,7 @@ usually extra Buildroot targets. ], cwd=kwargs['buildroot_src_dir'], ) - common.make_build_dirs() + self.make_build_dirs() if not kwargs['no_all']: extra_make_args.extend(['all', LF]) self.sh.run_cmd( @@ -161,7 +161,7 @@ usually extra Buildroot targets. # Skip if qemu is not present, because gem5 does not need the qcow2. # so we don't force a QEMU build for gem5. if not kwargs['no_all'] and os.path.exists(kwargs['qemu_img_executable']): - common.raw_to_qcow2() + self.raw_to_qcow2() def get_argparse_args(self): return { diff --git a/build-crosstool-ng b/build-crosstool-ng index 98c8c4d..95c23c5 100755 --- a/build-crosstool-ng +++ b/build-crosstool-ng @@ -4,9 +4,9 @@ import os import common -class CrosstoolNgComponent(common.Component): +class CrosstoolNgComponent(self.Component): def do_build(self, args): - common.assert_crosstool_ng_supports_arch(kwargs['arch']) + self.assert_crosstool_ng_supports_arch(kwargs['arch']) build_dir = self.get_build_dir(args) defconfig_dest = os.path.join(kwargs['crosstool_ng_util_dir'], 'defconfig') os.makedirs(kwargs['crosstool_ng_util_dir'], exist_ok=True) @@ -37,7 +37,7 @@ class CrosstoolNgComponent(common.Component): os.path.join(kwargs['root_dir'], 'crosstool_ng_config', kwargs['arch']), defconfig_dest ) - common.write_configs( + self.write_configs( kwargs['crosstool_ng_defconfig'], [ 'CT_PREFIX_DIR="{}"'.format(kwargs['crosstool_ng_install_dir']), diff --git a/build-docker b/build-docker index 06ac41e..c40b41f 100755 --- a/build-docker +++ b/build-docker @@ -7,7 +7,7 @@ import tarfile import common -class DockerComponent(common.Component): +class DockerComponent(self.Component): def get_argparse_args(self): return { 'description': '''\ @@ -68,7 +68,7 @@ See also:https://github.com/cirosantilli/linux-kernel-module-cheat#ubuntu-guest- kwargs['docker_tar_dir'], kwargs['docker_rootfs_raw_file'], ]) - common.raw_to_qcow2(prebuilt=True) + self.raw_to_qcow2(prebuilt=True) def get_build_dir(self, args): return kwargs['docker_build_dir'] diff --git a/build-linux b/build-linux index 43d951f..81bc586 100755 --- a/build-linux +++ b/build-linux @@ -37,7 +37,7 @@ Still uses options explicitly passed with `--config` and ''' ) self.add_argument( - '--config-only', default=False, action='store_true', + '--config-only', default=False, help='''\ Configure the kernel, but don't build it. ''' @@ -91,7 +91,7 @@ Configure the kernel, but don't build it. if self.env['config'] != []: cli_config_fragment_path = os.path.join(build_dir, 'lkmc_cli_config_fragment') cli_config_str = '\n'.join(self.env['config']) - common.write_string_to_file(cli_config_fragment_path, cli_config_str) + self.write_string_to_file(cli_config_fragment_path, cli_config_str) config_fragments.append(cli_config_fragment_path) self.sh.cp( base_config_file, @@ -133,7 +133,7 @@ Configure the kernel, but don't build it. ) # TODO: remove build and source https://stackoverflow.com/questions/13578618/what-does-build-and-source-link-do-in-lib-modules-kernel-version # TODO Basically all kernel modules also basically leak full host paths. Just terrible. Buildroot deals with that stuff nicely for us. - # common.rmrf() + # self.rmrf() def get_build_dir(self): return self.env['linux_build_dir'] diff --git a/build-m5 b/build-m5 index 0ae8bbe..af2218e 100755 --- a/build-m5 +++ b/build-m5 @@ -4,11 +4,11 @@ import os import common -class M5Component(common.Component): +class M5Component(self.Component): def get_make_cmd(self, args): allowed_toolchains = ['buildroot'] - cc = common.get_toolchain_tool('gcc', allowed_toolchains=allowed_toolchains) - ld = common.get_toolchain_tool('ld', allowed_toolchains=allowed_toolchains) + cc = self.get_toolchain_tool('gcc', allowed_toolchains=allowed_toolchains) + ld = self.get_toolchain_tool('ld', allowed_toolchains=allowed_toolchains) if kwargs['arch'] == 'x86_64': arch = 'x86' else: diff --git a/build-modules b/build-modules index 3c96d9b..bd28e53 100755 --- a/build-modules +++ b/build-modules @@ -7,7 +7,7 @@ import shutil import common -class ModulesComponent(common.Component): +class ModulesComponent(self.Component): def add_parser_arguments(self, parser): parser.add_argument( '--make-args', @@ -15,7 +15,6 @@ class ModulesComponent(common.Component): ) parser.add_argument( '--host', - action='store_true', default=False, help='''\ Build the Linux kernel modules for the host instead of guest. @@ -70,7 +69,7 @@ Use the host packaged cross toolchain. else: allowed_toolchains = None build_subdir = kwargs['kernel_modules_build_subdir'] - gcc = common.get_toolchain_tool(tool, allowed_toolchains=allowed_toolchains) + gcc = self.get_toolchain_tool(tool, allowed_toolchains=allowed_toolchains) prefix = gcc[:-len(tool)] ccache = shutil.which('ccache') if ccache is not None: @@ -97,13 +96,13 @@ Use the host packaged cross toolchain. 'M={}'.format(build_subdir), LF, 'OBJECT_FILES={}'.format(' '.join(object_files)), LF, ] + - common.shlex_split(kwargs['make_args']) + + self.sh.shlex_split(kwargs['make_args']) + verbose ), cwd=os.path.join(kwargs['kernel_modules_build_subdir']), ) if not kwargs['host']: - common.copy_dir_if_update_non_recursive( + self.copy_dir_if_update_non_recursive( srcdir=kwargs['kernel_modules_build_subdir'], destdir=kwargs['out_rootfs_overlay_dir'], filter_ext=kwargs['kernel_module_ext'], diff --git a/build-qemu b/build-qemu index 55d06d3..f981c99 100755 --- a/build-qemu +++ b/build-qemu @@ -4,12 +4,11 @@ import os import common -class QemuComponent(common.Component): +class QemuComponent(self.Component): def add_parser_arguments(self, parser): parser.add_argument( '--userland', default=False, - action='store_true', help='Build QEMU user mode instead of system.', ) parser.add_argument( diff --git a/build-userland b/build-userland index d225b09..8bb9c8d 100755 --- a/build-userland +++ b/build-userland @@ -8,7 +8,7 @@ import subprocess import common -class UserlandComponent(common.Component): +class UserlandComponent(self.Component): def add_parser_arguments(self, parser): parser.add_argument( '--has-package', @@ -21,7 +21,6 @@ allows us to build examples that rely on it. ) parser.add_argument( '--host', - action='store_true', default=False, help='''\ Build the userland programs for the host instead of guest. @@ -51,8 +50,8 @@ has the OpenBLAS libraries and headers installed. allowed_toolchains = ['host'] else: allowed_toolchains = ['buildroot'] - cc = common.get_toolchain_tool('gcc', allowed_toolchains=allowed_toolchains) - cxx = common.get_toolchain_tool('g++', allowed_toolchains=allowed_toolchains) + cc = self.get_toolchain_tool('gcc', allowed_toolchains=allowed_toolchains) + cxx = self.get_toolchain_tool('g++', allowed_toolchains=allowed_toolchains) self.sh.run_cmd( ( [ @@ -74,7 +73,7 @@ has the OpenBLAS libraries and headers installed. cwd=kwargs['userland_src_dir'], extra_paths=[kwargs['ccache_dir']], ) - common.copy_dir_if_update_non_recursive( + self.copy_dir_if_update_non_recursive( srcdir=build_dir, destdir=kwargs['out_rootfs_overlay_dir'], filter_ext=kwargs['userland_build_ext'], diff --git a/cli_function.py b/cli_function.py old mode 100644 new mode 100755 index 98df08e..e92e113 --- a/cli_function.py +++ b/cli_function.py @@ -41,15 +41,17 @@ class Argument: if nargs is not None: self.kwargs['nargs'] = nargs if default is True: - self.kwargs['action'] = 'store_false' + bool_action = 'store_false' self.is_bool = True elif default is False: - self.kwargs['action'] = 'store_true' + bool_action = 'store_true' self.is_bool = True else: self.is_bool = False if default is None and nargs in ('*', '+'): default = [] + if self.is_bool and not 'action' in kwargs: + self.kwargs['action'] = bool_action if help is not None: if not self.is_bool and default: help += ' Default: {}'.format(default) @@ -131,6 +133,9 @@ class CliFunction: help='Path to the configuration file to use' ) + def __str__(self): + return '\n'.join(str(arg) for arg in self._arguments) + def add_argument( self, *args, @@ -155,11 +160,10 @@ class CliFunction: new_longname = '--no' + argument.longname[1:] kwargs = argument.kwargs.copy() kwargs['default'] = not argument.default - if argument.default: - action = 'store_true' - else: - action = 'store_false' - kwargs['action'] = action + if kwargs['action'] == 'store_false': + kwargs['action'] = 'store_true' + elif kwargs['action'] == 'store_true': + kwargs['action'] = 'store_false' if 'help' in kwargs: del kwargs['help'] parser.add_argument(new_longname, dest=argument.key, **kwargs) @@ -188,6 +192,7 @@ amazing function! self.add_argument('-q', '--qwer', default='Q', help='Help for qwer'), self.add_argument('-b', '--bool', default=True, help='Help for bool'), self.add_argument('--bool-cli', default=False, help='Help for bool'), + self.add_argument('--bool-nargs', default=False, nargs='?', action='store', const='') self.add_argument('--no-default', help='Help for no-bool'), self.add_argument('pos-mandatory', help='Help for pos-mandatory', type=int), self.add_argument('pos-optional', default=0, help='Help for pos-optional', type=int), @@ -196,32 +201,62 @@ amazing function! del kwargs['config_file'] return kwargs - # Code calls. - default = OneCliFunction()(pos_mandatory=1) - assert default == {'asdf': 'A', 'qwer': 'Q', 'bool': True, 'bool_cli': True, 'no_default': None, 'pos_mandatory': 1, 'pos_optional': 0, 'args_star': []} + one_cli_function = OneCliFunction() + + # Default code call. + default = one_cli_function(pos_mandatory=1) + assert default == { + 'asdf': 'A', + 'qwer': 'Q', + 'bool': True, + 'bool_nargs': False, + 'bool_cli': True, + 'no_default': None, + 'pos_mandatory': 1, + 'pos_optional': 0, + 'args_star': [] + } + + # Default CLI call. + out = one_cli_function.cli(['1']) + assert out == default # asdf - out = OneCliFunction()(pos_mandatory=1, asdf='B') + out = one_cli_function(pos_mandatory=1, asdf='B') assert out['asdf'] == 'B' out['asdf'] = default['asdf'] assert(out == default) # asdf and qwer - out = OneCliFunction()(pos_mandatory=1, asdf='B', qwer='R') + out = one_cli_function(pos_mandatory=1, asdf='B', qwer='R') assert out['asdf'] == 'B' assert out['qwer'] == 'R' out['asdf'] = default['asdf'] out['qwer'] = default['qwer'] assert(out == default) - # bool - out = OneCliFunction()(pos_mandatory=1, bool=False) - assert out['bool'] == False - out['bool'] = default['bool'] - assert(out == default) + if '--bool': + out = one_cli_function(pos_mandatory=1, bool=False) + cli_out = one_cli_function.cli(['--bool', '1']) + assert out == cli_out + assert out['bool'] == False + out['bool'] = default['bool'] + assert(out == default) + + if '--bool-nargs': + + out = one_cli_function(pos_mandatory=1, bool_nargs=True) + assert out['bool_nargs'] == True + out['bool_nargs'] = default['bool_nargs'] + assert(out == default) + + out = one_cli_function(pos_mandatory=1, bool_nargs='asdf') + assert out['bool_nargs'] == 'asdf' + out['bool_nargs'] = default['bool_nargs'] + assert(out == default) # Force a boolean value set on the config to be False on CLI. - assert OneCliFunction().cli(['--no-bool-cli', '1'])['bool_cli'] is False + assert one_cli_function.cli(['--no-bool-cli', '1'])['bool_cli'] is False # CLI call. - print(OneCliFunction().cli()) + print(one_cli_function.cli()) diff --git a/common.py b/common.py index 25106fa..ca431ed 100644 --- a/common.py +++ b/common.py @@ -13,7 +13,6 @@ import os import re import shutil import signal -import stat import subprocess import sys import time @@ -120,7 +119,7 @@ mkdir are generally omitted since those are obvious ''' ) self.add_argument( - '-v', '--verbose', default=False, action='store_true', + '-v', '--verbose', default=False, help='Show full compilation commands when they are not shown by default.' ) @@ -163,10 +162,10 @@ Linux build ID. Allows you to keep multiple separate Linux builds. ''' ) self.add_argument( - '--initramfs', default=False, action='store_true', + '--initramfs', default=False, ) self.add_argument( - '--initrd', default=False, action='store_true', + '--initrd', default=False, ) # Baremetal. @@ -189,7 +188,7 @@ inside baremetal/ and then try to use corresponding executable. help='Buildroot build ID. Allows you to keep multiple separate gem5 builds.' ) self.add_argument( - '--buildroot-linux', default=False, action='store_true', + '--buildroot-linux', default=False, help='Boot with the Buildroot Linux kernel instead of our custom built one. Mostly for sanity checks.' ) @@ -199,7 +198,7 @@ inside baremetal/ and then try to use corresponding executable. help='Crosstool-NG build ID. Allows you to keep multiple separate crosstool-NG builds.' ) self.add_argument( - '--docker', default=False, action='store_true', + '--docker', default=False, help='''\ Use the docker download Ubuntu root filesystem instead of the default Buildroot one. ''' @@ -234,7 +233,7 @@ and then inspect separate outputs later in different output directories. ''' ) self.add_argument( - '-P', '--prebuilt', default=False, action='store_true', + '-P', '--prebuilt', default=False, help='''\ Use prebuilt packaged host utilities as much as possible instead of the ones we built ourselves. Saves build time, but decreases @@ -251,11 +250,11 @@ instances in parallel. Default: the run ID (-n) if that is an integer, otherwise # Misc. self.add_argument( - '-g', '--gem5', default=False, action='store_true', + '-g', '--gem5', default=False, help='Use gem5 instead of QEMU.' ) self.add_argument( - '--qemu', default=False, action='store_true', + '--qemu', default=False, help='''\ Use QEMU as the emulator. This option exists in addition to --gem5 to allow overriding configs from the CLI. @@ -426,6 +425,7 @@ to allow overriding configs from the CLI. env['executable'] = env['qemu_executable'] env['run_dir'] = env['qemu_run_dir'] env['termout_file'] = env['qemu_termout_file'] + env['guest_terminal_file'] = env['qemu_termout_file'] env['trace_txt_file'] = env['qemu_trace_txt_file'] env['run_cmd_file'] = join(env['run_dir'], 'run.sh') @@ -497,7 +497,7 @@ to allow overriding configs from the CLI. if env['baremetal'] == 'all': path = env['baremetal'] else: - path = resolve_executable( + path = self.resolve_executable( env['baremetal'], env['baremetal_src_dir'], env['baremetal_build_dir'], @@ -515,6 +515,23 @@ to allow overriding configs from the CLI. env['image'] = path self.env = env + def assert_crosstool_ng_supports_arch(self, arch): + if arch not in self.env['crosstool_ng_supported_archs']: + raise Exception('arch not yet supported: ' + arch) + + @staticmethod + def base64_encode(string): + return base64.b64encode(string.encode()).decode() + + def gem_list_checkpoint_dirs(self): + ''' + List checkpoint directory, oldest first. + ''' + prefix_re = re.compile(self.env['gem5_cpt_prefix']) + files = list(filter(lambda x: os.path.isdir(os.path.join(self.env['m5out_dir'], x)) and prefix_re.search(x), os.listdir(self.env['m5out_dir']))) + files.sort(key=lambda x: os.path.getmtime(os.path.join(self.env['m5out_dir'], x))) + return files + def get_elf_entry(self, elf_file_path): readelf_header = subprocess.check_output([ self.get_toolchain_tool('readelf'), @@ -528,6 +545,21 @@ to allow overriding configs from the CLI. break return int(addr, 0) + def get_stats(self, stat_re=None, stats_file=None): + if stat_re is None: + stat_re = '^system.cpu[0-9]*.numCycles$' + if stats_file is None: + stats_file = self.env['stats_file'] + stat_re = re.compile(stat_re) + ret = [] + with open(stats_file, 'r') as statfile: + for line in statfile: + if line[0] != '-': + cols = line.split() + if len(cols) > 1 and stat_re.search(cols[0]): + ret.append(cols[1]) + return ret + def get_toolchain_prefix(self, tool, allowed_toolchains=None): buildroot_full_prefix = os.path.join(self.env['host_bin_dir'], self.env['buildroot_toolchain_prefix']) buildroot_exists = os.path.exists('{}-{}'.format(buildroot_full_prefix, tool)) @@ -562,6 +594,41 @@ to allow overriding configs from the CLI. def get_toolchain_tool(self, tool, allowed_toolchains=None): return '{}-{}'.format(self.get_toolchain_prefix(tool, allowed_toolchains), tool) + @staticmethod + def github_make_request( + authenticate=False, + data=None, + extra_headers=None, + path='', + subdomain='api', + url_params=None, + **extra_request_args + ): + if extra_headers is None: + extra_headers = {} + headers = {'Accept': 'application/vnd.github.v3+json'} + headers.update(extra_headers) + if authenticate: + headers['Authorization'] = 'token ' + os.environ['LKMC_GITHUB_TOKEN'] + if url_params is not None: + path += '?' + urllib.parse.urlencode(url_params) + request = urllib.request.Request( + 'https://' + subdomain + '.github.com/repos/' + github_repo_id + path, + headers=headers, + data=data, + **extra_request_args + ) + response_body = urllib.request.urlopen(request).read().decode() + if response_body: + _json = json.loads(response_body) + else: + _json = {} + return _json + + @staticmethod + def log_error(msg): + print('error: {}'.format(msg), file=sys.stderr) + def main(self, **kwargs): ''' Time the main of the derived class. @@ -571,13 +638,107 @@ to allow overriding configs from the CLI. kwargs.update(consts) self._init_env(kwargs) self.sh = shell_helpers.ShellHelpers(dry_run=self.env['dry_run']) - self.timed_main() + ret = self.timed_main() if not kwargs['dry_run']: end_time = time.time() - print_time(end_time - start_time) + self.print_time(end_time - start_time) + return ret - def run_cmd(self, *args, **kwargs): - self.sh.run_cmd(*args, **kwargs) + def make_build_dirs(self): + os.makedirs(self.env['buildroot_build_build_dir'], exist_ok=True) + os.makedirs(self.env['gem5_build_dir'], exist_ok=True) + os.makedirs(self.env['out_rootfs_overlay_dir'], exist_ok=True) + + def make_run_dirs(self): + ''' + Make directories required for the run. + The user could nuke those anytime between runs to try and clean things up. + ''' + os.makedirs(self.env['gem5_run_dir'], exist_ok=True) + os.makedirs(self.env['p9_dir'], exist_ok=True) + os.makedirs(self.env['qemu_run_dir'], exist_ok=True) + + @staticmethod + def need_rebuild(srcs, dst): + if not os.path.exists(dst): + return True + for src in srcs: + if os.path.getmtime(src) > os.path.getmtime(dst): + return True + return False + + @staticmethod + def print_time(ellapsed_seconds): + hours, rem = divmod(ellapsed_seconds, 3600) + minutes, seconds = divmod(rem, 60) + print("time {:02}:{:02}:{:02}".format(int(hours), int(minutes), int(seconds))) + + def raw_to_qcow2(eslf, prebuilt=False, reverse=False): + if prebuilt or not os.path.exists(self.env['qemu_img_executable']): + disable_trace = [] + qemu_img_executable = self.env['qemu_img_basename'] + else: + # Prevent qemu-img from generating trace files like QEMU. Disgusting. + disable_trace = ['-T', 'pr_manager_run,file=/dev/null', LF,] + qemu_img_executable = self.env['qemu_img_executable'] + infmt = 'raw' + outfmt = 'qcow2' + infile = self.env['rootfs_raw_file'] + outfile = self.env['qcow2_file'] + if reverse: + tmp = infmt + infmt = outfmt + outfmt = tmp + tmp = infile + infile = outfile + outfile = tmp + self.sh.run_cmd( + [ + qemu_img_executable, LF, + ] + + disable_trace + + [ + 'convert', LF, + '-f', infmt, LF, + '-O', outfmt, LF, + infile, LF, + outfile, LF, + ] + ) + + @staticmethod + def resolve_args(defaults, args, extra_args): + if extra_args is None: + extra_args = {} + argcopy = copy.copy(args) + argcopy.__dict__ = dict(list(defaults.items()) + list(argcopy.__dict__.items()) + list(extra_args.items())) + return argcopy + + @staticmethod + def resolve_executable(in_path, magic_in_dir, magic_out_dir, out_ext): + if os.path.isabs(in_path): + return in_path + else: + paths = [ + os.path.join(magic_out_dir, in_path), + os.path.join( + magic_out_dir, + os.path.relpath(in_path, magic_in_dir), + ) + ] + paths[:] = [os.path.splitext(path)[0] + out_ext for path in paths] + for path in paths: + if os.path.exists(path): + return path + raise Exception('Executable file not found. Tried:\n' + '\n'.join(paths)) + + def resolve_userland(self, path): + return self.resolve_executable( + path, + self.env['userland_src_dir'], + self.env['userland_build_dir'], + self.env['userland_build_ext'], + ) def timed_main(self): ''' @@ -628,162 +789,6 @@ class BuildCliFunction(LkmcCliFunction): The actual build work is done by do_build in implementing classes. ''' if self.env['clean']: - self.clean() + return self.clean() else: - self.build() - -def assert_crosstool_ng_supports_arch(arch): - if arch not in kwargs['crosstool_ng_supported_archs']: - raise Exception('arch not yet supported: ' + arch) - -def base64_encode(string): - return base64.b64encode(string.encode()).decode() - -def gem_list_checkpoint_dirs(): - ''' - List checkpoint directory, oldest first. - ''' - prefix_re = re.compile(kwargs['gem5_cpt_prefix']) - files = list(filter(lambda x: os.path.isdir(os.path.join(kwargs['m5out_dir'], x)) and prefix_re.search(x), os.listdir(kwargs['m5out_dir']))) - files.sort(key=lambda x: os.path.getmtime(os.path.join(kwargs['m5out_dir'], x))) - return files - -def get_stats(stat_re=None, stats_file=None): - if stat_re is None: - stat_re = '^system.cpu[0-9]*.numCycles$' - if stats_file is None: - stats_file = kwargs['stats_file'] - stat_re = re.compile(stat_re) - ret = [] - with open(stats_file, 'r') as statfile: - for line in statfile: - if line[0] != '-': - cols = line.split() - if len(cols) > 1 and stat_re.search(cols[0]): - ret.append(cols[1]) - return ret - -def github_make_request( - authenticate=False, - data=None, - extra_headers=None, - path='', - subdomain='api', - url_params=None, - **extra_request_args - ): - if extra_headers is None: - extra_headers = {} - headers = {'Accept': 'application/vnd.github.v3+json'} - headers.update(extra_headers) - if authenticate: - headers['Authorization'] = 'token ' + os.environ['LKMC_GITHUB_TOKEN'] - if url_params is not None: - path += '?' + urllib.parse.urlencode(url_params) - request = urllib.request.Request( - 'https://' + subdomain + '.github.com/repos/' + github_repo_id + path, - headers=headers, - data=data, - **extra_request_args - ) - response_body = urllib.request.urlopen(request).read().decode() - if response_body: - _json = json.loads(response_body) - else: - _json = {} - return _json - -def log_error(msg): - print('error: {}'.format(msg), file=sys.stderr) - -def make_build_dirs(): - os.makedirs(kwargs['buildroot_build_build_dir'], exist_ok=True) - os.makedirs(kwargs['gem5_build_dir'], exist_ok=True) - os.makedirs(kwargs['out_rootfs_overlay_dir'], exist_ok=True) - -def make_run_dirs(): - ''' - Make directories required for the run. - The user could nuke those anytime between runs to try and clean things up. - ''' - os.makedirs(kwargs['gem5_run_dir'], exist_ok=True) - os.makedirs(kwargs['p9_dir'], exist_ok=True) - os.makedirs(kwargs['qemu_run_dir'], exist_ok=True) - -def need_rebuild(srcs, dst): - if not os.path.exists(dst): - return True - for src in srcs: - if os.path.getmtime(src) > os.path.getmtime(dst): - return True - return False - -def print_time(ellapsed_seconds): - hours, rem = divmod(ellapsed_seconds, 3600) - minutes, seconds = divmod(rem, 60) - print("time {:02}:{:02}:{:02}".format(int(hours), int(minutes), int(seconds))) - -def raw_to_qcow2(prebuilt=False, reverse=False): - if prebuilt or not os.path.exists(kwargs['qemu_img_executable']): - disable_trace = [] - qemu_img_executable = kwargs['qemu_img_basename'] - else: - # Prevent qemu-img from generating trace files like QEMU. Disgusting. - disable_trace = ['-T', 'pr_manager_run,file=/dev/null', LF,] - qemu_img_executable = kwargs['qemu_img_executable'] - infmt = 'raw' - outfmt = 'qcow2' - infile = kwargs['rootfs_raw_file'] - outfile = kwargs['qcow2_file'] - if reverse: - tmp = infmt - infmt = outfmt - outfmt = tmp - tmp = infile - infile = outfile - outfile = tmp - self.sh.run_cmd( - [ - qemu_img_executable, LF, - ] + - disable_trace + - [ - 'convert', LF, - '-f', infmt, LF, - '-O', outfmt, LF, - infile, LF, - outfile, LF, - ] - ) - -def resolve_args(defaults, args, extra_args): - if extra_args is None: - extra_args = {} - argcopy = copy.copy(args) - argcopy.__dict__ = dict(list(defaults.items()) + list(argcopy.__dict__.items()) + list(extra_args.items())) - return argcopy - -def resolve_executable(in_path, magic_in_dir, magic_out_dir, out_ext): - if os.path.isabs(in_path): - return in_path - else: - paths = [ - os.path.join(magic_out_dir, in_path), - os.path.join( - magic_out_dir, - os.path.relpath(in_path, magic_in_dir), - ) - ] - paths[:] = [os.path.splitext(path)[0] + out_ext for path in paths] - for path in paths: - if os.path.exists(path): - return path - raise Exception('Executable file not found. Tried:\n' + '\n'.join(paths)) - -def resolve_userland(path): - return resolve_executable( - path, - kwargs['userland_src_dir'], - kwargs['userland_build_dir'], - kwargs['userland_build_ext'], - ) + return self.build() diff --git a/copy-overlay b/copy-overlay index 7a5af04..a7cdcaf 100755 --- a/copy-overlay +++ b/copy-overlay @@ -6,7 +6,7 @@ import shutil import common -class CopyOverlayComponent(common.Component): +class CopyOverlayComponent(self.Component): def do_build(self, args): distutils.dir_util.copy_tree( kwargs['rootfs_overlay_dir'], diff --git a/gem5-shell b/gem5-shell index debb4b4..69c76a6 100755 --- a/gem5-shell +++ b/gem5-shell @@ -4,11 +4,11 @@ import sys import common -parser = common.get_argparse( +parser = self.get_argparse( default_args={'gem5':True}, argparse_args={'description':'Connect a terminal to a running gem5 instance'} ) -args = common.setup(parser) +args = self.setup(parser) sys.exit(self.sh.run_cmd([ kwargs['gem5_m5term'], LF, 'localhost', LF, diff --git a/gem5-stat b/gem5-stat index c0e7b84..0a42619 100755 --- a/gem5-stat +++ b/gem5-stat @@ -1,6 +1,6 @@ #!/usr/bin/env python3 import common -parser = common.get_argparse( +parser = self.get_argparse( argparse_args={'description':'Get the value of a gem5 stat from the stats.txt file.'} ) parser.add_argument( @@ -9,6 +9,6 @@ parser.add_argument( help='Python regexp matching the full stat name of interest', nargs='?', ) -args = common.setup(parser) -stats = common.get_stats(kwargs['stat']) +args = self.setup(parser) +stats = self.get_stats(kwargs['stat']) print('\n'.join(stats)) diff --git a/getvar b/getvar index c4b2bf6..53d2645 100755 --- a/getvar +++ b/getvar @@ -4,7 +4,7 @@ import types import common -parser = common.get_argparse(argparse_args={ +parser = self.get_argparse(argparse_args={ 'description': '''Print the value of a kwargs['py'] variable. This is useful to: @@ -27,7 +27,7 @@ List all available variables: ''' }) parser.add_argument('variable', nargs='?') -args = common.setup(parser) +args = self.setup(parser) if kwargs['variable']: print(getattr(common, kwargs['variable'])) else: diff --git a/qemu-monitor b/qemu-monitor index 5c6ba29..c1e8ddf 100755 --- a/qemu-monitor +++ b/qemu-monitor @@ -8,7 +8,7 @@ import common prompt = b'\n(qemu) ' -parser = common.get_argparse({ +parser = self.get_argparse({ 'description': '''\ Run a command on the QEMU monitor of a running QEMU instance @@ -21,7 +21,7 @@ parser.add_argument( help='If given, run this command and quit', nargs='*', ) -args = common.setup(parser) +args = self.setup(parser) def write_and_read(tn, cmd, prompt): tn.write(cmd.encode('utf-8')) diff --git a/qemu-trace2txt b/qemu-trace2txt index 2d40534..9ca4612 100755 --- a/qemu-trace2txt +++ b/qemu-trace2txt @@ -19,8 +19,8 @@ def main(): ) if __name__ == '__main__': - parser = common.get_argparse(argparse_args={ + parser = self.get_argparse(argparse_args={ 'description': 'Convert a QEMU `-trace exec_tb` to text form.' }) - args = common.setup(parser) + args = self.setup(parser) sys.exit(main()) diff --git a/release b/release index 34ac894..50ab913 100755 --- a/release +++ b/release @@ -29,4 +29,4 @@ release_zip.main() subprocess.check_call(['git', 'push']) release_upload.main() end_time = time.time() -common.print_time(end_time - start_time) +self.print_time(end_time - start_time) diff --git a/release-download-latest b/release-download-latest index d30ad8c..b4784ad 100755 --- a/release-download-latest +++ b/release-download-latest @@ -11,6 +11,6 @@ import urllib.request import common -_json = common.github_make_request(path='/releases') +_json = self.github_make_request(path='/releases') asset = _json[0]['assets'][0] urllib.request.urlretrieve(asset['browser_download_url'], asset['name']) diff --git a/release-upload b/release-upload index 5416a05..82e7134 100755 --- a/release-upload +++ b/release-upload @@ -24,7 +24,7 @@ def main(): # Check the release already exists. try: - _json = common.github_make_request(path='/releases/tags/' + tag) + _json = self.github_make_request(path='/releases/tags/' + tag) except urllib.error.HTTPError as e: if e.code == 404: release_exists = False @@ -36,7 +36,7 @@ def main(): # Create release if not yet created. if not release_exists: - _json = common.github_make_request( + _json = self.github_make_request( authenticate=True, data=json.dumps({ 'tag_name': tag, @@ -50,12 +50,12 @@ def main(): asset_name = os.path.split(upload_path)[1] # Clear the prebuilts for a upload. - _json = common.github_make_request( + _json = self.github_make_request( path=('/releases/' + str(release_id) + '/assets'), ) for asset in _json: if asset['name'] == asset_name: - _json = common.github_make_request( + _json = self.github_make_request( authenticate=True, path=('/releases/assets/' + str(asset['id'])), method='DELETE', @@ -65,7 +65,7 @@ def main(): # Upload the prebuilt. with open(upload_path, 'br') as myfile: content = myfile.read() - _json = common.github_make_request( + _json = self.github_make_request( authenticate=True, data=content, extra_headers={'Content-Type': 'application/zip'}, diff --git a/release-zip b/release-zip index c7aa747..a7d2d2b 100755 --- a/release-zip +++ b/release-zip @@ -16,7 +16,7 @@ def main(): os.unlink(kwargs['release_zip_file']) zipf = zipfile.ZipFile(kwargs['release_zip_file'], 'w', zipfile.ZIP_DEFLATED) for arch in kwargs['all_archs']: - common.setup(common.get_argparse(default_args={'arch': arch})) + self.setup(common.get_argparse(default_args={'arch': arch})) zipf.write(kwargs['qcow2_file'], arcname=os.path.relpath(kwargs['qcow2_file'], kwargs['root_dir'])) zipf.write(kwargs['linux_image'], arcname=os.path.relpath(kwargs['linux_image'], kwargs['root_dir'])) zipf.close() diff --git a/run b/run index 206ec5f..4e8c61e 100755 --- a/run +++ b/run @@ -8,433 +8,18 @@ import sys import time import common +from shell_helpers import LF -defaults = { - 'background': False, - 'cpus': 1, - 'wait_gdb': False, - 'debug_vm': None, - 'eval': None, - 'extra_emulator_args': [], - 'gem5_exe_args': '', - 'gem5_script': 'fs', - 'gem5_readfile': '', - 'gem5_restore': None, - 'graphic': False, - 'kernel_cli': None, - 'kernel_cli_after_dash': None, - 'eval_after': None, - 'kgdb': False, - 'kdb': False, - 'kvm': False, - 'memory': '256M', - 'record': False, - 'replay': False, - 'terminal': False, - 'tmux': None, - 'trace': None, - 'trace_stdout': False, - 'userland': None, - 'userland_before': '', - 'vnc': False, -} - -def main(args, extra_args=None): - global defaults - args = common.resolve_args(defaults, args, extra_args) - # Common qemu / gem5 logic. - # nokaslr: - # * https://unix.stackexchange.com/questions/397939/turning-off-kaslr-to-debug-linux-kernel-using-qemu-and-gdb - # * https://stackoverflow.com/questions/44612822/unable-to-debug-kernel-with-qemu-gdb/49840927#49840927 - # Turned on by default since v4.12 - kernel_cli = 'console_msg_format=syslog nokaslr norandmaps panic=-1 printk.devkmsg=on printk.time=y rw' - if kwargs['kernel_cli'] is not None: - kernel_cli += ' {}'.format(kwargs['kernel_cli']) - kernel_cli_after_dash = '' - extra_emulator_args = [] - extra_qemu_args = [] - if kwargs['debug_vm'] is not None: - debug_vm = ['gdb', LF, '-q', common.Newline] + common.shlex_split(kwargs['debug_vm']) + ['--args', common.Newline] - else: - debug_vm = [] - if kwargs['wait_gdb']: - extra_qemu_args.extend(['-S', LF]) - if kwargs['eval_after'] is not None: - kernel_cli_after_dash += ' lkmc_eval_base64="{}"'.format(common.base64_encode(kwargs['eval_after'])) - if kwargs['kernel_cli_after_dash'] is not None: - kernel_cli_after_dash += ' {}'.format(kwargs['kernel_cli_after_dash']) - if kwargs['vnc']: - vnc = ['-vnc', ':0', LF] - else: - vnc = [] - if kwargs['initrd'] or kwargs['initramfs']: - ramfs = True - else: - ramfs = False - if kwargs['eval'] is not None: - if ramfs: - initarg = 'rdinit' - else: - initarg = 'init' - kernel_cli += ' {}=/eval_base64.sh'.format(initarg) - kernel_cli_after_dash += ' lkmc_eval="{}"'.format(common.base64_encode(kwargs['eval'])) - if not kwargs['graphic']: - extra_qemu_args.extend(['-nographic', LF]) - console = None - console_type = None - console_count = 0 - if kwargs['arch'] == 'x86_64': - console_type = 'ttyS' - elif kwargs['is_arm']: - console_type = 'ttyAMA' - console = '{}{}'.format(console_type, console_count) - console_count += 1 - if not (kwargs['arch'] == 'x86_64' and kwargs['graphic']): - kernel_cli += ' console={}'.format(console) - extra_console = '{}{}'.format(console_type, console_count) - console_count += 1 - if kwargs['kdb'] or kwargs['kgdb']: - kernel_cli += ' kgdbwait' - if kwargs['kdb']: - if kwargs['graphic']: - kdb_cmd = 'kbd,' - else: - kdb_cmd = '' - kernel_cli += ' kgdboc={}{},115200'.format(kdb_cmd, console) - if kwargs['kgdb']: - kernel_cli += ' kgdboc={},115200'.format(extra_console) - if kernel_cli_after_dash: - kernel_cli += " -{}".format(kernel_cli_after_dash) - extra_env = {} - if kwargs['trace'] is None: - do_trace = False - # A dummy value that is already turned on by default and does not produce large output, - # just to prevent QEMU from emitting a warning that '' is not valid. - trace_type = 'load_file' - else: - do_trace = True - trace_type = kwargs['trace'] - - def raise_rootfs_not_found(): - if not kwargs['dry_run']: - raise Exception('Root filesystem not found. Did you build it?\n' \ - 'Tried to use: ' + kwargs['disk_image']) - def raise_image_not_found(): - if not kwargs['dry_run']: - raise Exception('Executable image not found. Did you build it?\n' \ - 'Tried to use: ' + kwargs['image']) - if kwargs['image'] is None: - raise Exception('Baremetal ELF file not found. Tried:\n' + '\n'.join(paths)) - cmd = debug_vm.copy() - if kwargs['emulator'] == 'gem5': - if kwargs['baremetal'] is None: - if not os.path.exists(kwargs['rootfs_raw_file']): - if not os.path.exists(kwargs['qcow2_file']): - raise_rootfs_not_found() - common.raw_to_qcow2(prebuilt=kwargs['prebuilt'], reverse=True) - else: - if not os.path.exists(kwargs['gem5_fake_iso']): - os.makedirs(os.path.dirname(kwargs['gem5_fake_iso']), exist_ok=True) - common.write_string_to_file(kwargs['gem5_fake_iso'], 'a' * 512) - if not os.path.exists(kwargs['image']): - # This is to run gem5 from a prebuilt download. - if (not kwargs['baremetal'] is None) or (not os.path.exists(kwargs['linux_image'])): - raise_image_not_found() - self.sh.run_cmd([os.path.join(kwargs['extract_vmlinux'], kwargs['linux_image'])]) - os.makedirs(os.path.dirname(kwargs['gem5_readfile']), exist_ok=True) - common.write_string_to_file(kwargs['gem5_readfile'], kwargs['gem5_readfile']) - memory = '{}B'.format(kwargs['memory']) - gem5_exe_args = common.shlex_split(kwargs['gem5_exe_args']) - if do_trace: - gem5_exe_args.extend(['--debug-flags={}'.format(trace_type), LF]) - extra_env['M5_PATH'] = kwargs['gem5_system_dir'] - # https://stackoverflow.com/questions/52312070/how-to-modify-a-file-under-src-python-and-run-it-without-rebuilding-in-gem5/52312071#52312071 - extra_env['M5_OVERRIDE_PY_SOURCE'] = 'true' - if kwargs['trace_stdout']: - debug_file = 'cout' - else: - debug_file = 'trace.txt' - cmd.extend( - [ - kwargs['executable'], LF, - '--debug-file', debug_file, LF, - '--listener-mode', 'on', LF, - '--outdir', kwargs['m5out_dir'], LF, - ] + - gem5_exe_args +class Main(common.LkmcCliFunction): + def __init__(self): + super().__init__( + description='''\ +Run some content on an emulator. +''' ) - if kwargs['userland'] is not None: - cmd.extend([ - kwargs['gem5_se_file'], LF, - '-c', common.resolve_userland(kwargs['userland']), LF, - ]) - else: - if kwargs['gem5_script'] == 'fs': - # TODO port - if kwargs['gem5_restore'] is not None: - cpt_dirs = common.gem_list_checkpoint_dirs() - cpt_dir = cpt_dirs[-kwargs['gem5_restore']] - extra_emulator_args.extend(['-r', str(sorted(cpt_dirs).index(cpt_dir) + 1)]) - cmd.extend([ - kwargs['gem5_fs_file'], LF, - '--disk-image', kwargs['disk_image'], LF, - '--kernel', kwargs['image'], LF, - '--mem-size', memory, LF, - '--num-cpus', str(kwargs['cpus']), LF, - '--script', kwargs['gem5_readfile'], LF, - ]) - if kwargs['arch'] == 'x86_64': - if kwargs['kvm']: - cmd.extend(['--cpu-type', 'X86KvmCPU', LF]) - cmd.extend(['--command-line', 'earlyprintk={} lpj=7999923 root=/dev/sda {}'.format(console, kernel_cli), LF]) - elif kwargs['is_arm']: - if kwargs['kvm']: - cmd.extend(['--cpu-type', 'ArmV8KvmCPU', LF]) - cmd.extend([ - # TODO why is it mandatory to pass mem= here? Not true for QEMU. - # Anything smaller than physical blows up as expected, but why can't it auto-detect the right value? - '--command-line', 'earlyprintk=pl011,0x1c090000 lpj=19988480 rw loglevel=8 mem={} root=/dev/sda {}'.format(memory, kernel_cli), LF, - '--dtb-filename', os.path.join(kwargs['gem5_system_dir'], 'arm', 'dt', 'armv{}_gem5_v1_{}cpu.dtb'.format(kwargs['armv'], kwargs['cpus'])), LF, - '--machine-type', kwargs['machine'], LF, - ]) - if kwargs['baremetal'] is None: - cmd.extend([ - '--param', 'system.panic_on_panic = True', LF]) - else: - cmd.extend([ - '--bare-metal', LF, - '--param', 'system.auto_reset_addr = True', LF, - ]) - if kwargs['arch'] == 'aarch64': - # https://stackoverflow.com/questions/43682311/uart-communication-in-gem5-with-arm-bare-metal/50983650#50983650 - cmd.extend(['--param', 'system.highest_el_is_64 = True', LF]) - elif kwargs['gem5_script'] == 'biglittle': - if kwargs['kvm']: - cpu_type = 'kvm' - else: - cpu_type = 'atomic' - if kwargs['gem5_restore'] is not None: - cpt_dir = common.gem_list_checkpoint_dirs()[-kwargs['gem5_restore']] - extra_emulator_args.extend(['--restore-from', os.path.join(kwargs['m5out_dir'], cpt_dir)]) - cmd.extend([ - os.path.join(kwargs['gem5_source_dir'], 'configs', 'example', 'arm', 'fs_bigLITTLE.py'), LF, - '--big-cpus', '2', LF, - '--cpu-type', cpu_type, LF, - '--disk', kwargs['disk_image'], LF, - '--dtb', os.path.join(kwargs['gem5_system_dir'], 'arm', 'dt', 'armv8_gem5_v1_big_little_2_2.dtb'), LF, - '--kernel', kwargs['image'], LF, - '--little-cpus', '2', LF, - ]) - if kwargs['wait_gdb']: - # https://stackoverflow.com/questions/49296092/how-to-make-gem5-wait-for-gdb-to-connect-to-reliably-break-at-start-kernel-of-th - cmd.extend(['--param', 'system.cpu[0].wait_for_remote_gdb = True', LF]) - else: - qemu_user_and_system_options = [ - '-trace', 'enable={},file={}'.format(trace_type, kwargs['qemu_trace_file']), LF, - ] - if kwargs['userland'] is not None: - if kwargs['wait_gdb']: - debug_args = ['-g', str(kwargs['gdb_port']), LF] - else: - debug_args = [] - cmd.extend( - [ - os.path.join(kwargs['qemu_build_dir'], '{}-linux-user'.format(kwargs['arch']), 'qemu-{}'.format(kwargs['arch'])), LF, - '-L', kwargs['target_dir'], LF - ] + - qemu_user_and_system_options + - common.shlex_split(kwargs['userland_before']) + - debug_args + - [ - common.resolve_userland(kwargs['userland']), LF - ] - ) - else: - if not os.path.exists(kwargs['image']): - raise_image_not_found() - extra_emulator_args.extend(extra_qemu_args) - common.make_run_dirs() - if kwargs['prebuilt'] or not os.path.exists(kwargs['qemu_executable']): - qemu_executable = kwargs['qemu_executable_basename'] - qemu_executable_prebuilt = True - else: - qemu_executable = kwargs['qemu_executable'] - qemu_executable_prebuilt = False - qemu_executable = shutil.which(qemu_executable) - if qemu_executable is None: - raise Exception('QEMU executable not found, did you forget to build or install it?\n' \ - 'Tried to use: ' + qemu_executable) - if kwargs['debug_vm']: - serial_monitor = [] - else: - if kwargs['background']: - serial_monitor = ['-serial', 'file:{}'.format(kwargs['qemu_background_serial_file']), LF] - else: - serial_monitor = ['-serial', 'mon:stdio', LF] - if kwargs['kvm']: - extra_emulator_args.extend(['-enable-kvm', LF]) - extra_emulator_args.extend(['-serial', 'tcp::{},server,nowait'.format(kwargs['extra_serial_port']), LF]) - virtfs_data = [ - (kwargs['p9_dir'], 'host_data'), - (kwargs['out_dir'], 'host_out'), - (kwargs['out_rootfs_overlay_dir'], 'host_out_rootfs_overlay'), - (kwargs['rootfs_overlay_dir'], 'host_rootfs_overlay'), - ] - virtfs_cmd = [] - for virtfs_dir, virtfs_tag in virtfs_data: - if os.path.exists(virtfs_dir): - virtfs_cmd.extend([ - '-virtfs', - 'local,path={virtfs_dir},mount_tag={virtfs_tag},security_model=mapped,id={virtfs_tag}' \ - .format(virtfs_dir=virtfs_dir, virtfs_tag=virtfs_tag), - LF, - ]) - cmd.extend( - [ - qemu_executable, LF, - '-device', 'rtl8139,netdev=net0', LF, - '-gdb', 'tcp::{}'.format(kwargs['gdb_port']), LF, - '-kernel', kwargs['image'], LF, - '-m', kwargs['memory'], LF, - '-monitor', 'telnet::{},server,nowait'.format(kwargs['qemu_monitor_port']), LF, - '-netdev', 'user,hostfwd=tcp::{}-:{},hostfwd=tcp::{}-:22,id=net0'.format(kwargs['qemu_hostfwd_generic_port'], kwargs['qemu_hostfwd_generic_port'], kwargs['qemu_hostfwd_ssh_port']), LF, - '-no-reboot', LF, - '-smp', str(kwargs['cpus']), LF, - ] + - virtfs_cmd + - serial_monitor + - vnc - ) - if not qemu_executable_prebuilt: - cmd.extend(qemu_user_and_system_options) - if kwargs['initrd']: - extra_emulator_args.extend(['-initrd', os.path.join(kwargs['buildroot_images_dir'], 'rootfs.cpio')]) - rr = kwargs['record'] or kwargs['replay'] - if ramfs: - # TODO why is this needed, and why any string works. - root = 'root=/dev/anything' - else: - if rr: - driveif = 'none' - rrid = ',id=img-direct' - root = 'root=/dev/sda' - snapshot = '' - else: - driveif = 'virtio' - root = 'root=/dev/vda' - rrid = '' - snapshot = ',snapshot' - if kwargs['baremetal'] is None: - if not os.path.exists(kwargs['qcow2_file']): - if not os.path.exists(kwargs['rootfs_raw_file']): - raise_rootfs_not_found() - common.raw_to_qcow2(prebuilt=kwargs['prebuilt']) - extra_emulator_args.extend([ - '-drive', - 'file={},format=qcow2,if={}{}{}'.format(kwargs['disk_image'], driveif, snapshot, rrid), - LF, - ]) - if rr: - extra_emulator_args.extend([ - '-drive', 'driver=blkreplay,if=none,image=img-direct,id=img-blkreplay', LF, - '-device', 'ide-hd,drive=img-blkreplay', LF, - ]) - if rr: - extra_emulator_args.extend([ - '-object', 'filter-replay,id=replay,netdev=net0', - '-icount', 'shift=7,rr={},rrfile={}'.format('record' if kwargs['record'] else 'replay', kwargs['qemu_rrfile']), - ]) - virtio_gpu_pci = [] - else: - virtio_gpu_pci = ['-device', 'virtio-gpu-pci', LF] - if kwargs['arch'] == 'x86_64': - append = ['-append', '{} nopat {}'.format(root, kernel_cli), LF] - cmd.extend([ - '-M', kwargs['machine'], LF, - '-device', 'edu', LF, - ]) - elif kwargs['is_arm']: - extra_emulator_args.extend(['-semihosting', LF]) - if kwargs['arch'] == 'arm': - cpu = 'cortex-a15' - else: - cpu = 'cortex-a57' - append = ['-append', '{} {}'.format(root, kernel_cli), LF] - cmd.extend( - [ - # highmem=off needed since v3.0.0 due to: - # http://lists.nongnu.org/archive/html/qemu-discuss/2018-08/msg00034.html - '-M', '{},highmem=off'.format(kwargs['machine']), LF, - '-cpu', cpu, LF, - ] + - virtio_gpu_pci - ) - if kwargs['baremetal'] is None: - cmd.extend(append) - if kwargs['tmux'] is not None: - tmux_args = '--run-id {}'.format(kwargs['run_id']) - if kwargs['emulator'] == 'gem5': - tmux_cmd = './gem5-shell' - elif kwargs['wait_gdb']: - tmux_cmd = './run-gdb' - # TODO find a nicer way to forward all those args automatically. - # Part of me wants to: https://github.com/jonathanslenders/pymux - # but it cannot be used as a library properly it seems, and it is - # slower than tmux. - tmux_args += " --arch {} --linux-build-id '{}' --run-id '{}'".format( - kwargs['arch'], - kwargs['linux_build_id'], - kwargs['run_id'], - ) - if kwargs['baremetal']: - tmux_args += " --baremetal '{}'".format(kwargs['baremetal']) - if kwargs['userland']: - tmux_args += " --userland '{}'".format(kwargs['userland']) - tmux_args += ' {}'.format(kwargs['tmux']) - subprocess.Popen([ - os.path.join(kwargs['root_dir'], 'tmu'), - "sleep 2;{} {}".format(tmux_cmd, tmux_args) - ]) - cmd.extend(extra_emulator_args) - cmd.extend(kwargs['extra_emulator_args']) - if debug_vm or kwargs['terminal']: - out_file = None - else: - out_file = kwargs['termout_file'] - self.sh.run_cmd(cmd, cmd_file=kwargs['run_cmd_file'], out_file=out_file, extra_env=extra_env) - # Check if guest panicked. - if kwargs['emulator'] == 'gem5': - # We have to do some parsing here because gem5 exits with status 0 even when panic happens. - # Grepping for '^panic: ' does not work because some errors don't show that message. - panic_msg = b'--- BEGIN LIBC BACKTRACE ---$' - else: - panic_msg = b'Kernel panic - not syncing' - panic_re = re.compile(panic_msg) - error_string_found = False - if out_file is not None and not kwargs['dry_run']: - with open(kwargs['termout_file'], 'br') as logfile: - for line in logfile: - if panic_re.search(line): - error_string_found = True - if os.path.exists(kwargs['guest_terminal_file']): - with open(kwargs['guest_terminal_file'], 'br') as logfile: - lines = logfile.readlines() - if lines: - last_line = lines[-1] - if last_line.rstrip() == kwargs['magic_fail_string']: - error_string_found = True - if error_string_found: - common.log_error('simulation error detected by parsing logs') - return 1 - return 0 - -def get_argparse(): - parser = common.get_argparse(argparse_args={'description':'Run Linux on an emulator'}) - init_group = parser.add_mutually_exclusive_group() - kvm_group = parser.add_mutually_exclusive_group() - parser.add_argument( - '--background', default=defaults['background'], action='store_true', - help='''\ + self.add_argument( + '--background', default=False, + help='''\ Send QEMU output to a file instead of the terminal so it does not require a terminal attached to run on the background. Interactive input cannot be given. TODO: use a port instead. If only there was a way to redirect a serial to multiple @@ -442,42 +27,46 @@ places, both to a port and a file? We use the file currently to be able to have any output at all. https://superuser.com/questions/1373226/how-to-redirect-qemu-serial-output-to-both-a-file-and-the-terminal-or-a-port ''' - ) - parser.add_argument( - '-c', '--cpus', default=defaults['cpus'], type=int, - help='Number of guest CPUs to emulate. Default: %(default)s' - ) - parser.add_argument( - '-D', '--debug-vm', default=defaults['debug_vm'], nargs='?', action='store', const='', - help='Run GDB on the emulator itself.' - ) - parser.add_argument( - '-E', '--eval', - help='''\ + ) + self.add_argument( + '-c', '--cpus', default=1, type=int, + help='Number of guest CPUs to emulate. Default: %(default)s' + ) + self.add_argument( + '-D', '--debug-vm', default=False, + help='Run GDB on the emulator itself.' + ) + self.add_argument( + '--debug-vm-args', default='', + help='Pass arguments to GDB.' + ) + self.add_argument( + '-E', '--eval', + help='''\ Replace the normal init with a minimal init that just evals the given string. See: https://github.com/cirosantilli/linux-kernel-module-cheat#replace-init ''' - ) - parser.add_argument( - '-e', '--kernel-cli', - help='''\ + ) + self.add_argument( + '-e', '--kernel-cli', + help='''\ Pass an extra Linux kernel command line options, and place them before the dash separator `-`. Only options that come before the `-`, i.e. "standard" options, should be passed with this option. Example: `./run -a arm -e 'init=/poweroff.out'` ''' - ) - parser.add_argument( - '-F', '--eval-after', - help='''\ + ) + self.add_argument( + '-F', '--eval-after', + help='''\ Pass a base64 encoded command line parameter that gets evalled at the end of the normal init. See: https://github.com/cirosantilli/linux-kernel-module-cheat#init-busybox ''' - ) - parser.add_argument( - '-f', '--kernel-cli-after-dash', - help='''\ + ) + self.add_argument( + '-f', '--kernel-cli-after-dash', + help='''\ Pass an extra Linux kernel command line options, add a dash `-` separator, and place the options after the dash. Intended for custom options understood by our `init` scripts, most of which are prefixed @@ -485,10 +74,10 @@ by `lkmc_`. Example: `./run --kernel-cli-after-dash 'lkmc_eval="wget google.com" lkmc_lala=y'` Mnenomic: `-f` comes after `-e`. ''' - ) - parser.add_argument( - '-G', '--gem5-exe-args', default=defaults['gem5_exe_args'], - help='''\ + ) + self.add_argument( + '-G', '--gem5-exe-args', default='', + help='''\ Pass extra options to the gem5 executable. Do not confuse with the arguments passed to config scripts, like `fs.py`. Example: @@ -496,132 +85,514 @@ like `fs.py`. Example: will run: gem.op5 --debug-flags=Exec fs.py --cpu-type=HPI --caches ''' - ) - parser.add_argument( - '--gem5-script', default=defaults['gem5_script'], choices=['fs', 'biglittle'], - help='Which gem5 script to use' - ) - parser.add_argument( - '--gem5-readfile', default=defaults['gem5_readfile'], - help='Set the contents of m5 readfile to this string.' - ) - kvm_group.add_argument( - '-K', '--kvm', default=defaults['kvm'], action='store_true', - help='Use KVM. Only works if guest arch == host arch' - ) - parser.add_argument( - '--kgdb', default=defaults['kgdb'], action='store_true' - ) - parser.add_argument( - '--kdb', default=defaults['kdb'], action='store_true' - ) - parser.add_argument( - '-l', '--gem5-restore', type=int, - help='''\ + ) + self.add_argument( + '--gem5-script', default='fs', choices=['fs', 'biglittle'], + help='Which gem5 script to use' + ) + self.add_argument( + '--gem5-readfile', default='', + help='Set the contents of m5 readfile to this string.' + ) + self.add_argument( + '-K', '--kvm', default=False, + help='Use KVM. Only works if guest arch == host arch' + ) + self.add_argument( + '--kgdb', default=False, + ) + self.add_argument( + '--kdb', default=False, + ) + self.add_argument( + '-l', '--gem5-restore', type=int, + help='''\ Restore the nth most recently taken gem5 checkpoint according to directory timestamps. ''' - ) - parser.add_argument( - '-m', '--memory', default=defaults['memory'], - help='''\ + ) + self.add_argument( + '-m', '--memory', default='256M', + help='''\ Set the memory size of the guest. E.g.: `-m 512M`. We try to keep the default at the minimal ammount amount that boots all archs. Anything lower could lead some arch to fail to boot. Default: %(default)s ''' - ) - group = parser.add_mutually_exclusive_group() - group.add_argument( - '-R', '--replay', default=defaults['replay'], action='store_true', - help='Replay a QEMU run record deterministically' - ) - group.add_argument( - '-r', '--record', default=defaults['record'], action='store_true', - help='Record a QEMU run record for later replay with `-R`' - ) - parser.add_argument( - '-T', '--trace', - help='''\ + ) + self.add_argument( + '-R', '--replay', default=False, + help='Replay a QEMU run record deterministically' + ) + self.add_argument( + '-r', '--record', default=False, + help='Record a QEMU run record for later replay with `-R`' + ) + self.add_argument( + '-T', '--trace', + help='''\ Set trace events to be enabled. If not given, gem5 tracing is completely disabled, while QEMU tracing is enabled but uses default traces that are very rare and don't affect performance, because `./configure --enable-trace-backends=simple` seems to enable some traces by default, e.g. `pr_manager_run`, and I don't know how to get rid of them. ''' - ) - parser.add_argument( - '--trace-stdout', default=defaults['trace_stdout'], action='store_true', - help='''\ + ) + self.add_argument( + '--trace-stdout', default=False, + help='''\ Output trace to stdout instead of a file. Only works for gem5 currently. ''' - ) - init_group.add_argument( - '--terminal', default=defaults['terminal'], action='store_true', - help='''\ + ) + self.add_argument( + '--terminal', default=False, + help='''\ Output to the terminal, don't pipe to tee as the default. Does not save the output to a file, but allows you to use debuggers. Set automatically by --debug-vm, but you still need this option to debug gem5 Python scripts. ''' - ) - parser.add_argument( - '-t', '--tmux', default=defaults['tmux'], nargs='?', action='store', const='', - help='''\ + ) + self.add_argument( + '-t', '--tmux', default=False, + help='''\ Create a tmux split the window. You must already be inside of a `tmux` session to use this option: * on the main window, run the emulator as usual * on the split: ** if on QEMU and `-d` is given, GDB ** if on gem5, the gem5 terminal -If values are given to this option, pass those as parameters -to the program running on the split. ''' - ) - parser.add_argument( - '-u', '--userland', default=defaults['userland'], - help='''\ + ) + self.add_argument( + '--tmux-args', + help='''\ +Parameters to pass to the program running on the tmux split. +''' + ) + self.add_argument( + '-u', '--userland', + help='''\ Run the given userland executable in user mode instead of booting the Linux kernel in full system mode. In gem5, user mode is called Syscall Emulation (SE) mode and uses se.py. - Path resolution is similar to --baremetal. ''' - ) - parser.add_argument( - '--userland-before', default=defaults['userland_before'], - help='''\ + ) + self.add_argument( + '--userland-before', default='', + help='''\ Pass these arguments to the QEMU user mode CLI before the program to execute. This is required with --userland since arguments that come at the end are interpreted as command line arguments to that executable. ''' - ) - kvm_group.add_argument( - '-w', '--wait-gdb', default=defaults['wait_gdb'], action='store_true', - help='Wait for GDB to connect before starting execution' - ) - parser.add_argument( - '-x', '--graphic', default=defaults['graphic'], action='store_true', - help='Run in graphic mode. Mnemonic: X11' - ) - parser.add_argument( - '-V', '--vnc', default=defaults['vnc'], action='store_true', - help='''\ + ) + self.add_argument( + '-w', '--wait-gdb', default=False, + help='Wait for GDB to connect before starting execution' + ) + self.add_argument( + '-x', '--graphic', default=False, + help='Run in graphic mode. Mnemonic: X11' + ) + self.add_argument( + '-V', '--vnc', default=False, + help='''\ Run QEMU with VNC instead of the default SDL. Connect to it with: `vinagre localhost:5900`. ''' - ) - parser.add_argument( - 'extra_emulator_args', nargs='*', default=defaults['extra_emulator_args'], - help='Extra options to append at the end of the emulator command line' - ) - return parser + ) + self.add_argument( + 'extra_emulator_args', nargs='*', default=[], + help='Extra options to append at the end of the emulator command line' + ) + + def timed_main(self): + # Common qemu / gem5 logic. + # nokaslr: + # * https://unix.stackexchange.com/questions/397939/turning-off-kaslr-to-debug-linux-kernel-using-qemu-and-gdb + # * https://stackoverflow.com/questions/44612822/unable-to-debug-kernel-with-qemu-gdb/49840927#49840927 + # Turned on by default since v4.12 + kernel_cli = 'console_msg_format=syslog nokaslr norandmaps panic=-1 printk.devkmsg=on printk.time=y rw' + if self.env['kernel_cli'] is not None: + kernel_cli += ' {}'.format(self.env['kernel_cli']) + kernel_cli_after_dash = '' + extra_emulator_args = [] + extra_qemu_args = [] + if self.env['debug_vm'] is not None: + debug_vm = ['gdb', LF, '-q', LF] + self.sh.shlex_split(self.env['debug_vm_args']) + ['--args', LF] + else: + debug_vm = [] + if self.env['wait_gdb']: + extra_qemu_args.extend(['-S', LF]) + if self.env['eval_after'] is not None: + kernel_cli_after_dash += ' lkmc_eval_base64="{}"'.format(self.base64_encode(self.env['eval_after'])) + if self.env['kernel_cli_after_dash'] is not None: + kernel_cli_after_dash += ' {}'.format(self.env['kernel_cli_after_dash']) + if self.env['vnc']: + vnc = ['-vnc', ':0', LF] + else: + vnc = [] + if self.env['initrd'] or self.env['initramfs']: + ramfs = True + else: + ramfs = False + if self.env['eval'] is not None: + if ramfs: + initarg = 'rdinit' + else: + initarg = 'init' + kernel_cli += ' {}=/eval_base64.sh'.format(initarg) + kernel_cli_after_dash += ' lkmc_eval="{}"'.format(self.base64_encode(self.env['eval'])) + if not self.env['graphic']: + extra_qemu_args.extend(['-nographic', LF]) + console = None + console_type = None + console_count = 0 + if self.env['arch'] == 'x86_64': + console_type = 'ttyS' + elif self.env['is_arm']: + console_type = 'ttyAMA' + console = '{}{}'.format(console_type, console_count) + console_count += 1 + if not (self.env['arch'] == 'x86_64' and self.env['graphic']): + kernel_cli += ' console={}'.format(console) + extra_console = '{}{}'.format(console_type, console_count) + console_count += 1 + if self.env['kdb'] or self.env['kgdb']: + kernel_cli += ' kgdbwait' + if self.env['kdb']: + if self.env['graphic']: + kdb_cmd = 'kbd,' + else: + kdb_cmd = '' + kernel_cli += ' kgdboc={}{},115200'.format(kdb_cmd, console) + if self.env['kgdb']: + kernel_cli += ' kgdboc={},115200'.format(extra_console) + if kernel_cli_after_dash: + kernel_cli += " -{}".format(kernel_cli_after_dash) + extra_env = {} + if self.env['trace'] is None: + do_trace = False + # A dummy value that is already turned on by default and does not produce large output, + # just to prevent QEMU from emitting a warning that '' is not valid. + trace_type = 'load_file' + else: + do_trace = True + trace_type = self.env['trace'] + + def raise_rootfs_not_found(): + if not self.env['dry_run']: + raise Exception('Root filesystem not found. Did you build it?\n' \ + 'Tried to use: ' + self.env['disk_image']) + def raise_image_not_found(): + if not self.env['dry_run']: + raise Exception('Executable image not found. Did you build it?\n' \ + 'Tried to use: ' + self.env['image']) + if self.env['image'] is None: + raise Exception('Baremetal ELF file not found. Tried:\n' + '\n'.join(paths)) + cmd = debug_vm.copy() + if self.env['emulator'] == 'gem5': + if self.env['baremetal'] is None: + if not os.path.exists(self.env['rootfs_raw_file']): + if not os.path.exists(self.env['qcow2_file']): + raise_rootfs_not_found() + self.raw_to_qcow2(prebuilt=self.env['prebuilt'], reverse=True) + else: + if not os.path.exists(self.env['gem5_fake_iso']): + os.makedirs(os.path.dirname(self.env['gem5_fake_iso']), exist_ok=True) + self.write_string_to_file(self.env['gem5_fake_iso'], 'a' * 512) + if not os.path.exists(self.env['image']): + # This is to run gem5 from a prebuilt download. + if (not self.env['baremetal'] is None) or (not os.path.exists(self.env['linux_image'])): + raise_image_not_found() + self.sh.run_cmd([os.path.join(self.env['extract_vmlinux'], self.env['linux_image'])]) + os.makedirs(os.path.dirname(self.env['gem5_readfile']), exist_ok=True) + self.write_string_to_file(self.env['gem5_readfile'], self.env['gem5_readfile']) + memory = '{}B'.format(self.env['memory']) + gem5_exe_args = self.sh.shlex_split(self.env['gem5_exe_args']) + if do_trace: + gem5_exe_args.extend(['--debug-flags={}'.format(trace_type), LF]) + extra_env['M5_PATH'] = self.env['gem5_system_dir'] + # https://stackoverflow.com/questions/52312070/how-to-modify-a-file-under-src-python-and-run-it-without-rebuilding-in-gem5/52312071#52312071 + extra_env['M5_OVERRIDE_PY_SOURCE'] = 'true' + if self.env['trace_stdout']: + debug_file = 'cout' + else: + debug_file = 'trace.txt' + cmd.extend( + [ + self.env['executable'], LF, + '--debug-file', debug_file, LF, + '--listener-mode', 'on', LF, + '--outdir', self.env['m5out_dir'], LF, + ] + + gem5_exe_args + ) + if self.env['userland'] is not None: + cmd.extend([ + self.env['gem5_se_file'], LF, + '-c', self.resolve_userland(self.env['userland']), LF, + ]) + else: + if self.env['gem5_script'] == 'fs': + # TODO port + if self.env['gem5_restore'] is not None: + cpt_dirs = self.gem_list_checkpoint_dirs() + cpt_dir = cpt_dirs[-self.env['gem5_restore']] + extra_emulator_args.extend(['-r', str(sorted(cpt_dirs).index(cpt_dir) + 1)]) + cmd.extend([ + self.env['gem5_fs_file'], LF, + '--disk-image', self.env['disk_image'], LF, + '--kernel', self.env['image'], LF, + '--mem-size', memory, LF, + '--num-cpus', str(self.env['cpus']), LF, + '--script', self.env['gem5_readfile'], LF, + ]) + if self.env['arch'] == 'x86_64': + if self.env['kvm']: + cmd.extend(['--cpu-type', 'X86KvmCPU', LF]) + cmd.extend(['--command-line', 'earlyprintk={} lpj=7999923 root=/dev/sda {}'.format(console, kernel_cli), LF]) + elif self.env['is_arm']: + if self.env['kvm']: + cmd.extend(['--cpu-type', 'ArmV8KvmCPU', LF]) + cmd.extend([ + # TODO why is it mandatory to pass mem= here? Not true for QEMU. + # Anything smaller than physical blows up as expected, but why can't it auto-detect the right value? + '--command-line', 'earlyprintk=pl011,0x1c090000 lpj=19988480 rw loglevel=8 mem={} root=/dev/sda {}'.format(memory, kernel_cli), LF, + '--dtb-filename', os.path.join(self.env['gem5_system_dir'], 'arm', 'dt', 'armv{}_gem5_v1_{}cpu.dtb'.format(self.env['armv'], self.env['cpus'])), LF, + '--machine-type', self.env['machine'], LF, + ]) + if self.env['baremetal'] is None: + cmd.extend([ + '--param', 'system.panic_on_panic = True', LF]) + else: + cmd.extend([ + '--bare-metal', LF, + '--param', 'system.auto_reset_addr = True', LF, + ]) + if self.env['arch'] == 'aarch64': + # https://stackoverflow.com/questions/43682311/uart-communication-in-gem5-with-arm-bare-metal/50983650#50983650 + cmd.extend(['--param', 'system.highest_el_is_64 = True', LF]) + elif self.env['gem5_script'] == 'biglittle': + if self.env['kvm']: + cpu_type = 'kvm' + else: + cpu_type = 'atomic' + if self.env['gem5_restore'] is not None: + cpt_dir = self.gem_list_checkpoint_dirs()[-self.env['gem5_restore']] + extra_emulator_args.extend(['--restore-from', os.path.join(self.env['m5out_dir'], cpt_dir)]) + cmd.extend([ + os.path.join(self.env['gem5_source_dir'], 'configs', 'example', 'arm', 'fs_bigLITTLE.py'), LF, + '--big-cpus', '2', LF, + '--cpu-type', cpu_type, LF, + '--disk', self.env['disk_image'], LF, + '--dtb', os.path.join(self.env['gem5_system_dir'], 'arm', 'dt', 'armv8_gem5_v1_big_little_2_2.dtb'), LF, + '--kernel', self.env['image'], LF, + '--little-cpus', '2', LF, + ]) + if self.env['wait_gdb']: + # https://stackoverflow.com/questions/49296092/how-to-make-gem5-wait-for-gdb-to-connect-to-reliably-break-at-start-kernel-of-th + cmd.extend(['--param', 'system.cpu[0].wait_for_remote_gdb = True', LF]) + else: + qemu_user_and_system_options = [ + '-trace', 'enable={},file={}'.format(trace_type, self.env['qemu_trace_file']), LF, + ] + if self.env['userland'] is not None: + if self.env['wait_gdb']: + debug_args = ['-g', str(self.env['gdb_port']), LF] + else: + debug_args = [] + cmd.extend( + [ + os.path.join(self.env['qemu_build_dir'], '{}-linux-user'.format(self.env['arch']), 'qemu-{}'.format(self.env['arch'])), LF, + '-L', self.env['target_dir'], LF + ] + + qemu_user_and_system_options + + self.sh.shlex_split(self.env['userland_before']) + + debug_args + + [ + self.resolve_userland(self.env['userland']), LF + ] + ) + else: + if not os.path.exists(self.env['image']): + raise_image_not_found() + extra_emulator_args.extend(extra_qemu_args) + self.make_run_dirs() + if self.env['prebuilt'] or not os.path.exists(self.env['qemu_executable']): + qemu_executable = self.env['qemu_executable_basename'] + qemu_executable_prebuilt = True + else: + qemu_executable = self.env['qemu_executable'] + qemu_executable_prebuilt = False + qemu_executable = shutil.which(qemu_executable) + if qemu_executable is None: + raise Exception('QEMU executable not found, did you forget to build or install it?\n' \ + 'Tried to use: ' + qemu_executable) + if self.env['debug_vm']: + serial_monitor = [] + else: + if self.env['background']: + serial_monitor = ['-serial', 'file:{}'.format(self.env['qemu_background_serial_file']), LF] + else: + serial_monitor = ['-serial', 'mon:stdio', LF] + if self.env['kvm']: + extra_emulator_args.extend(['-enable-kvm', LF]) + extra_emulator_args.extend(['-serial', 'tcp::{},server,nowait'.format(self.env['extra_serial_port']), LF]) + virtfs_data = [ + (self.env['p9_dir'], 'host_data'), + (self.env['out_dir'], 'host_out'), + (self.env['out_rootfs_overlay_dir'], 'host_out_rootfs_overlay'), + (self.env['rootfs_overlay_dir'], 'host_rootfs_overlay'), + ] + virtfs_cmd = [] + for virtfs_dir, virtfs_tag in virtfs_data: + if os.path.exists(virtfs_dir): + virtfs_cmd.extend([ + '-virtfs', + 'local,path={virtfs_dir},mount_tag={virtfs_tag},security_model=mapped,id={virtfs_tag}' \ + .format(virtfs_dir=virtfs_dir, virtfs_tag=virtfs_tag), + LF, + ]) + cmd.extend( + [ + qemu_executable, LF, + '-device', 'rtl8139,netdev=net0', LF, + '-gdb', 'tcp::{}'.format(self.env['gdb_port']), LF, + '-kernel', self.env['image'], LF, + '-m', self.env['memory'], LF, + '-monitor', 'telnet::{},server,nowait'.format(self.env['qemu_monitor_port']), LF, + '-netdev', 'user,hostfwd=tcp::{}-:{},hostfwd=tcp::{}-:22,id=net0'.format(self.env['qemu_hostfwd_generic_port'], self.env['qemu_hostfwd_generic_port'], self.env['qemu_hostfwd_ssh_port']), LF, + '-no-reboot', LF, + '-smp', str(self.env['cpus']), LF, + ] + + virtfs_cmd + + serial_monitor + + vnc + ) + if not qemu_executable_prebuilt: + cmd.extend(qemu_user_and_system_options) + if self.env['initrd']: + extra_emulator_args.extend(['-initrd', os.path.join(self.env['buildroot_images_dir'], 'rootfs.cpio')]) + rr = self.env['record'] or self.env['replay'] + if ramfs: + # TODO why is this needed, and why any string works. + root = 'root=/dev/anything' + else: + if rr: + driveif = 'none' + rrid = ',id=img-direct' + root = 'root=/dev/sda' + snapshot = '' + else: + driveif = 'virtio' + root = 'root=/dev/vda' + rrid = '' + snapshot = ',snapshot' + if self.env['baremetal'] is None: + if not os.path.exists(self.env['qcow2_file']): + if not os.path.exists(self.env['rootfs_raw_file']): + raise_rootfs_not_found() + self.raw_to_qcow2(prebuilt=self.env['prebuilt']) + extra_emulator_args.extend([ + '-drive', + 'file={},format=qcow2,if={}{}{}'.format(self.env['disk_image'], driveif, snapshot, rrid), + LF, + ]) + if rr: + extra_emulator_args.extend([ + '-drive', 'driver=blkreplay,if=none,image=img-direct,id=img-blkreplay', LF, + '-device', 'ide-hd,drive=img-blkreplay', LF, + ]) + if rr: + extra_emulator_args.extend([ + '-object', 'filter-replay,id=replay,netdev=net0', + '-icount', 'shift=7,rr={},rrfile={}'.format('record' if self.env['record'] else 'replay', self.env['qemu_rrfile']), + ]) + virtio_gpu_pci = [] + else: + virtio_gpu_pci = ['-device', 'virtio-gpu-pci', LF] + if self.env['arch'] == 'x86_64': + append = ['-append', '{} nopat {}'.format(root, kernel_cli), LF] + cmd.extend([ + '-M', self.env['machine'], LF, + '-device', 'edu', LF, + ]) + elif self.env['is_arm']: + extra_emulator_args.extend(['-semihosting', LF]) + if self.env['arch'] == 'arm': + cpu = 'cortex-a15' + else: + cpu = 'cortex-a57' + append = ['-append', '{} {}'.format(root, kernel_cli), LF] + cmd.extend( + [ + # highmem=off needed since v3.0.0 due to: + # http://lists.nongnu.org/archive/html/qemu-discuss/2018-08/msg00034.html + '-M', '{},highmem=off'.format(self.env['machine']), LF, + '-cpu', cpu, LF, + ] + + virtio_gpu_pci + ) + if self.env['baremetal'] is None: + cmd.extend(append) + if self.env['tmux']: + tmux_args = '--run-id {}'.format(self.env['run_id']) + if self.env['emulator'] == 'gem5': + tmux_cmd = './gem5-shell' + else: + tmux_cmd = './run-gdb' + # TODO find a nicer way to forward all those args automatically. + # Part of me wants to: https://github.com/jonathanslenders/pymux + # but it cannot be used as a library properly it seems, and it is + # slower than tmux. + tmux_args += " --arch {} --linux-build-id '{}' --run-id '{}'".format( + self.env['arch'], + self.env['linux_build_id'], + self.env['run_id'], + ) + if self.env['baremetal']: + tmux_args += " --baremetal '{}'".format(self.env['baremetal']) + if self.env['userland']: + tmux_args += " --userland '{}'".format(self.env['userland']) + tmux_args += ' {}'.format(self.env['tmux']) + subprocess.Popen([ + os.path.join(self.env['root_dir'], 'tmu'), + "sleep 2;{} {}".format(tmux_cmd, tmux_args) + ]) + cmd.extend(extra_emulator_args) + cmd.extend(self.env['extra_emulator_args']) + if debug_vm or self.env['terminal']: + out_file = None + else: + out_file = self.env['termout_file'] + self.sh.run_cmd(cmd, cmd_file=self.env['run_cmd_file'], out_file=out_file, extra_env=extra_env) + # Check if guest panicked. + if self.env['emulator'] == 'gem5': + # We have to do some parsing here because gem5 exits with status 0 even when panic happens. + # Grepping for '^panic: ' does not work because some errors don't show that message. + panic_msg = b'--- BEGIN LIBC BACKTRACE ---$' + else: + panic_msg = b'Kernel panic - not syncing' + panic_re = re.compile(panic_msg) + error_string_found = False + if out_file is not None and not self.env['dry_run']: + with open(self.env['termout_file'], 'br') as logfile: + for line in logfile: + if panic_re.search(line): + error_string_found = True + if os.path.exists(self.env['guest_terminal_file']): + with open(self.env['guest_terminal_file'], 'br') as logfile: + lines = logfile.readlines() + if lines: + last_line = lines[-1] + if last_line.rstrip() == self.env['magic_fail_string']: + error_string_found = True + if error_string_found: + self.log_error('simulation error detected by parsing logs') + return 1 + return 0 if __name__ == '__main__': - parser = get_argparse() - args = common.setup(parser) - start_time = time.time() - exit_status = main(args) - end_time = time.time() - common.print_time(end_time - start_time) - sys.exit(exit_status) + Main().cli() diff --git a/run-docker b/run-docker index a258a3a..513d888 100755 --- a/run-docker +++ b/run-docker @@ -58,7 +58,7 @@ cmd_action_map = { parser = argparse.ArgumentParser() parser.add_argument('cmd', choices=cmd_action_map) parser.add_argument('args', nargs='*') -common.add_dry_run_argument(parser) +self.add_dry_run_argument(parser) args = parser.parse_args() -common.setup_dry_run_arguments(args) +self.setup_dry_run_arguments(args) cmd_action_map[kwargs['cmd']](kwargs['args']) diff --git a/run-gdb b/run-gdb index 5a0e712..1516c02 100755 --- a/run-gdb +++ b/run-gdb @@ -34,8 +34,8 @@ class GdbTestcase: ''' self.prompt = '\(gdb\) ' self.source_path = source_path - common.print_cmd(cmd) - cmd = common.strip_newlines(cmd) + self.print_cmd(cmd) + cmd = self.strip_newlines(cmd) import pexpect self.child = pexpect.spawn( cmd[0], @@ -97,9 +97,9 @@ def main(args, extra_args=None): :rtype: int ''' global defaults - args = common.resolve_args(defaults, args, extra_args) - after = common.shlex_split(kwargs['after']) - before = common.shlex_split(kwargs['before']) + args = self.resolve_args(defaults, args, extra_args) + after = self.sh.shlex_split(kwargs['after']) + before = self.sh.shlex_split(kwargs['before']) no_continue = kwargs['no_continue'] if kwargs['test']: no_continue = True @@ -122,7 +122,7 @@ def main(args, extra_args=None): break_at = [] linux_full_system = (kwargs['baremetal'] is None and kwargs['userland'] is None) if kwargs['userland']: - image = common.resolve_userland(kwargs['userland']) + image = self.resolve_userland(kwargs['userland']) elif kwargs['baremetal']: image = kwargs['image'] test_script_path = os.path.splitext(kwargs['source_path'])[0] + '.py' @@ -133,7 +133,7 @@ def main(args, extra_args=None): else: allowed_toolchains = ['buildroot', 'crosstool-ng', 'host'] cmd = ( - [common.get_toolchain_tool('gdb', allowed_toolchains=allowed_toolchains), LF] + + [self.get_toolchain_tool('gdb', allowed_toolchains=allowed_toolchains), LF] + before + ['-q', LF] ) @@ -195,7 +195,7 @@ def main(args, extra_args=None): ) if __name__ == '__main__': - parser = common.get_argparse(argparse_args={'description': 'Connect with GDB to an emulator to debug Linux itself'}) + parser = self.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' @@ -205,23 +205,23 @@ if __name__ == '__main__': 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', + '-C', '--no-continue', default=defaults['no_continue'], help="Don't run continue after connecting" ) parser.add_argument( - '-k', '--kgdb', default=defaults['kgdb'], action='store_true' + '-k', '--kgdb', default=defaults['kgdb'], ) parser.add_argument( - '--sim', default=defaults['sim'], action='store_true', + '--sim', default=defaults['sim'], 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' + '-X', '--no-lxsymbols', default=defaults['no_lxsymbols'], ) parser.add_argument( - '--test', default=defaults['test'], action='store_true', + '--test', default=defaults['test'], 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. @@ -234,5 +234,5 @@ the script is a .py file next to the source code. 'break_at', nargs='?', help='Extra options to append at the end of the emulator command line' ) - args = common.setup(parser) + args = self.setup(parser) sys.exit(main(args)) diff --git a/run-gdb-user b/run-gdb-user index 5774bba..44884d1 100755 --- a/run-gdb-user +++ b/run-gdb-user @@ -7,7 +7,7 @@ import sys import common rungdb = imp.load_source('rungdb', os.path.join(kwargs['root_dir'], 'run-gdb')) -parser = common.get_argparse(argparse_args={ +parser = self.get_argparse(argparse_args={ 'description': '''GDB step debug guest userland processes without gdbserver. More information at: https://github.com/cirosantilli/linux-kernel-module-cheat#gdb-step-debug-userland-processes @@ -23,9 +23,9 @@ parser.add_argument( help='Break at this point, e.g. main.', nargs='?' ) -args = common.setup(parser) -executable = common.resolve_userland(kwargs['executable']) -addr = common.get_elf_entry(os.path.join(kwargs['buildroot_build_build_dir'], executable)) +args = self.setup(parser) +executable = self.resolve_userland(kwargs['executable']) +addr = self.get_elf_entry(os.path.join(kwargs['buildroot_build_build_dir'], executable)) extra_args = {} extra_args['before'] = '-ex \"add-symbol-file {} {}\"'.format(executable, hex(addr)) # Or else lx-symbols throws for arm: diff --git a/run-gdbserver b/run-gdbserver index 41a1546..c8de580 100755 --- a/run-gdbserver +++ b/run-gdbserver @@ -6,7 +6,7 @@ import sys import common -parser = common.get_argparse(argparse_args={ +parser = self.get_argparse(argparse_args={ 'description':'Connect to gdbserver running on the guest.' }) parser.add_argument( @@ -16,13 +16,13 @@ parser.add_argument( parser.add_argument( 'break_at', default='main', nargs='?' ) -args = common.setup(parser) +args = self.setup(parser) sys.exit(subprocess.Popen([ - common.get_toolchain_tool('gdb'), + self.get_toolchain_tool('gdb'), '-q', '-ex', 'set sysroot {}'.format(kwargs['buildroot_staging_dir']), '-ex', 'target remote localhost:{}'.format(kwargs['qemu_hostfwd_generic_port']), '-ex', 'tbreak {}'.format(kwargs['break_at']), '-ex', 'continue', - os.path.join(kwargs['buildroot_build_build_dir'], common.resolve_userland(kwargs['executable'])), + os.path.join(kwargs['buildroot_build_build_dir'], self.resolve_userland(kwargs['executable'])), ]).wait()) diff --git a/run-toolchain b/run-toolchain index 1d46142..b4af42b 100755 --- a/run-toolchain +++ b/run-toolchain @@ -5,7 +5,7 @@ import sys import common -parser = common.get_argparse(argparse_args={ +parser = self.get_argparse(argparse_args={ 'description': '''Run a Buildroot ToolChain tool like readelf or objdump. For example, to get some information about the arm vmlinux: @@ -24,7 +24,6 @@ ls "$(./getvar -a arm host_bin_dir)" parser.add_argument( '--dry', help='Just output the tool path to stdout but actually run it', - action='store_true', ) parser.add_argument('tool', help='Which tool to run.') parser.add_argument( @@ -34,12 +33,12 @@ parser.add_argument( metavar='extra-args', nargs='*' ) -args = common.setup(parser) +args = self.setup(parser) if kwargs['baremetal'] is None: image = kwargs['vmlinux'] else: image = kwargs['image'] -tool= common.get_toolchain_tool(kwargs['tool']) +tool= self.get_toolchain_tool(kwargs['tool']) if kwargs['dry']: print(tool) else: diff --git a/shell_helpers.py b/shell_helpers.py index 51d7fb8..39796ac 100644 --- a/shell_helpers.py +++ b/shell_helpers.py @@ -6,7 +6,9 @@ import os import shlex import shutil import signal +import stat import subprocess +import sys class LF: ''' diff --git a/trace-boot b/trace-boot index 56780fa..6b6a8f7 100755 --- a/trace-boot +++ b/trace-boot @@ -9,7 +9,7 @@ import common run = imp.load_source('run', os.path.join(kwargs['root_dir'], 'run')) qemu_trace2txt = imp.load_source('qemu_trace2txt', os.path.join(kwargs['root_dir'], 'qemu-trace2txt')) -parser = common.get_argparse(argparse_args={ +parser = self.get_argparse(argparse_args={ 'description': '''Trace the PIC addresses executed on a Linux kernel boot. More information at: https://github.com/cirosantilli/linux-kernel-module-cheat#tracing @@ -19,7 +19,7 @@ parser.add_argument( 'extra_emulator_args', nargs='*', help='Extra options to append at the end of the emulator command line' ) -args = common.setup(parser) +args = self.setup(parser) extra_args = { 'extra_emulator_args': kwargs['extra_emulator_args'], } @@ -39,7 +39,7 @@ else: # Instruction count. # We could put this on a separate script, but it just adds more arch boilerplate to a new script. # So let's just leave it here for now since it did not add a significant processing time. - kernel_entry_addr = hex(common.get_elf_entry(kwargs['vmlinux'])) + kernel_entry_addr = hex(self.get_elf_entry(kwargs['vmlinux'])) nlines = 0 nlines_firmware = 0 with open(kwargs['qemu_trace_txt_file'], 'r') as trace_file: diff --git a/trace2line b/trace2line index 4363848..4aa75a9 100755 --- a/trace2line +++ b/trace2line @@ -14,15 +14,15 @@ import sys import common -parser = common.get_argparse(argparse_args={ +parser = self.get_argparse(argparse_args={ 'description': 'Convert an execution trace containing PC values into the Linux kernel linex executed' }) -args = common.setup(parser) +args = self.setup(parser) sys.exit(subprocess.Popen([ os.path.join(kwargs['root_dir'], 'trace2line.sh'), 'true' if kwargs['emulator'] == 'gem5' else 'false', kwargs['trace_txt_file'], - common.get_toolchain_tool('addr2line'), + self.get_toolchain_tool('addr2line'), kwargs['vmlinux'], kwargs['run_dir'], ]).wait()) @@ -40,7 +40,7 @@ sys.exit(subprocess.Popen([ # with \ # subprocess.Popen( # [ -# common.get_toolchain_tool('addr2line'), +# self.get_toolchain_tool('addr2line'), # '-e', # kwargs['vmlinux'], # '-f',