From da900a579c9720b9cd895c0e1dd3de2ac93f36fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciro=20Santilli=20=E5=85=AD=E5=9B=9B=E4=BA=8B=E4=BB=B6=20?= =?UTF-8?q?=E6=B3=95=E8=BD=AE=E5=8A=9F?= Date: Tue, 22 Jan 2019 00:00:00 +0000 Subject: [PATCH] LKMC v3.0 This is a squash commit, the unsquashed development went through many unstable phases which would break bisects. The unsquashed branch is: https://github.com/cirosantilli/linux-kernel-module-cheat/tree/v3.0-unsquash The main improvement of this release was to greatly generalize the testing system. The key addition was cli_function.py, which allows scripts such as ./run to be transparently called either from Python or from the command line. New tests scripts were created using this improved framework: test-baremetal and test-user-mode. We were lazy to port some of less important tests to the new setup, TODO's were added, and we need comes they will be fixed. Getting started is however sacred as usual and should work. Other changes include: - gem5: update to 7fa4c946386e7207ad5859e8ade0bbfc14000d91 - run: --tmux-args implies --tmux - run: add --userland-args to make userland arguments across QEMU and gem5 Get rid of --userland-before as a consequence. - bring initrd and initramfs back to life - build-userland: create --static to make build a bit easier - gem5: --gem5-worktree also set --gem5-build-id - remove --gem5, use --emulator gem5 everywhere Allow passing --emulator multiple times for transparent tests selection just like --arch. - test-userland: allow selecting just a few tests - linux: update to v4.20 - buildroot: update to 2018.08 The main motivation for this was to fix the build for Ubuntu 18.10, which has glibc 2.28, which broke the 2018.05 build at the m4-host package with: #error "Please port gnulib fseeko.c to your platform! - getvar --type input - failed xen attempt, refactor timer, failed svc attempt, aarch64 use gicv3 - build-doc: exit 1 on error, add to release testing - build: add --apt option to make things easier on other distros - build-linux: --no-modules-install --- .gitmodules | 6 + README.adoc | 993 +++++++---- arm | 3 - baremetal/add.c | 12 +- baremetal/arch/aarch64/common_aarch64.h | 24 + baremetal/arch/aarch64/el.c | 8 +- baremetal/arch/aarch64/multicore.S | 1 + baremetal/arch/aarch64/svc.c | 28 + baremetal/arch/aarch64/timer.c | 59 + baremetal/arch/arm/el.c | 8 +- baremetal/exit.c | 3 +- baremetal/{interactive => }/hello.c | 4 +- baremetal/interactive/assert_fail.c | 2 +- baremetal/interactive/exit1.c | 2 +- baremetal/interactive/infinite_loop.c | 4 + baremetal/lib/syscalls.c | 22 +- bench-all | 12 +- bench-boot | 94 -- bisect-linux-boot-gem5 | 12 +- bisect-qemu-linux-boot | 8 +- bst-vs-heap | 6 +- build | 875 +++++----- build-baremetal | 154 +- build-bench-boot | 15 - build-buildroot | 168 +- build-crosstool-ng | 86 +- build-doc | 40 +- build-docker | 35 +- build-gem5 | 121 +- build-linux | 153 +- build-m5 | 55 +- build-modules | 110 +- build-qemu | 57 +- build-test | 6 +- build-test-boot | 20 + build-test-gdb | 2 - build-userland | 92 +- build-xen | 37 + buildroot_config/default | 4 - cli_function.py | 450 +++++ cli_function_test_config.py | 5 + cli_function_test_config_2.py | 6 + common.py | 2057 ++++++++++++----------- config.example | 4 - config.py | 10 + copy-overlay | 28 +- gem5-bench-cache | 2 +- gem5-bench-dhrystone | 4 +- gem5-shell | 31 +- gem5-stat | 36 +- getvar | 46 +- qemu-monitor | 11 +- qemu-trace2txt | 39 +- release | 32 - release-download-latest | 26 +- release-upload | 128 +- release-zip | 46 +- rootfs_overlay/gem5_exit.sh | 4 + rootfs_overlay/test_all.sh | 40 +- rootfs_overlay/test_fail.sh | 3 + run | 1151 +++++++------ run-docker | 52 +- run-gdb | 309 ++-- run-gdb-user | 12 +- run-gdbserver | 15 +- run-toolchain | 24 +- shell_helpers.py | 296 ++++ submodules/boot-wrapper-aarch64 | 1 + submodules/buildroot | 2 +- submodules/gem5 | 2 +- submodules/linux | 2 +- submodules/xen | 1 + test | 53 +- test-baremetal | 56 + test-boot | 92 + test-gdb | 92 +- test-modules | 7 - test-user-mode | 60 + test-userland-full-system | 22 + trace-boot | 100 +- trace2line | 29 +- userland/Makefile | 8 +- userland/count.c | 21 +- userland/external.desc | 1 - userland/false.c | 13 + userland/print_argv.c | 2 + 86 files changed, 5235 insertions(+), 3537 deletions(-) delete mode 100755 arm create mode 100644 baremetal/arch/aarch64/common_aarch64.h create mode 100644 baremetal/arch/aarch64/svc.c create mode 100644 baremetal/arch/aarch64/timer.c rename baremetal/{interactive => }/hello.c (54%) create mode 100644 baremetal/interactive/infinite_loop.c delete mode 100755 bench-boot delete mode 100755 build-bench-boot create mode 100755 build-test-boot delete mode 100755 build-test-gdb create mode 100755 build-xen create mode 100755 cli_function.py create mode 100644 cli_function_test_config.py create mode 100644 cli_function_test_config_2.py delete mode 100644 config.example create mode 100644 config.py delete mode 100755 release create mode 100755 rootfs_overlay/gem5_exit.sh create mode 100755 rootfs_overlay/test_fail.sh create mode 100644 shell_helpers.py create mode 160000 submodules/boot-wrapper-aarch64 create mode 160000 submodules/xen create mode 100755 test-baremetal create mode 100755 test-boot delete mode 100755 test-modules create mode 100755 test-user-mode create mode 100755 test-userland-full-system delete mode 100644 userland/external.desc create mode 100644 userland/false.c diff --git a/.gitmodules b/.gitmodules index b020cd46..81b53da0 100644 --- a/.gitmodules +++ b/.gitmodules @@ -21,3 +21,9 @@ [submodule "submodules/qemu"] path = submodules/qemu url = https://github.com/cirosantilli/qemu +[submodule "submodules/xen"] + path = submodules/xen + url = git://xenbits.xen.org/xen.git +[submodule "submodules/boot-wrapper-aarch64"] + path = submodules/boot-wrapper-aarch64 + url = git://git.kernel.org/pub/scm/linux/kernel/git/mark/boot-wrapper-aarch64.git diff --git a/README.adoc b/README.adoc index 60ea6058..24ce12c0 100644 --- a/README.adoc +++ b/README.adoc @@ -9,7 +9,7 @@ :toclevels: 6 :toc-title: -The perfect emulation setup to study and modify the <>, kernel modules, <> and <>. Highly automated. Thoroughly documented. <> and <> just work. Powered by <>. "Tested" in Ubuntu 18.04 host, x86 and ARM guests with kernel v4.19. +The perfect emulation setup to study and modify the <>, kernel modules, <> and <>. Highly automated. Thoroughly documented. <> and <> just work. Automated <>. Powered by <>. "Tested" in Ubuntu 18.04 host, x86 and ARM guests with kernel v4.20. TL;DR: <> @@ -84,7 +84,7 @@ See also: <>. All available modules can be found in the link:kernel_modules[] directory. -It is super easy to build for different CPU architectures, just use the `--arch` option: +It is super easy to build for different <>, just use the `--arch` option: .... ./build --arch aarch64 --download-dependencies qemu-buildroot @@ -93,8 +93,6 @@ It is super easy to build for different CPU architectures, just use the `--arch` To avoid typing `--arch aarch64` many times, you set the default arch as explained at: <> -See also: <>. - I now urge you to read the following sections which contain widely applicable information: * <> @@ -195,6 +193,8 @@ The link:build[] script is just a lightweight wrapper that calls the smaller bui ./build --dry-run .... +When you reach difficulties, QEMU makes it possible to easily GDB step debug the Linux kernel source code, see: <>. + ===== Your first kernel module hack Edit link:kernel_modules/hello.c[] to contain: @@ -219,9 +219,13 @@ insmod /mnt/9p/out_rootfs_overlay/hello.ko and the new `pr_info` message should now show on the terminal at the end of the boot. -This works because we have a <<9p>> mount there setup by default, which makes a host directory available on the guest. +This works because we have a <<9p>> mount there setup by default, which mounts the host directory that contains the Build outputs on the guest: + +.... +ls "$(./getvar out_rootfs_overlay_dir)" +.... -The fast method is slightly risky because your kernel module might have corrupted the kernel memory, which could affect future runs. +The fast method is slightly risky because your previously insmodded buggy kernel module attempt might have corrupted the kernel memory, which could affect future runs. Such failures are however unlikely, and you should be fine if you don't see anything weird happening. @@ -233,7 +237,7 @@ The safe way, is to fist quit QEMU, rebuild the modules, put them in the root fi ./run --eval-after 'insmod /hello.ko' .... -`./build-buildroot` is required after `./build-modules` because it generates the root filesystem with the modules that we compiled at `./build-modules`. +`./build-buildroot` is required after `./build-modules` because it re-generates the root filesystem with the modules that we compiled at `./build-modules`. You can see that `./build` does that as well, by running: @@ -251,6 +255,8 @@ If the guest and host are the same arch, typically x86_64, you can speed up boot All of this put together makes the safe procedure acceptably fast for regular development as well. +It is also easy to GDB step debug kernel modules with our setup, see: <>. + ===== Your first QEMU hack Not satisfied with mere software? OK then, let's hack up the QEMU x86 CPU identification: @@ -293,6 +299,8 @@ The only thing you can do with open source is purely functional designs with lin If you really want to develop semiconductors, your only choice is to join an university or a semiconductor company that has the EDA licenses. +While hacking QEMU, you will likely want to GDB step its source. That is trivial since QEMU is just another userland program like any other, but our setup has a shortcut to make it even more convenient, see: <>. + ==== About the QEMU Buildroot setup This is our reference setup, and the best supported one, use it unless you have good reason not to. @@ -330,13 +338,13 @@ See <> for a more thorough comparison. ==== gem5 Buildroot setup getting started -For the most part, if you just add the `--gem5` option or `*-gem5` suffix to all commands and everything should magically work. +For the most part, if you just add the `--emulator gem5` option or `*-gem5` suffix to all commands and everything should magically work. If you haven't built Buildroot yet for <>, you can build from the beginning with: .... ./build --download-dependencies gem5-buildroot -./run --gem5 +./run --emulator gem5 .... If you have already built previously, don't be afraid: gem5 and QEMU use almost the same root filesystem and kernel, so `./build` will be fast. @@ -358,7 +366,7 @@ You can quit the shell without killing gem5 by typing tilde followed by a period If you are inside <>, which I highly recommend, you can both run gem5 stdout and open the guest terminal on a split window with: .... -./run --gem5 --tmux +./run --emulator gem5 --tmux .... See also: <>. @@ -375,7 +383,7 @@ but if you look closely, the `PS1` prompt marker `#` is there already, just hit If you forgot to open the shell and gem5 exit, you can inspect the terminal output post-mortem at: .... -less "$(./getvar --gem5 m5out_dir)/system.pc.com_1.device" +less "$(./getvar --emulator gem5 m5out_dir)/system.pc.com_1.device" .... More gem5 information is present at: <> @@ -485,18 +493,20 @@ TODO make files created inside Docker be owned by the current user in host inste * https://stackoverflow.com/questions/31779802/shared-volume-file-permissions-ownership-docker [[prebuilt]] -=== Prebuilt Buildroot setup +=== Prebuilt setup -==== About the prebuilt Buildroot setup +==== About the prebuilt setup -This setup uses prebuilt binaries of the <> that we upload to GitHub from time to time. +This setup uses prebuilt binaries that we upload to GitHub from time to time. We don't currently provide a full prebuilt because it would be too big to host freely, notably because of the cross toolchain. Our prebuilts currently include: -* Linux kernel -* root filesystem +* <> binaries +** Linux kernel +** root filesystem +* <> binaries for QEMU For more details, see our our <>. @@ -512,9 +522,9 @@ Maybe we could work around this by just downloading the kernel source somehow, a This setup might be good enough for those developing simulators, as that requires less image modification. But once again, if you are serious about this, why not just let your computer build the <> while you take a coffee or a nap? :-) -==== Prebuilt Buildroot setup getting started +==== Prebuilt setup getting started -Checkout to the latest tag and use the Ubuntu packaged QEMU: +Checkout to the latest tag and use the Ubuntu packaged QEMU to boot Linux: .... sudo apt-get install qemu-system-x86 @@ -526,13 +536,18 @@ unzip lkmc-*.zip ./run --prebuilt .... +Or to run a baremetal example instead: + +.... +./run --arch aarch64 --baremetal baremetal/hello.c --prebuilt +.... + You have to checkout to the latest tag to ensure that the scripts match the release format: https://stackoverflow.com/questions/1404796/how-to-get-the-latest-tag-name-in-current-branch-in-git Be saner and use our custom built QEMU instead: .... -git submodule update --init --recursive "$(./getvar qemu_src_dir)" -./build-qemu +./build --download-dependencies qemu ./run .... @@ -541,18 +556,24 @@ This also allows you to <> if you're into that To build the kernel modules as in <> do: .... -./build-linux -- modules_prepare +git submodule update --depth 1 --init --recursive "$(./getvar linux_source_dir)" +./build-linux --no-modules-install -- modules_prepare ./build-modules ./run .... -`modules_prepare` does the minimal build procedure required on the kernel for us to be able to compile the kernel modules, and is way faster than doing a full kernel build. A full kernel build would also work however. +TODO: for now the only way to test those modules out without <> is with 9p, since we currently rely on Buildroot to manipulate the root filesystem. -This command automatically falls back to the Ubuntu packaged GCC since you don't have the Buildroot toolchain. +Command explanation: + +* `modules_prepare` does the minimal build procedure required on the kernel for us to be able to compile the kernel modules, and is way faster than doing a full kernel build. A full kernel build would also work however. +* the `./build-modules` command automatically falls back to the Ubuntu packaged GCC since you don't have the Buildroot toolchain +* `--no-modules-install` is required otherwise the `make modules_install` target we run by default fails, since the kernel wasn't built To modify the Linux kernel, build and use it as usual: .... +git submodule update --depth 1 --init --recursive "$(./getvar linux_source_dir)" ./build-linux ./run .... @@ -564,7 +585,7 @@ For gem5, do: git submodule update --init --depth 1 "$(./getvar linux_source_dir)" sudo apt-get install qemu-utils ./build-gem5 -./run --gem5 --prebuilt +./run --emulator gem5 --prebuilt .... `qemu-utils` is required because we currently distribute `.qcow2` files which <>, so we need `qemu-img` to extract them first. @@ -646,13 +667,13 @@ The best solution is to compile just your modules with: which is equivalent to: .... -./build-modules --host -- packages/kernel/modules/hello.c packages/kernel/modules/hello2.c +./build-modules --host -- kernel_modules/hello.c kernel_modules/hello2.c .... Or just remove the `.c` extension from the failing files and try again: .... -cd "$(./getvar kernel_modules_src_dir)" +cd "$(./getvar kernel_modules_source_dir)" mv broken.c broken.c~ .... @@ -708,62 +729,71 @@ Our C bare-metal compiler is built with link:https://github.com/crosstool-ng/cro ==== Baremetal setup getting started -QEMU: +Every `.c` file inside link:baremetal/[] and `.S` file inside `baremetal/arch//` generates a separate baremetal image. + +For example, to run link:baremetal/hello.c[] in QEMU do: .... ./build --arch aarch64 --download-dependencies qemu-baremetal -./run --arch aarch64 --baremetal interactive/prompt +./run --arch aarch64 --baremetal hello .... -You are now left inside QEMU running the tiny baremetal system link:baremetal/interactive/prompt.c[], which uses the UART to: +The terminal prints: -* print characters to the terminal -* read characters from your keyboard +.... +hello +.... -A session looks like this after typing `abc`: +Now let's run link:baremetal/arch/aarch64/add.S[]: .... -enter a character -got: a -new alloc of 1 bytes at address 0x0x4000a2c8 -enter a character -got: b -new alloc of 2 bytes at address 0x0x4000a2c8 -enter a character -got: c -new alloc of 4 bytes at address 0x0x4000a2c8 +./run --arch aarch64 --baremetal arch/aarch64/add +.... + +This time, the terminal does not print anything, which indicates success. + +If you look into the source, you will see that we just have an assertion there. + +You can see a sample assertion fail in link:baremetal/interactive/assert_fail.c[]: + +.... +./run --arch aarch64 --baremetal interactive/assert_fail .... -To modify that program, edit: +and the terminal contains: .... -vim baremetal/interactive/prompt.c +lkmc_test_fail +error: simulation error detected by parsing logs .... -and run: +and the exit status of our script is 1: .... -./build-baremetal --arch aarch64 +echo $? .... -`./build qemu-baremetal` had called link:build-baremetal[] for us previously, in addition to its requirements. `./build-baremetal` uses crosstool-NG, and so it must be preceded by link:build-crosstool-ng[], which `./build qemu-baremetal` also calls. +To modify a baremetal program, simply edit the file, .g. + +.... +vim baremetal/hello.c +.... -Every `.c` file inside link:baremetal/[] and `.S` file inside `baremetal/arch//` generates a separate baremetal image. You can run a different image with commands such as: +and rebuild: .... -./run --arch aarch64 --baremetal exit -./run --arch aarch64 --baremetal arch/aarch64/add +./build --arch aarch64 --download-dependencies qemu-baremetal +./run --arch aarch64 --baremetal hello .... -which will run respectively: +`./build qemu-baremetal` had called link:build-baremetal[] for us previously, in addition to its requirements. -* link:baremetal/exit.c[] -* link:baremetal/arch/aarch64/add.S[] +`./build-baremetal` uses crosstool-NG, and so it must be preceded by link:build-crosstool-ng[], which `./build qemu-baremetal` also calls. -Alternatively, for the sake of tab completion, we also accept relative paths inside `baremetal/`: +Alternatively, for the sake of tab completion, we also accept relative paths inside `baremetal/`, for example the following also work: .... -./run --arch aarch64 --baremetal baremetal/exit.c +./run --arch aarch64 --baremetal baremetal/hello.c ./run --arch aarch64 --baremetal baremetal/arch/aarch64/add.S .... @@ -777,7 +807,7 @@ To use gem5 instead of QEMU do: .... ./build --download-dependencies gem5-baremetal -./run --arch aarch64 --baremetal interactive/prompt --gem5 +./run --arch aarch64 --baremetal interactive/prompt --emulator gem5 .... and then <> open a shell with: @@ -786,20 +816,26 @@ and then <> open a shell with: ./gem5-shell .... +Or as usual, <> users can do both in one go with: + +.... +./run --arch aarch64 --baremetal interactive/prompt --emulator gem5 --tmux +.... + TODO: the carriage returns are a bit different than in QEMU, see: <>. -Note that `./build-baremetal` requires the `--gem5` option, and generates separate executable images for both, as can be seen from: +Note that `./build-baremetal` requires the `--emulator gem5` option, and generates separate executable images for both, as can be seen from: .... -echo "$(./getvar --arch aarch64 --baremetal interactive/prompt image)" -echo "$(./getvar --arch aarch64 --baremetal interactive/prompt --gem5 image)" +echo "$(./getvar --arch aarch64 --baremetal interactive/prompt --emulator qemu image)" +echo "$(./getvar --arch aarch64 --baremetal interactive/prompt --emulator gem5 image)" .... This is unlike the Linux kernel that has a single image for both QEMU and gem5: .... -echo "$(./getvar --arch aarch64 image)" -echo "$(./getvar --arch aarch64 --gem5 image)" +echo "$(./getvar --arch aarch64 --emulator qemu image)" +echo "$(./getvar --arch aarch64 --emulator gem5 image)" .... The reason for that is that on baremetal we don't parse the <> from memory like the Linux kernel does, which tells the kernel for example the UART address, and many other system parameters. @@ -807,15 +843,15 @@ The reason for that is that on baremetal we don't parse the <>. The following subjects are particularly important: +For more information on baremetal, see the section: <>. + +The following subjects are particularly important: * <> * <> +=== User mode setup + +Much like <>, this is another fun setup that does not require Buildroot or the Linux kernel. + +Introduction at: <>. + +Getting started at: <>. + [[gdb]] == GDB step debug @@ -866,6 +912,20 @@ See also: * http://stackoverflow.com/questions/11408041/how-to-debug-the-linux-kernel-with-gdb-and-qemu/33203642#33203642 * http://stackoverflow.com/questions/4943857/linux-kernel-live-debugging-how-its-done-and-what-tools-are-used/42316607#42316607 +==== GDB step debug kernel boot other archs + +Just don't forget to pass `--arch` to `./run-gdb`, e.g.: + +.... +./run --arch aarch64 --wait-gdb +.... + +and: + +.... +./run-gdb --arch aarch64 start_kernel +.... + [[kernel-o0]] ==== Disable kernel compiler optimizations @@ -960,7 +1020,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-args start_kernel .... See the tmux manual for further details: @@ -976,7 +1036,7 @@ Bibliography: https://unix.stackexchange.com/questions/152738/how-to-split-a-new If you are using gem5 instead of QEMU, `--tmux` has a different effect: it opens the gem5 terminal instead of the debugger: .... -./run --gem5 --tmux +./run --emulator gem5 --tmux .... If you also want to use the debugger with gem5, you will need to create new terminals as usual. @@ -987,7 +1047,7 @@ To see the debugger by default instead of the terminal, run: .... ./tmu ./run-gdb -./run --wait-gdb --gem5 +./run --wait-gdb --emulator gem5 .... === GDB step debug kernel module @@ -2449,7 +2509,7 @@ asdf=qwer Source: link:userland/init_env_poweroff.c[]. -==== init environment args +==== init arguments The annoying dash `-` gets passed as a parameter to `init`, which makes it impossible to use this method for most non custom executables. @@ -2470,6 +2530,8 @@ ab so see how `a.b` is gone. +The simple workaround is to just create a shell script that does it, e.g. as we've done at: link:rootfs_overlay/gem5_exit.sh[]. + ==== init environment env Wait, where do `HOME` and `TERM` come from? (greps the kernel). Ah, OK, the kernel sets those by default: https://github.com/torvalds/linux/blob/94710cac0ef4ee177a63b5227664b38c95bbf703/init/main.c#L173 @@ -2540,13 +2602,13 @@ where `$$` is the PID of the shell itself: https://stackoverflow.com/questions/2 == initrd -TODO: broken when we started building the Linux manually with `./build-linux` instead of Buildroot. Was working before, see e.g. 56738a1c70e50bf7b6d5fbe02372c5d277a8286f. - The kernel can boot from an CPIO file, which is a directory serialization format much like tar: https://superuser.com/questions/343915/tar-vs-cpio-what-is-the-difference -The bootloader, which for us is QEMU itself, is then configured to put that CPIO into memory, and tell the kernel that it is there. +The bootloader, which for us is provided by QEMU itself, is then configured to put that CPIO into memory, and tell the kernel that it is there. -With this setup, you don't even need to give a root filesystem to the kernel, it just does everything in memory in a ramfs. +This is very similar to the kernel image itself, which already gets put into memory by the QEMU `-kernel` option. + +With this setup, you don't even need to give a root filesystem to the kernel: it just does everything in memory in a ramfs. To enable initrd instead of the default ext2 disk image, do: @@ -2555,13 +2617,17 @@ To enable initrd instead of the default ext2 disk image, do: ./run --initrd .... -Notice how it boots fine, even though this leads to not giving QEMU the `-drive` option, as can be verified with: +By looking at the QEMU run command generated, you can see that we didn't give the `-drive` option at all: .... cat "$(./getvar run_dir)/run.sh" .... -Also as expected, there is no filesystem persistency, since we are doing everything in memory: +Instead, we used the QEMU `-initrd` option to point to the `.cpio` filesystem that Buildroot generated for us. + +Try removing that `-initrd` option to watch the kernel panic without rootfs at the end of boot. + +When using `.cpio`, there can be no filesystem persistency across boots, since all file operations happen in memory in a tmpfs: .... date >f @@ -2572,6 +2638,8 @@ cat f which can be good for automated tests, as it ensures that you are using a pristine unmodified system image every time. +Not however that we already disable disk persistency by default on ext2 filesystems even without `--initrd`: <>. + One downside of this method is that it has to put the entire filesystem into memory, and could lead to a panic: .... @@ -2594,7 +2662,7 @@ It is also possible to compress that image with other options. + Buildroot forces that option when `BR2_TARGET_ROOTFS_CPIO=y` is given -https://unix.stackexchange.com/questions/89923/how-does-linux-load-the-initrd-image asks how the mechanism works in more detail. +TODO: how does the bootloader inform the kernel where to find initrd? https://unix.stackexchange.com/questions/89923/how-does-linux-load-the-initrd-image === initrd in desktop distros @@ -2614,25 +2682,25 @@ Related: https://stackoverflow.com/questions/6405083/initrd-and-booting-the-linu === initramfs -initramfs is just like <>, but you also glue the image directly to the kernel image itself. - -So the only argument that QEMU needs is the `-kernel`, no `-drive` not even `-initrd`! Pretty cool. +initramfs is just like <>, but you also glue the image directly to the kernel image itself using the kernel's build system. Try it out with: .... -./build-buildroot --initramfs -l +./build-buildroot --initramfs +./build-linux --initramfs ./run --initramfs .... -The `-l` (ell) should only be used the first time you move to / from a different root filesystem method (ext2 or cpio) to initramfs to overcome: https://stackoverflow.com/questions/49260466/why-when-i-change-br2-linux-kernel-custom-config-file-and-run-make-linux-reconfi +Notice how we had to rebuild the Linux kernel this time around as well after Buildroot, since in that build we will be gluing the CPIO to the kernel image. + +Now, once again, if we look at the QEMU run command generated, we see all that QEMU needs is the `-kernel` option, no `-drive` not even `-initrd`! Pretty cool: .... -./build-buildroot --initramfs -./run --initramfs +cat "$(./getvar run_dir)/run.sh" .... -It is interesting to see how this increases the size of the kernel image if you do a: +It is also interesting to observe how this increases the size of the kernel image if you do a: .... ls -lh "$(./getvar linux_image)" @@ -2640,7 +2708,22 @@ ls -lh "$(./getvar linux_image)" before and after using initramfs, since the `.cpio` is now glued to the kernel image. -In the background, it uses `BR2_TARGET_ROOTFS_INITRAMFS`, and this makes the kernel config option `CONFIG_INITRAMFS_SOURCE` point to the CPIO that will be embedded in the kernel image. +Don't forget that to stop using initramfs, you must rebuild the kernel without `--initramfs` to get rid of the attached CPIO image: + +.... +./build-linux +./run +.... + +Alternatively, consider using <> if you need to switch between initramfs and non initramfs often: + +.... +./build-buildroot --initramfs +./build-linux --initramfs --linux-build-id initramfs +./run --initramfs --linux-build-id +.... + +Setting up initramfs is very easy: our scripts just set `CONFIG_INITRAMFS_SOURCE` to point to the CPIO path. http://nairobi-embedded.org/initramfs_tutorial.html shows a full manual setup. @@ -2648,6 +2731,30 @@ http://nairobi-embedded.org/initramfs_tutorial.html shows a full manual setup. TODO we were not able to get it working yet: https://stackoverflow.com/questions/49261801/how-to-boot-the-linux-kernel-with-initrd-or-initramfs-with-gem5 +This would require gem5 to load the CPIO into memory, just like QEMU. Grepping `initrd` shows some ARM hits under: + +.... +src/arch/arm/linux/atag.hh +.... + +but they are commented out. + +=== gem5 initramfs + +This could in theory be easier to make work than initrd since the emulator does not have to do anything special. + +However, it didn't: boot fails at the end because it does not see the initramfs, but rather tries to open our dummy root filesystem, which unsurprisingly does not have a format in a way that the kernel understands: + +.... +VFS: Cannot open root device "sda" or unknown-block(8,0): error -5 +.... + +We think that this might be because gem5 boots directly `vmlinux`, and not from the final compressed images that contain the attached rootfs such as `bzImage`, which is what QEMU does, see also: <>. + +To do this failed test, we automatically pass a dummy disk image as of gem5 7fa4c946386e7207ad5859e8ade0bbfc14000d91 since the scripts don't handle a missing `--disk-image` well, much like is currently done for <>. + +Interestingly, using initramfs significantly slows down the gem5 boot, even though it did not work. For example, we've observed a 4x slowdown of as 17062a2e8b6e7888a14c3506e9415989362c58bf for aarch64. This must be because expanding the large attached CPIO must be expensive. We can clearly see from the kernel logs that the kernel just hangs at a point after the message `PCI: CLS 0 bytes, default 64` for a long time before proceeding further. + == Device tree The device tree is a Linux kernel defined data structure that serves to inform the kernel how the hardware is setup. @@ -2808,9 +2915,17 @@ QEMU automatically adds a second CPU to the DTB! The action seems to be happening at: `hw/arm/virt.c`. +You can dump the DTB QEMU generated with: + +.... +./run --arch aarch64 -- -machine dumpdtb=dtb.dtb +.... + +as mentioned at: https://lists.gnu.org/archive/html/qemu-discuss/2017-02/msg00051.html + <> 2a9573f5942b5416fb0570cf5cb6cdecba733392 can also generate its own DTB. -gem5 can generate DTBs on ARM with `--generate-dtb`, but we don't use that feature as of f8c0502bb2680f2dbe7c1f3d7958f60265347005 because it was buggy. +gem5 can generate DTBs on ARM with `--generate-dtb`. The generated DTB is placed in the <> named as `system.dtb`. == KVM @@ -2900,18 +3015,24 @@ The target Linux kernel of the executable is a GCC toolchain build-time configur First let's run a dynamically linked executable built with the Buildroot toolchain: .... -./build-qemu --arch aarch64 --userland -./build-userland --arch aarch64 -./build-buildroot --arch aarch64 +./build --arch aarch64 user-mode-qemu ./run \ --arch aarch64 \ --userland print_argv \ - -- \ - asdf qwer \ + --userland-args 'asdf "qw er"' \ ; .... -This runs link:userland/print_argv.c[]. `--userland` path resolution is analogous to <>. +Output: + +.... +asdf +qw er +.... + +This runs link:userland/print_argv.c[]. + +`./run --userland` path resolution is analogous to <>. `./build-userland` is further documented at: <>. @@ -2924,15 +3045,14 @@ You can also try statically linked executables with: .... ./build-userland \ --arch aarch64 \ - --make-args='CCFLAGS_EXTRA=-static' \ + --static \ --userland-build-id static \ ; ./run \ --arch aarch64 \ --userland-build-id static \ --userland print_argv \ - -- \ - asdf qwer \ + --userland-args 'asdf "qw er"' \ ; .... @@ -2942,21 +3062,20 @@ Or you can run statically linked built by the host packaged toolchain with: ./build-userland \ --arch aarch64 \ --host \ - --make-args='-B CFLAGS_EXTRA=-static' \ + --static \ --userland-build-id host-static \ ; ./run \ --arch aarch64 \ --userland-build-id host-static \ --userland print_argv \ - -- \ - asdf qwer \ + --userland-args 'asdf "qw er"' \ ; .... TODO expose dynamically linked executables built by the host toolchain. It also works, we just have to use e.g. `-L /usr/aarch64-linux-gnu`, so it's not really hard, I'm just lazy. -==== QEMU user mode GDB +==== User mode GDB It's nice when <> just works, right? @@ -2964,9 +3083,8 @@ It's nice when <> just works, right? ./run \ --arch aarch64 \ --userland print_argv \ + --userland-args 'asdf "qw er"' \ --wait-gdb \ - -- \ - asdf qwer \ ; .... @@ -2985,11 +3103,10 @@ Or alternatively, if you are using <>, do everything in one go with: .... ./run \ --arch aarch64 \ + --tmux-args main \ --userland print_argv \ - --tmux=main \ + --userland-args 'asdf "qw er"' \ --wait-gdb \ - -- \ - asdf qwer \ ; .... @@ -3020,42 +3137,65 @@ So let's just play with some static ones: .... ./build-userland \ --arch aarch64 \ + --static \ --userland-build-id static \ - --make-args='CCFLAGS_EXTRA=-static' \ ; ./run \ --arch aarch64 \ - --gem5 \ + --emulator gem5 \ --userland print_argv \ - --userland-build-id static \ - -- \ - --options 'asdf "qw er"' \ + --userland-args 'asdf "qw er"' \ ; .... -TODO: how to escape spaces? +TODO: how to escape spaces on the command line arguments? Step debug also works: .... ./run \ - --arch arm \ - --wait-gdb \ - --gem5 \ + --arch aarch64 \ + --emulator gem5 \ --userland print_argv \ + --userland-args 'asdf "qw er"' \ --userland-build-id static \ - -- \ - --options 'asdf "qw er"' \ + --wait-gdb \ ; ./run-gdb \ - --arch arm \ - --gem5 \ + --arch aarch64 \ + --emulator gem5 \ --userland print_argv \ --userland-build-id static \ main \ ; .... +==== gem5 syscall emulation exit status + +As of gem5 7fa4c946386e7207ad5859e8ade0bbfc14000d91, the crappy `se.py` script does not forward the exit status of syscall emulation mode, you can test it with: + +.... +./run --dry-run --emulator gem5 --userland false --userland-build-id static +.... + +Source: link:userland/false[]. + +Then manually run the generated gem5 CLI, and do: + +.... +echo $? +.... + +and the output is always `0`. + +Instead, it just outputs a message to stdout just like for <>: + +.... +Simulated exit code not 0! Exit code is 1 +.... + +which we parse in link:run[] and then exit with the correct result ourselves... + ==== User mode vs full system benchmark Let's see if user mode runs considerably faster than full system or not. @@ -3073,11 +3213,9 @@ make \ time \ ./run \ --arch arm \ - --gem5 \ - --userland \ - "$(./getvar --arch arm buildroot_build_build_dir)/dhrystone-2/dhrystone" \ - -- \ - --options 100000 \ + --emulator gem5 \ + --userland "$(./getvar --arch arm buildroot_build_build_dir)/dhrystone-2/dhrystone" \ + --userland-args 'asdf qwer' \ ; .... @@ -3088,7 +3226,7 @@ time \ ./run \ --arch arm \ --eval-after '/gem5.sh' \ - --gem5 + --emulator gem5 --gem5-readfile 'dhrystone 100000' \ ; .... @@ -3116,6 +3254,30 @@ Result on <> at bad30f513c46c1b0995d3a10c0d9bc2a33dc4fa0: * QEMU user: 45 seconds * QEMU full system: 223 seconds +=== User mode tests + +Automatically run non-interactive userland tests that can be run in user mode simulation: + +.... +./build --all-archs test-user-mode +./test-user-mode --all-archs --all-emulators +.... + +Or just for QEMU: + +.... +./build --all-archs test-user-mode-qemu +./test-user-mode --all-archs --emulator qemu +.... + +Source: link:test-user-mode[] + +This testing excludes notably kernel module tests which depend on a full running kernel. + +The gem5 tests require building statically with build id `static`, see also: <>. TODO automate this better. + +See: <> for more useful testing tips. + == Kernel module utilities === insmod @@ -3254,7 +3416,7 @@ This would have several advantages: ** no need to regenerate the root filesystem at all and reboot ** overcomes the `check_bin_arch` problem: <> * we could keep the base root filesystem very small, which implies: -** less host disk usage, no need to copy the entire `out_rootfs_overlay_dir` to the image again +** less host disk usage, no need to copy the entire `./getvar out_rootfs_overlay_dir` to the image again ** no need to worry about <> We can already make host files appear on the guest with <<9p>>, but they appear on a subdirectory instead of the root. @@ -3496,7 +3658,7 @@ More concretely, first build the kernel with the <> and then run with: .... -./run --arch aarch64 --dp650 --gem5 --linux-build-id gem5-v4.15 +./run --arch aarch64 --dp650 --emulator gem5 --linux-build-id gem5-v4.15 .... ==== Graphic mode gem5 internals @@ -4106,6 +4268,12 @@ That file contains `BR2_LINUX_KERNEL_CUSTOM_CONFIG_FILE="board/qemu/x86_64/linux `arm`, on the other hand, uses link:https://github.com/buildroot/buildroot/blob/2018.05/configs/qemu_arm_vexpress_defconfig[`buildroot/configs/qemu_arm_vexpress_defconfig`], which contains `BR2_LINUX_KERNEL_DEFCONFIG="vexpress"`, and therefore just does a `make vexpress_defconfig`, and gets its config from the Linux kernel tree itself. +====== Linux kernel defconfigs + +It would be interesting to test out if `make defconfig` configs boot and work on QEMU + Buildroot: https://unix.stackexchange.com/questions/29439/compiling-the-kernel-with-default-configurations/204512#204512 + +TODO. + ===== Notable alternate gem5 kernel configs Other configs which we had previously tested at 4e0d9af81fcce2ce4e777cb82a1990d7c2ca7c1e are: @@ -5074,7 +5242,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-args dump_stack .... shows that traces are printed at `arch/x86/kernel/dumpstack.c`: @@ -5294,10 +5462,10 @@ Where the data comes from and how to modify it: In this repo, leaking host information, and to make builds more reproducible, we are setting: -- user and date to dummy values -- hostname to the kernel git commit +- user and date to dummy values with `KBUILD_BUILD_USER` and `KBUILD_BUILD_TIMESTAMP` +- hostname to the kernel git commit with `KBUILD_BUILD_HOST` and `KBUILD_BUILD_VERSION` -So the file contains something like: +A sample result is: .... Linux version 4.19.0-dirty (lkmc@84df9525b0c27f3ebc2ebb1864fa62a97fdedb7d) (gcc version 6.4.0 (Buildroot 2018.05-00002-gbc60382b8f)) #1 SMP Thu Jan 1 00:00:00 UTC 1970 @@ -6620,6 +6788,8 @@ Bibliography: ==== Count boot instructions +TODO: didn't port during refactor after 3b0a343647bed577586989fb702b760bd280844a. Reimplementing should not be hard. + * https://www.quora.com/How-many-instructions-does-a-typical-Linux-kernel-boot-take * https://github.com/cirosantilli/chat/issues/31 * https://rwmj.wordpress.com/2016/03/17/tracing-qemu-guest-execution/ @@ -6676,9 +6846,9 @@ instructions_firmware 20708 gem5: .... -./run --arch aarch64 --gem5 --eval 'm5 exit' +./run --arch aarch64 --emulator gem5 --eval 'm5 exit' # Or: -# ./run --arch aarch64 --gem5 --eval 'm5 exit' -- --cpu-type=HPI --caches +# ./run --arch aarch64 --emulator gem5 --eval 'm5 exit' -- --cpu-type=HPI --caches ./gem5-stat --arch aarch64 sim_insts .... @@ -7375,7 +7545,7 @@ Looks like a more raw alternative to libdrm: .... ./build-buildroot --config 'BR2_PACKABE_LIBDRI2=y' wget \ - -O "$(./getvar userland_src_dir)/dri2test.c" \ + -O "$(./getvar userland_source_dir)/dri2test.c" \ https://raw.githubusercontent.com/robclark/libdri2/master/test/dri2test.c \ ; ./build-userland @@ -8206,7 +8376,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: @@ -8224,7 +8394,7 @@ You can still send key presses to QEMU however even without the mouse capture, j Start pdb at the first instruction: .... -./run --gem5 --gem5-exe-args='--pdb' --terminal +./run --emulator gem5 --gem5-exe-args='--pdb' --terminal .... Requires `--terminal` as we must be on foreground. @@ -8238,7 +8408,7 @@ import ipdb; ipdb.set_trace() and then run with: .... -./run --gem5 --terminal +./run --emulator gem5 --terminal .... TODO test PyCharm: https://stackoverflow.com/questions/51982735/writing-gem5-configuration-scripts-with-pycharm @@ -8508,14 +8678,14 @@ just appears to output both cores intertwined without any clear differentiation. gem5 provides also provides a tracing mechanism documented at: link:http://www.gem5.org/Trace_Based_Debugging[]: .... -./run --arch aarch64 --eval 'm5 exit' --gem5 --trace Exec +./run --arch aarch64 --eval 'm5 exit' --emulator gem5 --trace Exec less "$(./getvar --arch aarch64 run_dir)/trace.txt" .... Output the trace to stdout instead of a file: .... -./run --arch aarch64 --eval 'm5 exit' --gem5 --trace Exec --trace-stdout +./run --arch aarch64 --eval 'm5 exit' --emulator gem5 --trace Exec --trace-stdout .... This would produce a lot of output however, so you will likely not want that when tracing a Linux kernel boot instructions. But it can be very convenient for smaller traces. @@ -8523,14 +8693,14 @@ This would produce a lot of output however, so you will likely not want that whe List all available debug flags: .... -./run --arch aarch64 --gem5-exe-args='--debug-help' --gem5 +./run --arch aarch64 --gem5-exe-args='--debug-help' --emulator gem5 .... but to understand most of them you have to look at the source code: .... -less "$(./getvar gem5_src_dir)/src/cpu/SConscript" -less "$(./getvar gem5_src_dir)/src/cpu/exetrace.cc" +less "$(./getvar gem5_source_dir)/src/cpu/SConscript" +less "$(./getvar gem5_source_dir)/src/cpu/exetrace.cc" .... The traces are generated from `DPRINTF(` calls scattered throughout the code. @@ -8576,8 +8746,8 @@ The best way to verify all of this is to write some <> Trace the source lines just like <> with: .... -./trace-boot --arch aarch64 --gem5 -./trace2line --arch aarch64 --gem5 +./trace-boot --arch aarch64 --emulator gem5 +./trace2line --arch aarch64 --emulator gem5 less "$(./getvar --arch aarch64 run_dir)/trace-lines.txt" .... @@ -8686,7 +8856,7 @@ Another interesting example can be found at: link:gem5-bench-cache[]. A more naive and simpler to understand approach would be a direct: .... -./run --arch aarch64 --gem5 --eval 'm5 checkpoint;m5 resetstats;dhrystone 10000;m5 exit' +./run --arch aarch64 --emulator gem5 --eval 'm5 checkpoint;m5 resetstats;dhrystone 10000;m5 exit' .... but the problem is that this method does not allow to easily run a different script without running the boot again, see: <>. @@ -8724,7 +8894,7 @@ The rabbit hole is likely deep, but let's scratch a bit of the surface. ===== Number of cores .... -./run --arch arm --cpus 2 --gem5 +./run --arch arm --cpus 2 --emulator gem5 .... Check with: @@ -8744,7 +8914,7 @@ Build the kernel with the <>, and then run: ./run \ --arch aarch64 \ --linux-build-id gem5-v4.15 \ - --gem5 \ + --emulator gem5 \ --cpus 16 \ -- \ --param 'system.realview.gic.gem5_extensions = True' \ @@ -8755,7 +8925,7 @@ Build the kernel with the <>, and then run: https://stackoverflow.com/questions/49624061/how-to-run-gem5-simulator-in-fs-mode-without-cache/49634544#49634544 -A quick `+./run --gem5 -- -h+` leads us to the options: +A quick `+./run --emulator gem5 -- -h+` leads us to the options: .... --caches @@ -8822,37 +8992,37 @@ cat "$(./getvar --arch aarch64 run_dir)/bench-cache.txt" which gives: .... -cmd ./run --gem5 --arch aarch64 --gem5-readfile "dhrystone 1000" --gem5-restore 1 -- --caches --l2cache --l1d_size=1024 --l1i_size=1024 --l2_size=1024 --l3_size=1024 --cpu-type=HPI --restore-with-cpu=HPI +cmd ./run --emulator gem5 --arch aarch64 --gem5-readfile "dhrystone 1000" --gem5-restore 1 -- --caches --l2cache --l1d_size=1024 --l1i_size=1024 --l2_size=1024 --l3_size=1024 --cpu-type=HPI --restore-with-cpu=HPI time 23.82 exit_status 0 cycles 93284622 instructions 4393457 -cmd ./run --gem5 --arch aarch64 --gem5-readfile "dhrystone 1000" --gem5-restore 1 -- --caches --l2cache --l1d_size=1024kB --l1i_size=1024kB --l2_size=1024kB --l3_size=1024kB --cpu-type=HPI --restore-with-cpu=HPI +cmd ./run --emulator gem5 --arch aarch64 --gem5-readfile "dhrystone 1000" --gem5-restore 1 -- --caches --l2cache --l1d_size=1024kB --l1i_size=1024kB --l2_size=1024kB --l3_size=1024kB --cpu-type=HPI --restore-with-cpu=HPI time 14.91 exit_status 0 cycles 10128985 instructions 4211458 -cmd ./run --gem5 --arch aarch64 --gem5-readfile "dhrystone 10000" --gem5-restore 1 -- --caches --l2cache --l1d_size=1024 --l1i_size=1024 --l2_size=1024 --l3_size=1024 --cpu-type=HPI --restore-with-cpu=HPI +cmd ./run --emulator gem5 --arch aarch64 --gem5-readfile "dhrystone 10000" --gem5-restore 1 -- --caches --l2cache --l1d_size=1024 --l1i_size=1024 --l2_size=1024 --l3_size=1024 --cpu-type=HPI --restore-with-cpu=HPI time 51.87 exit_status 0 cycles 188803630 instructions 12401336 -cmd ./run --gem5 --arch aarch64 --gem5-readfile "dhrystone 10000" --gem5-restore 1 -- --caches --l2cache --l1d_size=1024kB --l1i_size=1024kB --l2_size=1024kB --l3_size=1024kB --cpu-type=HPI --restore-with-cpu=HPI +cmd ./run --emulator gem5 --arch aarch64 --gem5-readfile "dhrystone 10000" --gem5-restore 1 -- --caches --l2cache --l1d_size=1024kB --l1i_size=1024kB --l2_size=1024kB --l3_size=1024kB --cpu-type=HPI --restore-with-cpu=HPI time 35.35 exit_status 0 cycles 20715757 instructions 12192527 -cmd ./run --gem5 --arch aarch64 --gem5-readfile "dhrystone 100000" --gem5-restore 1 -- --caches --l2cache --l1d_size=1024 --l1i_size=1024 --l2_size=1024 --l3_size=1024 --cpu-type=HPI --restore-with-cpu=HPI +cmd ./run --emulator gem5 --arch aarch64 --gem5-readfile "dhrystone 100000" --gem5-restore 1 -- --caches --l2cache --l1d_size=1024 --l1i_size=1024 --l2_size=1024 --l3_size=1024 --cpu-type=HPI --restore-with-cpu=HPI time 339.07 exit_status 0 cycles 1176559936 instructions 94222791 -cmd ./run --gem5 --arch aarch64 --gem5-readfile "dhrystone 100000" --gem5-restore 1 -- --caches --l2cache --l1d_size=1024kB --l1i_size=1024kB --l2_size=1024kB --l3_size=1024kB --cpu-type=HPI --restore-with-cpu=HPI +cmd ./run --emulator gem5 --arch aarch64 --gem5-readfile "dhrystone 100000" --gem5-restore 1 -- --caches --l2cache --l1d_size=1024kB --l1i_size=1024kB --l2_size=1024kB --l3_size=1024kB --cpu-type=HPI --restore-with-cpu=HPI time 240.37 exit_status 0 cycles 125666679 @@ -8906,7 +9076,7 @@ and also: `gem5-dist`: https://publish.illinois.edu/icsl-pdgem5/ Clock frequency: TODO how does it affect performance in benchmarks? .... -./run --arch aarch64 --gem5 -- --cpu-clock 10000000 +./run --arch aarch64 --emulator gem5 -- --cpu-clock 10000000 .... Check with: @@ -8954,10 +9124,10 @@ Usage: ./run \ --arch aarch64 \ --eval-after '/gem5.sh' \ - --gem5 \ + --emulator gem5 \ --gem5-readfile '/bst_vs_heap.out' \ ; -./bst-vs-heap --arch aarch64 --gem5 > bst_vs_heap.dat +./bst-vs-heap --arch aarch64 --emulator gem5 > bst_vs_heap.dat .... and then feed `bst_vs_heap.dat` into: https://github.com/cirosantilli/cpp-cheat/blob/9d0f77792fc8e55b20b6ee32018761ef3c5a3f2f/cpp/interactive/bst_vs_heap.gnuplot @@ -9059,7 +9229,7 @@ There are two ways to run PARSEC with this repo: .... ./build --arch arm --download-dependencies gem5-buildroot parsec-benchmark ./build-buildroot --arch arm --config 'BR2_PACKAGE_PARSEC_BENCHMARK=y' -./run --arch arm --gem5 +./run --arch arm --emulator gem5 .... Once inside the guest, launch one of the `test` input sized benchmarks manually as in: @@ -9188,7 +9358,7 @@ A few workarounds are: If you do this, don't forget to do a: + .... -cd "$(./getvar parsec_src_dir)" +cd "$(./getvar parsec_source_dir)" git clean -xdf . .... before going for the cross compile build. @@ -9213,7 +9383,7 @@ You may also want to test if your patches are still functionally correct inside Analogous <>: .... -./run --arch arm --kernel-cli 'init=/poweroff.out' --gem5 +./run --arch arm --kernel-cli 'init=/poweroff.out' --emulator gem5 .... Internals: when we give `--command-line=` to gem5, it overrides default command lines, including some mandatory ones which are required to boot properly. @@ -9223,7 +9393,7 @@ Our run script hardcodes the require options in the default `--command-line` and To find the default options in the first place, we removed `--command-line` and ran: .... -./run --arch arm --gem5 +./run --arch arm --emulator gem5 .... and then looked at the line of the Linux kernel that starts with: @@ -9239,13 +9409,13 @@ Kernel command line: Analogous <>, on the first shell: .... -./run --arch arm --wait-gdb --gem5 +./run --arch arm --wait-gdb --emulator gem5 .... On the second shell: .... -./run-gdb --arch arm --gem5 +./run-gdb --arch arm --emulator gem5 .... On a third shell: @@ -9258,44 +9428,12 @@ When you want to break, just do a `Ctrl-C` on GDB shell, and then `continue`. And we now see the boot messages, and then get a shell. Now try the `/count.sh` procedure described for QEMU: <>. -===== gem5 GDB step debug kernel aarch64 - -TODO: GDB fails with: - -.... -Reading symbols from vmlinux...done. -Remote debugging using localhost:7000 -Remote 'g' packet reply is too long: 000000000000000090a4f90fc0ffffff4875450ec0ffffff01000000000000000100000000000000000000000000000001000000000000000000000000000000ffffffffffffffff646d60616b64fffe7f7f7f7f7f7f7f7f0101010101010101300000000000000000000000ffffffff48454422207d2c2017162f21262820160100000000000000070000000000000001000000000000004075450ec0ffffffc073450ec0ffffff82080000000000004075450ec0ffffff8060f90fc0ffffffc073450ec0fffffff040900880ffffff40ab400ec0ffffff586d900880ffffff0068a20ec0ffffff903b010880ffffffc8ff210880ffffff903b010880ffffffccff210880ffffff050000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -.... - -See: <> - -and gem5 says: - -.... -4107766500: system.remote_gdb: remote gdb attached -warn: Couldn't read data from debugger. -4107767500: system.remote_gdb: remote gdb detached -.... - -I've also tried the fix at: https://stackoverflow.com/questions/27411621/remote-g-packet-reply-is-too-long-aarch64-arm64 by adding to the link:run-gdb[] script: - -.... --ex 'set tdesc filename out/aarch64/buildroot/build/gdb-7.11.1/./gdb/features/aarch64.xml' -.... - -but it did not help. - -https://www.mail-archive.com/gem5-users@gem5.org/msg15383.html - ==== gem5 GDB step debug userland process We are unable to use `gdbserver` because of networking: <> The alternative is to do as in <>. -First make sure that for your arch the kernel debugging on the given target works for the architecture: <>, on which we rely. When we last tested, this was not the case for aarch64: <> - Next, follow the exact same steps explained at <>, but passing `-g` to every command as usual. But then TODO (I'll still go crazy one of those days): for `arm`, while debugging `/myinsmod.out /hello.ko`, after then line: @@ -9310,7 +9448,7 @@ I press `n`, it just runs the program until the end, instead of stopping on the TODO: .... -./run-gdb-user --arch arm --gem5 gem5-1.0/gem5/util/m5/m5 main +./run-gdb-user --arch arm --emulator gem5 gem5-1.0/gem5/util/m5/m5 main .... breaks when `m5` is run on guest, but does not show the source code. @@ -9322,7 +9460,7 @@ Analogous to QEMU's <>, but better since it can be started from inside Documentation: http://gem5.org/Checkpoints .... -./run --arch arm --gem5 +./run --arch arm --emulator gem5 .... In the guest, wait for the boot to end and run: @@ -9336,7 +9474,7 @@ where <> is a guest utility present inside the gem5 tree which we cross-comp To restore the checkpoint, kill the VM and run: .... -./run --arch arm --gem5 --gem5-restore 1 +./run --arch arm --emulator gem5 --gem5-restore 1 .... The `--gem5-restore` option restores the checkpoint that was created most recently. @@ -9351,7 +9489,7 @@ m5 checkpoint Kill the VM, and try it out: .... -./run --arch arm --gem5 --gem5-restore 1 +./run --arch arm --emulator gem5 --gem5-restore 1 .... Here we use `--gem5-restore 1` again, since the second snapshot we took is now the most recent one @@ -9367,13 +9505,13 @@ contains the `date`. The file `f` wouldn't exist had we used the first checkpoin If you automate things with <> as in: .... -./run --arch arm --eval 'm5 checkpoint;m5 resetstats;dhrystone 1000;m5 exit' --gem5 +./run --arch arm --eval 'm5 checkpoint;m5 resetstats;dhrystone 1000;m5 exit' --emulator gem5 .... Then there is no need to pass the kernel command line again to gem5 for replay: .... -./run --arch arm --gem5 --gem5-restore 1 +./run --arch arm --emulator gem5 --gem5-restore 1 .... since boot has already happened, and the parameters are already in the RAM of the snapshot. @@ -9383,7 +9521,7 @@ since boot has already happened, and the parameters are already in the RAM of th Checkpoints are stored inside the <> at: .... -"$(./getvar --gem5 m5out_dir)/cpt." +"$(./getvar --emulator gem5 m5out_dir)/cpt." .... where `` is the cycle number at which the checkpoint was taken. @@ -9413,19 +9551,19 @@ So we can do it like: .... # Boot, checkpoint and exit. printf 'echo "setup run";m5 exit' > "$(./getvar gem5_readfile)" -./run --gem5 --eval 'm5 checkpoint;m5 readfile > a.sh;sh a.sh' +./run --emulator gem5 --eval 'm5 checkpoint;m5 readfile > a.sh;sh a.sh' # Restore and run the first benchmark. printf 'echo "first benchmark";m5 exit' > "$(./getvar gem5_readfile)" -./run --gem5 --gem5-restore 1 +./run --emulator gem5 --gem5-restore 1 # Restore and run the second benchmark. printf 'echo "second benchmark";m5 exit' > "$(./getvar gem5_readfile)" -./run --gem5 --gem5-restore 1 +./run --emulator gem5 --gem5-restore 1 # If something weird happened, create an interactive shell to examine the system. printf 'sh' > "$(./getvar gem5_readfile)" -./run --gem5 --gem5-restore 1 +./run --emulator gem5 --gem5-restore 1 .... Since this is such a common setup, we provide some helpers for it as described at <>: @@ -9462,7 +9600,7 @@ A common combo is to boot Linux with a fast CPU, make a checkpoint and then repl An illustrative interactive run: .... -./run --arch arm --gem5 +./run --arch arm --emulator gem5 .... In guest: @@ -9474,7 +9612,7 @@ m5 checkpoint And then restore the checkpoint with a different CPU: .... -./run --arch arm --gem5 --gem5-restore 1 -- --caches --restore-with-cpu=HPI +./run --arch arm --emulator gem5 --gem5-restore 1 -- --caches --restore-with-cpu=HPI .... === Pass extra options to gem5 @@ -9484,12 +9622,12 @@ Pass options to the `fs.py` script: * get help: + .... -./run --gem5 -- -h +./run --emulator gem5 -- -h .... * boot with the more detailed and slow `HPI` CPU model: + .... -./run --arch arm --gem5 -- --caches --cpu-type=HPI +./run --arch arm --emulator gem5 -- --caches --cpu-type=HPI .... Pass options to the `gem5` executable itself: @@ -9497,7 +9635,7 @@ Pass options to the `gem5` executable itself: * get help: + .... -./run --gem5-exe-args='-h' --gem5 +./run --gem5-exe-args='-h' --emulator gem5 .... === gem5 exit after a number of instructions @@ -9505,7 +9643,7 @@ Pass options to the `gem5` executable itself: Quit the simulation after `1024` instructions: .... -./run --gem5 -- -I 1024 +./run --emulator gem5 -- -I 1024 .... Can be nicely checked with <>. @@ -9513,7 +9651,7 @@ Can be nicely checked with <>. Cycles instead of instructions: .... -./run --gem5 -- --memory 1024 +./run --emulator gem5 -- --memory 1024 .... Otherwise the simulation runs forever by default. @@ -9568,7 +9706,9 @@ Simulated exit code not 0! Exit code is 1 and exits with status 0. -TODO: it used to exit non 0, be like that, but it actually got changed to just print the message. Why? https://gem5-review.googlesource.com/c/public/gem5/+/4880 +We then parse that string ourselves in link:run[] and exit with the correct status... + +TODO: it used to be like that, but it actually got changed to just print the message. Why? https://gem5-review.googlesource.com/c/public/gem5/+/4880 `m5 fail` is just a superset of `m5 exit`, which is just: @@ -9592,7 +9732,7 @@ m5 writefile myfileguest myfilehost Host: .... -cat "$(./getvar --arch aarch64 --gem5 m5out_dir)/myfilehost" +cat "$(./getvar --arch aarch64 --emulator gem5 m5out_dir)/myfilehost" .... Does not work for subdirectories, gem5 crashes: @@ -9628,8 +9768,8 @@ Ermm, just another <> that only takes integers and only from CLI op Host: .... -./run --gem5 --gem5-restore 1 -- --initparam 13 -./run --gem5 --gem5-restore 1 -- --initparam 42 +./run --emulator gem5 --gem5-restore 1 -- --initparam 13 +./run --emulator gem5 --gem5-restore 1 -- --initparam 42 .... Guest: @@ -9804,7 +9944,7 @@ It is obviously not possible to understand what they actually do from their comm When you run gem5, it generates an `m5out` directory at: .... -echo $(./getvar --arch arm --gem5 m5out_dir)" +echo $(./getvar --arch arm --emulator gem5 m5out_dir)" .... The location of that directory can be set with `./gem5.opt -d`, and defaults to `./m5out`. @@ -9847,7 +9987,7 @@ Let's have some fun and try to correlate the gem5 cycle count `system.cpu.numCyc .... ./build-userland -- rdtsc -./run --eval '/rdtsc.out;m5 exit;' --gem5 +./run --eval '/rdtsc.out;m5 exit;' --emulator gem5 ./gem5-stat .... @@ -9880,7 +10020,7 @@ TODO We didn't manage to find a working ARM analogue to <>: link:kernel_m The `config.ini` file, contains a very good high level description of the system: .... -less $(./getvar --arch arm --gem5 m5out_dir)" +less $(./getvar --arch arm --emulator gem5 m5out_dir)" .... That file contains a tree representation of the system, sample excerpt: @@ -9918,7 +10058,7 @@ For example, `AtomicSimpleCPU` maps is defined at link:https://github.com/gem5/g You can also get a simplified graphical view of the tree with: .... -xdg-open "$(./getvar --arch arm --gem5 m5out_dir)/config.dot.pdf" +xdg-open "$(./getvar --arch arm --emulator gem5 m5out_dir)/config.dot.pdf" .... Modifying the `config.ini` file manually does nothing since it gets overwritten every time. @@ -9971,13 +10111,13 @@ The `--gem5-script biglittle` option enables the alternative `configs/example/ar First apply: .... -patch -d "$(./getvar gem5_src_dir)" -p 1 < patches/manual/gem5-biglittle.patch +patch -d "$(./getvar gem5_source_dir)" -p 1 < patches/manual/gem5-biglittle.patch .... then: .... -./run --arch aarch64 --gem5 --gem5-script biglittle +./run --arch aarch64 --emulator gem5 --gem5-script biglittle .... Advantages over `fs.py`: @@ -10191,6 +10331,8 @@ Once you've built a package in to the image, there is no easy way to remove it. Documented at: link:https://github.com/buildroot/buildroot/blob/2017.08/docs/manual/rebuilding-packages.txt#L90[] +Also mentioned at: https://stackoverflow.com/questions/47320800/how-to-clean-only-target-in-buildroot + See this for a sample manual workaround: <>. === BR2_TARGET_ROOTFS_EXT2_SIZE @@ -10327,13 +10469,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-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-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[]. @@ -10341,13 +10483,11 @@ 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-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. -`aarch64` gem5 GDB step debug is broken as mentioned at: <>. - === Baremetal bootloaders As can be seen from <>, all examples under link:baremetal/[], with the exception of `baremetal/arch//no_bootloader`, start from our tiny bootloaders: @@ -10369,7 +10509,7 @@ The most important things that we setup in the bootloaders are: The C functions that become available as a result are: * Newlib functions implemented at link:baremetal/lib/syscalls.c[] -* non-Newlib functions implemented at link:common.c[] +* non-Newlib functions implemented at link:kwargs['c'][] It is not possible to call those C functions from the examples that don't use a bootloader. @@ -10401,7 +10541,7 @@ svc 0x00123456 and we can see from the docs that `0x18` stands for the `SYS_EXIT` command. -This is also how we implement the `exit(0)` system call in C for QEMU for link:baremetal/exit.c[] through the Newlib via the function `_exit` at link:baremetal/lib/common.c[]. +This is also how we implement the `exit(0)` system call in C for QEMU for link:baremetal/exit.c[] through the Newlib via the function `_exit` at link:baremetal/lib/kwargs['c'][]. Other magic operations we can do with semihosting besides exiting the on the host include: @@ -10441,7 +10581,7 @@ Bibliography: For gem5, you need: .... -patch -d "$(./getvar gem5_src_dir)" -p 1 < patches/manual/gem5-semihost.patch +patch -d "$(./getvar gem5_source_dir)" -p 1 < patches/manual/gem5-semihost.patch .... https://stackoverflow.com/questions/52475268/how-to-enable-arm-semihosting-in-gem5/52475269#52475269 @@ -10607,18 +10747,18 @@ TODO: why is `arm` stuck at `19` which equals Supervisor mode? In gem5, you can configure the lowest EL with: .... -./run --arch arm --baremetal arch/arm/el --gem5 -cat "$(./getvar --arch arm --gem5 gem5_guest_terminal_file)" -./run --arch arm --baremetal arch/arm/el --gem5 -- --param 'system.have_virtualization = True' -cat "$(./getvar --arch arm --gem5 gem5_guest_terminal_file)" -./run --arch arm --baremetal arch/arm/el --gem5 -- --param 'system.have_security = True' -cat "$(./getvar --arch arm --gem5 gem5_guest_terminal_file)" -./run --arch aarch64 --baremetal arch/aarch64/el --gem5 -cat "$(./getvar --arch aarch64 --gem5 gem5_guest_terminal_file)" -./run --arch aarch64 --baremetal arch/aarch64/el --gem5 -- --param 'system.have_virtualization = True' -cat "$(./getvar --arch aarch64 --gem5 gem5_guest_terminal_file)" -./run --arch aarch64 --baremetal arch/aarch64/el --gem5 -- --param 'system.have_security = True' -cat "$(./getvar --arch aarch64 --gem5 gem5_guest_terminal_file)" +./run --arch arm --baremetal arch/arm/el --emulator gem5 +cat "$(./getvar --arch arm --emulator gem5 gem5_guest_terminal_file)" +./run --arch arm --baremetal arch/arm/el --emulator gem5 -- --param 'system.have_virtualization = True' +cat "$(./getvar --arch arm --emulator gem5 gem5_guest_terminal_file)" +./run --arch arm --baremetal arch/arm/el --emulator gem5 -- --param 'system.have_security = True' +cat "$(./getvar --arch arm --emulator gem5 gem5_guest_terminal_file)" +./run --arch aarch64 --baremetal arch/aarch64/el --emulator gem5 +cat "$(./getvar --arch aarch64 --emulator gem5 gem5_guest_terminal_file)" +./run --arch aarch64 --baremetal arch/aarch64/el --emulator gem5 -- --param 'system.have_virtualization = True' +cat "$(./getvar --arch aarch64 --emulator gem5 gem5_guest_terminal_file)" +./run --arch aarch64 --baremetal arch/aarch64/el --emulator gem5 -- --param 'system.have_security = True' +cat "$(./getvar --arch aarch64 --emulator gem5 gem5_guest_terminal_file)" .... output: @@ -10636,9 +10776,9 @@ output: .... ./run --arch aarch64 --baremetal arch/aarch64/multicore --cpus 2 -./run --arch aarch64 --baremetal arch/aarch64/multicore --cpus 2 --gem5 +./run --arch aarch64 --baremetal arch/aarch64/multicore --cpus 2 --emulator gem5 ./run --arch arm --baremetal arch/aarch64/multicore --cpus 2 -./run --arch arm --baremetal arch/aarch64/multicore --cpus 2 --gem5 +./run --arch arm --baremetal arch/aarch64/multicore --cpus 2 --emulator gem5 .... Sources: @@ -10661,7 +10801,7 @@ and watch it hang forever. Note that if you try the same thing on gem5: .... -./run --arch aarch64 --baremetal arch/aarch64/multicore --cpus 1 --gem5 +./run --arch aarch64 --baremetal arch/aarch64/multicore --cpus 1 --emulator gem5 .... then the gem5 actually exits, but with a different message: @@ -10770,6 +10910,16 @@ Bibliography: https://stackoverflow.com/questions/20055754/arm-start-wakeup-brin TODO: create and study a minimal examples in gem5 where the `DMB` instruction leads to less cycles: https://stackoverflow.com/questions/15491751/real-life-use-cases-of-barriers-dsb-dmb-isb-in-arm +==== ARM baremetal bibliography + +The most useful ARM baremetal example sets we've seen so far are: + +* https://github.com/dwelch67/raspberrypi real hardware +* https://github.com/dwelch67/qemu_arm_samples QEMU `-m vexpress` +* https://github.com/bztsrc/raspi3-tutorial real hardware + QEMU `-m raspi` +* https://github.com/LdB-ECM/Raspberry-Pi real hardware +* https://github.com/NienfengYao/armv8-bare-metal QEMU `-m virt` aarch64. A large part of the code is taken from the awesome educational OS under 2-clause BSD: https://github.com/takeharukato/sample-tsk-sw/tree/ce7973aa5d46c9eedb58309de43df3b09d4f8d8d/hal/aarch64 + === How we got some baremetal stuff to work It is nice when thing just work. @@ -10868,6 +11018,25 @@ We then found out that QEMU starts in EL1, and so we kept just the EL1 part, and * https://stackoverflow.com/questions/42824706/qemu-system-aarch64-entering-el1-when-emulating-a53-power-up * https://stackoverflow.com/questions/37299524/neon-support-in-armv8-system-mode-qemu +=== Baremetal tests + +Automatically run non-interactive baremetal tests: + +.... +./test-baremetal +.... + +Source: link:test-baremetal[] + +We detect if tests failed by parsing logs for the <>. + +We also skip tests that cannot work on certain conditions based on their basenames, e.g.: + +* tests that start with `gem5_` only run in `gem5` +* tests that start with `semihost_` only run in QEMU, until we find a better way to automate <> + +See: <> for more useful testing tips. + === Baremetal bibliography https://stackoverflow.com/questions/43682311/uart-communication-in-gem5-with-arm-bare-metal @@ -10903,6 +11072,8 @@ make CROSS_COMPILE_DIR=/usr/bin == Benchmark this repo +TODO: didn't fully port during refactor after 3b0a343647bed577586989fb702b760bd280844a. Reimplementing should not be hard. + In this section document how benchmark builds and runs of this repo, and how to investigate what the bottleneck is. Ideally, we should setup an automated build server that benchmarks those things continuously for us, but our <> attempt failed. @@ -10931,14 +11102,13 @@ We tried to automate it on Travis with link:.travis.yml[] but it hits the curren Run all kernel boot benchmarks for one arch: .... -./build-bench-boot --size 3 && ./bench-boot --size 3 -cat "$(./getvar bench_boot)" +./build-test-boot --size 3 && ./test-boot --size 3 +cat "$(./getvar test_boot_benchmark_file)" .... Sample results at 8fb9db39316d43a6dbd571e04dd46ae73915027f: .... - cmd ./run --arch x86_64 --eval '/poweroff.out' time 8.25 exit_status 0 @@ -10952,7 +11122,7 @@ time 8.83 exit_status 0 instructions 2244297 -cmd ./run --arch x86_64 --eval 'm5 exit' --gem5 +cmd ./run --arch x86_64 --eval 'm5 exit' --emulator gem5 time 213.39 exit_status 0 instructions 318486337 @@ -10965,12 +11135,12 @@ time 6.90 exit_status 0 instructions 776374 -cmd ./run --arch arm --eval 'm5 exit' --gem5 +cmd ./run --arch arm --eval 'm5 exit' --emulator gem5 time 118.46 exit_status 0 instructions 153023392 -cmd ./run --arch arm --eval 'm5 exit' --gem5 -- --cpu-type=HPI --caches --l2cache --l1d_size=1024kB --l1i_size=1024kB --l2_size=1024kB --l3_size=1024kB +cmd ./run --arch arm --eval 'm5 exit' --emulator gem5 -- --cpu-type=HPI --caches --l2cache --l1d_size=1024kB --l1i_size=1024kB --l2_size=1024kB --l3_size=1024kB time 2250.40 exit_status 0 instructions 151981914 @@ -10984,22 +11154,22 @@ time 5.04 exit_status 0 instructions 233162 -cmd ./run --arch aarch64 --eval 'm5 exit' --gem5 +cmd ./run --arch aarch64 --eval 'm5 exit' --emulator gem5 time 70.89 exit_status 0 instructions 124346081 -cmd ./run --arch aarch64 --eval 'm5 exit' --gem5 -- --cpu-type=HPI --caches --l2cache --l1d_size=1024kB --l1i_size=1024kB --l2_size=1024kB --l3_size=1024kB +cmd ./run --arch aarch64 --eval 'm5 exit' --emulator gem5 -- --cpu-type=HPI --caches --l2cache --l1d_size=1024kB --l1i_size=1024kB --l2_size=1024kB --l3_size=1024kB time 381.86 exit_status 0 instructions 124564620 -cmd ./run --arch aarch64 --eval 'm5 exit' --gem5 --gem5-build-type fast +cmd ./run --arch aarch64 --eval 'm5 exit' --emulator gem5 --gem5-build-type fast time 58.00 exit_status 0 instructions 124346081 -cmd ./run --arch aarch64 --eval 'm5 exit' --gem5 --gem5-build-type debug +cmd ./run --arch aarch64 --eval 'm5 exit' --emulator gem5 --gem5-build-type debug time 1022.03 exit_status 0 instructions 124346081 @@ -11012,7 +11182,7 @@ TODO: aarch64 gem5 and QEMU use the same kernel, so why is the gem5 instruction TODO 62f6870e4e0b384c4bd2d514116247e81b241251 takes 33 minutes to finish at 62f6870e4e0b384c4bd2d514116247e81b241251: .... -cmd ./run --arch arm --eval 'm5 exit' --gem5 -- --caches --cpu-type=HPI +cmd ./run --arch arm --eval 'm5 exit' --emulator gem5 -- --caches --cpu-type=HPI .... while aarch64 only 7 minutes. @@ -11113,7 +11283,7 @@ Sample results at gem5 2a9573f5942b5416fb0570cf5cb6cdecba733392: 10 to 12 minute Get results with: .... -./bench-all --gem5 +./bench-all --emulator gem5 tail -n+1 ../linux-kernel-module-cheat-regression/*/gem5-bench-build-*.txt .... @@ -11189,13 +11359,23 @@ gem5: We tend to test this repo the most on the latest Ubuntu and on the latest link:https://askubuntu.com/questions/16366/whats-the-difference-between-a-long-term-support-release-and-a-normal-release[Ubuntu LTS]. -For other Linux distros, everything will likely also just work if you install the analogous required packages for your distro, find them out with: +For other Linux distros, everything will likely also just work if you install the analogous required packages for your distro + +Find out the packages that we install with: + +.... +./build --download-dependencies --dry-run | less +.... + +and then just look for the `apt-get` commands shown on the log. + +After installing the missing packages for your distro, do the build with: .... -./build --download-dependencies --dry-run +./build --download-dependencies --no-apt .... -which just prints what `build` would do quickly without doing anything. +which does everything as normal, except that it skips any `apt` commands. Ports to new host systems are welcome and will be merged. @@ -11240,16 +11420,37 @@ It gets annoying to retype `--arch aarch64` for every single command, or to reme So simplify that, do: .... -cp config.example data/config +cp config.py data/ .... and then edit the `data/config` file to your needs. +Source: link:config.py[] + +You can also choose a different configuration file explicitly with: + +.... +./run --config data/config2.py +.... + +Almost all options names are automatically deduced from their command line `--help` name: just replace `-` with `_`. + +More precisely, we use the `dest=` value of Python's link:https://docs.python.org/3/library/argparse.html[argparse module]. + +To get a list of all global options that you can use, try: + +.... +./getvar --type input +.... + +but note that this does not include script specific options. + === Build the documentation You don't need to depend on GitHub: .... +sudo apt install asciidoctor ./build-doc xdg-open out/README.html .... @@ -11387,22 +11588,22 @@ Our scripts solve two difficulties with simultaneous runs: Each run gets a separate output directory. For example: .... -./run --arch aarch64 --gem5 --run-id 0 &>/dev/null & -./run --arch aarch64 --gem5 --run-id 1 &>/dev/null & +./run --arch aarch64 --emulator gem5 --run-id 0 &>/dev/null & +./run --arch aarch64 --emulator gem5 --run-id 1 &>/dev/null & .... produces two separate <>: .... -echo "$(./getvar --arch aarch64 --gem5 --run-id 0 m5out_dir)" -echo "$(./getvar --arch aarch64 --gem5 --run-id 1 m5out_dir)" +echo "$(./getvar --arch aarch64 --emulator gem5 --run-id 0 m5out_dir)" +echo "$(./getvar --arch aarch64 --emulator gem5 --run-id 1 m5out_dir)" .... and the gem5 host executable stdout and stderr can be found at: .... -less "$(./getvar --arch aarch64 --gem5 --run-id 0 termout_file)" -less "$(./getvar --arch aarch64 --gem5 --run-id 1 termout_file)" +less "$(./getvar --arch aarch64 --emulator gem5 --run-id 0 termout_file)" +less "$(./getvar --arch aarch64 --emulator gem5 --run-id 1 termout_file)" .... Each line is prepended with the timestamp in seconds since the start of the program when it appeared. @@ -11410,7 +11611,7 @@ Each line is prepended with the timestamp in seconds since the start of the prog To have more semantic output directories names for later inspection, you can use a non numeric string for the run ID, and indicate the port offset explicitly: .... -./run --arch aarch64 --gem5 --run-id some-experiment --port-offset 1 +./run --arch aarch64 --emulator gem5 --run-id some-experiment --port-offset 1 .... `--port-offset` defaults to the run ID when that is a number. @@ -11484,9 +11685,9 @@ Analogous to the <> but with the `--qemu-build-id` .... ./build-qemu -git -C "$(./getvar qemu_src_dir)" checkout v2.12.0 +git -C "$(./getvar qemu_source_dir)" checkout v2.12.0 ./build-qemu --qemu-build-id v2.12.0 -git -C "$(./getvar qemu_src_dir)" checkout - +git -C "$(./getvar qemu_source_dir)" checkout - ./run ./run --qemu-build-id v2.12.0 .... @@ -11500,18 +11701,18 @@ Analogous to the <> but with the `--gem5-build-id` ./build-gem5 # Build another branch. -git -C "$(./getvar gem5_src_dir)" checkout some-branch +git -C "$(./getvar gem5_source_dir)" checkout some-branch ./build-gem5 --gem5-build-id some-branch # Restore master. -git -C "$(./getvar gem5_src_dir)" checkout - +git -C "$(./getvar gem5_source_dir)" checkout - # Run master. -./run --gem5 +./run --emulator gem5 # Run another branch. -git -C "$(./getvar gem5_src_dir)" checkout some-branch -./run --gem5-build-id some-branch --gem5 +git -C "$(./getvar gem5_source_dir)" checkout some-branch +./run --gem5-build-id some-branch --emulator gem5 .... Don't forget however that gem5 has Python scripts in its source code tree, and that those must match the source code of a given build. @@ -11536,11 +11737,11 @@ cd - ./build-gem5 --gem5-worktree my-new-feature # Run the submodule. -./run --gem5 --run-id 0 &>/dev/null & +./run --emulator gem5 --run-id 0 &>/dev/null & # Run the branch the need to check out anything. # With --gem5-worktree, we can do both runs at the same time! -./run --gem5 --gem5-worktree my-new-feature --run-id 1 &>/dev/null & +./run --emulator gem5 --gem5-worktree my-new-feature --run-id 1 &>/dev/null & .... `--gem5-worktree ` automatically creates: @@ -11595,7 +11796,7 @@ The `gem5.debug` executable has optimizations turned off unlike the default `gem .... ./build-gem5 --arch aarch64 --gem5-build-type debug -./run --arch aarch64 --debug-vm --gem5 --gem5-build-type debug +./run --arch aarch64 --debug-vm --emulator gem5 --gem5-build-type debug .... The build outputs are automatically stored in a different directory from other build types such as `.opt` build, which prevents `.debug` files from overwriting `.opt` ones. @@ -11605,8 +11806,8 @@ Therefore, `--gem5-build-id` is not required. The price to pay for debuggability is high however: a Linux kernel boot was about 14 times slower than opt at 71e927e63bda6507d5a528f22c78d65099bdf36f between the commands: .... -./run --arch aarch64 --eval 'm5 exit' --gem5 --linux-build-id v4.16 -./run --arch aarch64 --eval 'm5 exit' --gem5 --linux-build-id v4.16 --gem5-build-type debug +./run --arch aarch64 --eval 'm5 exit' --emulator gem5 --linux-build-id v4.16 +./run --arch aarch64 --eval 'm5 exit' --emulator gem5 --linux-build-id v4.16 --gem5-build-type debug .... so you will likely only use this when it is unavoidable. @@ -11619,9 +11820,9 @@ Analogous to the <> but with the `--build-id` optio .... ./build-buildroot -git -C "$(./getvar buildroot_src_dir)" checkout 2018.05 +git -C "$(./getvar buildroot_source_dir)" checkout 2018.05 ./build-buildroot --buildroot-build-id 2018.05 -git -C "$(./getvar buildroot_src_dir)" checkout - +git -C "$(./getvar buildroot_source_dir)" checkout - ./run ./run --buildroot-build-id 2018.05 .... @@ -11636,9 +11837,13 @@ They contain data structs and magic constant for kernel to userland communicatio ==== userland directory -Userland test programs. +Userland test programs. They can be used in the following ways: + +* inside a full system simulation, e.g.: <> +* inside <> +* directly on the host: <> -For usage in the guest, build with: +For usage inside full system simulation, first ensure that Buildroot has been built for the toolchain, and then build the examples with: .... ./build-userland @@ -11646,7 +11851,7 @@ For usage in the guest, build with: Source: link:build-userland[]. -This makes them visible immediately on the 9P mount `/mnt/9p/out_root_overlay`. +This makes them visible immediately on the <<9p>> mount of a running simulator. In order to place them in the root filesystem image itself, you must also run: @@ -11654,7 +11859,9 @@ In order to place them in the root filesystem image itself, you must also run: ./build-buildroot .... -It is possible to build and run those examples directly on your host: +===== userland directory host build + +It is possible to build and run some of the userland examples directly on your host: .... cd userland @@ -11688,14 +11895,6 @@ TODO: OpenMP does not like `-static`: See: https://stackoverflow.com/questions/23869981/linking-openmp-statically-with-gcc -It is also possible to build other architectures with the host toolchain for other archs than your host arch: - -.... -./build-userland --arch arm --host --userland-build-id host -.... - -You won't be able to run those executables directly, but this is interesting if you are playing around with <>. - ==== buildroot_packages directory Source: link:buildroot_packages/[] @@ -11795,13 +11994,18 @@ We use this directory for: + C files for example need compilation, and must go through the regular package system, e.g. through link:kernel_modules/user[]. -This directory is copied into the target filesystem by link:copy-overlay[], which then it visible via <<9p>> on the guest at: +This directory is copied into the target filesystem by: .... -ls /mnt/9p/out_rootfs_overlay +./copy-overlay +./build-buildroot .... -Furthermore, since this directory does not require compilation, we also make it <<9p>> available to the guest directly even without `copy-overlay` at: +Source: link:copy-overlay[] + +Build Buildroot is required for the same reason as described at: <>. + +However, since the link:rootfs_overlay[] directory does not require compilation, unlike say <>, we also make it <<9p>> available to the guest directly even without `./copy-overlay` at: .... ls /mnt/9p/rootfs_overlay @@ -11811,12 +12015,10 @@ This way you can just hack away the scripts and try them out immediately without === Test this repo -This section describes how to run the most complete set of tests possible. - -It takes too much time to be feasible for every patch, but it should be done for every release. - ==== Automated tests +Run almost all tests: + .... ./build-test --size 3 && \ ./test --size 3 @@ -11830,30 +12032,77 @@ Sources: * link:build-test[] * link:test[] -Test just the kernel modules: +The link:test[] script runs several different types of tests, which can also be run separately as explained at: + +* link:test-boot[] +* <> +* <> +* <> +* <> + +link:test[] does not all possible tests, because there are too many possible variations and that would take forever. The rationale is the same as for `./build all` and is explained in `./build --help`. + +===== Test arch and emulator selection + +You can select multiple archs and emulators of interest, as for an other command, with: .... -./test-modules -echo $? +./test-user-mode \ + --arch x86_64 \ + --arch aarch64 \ + --emulator gem5 \ + --emulator qemu \ +; +.... + +You can also test all supported archs and emulators with: + .... +./test-user-mode \ + --all-archs \ + --all-emulators \ +; +.... + +This command would run the test four times, using `x86_64` and `aarch64` with both gem5 and QEMU. -Source: link:test-module[] +Without those flags, it defaults to just running the default arch and emulator once: `x86_64` and `qemu`. -Test that the Internet works: +===== Quit on fail + +By default, tests stop running as soon as the first failure happens. + +You can prevent this with the `--no-quit-on-fail option, e.g.: .... -./run --arch x86_64 --kernel-cli '- lkmc_eval="ifup -a;wget -S google.com;poweroff;"' +./test-user-mode --no-quit-on-fail +.... + +You can then see which tests failed on the test summary report at the end. + +===== Test userland in full system + +Run all userland tests from inside full system simulation (i.e. not <>): + +.... +./test-userland-full-system .... -Source: link:rootfs_overlay/test_all.sh[]. +This includes, in particular, userland programs that test the kernel modules, which cannot be tested in user mode simulation. + +Basically just boots and runs: link:rootfs_overlay/test_all.sh[] + +Failure is detected by looking for the <> + +Most userland programs that don't rely on kernel modules can also be tested in user mode simulation as explained at: <>. ===== Test GDB We have some link:https://github.com/pexpect/pexpect[pexpect] automated tests for the baremetal programs! .... -./build-test-gdb && \ -./test-gdb +./build --all-archs test-gdb && \ +./test-gdb --all-archs --all-emulators .... Sources: @@ -11861,9 +12110,7 @@ Sources: * link:build-test-gdb[] * link:test-gdb[] -Not all of them are passing right now due to: <>. - -If something goes wrong, re-run the test commands manually and use `--verbose` to understand what happened: +If a test fails, re-run the test commands manually and use `--verbose` to understand what happened: .... ./run --arch arm --background --baremetal add --wait-gdb & @@ -11888,7 +12135,26 @@ To debug GDB problems on gem5, you might want to enable the following <> +* link:rootfs_overlay/test_fail.sh[], which is used by <> + +=== Non-automated tests + +==== Test GDB Linux kernel For the Linux kernel, do the following manual tests for now. @@ -11911,6 +12177,14 @@ Then proceed to do the following tests: * `/count.sh` and `break __x64_sys_write` * `insmod /timer.ko` and `break lkmc_timer_callback` +==== Test the Internet + +You should also test that the Internet works: + +.... +./run --arch x86_64 --kernel-cli '- lkmc_eval="ifup -a;wget -S google.com;poweroff;"' +.... + === Bisection When updating the Linux kernel, QEMU and gem5, things sometimes break. @@ -11923,7 +12197,7 @@ We then bisected it as explained at: https://stackoverflow.com/questions/4713088 .... root_dir="$(pwd)" -cd "$(./getvar qemu_src_dir)" +cd "$(./getvar qemu_source_dir)" git bisect start # Check that our test script fails on v3.0.0-rc3 as expected, and mark it as bad. @@ -11940,7 +12214,7 @@ echo #? git bisect good # This leaves us at the offending commit. -git bisect run ../biset-qemu-linux-boot +git bisect run ../bisect-qemu-linux-boot # Clean up after the bisection. git bisect reset @@ -11948,7 +12222,7 @@ git submodule update "${root_dir}/build-qemu" --clean --qemu-build-id bisect .... -An example of Linux kernel commit bisection on gem5 boots can be found at: link:bisect-linux-boot-gem5[]. +TODO broken, fix: An example of Linux kernel commit bisection on gem5 boots can be found at: link:bisect-linux-boot-gem5[]. === Update a forked submodule @@ -12014,37 +12288,73 @@ This can be used to check the determinism of: === Release -Create a release: +==== Release procedure + +Ensure that the <> are passing on a clean build: + +.... +mv out out.bak +./build-test --size 3 && ./test --size 3 +.... + +The clean build is necessary as it generates clean images since <> + +Run all tests in <> just QEMU x86_64 and QEMU aarch64. + +TODO: not working currently, so skipped: Ensure that the <> look fine: + +.... +./bench-all -A +.... + +Create a release candidate and upload it: + +.... +git tag -a -m '' v3.0-rc1 +git push --follow-tags +./release-zip --all-archs +# export LKMC_GITHUB_TOKEN= +./release-upload +.... + +Now let's do an out-of-box testing for the release candidate: .... +cd .. git clone https://github.com/cirosantilli/linux-kernel-module-cheat linux-kernel-module-cheat-release cd linux-kernel-module-cheat-release -# export LKMC_GITHUB_TOKEN= -./release .... -Source: link:release[] +Test <>. -This scripts does: +Clean up, and re-start from scratch: -* configure -* build -* package with <> -* creates a tag of form `sha-` -* upload to GitHub with link:release-create-github[] +.... +cd .. +rm -rf linux-kernel-module-cheat-release +git clone https://github.com/cirosantilli/linux-kernel-module-cheat linux-kernel-module-cheat-release +cd linux-kernel-module-cheat-release +.... -Cloning a clean tree is ideal as it generates clean images since <> +Go through all the other <> sections in order. -This should in particular enable to easily update <>. +Once everything looks fine, publish the release with: -TODO also run tests and only release if they pass. +.... +git tag -a v3.0 +# Describe the release int the tag message. +git push --follow-tags +./release-zip --all-archs +# export LKMC_GITHUB_TOKEN= +./release-upload +.... ==== release-zip Create a zip containing all files required for <>: .... -./build release && ./release-zip +./build --all-archs release && ./release-zip --all-archs .... Source: link:release-zip[] @@ -12057,7 +12367,14 @@ echo "$(./getvar release_zip_file)" which you can then upload somewhere. -For example, you can create or update a GitHub release and upload automatically with: +==== release-upload + +After: + +* running <> +* creating and pushing a tag to GitHub + +you can upload the release to GitHub automatically with: .... # export LKMC_GITHUB_TOKEN= @@ -12066,9 +12383,14 @@ For example, you can create or update a GitHub release and upload automatically Source: link:release-upload[] +The HEAD of the local repository must be on top of a tag that has been pushed for this to work. + Create `LKMC_GITHUB_TOKEN` under: https://github.com/settings/tokens/new and save it to your `.bashrc`. -TODO: generalize that so that people can upload to their forks. +The implementation of this script is described at: + +* https://stackoverflow.com/questions/5207269/how-to-release-a-build-artifact-asset-on-github-with-a-script/52354732#52354732 +* https://stackoverflow.com/questions/38153418/can-someone-give-a-python-requests-example-of-uploading-a-release-asset-in-githu/52354681#52354681 === Design rationale @@ -12215,6 +12537,7 @@ Runnable stuff: * https://github.com/linux-kernel-labs Yocto based, source inside a kernel fork subdir: https://github.com/linux-kernel-labs/linux/tree/f08b9e4238dfc612a9d019e3705bd906930057fc/tools/labs which the author would like to upstream https://www.reddit.com/r/programming/comments/79w2q9/linux_device_driver_labs_the_linux_kernel/dp6of43/ * Android AOSP: https://stackoverflow.com/questions/1809774/how-to-compile-the-android-aosp-kernel-and-test-it-with-the-android-emulator/48310014#48310014 AOSP is basically a uber bloated Buildroot (2 hours build vs 30 minutes), Android is Linux based, and QEMU is the emulator backend. These instructions might work for debugging the kernel: https://github.com/Fuzion24/AndroidKernelExploitationPlayground * https://github.com/s-matyukevich/raspberry-pi-os Does both an OS from scratch, and annotates the corresponding kernel source code. For RPI3, no QEMU support: https://github.com/s-matyukevich/raspberry-pi-os/issues/8 +* https://github.com/pw4ever/linux-kernel-hacking-helper as of bd9952127e7eda643cbb6cb4c51ad7b5b224f438, Bash, Arch Linux rootfs Theory: diff --git a/arm b/arm deleted file mode 100755 index c54105c6..00000000 --- a/arm +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash -build-crosstool-ng \ -; \ No newline at end of file diff --git a/baremetal/add.c b/baremetal/add.c index 76567b48..3ec66710 100644 --- a/baremetal/add.c +++ b/baremetal/add.c @@ -1,13 +1,13 @@ #include int main(void) { - int i, j, k; - i = 1; + int i, j, k; + i = 1; /* test-gdb-op1 */ - j = 2; + j = 2; /* test-gdb-op2 */ - k = i + j; + k = i + j; /* test-gdb-result */ - if (k != 3) - common_assert_fail(); + if (k != 3) + common_assert_fail(); } diff --git a/baremetal/arch/aarch64/common_aarch64.h b/baremetal/arch/aarch64/common_aarch64.h new file mode 100644 index 00000000..180b2385 --- /dev/null +++ b/baremetal/arch/aarch64/common_aarch64.h @@ -0,0 +1,24 @@ +#ifndef COMMON_AARCH64_H +#define COMMON_AARCH64_H + +#include + +#define SYSREG_READ(type, name) \ + type sysreg_ ## name ## _read(void) { \ + type name; \ + __asm__ __volatile__("mrs %0, " #name : "=r" (name) : : ); \ + return name; \ + } + +#define SYSREG_WRITE(type, name) \ + void sysreg_ ## name ## _write(type name) { \ + __asm__ __volatile__("msr " #name ", %0" : : "r" (name) : ); \ + } + +#define SYSREG_READ_WRITE(name, type) \ + SYSREG_READ(name, type) \ + SYSREG_WRITE(name, type) + +#define SVC(immediate) __asm__ __volatile__("svc " #immediate : : : ) + +#endif diff --git a/baremetal/arch/aarch64/el.c b/baremetal/arch/aarch64/el.c index 851c3626..63dfd268 100644 --- a/baremetal/arch/aarch64/el.c +++ b/baremetal/arch/aarch64/el.c @@ -4,8 +4,8 @@ #include int main(void) { - register uint64_t x0 __asm__ ("x0"); - __asm__ ("mrs x0, CurrentEL;" : : : "%x0"); - printf("%" PRIu64 "\n", x0 >> 2); - return 0; + uint64_t el; + __asm__ ("mrs %0, CurrentEL;" : "=r" (el) : :); + printf("%" PRIu64 "\n", el >> 2); + return 0; } diff --git a/baremetal/arch/aarch64/multicore.S b/baremetal/arch/aarch64/multicore.S index 2eafd355..45d8f8fc 100644 --- a/baremetal/arch/aarch64/multicore.S +++ b/baremetal/arch/aarch64/multicore.S @@ -9,6 +9,7 @@ main: /* Read cpu id into x1. * TODO: cores beyond 4th? + * Mnemonic: Main Processor ID Register */ mrs x1, mpidr_el1 ands x1, x1, 3 diff --git a/baremetal/arch/aarch64/svc.c b/baremetal/arch/aarch64/svc.c new file mode 100644 index 00000000..5c02da2c --- /dev/null +++ b/baremetal/arch/aarch64/svc.c @@ -0,0 +1,28 @@ +#include +#include + +#include "common_aarch64.h" + +/* Masks each of the 4 exception types: Synchronous, System error, + * IRQ and FIQ. + */ +SYSREG_READ_WRITE(uint32_t, daif) + +/* Determines if we use SP0 or SPx. Default: SP0. + * See also: https://stackoverflow.com/questions/29393677/armv8-exception-vector-significance-of-el0-sp + */ +SYSREG_READ_WRITE(uint32_t, spsel) + +/* Jump to this SP if spsel == SPx. */ +SYSREG_READ_WRITE(uint64_t, sp_el1) + +int main(void) { + printf("daif 0x%" PRIx32 "\n", sysreg_daif_read()); + printf("spsel 0x%" PRIx32 "\n", sysreg_spsel_read()); + /* TODO this breaks execution because reading system registers that end + * in ELx "trap", leading into an exception on the upper EL. + */ + /*printf("sp_el1 0x%" PRIx64 "\n", sysreg_sp_el1_read());*/ + /*SVC(0);*/ + return 0; +} diff --git a/baremetal/arch/aarch64/timer.c b/baremetal/arch/aarch64/timer.c new file mode 100644 index 00000000..b42d3694 --- /dev/null +++ b/baremetal/arch/aarch64/timer.c @@ -0,0 +1,59 @@ +#include +#include + +#include "common_aarch64.h" + +#define CNTV_CTL_ENABLE (1 << 0) +#define CNTV_CTL_IMASK (1 << 1) +#define CNTV_CTL_ISTATUS (1 << 2) + +/* Frequency in Hz. ? */ +SYSREG_READ_WRITE(uint64_t, cntfrq_el0) + +/* Current virtual counter value. */ +SYSREG_READ(uint64_t, cntvct_el0) + +/* Compare value. See: cntv_ctl_el0_enable. */ +SYSREG_READ_WRITE(uint64_t, cntv_cval_el0) + +/* On write, set cntv_cval_el0 = (cntvct_el0 + cntv_tval_el0). + * This means that the next interrupt will happen in cntv_tval_el0 cycles. + */ +SYSREG_READ_WRITE(uint64_t, cntv_tval_el0) + +/* Control register. */ +SYSREG_READ_WRITE(uint32_t, cntv_ctl_el0) + +void cntv_ctl_el0_disable(void) { + sysreg_cntv_ctl_el0_write(sysreg_cntv_ctl_el0_read() & ~CNTV_CTL_ENABLE); +} + +/* If enabled, when: cntv_ctl > cntv_cval then: + * + * * if CNTV_CTL_IMASK is clear, raise an interrupt + * * set CNTV_CTL_ISTATUS + */ +void cntv_ctl_el0_enable(void) { + sysreg_cntv_ctl_el0_write(sysreg_cntv_ctl_el0_read() | CNTV_CTL_ENABLE); +} + +int main(void) { + /* Initial state. */ + printf("cntv_ctl_el0 0x%" PRIx32 "\n", sysreg_cntv_ctl_el0_read()); + printf("cntfrq_el0 0x%" PRIx64 "\n", sysreg_cntfrq_el0_read()); + printf("cntv_cval_el0 0x%" PRIx64 "\n", sysreg_cntv_cval_el0_read()); + + /* Get the counter value many times to watch the time pass. */ + printf("cntvct_el0 0x%" PRIx64 "\n", sysreg_cntvct_el0_read()); + printf("cntvct_el0 0x%" PRIx64 "\n", sysreg_cntvct_el0_read()); + printf("cntvct_el0 0x%" PRIx64 "\n", sysreg_cntvct_el0_read()); + +#if 0 + /* TODO crashes gem5. */ + puts("cntfrq_el0 = 1"); + sysreg_cntfrq_el0_write(1); + printf("cntfrq_el0 0x%" PRIx64 "\n", sysreg_cntfrq_el0_read()); +#endif + + return 0; +} diff --git a/baremetal/arch/arm/el.c b/baremetal/arch/arm/el.c index 5944b58e..eda1f211 100644 --- a/baremetal/arch/arm/el.c +++ b/baremetal/arch/arm/el.c @@ -4,8 +4,8 @@ #include int main(void) { - register uint32_t r0 __asm__ ("r0"); - __asm__ ("mrs r0, CPSR" : : : "%r0"); - printf("%" PRIu32 "\n", r0 & 0x1F); - return 0; + uint32_t cpsr; + __asm__ ("mrs %0, CPSR" : "=r" (cpsr) : :); + printf("%" PRIu32 "\n", cpsr & 0x1F); + return 0; } diff --git a/baremetal/exit.c b/baremetal/exit.c index de81ab6b..98bcda29 100644 --- a/baremetal/exit.c +++ b/baremetal/exit.c @@ -2,6 +2,5 @@ #include int main(void) { - exit(0); + exit(0); } - diff --git a/baremetal/interactive/hello.c b/baremetal/hello.c similarity index 54% rename from baremetal/interactive/hello.c rename to baremetal/hello.c index fe49acd9..20d437d9 100644 --- a/baremetal/interactive/hello.c +++ b/baremetal/hello.c @@ -1,6 +1,6 @@ #include int main(void) { - puts("hello"); - return 0; + puts("hello"); + return 0; } diff --git a/baremetal/interactive/assert_fail.c b/baremetal/interactive/assert_fail.c index d4ea62c3..419a3c09 100644 --- a/baremetal/interactive/assert_fail.c +++ b/baremetal/interactive/assert_fail.c @@ -1,6 +1,6 @@ #include int main(void) { - common_assert_fail(); + common_assert_fail(); } diff --git a/baremetal/interactive/exit1.c b/baremetal/interactive/exit1.c index 90f1d147..342579e5 100644 --- a/baremetal/interactive/exit1.c +++ b/baremetal/interactive/exit1.c @@ -2,5 +2,5 @@ #include int main(void) { - exit(1); + exit(1); } diff --git a/baremetal/interactive/infinite_loop.c b/baremetal/interactive/infinite_loop.c new file mode 100644 index 00000000..6aaced06 --- /dev/null +++ b/baremetal/interactive/infinite_loop.c @@ -0,0 +1,4 @@ +int main(void) { + while(1) {} + return 0; +} diff --git a/baremetal/lib/syscalls.c b/baremetal/lib/syscalls.c index d9fbd611..a05be154 100644 --- a/baremetal/lib/syscalls.c +++ b/baremetal/lib/syscalls.c @@ -64,24 +64,24 @@ int _write(int file, char *ptr, int len) { void _exit(int status) { #if defined(GEM5) #if defined(__arm__) - __asm__ __volatile__ ("mov r0, #0; mov r1, #0; .inst 0xEE000110 | (0x21 << 16);"); + __asm__ __volatile__ ("mov r0, #0; mov r1, #0; .inst 0xEE000110 | (0x21 << 16);"); #elif defined(__aarch64__) - __asm__ __volatile__ ("mov x0, #0; .inst 0XFF000110 | (0x21 << 16);"); + __asm__ __volatile__ ("mov x0, #0; .inst 0XFF000110 | (0x21 << 16);"); #endif #else #if defined(__arm__) __asm__ __volatile__ ("mov r0, #0x18; ldr r1, =#0x20026; svc 0x00123456"); #elif defined(__aarch64__) - /* TODO actually use the exit value here, just for fun. */ + /* TODO actually use the exit value here, just for fun. */ __asm__ __volatile__ ( - "mov x1, #0x26\n" \ - "movk x1, #2, lsl #16\n" \ - "str x1, [sp,#0]\n" \ - "mov x0, #0\n" \ - "str x0, [sp,#8]\n" \ - "mov x1, sp\n" \ - "mov w0, #0x18\n" \ - "hlt 0xf000\n" + "mov x1, #0x26\n" \ + "movk x1, #2, lsl #16\n" \ + "str x1, [sp,#0]\n" \ + "mov x0, #0\n" \ + "str x0, [sp,#8]\n" \ + "mov x1, sp\n" \ + "mov w0, #0x18\n" \ + "hlt 0xf000\n" ); #endif #endif diff --git a/bench-all b/bench-all index 6915a02e..2945be13 100755 --- a/bench-all +++ b/bench-all @@ -100,9 +100,9 @@ if "$bench_gem5_build"; then common_arch="$default_arch" gem5_build_id=bench common_gem5_build_dir="$("$getvar" --arch "$common_arch" --gem5-build-id "$gem5_build_id" gem5_build_dir)" - common_gem5_src_dir="$("$getvar" --arch "$common_arch" --gem5-build-id "$gem5_build_id" gem5_src_dir)" + common_gem5_source_dir="$("$getvar" --arch "$common_arch" --gem5-build-id "$gem5_build_id" gem5_source_dir)" results_file="${common_gem5_build_dir}/lkmc-bench-build.txt" - git -C "${common_gem5_src_dir}" clean -xdf + git -C "${common_gem5_source_dir}" clean -xdf rm -f "$results_file" "${root_dir}/build-gem5" --arch "$common_arch" --clean --gem5-build-id "$gem5_build_id" # TODO understand better: --foreground required otherwise we cannot @@ -110,15 +110,15 @@ if "$bench_gem5_build"; then # bash -c "eval 'timeout 5 sleep 3'" "${root_dir}/bench-cmd" "timeout --foreground 900 ./build-gem5 --arch '$common_arch' --gem5-build-id '$gem5_build_id'" "$results_file" cp "$results_file" "${new_dir}/gem5-bench-build-${common_arch}.txt" - git -C "${common_gem5_src_dir}" clean -xdf + git -C "${common_gem5_source_dir}" clean -xdf "${root_dir}/build-gem5" --arch "$common_arch" --clean --gem5-build-id "$gem5_build_id" fi if "$bench_linux_boot"; then cd "${root_dir}" - "${root_dir}/build" --all - "${root_dir}/bench-boot" --size 3 - cp "$(${root_dir}/getvar bench_boot)" "$new_dir" + "${root_dir}/build" --all-archs all + "${root_dir}/test-boot" --size 3 + cp "$(${root_dir}/getvar test_boot_benchmark_file)" "$new_dir" fi if "$update_repo"; then diff --git a/bench-boot b/bench-boot deleted file mode 100755 index 410e2a66..00000000 --- a/bench-boot +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env bash -set -eu -root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" -test_size=1 -while [ $# -gt 0 ]; do - case "$1" in - --size) - # 1: a few seconds and important - # 2: < 5 minutes and important or a few seconds and not too important - # 3: all - test_size="$2" - shift 2 - ;; - esac -done - -if [ $# -gt 1 ]; then - extra_args=" $*" -else - extra_args= -fi -getvar="${root_dir}/getvar" -common_bench_boot="$("$getvar" bench_boot)" -caches='--caches --l2cache --l1d_size=1024kB --l1i_size=1024kB --l2_size=1024kB --l3_size=1024kB' - -bench() ( - "${root_dir}/bench-cmd" "./run --arch ${1}${extra_args}" "$common_bench_boot" -) - -newline() ( - echo >> "$common_bench_boot" -) - -gem5_insts() ( - printf "instructions $(./gem5-stat --arch "$1" sim_insts)\n" >> "$common_bench_boot" - newline -) - -qemu_insts() ( - common_arch="$1" - ./qemu-trace2txt --arch "$common_arch" - common_qemu_trace_txt_file="$("$getvar" --arch "$common_arch" qemu_trace_txt_file)" - printf "instructions $(wc -l "${common_qemu_trace_txt_file}" | cut -d' ' -f1)\n" >> "$common_bench_boot" - newline -) - -rm -f "${common_bench_boot}" - -arch=x86_64 -bench "${arch} --eval '/poweroff.out'" -newline -bench "${arch} --eval '/poweroff.out' --kvm" -newline -if [ "$test_size" -ge 2 ]; then - bench "${arch} --eval '/poweroff.out' --trace exec_tb" - qemu_insts "$arch" - bench "$arch --eval 'm5 exit' --gem5" - gem5_insts "$arch" -fi -#bench "$arch --eval 'm5 exit' --gem5 -- --cpu-type=DerivO3CPU ${caches}" -#gem5_insts "$arch" - -arch=arm -bench "$arch --eval '/poweroff.out'" -if [ "$test_size" -ge 2 ]; then - bench "$arch --eval '/poweroff.out' --trace exec_tb" - qemu_insts "$arch" - bench "$arch --eval 'm5 exit' --gem5" - gem5_insts "$arch" -fi -if [ "$test_size" -ge 3 ]; then - bench "$arch --eval 'm5 exit' --gem5 -- --cpu-type=HPI ${caches}" - gem5_insts "$arch" -fi - -arch=aarch64 -bench "$arch --eval '/poweroff.out'" -newline -if [ "$test_size" -ge 2 ]; then - bench "$arch --eval '/poweroff.out' --trace exec_tb" - qemu_insts "$arch" - bench "$arch --eval 'm5 exit' --gem5" - gem5_insts "$arch" -fi -if [ "$test_size" -ge 3 ]; then - bench "$arch --eval 'm5 exit' --gem5 -- --cpu-type=HPI ${caches}" - gem5_insts "$arch" - #bench "$arch --eval 'm5 exit' --gem5 --gem5-script biglittle" - #gem5_insts "$arch" - bench "$arch --eval 'm5 exit' --gem5 --gem5-build-type fast" - gem5_insts "$arch" - bench "$arch --eval 'm5 exit' --gem5 --gem5-build-type debug" - gem5_insts "$arch" -fi diff --git a/bisect-linux-boot-gem5 b/bisect-linux-boot-gem5 index 91aa3469..6d9a43cb 100755 --- a/bisect-linux-boot-gem5 +++ b/bisect-linux-boot-gem5 @@ -6,25 +6,25 @@ import shutil import sys import common -build_linux = imp.load_source('build-linux', os.path.join(common.root_dir, 'build_linux')) -run = imp.load_source('run', os.path.join(common.root_dir, 'run')) +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. More information at: https://github.com/cirosantilli/linux-kernel-module-cheat#bisection '''}, default_args={ - 'gem5': True, + 'emulators': ['gem5'], '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(common.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/bisect-qemu-linux-boot b/bisect-qemu-linux-boot index cf19d3d6..6e797a9f 100755 --- a/bisect-qemu-linux-boot +++ b/bisect-qemu-linux-boot @@ -1,6 +1,6 @@ #!/usr/bin/env bash set -eu -git submodule update -cd .. -./build-qemu --arch arm -Q bisect -./run --arch arm -E '/poweroff.out' -Q bisect +git submodule update --recursive +cd ../.. +./build-qemu --arch aarch64 --qemu-build-id bisect +./run --arch aarch64 --kernel-cli 'init=/poweroff.out' --qemu-build-id bisect diff --git a/bst-vs-heap b/bst-vs-heap index b8cbda50..69ec9b98 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 050f3ff2..ba59888c 100755 --- a/build +++ b/build @@ -1,17 +1,19 @@ #!/usr/bin/env python3 -import argparse -import collections -import platform import re import os +import cli_function +import collections import common +import copy +import shell_helpers +from shell_helpers import LF -class Component: +class _Component: ''' - Yes, we are re-inventing a crappy dependency resolution system. - I can't believe it. + Yes, we are re-inventing a crappy dependency resolution system, + reminescent of scons or apt or Buildroot. I can't believe it. The hard part is that we have optional dependencies as well... e.g. buildroot optionally depends on m5 to put m5 in the root filesystem, @@ -39,229 +41,28 @@ class Component: self.submodules_shallow = submodules_shallow or set() self.python2_pkgs = python2_pkgs or set() self.python3_pkgs = python3_pkgs or set() + def build(self, arch): if ( (self.build_callback is not None) and (self.supported_archs is None or arch in self.supported_archs) ): - self.build_callback(arch) - -def run_cmd(cmd, arch): - global args - cmd_abs = cmd.copy() - cmd_abs[0] = os.path.join(common.root_dir, cmd[0]) - cmd_abs.extend(['--arch', arch]) - if args.extra_args: - cmd_abs.append(args.extra_args) - common.run_cmd(cmd_abs, dry_run=args.dry_run) - -buildroot_component = Component( - lambda arch: run_cmd(['build-buildroot'], arch), - submodules = {'buildroot'}, - # https://buildroot.org/downloads/manual/manual.html#requirement - apt_get_pkgs={ - 'bash', - 'bc', - 'binutils', - 'build-essential', - 'bzip2', - 'cpio', - 'g++', - 'gcc', - 'graphviz', - 'gzip', - 'make', - 'patch', - 'perl', - 'python-matplotlib', - 'python3', - 'rsync', - 'sed', - 'tar', - 'unzip', - }, -) + self.build_callback() -name_to_component_map = { - # Leaves without dependencies. - 'baremetal-qemu': Component( - lambda arch: run_cmd(['build-baremetal', '--qemu'], arch), - supported_archs=common.crosstool_ng_supported_archs, - ), - 'baremetal-gem5': Component( - lambda arch: run_cmd(['build-baremetal', '--gem5'], arch), - supported_archs=common.crosstool_ng_supported_archs, - ), - 'baremetal-gem5-pbx': Component( - lambda arch: run_cmd(['build-baremetal', '--gem5', '--machine', 'RealViewPBX'], arch), - supported_archs=common.crosstool_ng_supported_archs, - ), - 'buildroot': buildroot_component, - 'buildroot-gcc': buildroot_component, - 'copy-overlay': Component( - lambda arch: run_cmd(['copy-overlay'], arch), - ), - 'crosstool-ng': Component( - lambda arch: run_cmd(['build-crosstool-ng'], arch), - supported_archs=common.crosstool_ng_supported_archs, - # http://crosstool-ng.github.io/docs/os-setup/ - apt_get_pkgs={ - 'bison', - 'docbook2x', - 'flex', - 'gawk', - 'gcc', - 'gperf', - 'help2man', - 'libncurses5-dev', - 'libtool-bin', - 'make', - 'python-dev', - 'texinfo', - }, - submodules={'crosstool-ng'}, - ), - 'gem5': Component( - lambda arch: run_cmd(['build-gem5'], arch), - # TODO test it out on Docker and answer that question properly: - # https://askubuntu.com/questions/350475/how-can-i-install-gem5 - apt_get_pkgs={ - 'device-tree-compiler', - 'diod', - 'libgoogle-perftools-dev', - 'm4', - 'protobuf-compiler', - 'python-dev', - 'python-pip', - # For prebuilt qcow2 unpack. - 'qemu-utils', - 'scons', - 'zlib1g-dev', - }, - python2_pkgs={ - # Generate graphs of config.ini under m5out. - 'pydot', - }, - submodules={'gem5'}, - ), - 'gem5-debug': Component( - lambda arch: run_cmd(['build-gem5', '--gem5-build-type', 'debug'], arch), - ), - 'gem5-fast': Component( - lambda arch: run_cmd(['build-gem5', '--gem5-build-type', 'fast'], arch), - ), - 'linux': Component( - lambda arch: run_cmd(['build-linux'], arch), - submodules_shallow={'linux'}, - apt_get_pkgs={ - 'bison', - 'flex', - # Without this started failing in kernel 4.15 with: - # Makefile:932: *** "Cannot generate ORC metadata for CONFIG_UNWINDER_ORC=y, please install libelf-dev, libelf-devel or elfutils-libelf-devel". Stop. - 'libelf-dev', - }, - ), - 'modules': Component( - lambda arch: run_cmd(['build-modules'], arch), - ), - 'm5': Component( - lambda arch: run_cmd(['build-m5'], arch), - submodules={'gem5'}, - ), - 'qemu': Component( - lambda arch: run_cmd(['build-qemu'], arch), - apt_build_deps={'qemu'}, - apt_get_pkgs={'libsdl2-dev'}, - submodules={'qemu'}, - ), - 'qemu-user': Component( - lambda arch: run_cmd(['build-qemu', '--userland'], arch), - apt_build_deps = {'qemu'}, - apt_get_pkgs={'libsdl2-dev'}, - submodules = {'qemu'}, - ), - 'parsec-benchmark': Component( - submodules = {'parsec-benchmark'}, - ), - 'userland': Component( - lambda arch: run_cmd(['build-userland'], arch), - ), - - # Dependency only nodes. - 'all': Component(dependencies=[ - 'all-linux', - 'all-baremetal', - ]), - 'all-baremetal': Component(dependencies=[ - 'qemu-baremetal', - 'gem5-baremetal', - 'baremetal-gem5-pbx', - ], - supported_archs=common.crosstool_ng_supported_archs, - ), - 'all-linux': Component(dependencies=[ - 'qemu-gem5-buildroot', - 'gem5-debug', - 'gem5-fast', - 'qemu-user', - ]), - 'baremetal': Component(dependencies=[ - 'baremetal-gem5', - 'baremetal-qemu', - ]), - 'gem5-buildroot': Component(dependencies=[ - 'buildroot-gcc', - 'linux', - 'm5', - 'overlay', - 'gem5', - ]), - 'gem5-baremetal': Component(dependencies=[ - 'gem5', - 'crosstool-ng', - 'baremetal-gem5', - ]), - 'overlay': Component(dependencies=[ - 'copy-overlay', - 'modules', - 'userland', - 'buildroot', - ]), - 'qemu-baremetal': Component(dependencies=[ - 'qemu', - 'crosstool-ng', - 'baremetal-qemu', - ]), - 'qemu-buildroot': Component(dependencies=[ - 'qemu', - 'buildroot-gcc', - 'overlay', - 'linux', - ]), - 'qemu-gem5-buildroot': Component(dependencies=[ - 'qemu', - 'gem5-buildroot', - ]), - 'release': Component(dependencies=[ - 'qemu-buildroot', - ]), -} -parser = argparse.ArgumentParser( - description= '''\ -Shallow helper to build everything, or a subset of everything conveniently. +class Main(common.LkmcCliFunction): + def __init__(self): + super().__init__( + description='''\ +Build a component and all its dependencies. Our build-* scripts don't build any dependencies to make iterative development fast and more predictable. -While modifying a specific component however, you will likely want to just run the -individual build-* commands which: - -* build no dependencies, and so are fast and predictable -* can take multiple options to custumize the build +It is currently not possible to configure indivitual components from the command line +when you build with this script. TODO. -Without any args, build only what is necessary for +Without any args, build only what is necessary for: https://github.com/cirosantilli/linux-kernel-module-cheat#qemu-buildroot-setup -for x86_64: .... ./%(prog)s @@ -273,211 +74,475 @@ This is equivalent to: ./%(prog)s --arch x86_64 qemu-buildroot .... -If `--arch` is given, build just for the given archs: +Another important target is `all`: .... -./%(prog)s --arch arm --arch aarch64 +./%(prog)s all .... -This will build `qemu-buildroot` for arm and aarch64 only, but not `x86_64`. +This does not trully build ALL configurations: that would be impractical. +But more precisely: build the reference configuration of each major component. -Clean all Linux kernel builds: +So e.g.: one config of Linux kenrel, Buildroot, gem5 and qemu. +Don't do for example all possible gem5 configs: debug, opt and fast, +as that would be huge. This ensures that every piece of software +builds in at least one config. + +TODO looping over emulators is not currently supported by this script, e.g.: .... -./build --all-archs --extra-args=--clean buildroot +./%(prog)s --arch x86_64 --arch aarch64 all .... + +Instead, for the targets that are emulator dependent, you must select the +taret version for the desired emulatore, e.g.: + +.... +./build --arch aarch64 baremetal-qemu baremetal-gem5 +.... + +The reason is that some targets depend on emulator, while others don't, +so looping over all of them would waste time. ''', - formatter_class=argparse.RawTextHelpFormatter, -) -parser.add_argument('--all', default=False, action='store_true', help='''\ -Build absolutely everything for all archs. -''') -group = parser.add_mutually_exclusive_group(required=False) -group.add_argument('-A', '--all-archs', default=False, action='store_true', help='''\ -Build the selected components for all archs. -''') -group.add_argument('-a', '--arch', choices=common.arch_choices, default=[], action='append', help='''\ -Build the selected components for this arch. Select multiple archs by -passing this option multiple times. Default: [{}] -'''.format(common.default_arch)) -parser.add_argument('-D', '--download-dependencies', default=False, action='store_true', help='''\ + ) + buildroot_component = _Component( + self._build_file('build-buildroot'), + submodules = {'buildroot'}, + # https://buildroot.org/downloads/manual/manual.html#requirement + apt_get_pkgs={ + 'bash', + 'bc', + 'binutils', + 'build-essential', + 'bzip2', + 'cpio', + 'g++', + 'gcc', + 'graphviz', + 'gzip', + 'make', + 'patch', + 'perl', + 'python-matplotlib', + 'python3', + 'rsync', + 'sed', + 'tar', + 'unzip', + }, + ) + buildroot_overlay_qemu_component = copy.copy(buildroot_component) + # We need to build QEMU before the final Buildroot to get qemu-img. + buildroot_overlay_qemu_component.dependencies = ['overlay', 'qemu'] + buildroot_overlay_gem5_component = copy.copy(buildroot_component) + buildroot_overlay_gem5_component.dependencies = ['overlay-gem5'] + gem5_deps = { + # TODO test it out on Docker and answer that question properly: + # https://askubuntu.com/questions/350475/how-can-i-install-gem5 + 'apt_get_pkgs': { + 'device-tree-compiler', + 'diod', + 'libgoogle-perftools-dev', + 'm4', + 'protobuf-compiler', + 'python-dev', + 'python-pip', + # For prebuilt qcow2 unpack. + 'qemu-utils', + 'scons', + 'zlib1g-dev', + }, + 'python2_pkgs': { + # Generate graphs of config.ini under m5out. + 'pydot', + }, + 'submodules': {'gem5'}, + } + + self.name_to_component_map = { + 'all': _Component(dependencies=[ + 'qemu-gem5-buildroot', + 'all-baremetal', + 'user-mode-qemu', + 'doc', + ]), + 'all-baremetal': _Component(dependencies=[ + 'qemu-baremetal', + 'gem5-baremetal', + 'baremetal-gem5-pbx', + ], + supported_archs=common.consts['crosstool_ng_supported_archs'], + ), + 'baremetal': _Component(dependencies=[ + 'baremetal-gem5', + 'baremetal-qemu', + ]), + 'baremetal-qemu': _Component( + self._build_file('build-baremetal', emulators=['qemu']), + supported_archs=common.consts['crosstool_ng_supported_archs'], + dependencies=['crosstool-ng'], + ), + 'baremetal-gem5': _Component( + self._build_file('build-baremetal', emulators=['gem5']), + supported_archs=common.consts['crosstool_ng_supported_archs'], + dependencies=['crosstool-ng'], + ), + 'baremetal-gem5-pbx': _Component( + self._build_file('build-baremetal', emulators=['gem5'], machine='RealViewPBX'), + supported_archs=common.consts['crosstool_ng_supported_archs'], + dependencies=['crosstool-ng'], + ), + 'buildroot': buildroot_component, + # We need those to avoid cirtulcar dependencies, since we need to run Buildroot + # twice: once to get the toolchain, and a second time to put the overlay into + # the root filesystem. + 'buildroot-overlay-qemu': buildroot_overlay_qemu_component, + 'buildroot-overlay-gem5': buildroot_overlay_gem5_component, + 'copy-overlay': _Component( + self._build_file('copy-overlay'), + ), + 'crosstool-ng': _Component( + self._build_file('build-crosstool-ng'), + supported_archs=common.consts['crosstool_ng_supported_archs'], + # http://crosstool-ng.github.io/docs/os-setup/ + apt_get_pkgs={ + 'bison', + 'docbook2x', + 'flex', + 'gawk', + 'gcc', + 'gperf', + 'help2man', + 'libncurses5-dev', + 'libtool-bin', + 'make', + 'python-dev', + 'texinfo', + }, + submodules={'crosstool-ng'}, + ), + 'doc': _Component( + self._build_file('build-doc'), + ), + 'gem5': _Component( + self._build_file('build-gem5'), + **gem5_deps + ), + 'gem5-baremetal': _Component(dependencies=[ + 'gem5', + 'baremetal-gem5', + ]), + 'gem5-buildroot': _Component(dependencies=[ + 'buildroot-overlay-gem5', + 'linux', + 'gem5', + ]), + 'gem5-debug': _Component( + self._build_file('build-gem5', gem5_build_type='debug'), + **gem5_deps + ), + 'gem5-fast': _Component( + self._build_file('build-gem5', gem5_build_type='fast'), + **gem5_deps + ), + 'linux': _Component( + self._build_file('build-linux'), + dependencies={'buildroot'}, + submodules_shallow={'linux'}, + apt_get_pkgs={ + 'bison', + 'flex', + # Without this started failing in kernel 4.15 with: + # Makefile:932: *** "Cannot generate ORC metadata for CONFIG_UNWINDER_ORC=y, please install libelf-dev, libelf-devel or elfutils-libelf-devel". Stop. + 'libelf-dev', + }, + ), + 'modules': _Component( + self._build_file('build-modules'), + dependencies=['buildroot', 'linux'], + ), + 'm5': _Component( + self._build_file('build-m5'), + dependencies=['buildroot'], + submodules={'gem5'}, + ), + 'overlay': _Component(dependencies=[ + 'copy-overlay', + 'modules', + 'userland', + ]), + 'overlay-gem5': _Component(dependencies=[ + 'm5', + 'overlay', + ]), + 'parsec-benchmark': _Component( + submodules={'parsec-benchmark'}, + dependencies=['buildroot'], + ), + 'qemu': _Component( + self._build_file('build-qemu'), + apt_build_deps={'qemu'}, + apt_get_pkgs={'libsdl2-dev'}, + submodules={'qemu'}, + ), + 'qemu-baremetal': _Component(dependencies=[ + 'qemu', + 'baremetal-qemu', + ]), + 'qemu-buildroot': _Component(dependencies=[ + 'buildroot-overlay-qemu', + 'linux', + ]), + 'qemu-gem5-buildroot': _Component(dependencies=[ + 'qemu', + 'gem5-buildroot', + ]), + 'qemu-user': _Component( + self._build_file('build-qemu', user_mode=True), + apt_build_deps = {'qemu'}, + apt_get_pkgs={'libsdl2-dev'}, + submodules={'qemu'}, + ), + 'release': _Component(dependencies=[ + 'qemu-buildroot', + 'doc', + ]), + 'test-gdb': _Component(dependencies=[ + 'all-baremetal', + ], + supported_archs=common.consts['crosstool_ng_supported_archs'], + ), + 'test-user-mode': _Component(dependencies=[ + 'test-user-mode-qemu', + 'test-user-mode-gem5', + ]), + 'test-user-mode-qemu': _Component(dependencies=[ + 'user-mode-qemu', + 'userland', + ]), + 'test-user-mode-gem5': _Component(dependencies=[ + 'gem5', + 'userland-gem5', + ]), + 'user-mode-qemu': _Component( + dependencies=['qemu-user', 'userland'], + ), + 'userland': _Component( + self._build_file('build-userland'), + dependencies=['buildroot'], + ), + 'userland-gem5': _Component( + self._build_file('build-userland', static=True, userland_build_id='static'), + dependencies=['buildroot'], + ), + } + self.component_to_name_map = {self.name_to_component_map[key]:key for key in self.name_to_component_map} + + self.add_argument( + '--apt', + default=True, + help='''\ +Don't run any apt-get commands. To make it easier to use with other archs: +https://github.com/cirosantilli/linux-kernel-module-cheat#supported-hosts +''' + ) + self.add_argument( + '--download-dependencies', + default=False, + help='''\ Also download all dependencies required for a given build: Ubuntu packages, Python packages and git submodules. -''') -parser.add_argument('--extra-args', default='', help='''\ -Extra args to pass to all scripts. ''' -) -parser.add_argument('--travis', default=False, action='store_true', help='''\ + ) + self.add_argument( + '--print-components', + default=False, + help='''\ +Print the components that would be built, including dependencies, but don't +build them, nor show the build commands. +''' + ) + self.add_argument( + '--travis', + default=False, + help='''\ Extra args to pass to all scripts. ''' -) -parser.add_argument('components', choices=list(name_to_component_map.keys()) + [[]], default=[], nargs='*', help='''\ + ) + self.add_argument( + 'components', + choices=list(self.name_to_component_map.keys()) + [[]], + default=[], + nargs='*', + help='''\ Which components to build. Default: qemu-buildroot -'''.format(common.default_arch)) -common.add_dry_run_argument(parser) -args = parser.parse_args() -common.setup_dry_run_arguments(args) +''' + ) -# Decide archs. -if args.arch == []: - if args.all or args.all_archs: - archs = common.all_archs.copy() - else: - archs = set([common.default_arch]) -else: - archs = set() - for arch in args.arch: - if arch in common.arch_short_to_long_dict: - arch = common.arch_short_to_long_dict[arch] - archs.add(arch) + def _build_file(self, component_file, **extra_args): + ''' + Build something based on a component file that defines a Main class. + ''' + def f(): + args = self.get_common_args() + args.update(extra_args) + args['print_time'] = False + self.import_path_main(component_file)(**args) + return f -# Decide components. -components = args.components -if args.all: - components = ['all'] -elif components == []: - components = ['qemu-buildroot'] -selected_components = [] -selected_component_name_set = set() -for component_name in components: - todo = [component_name] - while todo: - current_name = todo.pop(0) - if current_name not in selected_component_name_set: - selected_component_name_set.add(current_name) - component = name_to_component_map[current_name] - selected_components.append(component) - todo.extend(component.dependencies) + def timed_main(self): + self.sh = shell_helpers.ShellHelpers(dry_run=self.env['dry_run']) -if args.download_dependencies: - apt_get_pkgs = { - # Core requirements for this repo. - 'git', - 'moreutils', # ts - 'python3-pip', - 'tmux', - 'vinagre', - 'wget', - } - # E.e. on an ARM host, the package gcc-arm-linux-gnueabihf - # is called just gcc. - processor = platform.processor() - if processor != 'arm': - apt_get_pkgs.update({ - 'gcc-arm-linux-gnueabihf', - 'g++-arm-linux-gnueabihf', - }) - if processor != 'aarch64': - apt_get_pkgs.update({ - 'gcc-aarch64-linux-gnu', - 'g++-aarch64-linux-gnu', - }) - apt_build_deps = set() - submodules = set() - submodules_shallow = set() - python2_pkgs = set() - python3_pkgs = { - 'pexpect==4.6.0', - } - for component in selected_components: - apt_get_pkgs.update(component.apt_get_pkgs) - apt_build_deps.update(component.apt_build_deps) - submodules.update(component.submodules) - submodules_shallow.update(component.submodules_shallow) - python2_pkgs.update(component.python2_pkgs) - python3_pkgs.update(component.python3_pkgs) - if apt_get_pkgs or apt_build_deps: - if args.travis: - interacive_pkgs = { - 'libsdl2-dev', + # Decide components. + components = self.env['components'] + if components == []: + components = ['qemu-buildroot'] + selected_components = [] + for component_name in components: + todo = [component_name] + while todo: + current_name = todo.pop(0) + component = self.name_to_component_map[current_name] + selected_components.insert(0, component) + todo.extend(component.dependencies) + # Remove duplicates, keep only the first one of each. + # https://stackoverflow.com/questions/7961363/removing-duplicates-in-lists/7961390#7961390 + selected_components = collections.OrderedDict.fromkeys(selected_components) + + if self.env['download_dependencies']: + apt_get_pkgs = { + # Core requirements for this repo. + 'git', + 'moreutils', # ts + 'python3-pip', + 'tmux', + 'vinagre', + 'wget', } - apt_get_pkgs.difference_update(interacive_pkgs) - if common.in_docker: - sudo = [] - # https://askubuntu.com/questions/909277/avoiding-user-interaction-with-tzdata-when-installing-certbot-in-a-docker-contai - os.environ['DEBIAN_FRONTEND'] = 'noninteractive' - # https://askubuntu.com/questions/496549/error-you-must-put-some-source-uris-in-your-sources-list - sources_path = os.path.join('/etc', 'apt', 'sources.list') - with open(sources_path, 'r') as f: - sources_txt = f.read() - sources_txt = re.sub('^# deb-src ', 'deb-src ', sources_txt, flags=re.MULTILINE) - with open(sources_path, 'w') as f: - f.write(sources_txt) - else: - sudo = ['sudo'] - if common.in_docker or args.travis: - y = ['-y'] - else: - y = [] - common.run_cmd( - sudo + ['apt-get', 'update', common.Newline] - ) - if apt_get_pkgs: - common.run_cmd( - sudo + ['apt-get', 'install'] + y + [common.Newline] + - common.add_newlines(sorted(apt_get_pkgs)) - ) - if apt_build_deps: - common.run_cmd( - sudo + - ['apt-get', 'build-dep'] + y + [common.Newline] + - common.add_newlines(sorted(apt_build_deps)) - ) - if python2_pkgs: - common.run_cmd( - ['python', '-m', 'pip', 'install', '--user', common.Newline] + - common.add_newlines(sorted(python2_pkgs)) - ) - if python3_pkgs: - # Not with pip executable directly: - # https://stackoverflow.com/questions/49836676/error-after-upgrading-pip-cannot-import-name-main/51846054#51846054 - common.run_cmd( - ['python3', '-m', 'pip', 'install', '--user', common.Newline] + - common.add_newlines(sorted(python3_pkgs)) - ) - git_cmd_common = ['git', 'submodule', 'update', '--init', '--recursive'] - if submodules: - # == Other nice git options for when distros move to newer Git - # - # Currently not on Ubuntu 16.04: - # - # `--progress`: added on Git 2.10: - # - # * https://stackoverflow.com/questions/32944468/how-to-show-progress-for-submodule-fetching - # * https://stackoverflow.com/questions/4640020/progress-indicator-for-git-clone - # - # `--jobs"`: https://stackoverflow.com/questions/26957237/how-to-make-git-clone-faster-with-multiple-threads/52327638#52327638 - common.run_cmd( - git_cmd_common + ['--', common.Newline] + - common.add_newlines([os.path.join(common.submodules_dir, x) for x in sorted(submodules)]) - ) - if submodules_shallow: - # == Shallow cloning. - # - # TODO Ideally we should shallow clone --depth 1 all of them. - # - # However, most git servers out there are crap or craply configured - # and don't allow shallow cloning except for branches. - # - # So for now, let's shallow clone only the Linux kernel, which has by far - # the largest .git repo history, and full clone the others. - # - # Then we will maintain a GitHub Linux kernel mirror / fork that always has a - # lkmc branch, and point to it, so that it will always succeed. - # - # See also: - # - # * https://stackoverflow.com/questions/3489173/how-to-clone-git-repository-with-specific-revision-changeset - # * https://stackoverflow.com/questions/2144406/git-shallow-submodules/47374702#47374702 - # * https://unix.stackexchange.com/questions/338578/why-is-the-git-clone-of-the-linux-kernel-source-code-much-larger-than-the-extrac - # - common.run_cmd( - git_cmd_common + ['--depth', '1', '--', common.Newline] + - common.add_newlines([os.path.join(common.submodules_dir, x) for x in sorted(submodules_shallow)]) - ) + # E.g. on an ARM host, the package gcc-arm-linux-gnueabihf + # is called just gcc. + processor = self.env['host_arch'] + if processor != 'arm': + apt_get_pkgs.update({ + 'gcc-arm-linux-gnueabihf', + 'g++-arm-linux-gnueabihf', + }) + if processor != 'aarch64': + apt_get_pkgs.update({ + 'gcc-aarch64-linux-gnu', + 'g++-aarch64-linux-gnu', + }) + apt_build_deps = set() + submodules = set() + submodules_shallow = set() + python2_pkgs = set() + python3_pkgs = { + 'pexpect==4.6.0', + } + for component in selected_components: + apt_get_pkgs.update(component.apt_get_pkgs) + apt_build_deps.update(component.apt_build_deps) + submodules.update(component.submodules) + submodules_shallow.update(component.submodules_shallow) + python2_pkgs.update(component.python2_pkgs) + python3_pkgs.update(component.python3_pkgs) + if apt_get_pkgs or apt_build_deps: + if self.env['travis']: + interacive_pkgs = { + 'libsdl2-dev', + } + apt_get_pkgs.difference_update(interacive_pkgs) + if common.consts['in_docker']: + sudo = [] + # https://askubuntu.com/questions/909277/avoiding-user-interaction-with-tzdata-when-installing-certbot-in-a-docker-contai + os.environ['DEBIAN_FRONTEND'] = 'noninteractive' + # https://askubuntu.com/questions/496549/error-you-must-put-some-source-uris-in-your-sources-list + sources_path = os.path.join('/etc', 'apt', 'sources.list') + with open(sources_path, 'r') as f: + sources_txt = f.read() + sources_txt = re.sub('^# deb-src ', 'deb-src ', sources_txt, flags=re.MULTILINE) + with open(sources_path, 'w') as f: + f.write(sources_txt) + else: + sudo = ['sudo'] + if common.consts['in_docker'] or self.env['travis']: + y = ['-y'] + else: + y = [] + if self.env['apt']: + self.sh.run_cmd( + sudo + ['apt-get', 'update', LF] + ) + if apt_get_pkgs: + self.sh.run_cmd( + sudo + ['apt-get', 'install'] + y + [LF] + + self.sh.add_newlines(sorted(apt_get_pkgs)) + ) + if apt_build_deps: + self.sh.run_cmd( + sudo + + ['apt-get', 'build-dep'] + y + [LF] + + self.sh.add_newlines(sorted(apt_build_deps)) + ) + if python2_pkgs: + self.sh.run_cmd( + ['python', '-m', 'pip', 'install', '--user', LF] + + self.sh.add_newlines(sorted(python2_pkgs)) + ) + if python3_pkgs: + # Not with pip executable directly: + # https://stackoverflow.com/questions/49836676/error-after-upgrading-pip-cannot-import-name-main/51846054#51846054 + self.sh.run_cmd( + ['python3', '-m', 'pip', 'install', '--user', LF] + + self.sh.add_newlines(sorted(python3_pkgs)) + ) + git_cmd_common = ['git', 'submodule', 'update', '--init', '--recursive'] + if submodules: + # == Other nice git options for when distros move to newer Git + # + # Currently not on Ubuntu 16.04: + # + # `--progress`: added on Git 2.10: + # + # * https://stackoverflow.com/questions/32944468/how-to-show-progress-for-submodule-fetching + # * https://stackoverflow.com/questions/4640020/progress-indicator-for-git-clone + # + # `--jobs"`: https://stackoverflow.com/questions/26957237/how-to-make-git-clone-faster-with-multiple-threads/52327638#52327638 + self.sh.run_cmd( + git_cmd_common + ['--', LF] + + self.sh.add_newlines([os.path.join(common.consts['submodules_dir'], x) for x in sorted(submodules)]) + ) + if submodules_shallow: + # == Shallow cloning. + # + # TODO Ideally we should shallow clone --depth 1 all of them. + # + # However, most git servers out there are crap or craply configured + # and don't allow shallow cloning except for branches. + # + # So for now, let's shallow clone only the Linux kernel, which has by far + # the largest .git repo history, and full clone the others. + # + # Then we will maintain a GitHub Linux kernel mirror / fork that always has a + # lkmc branch, and point to it, so that it will always succeed. + # + # See also: + # + # * https://stackoverflow.com/questions/3489173/how-to-clone-git-repository-with-specific-revision-changeset + # * https://stackoverflow.com/questions/2144406/git-shallow-submodules/47374702#47374702 + # * https://unix.stackexchange.com/questions/338578/why-is-the-git-clone-of-the-linux-kernel-source-code-much-larger-than-the-extrac + # + self.sh.run_cmd( + git_cmd_common + ['--depth', '1', '--', LF] + + self.sh.add_newlines([os.path.join(common.consts['submodules_dir'], x) for x in sorted(submodules_shallow)]) + ) + + # Do the build. + for component in selected_components: + if self.env['print_components']: + print(self.component_to_name_map[component]) + else: + component.build(self.env['arch']) -# Do the build. -for arch in archs: - for component in selected_components: - component.build(arch) +if __name__ == '__main__': + Main().cli() diff --git a/build-baremetal b/build-baremetal index 16594bbf..c731c04d 100755 --- a/build-baremetal +++ b/build-baremetal @@ -1,73 +1,80 @@ #!/usr/bin/env python3 - import os import common +from shell_helpers import LF + +class Main(common.BuildCliFunction): + def __init__(self): + super().__init__( + description='''\ +Build the baremetal examples with crosstool-NG. +''', + supported_archs=common.consts['crosstool_ng_supported_archs'] + ) -class BaremetalComponent(common.Component): - def do_build(self, args): - common.assert_crosstool_ng_supports_arch(args.arch) - build_dir = self.get_build_dir(args) - bootloader_obj = os.path.join(common.baremetal_build_lib_dir, 'bootloader{}'.format(common.obj_ext)) + def build(self): + build_dir = self.get_build_dir() + bootloader_obj = os.path.join(self.env['baremetal_build_lib_dir'], 'bootloader{}'.format(self.env['obj_ext'])) common_basename_noext = 'common' - common_src = os.path.join(common.root_dir, common_basename_noext + common.c_ext) - common_obj = os.path.join(common.baremetal_build_lib_dir, common_basename_noext + common.obj_ext) + common_src = os.path.join(self.env['root_dir'], common_basename_noext + self.env['c_ext']) + common_obj = os.path.join(self.env['baremetal_build_lib_dir'], common_basename_noext + self.env['obj_ext']) syscalls_basename_noext = 'syscalls' - syscalls_src = os.path.join(common.baremetal_src_lib_dir, syscalls_basename_noext + common.c_ext) - syscalls_obj = os.path.join(common.baremetal_build_lib_dir, syscalls_basename_noext + common.obj_ext) + syscalls_src = os.path.join(self.env['baremetal_source_lib_dir'], syscalls_basename_noext + self.env['c_ext']) + syscalls_obj = os.path.join(self.env['baremetal_build_lib_dir'], syscalls_basename_noext + self.env['obj_ext']) common_objs = [common_obj, syscalls_obj] cflags = [ - '-I', common.baremetal_src_lib_dir, common.Newline, - '-I', common.root_dir, common.Newline, - '-O0', common.Newline, - '-ggdb3', common.Newline, - '-mcpu={}'.format(common.mcpu), common.Newline, - '-nostartfiles', common.Newline, + '-I', self.env['baremetal_source_lib_dir'], LF, + '-I', self.env['root_dir'], LF, + '-O0', LF, + '-ggdb3', LF, + '-mcpu={}'.format(self.env['mcpu']), LF, + '-nostartfiles', LF, ] - if args.prebuilt: + if self.env['prebuilt']: gcc = 'arm-none-eabi-gcc' else: - os.environ['PATH'] = common.crosstool_ng_bin_dir + os.environ['PATH'] - gcc = common.get_toolchain_tool('gcc', allowed_toolchains=['crosstool-ng']) - if common.emulator == 'gem5': - if common.machine == 'VExpress_GEM5_V1': + os.environ['PATH'] = self.env['crosstool_ng_bin_dir'] + os.environ['PATH'] + gcc = self.get_toolchain_tool('gcc', allowed_toolchains=['crosstool-ng']) + if self.env['emulator'] == 'gem5': + if self.env['machine'] == 'VExpress_GEM5_V1': entry_address = 0x80000000 uart_address = 0x1c090000 - elif common.machine == 'RealViewPBX': + elif self.env['machine'] == 'RealViewPBX': entry_address = 0x10000 uart_address = 0x10009000 else: - raise Exception('unknown machine: ' + common.machine) - cflags.extend(['-D', 'GEM5'.format(uart_address), common.Newline]) + raise Exception('unknown machine: ' + self.env['machine']) + cflags.extend(['-D', 'GEM5'.format(uart_address), LF]) else: entry_address = 0x40000000 uart_address = 0x09000000 os.makedirs(build_dir, exist_ok=True) - os.makedirs(common.baremetal_build_lib_dir, exist_ok=True) - src = os.path.join(common.baremetal_src_lib_dir, '{}{}'.format(args.arch, common.asm_ext)) - if common.need_rebuild([src], bootloader_obj): - common.run_cmd( - [gcc, common.Newline] + + os.makedirs(self.env['baremetal_build_lib_dir'], exist_ok=True) + src = os.path.join(self.env['baremetal_source_lib_dir'], '{}{}'.format(self.env['arch'], self.env['asm_ext'])) + if self.need_rebuild([src], bootloader_obj): + self.sh.run_cmd( + [gcc, LF] + cflags + [ - '-c', common.Newline, - '-o', bootloader_obj, common.Newline, - src, common.Newline, + '-c', LF, + '-o', bootloader_obj, LF, + src, LF, ] ) for src, obj in [ (common_src, common_obj), (syscalls_src, syscalls_obj), ]: - if common.need_rebuild([src], obj): - common.run_cmd( - [gcc, common.Newline] + + if self.need_rebuild([src], obj): + self.sh.run_cmd( + [gcc, LF] + cflags + [ - '-c', common.Newline, - '-D', 'UART0_ADDR={:#x}'.format(uart_address), common.Newline, - '-o', obj, common.Newline, - src, common.Newline, + '-c', LF, + '-D', 'UART0_ADDR={:#x}'.format(uart_address), LF, + '-o', obj, LF, + src, LF, ] ) self._build_dir( @@ -86,18 +93,17 @@ class BaremetalComponent(common.Component): bootloader_obj=bootloader_obj, common_objs=common_objs, ) - arch_dir = os.path.join('arch', args.arch) - if os.path.isdir(os.path.join(common.baremetal_src_dir, arch_dir)): + if os.path.isdir(os.path.join(self.env['baremetal_source_arch_dir'])): self._build_dir( - arch_dir, + self.env['baremetal_source_arch_subpath'], gcc=gcc, cflags=cflags, entry_address=entry_address, bootloader_obj=bootloader_obj, common_objs=common_objs, ) - arch_dir = os.path.join('arch', args.arch, 'no_bootloader') - if os.path.isdir(os.path.join(common.baremetal_src_dir, arch_dir)): + arch_dir = os.path.join('arch', self.env['arch'], 'no_bootloader') + if os.path.isdir(os.path.join(self.env['baremetal_source_dir'], arch_dir)): self._build_dir( arch_dir, gcc=gcc, @@ -108,18 +114,8 @@ class BaremetalComponent(common.Component): bootloader=False, ) - def get_argparse_args(self): - return { - 'description': '''\ -Build the baremetal examples with crosstool-NG. -''' - } - - def get_build_dir(self, args): - return common.baremetal_build_dir - - def get_default_args(self): - return {'baremetal': 'all'} + def get_build_dir(self): + return self.env['baremetal_build_dir'] def _build_dir( self, @@ -131,48 +127,48 @@ Build the baremetal examples with crosstool-NG. common_objs, bootloader=True ): - """ + ''' Build all .c and .S files in a given subpath of the baremetal source directory non recursively. Place outputs on the same subpath or the output directory. - """ - in_dir = os.path.join(common.baremetal_src_dir, subpath) - out_dir = os.path.join(common.baremetal_build_dir, subpath) + ''' + in_dir = os.path.join(self.env['baremetal_source_dir'], subpath) + out_dir = os.path.join(self.env['baremetal_build_dir'], subpath) os.makedirs(out_dir, exist_ok=True) common_objs = common_objs.copy() if bootloader: common_objs.append(bootloader_obj) for in_basename in os.listdir(in_dir): in_path = os.path.join(in_dir, in_basename) - if os.path.isfile(in_path) and os.path.splitext(in_basename)[1] in (common.c_ext, common.asm_ext): + if os.path.isfile(in_path) and os.path.splitext(in_basename)[1] in (self.env['c_ext'], self.env['asm_ext']): in_name = os.path.splitext(in_basename)[0] - main_obj = os.path.join(common.baremetal_build_dir, subpath, '{}{}'.format(in_name, common.obj_ext)) - src = os.path.join(common.baremetal_src_dir, in_path) - if common.need_rebuild([src], main_obj): - common.run_cmd( - [gcc, common.Newline] + + main_obj = os.path.join(self.env['baremetal_build_dir'], subpath, '{}{}'.format(in_name, self.env['obj_ext'])) + src = os.path.join(self.env['baremetal_source_dir'], in_path) + if self.need_rebuild([src], main_obj): + self.sh.run_cmd( + [gcc, LF] + cflags + [ - '-c', common.Newline, - '-o', main_obj, common.Newline, - src, common.Newline, + '-c', LF, + '-o', main_obj, LF, + src, LF, ] ) objs = common_objs + [main_obj] - out = os.path.join(common.baremetal_build_dir, subpath, in_name + common.baremetal_build_ext) - link_script = os.path.join(common.baremetal_src_dir, 'link.ld') - if common.need_rebuild(objs + [link_script], out): - common.run_cmd( - [gcc, common.Newline] + + out = os.path.join(self.env['baremetal_build_dir'], subpath, in_name + self.env['baremetal_build_ext']) + link_script = os.path.join(self.env['baremetal_source_dir'], 'link.ld') + if self.need_rebuild(objs + [link_script], out): + self.sh.run_cmd( + [gcc, LF] + cflags + [ - '-Wl,--section-start=.text={:#x}'.format(entry_address), common.Newline, - '-o', out, common.Newline, - '-T', link_script, common.Newline, + '-Wl,--section-start=.text={:#x}'.format(entry_address), LF, + '-o', out, LF, + '-T', link_script, LF, ] + - common.add_newlines(objs) + self.sh.add_newlines(objs) ) if __name__ == '__main__': - BaremetalComponent().build() + Main().cli() diff --git a/build-bench-boot b/build-bench-boot deleted file mode 100755 index 34efc119..00000000 --- a/build-bench-boot +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash -set -eu -test_size=1 -while [ $# -gt 0 ]; do - case "$1" in - --size) - test_size="$2" - shift 2 - ;; - esac -done -./build --all-archs qemu-gem5-buildroot -if [ "$test_size" -ge 3 ]; then - ./build --arch aarch64 all-linux -fi diff --git a/build-buildroot b/build-buildroot index 0d0e9eac..9cd16b27 100755 --- a/build-buildroot +++ b/build-buildroot @@ -9,11 +9,17 @@ import time import re import common +from shell_helpers import LF -class BuildrootComponent(common.Component): - def add_parser_arguments(self, parser): - parser.add_argument( - '--build-linux', default=self._defaults['build_linux'], action='store_true', +class Main(common.BuildCliFunction): + def __init__(self): + super().__init__( + description='''\ +Build Buildroot. This includes, notably: the userland GCC cross-toolchain, +and the root filesystem. +''') + self.add_argument( + '--build-linux', default=False, help='''\ Enable building the Linux kernel with Buildroot. This is done mostly to extract Buildroot's default kernel configurations when updating Buildroot. @@ -21,35 +27,35 @@ This kernel will not be use by our other scripts. Configuring this kernel is 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', + self.add_argument( + '--baseline', default=False, 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. ''' ) - parser.add_argument( - '--config', default=self._defaults['config'], action='append', + self.add_argument( + '--config', default=[], action='append', help='''Add a single Buildroot config to the current build. Example value: 'BR2_TARGET_ROOTFS_EXT2_SIZE="512M"'. Can be used multiple times to add multiple configs. Takes precedence over any Buildroot config files. ''' ) - parser.add_argument( - '--config-fragment', default=self._defaults['config_fragment'], action='append', + self.add_argument( + '--config-fragment', default=[], action='append', help='''Also use the given Buildroot configuration fragment file. Pass multiple times to use multiple fragment files. ''' ) - parser.add_argument( - '--no-all', default=self._defaults['no_all'], action='store_true', + self.add_argument( + '--no-all', default=False, 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', + self.add_argument( + '--no-overlay', default=False, 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 @@ -57,132 +63,116 @@ still rebuild the Buildroot package that provides those files to actually put th files on the root filesystem. ''' ) - parser.add_argument( - 'extra_make_args', default=self._defaults['extra_make_args'], metavar='extra-make-args', nargs='*', + self.add_argument( + 'extra-make-args', default=[], nargs='*', help='''\ Extra arguments to be passed to the Buildroot make, usually extra Buildroot targets. ''' ) - def do_build(self, args): - build_dir = self.get_build_dir(args) - os.makedirs(common.out_dir, exist_ok=True) - extra_make_args = common.add_newlines(args.extra_make_args) - if args.build_linux: - extra_make_args.extend(['linux-reconfigure', common.Newline]) - if common.emulator == 'gem5': - extra_make_args.extend(['gem5-reconfigure', common.Newline]) - if args.arch == 'x86_64': + def build(self): + build_dir = self.get_build_dir() + os.makedirs(self.env['out_dir'], exist_ok=True) + extra_make_args = self.sh.add_newlines(self.env['extra_make_args']) + if self.env['build_linux']: + extra_make_args.extend(['linux-reconfigure', LF]) + if self.env['arch'] == 'x86_64': defconfig = 'qemu_x86_64_defconfig' - elif args.arch == 'arm': + elif self.env['arch'] == 'arm': defconfig = 'qemu_arm_vexpress_defconfig' - elif args.arch == 'aarch64': + elif self.env['arch'] == 'aarch64': defconfig = 'qemu_aarch64_virt_defconfig' br2_external_dirs = [] - for package_dir in os.listdir(common.packages_dir): - package_dir_abs = os.path.join(common.packages_dir, package_dir) + for package_dir in os.listdir(self.env['packages_dir']): + package_dir_abs = os.path.join(self.env['packages_dir'], package_dir) if os.path.isdir(package_dir_abs): br2_external_dirs.append(self._path_relative_to_buildroot(package_dir_abs)) br2_external_str = ':'.join(br2_external_dirs) - common.run_cmd( + self.sh.run_cmd( [ - 'make', common.Newline, - 'O={}'.format(common.buildroot_build_dir), common.Newline, - 'BR2_EXTERNAL={}'.format(br2_external_str), common.Newline, - defconfig, common.Newline, + 'make', LF, + 'O={}'.format(self.env['buildroot_build_dir']), LF, + 'BR2_EXTERNAL={}'.format(br2_external_str), LF, + defconfig, LF, ], - cwd=common.buildroot_src_dir, + cwd=self.env['buildroot_source_dir'], ) - configs = args.config + configs = self.env['config'] configs.extend([ - 'BR2_JLEVEL={}'.format(args.nproc), - 'BR2_DL_DIR="{}"'.format(common.buildroot_download_dir), + 'BR2_JLEVEL={}'.format(self.env['nproc']), + 'BR2_DL_DIR="{}"'.format(self.env['buildroot_download_dir']), ]) - if not args.build_linux: + if not self.env['build_linux']: configs.extend([ '# BR2_LINUX_KERNEL is not set', ]) config_fragments = [] - if not args.baseline: + if not self.env['baseline']: configs.extend([ 'BR2_GLOBAL_PATCH_DIR="{}"'.format( - self._path_relative_to_buildroot(os.path.join(common.root_dir, 'patches', 'global')) + self._path_relative_to_buildroot(os.path.join(self.env['root_dir'], 'patches', 'global')) ), 'BR2_PACKAGE_BUSYBOX_CONFIG_FRAGMENT_FILES="{}"'.format( - self._path_relative_to_buildroot(os.path.join(common.root_dir, 'busybox_config_fragment')) + self._path_relative_to_buildroot(os.path.join(self.env['root_dir'], 'busybox_config_fragment')) ), 'BR2_PACKAGE_OVERRIDE_FILE="{}"'.format( - self._path_relative_to_buildroot(os.path.join(common.root_dir, 'buildroot_override')) + self._path_relative_to_buildroot(os.path.join(self.env['root_dir'], 'buildroot_override')) ), 'BR2_ROOTFS_POST_BUILD_SCRIPT="{}"'.format( - self._path_relative_to_buildroot(os.path.join(common.root_dir, 'rootfs-post-build-script')) + self._path_relative_to_buildroot(os.path.join(self.env['root_dir'], 'rootfs-post-build-script')) ), 'BR2_ROOTFS_USERS_TABLES="{}"'.format( - self._path_relative_to_buildroot(os.path.join(common.root_dir, 'user_table')) + self._path_relative_to_buildroot(os.path.join(self.env['root_dir'], 'user_table')) ), ]) - if not args.no_overlay: + if not self.env['no_overlay']: configs.append('BR2_ROOTFS_OVERLAY="{}"'.format( - self._path_relative_to_buildroot(common.out_rootfs_overlay_dir) + self._path_relative_to_buildroot(self.env['out_rootfs_overlay_dir']) )) config_fragments = [ - os.path.join(common.root_dir, 'buildroot_config', 'default') - ] + args.config_fragment - common.write_configs(common.buildroot_config_file, configs, config_fragments) - common.run_cmd( + os.path.join(self.env['root_dir'], 'buildroot_config', 'default') + ] + self.env['config_fragment'] + if self.env['initrd'] or self.env['initramfs']: + configs.append('BR2_TARGET_ROOTFS_CPIO=y') + # TODO Can't get rid of these for now with nice fragments on Buildroot: + # http://stackoverflow.com/questions/44078245/is-it-possible-to-use-config-fragments-with-buildroots-config + self.sh.write_configs(self.env['buildroot_config_file'], configs, config_fragments) + self.sh.run_cmd( [ - 'make', common.Newline, - 'O={}'.format(common.buildroot_build_dir), common.Newline, - 'olddefconfig', common.Newline, + 'make', LF, + 'O={}'.format(self.env['buildroot_build_dir']), LF, + 'olddefconfig', LF, ], - cwd=common.buildroot_src_dir, + cwd=self.env['buildroot_source_dir'], ) - common.make_build_dirs() - if not args.no_all: - extra_make_args.extend(['all', common.Newline]) - common.run_cmd( + self.make_build_dirs() + if not self.env['no_all']: + extra_make_args.extend(['all', LF]) + self.sh.run_cmd( [ - 'make', common.Newline, - 'LKMC_GEM5_SRCDIR="{}"'.format(common.gem5_src_dir), common.Newline, - 'LKMC_PARSEC_BENCHMARK_SRCDIR="{}"'.format(common.parsec_benchmark_src_dir), common.Newline, - 'O={}'.format(common.buildroot_build_dir), common.Newline, - 'V={}'.format(int(args.verbose)), common.Newline, + 'make', LF, + 'LKMC_PARSEC_BENCHMARK_SRCDIR="{}"'.format(self.env['parsec_benchmark_source_dir']), LF, + 'O={}'.format(self.env['buildroot_build_dir']), LF, + 'V={}'.format(int(self.env['verbose'])), LF, ] + extra_make_args , - out_file=os.path.join(common.buildroot_build_dir, 'lkmc.log'), + out_file=os.path.join(self.env['buildroot_build_dir'], 'lkmc.log'), delete_env=['LD_LIBRARY_PATH'], - cwd=common.buildroot_src_dir, + cwd=self.env['buildroot_source_dir'], ) # Create the qcow2 from ext2. # 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 args.no_all and os.path.exists(common.qemu_img_executable): - common.raw_to_qcow2() + if not self.env['no_all'] and os.path.exists(self.env['qemu_img_executable']): + self.raw_to_qcow2() - def get_argparse_args(self): - return { - 'description': '''\ -Run Linux on an emulator -''' - } - - def get_build_dir(self, args): - return common.buildroot_build_dir - - _defaults = { - 'baseline': False, - 'build_linux': False, - 'config': [], - 'config_fragment': [], - 'extra_make_args': [], - 'no_all': False, - 'skip_configure': False, - } + def get_build_dir(self): + return self.env['buildroot_build_dir'] def _path_relative_to_buildroot(self, abspath): - return os.path.relpath(abspath, common.buildroot_src_dir) + return os.path.relpath(abspath, self.env['buildroot_source_dir']) if __name__ == '__main__': - BuildrootComponent().build() + Main().cli() diff --git a/build-crosstool-ng b/build-crosstool-ng index 3386ad69..af40db36 100755 --- a/build-crosstool-ng +++ b/build-crosstool-ng @@ -3,75 +3,71 @@ import os import common +from shell_helpers import LF -class CrosstoolNgComponent(common.Component): - def do_build(self, args): - common.assert_crosstool_ng_supports_arch(args.arch) - build_dir = self.get_build_dir(args) - defconfig_dest = os.path.join(common.crosstool_ng_util_dir, 'defconfig') - os.makedirs(common.crosstool_ng_util_dir, exist_ok=True) - os.makedirs(common.crosstool_ng_download_dir, exist_ok=True) +class Main(common.BuildCliFunction): + def __init__(self): + super().__init__( + description='''\ +Build crosstool-NG with Newlib for bare metal compilation +''', + supported_archs=common.consts['crosstool_ng_supported_archs'] + ) + + def build(self): + build_dir = self.get_build_dir() + defconfig_dest = os.path.join(self.env['crosstool_ng_util_dir'], 'defconfig') + os.makedirs(self.env['crosstool_ng_util_dir'], exist_ok=True) + os.makedirs(self.env['crosstool_ng_download_dir'], exist_ok=True) # Bootstrap out-ot-tree WONTFIX. I've tried. # https://github.com/crosstool-ng/crosstool-ng/issues/1021 - os.chdir(common.crosstool_ng_src_dir) - common.run_cmd( - [os.path.join(common.crosstool_ng_src_dir, 'bootstrap'), common.Newline], - ) - os.chdir(common.crosstool_ng_util_dir) - common.run_cmd( - [ - os.path.join(common.crosstool_ng_src_dir, 'configure'), common.Newline, - '--enable-local', common.Newline, - ], + os.chdir(self.env['crosstool_ng_source_dir']) + self.sh.run_cmd( + [os.path.join(self.env['crosstool_ng_source_dir'], 'bootstrap'), LF], ) - common.run_cmd( + os.chdir(self.env['crosstool_ng_util_dir']) + self.sh.run_cmd( [ - 'make', common.Newline, - '-j', str(args.nproc), common.Newline, + os.path.join(self.env['crosstool_ng_source_dir'], 'configure'), LF, + '--enable-local', LF, ], ) + self.sh.run_cmd(['make', '-j', str(self.env['nproc']), LF]) # Build the toolchain. - common.cp( - os.path.join(common.root_dir, 'crosstool_ng_config', args.arch), + self.sh.cp( + os.path.join(self.env['root_dir'], 'crosstool_ng_config', self.env['arch']), defconfig_dest ) - common.write_configs( - common.crosstool_ng_defconfig, + self.sh.write_configs( + self.env['crosstool_ng_defconfig'], [ - 'CT_PREFIX_DIR="{}"'.format(common.crosstool_ng_install_dir), + 'CT_PREFIX_DIR="{}"'.format(self.env['crosstool_ng_install_dir']), 'CT_WORK_DIR="{}"'.format(build_dir), - 'CT_LOCAL_TARBALLS_DIR="{}"'.format(common.crosstool_ng_download_dir), + 'CT_LOCAL_TARBALLS_DIR="{}"'.format(self.env['crosstool_ng_download_dir']), ] ) - common.run_cmd( + self.sh.run_cmd( [ - common.crosstool_ng_executable, common.Newline, - 'defconfig', common.Newline, + self.env['crosstool_ng_executable'], LF, + 'defconfig', LF, ], ) - os.unlink(defconfig_dest) - common.run_cmd( + self.sh.rmrf(defconfig_dest) + self.sh.run_cmd( [ - common.crosstool_ng_executable, common.Newline, - 'build', common.Newline, - 'CT_JOBS={}'.format(str(args.nproc)), common.Newline, + self.env['crosstool_ng_executable'], LF, + 'build', LF, + 'CT_JOBS={}'.format(str(self.env['nproc'])), LF, ], out_file=os.path.join(build_dir, 'lkmc.log'), delete_env=['LD_LIBRARY_PATH'], - extra_paths=[common.ccache_dir], + extra_paths=[self.env['ccache_dir']], ) - def get_argparse_args(self): - return { - 'description': '''\ -Build crosstool-NG with Newlib for bare metal compilation' -''' - } - - def get_build_dir(self, args): - return common.crosstool_ng_build_dir + def get_build_dir(self): + return self.env['crosstool_ng_build_dir'] if __name__ == '__main__': - CrosstoolNgComponent().build() + Main().cli() diff --git a/build-doc b/build-doc index 5d514f85..8e958ab7 100755 --- a/build-doc +++ b/build-doc @@ -1,2 +1,38 @@ -#!/usr/bin/env bash -asciidoctor -o out/README.html -v README.adoc +#!/usr/bin/env python3 + +import re + +import common +from shell_helpers import LF + +class Main(common.LkmcCliFunction): + def __init__(self): + super().__init__( + defaults = { + 'print_time': False, + }, + description='''\ +https://github.com/cirosantilli/linux-kernel-module-cheat#build-the-documentation +''', + ) + + def timed_main(self): + self.sh.run_cmd( + [ + 'asciidoctor', LF, + '-o', self.env['readme_out'], LF, + '-v', self.env['readme'], LF, + ], + out_file=self.env['build_doc_log'], + ) + error_re = re.compile('^asciidoctor: WARNING: ') + exit_status = 0 + with open(self.env['build_doc_log']) as f: + for line in f: + if error_re.search(line): + exit_status = 1 + break + return exit_status + +if __name__ == '__main__': + Main().cli() diff --git a/build-docker b/build-docker index 1882b566..6c2ef2cd 100755 --- a/build-docker +++ b/build-docker @@ -5,9 +5,10 @@ import subprocess import tarfile import common +from shell_helpers import LF -class DockerComponent(common.Component): +class DockerComponent(self.Component): def get_argparse_args(self): return { 'description': '''\ @@ -17,8 +18,8 @@ See also:https://github.com/cirosantilli/linux-kernel-module-cheat#ubuntu-guest- ''' } - def do_build(self, args): - build_dir = self.get_build_dir(args) + def build(self): + build_dir = self.get_build_dir() container_name = 'lkmc-guest' target_dir = os.path.join('/root', 'linux-kernel-module-cheat') os.makedirs(build_dir, exist_ok=True) @@ -29,12 +30,12 @@ See also:https://github.com/cirosantilli/linux-kernel-module-cheat#ubuntu-guest- '--format', '{{.Names}}', ]).decode() if container_name in containers.split(): - common.run_cmd([ + self.sh.run_cmd([ 'docker', 'rm', container_name, ]) - common.run_cmd([ + self.sh.run_cmd([ 'docker', 'create', '--name', container_name, @@ -44,36 +45,36 @@ See also:https://github.com/cirosantilli/linux-kernel-module-cheat#ubuntu-guest- '--privileged', '-t', '-w', target_dir, - '-v', '{}:{}'.format(common.root_dir, target_dir), + '-v', '{}:{}'.format(kwargs['root_dir'], target_dir), 'ubuntu:18.04', 'bash', ]) - common.run_cmd([ + self.sh.run_cmd([ 'docker', 'export', '-o', - common.docker_tar_file, + kwargs['docker_tar_file'], container_name, ]) - tar = tarfile.open(common.docker_tar_file) - tar.extractall(common.docker_tar_dir) + tar = tarfile.open(kwargs['docker_tar_file']) + tar.extractall(kwargs['docker_tar_dir']) tar.close() # sudo not required in theory # https://askubuntu.com/questions/1046828/how-to-run-libguestfs-tools-tools-such-as-virt-make-fs-without-sudo - common.run_cmd([ + self.sh.run_cmd([ 'virt-make-fs', '--format', 'raw', '--size', '+1G', '--type', 'ext2', - common.docker_tar_dir, - common.docker_rootfs_raw_file, + 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 common.docker_build_dir + def get_build_dir(self): + return kwargs['docker_build_dir'] def get_default_args(self): return {'docker': True} -DockerComponent().build() +Main().cli() diff --git a/build-gem5 b/build-gem5 index 4a89b61c..06e7bd83 100755 --- a/build-gem5 +++ b/build-gem5 @@ -5,110 +5,105 @@ import pathlib import subprocess import common +from shell_helpers import LF -class Gem5Component(common.Component): - def add_parser_arguments(self, parser): - parser.add_argument( +class Main(common.BuildCliFunction): + def __init__(self): + super().__init__() + self.add_argument( 'extra_scons_args', - default=[], metavar='extra-scons-args', - nargs='*' + nargs='*', ) - def do_build(self, args): - build_dir = self.get_build_dir(args) - binaries_dir = os.path.join(common.gem5_system_dir, 'binaries') - disks_dir = os.path.join(common.gem5_system_dir, 'disks') + def build(self): + build_dir = self.get_build_dir() + binaries_dir = os.path.join(self.env['gem5_system_dir'], 'binaries') + disks_dir = os.path.join(self.env['gem5_system_dir'], 'disks') os.makedirs(binaries_dir, exist_ok=True) os.makedirs(disks_dir, exist_ok=True) - if args.gem5_source_dir is None: - if not os.path.exists(os.path.join(common.gem5_src_dir, '.git')): - if common.gem5_src_dir == common.gem5_default_src_dir: + if self.env['gem5_source_dir'] is None: + if not os.path.exists(os.path.join(self.env['gem5_source_dir'], '.git')): + if self.env['gem5_source_dir'] == self.env['gem5_default_source_dir']: raise Exception('gem5 submodule not checked out') - common.run_cmd([ - 'git', common.Newline, - '-C', common.gem5_default_src_dir, common.Newline, - 'worktree', 'add', common.Newline, - '-b', os.path.join('wt', args.gem5_build_id), common.Newline, - common.gem5_src_dir, common.Newline, + self.sh.run_cmd([ + 'git', LF, + '-C', self.env['gem5_default_source_dir'], LF, + 'worktree', 'add', LF, + '-b', os.path.join('wt', self.env['gem5_build_id']), LF, + self.env['gem5_source_dir'], LF, ]) - if args.verbose: - verbose = ['--verbose', common.Newline] + if self.env['verbose']: + verbose = ['--verbose', LF] else: verbose = [] - if args.arch == 'x86_64': + if self.env['arch'] == 'x86_64': dummy_img_path = os.path.join(disks_dir, 'linux-bigswap2.img') with open(dummy_img_path, 'wb') as dummy_img_file: zeroes = b'\x00' * (2 ** 16) for i in range(2 ** 10): dummy_img_file.write(zeroes) - common.run_cmd(['mkswap', dummy_img_path]) + self.sh.run_cmd(['mkswap', dummy_img_path, LF]) with open(os.path.join(binaries_dir, 'x86_64-vmlinux-2.6.22.9'), 'w'): # This file must always be present, despite --kernel overriding that default and selecting the kernel. # I'm not even joking. No one has ever built x86 gem5 without the magic dist dir present. pass - elif args.arch == 'arm' or args.arch == 'aarch64': - gem5_system_src_dir = os.path.join(common.gem5_src_dir, 'system') + elif self.env['arch'] == 'arm' or self.env['arch'] == 'aarch64': + gem5_system_source_dir = os.path.join(self.env['gem5_source_dir'], 'system') # dtb - dt_src_dir = os.path.join(gem5_system_src_dir, 'arm', 'dt') - dt_build_dir = os.path.join(common.gem5_system_dir, 'arm', 'dt') - common.run_cmd([ - 'make', common.Newline, - '-C', dt_src_dir, common.Newline, - ]) - common.copy_dir_if_update_non_recursive( - srcdir=dt_src_dir, + dt_source_dir = os.path.join(gem5_system_source_dir, 'arm', 'dt') + dt_build_dir = os.path.join(self.env['gem5_system_dir'], 'arm', 'dt') + self.sh.run_cmd(['make', '-C', dt_source_dir, LF]) + self.sh.copy_dir_if_update_non_recursive( + srcdir=dt_source_dir, destdir=dt_build_dir, filter_ext='.dtb', ) # Bootloader 32. - bootloader32_dir = os.path.join(gem5_system_src_dir, 'arm', 'simple_bootloader') + bootloader32_dir = os.path.join(gem5_system_source_dir, 'arm', 'simple_bootloader') # TODO use the buildroot cross compiler here, and remove the dependencies from configure. - common.run_cmd([ - 'make', common.Newline, - '-C', bootloader32_dir, common.Newline, - 'CROSS_COMPILE=arm-linux-gnueabihf-', common.Newline, + self.sh.run_cmd([ + 'make', LF, + '-C', bootloader32_dir, LF, + 'CROSS_COMPILE=arm-linux-gnueabihf-', LF, ]) # bootloader - common.cp(os.path.join(bootloader32_dir, 'boot_emm.arm'), binaries_dir) + self.sh.cp(os.path.join(bootloader32_dir, 'boot_emm.arm'), binaries_dir) # Bootloader 64. - bootloader64_dir = os.path.join(gem5_system_src_dir, 'arm', 'aarch64_bootloader') + bootloader64_dir = os.path.join(gem5_system_source_dir, 'arm', 'aarch64_bootloader') # TODO cross_compile is ignored because the make does not use CC... - common.run_cmd([ - 'make', common.Newline, - '-C', bootloader64_dir, common.Newline - ]) - common.cp(os.path.join(bootloader64_dir, 'boot_emm.arm64'), binaries_dir) - common.run_cmd( + self.sh.run_cmd(['make', '-C', bootloader64_dir, LF]) + self.sh.cp(os.path.join(bootloader64_dir, 'boot_emm.arm64'), binaries_dir) + self.sh.run_cmd( ( [ - 'scons', common.Newline, - '-j', str(args.nproc), common.Newline, - '--gold-linker', common.Newline, - '--ignore-style', common.Newline, - common.gem5_executable, common.Newline, + 'scons', LF, + '-j', str(self.env['nproc']), LF, + '--gold-linker', LF, + '--ignore-style', LF, + self.env['gem5_executable'], LF, ] + verbose + - common.add_newlines(args.extra_scons_args) + self.sh.add_newlines(self.env['extra_scons_args']) ), - cwd=common.gem5_src_dir, - extra_paths=[common.ccache_dir], + cwd=self.env['gem5_source_dir'], + extra_paths=[self.env['ccache_dir']], ) - term_src_dir = os.path.join(common.gem5_src_dir, 'util/term') - m5term_build = os.path.join(term_src_dir, 'm5term') - common.run_cmd(['make', '-C', term_src_dir]) - if os.path.exists(common.gem5_m5term): - # Otherwise common.cp would fail with "Text file busy" if you + term_source_dir = os.path.join(self.env['gem5_source_dir'], 'util/term') + m5term_build = os.path.join(term_source_dir, 'm5term') + self.sh.run_cmd(['make', '-C', term_source_dir, LF]) + if os.path.exists(self.env['gem5_m5term']): + # Otherwise self.sh.cp would fail with "Text file busy" if you # tried to rebuild while running m5term: # https://stackoverflow.com/questions/16764946/what-generates-the-text-file-busy-message-in-unix/52427512#52427512 - os.unlink(common.gem5_m5term) - common.cp(m5term_build, common.gem5_m5term) + self.sh.rmrf(self.env['gem5_m5term']) + self.sh.cp(m5term_build, self.env['gem5_m5term']) - def get_build_dir(self, args): - return common.gem5_build_dir + def get_build_dir(self): + return self.env['gem5_build_dir'] if __name__ == '__main__': - Gem5Component().build() + Main().cli() diff --git a/build-linux b/build-linux index 8545684a..766aeb17 100755 --- a/build-linux +++ b/build-linux @@ -4,10 +4,16 @@ import os import shutil import common +from shell_helpers import LF -class LinuxComponent(common.Component): - def add_parser_arguments(self, parser): - parser.add_argument( +class Main(common.BuildCliFunction): + def __init__(self): + super().__init__( + description='''\ +Build the Linux kernel. +''' + ) + self.add_argument( '--config', default=[], action='append', help='''\ Add a single kernel config configs to the current build. Sample value: @@ -15,14 +21,20 @@ Add a single kernel config configs to the current build. Sample value: configs. Takes precedence over any config files. ''' ) - parser.add_argument( + self.add_argument( '--config-fragment', default=[], action='append', help='''\ Also use the given kernel configuration fragment file. Pass multiple times to use multiple fragment files. ''' ) - parser.add_argument( + self.add_argument( + '--config-only', default=False, + help='''\ +Configure the kernel, but don't build it. +''' + ) + self.add_argument( '--custom-config-file', help='''\ Ignore all default kernel configurations and use this file instead. @@ -30,137 +42,126 @@ Still uses options explicitly passed with `--config` and `--config-fragment` on top of it. ''' ) - parser.add_argument( - '--custom-config-file-gem5', default=False, action='store_true', + self.add_argument( + '--custom-config-file-gem5', default=False, help='''\ Use the gem5 Linux kernel fork config as the custom config file. Ignore --custom-config-file. ''' ) - parser.add_argument( - '--config-only', default=False, action='store_true', + self.add_argument( + '--modules-install', default=True, help='''\ -Configure the kernel, but don't build it. +Run `make modules_install` after `make`. ''' ) - parser.add_argument( - '--initramfs', default=False, action='store_true', - ) - parser.add_argument( - '--initrd', default=False, action='store_true', - ) - parser.add_argument( + self.add_argument( 'extra_make_args', default=[], metavar='extra-make-args', nargs='*' ) - def do_build(self, args): - build_dir = self.get_build_dir(args) - if args.initrd or args.initramfs: - raise Exception('just trolling, --initrd and --initramfs are broken for now') + def build(self): + build_dir = self.get_build_dir() os.makedirs(build_dir, exist_ok=True) tool = 'gcc' - gcc = common.get_toolchain_tool(tool) + gcc = self.get_toolchain_tool(tool) prefix = gcc[:-len(tool)] common_args = { - 'cwd': common.linux_source_dir, + 'cwd': self.env['linux_source_dir'], } ccache = shutil.which('ccache') if ccache is not None: cc = '{} {}'.format(ccache, gcc) else: cc = gcc - if args.verbose: + if self.env['verbose']: verbose = ['V=1'] else: verbose = [] common_make_args = [ - 'make', common.Newline, - '-j', str(args.nproc), common.Newline, - 'ARCH={}'.format(common.linux_arch), common.Newline, - 'CROSS_COMPILE={}'.format(prefix), common.Newline, - 'CC={}'.format(cc), common.Newline, - 'O={}'.format(build_dir), common.Newline, + 'make', LF, + '-j', str(self.env['nproc']), LF, + 'ARCH={}'.format(self.env['linux_arch']), LF, + 'CROSS_COMPILE={}'.format(prefix), LF, + 'CC={}'.format(cc), LF, + 'O={}'.format(build_dir), LF, ] + verbose - if args.custom_config_file_gem5: - custom_config_file = os.path.join(common.linux_source_dir, 'arch', common.linux_arch, 'configs', 'gem5_defconfig') + if self.env['custom_config_file_gem5']: + custom_config_file = os.path.join(self.env['linux_source_dir'], 'arch', self.env['linux_arch'], 'configs', 'gem5_defconfig') else: - custom_config_file = args.custom_config_file + custom_config_file = self.env['custom_config_file'] if custom_config_file is not None: if not os.path.exists(custom_config_file): - raise Exception('config fragment file does not exist: {}'.format(args.custom_config_file)) + raise Exception('config fragment file does not exist: {}'.format(custom_config_file)) base_config_file = custom_config_file config_fragments = [] else: - base_config_file = os.path.join(common.linux_config_dir, 'buildroot-{}'.format(args.arch)) + base_config_file = os.path.join(self.env['linux_config_dir'], 'buildroot-{}'.format(self.env['arch'])) config_fragments = ['min', 'default'] for i, config_fragment in enumerate(config_fragments): - config_fragments[i] = os.path.join(common.linux_config_dir, config_fragment) - config_fragments.extend(args.config_fragment) - if args.config != []: + config_fragments[i] = os.path.join(self.env['linux_config_dir'], config_fragment) + config_fragments.extend(self.env['config_fragment']) + cli_configs = self.env['config'] + if self.env['initramfs']: + cli_configs.append('CONFIG_INITRAMFS_SOURCE="{}"'.format(self.env['buildroot_cpio'])) + if cli_configs: cli_config_fragment_path = os.path.join(build_dir, 'lkmc_cli_config_fragment') - cli_config_str = '\n'.join(args.config) - common.write_string_to_file(cli_config_fragment_path, cli_config_str) + self.sh.write_configs(cli_config_fragment_path, cli_configs, mode='w') config_fragments.append(cli_config_fragment_path) - common.cp( + self.sh.cp( base_config_file, os.path.join(build_dir, '.config'), ) - common.run_cmd( + self.sh.run_cmd( [ - os.path.join(common.linux_source_dir, 'scripts', 'kconfig', 'merge_config.sh'), common.Newline, - '-m', common.Newline, - '-O', build_dir, common.Newline, - os.path.join(build_dir, '.config'), common.Newline, + os.path.join(self.env['linux_source_dir'], 'scripts', 'kconfig', 'merge_config.sh'), LF, + '-m', LF, + '-O', build_dir, LF, + os.path.join(build_dir, '.config'), LF, ] + - common.add_newlines(config_fragments) + self.sh.add_newlines(config_fragments) ) - common.run_cmd( + self.sh.run_cmd( ( common_make_args + - ['olddefconfig', common.Newline] + ['olddefconfig', LF] ), **common_args ) - if not args.config_only: - common.run_cmd( + if not self.env['config_only']: + self.sh.run_cmd( ( common_make_args + - common.add_newlines(args.extra_make_args) + self.sh.add_newlines(self.env['extra_make_args']) ), + # https://github.com/cirosantilli/linux-kernel-module-cheat#proc-version extra_env={ 'KBUILD_BUILD_VERSION': '1', 'KBUILD_BUILD_TIMESTAMP': 'Thu Jan 1 00:00:00 UTC 1970', 'KBUILD_BUILD_USER': 'lkmc', - 'KBUILD_BUILD_HOST': common.git_sha(common.linux_source_dir), + 'KBUILD_BUILD_HOST': common.git_sha(self.env['linux_source_dir']), }, **common_args ) - common.run_cmd( - ( - common_make_args + - [ - 'INSTALL_MOD_PATH={}'.format(common.out_rootfs_overlay_dir), common.Newline, - 'modules_install', common.Newline, - ] - ), - **common_args - ) - # 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() - - def get_argparse_args(self): - return { - 'description': '''\ -Build the Linux kernel. -''' - } + if self.env['modules_install']: + self.sh.run_cmd( + ( + common_make_args + + [ + 'INSTALL_MOD_PATH={}'.format(self.env['out_rootfs_overlay_dir']), LF, + 'modules_install', LF, + ] + ), + **common_args + ) + # 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. + # self.rmrf() - def get_build_dir(self, args): - return common.linux_build_dir + def get_build_dir(self): + return self.env['linux_build_dir'] if __name__ == '__main__': - LinuxComponent().build() + Main().cli() diff --git a/build-m5 b/build-m5 index 18c7ee2e..5713e36d 100755 --- a/build-m5 +++ b/build-m5 @@ -3,42 +3,47 @@ import os import common +from shell_helpers import LF -class M5Component(common.Component): - def get_make_cmd(self, args): +class Main(common.BuildCliFunction): + def __init__(self): + super().__init__() + + def _get_make_cmd(self): allowed_toolchains = ['buildroot'] - cc = common.get_toolchain_tool('gcc', allowed_toolchains=allowed_toolchains) - ld = common.get_toolchain_tool('ld', allowed_toolchains=allowed_toolchains) - if args.arch == 'x86_64': + cc = self.get_toolchain_tool('gcc', allowed_toolchains=allowed_toolchains) + ld = self.get_toolchain_tool('ld', allowed_toolchains=allowed_toolchains) + if self.env['arch'] == 'x86_64': arch = 'x86' else: - arch = args.arch + arch = self.env['arch'] return [ - 'make', common.Newline, - '-j', str(args.nproc), common.Newline, - '-f', 'Makefile.{}'.format(arch), common.Newline, - 'CC={}'.format(cc), common.Newline, - 'LD={}'.format(ld), common.Newline, - 'PWD={}'.format(common.gem5_m5_src_dir), common.Newline, + 'make', LF, + '-j', str(self.env['nproc']), LF, + '-f', 'Makefile.{}'.format(arch), LF, + 'CC={}'.format(cc), LF, + 'LD={}'.format(ld), LF, + 'PWD={}'.format(self.env['gem5_m5_source_dir']), LF, ] - def do_build(self, args): - os.makedirs(common.gem5_m5_build_dir, exist_ok=True) + def build(self): + os.makedirs(self.env['gem5_m5_build_dir'], exist_ok=True) # We must clean first or else the build outputs of one arch can conflict with the other. # I should stop being lazy and go actually patch gem5 to support out of tree m5 build... - self.clean(args) - common.run_cmd( - self.get_make_cmd(args), - cwd=common.gem5_m5_src_dir, + self.clean() + self.sh.run_cmd( + self._get_make_cmd(), + cwd=self.env['gem5_m5_source_dir'], ) - os.makedirs(common.out_rootfs_overlay_bin_dir, exist_ok=True) - common.cp(os.path.join(common.gem5_m5_src_dir, 'm5'), common.out_rootfs_overlay_bin_dir) + os.makedirs(self.env['out_rootfs_overlay_bin_dir'], exist_ok=True) + self.sh.cp(os.path.join(self.env['gem5_m5_source_dir'], 'm5'), self.env['out_rootfs_overlay_bin_dir']) - def clean(self, args): - common.run_cmd( - self.get_make_cmd(args) + ['clean', common.Newline], - cwd=common.gem5_m5_src_dir, + def clean(self): + self.sh.run_cmd( + self._get_make_cmd() + ['clean', LF], + cwd=self.env['gem5_m5_source_dir'], ) + return None if __name__ == '__main__': - M5Component().build() + Main().cli() diff --git a/build-modules b/build-modules index 73432e5f..439ea001 100755 --- a/build-modules +++ b/build-modules @@ -6,32 +6,37 @@ import platform import shutil import common +from shell_helpers import LF -class ModulesComponent(common.Component): - def add_parser_arguments(self, parser): - parser.add_argument( +class Main(common.BuildCliFunction): + def __init__(self): + super().__init__( + description='''\ +Build our Linux kernel modules without using Buildroot. + +See also: https://github.com/cirosantilli/linux-kernel-module-cheat#host +''') + self.add_argument( '--make-args', default='', ) - parser.add_argument( + self.add_argument( '--host', - action='store_true', default=False, help='''\ Build the Linux kernel modules for the host instead of guest. Use the host packaged cross toolchain. ''', ) - parser.add_argument( - 'kernel_modules', + self.add_argument( + 'kernel-modules', default=[], help='Which kernel modules to build. Default: build all', - metavar='kernel-modules', nargs='*', ) - def do_build(self, args): - build_dir = self.get_build_dir(args) + def build(self): + build_dir = self.get_build_dir() os.makedirs(build_dir, exist_ok=True) # I kid you not, out-of-tree build is not possible, O= does not work as for the kernel build: # @@ -42,87 +47,78 @@ Use the host packaged cross toolchain. # This copies only modified files as per: # https://stackoverflow.com/questions/5718899/building-an-out-of-tree-linux-kernel-module-in-a-separate-object-directory distutils.dir_util.copy_tree( - common.kernel_modules_src_dir, - os.path.join(build_dir, common.kernel_modules_subdir), + self.env['kernel_modules_source_dir'], + os.path.join(build_dir, self.env['kernel_modules_subdir']), update=1, ) distutils.dir_util.copy_tree( - common.include_src_dir, - os.path.join(build_dir, common.include_subdir), + self.env['include_source_dir'], + os.path.join(build_dir, self.env['include_subdir']), update=1, ) all_kernel_modules = [] - for basename in os.listdir(common.kernel_modules_src_dir): - src = os.path.join(common.kernel_modules_src_dir, basename) + for basename in os.listdir(self.env['kernel_modules_source_dir']): + src = os.path.join(self.env['kernel_modules_source_dir'], basename) if os.path.isfile(src): noext, ext = os.path.splitext(basename) - if ext == common.c_ext: + if ext == self.env['c_ext']: all_kernel_modules.append(noext) - if args.kernel_modules == []: + if self.env['kernel_modules'] == []: kernel_modules = all_kernel_modules else: - kernel_modules = map(lambda x: os.path.splitext(os.path.split(x)[1])[0], args.kernel_modules) - object_files = map(lambda x: x + common.obj_ext, kernel_modules) + kernel_modules = map(lambda x: os.path.splitext(os.path.split(x)[1])[0], self.env['kernel_modules']) + object_files = map(lambda x: x + self.env['obj_ext'], kernel_modules) tool = 'gcc' - if args.host: + if self.env['host']: allowed_toolchains = ['host'] - build_subdir = common.kernel_modules_build_host_subdir + build_subdir = self.env['kernel_modules_build_host_subdir'] else: allowed_toolchains = None - build_subdir = common.kernel_modules_build_subdir - gcc = common.get_toolchain_tool(tool, allowed_toolchains=allowed_toolchains) + build_subdir = self.env['kernel_modules_build_subdir'] + gcc = self.get_toolchain_tool(tool, allowed_toolchains=allowed_toolchains) prefix = gcc[:-len(tool)] ccache = shutil.which('ccache') if ccache is not None: cc = '{} {}'.format(ccache, gcc) else: cc = gcc - if args.verbose: + if self.env['verbose']: verbose = ['V=1'] else: verbose = [] - if args.host: + if self.env['host']: linux_dir = os.path.join('/lib', 'modules', platform.uname().release, 'build') else: - linux_dir = common.linux_build_dir - common.run_cmd( + linux_dir = self.env['linux_build_dir'] + self.sh.run_cmd( ( [ - 'make', common.Newline, - '-j', str(args.nproc), common.Newline, - 'ARCH={}'.format(common.linux_arch), common.Newline, - 'CC={}'.format(cc), common.Newline, - 'CROSS_COMPILE={}'.format(prefix), common.Newline, - 'LINUX_DIR={}'.format(linux_dir), common.Newline, - 'M={}'.format(build_subdir), common.Newline, - 'OBJECT_FILES={}'.format(' '.join(object_files)), common.Newline, + 'make', LF, + '-j', str(self.env['nproc']), LF, + 'ARCH={}'.format(self.env['linux_arch']), LF, + 'CC={}'.format(cc), LF, + 'CROSS_COMPILE={}'.format(prefix), LF, + 'LINUX_DIR={}'.format(linux_dir), LF, + 'M={}'.format(build_subdir), LF, + 'OBJECT_FILES={}'.format(' '.join(object_files)), LF, ] + - common.shlex_split(args.make_args) + + self.sh.shlex_split(self.env['make_args']) + verbose ), - cwd=os.path.join(common.kernel_modules_build_subdir), + cwd=os.path.join(self.env['kernel_modules_build_subdir']), ) - if not args.host: - common.copy_dir_if_update_non_recursive( - srcdir=common.kernel_modules_build_subdir, - destdir=common.out_rootfs_overlay_dir, - filter_ext=common.kernel_module_ext, + if not self.env['host']: + self.sh.copy_dir_if_update_non_recursive( + srcdir=self.env['kernel_modules_build_subdir'], + destdir=self.env['out_rootfs_overlay_dir'], + filter_ext=self.env['kernel_module_ext'], ) - def get_argparse_args(self): - return { - 'description': '''\ -Build our Linux kernel modules without using Buildroot. - -See also: https://github.com/cirosantilli/linux-kernel-module-cheat#host -''' - } - - def get_build_dir(self, args): - if args.host: - return os.path.join(common.kernel_modules_build_host_dir) + def get_build_dir(self): + if self.env['host']: + return self.env['kernel_modules_build_host_dir'] else: - return os.path.join(common.kernel_modules_build_dir) + return self.env['kernel_modules_build_dir'] if __name__ == '__main__': - ModulesComponent().build() + Main().cli() diff --git a/build-qemu b/build-qemu index 5299d07f..06ee1680 100755 --- a/build-qemu +++ b/build-qemu @@ -3,61 +3,62 @@ import os import common +from shell_helpers import LF -class QemuComponent(common.Component): - def add_parser_arguments(self, parser): - parser.add_argument( - '--userland', +class Main(common.BuildCliFunction): + def __init__(self): + super().__init__() + self.add_argument( + '--user-mode', default=False, - action='store_true', help='Build QEMU user mode instead of system.', ) - parser.add_argument( + self.add_argument( 'extra_config_args', default=[], metavar='extra-config-args', nargs='*' ) - def do_build(self, args): - build_dir = self.get_build_dir(args) + def build(self): + build_dir = self.get_build_dir() os.makedirs(build_dir, exist_ok=True) - if args.verbose: + if self.env['verbose']: verbose = ['V=1'] else: verbose = [] - if args.userland: - target_list = '{}-linux-user'.format(args.arch) + if self.env['user_mode']: + target_list = '{}-linux-user'.format(self.env['arch']) else: - target_list = '{}-softmmu'.format(args.arch) - common.run_cmd( + target_list = '{}-softmmu'.format(self.env['arch']) + self.sh.run_cmd( [ - os.path.join(common.qemu_src_dir, 'configure'), common.Newline, - '--enable-debug', common.Newline, - '--enable-trace-backends=simple', common.Newline, - '--target-list={}'.format(target_list), common.Newline, - '--enable-sdl', common.Newline, - '--with-sdlabi=2.0', common.Newline, + os.path.join(self.env['qemu_source_dir'], 'configure'), LF, + '--enable-debug', LF, + '--enable-trace-backends=simple', LF, + '--target-list={}'.format(target_list), LF, + '--enable-sdl', LF, + '--with-sdlabi=2.0', LF, ] + - common.add_newlines(args.extra_config_args), - extra_paths=[common.ccache_dir], + self.sh.add_newlines(self.env['extra_config_args']), + extra_paths=[self.env['ccache_dir']], cwd=build_dir ) - common.run_cmd( + self.sh.run_cmd( ( [ - 'make', common.Newline, - '-j', str(args.nproc), common.Newline, + 'make', LF, + '-j', str(self.env['nproc']), LF, ] + verbose ), cwd=build_dir, - extra_paths=[common.ccache_dir], + extra_paths=[self.env['ccache_dir']], ) - def get_build_dir(self, args): - return common.qemu_build_dir + def get_build_dir(self): + return self.env['qemu_build_dir'] if __name__ == '__main__': - QemuComponent().build() + Main().cli() diff --git a/build-test b/build-test index 552770a1..5a8eafbf 100755 --- a/build-test +++ b/build-test @@ -1,4 +1,6 @@ #!/usr/bin/env bash +# Build just enough to run ./test: +# https://github.com/cirosantilli/linux-kernel-module-cheat#automated-tests set -eu test_size=1 while [ $# -gt 0 ]; do @@ -9,5 +11,5 @@ while [ $# -gt 0 ]; do ;; esac done -./build-bench-boot --size "$test_size" -./build-test-gdb +./build-test-boot --size "$test_size" +./build --all-archs test-gdb test-user-mode diff --git a/build-test-boot b/build-test-boot new file mode 100755 index 00000000..2cc5d9a4 --- /dev/null +++ b/build-test-boot @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# We want to move this into ./build. The only reason we haven't is that +# what to build depends on --size, which ./build does not support right now. +# The best way to solve this is to move the dependency checking into the run +# scripts, which will take a while to refactor. +set -eu +test_size=1 +while [ $# -gt 0 ]; do + case "$1" in + --size) + test_size="$2" + shift 2 + ;; + esac +done +./build --all-archs qemu-gem5-buildroot +if [ "$test_size" -ge 3 ]; then + ./build-gem5 --arch aarch64 --gem5-build-type debug + ./build-gem5 --arch aarch64 --gem5-build-type fast +fi diff --git a/build-test-gdb b/build-test-gdb deleted file mode 100755 index 1fcb056b..00000000 --- a/build-test-gdb +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env bash -./build --all-archs all-baremetal diff --git a/build-userland b/build-userland index 3a7e5277..37a1dfa8 100755 --- a/build-userland +++ b/build-userland @@ -1,16 +1,19 @@ #!/usr/bin/env python3 import os -import platform import shlex -import shutil -import subprocess import common +from shell_helpers import LF -class UserlandComponent(common.Component): - def add_parser_arguments(self, parser): - parser.add_argument( +class Main(common.BuildCliFunction): + def __init__(self): + super().__init__( + description='''\ +Build our compiled userland examples. +''' + ) + self.add_argument( '--has-package', action='append', default=[], @@ -19,20 +22,27 @@ Indicate that a given package is present in the root filesystem, which allows us to build examples that rely on it. ''', ) - parser.add_argument( + self.add_argument( '--host', - action='store_true', default=False, help='''\ Build the userland programs for the host instead of guest. Use the host packaged cross toolchain. ''', ) - parser.add_argument( + self.add_argument( '--make-args', default='', ) - parser.add_argument( + self.add_argument( + '--static', + default=False, + help='''\ +Build the executables statically. TODO not implemented: Set the build id to 'static' +if one was not given explicitly. +''', + ) + self.add_argument( 'targets', default=[], help='''\ @@ -44,49 +54,47 @@ has the OpenBLAS libraries and headers installed. nargs='*', ) - def do_build(self, args): - build_dir = self.get_build_dir(args) + def build(self): + build_dir = self.get_build_dir() os.makedirs(build_dir, exist_ok=True) - if args.host: + if self.env['host']: 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) - common.run_cmd( + cc = self.get_toolchain_tool('gcc', allowed_toolchains=allowed_toolchains) + cxx = self.get_toolchain_tool('g++', allowed_toolchains=allowed_toolchains) + make_args = shlex.split(self.env['make_args']) + if self.env['static']: + make_args.extend(['CCFLAGS_EXTRA=-static', LF]) + self.sh.run_cmd( ( [ - 'make', common.Newline, - '-j', str(args.nproc), common.Newline, - 'ARCH={}'.format(args.arch), common.Newline, - 'CCFLAGS_SCRIPT={} {}'.format('-I', common.userland_src_dir), common.Newline, - 'COMMON_DIR={}'.format(common.root_dir), common.Newline, - 'CC={}'.format(cc), common.Newline, - 'CXX={}'.format(cxx), common.Newline, - 'PKG_CONFIG={}'.format(common.buildroot_pkg_config), common.Newline, - 'STAGING_DIR={}'.format(common.buildroot_staging_dir), common.Newline, - 'OUT_DIR={}'.format(build_dir), common.Newline, + 'make', LF, + '-j', str(self.env['nproc']), LF, + 'ARCH={}'.format(self.env['arch']), LF, + 'CCFLAGS_SCRIPT={} {}'.format('-I', self.env['userland_source_dir']), LF, + 'COMMON_DIR={}'.format(self.env['root_dir']), LF, + 'CC={}'.format(cc), LF, + 'CXX={}'.format(cxx), LF, + 'PKG_CONFIG={}'.format(self.env['buildroot_pkg_config']), LF, + 'STAGING_DIR={}'.format(self.env['buildroot_staging_dir']), LF, + 'OUT_DIR={}'.format(build_dir), LF, ] + - common.add_newlines(['HAS_{}=y'.format(package.upper()) for package in args.has_package]) + - shlex.split(args.make_args) + - common.add_newlines([os.path.join(build_dir, os.path.splitext(os.path.split(target)[1])[0]) + common.userland_build_ext for target in args.targets]) + self.sh.add_newlines(['HAS_{}=y'.format(package.upper()) for package in self.env['has_package']]) + + make_args + + self.sh.add_newlines([os.path.join(build_dir, os.path.splitext(os.path.split(target)[1])[0]) + self.env['userland_build_ext'] for target in self.env['targets']]) ), - cwd=common.userland_src_dir, - extra_paths=[common.ccache_dir], + cwd=self.env['userland_source_dir'], + extra_paths=[self.env['ccache_dir']], ) - common.copy_dir_if_update_non_recursive( + self.sh.copy_dir_if_update_non_recursive( srcdir=build_dir, - destdir=common.out_rootfs_overlay_dir, - filter_ext=common.userland_build_ext, + destdir=self.env['out_rootfs_overlay_dir'], + filter_ext=self.env['userland_build_ext'], ) - def get_argparse_args(self): - return { - 'description': 'Build our compiled userland examples', - } - - def get_build_dir(self, args): - return common.userland_build_dir + def get_build_dir(self): + return self.env['userland_build_dir'] if __name__ == '__main__': - UserlandComponent().build() + Main().cli() diff --git a/build-xen b/build-xen new file mode 100755 index 00000000..f57e2fb4 --- /dev/null +++ b/build-xen @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# TODO get working, aarch64 Xen integration attempt. +# Current state: prints to Boot-wrapper v0.2 to screen and hangs. +# Bibliography: +# https://wiki.xenproject.org/wiki/Xen_ARM_with_Virtualization_Extensions/qemu-system-aarch64 +# https://blog.xenproject.org/2014/04/01/virtualization-on-arm-with-xen/ +cd submodules/xen +make \ + -j`nproc` \ + dist-xen \ + CONFIG_DEBUG=y \ + CONFIG_EARLY_PRINTK=vexpress \ + CROSS_COMPILE=aarch64-linux-gnu- \ + XEN_TARGET_ARCH=arm64 \ +; +cd ../boot-wraper-aarch64 +autoreconf -i +# DTB dumped from QEMU with: -machine dumpdtb=dtb.dtb +./configure \ + --enable-gicv3 \ + --enable-psci \ + --host=aarch64-linux-gnu \ + --with-cmdline="console=hvc0 root=/dev/vda rw mem=1G" \ + --with-dtb=dtb.dtb \ + --with-kernel-dir=../../out/linux/default/aarch64 \ + --with-xen-cmdline="dtuart=/uart@1c090000 console=dtuart no-bootscrub dom0_mem=1G loglvl=all guest_loglvl=all" \ + --with-xen=../xen/xen/xen \ +; +dtb.dtb -j`nproc` +../../out/qemu/default/aarch64-softmmu/qemu-system-aarch64 \ + -M virt \ + -M virtualization=on \ + -cpu cortex-a57 \ + -kernel xen-system.axf \ + -serial mon:stdio \ + -nographic \ +; diff --git a/buildroot_config/default b/buildroot_config/default index 4764505a..e5b5b4ca 100644 --- a/buildroot_config/default +++ b/buildroot_config/default @@ -23,10 +23,6 @@ BR2_TARGET_ROOTFS_INITRAMFS=n BR2_TARGET_ROOTFS_TAR=n # Host GDB -BR2_GDB_VERSION="7.11.1" -BR2_GDB_VERSION_7_10=n -BR2_GDB_VERSION_7_11=y -BR2_GDB_VERSION_7_12=n BR2_PACKAGE_HOST_GDB=y BR2_PACKAGE_HOST_GDB_PYTHON=y BR2_PACKAGE_HOST_GDB_SIM=y diff --git a/cli_function.py b/cli_function.py new file mode 100755 index 00000000..25bc7d24 --- /dev/null +++ b/cli_function.py @@ -0,0 +1,450 @@ +#!/usr/bin/env python3 + +import argparse +import bisect +import collections +import imp +import os +import sys + +class _Argument: + def __init__( + self, + long_or_short_1, + long_or_short_2=None, + default=None, + dest=None, + help=None, + nargs=None, + **kwargs + ): + self.args = [] + # argparse is crappy and cannot tell us if arguments were given or not. + # We need that information to decide if the config file should override argparse or not. + # So we just use None as a sentinel. + self.kwargs = {'default': None} + shortname, longname, key, is_option = self.get_key( + long_or_short_1, + long_or_short_2, + dest + ) + if shortname is not None: + self.args.append(shortname) + if is_option: + self.args.append(longname) + else: + self.args.append(key) + self.kwargs['metavar'] = longname + if default is not None and nargs is None: + self.kwargs['nargs'] = '?' + if dest is not None: + self.kwargs['dest'] = dest + if nargs is not None: + self.kwargs['nargs'] = nargs + if default is True or default is False: + bool_action = 'store_true' + self.is_bool = True + else: + self.is_bool = False + if default is None and ( + nargs in ('*', '+') + or ('action' in kwargs and kwargs['action'] == 'append') + ): + default = [] + if self.is_bool and not 'action' in kwargs: + self.kwargs['action'] = bool_action + if help is not None: + if default is not None: + if help[-1] == '\n': + if '\n\n' in help[:-1]: + help += '\n' + elif help[-1] == ' ': + pass + else: + help += ' ' + help += 'Default: {}'.format(default) + self.kwargs['help'] = help + self.optional = ( + default is not None or + self.is_bool or + is_option or + nargs in ('?', '*', '+') + ) + self.kwargs.update(kwargs) + self.default = default + self.longname = longname + self.key = key + self.is_option = is_option + self.nargs = nargs + + def __str__(self): + return str(self.args) + ' ' + str(self.kwargs) + + @staticmethod + def get_key( + long_or_short_1, + long_or_short_2=None, + dest=None, + **kwargs + ): + if long_or_short_2 is None: + shortname = None + longname = long_or_short_1 + else: + shortname = long_or_short_1 + longname = long_or_short_2 + if longname[0] == '-': + key = longname.lstrip('-').replace('-', '_') + is_option = True + else: + key = longname.replace('-', '_') + is_option = False + if dest is not None: + key = dest + return shortname, longname, key, is_option + +class CliFunction: + ''' + Represent a function that can be called either from Python code, or + from the command line. + + Features: + + * single argument description in format very similar to argparse + * handle default arguments transparently in both cases + * expose a configuration file mechanism to get default parameters from a file + * fix some argparse.ArgumentParser() annoyances: + ** allow dashes in positional arguments: + https://stackoverflow.com/questions/12834785/having-options-in-argparse-with-a-dash + ** boolean defaults automatically use store_true or store_false, and add a --no-* CLI + option to invert them if set from the config + * from a Python call, get the corresponding CLI string list. See get_cli. + * easily determine if arguments were given on the command line + https://stackoverflow.com/questions/30487767/check-if-argparse-optional-argument-is-set-or-not/30491369 + + This somewhat duplicates: https://click.palletsprojects.com but: + + * that decorator API is insane + * CLI + Python for single functions was wontfixed: https://github.com/pallets/click/issues/40 + ''' + def __call__(self, **kwargs): + ''' + Python version of the function call. Not called by cli() indirectly, + so can be overridden to distinguish between Python and CLI calls. + + :type arguments: Dict + ''' + return self._do_main(kwargs) + + def _do_main(self, kwargs): + return self.main(**self._get_args(kwargs)) + + def __init__(self, config_file=None, description=None, extra_config_params=None): + self._arguments = collections.OrderedDict() + self._config_file = config_file + self._description = description + self.extra_config_params = extra_config_params + if self._config_file is not None: + self.add_argument( + '--config-file', + default=self._config_file, + help='Path to the configuration file to use' + ) + + def __str__(self): + return '\n'.join(str(arg[key]) for key in self._arguments) + + def _get_args(self, kwargs): + ''' + Resolve default arguments from the config file and CLI param defaults. + + Add an extra _args_given argument which determines if an argument was given or not. + Args set from the config file count as given. + ''' + args_with_defaults = kwargs.copy() + # Add missing args from config file. + config_file = None + if 'config_file' in args_with_defaults and args_with_defaults['config_file'] is not None: + config_file = args_with_defaults['config_file'] + else: + config_file = self._config_file + args_given = {} + for key in self._arguments: + args_given[key] = not ( + not key in args_with_defaults or + args_with_defaults[key] is None or + self._arguments[key].nargs == '*' and args_with_defaults[key] == [] + ) + if config_file is not None and os.path.exists(config_file): + config_configs = {} + config = imp.load_source('config', config_file) + if self.extra_config_params is None: + config.set_args(config_configs) + else: + config.set_args(config_configs, self.extra_config_params) + for key in config_configs: + if key not in self._arguments: + raise Exception('Unknown key in config file: ' + key) + if not args_given[key]: + args_with_defaults[key] = config_configs[key] + args_given[key] = True + # Add missing args from hard-coded defaults. + for key in self._arguments: + argument = self._arguments[key] + if (not key in args_with_defaults) or args_with_defaults[key] is None: + if argument.optional: + args_with_defaults[key] = argument.default + else: + raise Exception('Value not given for mandatory argument: ' + key) + args_with_defaults['_args_given'] = args_given + if 'config_file' in args_with_defaults: + del args_with_defaults['config_file'] + return args_with_defaults + + def add_argument( + self, + *args, + **kwargs + ): + argument = _Argument(*args, **kwargs) + self._arguments[argument.key] = argument + + def cli_noexit(self, cli_args=None): + ''' + Call the function from the CLI. Parse command line arguments + to get all arguments. + + :return: the return of main + ''' + parser = argparse.ArgumentParser( + description=self._description, + formatter_class=argparse.RawTextHelpFormatter, + ) + for key in self._arguments: + argument = self._arguments[key] + parser.add_argument(*argument.args, **argument.kwargs) + if argument.is_bool: + new_longname = '--no' + argument.longname[1:] + kwargs = argument.kwargs.copy() + kwargs['default'] = not argument.default + if kwargs['action'] in ('store_true', 'store_false'): + kwargs['action'] = 'store_false' + if 'help' in kwargs: + del kwargs['help'] + parser.add_argument(new_longname, dest=argument.key, **kwargs) + args = parser.parse_args(args=cli_args) + return self._do_main(vars(args)) + + def cli(self, *args, **kwargs): + ''' + Same as cli, but also exit the program with status equal to the return value of main. + main must return an integer for this to be used. + + None is considered 0. + ''' + exit_status = self.cli_noexit(*args, **kwargs) + if exit_status is None: + exit_status = 0 + sys.exit(exit_status) + + def get_cli(self, **kwargs): + ''' + :rtype: List[Type(str)] + :return: the canonical command line arguments arguments that would + generate this Python function call. + + (--key, value) option pairs are grouped into tuples, and all + other values are grouped in their own tuple (positional_arg,) + or (--bool-arg,). + + Arguments with default values are not added, but arguments + that are set by the config are also given. + + The optional arguments are sorted alphabetically, followed by + positional arguments. + + The long option name is used if both long and short versions + are given. + ''' + options = [] + positional_dict = {} + kwargs = self._get_args(kwargs) + for key in kwargs: + if not key in ('_args_given',): + argument = self._arguments[key] + default = argument.default + value = kwargs[key] + if value != default: + if argument.is_option: + if argument.is_bool: + vals = [(argument.longname,)] + elif 'action' in argument.kwargs and argument.kwargs['action'] == 'append': + vals = [(argument.longname, str(val)) for val in value] + else: + vals = [(argument.longname, str(value))] + for val in vals: + bisect.insort(options, val) + else: + if type(value) is list: + positional_dict[key] = [tuple([v]) for v in value] + else: + positional_dict[key] = [(str(value),)] + # Python built-in data structures suck. + # https://stackoverflow.com/questions/27726245/getting-the-key-index-in-a-python-ordereddict/27726534#27726534 + positional = [] + for key in self._arguments.keys(): + if key in positional_dict: + positional.extend(positional_dict[key]) + return options + positional + + @staticmethod + def get_key(*args, **kwargs): + return _Argument.get_key(*args, **kwargs) + + def main(self, **kwargs): + ''' + Do the main function call work. + + :type arguments: Dict + ''' + raise NotImplementedError + +if __name__ == '__main__': + class OneCliFunction(CliFunction): + def __init__(self): + super().__init__( + config_file='cli_function_test_config.py', + description = '''\ +Description of this +amazing function! +''', + ) + self.add_argument('-a', '--asdf', default='A', help='Help for asdf'), + self.add_argument('-q', '--qwer', default='Q', help='Help for qwer'), + self.add_argument('-b', '--bool-true', default=True, help='Help for bool-true'), + self.add_argument('--bool-false', default=False, help='Help for bool-false'), + self.add_argument('--dest', dest='custom_dest', help='Help for dest'), + 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('--append', action='append') + 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), + self.add_argument('args-star', help='Help for args-star', nargs='*'), + + def main(self, **kwargs): + del kwargs['_args_given'] + return kwargs + + one_cli_function = OneCliFunction() + + # Default code call. + default = one_cli_function(pos_mandatory=1) + assert default == { + 'asdf': 'A', + 'qwer': 'Q', + 'bool_true': True, + 'bool_false': False, + 'bool_nargs': False, + 'bool_cli': True, + 'custom_dest': None, + 'no_default': None, + 'append': [], + 'pos_mandatory': 1, + 'pos_optional': 0, + 'args_star': [] + } + + # Default CLI call with programmatic CLI arguments. + out = one_cli_function.cli_noexit(['1']) + assert out == default + + # asdf + out = one_cli_function(pos_mandatory=1, asdf='B') + assert out['asdf'] == 'B' + out['asdf'] = default['asdf'] + assert out == default + + # asdf and qwer + 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 + + if '--bool-true': + out = one_cli_function(pos_mandatory=1, bool_true=False) + cli_out = one_cli_function.cli_noexit(['--no-bool-true', '1']) + assert out == cli_out + assert out['bool_true'] == False + out['bool_true'] = default['bool_true'] + assert out == default + + if '--bool-false': + out = one_cli_function(pos_mandatory=1, bool_false=True) + cli_out = one_cli_function.cli_noexit(['--bool-false', '1']) + assert out == cli_out + assert out['bool_false'] == True + out['bool_false'] = default['bool_false'] + 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 + + # --dest + out = one_cli_function(pos_mandatory=1, custom_dest='a') + cli_out = one_cli_function.cli_noexit(['--dest', 'a', '1']) + assert out == cli_out + assert out['custom_dest'] == 'a' + out['custom_dest'] = default['custom_dest'] + assert out == default + + # Positional + out = one_cli_function(pos_mandatory=1, pos_optional=2, args_star=['3', '4']) + assert out['pos_mandatory'] == 1 + assert out['pos_optional'] == 2 + assert out['args_star'] == ['3', '4'] + cli_out = one_cli_function.cli_noexit(['1', '2', '3', '4']) + assert out == cli_out + out['pos_mandatory'] = default['pos_mandatory'] + out['pos_optional'] = default['pos_optional'] + out['args_star'] = default['args_star'] + assert out == default + + # Star + out = one_cli_function(append=['1', '2'], pos_mandatory=1) + cli_out = one_cli_function.cli_noexit(['--append', '1', '--append', '2', '1']) + assert out == cli_out + assert out['append'] == ['1', '2'] + out['append'] = default['append'] + assert out == default + + # Force a boolean value set on the config to be False on CLI. + assert one_cli_function.cli_noexit(['--no-bool-cli', '1'])['bool_cli'] is False + + # Pick another config file. + assert one_cli_function.cli_noexit(['--config-file', 'cli_function_test_config_2.py', '1'])['bool_cli'] is False + + # Extra config file for '*'. + assert one_cli_function.cli_noexit(['--config-file', 'cli_function_test_config_2.py', '1', '2', '3', '4'])['args_star'] == ['3', '4'] + assert one_cli_function.cli_noexit(['--config-file', 'cli_function_test_config_2.py', '1', '2'])['args_star'] == ['asdf', 'qwer'] + + # get_cli + assert one_cli_function.get_cli(pos_mandatory=1, asdf='B') == [('--asdf', 'B'), ('--bool-cli',), ('1',)] + assert one_cli_function.get_cli(pos_mandatory=1, asdf='B', qwer='R') == [('--asdf', 'B'), ('--bool-cli',), ('--qwer', 'R'), ('1',)] + assert one_cli_function.get_cli(pos_mandatory=1, bool_true=False) == [('--bool-cli',), ('--bool-true',), ('1',)] + assert one_cli_function.get_cli(pos_mandatory=1, pos_optional=2, args_star=['asdf', 'qwer']) == [('--bool-cli',), ('1',), ('2',), ('asdf',), ('qwer',)] + assert one_cli_function.get_cli(pos_mandatory=1, append=['2', '3']) == [('--append', '2'), ('--append', '3',), ('--bool-cli',), ('1',)] + + if len(sys.argv) > 1: + # CLI call with argv command line arguments. + print(one_cli_function.cli()) diff --git a/cli_function_test_config.py b/cli_function_test_config.py new file mode 100644 index 00000000..ccf7d1c9 --- /dev/null +++ b/cli_function_test_config.py @@ -0,0 +1,5 @@ +def set_args(args): + ''' + :type args: Dict[str, Any] + ''' + args['bool_cli'] = True diff --git a/cli_function_test_config_2.py b/cli_function_test_config_2.py new file mode 100644 index 00000000..868c642a --- /dev/null +++ b/cli_function_test_config_2.py @@ -0,0 +1,6 @@ +def set_args(args): + ''' + :type args: Dict[str, Any] + ''' + args['bool_cli'] = False + args['args_star'] = ['asdf', 'qwer'] diff --git a/common.py b/common.py index db9f702b..e597cef9 100644 --- a/common.py +++ b/common.py @@ -5,279 +5,276 @@ import collections import copy import datetime -import distutils.file_util +import enum import glob import imp -import itertools +import inspect import json +import math import multiprocessing import os +import platform import re -import shlex import shutil import signal -import stat import subprocess import sys import time import urllib import urllib.request +import cli_function +import shell_helpers +from shell_helpers import LF + common = sys.modules[__name__] -repo_short_id = 'lkmc' + +# Fixed parameters that don't depend on CLI arguments. +consts = {} +consts['repo_short_id'] = 'lkmc' # https://stackoverflow.com/questions/20010199/how-to-determine-if-a-process-runs-inside-lxc-docker -in_docker = os.path.exists('/.dockerenv') -root_dir = os.path.dirname(os.path.abspath(__file__)) -data_dir = os.path.join(root_dir, 'data') -p9_dir = os.path.join(data_dir, '9p') -gem5_non_default_src_root_dir = os.path.join(data_dir, 'gem5') -if in_docker: - out_dir = os.path.join(root_dir, 'out.docker') +consts['in_docker'] = os.path.exists('/.dockerenv') +consts['root_dir'] = os.path.dirname(os.path.abspath(__file__)) +consts['data_dir'] = os.path.join(consts['root_dir'], 'data') +consts['p9_dir'] = os.path.join(consts['data_dir'], '9p') +consts['gem5_non_default_source_root_dir'] = os.path.join(consts['data_dir'], 'gem5') +if consts['in_docker']: + consts['out_dir'] = os.path.join(consts['root_dir'], 'out.docker') else: - out_dir = os.path.join(root_dir, 'out') -bench_boot = os.path.join(out_dir, 'bench-boot.txt') -packages_dir = os.path.join(root_dir, 'buildroot_packages') -kernel_modules_subdir = 'kernel_modules' -kernel_modules_src_dir = os.path.join(common.root_dir, common.kernel_modules_subdir) -userland_subdir = 'userland' -userland_src_dir = os.path.join(common.root_dir, common.userland_subdir) -userland_build_ext = '.out' -include_subdir = 'include' -include_src_dir = os.path.join(common.root_dir, common.include_subdir) -submodules_dir = os.path.join(root_dir, 'submodules') -buildroot_src_dir = os.path.join(submodules_dir, 'buildroot') -crosstool_ng_src_dir = os.path.join(submodules_dir, 'crosstool-ng') -crosstool_ng_supported_archs = set(['arm', 'aarch64']) -linux_config_dir = os.path.join(common.root_dir, 'linux_config') -rootfs_overlay_dir = os.path.join(common.root_dir, 'rootfs_overlay') -qemu_src_dir = os.path.join(submodules_dir, 'qemu') -parsec_benchmark_src_dir = os.path.join(submodules_dir, 'parsec-benchmark') -ccache_dir = os.path.join('/usr', 'lib', 'ccache') -default_build_id = 'default' -arch_short_to_long_dict = collections.OrderedDict([ + consts['out_dir'] = os.path.join(consts['root_dir'], 'out') +consts['readme'] = os.path.join(consts['root_dir'], 'README.adoc') +consts['readme_out'] = os.path.join(consts['out_dir'], 'README.html') +consts['build_doc_log'] = os.path.join(consts['out_dir'], 'build-doc.log') +consts['gem5_out_dir'] = os.path.join(consts['out_dir'], 'gem5') +consts['kernel_modules_build_base_dir'] = os.path.join(consts['out_dir'], 'kernel_modules') +consts['buildroot_out_dir'] = os.path.join(consts['out_dir'], 'buildroot') +consts['gem5_m5_build_dir'] = os.path.join(consts['out_dir'], 'util', 'm5') +consts['run_dir_base'] = os.path.join(consts['out_dir'], 'run') +consts['crosstool_ng_out_dir'] = os.path.join(consts['out_dir'], 'crosstool-ng') +consts['test_boot_benchmark_file'] = os.path.join(consts['out_dir'], 'test-boot.txt') +consts['packages_dir'] = os.path.join(consts['root_dir'], 'buildroot_packages') +consts['kernel_modules_subdir'] = 'kernel_modules' +consts['kernel_modules_source_dir'] = os.path.join(consts['root_dir'], consts['kernel_modules_subdir']) +consts['userland_subdir'] = 'userland' +consts['userland_source_dir'] = os.path.join(consts['root_dir'], consts['userland_subdir']) +consts['userland_build_ext'] = '.out' +consts['include_subdir'] = 'include' +consts['include_source_dir'] = os.path.join(consts['root_dir'], consts['include_subdir']) +consts['submodules_dir'] = os.path.join(consts['root_dir'], 'submodules') +consts['buildroot_source_dir'] = os.path.join(consts['submodules_dir'], 'buildroot') +consts['crosstool_ng_source_dir'] = os.path.join(consts['submodules_dir'], 'crosstool-ng') +consts['crosstool_ng_supported_archs'] = set(['arm', 'aarch64']) +consts['linux_source_dir'] = os.path.join(consts['submodules_dir'], 'linux') +consts['linux_config_dir'] = os.path.join(consts['root_dir'], 'linux_config') +consts['gem5_default_source_dir'] = os.path.join(consts['submodules_dir'], 'gem5') +consts['rootfs_overlay_dir'] = os.path.join(consts['root_dir'], 'rootfs_overlay') +consts['extract_vmlinux'] = os.path.join(consts['linux_source_dir'], 'scripts', 'extract-vmlinux') +consts['qemu_source_dir'] = os.path.join(consts['submodules_dir'], 'qemu') +consts['parsec_benchmark_source_dir'] = os.path.join(consts['submodules_dir'], 'parsec-benchmark') +consts['ccache_dir'] = os.path.join('/usr', 'lib', 'ccache') +consts['default_build_id'] = 'default' +consts['arch_short_to_long_dict'] = collections.OrderedDict([ ('x', 'x86_64'), ('a', 'arm'), ('A', 'aarch64'), ]) -all_archs = [arch_short_to_long_dict[k] for k in arch_short_to_long_dict] -arch_choices = [] -for key in common.arch_short_to_long_dict: - arch_choices.append(key) - arch_choices.append(common.arch_short_to_long_dict[key]) -default_arch = 'x86_64' -gem5_cpt_prefix = '^cpt\.' +# All long arch names. +consts['all_long_archs'] = [consts['arch_short_to_long_dict'][k] for k in consts['arch_short_to_long_dict']] +# All long and short arch names. +consts['arch_choices'] = set() +for key in consts['arch_short_to_long_dict']: + consts['arch_choices'].add(key) + consts['arch_choices'].add(consts['arch_short_to_long_dict'][key]) +consts['default_arch'] = 'x86_64' +consts['gem5_cpt_prefix'] = '^cpt\.' def git_sha(repo_path): return subprocess.check_output(['git', '-C', repo_path, 'log', '-1', '--format=%H']).decode().rstrip() -sha = common.git_sha(root_dir) -release_dir = os.path.join(common.out_dir, 'release') -release_zip_file = os.path.join(common.release_dir, 'lkmc-{}.zip'.format(common.sha)) -github_repo_id = 'cirosantilli/linux-kernel-module-cheat' -asm_ext = '.S' -c_ext = '.c' -header_ext = '.h' -kernel_module_ext = '.ko' -obj_ext = '.o' -config_file = os.path.join(data_dir, 'config') -command_prefix = '+ ' -magic_fail_string = b'lkmc_test_fail' -if os.path.exists(config_file): - config = imp.load_source('config', config_file) - configs = {x:getattr(config, x) for x in dir(config) if not x.startswith('__')} - -class Component: - def __init__(self): - pass +consts['sha'] = common.git_sha(consts['root_dir']) +consts['release_dir'] = os.path.join(consts['out_dir'], 'release') +consts['release_zip_file'] = os.path.join(consts['release_dir'], 'lkmc-{}.zip'.format(consts['sha'])) +consts['github_repo_id'] = 'cirosantilli/linux-kernel-module-cheat' +consts['asm_ext'] = '.S' +consts['c_ext'] = '.c' +consts['header_ext'] = '.h' +consts['kernel_module_ext'] = '.ko' +consts['obj_ext'] = '.o' +consts['config_file'] = os.path.join(consts['data_dir'], 'config.py') +consts['magic_fail_string'] = b'lkmc_test_fail' +consts['baremetal_lib_basename'] = 'lib' +consts['emulator_short_to_long_dict'] = collections.OrderedDict([ + ('q', 'qemu'), + ('g', 'gem5'), +]) +consts['all_long_emulators'] = [consts['emulator_short_to_long_dict'][k] for k in consts['emulator_short_to_long_dict']] +consts['emulator_choices'] = set() +for key in consts['emulator_short_to_long_dict']: + consts['emulator_choices'].add(key) + consts['emulator_choices'].add(consts['emulator_short_to_long_dict'][key]) +consts['host_arch'] = platform.processor() + +class LkmcCliFunction(cli_function.CliFunction): + ''' + Common functionality shared across our CLI functions: - def build(self): + * command timing + * some common flags, e.g.: --arch, --dry-run, --quiet, --verbose + ''' + def __init__(self, *args, defaults=None, supported_archs=None, **kwargs): ''' - Parse CLI, and to the build based on it. - - The actual build work is done by do_build in implementing classes. + :ptype defaults: Dict[str,Any] + :param defaults: override the default value of an argument ''' - parser = common.get_argparse( - argparse_args=self.get_argparse_args(), - default_args=self.get_default_args(), + kwargs['config_file'] = consts['config_file'] + kwargs['extra_config_params'] = os.path.basename(inspect.getfile(self.__class__)) + if defaults is None: + defaults = {} + self._defaults = defaults + self._is_common = True + self._common_args = set() + super().__init__(*args, **kwargs) + self.supported_archs = supported_archs + + # Args for all scripts. + arches = consts['arch_short_to_long_dict'] + arches_string = [] + for arch_short in arches: + arch_long = arches[arch_short] + arches_string.append('{} ({})'.format(arch_long, arch_short)) + arches_string = ', '.join(arches_string) + self.add_argument( + '-A', '--all-archs', default=False, + help='''\ +Run action for all supported --archs archs. Ignore --archs. +'''.format(arches_string) ) - self.add_parser_arguments(parser) - parser.add_argument( - '--clean', - help='Clean the build instead of building.', - action='store_true', - ) - parser.add_argument( - '-j', '--nproc', - help='Number of processors to use for the build. Default: use all cores.', - type=int, - default=multiprocessing.cpu_count(), + self.add_argument( + '-a', + '--arch', + action='append', + choices=consts['arch_choices'], + default=[consts['default_arch']], + dest='archs', + help='''\ +CPU architecture to use. If given multiple times, run the action +for each arch sequentially in that order. If one of them fails, stop running. +Valid archs: {} +'''.format(arches_string) ) - args = common.setup(parser) - if not common.dry_run: - start_time = time.time() - if args.clean: - self.clean(args) - else: - self.do_build(args) - if not common.dry_run: - end_time = time.time() - common.print_time(end_time - start_time) - - def add_parser_arguments(self, parser): - pass - - def clean(self, args): - build_dir = self.get_build_dir(args) - if build_dir is not None: - common.rmrf(build_dir) - - def do_build(self, args): - ''' - Do the actual main build work. - ''' - raise NotImplementedError() - - def get_argparse_args(self): - ''' - Extra arguments for argparse.ArgumentParser. - ''' - return {} - - def get_build_dir(self, args): - ''' - Build directory, gets cleaned by --clean if not None. - ''' - return None - - def get_default_args(self): - ''' - Default values for command line arguments. - ''' - return {} - -class Newline: - ''' - Singleton class. Can be used in print_cmd to print out nicer command lines - with -key on the same line as "-key value". - ''' - pass - -def add_dry_run_argument(parser): - parser.add_argument('--dry-run', default=False, action='store_true', help='''\ + self.add_argument( + '--dry-run', + default=False, + help='''\ Print the commands that would be run, but don't run them. We aim display every command that modifies the filesystem state, and generate Bash equivalents even for actions taken directly in Python without shelling out. -mkdir are generally omitted since those are obvious. -''') - -def add_newlines(cmd): - out = [] - for arg in cmd: - out.extend([arg, common.Newline]) - return out - -def assert_crosstool_ng_supports_arch(arch): - if arch not in common.crosstool_ng_supported_archs: - raise Exception('arch not yet supported: ' + arch) - -def base64_encode(string): - return base64.b64encode(string.encode()).decode() - -def write_string_to_file(path, string, mode='w'): - if mode == 'a': - redirect = '>>' - else: - redirect = '>' - print_cmd("cat << 'EOF' {} {}\n{}\nEOF".format(redirect, path, string)) - if not common.dry_run: - with open(path, 'a') as f: - f.write(string) - -def cmd_to_string(cmd, cwd=None, extra_env=None, extra_paths=None): - ''' - Format a command given as a list of strings so that it can - be viewed nicely and executed by bash directly and print it to stdout. - ''' - last_newline = ' \\\n' - newline_separator = last_newline + ' ' - out = [] - if extra_env is None: - extra_env = {} - if cwd is not None: - out.append('cd {} &&'.format(shlex.quote(cwd))) - if extra_paths is not None: - out.append('PATH="{}:${{PATH}}"'.format(':'.join(extra_paths))) - for key in extra_env: - out.append('{}={}'.format(shlex.quote(key), shlex.quote(extra_env[key]))) - cmd_quote = [] - newline_count = 0 - for arg in cmd: - if arg == common.Newline: - cmd_quote.append(arg) - newline_count += 1 - else: - cmd_quote.append(shlex.quote(arg)) - if newline_count > 0: - cmd_quote = [' '.join(list(y)) for x, y in itertools.groupby(cmd_quote, lambda z: z == common.Newline) if not x] - out.extend(cmd_quote) - if newline_count == 1 and cmd[-1] == common.Newline: - ending = '' - else: - ending = last_newline + ';' - return newline_separator.join(out) + ending - -def copy_dir_if_update_non_recursive(srcdir, destdir, filter_ext=None): - # TODO print rsync equivalent. - os.makedirs(destdir, exist_ok=True) - for basename in os.listdir(srcdir): - src = os.path.join(srcdir, basename) - if os.path.isfile(src): - noext, ext = os.path.splitext(basename) - if filter_ext is not None and ext == filter_ext: - distutils.file_util.copy_file( - src, - os.path.join(destdir, basename), - update=1, - ) - -def cp(src, dest): - print_cmd(['cp', src, dest]) - if not common.dry_run: - shutil.copy2(src, dest) +mkdir are generally omitted since those are obvious +''' + ) + self.add_argument( + '--print-time', default=True, + help='''\ +Print how long it took to run the command at the end. +Implied by --quiet. +''' + ) + self.add_argument( + '-q', '--quiet', default=False, + help='''\ +Don't print anything to stdout, except if it is part of an interactive terminal. +TODO: implement fully, some stuff is escaping it currently. +''' + ) + self.add_argument( + '--quit-on-fail', + default=True, + help='''\ +Stop running at the first failed test. +''' + ) + self.add_argument( + '-v', '--verbose', default=False, + help='Show full compilation commands when they are not shown by default.' + ) -def gem_list_checkpoint_dirs(): - ''' - List checkpoint directory, oldest first. - ''' - prefix_re = re.compile(common.gem5_cpt_prefix) - files = list(filter(lambda x: os.path.isdir(os.path.join(common.m5out_dir, x)) and prefix_re.search(x), os.listdir(common.m5out_dir))) - files.sort(key=lambda x: os.path.getmtime(os.path.join(common.m5out_dir, x))) - return files + # Gem5 args. + self.add_argument( + '--dp650', default=False, + help='Use the ARM DP650 display processor instead of the default HDLCD on gem5.' + ) + self.add_argument( + '--gem5-build-dir', + help='''\ +Use the given directory as the gem5 build directory. +Ignore --gem5-build-id and --gem5-build-type. +''' + ) + self.add_argument( + '-M', '--gem5-build-id', + help='''\ +gem5 build ID. Allows you to keep multiple separate gem5 builds. +Default: {} +'''.format(consts['default_build_id']) + ) + self.add_argument( + '--gem5-build-type', default='opt', + help='gem5 build type, most often used for "debug" builds.' + ) + self.add_argument( + '--gem5-source-dir', + help='''\ +Use the given directory as the gem5 source tree. Ignore `--gem5-worktree`. +''' + ) + self.add_argument( + '-N', '--gem5-worktree', + help='''\ +Create and use a git worktree of the gem5 submodule. +See: https://github.com/cirosantilli/linux-kernel-module-cheat#gem5-worktree +''' + ) -def get_argparse(default_args=None, argparse_args=None): - ''' - Return an argument parser with common arguments set. + # Linux kernel. + self.add_argument( + '--linux-build-dir', + help='''\ +Use the given directory as the Linux build directory. Ignore --linux-build-id. +''' + ) + self.add_argument( + '-L', '--linux-build-id', default=consts['default_build_id'], + help='''\ +Linux build ID. Allows you to keep multiple separate Linux builds. +''' + ) + self.add_argument( + '--linux-source-dir', + help='''\ +Use the given directory as the Linux source tree. +''' + ) + self.add_argument( + '--initramfs', + default=False, + help='''\ +See: https://github.com/cirosantilli/linux-kernel-module-cheat#initramfs +''' + ) + self.add_argument( + '--initrd', + default=False, + help='''\ +For Buildroot: create a CPIO root filessytem. +For QEMU use that CPUI root filesystem initrd instead of the default ext2. +See: https://github.com/cirosantilli/linux-kernel-module-cheat#initrd +''' + ) - :type default_args: Dict[str,str] - :type argparse_args: Dict - ''' - if default_args is None: - default_args = {} - if argparse_args is None: - argparse_args = {} - parser = argparse.ArgumentParser( - formatter_class=argparse.RawTextHelpFormatter, - **argparse_args - ) - common.add_dry_run_argument(parser) - emulator_group = parser.add_mutually_exclusive_group(required=False) - parser.add_argument( - '-a', '--arch', choices=common.arch_choices, default=common.default_arch, - help='CPU architecture. Default: %(default)s' - ) - parser.add_argument( - '-b', '--baremetal', - help='''\ + # Baremetal. + self.add_argument( + '-b', '--baremetal', + help='''\ Use the given baremetal executable instead of the Linux kernel. If the path is absolute, it is used as is. @@ -285,753 +282,913 @@ def get_argparse(default_args=None, argparse_args=None): If the path is relative, we assume that it points to a source code inside baremetal/ and then try to use corresponding executable. ''' - ) - parser.add_argument( - '--buildroot-build-id', - default=default_build_id, - help='Buildroot build ID. Allows you to keep multiple separate gem5 builds. Default: %(default)s' - ) - parser.add_argument( - '--buildroot-linux', default=False, action='store_true', - help='Boot with the Buildroot Linux kernel instead of our custom built one. Mostly for sanity checks.' - ) - parser.add_argument( - '--crosstool-ng-build-id', default=default_build_id, - help='Crosstool-NG build ID. Allows you to keep multiple separate crosstool-NG builds. Default: %(default)s' - ) - parser.add_argument( - '--docker', default=False, action='store_true', - help='''\ + ) + + # Buildroot. + self.add_argument( + '--buildroot-build-id', + default=consts['default_build_id'], + help='Buildroot build ID. Allows you to keep multiple separate gem5 builds.' + ) + self.add_argument( + '--buildroot-linux', default=False, + help='Boot with the Buildroot Linux kernel instead of our custom built one. Mostly for sanity checks.' + ) + + # crosstool-ng + self.add_argument( + '--crosstool-ng-build-id', default=consts['default_build_id'], + help='Crosstool-NG build ID. Allows you to keep multiple separate crosstool-NG builds.' + ) + self.add_argument( + '--docker', default=False, + help='''\ Use the docker download Ubuntu root filesystem instead of the default Buildroot one. ''' - ) - parser.add_argument( - '--dp650', default=False, action='store_true' - ) - parser.add_argument( - '-L', '--linux-build-id', default=default_build_id, - help='Linux build ID. Allows you to keep multiple separate Linux builds. Default: %(default)s' - ) - parser.add_argument( - '--linux-build-dir', - help='Select the directory that contains the Linux kernel build. Overrides linux-build-id.' - ) - parser.add_argument( - '--linux-source-dir', - help='''\ -Use the given directory as the Linux source tree. -''' - ) - parser.add_argument( - '--machine', - help='''Machine type. + ) + + self.add_argument( + '--machine', + help='''Machine type. QEMU default: virt gem5 default: VExpress_GEM5_V1 See the documentation for other values known to work. ''' - ) - emulator_group.add_argument( - '-g', '--gem5', default=False, action='store_true', - help='Use gem5 instead of QEMU. Default: False' - ) - parser.add_argument( - '--gem5-build-dir', - help='''\ -Use the given directory as the gem5 build directory. -''' - ) - parser.add_argument( - '-M', '--gem5-build-id', - help='''\ -gem5 build ID. Allows you to keep multiple separate gem5 builds. Default: {} -'''.format(default_build_id) - ) - parser.add_argument( - '--gem5-build-type', default='opt', - help='gem5 build type, most often used for "debug" builds. Default: %(default)s' - ) - parser.add_argument( - '--gem5-source-dir', - help='''\ -Use the given directory as the gem5 source tree. Ignore `--gem5-worktree`. + ) + + # QEMU. + self.add_argument( + '-Q', '--qemu-build-id', default=consts['default_build_id'], + help='QEMU build ID. Allows you to keep multiple separate QEMU builds.' + ) + + # Userland. + 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( - '-N', '--gem5-worktree', - help='''\ -Create and use a git worktree of the gem5 submodule. -See: https://github.com/cirosantilli/linux-kernel-module-cheat#gem5-worktree + ) + self.add_argument( + '--userland-args', + help='''\ +CLI arguments to pass to the userland executable. ''' - ) - parser.add_argument( - '-n', '--run-id', default='0', - help='''\ + ) + self.add_argument( + '--userland-build-id' + ) + + # Run. + self.add_argument( + '-n', '--run-id', default='0', + help='''\ ID for run outputs such as gem5's m5out. Allows you to do multiple runs, and then inspect separate outputs later in different output directories. -Default: %(default)s ''' - ) - parser.add_argument( - '-P', '--prebuilt', default=False, action='store_true', - help='''\ + ) + self.add_argument( + '-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 the likelihood of incompatibilities. ''' - ) - parser.add_argument( - '--port-offset', type=int, - help='''\ + ) + self.add_argument( + '--port-offset', type=int, + help='''\ Increase the ports to be used such as for GDB by an offset to run multiple -instances in parallel. -Default: the run ID (-n) if that is an integer, otherwise 0. +instances in parallel. Default: the run ID (-n) if that is an integer, otherwise 0. ''' - ) - emulator_group.add_argument( - '--qemu', default=False, action='store_true', - help='''\ -Use QEMU as the emulator. This option exists in addition to --gem5 -to allow overriding configs from the CLI. -''' - ) - parser.add_argument( - '-Q', '--qemu-build-id', default=default_build_id, - help='QEMU build ID. Allows you to keep multiple separate QEMU builds. Default: %(default)s' - ) - parser.add_argument( - '--userland-build-id', default=None - ) - parser.add_argument( - '-v', '--verbose', default=False, action='store_true', - help='Show full compilation commands when they are not shown by default.' - ) - if hasattr(common, 'configs'): - defaults = common.configs.copy() - else: - defaults = {} - defaults.update(default_args) - # A bit ugly as it actually changes the defaults shown on --help, but we can't do any better - # because it is impossible to check if arguments were given or not... - # - https://stackoverflow.com/questions/30487767/check-if-argparse-optional-argument-is-set-or-not - # - https://stackoverflow.com/questions/3609852/which-is-the-best-way-to-allow-configuration-options-be-overridden-at-the-comman - parser.set_defaults(**defaults) - return parser - -def get_elf_entry(elf_file_path): - readelf_header = subprocess.check_output([ - common.get_toolchain_tool('readelf'), - '-h', - elf_file_path - ]) - for line in readelf_header.decode().split('\n'): - split = line.split() - if line.startswith(' Entry point address:'): - addr = line.split()[-1] - break - return int(addr, 0) - -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 = common.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(tool, allowed_toolchains=None): - buildroot_full_prefix = os.path.join(common.host_bin_dir, common.buildroot_toolchain_prefix) - buildroot_exists = os.path.exists('{}-{}'.format(buildroot_full_prefix, tool)) - crosstool_ng_full_prefix = os.path.join(common.crosstool_ng_bin_dir, common.crosstool_ng_toolchain_prefix) - crosstool_ng_exists = os.path.exists('{}-{}'.format(crosstool_ng_full_prefix, tool)) - host_tool = '{}-{}'.format(common.ubuntu_toolchain_prefix, tool) - host_path = shutil.which(host_tool) - if host_path is not None: - host_exists = True - host_full_prefix = host_path[:-(len(tool)+1)] - else: - host_exists = False - host_full_prefix = None - known_toolchains = { - 'crosstool-ng': (crosstool_ng_exists, crosstool_ng_full_prefix), - 'buildroot': (buildroot_exists, buildroot_full_prefix), - 'host': (host_exists, host_full_prefix), - } - if allowed_toolchains is None: - if common.baremetal is None: - allowed_toolchains = ['buildroot', 'crosstool-ng', 'host'] + ) + + # Misc. + emulators = consts['emulator_short_to_long_dict'] + emulators_string = [] + for emulator_short in emulators: + emulator_long = emulators[emulator_short] + emulators_string.append('{} ({})'.format(emulator_long, emulator_short)) + emulators_string = ', '.join(emulators_string) + self.add_argument( + '--all-emulators', default=False, + help='''\ +Run action for all supported --emulators emulators. Ignore --emulators. +'''.format(emulators_string) + ) + self.add_argument( + '-e', + '--emulator', + action='append', + choices=consts['emulator_choices'], + default=['qemu'], + dest='emulators', + help='''\ +Emulator to use. If given multiple times, semantics are similar to --arch. +Valid emulators: {} +'''.format(emulators_string) + ) + self._is_common = False + + def __call__(self, *args, **kwargs): + ''' + For Python code calls, in addition to base: + + - print the CLI equivalent of the call + - automatically forward common arguments + ''' + print_cmd = ['./' + self.extra_config_params, LF] + for line in self.get_cli(**kwargs): + print_cmd.extend(line) + print_cmd.append(LF) + if not ('quiet' in kwargs and kwargs['quiet']): + shell_helpers.ShellHelpers().print_cmd(print_cmd) + return super().__call__(**kwargs) + + def _init_env(self, env): + ''' + Update the kwargs from the command line with values derived from them. + ''' + def join(*paths): + return os.path.join(*paths) + if env['emulator'] in env['emulator_short_to_long_dict']: + env['emulator'] = env['emulator_short_to_long_dict'][env['emulator']] + if not env['_args_given']['userland_build_id']: + env['userland_build_id'] = env['default_build_id'] + if not env['_args_given']['gem5_build_id']: + if env['_args_given']['gem5_worktree']: + env['gem5_build_id'] = env['gem5_worktree'] + else: + env['gem5_build_id'] = consts['default_build_id'] + env['is_arm'] = False + if env['arch'] == 'arm': + env['armv'] = 7 + env['mcpu'] = 'cortex-a15' + env['buildroot_toolchain_prefix'] = 'arm-buildroot-linux-uclibcgnueabihf' + env['crosstool_ng_toolchain_prefix'] = 'arm-unknown-eabi' + env['ubuntu_toolchain_prefix'] = 'arm-linux-gnueabihf' + env['is_arm'] = True + elif env['arch'] == 'aarch64': + env['armv'] = 8 + env['mcpu'] = 'cortex-a57' + env['buildroot_toolchain_prefix'] = 'aarch64-buildroot-linux-uclibc' + env['crosstool_ng_toolchain_prefix'] = 'aarch64-unknown-elf' + env['ubuntu_toolchain_prefix'] = 'aarch64-linux-gnu' + env['is_arm'] = True + elif env['arch'] == 'x86_64': + env['crosstool_ng_toolchain_prefix'] = 'x86_64-unknown-elf' + env['gem5_arch'] = 'X86' + env['buildroot_toolchain_prefix'] = 'x86_64-buildroot-linux-uclibc' + env['ubuntu_toolchain_prefix'] = 'x86_64-linux-gnu' + if env['emulator'] == 'gem5': + if not env['_args_given']['machine']: + env['machine'] = 'TODO' + else: + if not env['_args_given']['machine']: + env['machine'] = 'pc' + if env['is_arm']: + env['gem5_arch'] = 'ARM' + if env['emulator'] == 'gem5': + if not env['_args_given']['machine']: + if env['dp650']: + env['machine'] = 'VExpress_GEM5_V1_DPU' + else: + env['machine'] = 'VExpress_GEM5_V1' + else: + if not env['_args_given']['machine']: + env['machine'] = 'virt' + if env['arch'] == 'arm': + # highmem=off needed since v3.0.0 due to: + # http://lists.nongnu.org/archive/html/qemu-discuss/2018-08/msg00034.html + env['machine2'] = 'highmem=off' + elif env['arch'] == 'aarch64': + env['machine2'] = 'gic_version=3' else: - allowed_toolchains = ['crosstool-ng', 'buildroot', 'host'] - tried = [] - for toolchain in allowed_toolchains: - exists, prefix = known_toolchains[toolchain] - tried.append('{}-{}'.format(prefix, tool)) - if exists: - return prefix - raise Exception('Tool not found. Tried:\n' + '\n'.join(tried)) - -def get_toolchain_tool(tool, allowed_toolchains=None): - return '{}-{}'.format(common.get_toolchain_prefix(tool, allowed_toolchains), tool) - -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(common.buildroot_build_build_dir, exist_ok=True) - os.makedirs(common.gem5_build_dir, exist_ok=True) - os.makedirs(common.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(common.gem5_run_dir, exist_ok=True) - os.makedirs(common.p9_dir, exist_ok=True) - os.makedirs(common.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 + env['machine2'] = None + + # Buildroot + env['buildroot_build_dir'] = join(env['buildroot_out_dir'], 'build', env['buildroot_build_id'], env['arch']) + env['buildroot_download_dir'] = join(env['buildroot_out_dir'], 'download') + env['buildroot_config_file'] = join(env['buildroot_build_dir'], '.config') + env['buildroot_build_build_dir'] = join(env['buildroot_build_dir'], 'build') + env['buildroot_linux_build_dir'] = join(env['buildroot_build_build_dir'], 'linux-custom') + env['buildroot_vmlinux'] = join(env['buildroot_linux_build_dir'], 'vmlinux') + env['host_dir'] = join(env['buildroot_build_dir'], 'host') + env['host_bin_dir'] = join(env['host_dir'], 'usr', 'bin') + env['buildroot_pkg_config'] = join(env['host_bin_dir'], 'pkg-config') + env['buildroot_images_dir'] = join(env['buildroot_build_dir'], 'images') + env['buildroot_rootfs_raw_file'] = join(env['buildroot_images_dir'], 'rootfs.ext2') + env['buildroot_qcow2_file'] = env['buildroot_rootfs_raw_file'] + '.qcow2' + env['buildroot_cpio'] = join(env['buildroot_images_dir'], 'rootfs.cpio') + env['staging_dir'] = join(env['out_dir'], 'staging', env['arch']) + env['buildroot_staging_dir'] = join(env['buildroot_build_dir'], 'staging') + env['target_dir'] = join(env['buildroot_build_dir'], 'target') + if not env['_args_given']['linux_source_dir']: + env['linux_source_dir'] = os.path.join(consts['submodules_dir'], 'linux') + common.extract_vmlinux = os.path.join(env['linux_source_dir'], 'scripts', 'extract-vmlinux') + env['linux_buildroot_build_dir'] = join(env['buildroot_build_build_dir'], 'linux-custom') + + # QEMU + env['qemu_build_dir'] = join(env['out_dir'], 'qemu', env['qemu_build_id']) + env['qemu_executable_basename'] = 'qemu-system-{}'.format(env['arch']) + env['qemu_executable'] = join(env['qemu_build_dir'], '{}-softmmu'.format(env['arch']), env['qemu_executable_basename']) + env['qemu_img_basename'] = 'qemu-img' + env['qemu_img_executable'] = join(env['qemu_build_dir'], env['qemu_img_basename']) + + # gem5 + if not env['_args_given']['gem5_build_dir']: + env['gem5_build_dir'] = join(env['gem5_out_dir'], env['gem5_build_id'], env['gem5_build_type']) + env['gem5_fake_iso'] = join(env['gem5_out_dir'], 'fake.iso') + env['gem5_m5term'] = join(env['gem5_build_dir'], 'm5term') + env['gem5_build_build_dir'] = join(env['gem5_build_dir'], 'build') + env['gem5_executable'] = join(env['gem5_build_build_dir'], env['gem5_arch'], 'gem5.{}'.format(env['gem5_build_type'])) + env['gem5_system_dir'] = join(env['gem5_build_dir'], 'system') + + # gem5 source + if env['gem5_source_dir'] is not None: + assert os.path.exists(env['gem5_source_dir']) + else: + if env['gem5_worktree'] is not None: + env['gem5_source_dir'] = join(env['gem5_non_default_source_root_dir'], env['gem5_worktree']) + else: + env['gem5_source_dir'] = env['gem5_default_source_dir'] + env['gem5_m5_source_dir'] = join(env['gem5_source_dir'], 'util', 'm5') + env['gem5_config_dir'] = join(env['gem5_source_dir'], 'configs') + env['gem5_se_file'] = join(env['gem5_config_dir'], 'example', 'se.py') + env['gem5_fs_file'] = join(env['gem5_config_dir'], 'example', 'fs.py') + + # crosstool-ng + env['crosstool_ng_buildid_dir'] = join(env['crosstool_ng_out_dir'], 'build', env['crosstool_ng_build_id']) + env['crosstool_ng_install_dir'] = join(env['crosstool_ng_buildid_dir'], 'install', env['arch']) + env['crosstool_ng_bin_dir'] = join(env['crosstool_ng_install_dir'], 'bin') + env['crosstool_ng_util_dir'] = join(env['crosstool_ng_buildid_dir'], 'util') + env['crosstool_ng_config'] = join(env['crosstool_ng_util_dir'], '.config') + env['crosstool_ng_defconfig'] = join(env['crosstool_ng_util_dir'], 'defconfig') + env['crosstool_ng_executable'] = join(env['crosstool_ng_util_dir'], 'ct-ng') + env['crosstool_ng_build_dir'] = join(env['crosstool_ng_buildid_dir'], 'build') + env['crosstool_ng_download_dir'] = join(env['crosstool_ng_out_dir'], 'download') + + # run + env['gem5_run_dir'] = join(env['run_dir_base'], 'gem5', env['arch'], str(env['run_id'])) + env['m5out_dir'] = join(env['gem5_run_dir'], 'm5out') + env['stats_file'] = join(env['m5out_dir'], 'stats.txt') + env['gem5_trace_txt_file'] = join(env['m5out_dir'], 'trace.txt') + env['gem5_guest_terminal_file'] = join(env['m5out_dir'], 'system.terminal') + env['gem5_readfile'] = join(env['gem5_run_dir'], 'readfile') + env['gem5_termout_file'] = join(env['gem5_run_dir'], 'termout.txt') + env['qemu_run_dir'] = join(env['run_dir_base'], 'qemu', env['arch'], str(env['run_id'])) + env['qemu_termout_file'] = join(env['qemu_run_dir'], 'termout.txt') + env['qemu_trace_basename'] = 'trace.bin' + env['qemu_trace_file'] = join(env['qemu_run_dir'], 'trace.bin') + env['qemu_trace_txt_file'] = join(env['qemu_run_dir'], 'trace.txt') + env['qemu_rrfile'] = join(env['qemu_run_dir'], 'rrfile') + env['gem5_out_dir'] = join(env['out_dir'], 'gem5') + + # Ports + if not env['_args_given']['port_offset']: + try: + env['port_offset'] = int(env['run_id']) + except ValueError: + env['port_offset'] = 0 + if env['emulator'] == 'gem5': + env['gem5_telnet_port'] = 3456 + env['port_offset'] + env['gdb_port'] = 7000 + env['port_offset'] + else: + env['qemu_base_port'] = 45454 + 10 * env['port_offset'] + env['qemu_monitor_port'] = env['qemu_base_port'] + 0 + env['qemu_hostfwd_generic_port'] = env['qemu_base_port'] + 1 + env['qemu_hostfwd_ssh_port'] = env['qemu_base_port'] + 2 + env['qemu_gdb_port'] = env['qemu_base_port'] + 3 + env['extra_serial_port'] = env['qemu_base_port'] + 4 + env['gdb_port'] = env['qemu_gdb_port'] + env['qemu_background_serial_file'] = join(env['qemu_run_dir'], 'background.log') + + # gem5 QEMU polymorphism. + if env['emulator'] == 'gem5': + env['executable'] = env['gem5_executable'] + env['run_dir'] = env['gem5_run_dir'] + env['termout_file'] = env['gem5_termout_file'] + env['guest_terminal_file'] = env['gem5_guest_terminal_file'] + env['trace_txt_file'] = env['gem5_trace_txt_file'] + else: + 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') + + # Linux kernel. + if not env['_args_given']['linux_build_dir']: + env['linux_build_dir'] = join(env['out_dir'], 'linux', env['linux_build_id'], env['arch']) + env['lkmc_vmlinux'] = join(env['linux_build_dir'], 'vmlinux') + if env['arch'] == 'arm': + env['linux_arch'] = 'arm' + env['linux_image_prefix'] = join('arch', env['linux_arch'], 'boot', 'zImage') + elif env['arch'] == 'aarch64': + env['linux_arch'] = 'arm64' + env['linux_image_prefix'] = join('arch', env['linux_arch'], 'boot', 'Image') + elif env['arch'] == 'x86_64': + env['linux_arch'] = 'x86' + env['linux_image_prefix'] = join('arch', env['linux_arch'], 'boot', 'bzImage') + env['lkmc_linux_image'] = join(env['linux_build_dir'], env['linux_image_prefix']) + env['buildroot_linux_image'] = join(env['buildroot_linux_build_dir'], env['linux_image_prefix']) + if env['buildroot_linux']: + env['vmlinux'] = env['buildroot_vmlinux'] + env['linux_image'] = env['buildroot_linux_image'] + else: + env['vmlinux'] = env['lkmc_vmlinux'] + env['linux_image'] = env['lkmc_linux_image'] + if env['emulator']== 'gem5': + env['userland_quit_cmd'] = '/gem5_exit.sh' + else: + env['userland_quit_cmd'] = '/poweroff.out' + env['ramfs'] = env['initrd'] or env['initramfs'] + if env['ramfs']: + env['initarg'] = 'rdinit' + else: + env['initarg'] = 'init' + env['quit_init'] = '{}={}'.format(env['initarg'], env['userland_quit_cmd']) + + # Kernel modules. + env['kernel_modules_build_dir'] = join(env['kernel_modules_build_base_dir'], env['arch']) + env['kernel_modules_build_subdir'] = join(env['kernel_modules_build_dir'], env['kernel_modules_subdir']) + env['kernel_modules_build_host_dir'] = join(env['kernel_modules_build_base_dir'], 'host') + env['kernel_modules_build_host_subdir'] = join(env['kernel_modules_build_host_dir'], env['kernel_modules_subdir']) + env['userland_build_dir'] = join(env['out_dir'], 'userland', env['userland_build_id'], env['arch']) + env['out_rootfs_overlay_dir'] = join(env['out_dir'], 'rootfs_overlay', env['arch']) + env['out_rootfs_overlay_bin_dir'] = join(env['out_rootfs_overlay_dir'], 'bin') + + # Baremetal. + env['baremetal_source_dir'] = join(env['root_dir'], 'baremetal') + env['baremetal_source_arch_subpath'] = join('arch', env['arch']) + env['baremetal_source_arch_dir'] = join(env['baremetal_source_dir'], env['baremetal_source_arch_subpath']) + env['baremetal_source_lib_dir'] = join(env['baremetal_source_dir'], env['baremetal_lib_basename']) + if env['emulator'] == 'gem5': + env['simulator_name'] = 'gem5' + else: + env['simulator_name'] = 'qemu' + env['baremetal_build_dir'] = join(env['out_dir'], 'baremetal', env['arch'], env['simulator_name'], env['machine']) + env['baremetal_build_lib_dir'] = join(env['baremetal_build_dir'], env['baremetal_lib_basename']) + env['baremetal_build_ext'] = '.elf' + + # Docker + env['docker_build_dir'] = join(env['out_dir'], 'docker', env['arch']) + env['docker_tar_dir'] = join(env['docker_build_dir'], 'export') + env['docker_tar_file'] = join(env['docker_build_dir'], 'export.tar') + env['docker_rootfs_raw_file'] = join(env['docker_build_dir'], 'export.ext2') + env['docker_qcow2_file'] = join(env['docker_rootfs_raw_file'] + '.qcow2') + if env['docker']: + env['rootfs_raw_file'] = env['docker_rootfs_raw_file'] + env['qcow2_file'] = env['docker_qcow2_file'] + else: + env['rootfs_raw_file'] = env['buildroot_rootfs_raw_file'] + env['qcow2_file'] = env['buildroot_qcow2_file'] + + # Image + if env['_args_given']['baremetal']: + env['disk_image'] = env['gem5_fake_iso'] + if env['baremetal'] == 'all': + path = env['baremetal'] + else: + path = self.resolve_executable( + env['baremetal'], + env['baremetal_source_dir'], + env['baremetal_build_dir'], + env['baremetal_build_ext'], + ) + source_path_noext = os.path.splitext(join( + env['baremetal_source_dir'], + os.path.relpath(path, env['baremetal_build_dir']) + ))[0] + for ext in [env['c_ext'], env['asm_ext']]: + source_path = source_path_noext + ext + if os.path.exists(source_path): + env['source_path'] = source_path + break + env['image'] = path + else: + if env['emulator'] == 'gem5': + env['image'] = env['vmlinux'] + if env['ramfs']: + env['disk_image'] = env['gem5_fake_iso'] + else: + env['disk_image'] = env['rootfs_raw_file'] + else: + env['image'] = env['linux_image'] + env['disk_image'] = env['qcow2_file'] -def print_cmd(cmd, cwd=None, cmd_file=None, extra_env=None, extra_paths=None): - ''' - Print cmd_to_string to stdout. + def add_argument(self, *args, **kwargs): + ''' + Also handle: - Optionally save the command to cmd_file file, and add extra_env - environment variables to the command generated. + - modified defaults from child classes. + - common arguments to forward on Python calls + ''' + shortname, longname, key, is_option = self.get_key(*args, **kwargs) + if key in self._defaults: + kwargs['default'] = self._defaults[key] + if self._is_common: + self._common_args.add(key) + super().add_argument(*args, **kwargs) + + @staticmethod + def base64_encode(string): + return base64.b64encode(string.encode()).decode() + + def get_elf_entry(self, elf_file_path): + readelf_header = subprocess.check_output([ + self.get_toolchain_tool('readelf'), + '-h', + elf_file_path + ]) + for line in readelf_header.decode().split('\n'): + split = line.split() + if line.startswith(' Entry point address:'): + addr = line.split()[-1] + break + return int(addr, 0) + + def gem5_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 - If cmd contains at least one common.Newline, newlines are only added on common.Newline. - Otherwise, newlines are added automatically after every word. - ''' - global dry_run - if type(cmd) is str: - cmd_string = cmd - else: - cmd_string = cmd_to_string(cmd, cwd=cwd, extra_env=extra_env, extra_paths=extra_paths) - print(common.command_prefix + cmd_string) - if cmd_file is not None: - with open(cmd_file, 'w') as f: - f.write('#!/usr/bin/env bash\n') - f.write(cmd_string) - st = os.stat(cmd_file) - os.chmod(cmd_file, st.st_mode | stat.S_IXUSR) - -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(common.qemu_img_executable): - disable_trace = [] - qemu_img_executable = common.qemu_img_basename - else: - # Prevent qemu-img from generating trace files like QEMU. Disgusting. - disable_trace = ['-T', 'pr_manager_run,file=/dev/null', common.Newline,] - qemu_img_executable = common.qemu_img_executable - infmt = 'raw' - outfmt = 'qcow2' - infile = common.rootfs_raw_file - outfile = common.qcow2_file - if reverse: - tmp = infmt - infmt = outfmt - outfmt = tmp - tmp = infile - infile = outfile - outfile = tmp - common.run_cmd( - [ - qemu_img_executable, common.Newline, - ] + - disable_trace + - [ - 'convert', common.Newline, - '-f', infmt, common.Newline, - '-O', outfmt, common.Newline, - infile, common.Newline, - outfile, common.Newline, - ] - ) - -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 rmrf(path): - print_cmd(['rm', '-r', '-f', path]) - if not common.dry_run and os.path.exists(path): - shutil.rmtree(path) - -def run_cmd( - cmd, - cmd_file=None, - out_file=None, - show_stdout=True, - show_cmd=True, - extra_env=None, - extra_paths=None, - delete_env=None, - dry_run=False, - raise_on_failure=True, - **kwargs - ): - ''' - Run a command. Write the command to stdout before running it. + def get_common_args(self): + ''' + These are arguments that might be used by more than one script, + and are all defined in this class instead of in the derived class + of the script. + ''' + return { + key:self.env[key] for key in self._common_args if self.env['_args_given'][key] + } + + 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)) + crosstool_ng_full_prefix = os.path.join(self.env['crosstool_ng_bin_dir'], self.env['crosstool_ng_toolchain_prefix']) + crosstool_ng_exists = os.path.exists('{}-{}'.format(crosstool_ng_full_prefix, tool)) + host_tool = '{}-{}'.format(self.env['ubuntu_toolchain_prefix'], tool) + host_path = shutil.which(host_tool) + if host_path is not None: + host_exists = True + host_full_prefix = host_path[:-(len(tool)+1)] + else: + host_exists = False + host_full_prefix = None + known_toolchains = { + 'crosstool-ng': (crosstool_ng_exists, crosstool_ng_full_prefix), + 'buildroot': (buildroot_exists, buildroot_full_prefix), + 'host': (host_exists, host_full_prefix), + } + if allowed_toolchains is None: + if self.env['baremetal'] is None: + allowed_toolchains = ['buildroot', 'crosstool-ng', 'host'] + else: + allowed_toolchains = ['crosstool-ng', 'buildroot', 'host'] + tried = [] + for toolchain in allowed_toolchains: + exists, prefix = known_toolchains[toolchain] + tried.append('{}-{}'.format(prefix, tool)) + if exists: + return prefix + error_message = 'Tool not found. Tried:\n' + '\n'.join(tried) + if self.env['dry_run']: + self.log_error(error_message) + return '' + else: + raise Exception(error_message) + + def get_toolchain_tool(self, tool, allowed_toolchains=None): + return '{}-{}'.format(self.get_toolchain_prefix(tool, allowed_toolchains), tool) + + def github_make_request( + self, + 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/' + self.env['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 - Wait until the command finishes execution. + def import_path(self, basename): + ''' + https://stackoverflow.com/questions/2601047/import-a-python-module-without-the-py-extension + https://stackoverflow.com/questions/31773310/what-does-the-first-argument-of-the-imp-load-source-method-do + ''' + return imp.load_source(basename.replace('-', '_'), os.path.join(self.env['root_dir'], basename)) - :param cmd: command to run. common.Newline entries are magic get skipped. - :type cmd: List[str] + def import_path_main(self, path): + ''' + Import an object of the Main class of a given file. - :param cmd_file: if not None, write the command to be run to that file - :type cmd_file: str + By convention, we call the main object of all our CLI scripts as Main. + ''' + return self.import_path(path).Main() - :param out_file: if not None, write the stdout and stderr of the command the file - :type out_file: str + def is_arch_supported(self, arch): + return self.supported_archs is None or arch in self.supported_archs - :param show_stdout: wether to show stdout and stderr on the terminal or not - :type show_stdout: bool + def log_error(self, msg): + print('error: {}'.format(msg), file=sys.stdout) - :param extra_env: extra environment variables to add when running the command - :type extra_env: Dict[str,str] + def log_info(self, msg='', flush=False, **kwargs): + if not self.env['quiet']: + print('{}'.format(msg), **kwargs) + if flush: + sys.stdout.flush() - :param dry_run: don't run the commands, just potentially print them. Debug aid. - :type dry_run: Bool - ''' - if out_file is not None: - stdout = subprocess.PIPE - stderr = subprocess.STDOUT - else: - if show_stdout: - stdout = None - stderr = None - else: - stdout = subprocess.DEVNULL - stderr = subprocess.DEVNULL - if extra_env is None: - extra_env = {} - if delete_env is None: - delete_env = [] - if 'cwd' in kwargs: - cwd = kwargs['cwd'] - else: - cwd = None - env = os.environ.copy() - env.update(extra_env) - if extra_paths is not None: - path = ':'.join(extra_paths) - if 'PATH' in os.environ: - path += ':' + os.environ['PATH'] - env['PATH'] = path - for key in delete_env: - if key in env: - del env[key] - if show_cmd: - print_cmd(cmd, cwd=cwd, cmd_file=cmd_file, extra_env=extra_env, extra_paths=extra_paths) - - # Otherwise Ctrl + C gives: - # - ugly Python stack trace for gem5 (QEMU takes over terminal and is fine). - # - kills Python, and that then kills GDB: https://stackoverflow.com/questions/19807134/does-python-always-raise-an-exception-if-you-do-ctrlc-when-a-subprocess-is-exec - sigint_old = signal.getsignal(signal.SIGINT) - signal.signal(signal.SIGINT, signal.SIG_IGN) - - # Otherwise BrokenPipeError when piping through | grep - # But if I do this_module, my terminal gets broken at the end. Why, why, why. - # https://stackoverflow.com/questions/14207708/ioerror-errno-32-broken-pipe-python - # Ignoring the exception is not enough as it prints a warning anyways. - #sigpipe_old = signal.getsignal(signal.SIGPIPE) - #signal.signal(signal.SIGPIPE, signal.SIG_DFL) - - cmd = common.strip_newlines(cmd) - if not dry_run and not common.dry_run: - # https://stackoverflow.com/questions/15535240/python-popen-write-to-stdout-and-log-file-simultaneously/52090802#52090802 - with subprocess.Popen(cmd, stdout=stdout, stderr=stderr, env=env, **kwargs) as proc: - if out_file is not None: - os.makedirs(os.path.split(os.path.abspath(out_file))[0], exist_ok=True) - with open(out_file, 'bw') as logfile: - while True: - byte = proc.stdout.read(1) - if byte: - if show_stdout: - sys.stdout.buffer.write(byte) - try: - sys.stdout.flush() - except BlockingIOError: - # TODO understand. Why, Python, why. - pass - logfile.write(byte) - else: - break - signal.signal(signal.SIGINT, sigint_old) - #signal.signal(signal.SIGPIPE, sigpipe_old) - returncode = proc.returncode - if returncode != 0 and raise_on_failure: - raise Exception('Command exited with status: {}'.format(returncode)) - return returncode - else: - return 0 + def main(self, *args, **kwargs): + ''' + Run timed_main across all selected archs and emulators. -def setup(parser): - ''' - Parse the command line arguments, and setup several variables based on them. - Typically done after getting inputs from the command line arguments. - ''' - args = parser.parse_args() - if args.qemu or not args.gem5: - common.emulator = 'qemu' - else: - common.emulator = 'gem5' - if args.arch in common.arch_short_to_long_dict: - args.arch = common.arch_short_to_long_dict[args.arch] - if args.gem5_build_id is None: - args.gem5_build_id = default_build_id - gem5_build_id_given = False - else: - gem5_build_id_given = True - if args.userland_build_id is None: - args.userland_build_id = default_build_id - common.userland_build_id_given = False - else: - common.userland_build_id_given = True - if args.gem5_worktree is not None and not gem5_build_id_given: - args.gem5_build_id = args.gem5_worktree - common.machine = args.machine - common.setup_dry_run_arguments(args) - common.is_arm = False - if args.arch == 'arm': - common.armv = 7 - common.gem5_arch = 'ARM' - common.mcpu = 'cortex-a15' - common.buildroot_toolchain_prefix = 'arm-buildroot-linux-uclibcgnueabihf' - common.crosstool_ng_toolchain_prefix = 'arm-unknown-eabi' - common.ubuntu_toolchain_prefix = 'arm-linux-gnueabihf' - common.is_arm = True - elif args.arch == 'aarch64': - common.armv = 8 - common.gem5_arch = 'ARM' - common.mcpu = 'cortex-a57' - common.buildroot_toolchain_prefix = 'aarch64-buildroot-linux-uclibc' - common.crosstool_ng_toolchain_prefix = 'aarch64-unknown-elf' - common.ubuntu_toolchain_prefix = 'aarch64-linux-gnu' - common.is_arm = True - elif args.arch == 'x86_64': - common.crosstool_ng_toolchain_prefix = 'x86_64-unknown-elf' - common.gem5_arch = 'X86' - common.buildroot_toolchain_prefix = 'x86_64-buildroot-linux-uclibc' - common.ubuntu_toolchain_prefix = 'x86_64-linux-gnu' - if common.emulator == 'gem5': - if common.machine is None: - common.machine = 'TODO' - else: - if common.machine is None: - common.machine = 'pc' - if is_arm: - if common.emulator == 'gem5': - if common.machine is None: - if args.dp650: - common.machine = 'VExpress_GEM5_V1_DPU' - else: - common.machine = 'VExpress_GEM5_V1' + :return: if any of the timed_mains exits non-zero and non-null, + return that. Otherwise, return 0. + ''' + env = kwargs.copy() + self.input_args = env.copy() + env.update(consts) + real_all_archs = env['all_archs'] + if real_all_archs: + real_archs = consts['all_long_archs'] else: - if common.machine is None: - common.machine = 'virt' - common.buildroot_out_dir = os.path.join(common.out_dir, 'buildroot') - common.buildroot_build_dir = os.path.join(common.buildroot_out_dir, 'build', args.buildroot_build_id, args.arch) - common.buildroot_download_dir = os.path.join(common.buildroot_out_dir, 'download') - common.buildroot_config_file = os.path.join(common.buildroot_build_dir, '.config') - common.buildroot_build_build_dir = os.path.join(common.buildroot_build_dir, 'build') - common.buildroot_linux_build_dir = os.path.join(common.buildroot_build_build_dir, 'linux-custom') - common.buildroot_vmlinux = os.path.join(common.buildroot_linux_build_dir, "vmlinux") - common.qemu_build_dir = os.path.join(common.out_dir, 'qemu', args.qemu_build_id) - common.qemu_executable_basename = 'qemu-system-{}'.format(args.arch) - common.qemu_executable = os.path.join(common.qemu_build_dir, '{}-softmmu'.format(args.arch), common.qemu_executable_basename) - common.qemu_img_basename = 'qemu-img' - common.qemu_img_executable = os.path.join(common.qemu_build_dir, common.qemu_img_basename) - common.host_dir = os.path.join(common.buildroot_build_dir, 'host') - common.host_bin_dir = os.path.join(common.host_dir, 'usr', 'bin') - common.buildroot_pkg_config = os.path.join(common.host_bin_dir, 'pkg-config') - common.buildroot_images_dir = os.path.join(common.buildroot_build_dir, 'images') - common.buildroot_rootfs_raw_file = os.path.join(common.buildroot_images_dir, 'rootfs.ext2') - common.buildroot_qcow2_file = common.buildroot_rootfs_raw_file + '.qcow2' - common.staging_dir = os.path.join(common.out_dir, 'staging', args.arch) - common.buildroot_staging_dir = os.path.join(common.buildroot_build_dir, 'staging') - common.target_dir = os.path.join(common.buildroot_build_dir, 'target') - common.run_dir_base = os.path.join(common.out_dir, 'run') - common.gem5_run_dir = os.path.join(common.run_dir_base, 'gem5', args.arch, str(args.run_id)) - common.m5out_dir = os.path.join(common.gem5_run_dir, 'm5out') - common.stats_file = os.path.join(common.m5out_dir, 'stats.txt') - common.gem5_trace_txt_file = os.path.join(common.m5out_dir, 'trace.txt') - common.gem5_guest_terminal_file = os.path.join(common.m5out_dir, 'system.terminal') - common.gem5_readfile = os.path.join(common.gem5_run_dir, 'readfile') - common.gem5_termout_file = os.path.join(common.gem5_run_dir, 'termout.txt') - common.qemu_run_dir = os.path.join(common.run_dir_base, 'qemu', args.arch, str(args.run_id)) - common.qemu_trace_basename = 'trace.bin' - common.qemu_trace_file = os.path.join(common.qemu_run_dir, 'trace.bin') - common.qemu_trace_txt_file = os.path.join(common.qemu_run_dir, 'trace.txt') - common.qemu_termout_file = os.path.join(common.qemu_run_dir, 'termout.txt') - common.qemu_rrfile = os.path.join(common.qemu_run_dir, 'rrfile') - common.qemu_guest_terminal_file = os.path.join(common.m5out_dir, qemu_termout_file) - common.gem5_out_dir = os.path.join(common.out_dir, 'gem5') - if args.gem5_build_dir is None: - common.gem5_build_dir = os.path.join(common.gem5_out_dir, args.gem5_build_id, args.gem5_build_type) - else: - common.gem5_build_dir = args.gem5_build_dir - common.gem5_fake_iso = os.path.join(common.gem5_out_dir, 'fake.iso') - common.gem5_m5term = os.path.join(common.gem5_build_dir, 'm5term') - common.gem5_build_build_dir = os.path.join(common.gem5_build_dir, 'build') - common.gem5_executable = os.path.join(common.gem5_build_build_dir, gem5_arch, 'gem5.{}'.format(args.gem5_build_type)) - common.gem5_system_dir = os.path.join(common.gem5_build_dir, 'system') - common.crosstool_ng_out_dir = os.path.join(common.out_dir, 'crosstool-ng') - common.crosstool_ng_buildid_dir = os.path.join(common.crosstool_ng_out_dir, 'build', args.crosstool_ng_build_id) - common.crosstool_ng_install_dir = os.path.join(common.crosstool_ng_buildid_dir, 'install', args.arch) - common.crosstool_ng_bin_dir = os.path.join(common.crosstool_ng_install_dir, 'bin') - common.crosstool_ng_util_dir = os.path.join(common.crosstool_ng_buildid_dir, 'util') - common.crosstool_ng_config = os.path.join(common.crosstool_ng_util_dir, '.config') - common.crosstool_ng_defconfig = os.path.join(common.crosstool_ng_util_dir, 'defconfig') - common.crosstool_ng_executable = os.path.join(common.crosstool_ng_util_dir, 'ct-ng') - common.crosstool_ng_build_dir = os.path.join(common.crosstool_ng_buildid_dir, 'build') - common.crosstool_ng_download_dir = os.path.join(common.crosstool_ng_out_dir, 'download') - common.gem5_default_src_dir = os.path.join(submodules_dir, 'gem5') - if args.gem5_source_dir is not None: - common.gem5_src_dir = args.gem5_source_dir - assert(os.path.exists(args.gem5_source_dir)) - else: - if args.gem5_worktree is not None: - common.gem5_src_dir = os.path.join(common.gem5_non_default_src_root_dir, args.gem5_worktree) + real_archs = env['archs'] + if env['all_emulators']: + real_emulators = consts['all_long_emulators'] else: - common.gem5_src_dir = common.gem5_default_src_dir - common.gem5_m5_src_dir = os.path.join(common.gem5_src_dir, 'util', 'm5') - common.gem5_m5_build_dir = os.path.join(common.out_dir, 'util', 'm5') - if common.emulator == 'gem5': - common.executable = common.gem5_executable - common.run_dir = common.gem5_run_dir - common.termout_file = common.gem5_termout_file - common.guest_terminal_file = gem5_guest_terminal_file - common.trace_txt_file = gem5_trace_txt_file - else: - common.executable = common.qemu_executable - common.run_dir = common.qemu_run_dir - common.termout_file = common.qemu_termout_file - common.guest_terminal_file = qemu_guest_terminal_file - common.trace_txt_file = qemu_trace_txt_file - common.gem5_config_dir = os.path.join(common.gem5_src_dir, 'configs') - common.gem5_se_file = os.path.join(common.gem5_config_dir, 'example', 'se.py') - common.gem5_fs_file = os.path.join(common.gem5_config_dir, 'example', 'fs.py') - common.run_cmd_file = os.path.join(common.run_dir, 'run.sh') - - # Linux - if args.linux_source_dir is None: - common.linux_source_dir = os.path.join(submodules_dir, 'linux') - else: - common.linux_source_dir = args.linux_source_dir - common.extract_vmlinux = os.path.join(linux_source_dir, 'scripts', 'extract-vmlinux') - common.linux_buildroot_build_dir = os.path.join(common.buildroot_build_build_dir, 'linux-custom') - if args.linux_build_dir is None: - common.linux_build_dir = os.path.join(common.out_dir, 'linux', args.linux_build_id, args.arch) - else: - common.linux_build_dir = args.linux_build_dir - common.lkmc_vmlinux = os.path.join(common.linux_build_dir, 'vmlinux') - if args.arch == 'arm': - common.linux_arch = 'arm' - common.linux_image_prefix = os.path.join('arch', common.linux_arch, 'boot', 'zImage') - elif args.arch == 'aarch64': - common.linux_arch = 'arm64' - common.linux_image_prefix = os.path.join('arch', common.linux_arch, 'boot', 'Image') - elif args.arch == 'x86_64': - common.linux_arch = 'x86' - common.linux_image_prefix = os.path.join('arch', common.linux_arch, 'boot', 'bzImage') - common.lkmc_linux_image = os.path.join(common.linux_build_dir, common.linux_image_prefix) - common.buildroot_linux_image = os.path.join(common.buildroot_linux_build_dir, linux_image_prefix) - if args.buildroot_linux: - common.vmlinux = common.buildroot_vmlinux - common.linux_image = common.buildroot_linux_image - else: - common.vmlinux = common.lkmc_vmlinux - common.linux_image = common.lkmc_linux_image - - # Kernel modules. - common.kernel_modules_build_base_dir = os.path.join(common.out_dir, 'kernel_modules') - common.kernel_modules_build_dir = os.path.join(common.kernel_modules_build_base_dir, args.arch) - common.kernel_modules_build_subdir = os.path.join(common.kernel_modules_build_dir, kernel_modules_subdir) - common.kernel_modules_build_host_dir = os.path.join(common.kernel_modules_build_base_dir, 'host') - common.kernel_modules_build_host_subdir = os.path.join(common.kernel_modules_build_host_dir, kernel_modules_subdir) - common.userland_build_dir = os.path.join(common.out_dir, 'userland', args.userland_build_id, args.arch) - common.out_rootfs_overlay_dir = os.path.join(common.out_dir, 'rootfs_overlay', args.arch) - common.out_rootfs_overlay_bin_dir = os.path.join(common.out_rootfs_overlay_dir, 'bin') - - # Ports - if args.port_offset is None: + real_emulators = env['emulators'] + return_value = 0 + class GetOutOfLoop(Exception): pass try: - args.port_offset = int(args.run_id) - except ValueError: - args.port_offset = 0 - if common.emulator == 'gem5': - common.gem5_telnet_port = 3456 + args.port_offset - common.gdb_port = 7000 + args.port_offset - else: - common.qemu_base_port = 45454 + 10 * args.port_offset - common.qemu_monitor_port = common.qemu_base_port + 0 - common.qemu_hostfwd_generic_port = common.qemu_base_port + 1 - common.qemu_hostfwd_ssh_port = common.qemu_base_port + 2 - common.qemu_gdb_port = common.qemu_base_port + 3 - common.extra_serial_port = common.qemu_base_port + 4 - common.gdb_port = common.qemu_gdb_port - common.qemu_background_serial_file = os.path.join(common.qemu_run_dir, 'background.log') - - # Baremetal. - common.baremetal = args.baremetal - common.baremetal_lib_basename = 'lib' - common.baremetal_src_dir = os.path.join(common.root_dir, 'baremetal') - common.baremetal_src_lib_dir = os.path.join(common.baremetal_src_dir, common.baremetal_lib_basename) - if common.emulator == 'gem5': - common.simulator_name = 'gem5' - else: - common.simulator_name = 'qemu' - common.baremetal_build_dir = os.path.join(out_dir, 'baremetal', args.arch, common.simulator_name, common.machine) - common.baremetal_build_lib_dir = os.path.join(common.baremetal_build_dir, common.baremetal_lib_basename) - common.baremetal_build_ext = '.elf' - - # Docker - common.docker_build_dir = os.path.join(common.out_dir, 'docker', args.arch) - common.docker_tar_dir = os.path.join(common.docker_build_dir, 'export') - common.docker_tar_file = os.path.join(common.docker_build_dir, 'export.tar') - common.docker_rootfs_raw_file = os.path.join(common.docker_build_dir, 'export.ext2') - common.docker_qcow2_file = os.path.join(common.docker_rootfs_raw_file + '.qcow2') - if args.docker: - common.rootfs_raw_file = common.docker_rootfs_raw_file - common.qcow2_file = common.docker_qcow2_file - else: - common.rootfs_raw_file = common.buildroot_rootfs_raw_file - common.qcow2_file = common.buildroot_qcow2_file - - # Image. - if common.baremetal is None: - if common.emulator == 'gem5': - common.image = common.vmlinux - common.disk_image = common.rootfs_raw_file + ret = self.setup() + if ret is not None and ret != 0: + return_value = ret + raise GetOutOfLoop() + for emulator in real_emulators: + for arch in real_archs: + if arch in env['arch_short_to_long_dict']: + arch = env['arch_short_to_long_dict'][arch] + if self.is_arch_supported(arch): + if not env['dry_run']: + start_time = time.time() + env['arch'] = arch + env['archs'] = [arch] + env['_args_given']['archs'] = True + env['all_archs'] = False + env['emulator'] = emulator + env['emulators'] = [emulator] + env['_args_given']['emulators'] = True + env['all_emulators'] = False + self.env = env.copy() + self._init_env(self.env) + self.sh = shell_helpers.ShellHelpers( + dry_run=self.env['dry_run'], + quiet=self.env['quiet'], + ) + ret = self.timed_main() + if not env['dry_run']: + end_time = time.time() + self.ellapsed_seconds = end_time - start_time + self.print_time(self.ellapsed_seconds) + if ret is not None and ret != 0: + return_value = ret + if self.env['quit_on_fail']: + raise GetOutOfLoop() + elif not real_all_archs: + raise Exception('Unsupported arch for this action: ' + arch) + + except GetOutOfLoop: + pass + ret = self.teardown() + if ret is not None and ret != 0: + return_value = ret + return return_value + + 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 seconds_to_hms(seconds): + ''' + Seconds to hour:minute:seconds + + :ptype seconds: float + :rtype: str + + https://stackoverflow.com/questions/775049/how-do-i-convert-seconds-to-hours-minutes-and-seconds + ''' + frac, whole = math.modf(seconds) + hours, rem = divmod(whole, 3600) + minutes, seconds = divmod(rem, 60) + return '{:02}:{:02}:{:02}'.format(int(hours), int(minutes), int(seconds)) + + def print_time(self, ellapsed_seconds): + if self.env['print_time'] and not self.env['quiet']: + print('time {}'.format(self.seconds_to_hms(ellapsed_seconds))) + + def raw_to_qcow2(self, 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 + + def resolve_executable(self, in_path, magic_in_dir, magic_out_dir, out_ext): + if os.path.isabs(in_path): + return in_path else: - common.image = common.linux_image - common.disk_image = common.qcow2_file - else: - common.disk_image = common.gem5_fake_iso - if common.baremetal == 'all': - path = common.baremetal + 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 + if not self.env['dry_run']: + raise Exception('Executable file not found. Tried:\n' + '\n'.join(paths)) + + def resolve_userland(self, path): + return self.resolve_executable( + path, + self.env['userland_source_dir'], + self.env['userland_build_dir'], + self.env['userland_build_ext'], + ) + + def setup(self): + ''' + Similar to timed_main, but gets run only once for all --arch and --emulator, + before timed_main. + + Different from __init__, since at this point env has already been calculated, + so variables that don't depend on --arch or --emulator can be used. + ''' + pass + + def timed_main(self): + ''' + Main action of the derived class. + + Gets run once for every --arch and every --emulator. + ''' + pass + + def teardown(self): + ''' + Similar to setup, but run after timed_main. + ''' + pass + +class BuildCliFunction(LkmcCliFunction): + ''' + A CLI function with common facilities to build stuff, e.g.: + + * `--clean` to clean the build directory + * `--nproc` to set he number of build threads + ''' + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.add_argument( + '--clean', + default=False, + help='Clean the build instead of building.', + ), + self.add_argument( + '-j', + '--nproc', + default=multiprocessing.cpu_count(), + type=int, + help='Number of processors to use for the build.', + ) + self.test_results = [] + + def clean(self): + build_dir = self.get_build_dir() + if build_dir is not None: + self.sh.rmrf(build_dir) + + def build(self): + ''' + Do the actual main build work. + ''' + raise NotImplementedError() + + def get_build_dir(self): + return None + + def timed_main(self): + ''' + Parse CLI, and to the build based on it. + + The actual build work is done by do_build in implementing classes. + ''' + if self.env['clean']: + return self.clean() else: - path = common.resolve_executable( - common.baremetal, - common.baremetal_src_dir, - common.baremetal_build_dir, - common.baremetal_build_ext, - ) - source_path_noext = os.path.splitext(os.path.join( - common.baremetal_src_dir, - os.path.relpath(path, common.baremetal_build_dir) - ))[0] - for ext in [c_ext, asm_ext]: - source_path = source_path_noext + ext - if os.path.exists(source_path): - common.source_path = source_path - break - common.image = path - return args - -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 common.resolve_executable( - path, - common.userland_src_dir, - common.userland_build_dir, - common.userland_build_ext, - ) - -def setup_dry_run_arguments(args): - common.dry_run = args.dry_run - -def shlex_split(string): + return self.build() + +# from aenum import Enum # for the aenum version +TestResult = enum.Enum('TestResult', ['PASS', 'FAIL']) + +class Test: + def __init__( + self, + test_id: str, + result : TestResult =None, + ellapsed_seconds : float =None + ): + self.test_id = test_id + self.result = result + self.ellapsed_seconds = ellapsed_seconds + def __str__(self): + out = [] + if self.result is not None: + out.append(self.result.name) + if self.ellapsed_seconds is not None: + out.append(LkmcCliFunction.seconds_to_hms(self.ellapsed_seconds)) + out.append(self.test_id) + return ' '.join(out) + +class TestCliFunction(LkmcCliFunction): ''' - shlex_split, but also add Newline after every word. + Represents a CLI command that runs tests. - Not perfect since it does not group arguments, but I don't see a solution. + Automates test reporting boilerplate for those commands. ''' - return common.add_newlines(shlex.split(string)) - -def strip_newlines(cmd): - return [x for x in cmd if x != common.Newline] - -def write_configs(config_path, configs, config_fragments=None): - """ - Write extra configs into the Buildroot config file. - TODO Can't get rid of these for now with nice fragments: - http://stackoverflow.com/questions/44078245/is-it-possible-to-use-config-fragments-with-buildroots-config - """ - if config_fragments is None: - config_fragments = [] - with open(config_path, 'a') as config_file: - for config_fragment in config_fragments: - with open(config_fragment, 'r') as config_fragment_file: - print_cmd(['cat', config_fragment, '>>', config_path]) - if not common.dry_run: - for line in config_fragment_file: - config_file.write(line) - write_string_to_file(config_path, '\n'.join(configs), mode='a') + + def __init__(self, *args, **kwargs): + defaults = { + 'print_time': False, + } + if 'defaults' in kwargs: + defaults.update(kwargs['defaults']) + kwargs['defaults'] = defaults + super().__init__(*args, **kwargs) + self.tests = [] + + def run_test(self, run_obj, run_args=None, test_id=None): + ''' + This is a setup / run / teardown setup for simple tests that just do a single run. + + More complex tests might need to run the steps separately, e.g. gdb tests + must run multiple commands: one for the run and one GDB. + + :param run_obj: callable object + :param run_args: arguments to be passed to the runnable object + :param test_id: test identifier, to be added in addition to of arch and emulator ids + ''' + if run_obj.is_arch_supported(self.env['arch']): + if run_args is None: + run_args = {} + test_id_string = self.test_setup(test_id) + exit_status = run_obj(**run_args) + self.test_teardown(run_obj, exit_status, test_id_string) + + def test_setup(self, test_id): + test_id_string = '{} {}'.format(self.env['emulator'], self.env['arch']) + if test_id is not None: + test_id_string += ' {}'.format(test_id) + self.log_info('test_id {}'.format(test_id_string), flush=True) + return test_id_string + + def test_teardown(self, run_obj, exit_status, test_id_string): + if not self.env['dry_run']: + if exit_status == 0: + test_result = TestResult.PASS + else: + test_result = TestResult.FAIL + if self.env['quit_on_fail']: + self.log_error('Test failed') + sys.exit(1) + self.log_info('test_result {}'.format(test_result.name)) + ellapsed_seconds = run_obj.ellapsed_seconds + else: + test_result = None + ellapsed_seconds = None + self.log_info() + self.tests.append(Test(test_id_string, test_result, ellapsed_seconds)) + + def teardown(self): + ''' + :return: 1 if any test failed, 0 otherwise + ''' + self.log_info('Test result summary') + passes = [] + fails = [] + for test in self.tests: + if test.result in (TestResult.PASS, None): + passes.append(test) + else: + fails.append(test) + if passes: + for test in passes: + self.log_info(test) + if fails: + for test in fails: + self.log_info(test) + self.log_error('A test failed') + return 1 + return 0 diff --git a/config.example b/config.example deleted file mode 100644 index dd8c8d96..00000000 --- a/config.example +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env python -arch = 'aarch64' -gem5 = True -run_id = 'asdf' diff --git a/config.py b/config.py new file mode 100644 index 00000000..4846754b --- /dev/null +++ b/config.py @@ -0,0 +1,10 @@ +''' +https://github.com/cirosantilli/linux-kernel-module-cheat#default-command-line-arguments +''' + +def set_args(args, script_name): + args['arch'] = 'aarch64' + args['emulators'] = ['gem5'] + if script_name == 'build-gem5': + # This argument is defined only for ./build-gem5. + args['extra_scons_args'] = ['ADSF=qwer'] diff --git a/copy-overlay b/copy-overlay index bbf34702..53bae552 100755 --- a/copy-overlay +++ b/copy-overlay @@ -5,25 +5,21 @@ import os import shutil import common +from shell_helpers import LF -class CopyOverlayComponent(common.Component): - def do_build(self, args): +class Main(common.BuildCliFunction): + def __init__(self): + super().__init__( + description='''\ +https://github.com/cirosantilli/linux-kernel-module-cheat#rootfs_overlay +''') + def build(self): + # TODO: print rsync equivalent, move into shell_helpers. distutils.dir_util.copy_tree( - common.rootfs_overlay_dir, - common.out_rootfs_overlay_dir, + self.env['rootfs_overlay_dir'], + self.env['out_rootfs_overlay_dir'], update=1, ) - def get_argparse_args(self): - return { - 'description': '''\ -Copy our git tracked rootfs_overlay to the final generated rootfs_overlay -that also contains generated build outputs. This has the following advantages -over just adding that to BR2_ROOTFS_OVERLAY: -- also works for non Buildroot root filesystesms -- places everything in one place for a nice 9P mount -''', - } - if __name__ == '__main__': - CopyOverlayComponent().build() + Main().cli() diff --git a/gem5-bench-cache b/gem5-bench-cache index 4267680b..d1d0476d 100755 --- a/gem5-bench-cache +++ b/gem5-bench-cache @@ -13,7 +13,7 @@ while getopts "C" OPT; do esac done shift "$(($OPTIND - 1))" -common_opts="--gem5 $@" +common_opts="--emulator gem5 $@" # Vars cmd="./run ${common_opts}" diff --git a/gem5-bench-dhrystone b/gem5-bench-dhrystone index 0910c4c8..24d069dd 100755 --- a/gem5-bench-dhrystone +++ b/gem5-bench-dhrystone @@ -6,11 +6,11 @@ set -eu root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" outfile="${root_dir}/out/gem5-bench-dhrystone.txt" arch=aarch64 -cmd="./run -a '$arch' --gem5 --eval-busybox '/gem5.sh'" +cmd="./run --arch '$arch' --emulator gem5 --eval-busybox '/gem5.sh'" # These cache sizes roughly match the ARM Cortex A75 # https://en.wikipedia.org/wiki/ARM_Cortex-A75 -restore='-l 1 -- --cpu-type=HPI --restore-with-cpu=HPI --caches --l2cache --l1d_size=64kB --l1i_size=64kB --l2_size=256kB' +restore='--gem5-restore 1 -- --cpu-type=HPI --restore-with-cpu=HPI --caches --l2cache --l1d_size=64kB --l1i_size=64kB --l2_size=256kB' # Generate a checkpoint after Linux boots, using the faster and less detailed CPU. # The boot takes a while, be patient young Padawan. diff --git a/gem5-shell b/gem5-shell index f669b574..58416971 100755 --- a/gem5-shell +++ b/gem5-shell @@ -1,16 +1,23 @@ #!/usr/bin/env python3 -import sys - import common +from shell_helpers import LF + +class Main(common.LkmcCliFunction): + def __init__(self): + super().__init__( + defaults={ + 'emulators': ['gem5'], + }, + description='Connect a terminal to a running gem5 instance', + ) + def timed_main(self): + return self.sh.run_cmd([ + self.env['gem5_m5term'], + 'localhost', + str(self.env['gem5_telnet_port']), + LF, + ]) -parser = common.get_argparse( - default_args={'gem5':True}, - argparse_args={'description':'Connect a terminal to a running gem5 instance'} -) -args = common.setup(parser) -sys.exit(common.run_cmd([ - common.gem5_m5term, common.Newline, - 'localhost', common.Newline, - str(common.gem5_telnet_port), common.Newline, -])) +if __name__ == '__main__': + Main().cli() diff --git a/gem5-stat b/gem5-stat index 9b19b4c8..cf1adffd 100755 --- a/gem5-stat +++ b/gem5-stat @@ -1,14 +1,26 @@ #!/usr/bin/env python3 + import common -parser = common.get_argparse( - argparse_args={'description':'Get the value of a gem5 stat from the stats.txt file.'} -) -parser.add_argument( - 'stat', - default=None, - help='Python regexp matching the full stat name of interest', - nargs='?', -) -args = common.setup(parser) -stats = common.get_stats(args.stat) -print('\n'.join(stats)) + +class Main(common.LkmcCliFunction): + def __init__(self): + super().__init__( + defaults={ + 'print_time': False, + }, + description='''\ +Get the value of a gem5 stat from the stats.txt file. +''', + ) + self.add_argument( + 'stat', + help='Python regexp matching the full stat name of interest', + nargs='?', + ) + + def timed_main(self): + stats = self.get_stats(self.env['stat']) + print('\n'.join(stats)) + +if __name__ == '__main__': + Main().cli() diff --git a/getvar b/getvar index 431d4f99..e9c7c28d 100755 --- a/getvar +++ b/getvar @@ -1,16 +1,20 @@ #!/usr/bin/env python3 -import types - import common -parser = common.get_argparse(argparse_args={ - 'description': '''Print the value of a common.py variable. +class Main(common.LkmcCliFunction): + def __init__(self): + super().__init__( + defaults = { + 'print_time': False, + }, + description='''\ +Print the value of a self.env['py'] variable. This is useful to: * give dry commands on the README that don't change when we refactor directory structure -* create simple bash scripts that call use common.py variables +* create simple bash scripts that call use self.env['py'] variables For example, to get the Buildroot output directory for an ARM build, use: @@ -23,16 +27,22 @@ List all available variables: .... ./%(prog)s .... -.... -''' -}) -parser.add_argument('variable', nargs='?') -args = common.setup(parser) -if args.variable: - print(getattr(common, args.variable)) -else: - for attr in dir(common): - if not attr.startswith('__'): - val = getattr(common, attr) - if not callable(val) and not type(val) is types.ModuleType: - print('{} {}'.format(attr, val)) +''', + ) + self.add_argument('--type', choices=['input', 'all'], default='all') + self.add_argument('variable', nargs='?') + + def timed_main(self): + variable = self.env['variable'] + if variable: + print(self.env[variable]) + else: + if self.env['type'] == 'input': + to_print = self.input_args + elif self.env['type'] == 'all': + to_print = self.env + for key in sorted(to_print): + print('{}={}'.format(key, self.env[key])) + +if __name__ == '__main__': + Main().cli() diff --git a/qemu-monitor b/qemu-monitor index a2bcd9dc..50e8f515 100755 --- a/qemu-monitor +++ b/qemu-monitor @@ -5,10 +5,11 @@ import sys import telnetlib import common +from shell_helpers import LF 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,24 +22,24 @@ 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')) return '\n'.join(tn.read_until(prompt).decode('utf-8').splitlines()[1:])[:-len(prompt)] -with telnetlib.Telnet('localhost', common.qemu_monitor_port) as tn: +with telnetlib.Telnet('localhost', kwargs['qemu_monitor_port']) as tn: # Couldn't disable server echo, so just removing the write for now. # https://stackoverflow.com/questions/12421799/how-to-disable-telnet-echo-in-python-telnetlib # sock = tn.get_socket() # sock.send(telnetlib.IAC + telnetlib.WILL + telnetlib.ECHO) if os.isatty(sys.stdin.fileno()): - if args.command == []: + if kwargs['command'] == []: print(tn.read_until(prompt).decode('utf-8'), end='') tn.interact() else: tn.read_until(prompt) - print(write_and_read(tn, ' '.join(args.command) + '\n', prompt)) + print(write_and_read(tn, ' '.join(kwargs['command']) + '\n', prompt)) else: tn.read_until(prompt) print(write_and_read(tn, sys.stdin.read() + '\n', prompt)) diff --git a/qemu-trace2txt b/qemu-trace2txt index fa4ab1f8..725c09a9 100755 --- a/qemu-trace2txt +++ b/qemu-trace2txt @@ -1,26 +1,29 @@ #!/usr/bin/env python3 import os -import subprocess -import sys import common +from shell_helpers import LF -def main(): - return common.run_cmd( - [ - os.path.join(common.qemu_src_dir, 'scripts/simpletrace.py'), common.Newline, - os.path.join(common.qemu_build_dir, 'trace-events-all'), common.Newline, - os.path.join(common.qemu_trace_file), common.Newline, - ], - cmd_file=os.path.join(common.run_dir, 'qemu-trace2txt'), - out_file=common.qemu_trace_txt_file, - show_stdout=False, - ) +class Main(common.LkmcCliFunction): + def __init__(self): + super().__init__( + description='''\ +Convert a QEMU `-trace exec_tb` to text form. +''' + ) + + def timed_main(self): + return self.sh.run_cmd( + [ + os.path.join(self.env['qemu_source_dir'], 'scripts/simpletrace.py'), LF, + os.path.join(self.env['qemu_build_dir'], 'trace-events-all'), LF, + os.path.join(self.env['qemu_trace_file']), LF, + ], + cmd_file=os.path.join(self.env['run_dir'], 'qemu-trace2txt'), + out_file=self.env['qemu_trace_txt_file'], + show_stdout=False, + ) if __name__ == '__main__': - parser = common.get_argparse(argparse_args={ - 'description': 'Convert a QEMU `-trace exec_tb` to text form.' - }) - args = common.setup(parser) - sys.exit(main()) + Main().cli() diff --git a/release b/release deleted file mode 100755 index dc8cc54f..00000000 --- a/release +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env python3 - -''' -https://upload.com/cirosantilli/linux-kernel-module-cheat#release -''' - -import imp -import os -import subprocess -import time - -import common -release_zip = imp.load_source('release_zip', os.path.join(common.root_dir, 'release-zip')) -release_upload = imp.load_source('release_upload', os.path.join(common.root_dir, 'release-upload')) - -start_time = time.time() -# TODO factor those out so we don't redo the same thing multiple times. -# subprocess.check_call([os.path.join(common.root_dir, 'test')]) -# subprocess.check_call([os.path.join(common.root_dir, ''bench-all', '-A', '-u']) - -# A clean release requires a full rebuild unless we hack it :-( -# We can't just use our current build as it contains packages we've -# installed in random experiments. And with EXT2: we can't easily -# know what the smallest root filesystem size is and use it either... -# https://stackoverflow.com/questions/47320800/how-to-clean-only-target-in-buildroot -subprocess.check_call([os.path.join(common.root_dir, 'configure'), '--all']) -subprocess.check_call([os.path.join(common.root_dir, 'build'), '--all-archs', 'release']) -release_zip.main() -subprocess.check_call(['git', 'push']) -release_upload.main() -end_time = time.time() -common.print_time(end_time - start_time) diff --git a/release-download-latest b/release-download-latest index d30ad8c6..20def80a 100755 --- a/release-download-latest +++ b/release-download-latest @@ -1,16 +1,26 @@ #!/usr/bin/env python3 -''' +import urllib.request + +import common +from shell_helpers import LF + +class Main(common.LkmcCliFunction): + def __init__(self): + super().__init__( + description='''\ Usage: https://github.com/cirosantilli/linux-kernel-module-cheat#prebuilt Implementation: https://stackoverflow.com/questions/24987542/is-there-a-link-to-github-for-downloading-a-file-in-the-latest-release-of-a-repo/50540591#50540591 -''' +''', + ) -import urllib.request - -import common + def timed_main(self): + self.log_info('Downloading the release, this may take several seconds / a few minutes.') + _json = self.github_make_request(path='/releases') + asset = _json[0]['assets'][0] + urllib.request.urlretrieve(asset['browser_download_url'], asset['name']) -_json = common.github_make_request(path='/releases') -asset = _json[0]['assets'][0] -urllib.request.urlretrieve(asset['browser_download_url'], asset['name']) +if __name__ == '__main__': + Main().cli() diff --git a/release-upload b/release-upload index 5dc65812..4b014d31 100755 --- a/release-upload +++ b/release-upload @@ -1,78 +1,84 @@ #!/usr/bin/env python3 -''' -Usage: https://github.com/cirosantilli/linux-kernel-module-cheat#release-zip - -Implementation: - -* https://stackoverflow.com/questions/5207269/how-to-release-a-build-artifact-asset-on-github-with-a-script/52354732#52354732 -* https://stackoverflow.com/questions/38153418/can-someone-give-a-python-requests-example-of-uploading-a-release-asset-in-githu/52354681#52354681 -''' - import json import os +import subprocess import sys - import urllib.error import common +from shell_helpers import LF -def main(): - repo = common.github_repo_id - tag = 'sha-{}'.format(common.sha) - upload_path = common.release_zip_file - - # Check the release already exists. - try: - _json = common.github_make_request(path='/releases/tags/' + tag) - except urllib.error.HTTPError as e: - if e.code == 404: - release_exists = False - else: - raise e - else: - release_exists = True - release_id = _json['id'] - - # Create release if not yet created. - if not release_exists: - _json = common.github_make_request( - authenticate=True, - data=json.dumps({ - 'tag_name': tag, - 'name': tag, - 'prerelease': True, - }).encode(), - path='/releases' +class Main(common.LkmcCliFunction): + def __init__(self): + super().__init__( + description='''\ +https://github.com/cirosantilli/linux-kernel-module-cheat#release-upload +''', ) - release_id = _json['id'] - asset_name = os.path.split(upload_path)[1] + def timed_main(self): + # https://stackoverflow.com/questions/3404936/show-which-git-tag-you-are-on + tag = subprocess.check_output([ + 'git', + 'describe', + '--exact-match', + '--tags' + ]).decode().rstrip() + upload_path = self.env['release_zip_file'] - # Clear the prebuilts for a upload. - _json = common.github_make_request( - path=('/releases/' + str(release_id) + '/assets'), - ) - for asset in _json: - if asset['name'] == asset_name: - _json = common.github_make_request( + # Check the release already exists. + try: + _json = self.github_make_request(path='/releases/tags/' + tag) + except urllib.error.HTTPError as e: + if e.code == 404: + release_exists = False + else: + raise e + else: + release_exists = True + release_id = _json['id'] + + # Create release if not yet created. + if not release_exists: + _json = self.github_make_request( authenticate=True, - path=('/releases/assets/' + str(asset['id'])), - method='DELETE', + data=json.dumps({ + 'tag_name': tag, + 'name': tag, + 'prerelease': True, + }).encode(), + path='/releases' ) - break + release_id = _json['id'] - # Upload the prebuilt. - with open(upload_path, 'br') as myfile: - content = myfile.read() - _json = common.github_make_request( - authenticate=True, - data=content, - extra_headers={'Content-Type': 'application/zip'}, - path=('/releases/' + str(release_id) + '/assets'), - subdomain='uploads', - url_params={'name': asset_name}, - ) + asset_name = os.path.split(upload_path)[1] + + # Clear the prebuilts for a upload. + _json = self.github_make_request( + path=('/releases/' + str(release_id) + '/assets'), + ) + for asset in _json: + if asset['name'] == asset_name: + _json = self.github_make_request( + authenticate=True, + path=('/releases/assets/' + str(asset['id'])), + method='DELETE', + ) + break + + # Upload the prebuilt. + self.log_info('Uploading the release, this may take several seconds / a few minutes.') + with open(upload_path, 'br') as myfile: + content = myfile.read() + _json = self.github_make_request( + authenticate=True, + data=content, + extra_headers={'Content-Type': 'application/zip'}, + path=('/releases/' + str(release_id) + '/assets'), + subdomain='uploads', + url_params={'name': asset_name}, + ) if __name__ == '__main__': - main() + Main().cli() diff --git a/release-zip b/release-zip index 05b23901..5139b406 100755 --- a/release-zip +++ b/release-zip @@ -1,25 +1,39 @@ #!/usr/bin/env python3 -''' -https://github.com/cirosantilli/linux-kernel-module-cheat#release-zip -''' - import os -import subprocess import zipfile import common -def main(): - os.makedirs(common.release_dir, exist_ok=True) - if os.path.exists(common.release_zip_file): - os.unlink(common.release_zip_file) - zipf = zipfile.ZipFile(common.release_zip_file, 'w', zipfile.ZIP_DEFLATED) - for arch in common.all_archs: - common.setup(common.get_argparse(default_args={'arch': arch})) - zipf.write(common.qcow2_file, arcname=os.path.relpath(common.qcow2_file, common.root_dir)) - zipf.write(common.linux_image, arcname=os.path.relpath(common.linux_image, common.root_dir)) - zipf.close() +class Main(common.LkmcCliFunction): + def __init__(self): + super().__init__( + description='''\ +https://github.com/cirosantilli/linux-kernel-module-cheat#release-zip +''', + defaults = { + 'print_time': False, + } + ) + self.zip_files = [] + + def timed_main(self): + self.zip_files.append(self.env['qcow2_file']) + self.zip_files.append(self.env['linux_image']) + for root, dirs, files in os.walk(self.env['baremetal_build_dir']): + for file in files: + path = os.path.join(root, file) + if os.path.splitext(path)[1] == self.env['baremetal_build_ext']: + self.zip_files.append(path) + + def teardown(self): + os.makedirs(self.env['release_dir'], exist_ok=True) + self.sh.rmrf(self.env['release_zip_file']) + self.log_info('Creating zip: ' + self.env['release_zip_file']) + with zipfile.ZipFile(self.env['release_zip_file'], 'w', zipfile.ZIP_DEFLATED) as zipf: + for zip_file in self.zip_files: + self.log_info('Adding file: ' + zip_file) + zipf.write(zip_file, arcname=os.path.relpath(zip_file, self.env['root_dir'])) if __name__ == '__main__': - main() + Main().cli() diff --git a/rootfs_overlay/gem5_exit.sh b/rootfs_overlay/gem5_exit.sh new file mode 100755 index 00000000..4ff5c6ad --- /dev/null +++ b/rootfs_overlay/gem5_exit.sh @@ -0,0 +1,4 @@ +#!/bin/sh +# To be able to do init=/gem5_exit.sh, since kernel CLI argument passing is too messy: +# https://github.com/cirosantilli/linux-kernel-module-cheat#init-arguments +m5 exit diff --git a/rootfs_overlay/test_all.sh b/rootfs_overlay/test_all.sh index 39b59b80..f28f2e8e 100755 --- a/rootfs_overlay/test_all.sh +++ b/rootfs_overlay/test_all.sh @@ -1,25 +1,27 @@ #!/bin/sh +test_dir="${1:-.}" for test in \ - /anonymous_inode.sh \ - /character_device.sh \ - /character_device_create.sh \ - /debugfs.sh \ - /dep.sh \ - /fops.sh \ - /init_module.sh \ - /ioctl.sh \ - /kstrto.sh \ - /mmap.sh \ - /netlink.sh \ - /params.sh \ - /procfs.sh \ - /seq_file.sh \ - /seq_file_single_open.sh \ - /sysfs.sh \ + anonymous_inode.sh \ + character_device.sh \ + character_device_create.sh \ + debugfs.sh \ + dep.sh \ + fops.sh \ + init_module.sh \ + ioctl.sh \ + kstrto.sh \ + mmap.sh \ + netlink.sh \ + params.sh \ + procfs.sh \ + seq_file.sh \ + seq_file_single_open.sh \ + sysfs.sh \ ; do - if ! "$test"; then - echo "lkmc_test_fail: ${test}" + if ! "${test_dir}/${test}"; then + echo "Test failed: ${test}" + test_fail.sh exit 1 fi done -echo lkmc_test_pass +echo 'All tests passed.' diff --git a/rootfs_overlay/test_fail.sh b/rootfs_overlay/test_fail.sh new file mode 100755 index 00000000..682d36bf --- /dev/null +++ b/rootfs_overlay/test_fail.sh @@ -0,0 +1,3 @@ +#!/bin/sh +# https://github.com/cirosantilli/linux-kernel-module-cheat#magic-failure-string +echo lkmc_test_fail diff --git a/run b/run index e28dbd41..0a55788c 100755 --- a/run +++ b/run @@ -8,451 +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, - 'initramfs': False, - 'initrd': 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 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: - debug_vm = ['gdb', common.Newline, '-q', common.Newline] + common.shlex_split(args.debug_vm) + ['--args', common.Newline] - else: - debug_vm = [] - if args.wait_gdb: - extra_qemu_args.extend(['-S', common.Newline]) - if args.eval_after is not None: - kernel_cli_after_dash += ' lkmc_eval_base64="{}"'.format(common.base64_encode(args.eval_after)) - 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', common.Newline] - 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.extend(['-nographic', common.Newline]) - 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(): - if not args.dry_run: - raise Exception('Root filesystem not found. Did you build it?\n' \ - 'Tried to use: ' + common.disk_image) - def raise_image_not_found(): - if not args.dry_run: - 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 common.emulator == 'gem5': - if common.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 common.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 = common.shlex_split(args.gem5_exe_args) - if do_trace: - gem5_exe_args.extend(['--debug-flags={}'.format(trace_type), common.Newline]) - 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' - if args.trace_stdout: - debug_file = 'cout' - else: - debug_file = 'trace.txt' - cmd.extend( - [ - common.executable, common.Newline, - '--debug-file', debug_file, common.Newline, - '--listener-mode', 'on', common.Newline, - '--outdir', common.m5out_dir, common.Newline, - ] + - gem5_exe_args - ) - if args.userland is not None: - cmd.extend([ - common.gem5_se_file, common.Newline, - '-c', common.resolve_userland(args.userland), common.Newline, - ]) - 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, common.Newline, - '--disk-image', common.disk_image, common.Newline, - '--kernel', common.image, common.Newline, - '--mem-size', memory, common.Newline, - '--num-cpus', str(args.cpus), common.Newline, - '--script', common.gem5_readfile, common.Newline, - ]) - if args.arch == 'x86_64': - if args.kvm: - cmd.extend(['--cpu-type', 'X86KvmCPU', common.Newline]) - cmd.extend(['--command-line', 'earlyprintk={} lpj=7999923 root=/dev/sda {}'.format(console, kernel_cli), common.Newline]) - elif common.is_arm: - if args.kvm: - cmd.extend(['--cpu-type', 'ArmV8KvmCPU', common.Newline]) - if args.dp650: - dp650_cmd = 'dpu_' - else: - dp650_cmd = '' - 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), common.Newline, - '--machine-type', common.machine, common.Newline, - ]) - dtb = None - if args.dtb is not None: - dtb = args.dtb - elif args.dp650: - dtb = os.path.join(common.gem5_system_dir, 'arm', 'dt', 'armv{}_gem5_v1_{}{}cpu.dtb'.format(common.armv, dp650_cmd, args.cpus)), common.Newline, - if dtb is None: - cmd.extend(['--generate-dtb', common.Newline]) - else: - cmd.extend(['--dtb-filename', dtb, common.Newline]) - if common.baremetal is None: - cmd.extend([ - '--param', 'system.panic_on_panic = True', common.Newline - ]) - else: - cmd.extend([ - '--bare-metal', common.Newline, - '--param', 'system.auto_reset_addr = True', common.Newline, - ]) - 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', common.Newline]) - elif args.gem5_script == 'biglittle': - if args.kvm: - cpu_type = 'kvm' - else: - cpu_type = 'atomic' - 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'), common.Newline, - '--big-cpus', '2', common.Newline, - '--cpu-type', cpu_type, common.Newline, - '--disk', common.disk_image, common.Newline, - '--kernel', common.image, common.Newline, - '--little-cpus', '2', common.Newline, - ]) - if args.dtb: - cmd.extend(['--dtb', os.path.join(common.gem5_system_dir, 'arm', 'dt', 'armv8_gem5_v1_big_little_2_2.dtb'), common.Newline,]) - if args.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', common.Newline]) - else: - qemu_user_and_system_options = [ - '-trace', 'enable={},file={}'.format(trace_type, common.qemu_trace_file), common.Newline, - ] - if args.userland is not None: - if args.wait_gdb: - debug_args = ['-g', str(common.gdb_port), common.Newline] - else: - debug_args = [] - cmd.extend( - [ - os.path.join(common.qemu_build_dir, '{}-linux-user'.format(args.arch), 'qemu-{}'.format(args.arch)), common.Newline, - '-L', common.target_dir, common.Newline - ] + - qemu_user_and_system_options + - common.shlex_split(args.userland_before) + - debug_args + - [ - common.resolve_userland(args.userland), common.Newline - ] - ) - 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 or not os.path.exists(common.qemu_executable): - qemu_executable = common.qemu_executable_basename - qemu_executable_prebuilt = True - else: - qemu_executable = common.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 args.debug_vm: - serial_monitor = [] - else: - if args.background: - serial_monitor = ['-serial', 'file:{}'.format(common.qemu_background_serial_file), common.Newline] - else: - serial_monitor = ['-serial', 'mon:stdio', common.Newline] - if args.kvm: - extra_emulator_args.extend(['-enable-kvm', common.Newline]) - extra_emulator_args.extend(['-serial', 'tcp::{},server,nowait'.format(common.extra_serial_port), common.Newline]) - 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), - common.Newline, - ]) - cmd.extend( - [ - qemu_executable, common.Newline, - '-device', 'rtl8139,netdev=net0', common.Newline, - '-gdb', 'tcp::{}'.format(common.gdb_port), common.Newline, - '-kernel', common.image, common.Newline, - '-m', args.memory, common.Newline, - '-monitor', 'telnet::{},server,nowait'.format(common.qemu_monitor_port), common.Newline, - '-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), common.Newline, - '-no-reboot', common.Newline, - '-smp', str(args.cpus), common.Newline, - ] + - virtfs_cmd + - serial_monitor + - vnc - ) - if args.dtb is not None: - cmd.extend(['-dtb', args.dtb, common.Newline]) - if not qemu_executable_prebuilt: - cmd.extend(qemu_user_and_system_options) - 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 common.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), - common.Newline, - ]) - if rr: - extra_emulator_args.extend([ - '-drive', 'driver=blkreplay,if=none,image=img-direct,id=img-blkreplay', common.Newline, - '-device', 'ide-hd,drive=img-blkreplay', common.Newline, - ]) - 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', common.Newline] - if args.arch == 'x86_64': - append = ['-append', '{} nopat {}'.format(root, kernel_cli), common.Newline] - cmd.extend([ - '-M', common.machine, common.Newline, - '-device', 'edu', common.Newline, - ]) - elif common.is_arm: - extra_emulator_args.extend(['-semihosting', common.Newline]) - if args.arch == 'arm': - cpu = 'cortex-a15' - else: - cpu = 'cortex-a57' - append = ['-append', '{} {}'.format(root, kernel_cli), common.Newline] - 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), common.Newline, - '-cpu', cpu, common.Newline, - ] + - virtio_gpu_pci - ) - if common.baremetal is None: - cmd.extend(append) - if args.tmux is not None: - tmux_args = '--run-id {}'.format(args.run_id) - if common.emulator == 'gem5': - tmux_cmd = './gem5-shell' - elif args.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( - args.arch, - args.linux_build_id, - args.run_id, - ) - if common.baremetal: - tmux_args += " --baremetal '{}'".format(common.baremetal) - if args.userland: - tmux_args += " --userland '{}'".format(args.userland) - tmux_args += ' {}'.format(args.tmux) - subprocess.Popen([ - os.path.join(common.root_dir, 'tmu'), - "sleep 2;{} {}".format(tmux_cmd, tmux_args) - ]) - 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 common.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 args.dry_run: - with open(common.termout_file, 'br') as logfile: - for line in logfile: - if panic_re.search(line): - error_string_found = True - if os.path.exists(common.guest_terminal_file): - with open(common.guest_terminal_file, 'br') as logfile: - lines = logfile.readlines() - if lines: - last_line = lines[-1] - if last_line.rstrip() == common.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='''\ +class Main(common.LkmcCliFunction): + def __init__(self): + super().__init__( + description='''\ +Run some content on an emulator. +''' + ) + 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 @@ -460,201 +27,619 @@ 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( - '--dtb', - help='''\ + ) + self.add_argument( + '-c', '--cpus', default=1, type=int, + help='Number of guest CPUs to emulate. Default: %(default)s' + ) + self.add_argument( + '--ctrl-c-host', default=False, + help='''\ +Ctrl +C kills the QEMU simulator instead of being passed to the guest. +''' + ) + 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( + '--dtb', + help='''\ Use the specified DTB file. If not given, let the emulator generate a DTB for us, which is what you usually want. ''' - ) - parser.add_argument( - '-E', '--eval', - help='''\ + ) + 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( + '--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'` +Example: `./run --arch arm --kernel-cli '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( + '--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 --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: -./run -G '--debug-flags=Exec --debug' --gem5 -- --cpu-type=HPI --caches +./run --emulator gem5 --gem5-exe-args '--debug-flags=Exec --debug' -- --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='''\ + ) + 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( + '--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( + '--quit-after-boot', + default=False, + help='''\ +Setup a kernel init parameter that makes the emulator quit immediately after boot. +''' + ) + 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. +gem5 Python scripts with pdb. ''' - ) - 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='''\ -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 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. + ) + self.add_argument( + '--tmux-args', + help='''\ +Parameters to pass to the program running on the tmux split. Implies --tmux. ''' - ) - 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): + show_stdout = True + # 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']) + if self.env['quit_after_boot']: + kernel_cli += ' {}'.format(self.env['quit_init']) + kernel_cli_after_dash = '' + extra_emulator_args = [] + extra_qemu_args = [] + if self.env['tmux_args'] is not None: + self.env['tmux'] = True + if self.env['debug_vm']: + 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['eval'] is not None: + kernel_cli += ' {}=/eval_base64.sh'.format(self.env['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? ' \ + '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? ' \ + '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['quiet']: + show_stdout = False + 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.sh.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.sh.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, + '--cmd', self.resolve_userland(self.env['userland']), LF, + ]) + if self.env['userland_args'] is not None: + cmd.extend(['--options', self.env['userland_args'], LF]) + else: + if self.env['gem5_script'] == 'fs': + # TODO port + if self.env['gem5_restore'] is not None: + cpt_dirs = self.gem5_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]) + if self.env['dp650']: + dp650_cmd = 'dpu_' + else: + dp650_cmd = '' + 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, + '--machine-type', self.env['machine'], LF, + ]) + dtb = None + if self.env['dtb'] is not None: + dtb = self.env['dtb'] + elif self.env['dp650']: + dtb = os.path.join(self.env['gem5_system_dir'], 'arm', 'dt', 'armv{}_gem5_v1_{}{}cpu.dtb'.format(self.env['armv'], dp650_cmd, self.env['cpus'])) + if dtb is None: + if not self.env['baremetal']: + cmd.extend(['--generate-dtb', LF]) + else: + cmd.extend(['--dtb-filename', dtb, 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.gem5_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, + '--kernel', self.env['image'], LF, + '--little-cpus', '2', LF, + ]) + if self.env['dtb']: + cmd.extend(['--dtb', os.path.join(self.env['gem5_system_dir'], 'arm', 'dt', 'armv8_gem5_v1_big_little_2_2.dtb'), NL]) + 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]) + elif self.env['emulator'] == 'qemu': + 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 + + debug_args + ) + 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] + if self.env['quiet']: + show_stdout = False + else: + if self.env['ctrl_c_host']: + serial = 'stdio' + else: + serial = 'mon:stdio' + serial_monitor = ['-serial', serial, 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, + ]) + if self.env['machine2'] is not None: + # Multiple -machine options can also be given comma separated in one -machine. + # We use multiple because the machine is used as an identifier on baremetal tests + # build paths, so better keep them clean. + machine2 = ['-machine', self.env['machine2'], LF] + else: + machine2 = [] + cmd.extend( + [ + qemu_executable, LF, + '-machine', self.env['machine'], LF, + ] + + machine2 + + [ + '-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 self.env['dtb'] is not None: + cmd.extend(['-dtb', self.env['dtb'], LF]) + if not qemu_executable_prebuilt: + cmd.extend(qemu_user_and_system_options) + if self.env['initrd']: + extra_emulator_args.extend(['-initrd', self.env['buildroot_cpio'], LF]) + rr = self.env['record'] or self.env['replay'] + if self.env['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([ + '-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( + [ + '-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']) + if self.env['tmux_args'] is not None: + tmux_args += ' {}'.format(self.env['tmux_args']) + 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 self.env['emulator'] == 'qemu' and self.env['userland']: + # The program and arguments must come at the every end of the CLI. + cmd.extend([self.resolve_userland(self.env['userland']), LF]) + if self.env['userland_args'] is not None: + cmd.extend(self.sh.shlex_split(self.env['userland_args'])) + if debug_vm or self.env['terminal']: + out_file = None + else: + out_file = self.env['termout_file'] + exit_status = self.sh.run_cmd( + cmd, + cmd_file=self.env['run_cmd_file'], + extra_env=extra_env, + out_file=out_file, + raise_on_failure=False, + show_stdout=show_stdout, + ) + if exit_status == 0: + # 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 + exit_status = 0 + if out_file is not None and not self.env['dry_run']: + with open(self.env['termout_file'], 'br') as logfile: + line = None + for line in logfile: + if panic_re.search(line): + exit_status = 1 + if line is not None: + last_line = line.rstrip() + match = re.search(b'Simulated exit code not 0! Exit code is (\d+)', last_line) + if match: + exit_status = int(match.group(1)) + if not self.env['userland']: + if os.path.exists(self.env['guest_terminal_file']): + with open(self.env['guest_terminal_file'], 'br') as logfile: + for line in logfile.readlines(): + if line.rstrip() == self.env['magic_fail_string']: + exit_status = 1 + break + if exit_status != 0: + self.log_error('simulation error detected by parsing logs') + return exit_status 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 56e471b3..b51136d8 100755 --- a/run-docker +++ b/run-docker @@ -4,61 +4,63 @@ import argparse import os import common +import shell_helpers +from shell_helpers import LF -container_name = common.repo_short_id -container_hostname = common.repo_short_id -image_name = common.repo_short_id -target_dir = '/root/{}'.format(common.repo_short_id) +container_name = common.consts['repo_short_id'] +container_hostname = common.consts['repo_short_id'] +image_name = common.consts['repo_short_id'] +target_dir = '/root/{}'.format(common.consts['repo_short_id']) docker = ['sudo', 'docker'] def create(args): - common.run_cmd(docker + ['build', '-t', image_name, '.', common.Newline]) + sh.run_cmd(docker + ['build', '-t', image_name, '.', LF]) # --privileged for KVM: # https://stackoverflow.com/questions/48422001/launching-qemu-kvm-from-inside-docker-container - common.run_cmd( + sh.run_cmd( docker + [ - 'create', common.Newline, - '--hostname', container_hostname, common.Newline, - '-i', common.Newline, - '--name', container_name, common.Newline, - '--net', 'host', common.Newline, - '--privileged', common.Newline, - '-t', common.Newline, - '-w', target_dir, common.Newline, - '-v', '{}:{}'.format(os.getcwd(), target_dir), common.Newline, + 'create', LF, + '--hostname', container_hostname, LF, + '-i', LF, + '--name', container_name, LF, + '--net', 'host', LF, + '--privileged', LF, + '-t', LF, + '-w', target_dir, LF, + '-v', '{}:{}'.format(os.getcwd(), target_dir), LF, image_name, ] ) def destroy(args): stop(args) - common.run_cmd(docker + ['rm', container_name, common.Newline]) - common.run_cmd(docker + ['rmi', image_name, common.Newline]) -def sh(args): + sh.run_cmd(docker + ['rm', container_name, LF]) + sh.run_cmd(docker + ['rmi', image_name, LF]) +def sh_func(args): start(args) if args: sh_args = args else: sh_args = ['bash'] - common.run_cmd( + sh.run_cmd( docker + ['exec', '-i', '-t', container_name] + sh_args + - [common.Newline], + [LF], ) def start(args): - common.run_cmd(docker + ['start', container_name, common.Newline]) + sh.run_cmd(docker + ['start', container_name, LF]) def stop(args): - common.run_cmd(docker + ['stop', container_name, common.Newline]) + sh.run_cmd(docker + ['stop', container_name, LF]) cmd_action_map = { 'create': lambda args: create(args), 'DESTROY': lambda args: destroy(args), - 'sh': lambda args: sh(args), + 'sh': lambda args: sh_func(args), 'start': lambda args: start(args), 'stop': lambda args: stop(args), } parser = argparse.ArgumentParser() +parser.add_argument('--dry-run', default=False, action='store_true') parser.add_argument('cmd', choices=cmd_action_map) parser.add_argument('args', nargs='*') -common.add_dry_run_argument(parser) args = parser.parse_args() -common.setup_dry_run_arguments(args) +sh = shell_helpers.ShellHelpers(dry_run=args.dry_run) cmd_action_map[args.cmd](args.args) diff --git a/run-gdb b/run-gdb index 55fc02fc..332454e9 100755 --- a/run-gdb +++ b/run-gdb @@ -7,18 +7,7 @@ import subprocess import sys import common - -defaults = { - 'after': '', - 'before': '', - 'break_at': None, - 'kgdb': False, - 'no_continue': False, - 'no_lxsymbols': False, - 'test': False, - 'sim': False, - 'userland': None, -} +from shell_helpers import LF class GdbTestcase: def __init__( @@ -34,8 +23,6 @@ class GdbTestcase: ''' self.prompt = '\(gdb\) ' self.source_path = source_path - common.print_cmd(cmd) - cmd = common.strip_newlines(cmd) import pexpect self.child = pexpect.spawn( cmd[0], @@ -48,9 +35,15 @@ class GdbTestcase: self.child.waitnoecho() self.child.expect(self.prompt) test = imp.load_source('test', test_script_path) - test.test(self) + exception = None + try: + test.test(self) + except AssertionError as e: + exception = e self.child.sendcontrol('d') self.child.close() + if exception is not None: + raise exception def before(self): return self.child.before.rstrip() @@ -84,155 +77,147 @@ class GdbTestcase: self.child.sendline(line) self.child.expect(self.prompt) -def main(args, extra_args=None): - ''' - :param args: argparse parse_argument() output. Must contain all the common options, - but does not need GDB specific ones. - :type args: argparse.Namespace - - :param extra_args: extra arguments to be added to args - :type extra_args: Dict[str,Any] - - :return: GDB exit status - :rtype: int - ''' - global defaults - args = common.resolve_args(defaults, args, extra_args) - after = common.shlex_split(args.after) - before = common.shlex_split(args.before) - no_continue = args.no_continue - if args.test: - no_continue = True - before.extend([ - '-q', common.Newline, - '-nh', common.Newline, - '-ex', 'set confirm off', common.Newline - ]) - elif args.verbose: - # The output of this would affect the tests. - # https://stackoverflow.com/questions/13496389/gdb-remote-protocol-how-to-analyse-packets - # Also be opinionated and set remotetimeout to allow you to step debug the emulator at the same time. - before.extend([ - '-ex', 'set debug remote 1', common.Newline, - '-ex', 'set remotetimeout 99999', common.Newline, - ]) - if args.break_at is not None: - break_at = ['-ex', 'break {}'.format(args.break_at), common.Newline] - else: - break_at = [] - linux_full_system = (common.baremetal is None and args.userland is None) - if args.userland: - image = common.resolve_userland(args.userland) - elif common.baremetal: - image = common.image - test_script_path = os.path.splitext(common.source_path)[0] + '.py' - else: - image = common.vmlinux - if common.baremetal: - allowed_toolchains = ['crosstool-ng', 'buildroot', 'host'] - else: - allowed_toolchains = ['buildroot', 'crosstool-ng', 'host'] - cmd = ( - [common.get_toolchain_tool('gdb', allowed_toolchains=allowed_toolchains), common.Newline] + - before + - ['-q', common.Newline] - ) - if linux_full_system: - cmd.extend(['-ex', 'add-auto-load-safe-path {}'.format(common.linux_build_dir), common.Newline]) - if args.sim: - target = 'sim' - else: - if args.kgdb: - port = common.extra_serial_port - else: - port = common.gdb_port - target = 'remote localhost:{}'.format(port) - cmd.extend([ - '-ex', 'file {}'.format(image), common.Newline, - '-ex', 'target {}'.format(target), common.Newline, - ]) - if not args.kgdb: - cmd.extend(break_at) - if not no_continue: - # ## lx-symbols - # - # ### lx-symbols after continue - # - # lx symbols must be run after continue. - # - # running it immediately after the connect on the bootloader leads to failure, - # likely because kernel structure on which it depends are not yet available. - # - # With this setup, continue runs, and lx-symbols only runs when a break happens, - # either by hitting the breakpoint, or by entering Ctrl + C. - # - # Sure, if the user sets a break on a raw address of the bootloader, - # problems will still arise, but let's think about that some other time. - # - # ### lx-symbols autoload - # - # The lx-symbols commands gets loaded through the file vmlinux-gdb.py - # which gets put on the kernel build root when python debugging scripts are enabled. - cmd.extend(['-ex', 'continue', common.Newline]) - if not args.no_lxsymbols and linux_full_system: - cmd.extend(['-ex', 'lx-symbols {}'.format(common.kernel_modules_build_subdir), common.Newline]) - cmd.extend(after) - if args.test: - GdbTestcase( - common.source_path, - test_script_path, - cmd, - verbose=args.verbose, +class Main(common.LkmcCliFunction): + def __init__(self): + super().__init__(description='''\ +Connect with GDB to an emulator to debug Linux itself +''') + self.add_argument( + '--after', default='', + help='Pass extra arguments to GDB, to be appended after all other arguments' ) - else: - # I would rather have cwd be out_rootfs_overlay_dir, - # but then lx-symbols cannot fine the vmlinux and fails with: - # vmlinux: No such file or directory. - return common.run_cmd( - cmd, - cmd_file=os.path.join(common.run_dir, 'run-gdb.sh'), - cwd=common.linux_build_dir + self.add_argument( + '--before', default='', + help='Pass extra arguments to GDB to be prepended before any of the arguments passed by this script' ) - -if __name__ == '__main__': - parser = common.get_argparse(argparse_args={'description': 'Connect with GDB to an emulator to debug Linux itself'}) - parser.add_argument( - '-A', '--after', default=defaults['after'], - help='Pass extra arguments to GDB, to be appended after all other arguments' - ) - parser.add_argument( - '--before', default=defaults['before'], - help='Pass extra arguments to GDB to be prepended before any of the arguments passed by this script' - ) - parser.add_argument( - '-C', '--no-continue', default=defaults['no_continue'], action='store_true', - help="Don't run continue after connecting" - ) - parser.add_argument( - '-k', '--kgdb', default=defaults['kgdb'], action='store_true' - ) - parser.add_argument( - '--sim', default=defaults['sim'], action='store_true', - help='''Use the built-in GDB CPU simulator -See: https://github.com/cirosantilli/linux-kernel-module-cheat#gdb-builtin-cpu-simulator -''' - ) - parser.add_argument( - '-X', '--no-lxsymbols', default=defaults['no_lxsymbols'], action='store_true' - ) - parser.add_argument( - '--test', default=defaults['test'], action='store_true', - help='''\ + self.add_argument( + 'break_at', nargs='?', + help='Extra options to append at the end of the emulator command line' + ) + self.add_argument( + '-k', '--kgdb', default=False, + ) + self.add_argument( + '-C', '--no-continue', default=False, + help="Don't run continue after connecting" + ) + self.add_argument( + '-X', '--no-lxsymbols', default=False, + ) + self.add_argument( + '--test', default=False, + help='''\ Run an expect test case instead of interactive usage. For baremetal and userland, the script is a .py file next to the source code. ''' - ) - parser.add_argument( - '-u', '--userland', default=defaults['userland'], - ) - parser.add_argument( - 'break_at', nargs='?', - help='Extra options to append at the end of the emulator command line' - ) - args = common.setup(parser) - sys.exit(main(args)) + ) + self.add_argument( + '--sim', default=False, + help='''Use the built-in GDB CPU simulator +See: https://github.com/cirosantilli/linux-kernel-module-cheat#gdb-builtin-cpu-simulator +''' + ) + self.add_argument( + '-u', '--userland', + ) + + def timed_main(self): + after = self.sh.shlex_split(self.env['after']) + before = self.sh.shlex_split(self.env['before']) + no_continue = self.env['no_continue'] + if self.env['test']: + no_continue = True + before.extend([ + '-q', LF, + '-nh', LF, + '-ex', 'set confirm off', LF + ]) + elif self.env['verbose']: + # The output of this would affect the tests. + # https://stackoverflow.com/questions/13496389/gdb-remote-protocol-how-to-analyse-packets + # Also be opinionated and set remotetimeout to allow you to step debug the emulator at the same time. + before.extend([ + '-ex', 'set debug remote 1', LF, + '-ex', 'set remotetimeout 99999', LF, + ]) + if self.env['break_at'] is not None: + break_at = ['-ex', 'break {}'.format(self.env['break_at']), LF] + else: + break_at = [] + linux_full_system = (self.env['baremetal'] is None and self.env['userland'] is None) + if self.env['userland']: + image = self.resolve_userland(self.env['userland']) + elif self.env['baremetal']: + image = self.env['image'] + test_script_path = os.path.splitext(self.env['source_path'])[0] + '.py' + else: + image = self.env['vmlinux'] + if self.env['baremetal']: + allowed_toolchains = ['crosstool-ng', 'buildroot', 'host'] + else: + allowed_toolchains = ['buildroot', 'crosstool-ng', 'host'] + cmd = ( + [self.get_toolchain_tool('gdb', allowed_toolchains=allowed_toolchains), LF] + + before + ) + if linux_full_system: + cmd.extend(['-ex', 'add-auto-load-safe-path {}'.format(self.env['linux_build_dir']), LF]) + if self.env['sim']: + target = 'sim' + else: + if self.env['kgdb']: + port = self.env['extra_serial_port'] + else: + port = self.env['gdb_port'] + target = 'remote localhost:{}'.format(port) + cmd.extend([ + '-ex', 'file {}'.format(image), LF, + '-ex', 'target {}'.format(target), LF, + ]) + if not self.env['kgdb']: + cmd.extend(break_at) + if not no_continue: + # ## lx-symbols + # + # ### lx-symbols after continue + # + # lx symbols must be run after continue. + # + # running it immediately after the connect on the bootloader leads to failure, + # likely because kernel structure on which it depends are not yet available. + # + # With this setup, continue runs, and lx-symbols only runs when a break happens, + # either by hitting the breakpoint, or by entering Ctrl + C. + # + # Sure, if the user sets a break on a raw address of the bootloader, + # problems will still arise, but let's think about that some other time. + # + # ### lx-symbols autoload + # + # The lx-symbols commands gets loaded through the file vmlinux-gdb.py + # which gets put on the kernel build root when python debugging scripts are enabled. + cmd.extend(['-ex', 'continue', LF]) + if not self.env['no_lxsymbols'] and linux_full_system: + cmd.extend(['-ex', 'lx-symbols {}'.format(self.env['kernel_modules_build_subdir']), LF]) + cmd.extend(after) + if self.env['test']: + self.sh.print_cmd(cmd) + if not self.env['dry_run']: + GdbTestcase( + self.env['source_path'], + test_script_path, + self.sh.strip_newlines(cmd), + verbose=self.env['verbose'], + ) + else: + # I would rather have cwd be out_rootfs_overlay_dir, + # but then lx-symbols cannot fine the vmlinux and fails with: + # vmlinux: No such file or directory. + return self.sh.run_cmd( + cmd, + cmd_file=os.path.join(self.env['run_dir'], 'run-gdb.sh'), + cwd=self.env['linux_build_dir'] + ) + +if __name__ == '__main__': + Main().cli() diff --git a/run-gdb-user b/run-gdb-user index 67567015..14c5bdf3 100755 --- a/run-gdb-user +++ b/run-gdb-user @@ -5,9 +5,9 @@ import os import sys import common -rungdb = imp.load_source('rungdb', os.path.join(common.root_dir, 'run-gdb')) +rungdb = imp.load_source('run_gdb', 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(args.executable) -addr = common.get_elf_entry(os.path.join(common.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: @@ -33,5 +33,5 @@ extra_args['before'] = '-ex \"add-symbol-file {} {}\"'.format(executable, hex(ad # TODO understand better. # Also, lx-symbols overrides the add-symbol-file commands. extra_args['no_lxsymbols'] = True -extra_args['break_at'] = args.break_at +extra_args['break_at'] = kwargs['break_at'] sys.exit(rungdb.main(args, extra_args)) diff --git a/run-gdbserver b/run-gdbserver index e34656f1..f67002b6 100755 --- a/run-gdbserver +++ b/run-gdbserver @@ -5,8 +5,9 @@ import subprocess import sys import common +from shell_helpers import LF -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 +17,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(common.buildroot_staging_dir), - '-ex', 'target remote localhost:{}'.format(common.qemu_hostfwd_generic_port), - '-ex', 'tbreak {}'.format(args.break_at), + '-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(common.buildroot_build_build_dir, common.resolve_userland(args.executable)), + os.path.join(kwargs['buildroot_build_build_dir'], self.resolve_userland(kwargs['executable'])), ]).wait()) diff --git a/run-toolchain b/run-toolchain index 041e1b58..e84a06c6 100755 --- a/run-toolchain +++ b/run-toolchain @@ -4,8 +4,9 @@ import os import sys import common +from shell_helpers import LF -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 +25,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,17 +34,17 @@ parser.add_argument( metavar='extra-args', nargs='*' ) -args = common.setup(parser) -if common.baremetal is None: - image = common.vmlinux +args = self.setup(parser) +if kwargs['baremetal'] is None: + image = kwargs['vmlinux'] else: - image = common.image -tool= common.get_toolchain_tool(args.tool) -if args.dry: + image = kwargs['image'] +tool= self.get_toolchain_tool(kwargs['tool']) +if kwargs['dry']: print(tool) else: - sys.exit(common.run_cmd( - [tool, common.Newline] - + common.add_newlines(args.extra_args), - cmd_file=os.path.join(common.run_dir, 'run-toolchain.sh'), + sys.exit(self.sh.run_cmd( + [tool, LF] + + self.sh.add_newlines(kwargs['extra_args']), + cmd_file=os.path.join(kwargs['run_dir'], 'run-toolchain.sh'), )) diff --git a/shell_helpers.py b/shell_helpers.py new file mode 100644 index 00000000..709441cb --- /dev/null +++ b/shell_helpers.py @@ -0,0 +1,296 @@ +#!/usr/bin/env python3 + +import distutils.file_util +import itertools +import os +import shlex +import shutil +import signal +import stat +import subprocess +import sys +import threading + +class LF: + ''' + LineFeed (AKA newline). + + Singleton class. Can be used in print_cmd to print out nicer command lines + with --key on the same line as "--key value". + ''' + pass + +class ShellHelpers: + ''' + Helpers to do things which are easy from the shell, + usually filesystem, process or pipe operations. + + Attempt to print shell equivalents of all commands to make things + easy to debug and understand what is going on. + ''' + + _print_lock = threading.Lock() + + def __init__(self, dry_run=False, quiet=False): + ''' + :param dry_run: don't run the commands, just potentially print them. Debug aid. + :type dry_run: Bool + + :param quiet: don't print the commands + :type dry_run: Bool + ''' + self.dry_run = dry_run + self.quiet = quiet + + @classmethod + def _print_thread_safe(cls, string): + ''' + Python sucks: a naive print adds a bunch of random spaces to stdout, + and then copy pasting the command fails. + https://stackoverflow.com/questions/3029816/how-do-i-get-a-thread-safe-print-in-python-2-6 + The initial use case was test-gdb which must create a thread for GDB to run the program in parallel. + ''' + cls._print_lock.acquire() + sys.stdout.write(string + '\n') + sys.stdout.flush() + cls._print_lock.release() + + def add_newlines(self, cmd): + out = [] + for arg in cmd: + out.extend([arg, LF]) + return out + + def cp(self, src, dest, **kwargs): + self.print_cmd(['cp', src, dest]) + if not self.dry_run: + shutil.copy2(src, dest) + + @staticmethod + def cmd_to_string(cmd, cwd=None, extra_env=None, extra_paths=None): + ''' + Format a command given as a list of strings so that it can + be viewed nicely and executed by bash directly and print it to stdout. + ''' + last_newline = ' \\\n' + newline_separator = last_newline + ' ' + out = [] + if extra_env is None: + extra_env = {} + if cwd is not None: + out.append('cd {} &&'.format(shlex.quote(cwd))) + if extra_paths is not None: + out.append('PATH="{}:${{PATH}}"'.format(':'.join(extra_paths))) + for key in extra_env: + out.append('{}={}'.format(shlex.quote(key), shlex.quote(extra_env[key]))) + cmd_quote = [] + newline_count = 0 + for arg in cmd: + if arg == LF: + cmd_quote.append(arg) + newline_count += 1 + else: + cmd_quote.append(shlex.quote(arg)) + if newline_count > 0: + cmd_quote = [' '.join(list(y)) for x, y in itertools.groupby(cmd_quote, lambda z: z == LF) if not x] + out.extend(cmd_quote) + if newline_count == 1 and cmd[-1] == LF: + ending = '' + else: + ending = last_newline + ';' + return newline_separator.join(out) + ending + + def copy_dir_if_update_non_recursive(self, srcdir, destdir, filter_ext=None): + # TODO print rsync equivalent. + os.makedirs(destdir, exist_ok=True) + for basename in os.listdir(srcdir): + src = os.path.join(srcdir, basename) + if os.path.isfile(src): + noext, ext = os.path.splitext(basename) + if filter_ext is not None and ext == filter_ext: + distutils.file_util.copy_file( + src, + os.path.join(destdir, basename), + update=1, + ) + + def print_cmd(self, cmd, cwd=None, cmd_file=None, extra_env=None, extra_paths=None): + ''' + Print cmd_to_string to stdout. + + Optionally save the command to cmd_file file, and add extra_env + environment variables to the command generated. + + If cmd contains at least one LF, newlines are only added on LF. + Otherwise, newlines are added automatically after every word. + ''' + if type(cmd) is str: + cmd_string = cmd + else: + cmd_string = self.cmd_to_string(cmd, cwd=cwd, extra_env=extra_env, extra_paths=extra_paths) + if not self.quiet: + self._print_thread_safe('+ ' + cmd_string) + if cmd_file is not None: + with open(cmd_file, 'w') as f: + f.write('#!/usr/bin/env bash\n') + f.write(cmd_string) + st = os.stat(cmd_file) + os.chmod(cmd_file, st.st_mode | stat.S_IXUSR) + + def run_cmd( + self, + cmd, + cmd_file=None, + out_file=None, + show_stdout=True, + show_cmd=True, + extra_env=None, + extra_paths=None, + delete_env=None, + raise_on_failure=True, + **kwargs + ): + ''' + Run a command. Write the command to stdout before running it. + + Wait until the command finishes execution. + + :param cmd: command to run. LF entries are magic get skipped. + :type cmd: List[str] + + :param cmd_file: if not None, write the command to be run to that file + :type cmd_file: str + + :param out_file: if not None, write the stdout and stderr of the command the file + :type out_file: str + + :param show_stdout: wether to show stdout and stderr on the terminal or not + :type show_stdout: bool + + :param extra_env: extra environment variables to add when running the command + :type extra_env: Dict[str,str] + + :return: exit status of the command + :rtype: int + ''' + if out_file is not None: + stdout = subprocess.PIPE + stderr = subprocess.STDOUT + else: + if show_stdout: + stdout = None + stderr = None + else: + stdout = subprocess.DEVNULL + stderr = subprocess.DEVNULL + if extra_env is None: + extra_env = {} + if delete_env is None: + delete_env = [] + if 'cwd' in kwargs: + cwd = kwargs['cwd'] + else: + cwd = None + env = os.environ.copy() + env.update(extra_env) + if extra_paths is not None: + path = ':'.join(extra_paths) + if 'PATH' in os.environ: + path += ':' + os.environ['PATH'] + env['PATH'] = path + for key in delete_env: + if key in env: + del env[key] + if show_cmd: + self.print_cmd(cmd, cwd=cwd, cmd_file=cmd_file, extra_env=extra_env, extra_paths=extra_paths) + + # Otherwise, if called from a non-main thread: + # ValueError: signal only works in main thread + if threading.current_thread() == threading.main_thread(): + # Otherwise Ctrl + C gives: + # - ugly Python stack trace for gem5 (QEMU takes over terminal and is fine). + # - kills Python, and that then kills GDB: https://stackoverflow.com/questions/19807134/does-python-always-raise-an-exception-if-you-do-ctrlc-when-a-subprocess-is-exec + sigint_old = signal.getsignal(signal.SIGINT) + signal.signal(signal.SIGINT, signal.SIG_IGN) + + # Otherwise BrokenPipeError when piping through | grep + # But if I do this_module, my terminal gets broken at the end. Why, why, why. + # https://stackoverflow.com/questions/14207708/ioerror-errno-32-broken-pipe-python + # Ignoring the exception is not enough as it prints a warning anyways. + #sigpipe_old = signal.getsignal(signal.SIGPIPE) + #signal.signal(signal.SIGPIPE, signal.SIG_DFL) + + cmd = self.strip_newlines(cmd) + if not self.dry_run: + # https://stackoverflow.com/questions/15535240/python-popen-write-to-stdout-and-log-file-simultaneously/52090802#52090802 + with subprocess.Popen(cmd, stdout=stdout, stderr=stderr, env=env, **kwargs) as proc: + if out_file is not None: + os.makedirs(os.path.split(os.path.abspath(out_file))[0], exist_ok=True) + with open(out_file, 'bw') as logfile: + while True: + byte = proc.stdout.read(1) + if byte: + if show_stdout: + sys.stdout.buffer.write(byte) + try: + sys.stdout.flush() + except BlockingIOError: + # TODO understand. Why, Python, why. + pass + logfile.write(byte) + else: + break + if threading.current_thread() == threading.main_thread(): + signal.signal(signal.SIGINT, sigint_old) + #signal.signal(signal.SIGPIPE, sigpipe_old) + returncode = proc.returncode + if returncode != 0 and raise_on_failure: + raise Exception('Command exited with status: {}'.format(returncode)) + return returncode + else: + return 0 + + def shlex_split(self, string): + ''' + shlex_split, but also add Newline after every word. + + Not perfect since it does not group arguments, but I don't see a solution. + ''' + return self.add_newlines(shlex.split(string)) + + def strip_newlines(self, cmd): + return [x for x in cmd if x != LF] + + def rmrf(self, path): + self.print_cmd(['rm', '-r', '-f', path, LF]) + if not self.dry_run and os.path.exists(path): + if os.path.isdir(path): + shutil.rmtree(path) + else: + os.unlink(path) + + def write_configs(self, config_path, configs, config_fragments=None, mode='a'): + ''' + Append extra KEY=val configs into the given config file. + ''' + if config_fragments is None: + config_fragments = [] + for config_fragment in config_fragments: + self.print_cmd(['cat', config_fragment, '>>', config_path]) + if not self.dry_run: + with open(config_path, 'a') as config_file: + for config_fragment in config_fragments: + with open(config_fragment, 'r') as config_fragment_file: + for line in config_fragment_file: + config_file.write(line) + self.write_string_to_file(config_path, '\n'.join(configs), mode=mode) + + def write_string_to_file(self, path, string, mode='w'): + if mode == 'a': + redirect = '>>' + else: + redirect = '>' + self.print_cmd("cat << 'EOF' {} {}\n{}\nEOF".format(redirect, path, string)) + if not self.dry_run: + with open(path, mode) as f: + f.write(string) diff --git a/submodules/boot-wrapper-aarch64 b/submodules/boot-wrapper-aarch64 new file mode 160000 index 00000000..ed609635 --- /dev/null +++ b/submodules/boot-wrapper-aarch64 @@ -0,0 +1 @@ +Subproject commit ed60963595855e66ffc06a8a543cbb429c7ede03 diff --git a/submodules/buildroot b/submodules/buildroot index bc60382b..653eaa17 160000 --- a/submodules/buildroot +++ b/submodules/buildroot @@ -1 +1 @@ -Subproject commit bc60382b8ff776bd58dfd4732221e5c927c50c37 +Subproject commit 653eaa178bf7352791d988a8ee9db79c8bd7975e diff --git a/submodules/gem5 b/submodules/gem5 index a5bc2291..7fa4c946 160000 --- a/submodules/gem5 +++ b/submodules/gem5 @@ -1 +1 @@ -Subproject commit a5bc2291391b0497fdc60fdc960e07bcecebfb8f +Subproject commit 7fa4c946386e7207ad5859e8ade0bbfc14000d91 diff --git a/submodules/linux b/submodules/linux index 84df9525..8fe28cb5 160000 --- a/submodules/linux +++ b/submodules/linux @@ -1 +1 @@ -Subproject commit 84df9525b0c27f3ebc2ebb1864fa62a97fdedb7d +Subproject commit 8fe28cb58bcb235034b64cbbb7550a8a43fd88be diff --git a/submodules/xen b/submodules/xen new file mode 160000 index 00000000..96cbd089 --- /dev/null +++ b/submodules/xen @@ -0,0 +1 @@ +Subproject commit 96cbd0893f783997caaf117e897d5fa8f2dc7b5f diff --git a/test b/test index e0f144d2..67b3b2ff 100755 --- a/test +++ b/test @@ -1,14 +1,39 @@ -#!/usr/bin/env bash -set -eu -test_size=1 -while [ $# -gt 0 ]; do - case "$1" in - --size) - test_size="$2" - shift 2 - ;; - esac -done -./bench-boot --size "$test_size" -./test-modules -./test-gdb +#!/usr/bin/env python3 + +import common +import shell_helpers +from shell_helpers import LF + +class Main(common.TestCliFunction): + def __init__(self): + super().__init__( + description='''\ +https://github.com/cirosantilli/linux-kernel-module-cheat#automated-tests +''' + ) + self.add_argument( + '--size', + default=1, + type=int, + help='''\ +Size of the tests to run. Scale: + +* 1: a few seconds and important +* 2: < 5 minutes and important or a few seconds and not too important +* 3: all +''' + ) + + def timed_main(self): + run_args = self.get_common_args() + test_boot_args = run_args.copy() + test_boot_args['size'] = self.env['size'] + self.run_test(self.import_path_main('test-boot'), test_boot_args, 'test-boot') + self.run_test(self.import_path_main('test-userland-full-system'), run_args, 'test-userland') + self.run_test(self.import_path_main('test-baremetal'), run_args, 'test-baremetal') + self.run_test(self.import_path_main('test-user-mode'), run_args, 'test-user-mode') + self.run_test(self.import_path_main('test-gdb'), run_args, 'test-gdb') + +if __name__ == '__main__': + Main().cli() + diff --git a/test-baremetal b/test-baremetal new file mode 100755 index 00000000..ad5a467f --- /dev/null +++ b/test-baremetal @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 + +import os +import sys + +import common + +class Main(common.TestCliFunction): + def __init__(self): + super().__init__( + supported_archs=common.consts['crosstool_ng_supported_archs'], + ) + self.add_argument( + 'tests', + nargs='*', + help='''\ +If given, run only the given tests. Otherwise, run all tests. +''' + ) + + def timed_main(self): + run = self.import_path_main('run') + run_args = self.get_common_args() + if self.env['emulator'] == 'gem5': + run_args['userland_build_id'] = 'static' + if self.env['tests'] == []: + baremetal_source_exts = (self.env['c_ext'], self.env['asm_ext']) + paths = [] + for f in os.listdir(self.env['baremetal_source_dir']): + path = os.path.join(self.env['baremetal_source_dir'], f) + if os.path.isfile(path) and os.path.splitext(path)[1] in baremetal_source_exts: + paths.append(path) + for root, dirs, files in os.walk(self.env['baremetal_source_arch_dir'], topdown=True): + dirs[:] = [d for d in dirs if d != 'interactive'] + for file in files: + path = os.path.join(root, file) + if os.path.splitext(path)[1] in baremetal_source_exts: + paths.append(path) + sources = [] + for path in paths: + if not ( + self.env['emulator'] == 'gem5' and os.path.basename(path).startswith('semihost_') or + self.env['emulator'] == 'qemu' and os.path.basename(path).startswith('gem5_') + ): + sources.append(os.path.relpath(path, self.env['baremetal_source_dir'])) + else: + sources = self.env['tests'] + for source in sources: + run_args['baremetal'] = source + run_args['ctrl_c_host'] = True + if os.path.splitext(os.path.basename(source))[0] == 'multicore': + run_args['cpus'] = 2 + self.run_test(run, run_args, source) + +if __name__ == '__main__': + Main().cli() diff --git a/test-boot b/test-boot new file mode 100755 index 00000000..d3732776 --- /dev/null +++ b/test-boot @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 + +import common +import shell_helpers +from shell_helpers import LF + +class Main(common.TestCliFunction): + def __init__(self): + super().__init__( + description='''\ +Test and benchmark the Linux kernel boot. Use inits that exit immediately. +''' + ) + self.add_argument( + '--size', + default=1, + type=int, + help='''\ +See ./test --help for --size. +''' + ) + + def _bench(self, **kwargs): + words = [] + for line in self.run.get_cli(**kwargs): + words.extend(line) + extra_params = shell_helpers.ShellHelpers().cmd_to_string(words + [LF]) + run_args = kwargs.copy() + run_args.update(self.common_args) + self.run_test(self.run, run_args, extra_params) + + def timed_main(self): + # TODO bring this benchmark code back to life. Likely should go inside run with an option + #gem5_insts() ( + # printf "instructions $(./gem5-stat --arch "$1" sim_insts)\n" >> "$self.env['test_boot_benchmark_file']" + # newline + #) + # + #qemu_insts() ( + # common_arch="$1" + # ./qemu-trace2txt --arch "$common_arch" + # common_qemu_trace_txt_file="$("$getvar" --arch "$common_arch" qemu_trace_txt_file)" + # printf "instructions $(wc -l "${common_qemu_trace_txt_file}" | cut -d' ' -f1)\n" >> "$self.env['test_boot_benchmark_file']" + # newline + #) + # + #rm -f "${self.env['test_boot_benchmark_file']}" + self.run = self.import_path_main('run') + self.common_args = self.get_common_args() + self.common_args['ctrl_c_host'] = True + self.common_args['quit_after_boot'] = True + if (self.env['emulator'] == 'qemu' or + (self.env['emulator'] == 'gem5' and self.env['size'] >= 2)): + self._bench() + if self.env['host_arch'] == self.env['arch']: + # TODO: find out why it fails. + if self.env['emulator'] != 'gem5': + self._bench(kvm=True) + if self.env['emulator'] == 'qemu' and self.env['size'] >= 2: + self._bench(trace='exec_tb') + if self.env['emulator'] == 'gem5' and self.env['size'] >= 3: + if self.env['arch'] == 'x86_64': + cpu_types = [ + # TODO segfault + #'DerivO3CPU' + ] + elif self.env['is_arm']: + cpu_types = [ + 'DerivO3CPU', + 'HPI', + ] + for cpu_type in cpu_types: + self._bench( + extra_emulator_args=[ + '--cpu-type', cpu_type, + '--caches', + '--l2cache', + '--l1d_size', '1024kB', + '--l1i_size', '1024kB', + '--l2_size', '1024kB', + '--l3_size', '1024kB', + ], + ) + if self.env['arch'] == 'aarch64': + # Do a fuller testing for aarch64. + for build_type in ['debug', 'fast']: + self._bench(gem5_build_type=build_type) + # Requires patching the executable. + # self._bench(gem5_script='biglittle') + +if __name__ == '__main__': + Main().cli() diff --git a/test-gdb b/test-gdb index 3840fe4d..1590abf6 100755 --- a/test-gdb +++ b/test-gdb @@ -1,35 +1,59 @@ -#!/usr/bin/env bash -set -eux -for emulator in --qemu --gem5; do - # Userland. - # TODO make work. - #./run --arch x86_64 --background --userland add "$emulator" --wait-gdb & - #./run-gdb --arch x86_64 --userland add "$emulator" --test "$@" - #wait +#!/usr/bin/env python3 - # Baremetal. - ./run --arch arm --background --baremetal add "$emulator" --wait-gdb & - ./run-gdb --arch arm --baremetal add "$emulator" --test "$@" - wait - ./run --arch arm --background --baremetal arch/arm/add "$emulator" --wait-gdb & - ./run-gdb --arch arm --baremetal arch/arm/add "$emulator" --test "$@" - wait - ./run --arch arm --background --baremetal arch/arm/regs "$emulator" --wait-gdb & - ./run-gdb --arch arm --baremetal arch/arm/regs "$emulator" --test "$@" - wait - ./run --arch aarch64 --background --baremetal add "$emulator" --wait-gdb & - ./run-gdb --arch aarch64 --baremetal add "$emulator" --test "$@" - wait - ./run --arch aarch64 --background --baremetal arch/aarch64/add "$emulator" --wait-gdb & - ./run-gdb --arch aarch64 --baremetal arch/aarch64/add "$emulator" --test "$@" - wait - ./run --arch aarch64 --background --baremetal arch/aarch64/regs "$emulator" --wait-gdb & - ./run-gdb --arch aarch64 --baremetal arch/aarch64/regs "$emulator" --test "$@" - wait - ./run --arch aarch64 --background --baremetal arch/aarch64/fadd "$emulator" --wait-gdb & - ./run-gdb --arch aarch64 --baremetal arch/aarch64/fadd "$emulator" --test "$@" - wait - ./run --arch aarch64 --background --baremetal arch/aarch64/regs "$emulator" --wait-gdb & - ./run-gdb --arch aarch64 --baremetal arch/aarch64/regs "$emulator" --test "$@" - wait -done +import threading +import os + +import common + +class Main(common.TestCliFunction): + def __init__(self): + super().__init__( + description='''\ +https://github.com/cirosantilli/linux-kernel-module-cheat#test-gdb +''' + ) + self.add_argument( + 'tests', + nargs='*', + help='''\ +If given, run only the given tests. Otherwise, run all tests, +found by searching for the Python test files. +''' + ) + + def timed_main(self): + run = self.import_path_main('run') + run_gdb = self.import_path_main('run-gdb') + if self.env['arch'] in self.env['crosstool_ng_supported_archs']: + if self.env['tests'] == []: + test_scripts_noext = [] + for f in os.listdir(self.env['baremetal_source_dir']): + base, ext = os.path.splitext(f) + if ext == '.py': + test_scripts_noext.append(base) + for root, dirs, files in os.walk(os.path.join(self.env['baremetal_source_dir'], 'arch', self.env['arch'])): + for f in files: + base, ext = os.path.splitext(f) + if ext == '.py': + full_path = os.path.join(root, base) + relpath = os.path.relpath(full_path, self.env['baremetal_source_dir']) + test_scripts_noext.append(relpath) + else: + test_scripts_noext = self.env['tests'] + for test_script_noext in test_scripts_noext: + common_args = self.get_common_args() + common_args['baremetal'] = test_script_noext + test_id_string = self.test_setup(test_script_noext) + run_args = common_args.copy() + run_args['wait_gdb'] = True + run_args['background'] = True + run_thread = threading.Thread(target=lambda: run(**run_args)) + run_thread.start() + gdb_args = common_args.copy() + gdb_args['test'] = True + run_gdb(**gdb_args) + run_thread.join() + self.test_teardown(run, 0, test_id_string) + +if __name__ == '__main__': + Main().cli() diff --git a/test-modules b/test-modules deleted file mode 100755 index 83a539a1..00000000 --- a/test-modules +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash -set -eu -root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" -getvar="${root_dir}/getvar" -termout_file="$("$getvar" termout_file)" -./run --eval-busybox '/test_all.sh;/poweroff.out' --kvm -grep -q lkmc_test_pass "$termout_file" diff --git a/test-user-mode b/test-user-mode new file mode 100755 index 00000000..fc3c70c4 --- /dev/null +++ b/test-user-mode @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 + +import os +import sys + +import common + +class Main(common.TestCliFunction): + def __init__(self): + super().__init__( + description='''\ +https://github.com/cirosantilli/linux-kernel-module-cheat#user-mode-tests +''' + , + ) + self.add_argument( + 'tests', + nargs='*', + help='''\ +If given, run only the given tests. Otherwise, run all tests. +''' + ) + + def timed_main(self): + run = self.import_path_main('run') + run_args = self.get_common_args() + run_args['ctrl_c_host'] = True + if self.env['emulator'] == 'gem5': + run_args['userland_build_id'] = 'static' + if self.env['tests'] == []: + sources = [ + 'add.c', + 'hello.c', + 'hello_cpp.cpp', + 'print_argv.c', + ] + if self.env['arch'] == 'x86_64': + arch_sources = [ + 'asm_hello' + ] + elif self.env['arch'] == 'aarch64': + arch_sources = [ + 'asm_hello' + ] + else: + arch_sources = [] + arch_sources[:] = [ + os.path.join('arch', self.env['arch'], arch_source) + for arch_source + in arch_sources + ] + sources.extend(arch_sources) + else: + sources = self.env['tests'] + for source in sources: + run_args['userland'] = source + self.run_test(run, run_args, source) + +if __name__ == '__main__': + Main().cli() diff --git a/test-userland-full-system b/test-userland-full-system new file mode 100755 index 00000000..9178d2d9 --- /dev/null +++ b/test-userland-full-system @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +import os +import sys + +import common + +class Main(common.TestCliFunction): + def __init__(self): + super().__init__( + description='''\ +https://github.com/cirosantilli/linux-kernel-module-cheat#test-userland-in-full-system +''' + ) + def timed_main(self): + run = self.import_path_main('run') + run_args = self.get_common_args() + run_args['eval_after'] = '/test_all.sh;{};'.format(self.env['userland_quit_cmd']) + self.run_test(run, run_args) + +if __name__ == '__main__': + Main().cli() diff --git a/trace-boot b/trace-boot index 2daac4b2..43369798 100755 --- a/trace-boot +++ b/trace-boot @@ -1,63 +1,57 @@ #!/usr/bin/env python3 -import imp -import os -import subprocess -import re - import common -run = imp.load_source('run', os.path.join(common.root_dir, 'run')) -qemu_trace2txt = imp.load_source('qemu_trace2txt', os.path.join(common.root_dir, 'qemu-trace2txt')) +from shell_helpers import LF -parser = common.get_argparse(argparse_args={ - 'description': '''Trace the PIC addresses executed on a Linux kernel boot. +class Main(common.LkmcCliFunction): + def __init__(self): + super().__init__( + description='''Trace the PIC addresses executed on a Linux kernel boot. More information at: https://github.com/cirosantilli/linux-kernel-module-cheat#tracing ''' -}) -parser.add_argument( - 'extra_emulator_args', nargs='*', - help='Extra options to append at the end of the emulator command line' -) -args = common.setup(parser) -extra_args = { - 'extra_emulator_args': args.extra_emulator_args, -} -if common.emulator == 'gem5': - extra_args.update({ - 'eval': 'm5 exit', - 'trace': 'Exec,-ExecSymbol,-ExecMicro', - }) - run.main(args, extra_args) -else: - extra_args.update({ - 'kernel_cli': 'init=/poweroff.out', - 'trace': 'exec_tb', - }) - run.main(args, extra_args) - qemu_trace2txt.main() - # 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(common.vmlinux)) - nlines = 0 - nlines_firmware = 0 - with open(common.qemu_trace_txt_file, 'r') as trace_file: - in_firmware = True - for line in trace_file: - line = line.rstrip() - nlines += 1 - pc = line.split('=')[-1] - if pc == kernel_entry_addr: - in_firmware = False - if in_firmware: - nlines_firmware += 1 - print('''\ + ) + + def timed_main(self): + args = self.get_common_args() + run = self.import_path_main('run') + if self.env['emulator'] == 'gem5': + args['trace'] = 'Exec,-ExecSymbol,-ExecMicro' + run.main(**args) + elif self.env['emulator'] == 'qemu': + run_args = args.copy() + run_args['trace'] = 'exec_tb' + run_args['quit_after_boot'] = True + run.main(**run_args) + qemu_trace2txt = self.import_path_main('qemu-trace2txt') + qemu_trace2txt.main(**args) + # 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(self.get_elf_entry(self.env['vmlinux'])) + nlines = 0 + nlines_firmware = 0 + with open(self.env['qemu_trace_txt_file'], 'r') as trace_file: + in_firmware = True + for line in trace_file: + line = line.rstrip() + nlines += 1 + pc = line.split('=')[-1] + if pc == kernel_entry_addr: + in_firmware = False + if in_firmware: + nlines_firmware += 1 + print('''\ instructions {} entry_address {} -instructions_firmware {}\ +instructions_firmware {} '''.format( - nlines, - kernel_entry_addr, - nlines_firmware - )) + nlines, + kernel_entry_addr, + nlines_firmware + ), + end='' + ) + +if __name__ == '__main__': + Main().cli() diff --git a/trace2line b/trace2line index 451a50db..cd53d98c 100755 --- a/trace2line +++ b/trace2line @@ -13,23 +13,24 @@ import subprocess import sys import common +from shell_helpers import LF -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(common.root_dir, 'trace2line.sh'), - 'true' if common.emulator == 'gem5' else 'false', - common.trace_txt_file, - common.get_toolchain_tool('addr2line'), - common.vmlinux, - common.run_dir, + os.path.join(kwargs['root_dir'], 'trace2line.sh'), + 'true' if kwargs['emulator'] == 'gem5' else 'false', + kwargs['trace_txt_file'], + self.get_toolchain_tool('addr2line'), + kwargs['vmlinux'], + kwargs['run_dir'], ]).wait()) # This was the full conversion attempt. -# if common.emulator == 'gem5': +# if kwargs['emulator'] == 'gem5': # def get_pc(line): # # TODO # # stdin = sed -r 's/^.* (0x[^. ]*)[. ].*/\1/' "$common_trace_txt_file") @@ -40,17 +41,17 @@ sys.exit(subprocess.Popen([ # with \ # subprocess.Popen( # [ -# common.get_toolchain_tool('addr2line'), +# self.get_toolchain_tool('addr2line'), # '-e', -# common.vmlinux, +# kwargs['vmlinux'], # '-f', # '-p', # ], # stdout=subprocess.PIPE, # stdin=subprocess.PIPE, # ) as proc, \ -# open(common.trace_txt_file, 'r') as infile, \ -# open(os.path.join(common.run_dir, 'trace-lines.txt'), 'w') as outfile \ +# open(kwargs['trace_txt_file'], 'r') as infile, \ +# open(os.path.join(kwargs['run_dir'], 'trace-lines.txt'), 'w') as outfile \ # : # for in_line in infile: # proc.stdin.write(get_pc(in_line).encode()) @@ -58,5 +59,5 @@ sys.exit(subprocess.Popen([ # stdout = proc.stdout.read() # outfile.write(stdout.decode()) # # TODO -# # sed -E "s|at ${common.linux_build_dir}/(\./\|)||" +# # sed -E "s|at ${kwargs['linux_build_dir']}/(\./\|)||" # # uniq -c diff --git a/userland/Makefile b/userland/Makefile index a02e2295..bb0bee3f 100644 --- a/userland/Makefile +++ b/userland/Makefile @@ -46,7 +46,9 @@ OUTS := $(addprefix $(OUT_DIR)/,$(OUTS)) all: mkdir $(OUTS) for subdir in $(SUBDIRS); do \ - $(MAKE) -C $${subdir} OUT_DIR="$(OUT_DIR)/$$subdir"; \ + if [ -d "$${subdir}" ]; then \ + $(MAKE) -C "$${subdir}" OUT_DIR="$(OUT_DIR)/$$subdir"; \ + fi \ done $(COMMON_OBJ): $(COMMON_DIR)/$(COMMON_BASENAME)$(IN_EXT_C) @@ -64,7 +66,9 @@ $(OUT_DIR)/%$(OUT_EXT): %$(IN_EXT_CXX) $(COMMON_OBJ) clean: rm -f *'$(OBJ_EXT)' *'$(OUT_EXT)' for subdir in $(SUBDIRS); do \ - $(MAKE) -C $${subdir} clean; \ + if [ -d "$${subdir}" ]; then \ + $(MAKE) -C $${subdir} clean; \ + fi \ done mkdir: diff --git a/userland/count.c b/userland/count.c index e6a60825..9a29b324 100644 --- a/userland/count.c +++ b/userland/count.c @@ -1,11 +1,18 @@ +#include #include +#include #include -int main(void) { - int i = 0; - while (1) { - printf("%d\n", i); - i++; - sleep(1); - } +int main(int argc, char **argv) { + unsigned long i = 0, max; + if (argc > 1) { + max = strtoul(argv[1], NULL, 10); + } else { + max = ULONG_MAX; + } + while (i < max) { + printf("%lu\n", i); + i++; + sleep(1); + } } diff --git a/userland/external.desc b/userland/external.desc deleted file mode 100644 index 5c5ffabb..00000000 --- a/userland/external.desc +++ /dev/null @@ -1 +0,0 @@ -name: USERLAND diff --git a/userland/false.c b/userland/false.c new file mode 100644 index 00000000..54252370 --- /dev/null +++ b/userland/false.c @@ -0,0 +1,13 @@ +/* Test that emulators forward the exit status properly. */ + +#include + +int main(int argc, char **argv) { + int ret; + if (argc == 1) { + ret = 1; + } else { + ret = strtoull(argv[1], NULL, 0); + } + return ret; +} diff --git a/userland/print_argv.c b/userland/print_argv.c index afecb822..da7bc6ad 100644 --- a/userland/print_argv.c +++ b/userland/print_argv.c @@ -1,3 +1,5 @@ +/* Print each command line argument received, one per line. */ + #include int main(int argc, char **argv) {