123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579 |
- #!/usr/bin/env python3
- import os
- import re
- import shlex
- import shutil
- import subprocess
- import sys
- import time
- import common
- defaults = {
- 'cpus': 1,
- 'debug_guest': False,
- 'debug_vm': None,
- 'eval': None,
- 'extra_emulator_args': [],
- 'gem5_exe_args': '',
- 'gem5_script': 'fs',
- 'gem5_readfile': '',
- 'gem5_restore': None,
- 'graphic': False,
- 'initramfs': False,
- 'initrd': False,
- 'kernel_cli': None,
- 'kernel_cli_after_dash': None,
- 'eval_busybox': None,
- 'kgdb': False,
- 'kdb': False,
- 'kvm': False,
- 'memory': '256M',
- 'record': False,
- 'replay': False,
- 'terminal': False,
- 'tmux': None,
- 'trace': None,
- '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 args.kernel_cli is not None:
- kernel_cli += ' {}'.format(args.kernel_cli)
- kernel_cli_after_dash = ''
- extra_emulator_args = []
- extra_qemu_args = []
- if args.debug_vm is not None:
- print(args.debug_vm)
- debug_vm = ['gdb', '-q'] + shlex.split(args.debug_vm) + ['--args']
- else:
- debug_vm = []
- if args.debug_guest:
- extra_qemu_args.append('-S')
- if args.eval_busybox is not None:
- kernel_cli_after_dash += ' lkmc_eval_base64="{}"'.format(common.base64_encode(args.eval_busybox))
- if args.kernel_cli_after_dash is not None:
- kernel_cli_after_dash += ' {}'.format(args.kernel_cli_after_dash)
- if args.vnc:
- vnc = ['-vnc', ':0']
- else:
- vnc = []
- if args.initrd or args.initramfs:
- ramfs = True
- else:
- ramfs = False
- if args.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(args.eval))
- if not args.graphic:
- extra_qemu_args.append('-nographic')
- console = None
- console_type = None
- console_count = 0
- if args.arch == 'x86_64':
- console_type = 'ttyS'
- elif common.is_arm:
- console_type = 'ttyAMA'
- console = '{}{}'.format(console_type, console_count)
- console_count += 1
- if not (args.arch == 'x86_64' and args.graphic):
- kernel_cli += ' console={}'.format(console)
- extra_console = '{}{}'.format(console_type, console_count)
- console_count += 1
- if args.kdb or args.kgdb:
- kernel_cli += ' kgdbwait'
- if args.kdb:
- if args.graphic:
- kdb_cmd = 'kbd,'
- else:
- kdb_cmd = ''
- kernel_cli += ' kgdboc={}{},115200'.format(kdb_cmd, console)
- if args.kgdb:
- kernel_cli += ' kgdboc={},115200'.format(extra_console)
- if kernel_cli_after_dash:
- kernel_cli += " -{}".format(kernel_cli_after_dash)
- extra_env = {}
- if args.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 = args.trace
- def raise_rootfs_not_found():
- raise Exception('Root filesystem not found. Did you build it?\n' \
- 'Tried to use: ' + common.disk_image)
- def raise_image_not_found():
- raise Exception('Executable image not found. Did you build it?\n' \
- 'Tried to use: ' + common.image)
- if common.image is None:
- raise Exception('Baremetal ELF file not found. Tried:\n' + '\n'.join(paths))
- cmd = debug_vm.copy()
- if args.gem5:
- if args.baremetal is None:
- if not os.path.exists(common.rootfs_raw_file):
- if not os.path.exists(common.qcow2_file):
- raise_rootfs_not_found()
- common.raw_to_qcow2(prebuilt=args.prebuilt, reverse=True)
- else:
- if not os.path.exists(common.gem5_fake_iso):
- os.makedirs(os.path.dirname(common.gem5_fake_iso), exist_ok=True)
- common.write_string_to_file(common.gem5_fake_iso, 'a' * 512)
- if not os.path.exists(common.image):
- # This is to run gem5 from a prebuilt download.
- if (not args.baremetal is None) or (not os.path.exists(common.linux_image)):
- raise_image_not_found()
- common.run_cmd([os.path.join(common.extract_vmlinux, common.linux_image)])
- os.makedirs(os.path.dirname(common.gem5_readfile), exist_ok=True)
- common.write_string_to_file(common.gem5_readfile, args.gem5_readfile)
- memory = '{}B'.format(args.memory)
- gem5_exe_args = shlex.split(args.gem5_exe_args)
- if do_trace:
- gem5_exe_args.append('--debug-flags={}'.format(trace_type))
- extra_env['M5_PATH'] = common.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'
- cmd.extend(
- [
- common.executable,
- '--debug-file=trace.txt',
- ] +
- gem5_exe_args +
- [
- '-d', common.m5out_dir
- ]
- )
- if args.userland is not None:
- cmd.extend([common.gem5_se_file, '-c', common.resolve_userland(args.userland)])
- else:
- if args.gem5_script == 'fs':
- # TODO port
- if args.gem5_restore is not None:
- cpt_dirs = common.gem_list_checkpoint_dirs()
- cpt_dir = cpt_dirs[-args.gem5_restore]
- extra_emulator_args.extend(['-r', str(sorted(cpt_dirs).index(cpt_dir) + 1)])
- cmd.extend([
- common.gem5_fs_file,
- '--disk-image', common.disk_image,
- '--kernel', common.image,
- '--mem-size', memory,
- '--num-cpus', str(args.cpus),
- '--script', common.gem5_readfile,
- ])
- if args.arch == 'x86_64':
- if args.kvm:
- cmd.extend(['--cpu-type', 'X86KvmCPU'])
- cmd.extend(['--command-line', 'earlyprintk={} lpj=7999923 root=/dev/sda {}'.format(console, kernel_cli)])
- elif common.is_arm:
- # 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?
- cmd.extend([
- '--command-line', 'earlyprintk=pl011,0x1c090000 lpj=19988480 rw loglevel=8 mem={} root=/dev/sda {}'.format(memory, kernel_cli),
- '--dtb-filename', os.path.join(common.gem5_system_dir, 'arm', 'dt', 'armv{}_gem5_v1_{}cpu.dtb'.format(common.armv, args.cpus)),
- '--machine-type', common.machine,
- '--param', 'system.panic_on_panic = True',
- ])
- if not args.baremetal is None:
- cmd.append('--bare-metal')
- if args.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'])
- cmd.extend(['--param', 'system.auto_reset_addr_64 = True'])
- elif args.gem5_script == 'biglittle':
- if args.gem5_restore is not None:
- cpt_dir = common.gem_list_checkpoint_dirs()[-args.gem5_restore]
- extra_emulator_args.extend(['--restore-from', os.path.join(common.m5out_dir, cpt_dir)])
- cmd.extend([
- os.path.join(common.gem5_src_dir, 'configs', 'example', 'arm', 'fs_bigLITTLE.py'),
- '--big-cpus', '2',
- '--cpu-type', 'atomic',
- '--disk', common.disk_image,
- '--dtb', os.path.join(common.gem5_system_dir, 'arm', 'dt', 'armv8_gem5_v1_big_little_2_2.dtb'),
- '--kernel', common.image,
- '--little-cpus', '2'
- ])
- if args.debug_guest:
- # 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'])
- else:
- qemu_user_and_system_options = [
- '-trace', 'enable={},file={}'.format(trace_type, common.qemu_trace_file),
- ]
- if args.userland is not None:
- if args.debug_guest:
- debug_args = ['-g', str(common.gdb_port)]
- else:
- debug_args = []
- cmd.extend(
- [
- os.path.join(common.qemu_build_dir, '{}-linux-user'.format(args.arch), 'qemu-{}'.format(args.arch)),
- '-L', common.target_dir,
- ] +
- qemu_user_and_system_options +
- shlex.split(args.userland_before) +
- debug_args +
- [
- common.resolve_userland(args.userland)
- ]
- )
- else:
- if not os.path.exists(common.image):
- raise_image_not_found()
- extra_emulator_args.extend(extra_qemu_args)
- common.make_run_dirs()
- if args.prebuilt:
- qemu_executable = common.qemu_executable_basename
- qemu_found = shutil.which(qemu_executable) is not None
- else:
- qemu_executable = common.qemu_executable
- qemu_found = os.path.exists(qemu_executable)
- if not qemu_found:
- raise Exception('QEMU executable not found, did you forget to build or install it?\n' \
- 'Tried to use: ' + qemu_executable)
- if args.debug_vm:
- serial_monitor = []
- else:
- serial_monitor = ['-serial', 'mon:stdio']
- if args.kvm:
- extra_emulator_args.append('-enable-kvm')
- extra_emulator_args.extend(['-serial', 'tcp::{},server,nowait'.format(common.extra_serial_port)])
- virtfs_data = [
- (common.p9_dir, 'host_data'),
- (common.out_dir, 'host_out'),
- (common.out_rootfs_overlay_dir, 'host_out_rootfs_overlay'),
- (common.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
- )])
- cmd.extend(
- [
- qemu_executable,
- '-device', 'rtl8139,netdev=net0',
- '-gdb', 'tcp::{}'.format(common.gdb_port),
- '-kernel', common.image,
- '-m', args.memory,
- '-monitor', 'telnet::{},server,nowait'.format(common.qemu_monitor_port),
- '-netdev', 'user,hostfwd=tcp::{}-:{},hostfwd=tcp::{}-:22,id=net0'.format(common.qemu_hostfwd_generic_port, common.qemu_hostfwd_generic_port, common.qemu_hostfwd_ssh_port),
- '-no-reboot',
- '-smp', str(args.cpus),
- ] +
- virtfs_cmd +
- qemu_user_and_system_options +
- serial_monitor +
- vnc
- )
- if args.initrd:
- extra_emulator_args.extend(['-initrd', os.path.join(common.buildroot_images_dir, 'rootfs.cpio')])
- rr = args.record or args.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 args.baremetal is None:
- if not os.path.exists(common.qcow2_file):
- if not os.path.exists(common.rootfs_raw_file):
- raise_rootfs_not_found()
- common.raw_to_qcow2(prebuilt=args.prebuilt)
- extra_emulator_args.extend([
- '-drive',
- 'file={},format=qcow2,if={}{}{}'.format(common.disk_image, driveif, snapshot, rrid)
- ])
- if rr:
- extra_emulator_args.extend([
- '-drive', 'driver=blkreplay,if=none,image=img-direct,id=img-blkreplay',
- '-device', 'ide-hd,drive=img-blkreplay'
- ])
- if rr:
- extra_emulator_args.extend([
- '-object', 'filter-replay,id=replay,netdev=net0',
- '-icount', 'shift=7,rr={},rrfile={}'.format('record' if args.record else 'replay', common.qemu_rrfile),
- ])
- virtio_gpu_pci = []
- else:
- virtio_gpu_pci = ['-device', 'virtio-gpu-pci']
- if args.arch == 'x86_64':
- append = ['-append', '{} nopat {}'.format(root, kernel_cli)]
- cmd.extend([
- '-M', common.machine,
- '-device', 'edu',
- ])
- elif common.is_arm:
- extra_emulator_args.append('-semihosting')
- if args.arch == 'arm':
- cpu = 'cortex-a15'
- else:
- cpu = 'cortex-a57'
- append = ['-append', '{} {}'.format(root, kernel_cli)]
- 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(common.machine),
- '-cpu', cpu,
- ] +
- virtio_gpu_pci
- )
- if args.baremetal is None:
- cmd.extend(append)
- if args.tmux is not None:
- if args.gem5:
- subprocess.Popen([os.path.join(common.root_dir, 'tmu'),
- 'sleep 2;./gem5-shell -n {} {}' \
- .format(args.run_id, args.tmux)
- ])
- elif args.debug_guest:
- # 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.
- subprocess.Popen([os.path.join(common.root_dir, 'tmu'),
- "sleep 2;./run-gdb --arch '{}' --linux-build-id '{}' --run-id '{}' {}" \
- .format(
- args.arch,
- args.linux_build_id,
- args.run_id,
- args.tmux
- )
- ])
- cmd.extend(extra_emulator_args)
- cmd.extend(args.extra_emulator_args)
- if debug_vm or args.terminal:
- out_file = None
- else:
- out_file = common.termout_file
- common.run_cmd(cmd, cmd_file=common.run_cmd_file, out_file=out_file, extra_env=extra_env)
- # Check if guest panicked.
- if args.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)
- with open(common.termout_file, 'br') as logfile:
- for line in logfile:
- if panic_re.search(line):
- 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(
- '-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.'
- )
- kvm_group.add_argument(
- '-d', '--debug-guest', default=defaults['debug_guest'], action='store_true',
- help='Wait for GDB to connect before starting execution'
- )
- parser.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='''\
- 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-busybox',
- help='''\
- Pass a base64 encoded command line parameter that gets evalled by the Busybox init.
- See: https://github.com/cirosantilli/linux-kernel-module-cheat#init-busybox
- '''
- )
- parser.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
- by `lkmc_`.
- Example: `./run -f '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='''\
- Pass extra options to the gem5 executable.
- Do not confuse with the arguments passed to config scripts,
- like `fs.py`. Example:
- ./run -G '--debug-flags=Exec --debug' --gem5 -- --cpu-type=HPI --caches
- 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.'
- )
- init_group.add_argument(
- '-I', '--initramfs', default=defaults['initramfs'], action='store_true',
- help='Use initramfs instead of a root filesystem'
- )
- init_group.add_argument(
- '-i', '--initrd', default=defaults['initrd'], action='store_true',
- help='Use initrd instead of a root filesystem'
- )
- 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='''\
- Restore the nth most recently taken gem5 checkpoint according to directory
- timestamps.
- '''
- )
- parser.add_argument(
- '-m', '--memory', default=defaults['memory'],
- 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='''\
- 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.
- '''
- )
- init_group.add_argument(
- '--terminal', default=defaults['terminal'], action='store_true',
- 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='''\
- 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='''\
- 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='''\
- Pass these Krguments 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.
- '''
- )
- 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='''\
- 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
- 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)
|