diff --git a/.cargo/config b/.cargo/config index 8292c1c0..44e0274a 100644 --- a/.cargo/config +++ b/.cargo/config @@ -12,4 +12,4 @@ rustflags = [ "-C", "relocation-model=static", "-C", "link-arg=-Tlayout.ld", ] -runner = "./tools/flash.sh" +runner = ["cargo", "run", "-p", "runner", "--release"] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 25f832f5..57efd081 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,7 +43,7 @@ jobs: - name: Build and Test run: | sudo apt-get install binutils-arm-none-eabi \ - binutils-riscv64-unknown-elf + binutils-riscv64-unknown-elf ninja-build cd "${GITHUB_WORKSPACE}" echo "[target.'cfg(all())']" >> .cargo/config echo 'rustflags = ["-D", "warnings"]' >> .cargo/config diff --git a/Cargo.toml b/Cargo.toml index 81f248d9..fac60e5a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -82,7 +82,7 @@ lto = true debug = true [workspace] -exclude = [ "tock" ] +exclude = ["tock", "tock2"] members = [ "apis/low_level_debug", "codegen", @@ -90,6 +90,7 @@ members = [ "libtock2", "panic_handlers/small_panic", "platform", + "runner", "runtime", "syscalls_tests", "test_runner", diff --git a/Makefile b/Makefile index 37e4e376..01e22b89 100644 --- a/Makefile +++ b/Makefile @@ -26,6 +26,7 @@ usage: @echo " Set the DEBUG flag to enable the debug build" @echo " Set the FEATURES flag to enable features" @echo "Run 'make flash- EXAMPLE=<>' to flash EXAMPLE to that board" + @echo "Run 'make qemu-example EXAMPLE=<>' to run EXAMPLE in QEMU" @echo "Run 'make test' to test any local changes you have made" @echo "Run 'make print-sizes' to print size data for the example binaries" @@ -38,7 +39,7 @@ release=--release endif .PHONY: setup -setup: setup-qemu +setup: setup-qemu setup-qemu-2 cargo install elf2tab cargo install stack-sizes cargo miri setup @@ -49,6 +50,12 @@ setup: setup-qemu setup-qemu: CI=true $(MAKE) -C tock ci-setup-qemu +# Sets up QEMU in the tock2/ directory. We use Tock's QEMU which may contain +# patches to better support boards that Tock supports. +.PHONY: setup-qemu-2 +setup-qemu-2: + CI=true $(MAKE) -C tock2 ci-setup-qemu + # Builds a Tock kernel for the HiFive board for use by QEMU tests. .PHONY: kernel-hifive kernel-hifive: @@ -63,11 +70,25 @@ kernel-hifive-2: $(MAKE) -C tock2/boards/hifive1 \ $(CURDIR)/tock2/target/riscv32imac-unknown-none-elf/release/hifive1.elf +# Builds a Tock kernel for the OpenTitan board on the cw310 FPGA for use by QEMU +# tests. +.PHONY: kernel-opentitan +kernel-opentitan: + CARGO_TARGET_RISCV32IMC_UNKNOWN_NONE_ELF_RUNNER="[]" \ + $(MAKE) -C tock2/boards/opentitan/earlgrey-cw310 \ + $(CURDIR)/tock2/target/riscv32imc-unknown-none-elf/release/earlgrey-cw310.elf + # Prints out the sizes of the example binaries. .PHONY: print-sizes print-sizes: examples cargo run --release -p print_sizes +# Runs a libtock2 example in QEMU on a simulated HiFive board. +.PHONY: qemu-example +qemu-example: kernel-hifive-2 + LIBTOCK_PLATFORM="hifive1" cargo run --example "$(EXAMPLE)" -p libtock2 \ + --release --target=riscv32imac-unknown-none-elf -- --deploy qemu + # Runs the libtock_test tests in QEMU on a simulated HiFive board. .PHONY: test-qemu-hifive test-qemu-hifive: kernel-hifive @@ -100,7 +121,7 @@ EXCLUDE_MIRI := $(EXCLUDE_RUNTIME) --exclude libtock_codegen \ # Arguments to pass to cargo to exclude `std` and crates that depend on it. Used # when we build a crate for an embedded target, as those targets lack `std`. EXCLUDE_STD := --exclude libtock_unittest --exclude print_sizes \ - --exclude syscalls_tests --exclude test_runner + --exclude runner --exclude syscalls_tests --exclude test_runner # Some of our crates should build with a stable toolchain. This verifies those # crates don't depend on unstable features by using cargo check. We specify a @@ -112,7 +133,7 @@ test-stable: $(EXCLUDE_RUNTIME) --exclude libtock --exclude libtock_core .PHONY: test -test: examples test-qemu-hifive test-stable +test: examples test-stable PLATFORM=nrf52 cargo test $(EXCLUDE_RUNTIME) --workspace # TODO: When we have a working embedded test harness, change the libtock2 # builds to --all-targets rather than --examples. @@ -238,3 +259,4 @@ flash-msp432: clean: cargo clean $(MAKE) -C tock clean + $(MAKE) -C tock2 clean diff --git a/runner/Cargo.toml b/runner/Cargo.toml new file mode 100644 index 00000000..b6b055eb --- /dev/null +++ b/runner/Cargo.toml @@ -0,0 +1,15 @@ +[package] +authors = ["Tock Project Developers "] +description = """Tool used to run libtock-rs process binaries.""" +edition = "2018" +license = "Apache-2.0 OR MIT" +name = "runner" +publish = false +repository = "https://www.github.com/tock/libtock-rs" +version = "0.1.0" + +[dependencies] +clap = { features = ["derive"], version = "3.0.10" } +elf = "0.0.10" +libc = "0.2.113" +termion = "1.5.6" diff --git a/runner/src/elf2tab.rs b/runner/src/elf2tab.rs new file mode 100644 index 00000000..84a7ee90 --- /dev/null +++ b/runner/src/elf2tab.rs @@ -0,0 +1,113 @@ +use super::Cli; +use std::fs::{metadata, remove_file}; +use std::io::ErrorKind; +use std::path::PathBuf; +use std::process::Command; + +// Converts the ELF file specified on the command line into TBF and TAB files, +// and returns the paths to those files. +pub fn convert_elf(cli: &Cli) -> OutFiles { + let package_name = cli.elf.file_stem().expect("ELF must be a file"); + let mut tab_path = cli.elf.clone(); + tab_path.set_extension("tab"); + let protected_size = TBF_HEADER_SIZE.to_string(); + if cli.verbose { + println!("Package name: {:?}", package_name); + println!("TAB path: {}", tab_path.display()); + println!("Protected region size: {}", protected_size); + } + let stack_size = read_stack_size(cli); + let elf = cli.elf.as_os_str(); + let mut tbf_path = cli.elf.clone(); + tbf_path.set_extension("tbf"); + if cli.verbose { + println!("ELF file: {:?}", elf); + println!("TBF path: {}", tbf_path.display()); + } + + // If elf2tab returns a successful status but does not write to the TBF + // file, then we run the risk of using an outdated TBF file, creating a + // hard-to-debug situation. Therefore, we delete the TBF file, forcing + // elf2tab to create it, and later verify that it exists. + if let Err(io_error) = remove_file(&tbf_path) { + // Ignore file-no-found errors, panic on any other error. + if io_error.kind() != ErrorKind::NotFound { + panic!("Unable to remove the TBF file. Error: {}", io_error); + } + } + + let mut command = Command::new("elf2tab"); + #[rustfmt::skip] + command.args([ + // TODO: libtock-rs' crates are designed for Tock 2.1's Allow interface, + // so we should increment this as soon as the Tock kernel will accept a + // 2.1 app. + "--kernel-major".as_ref(), "2".as_ref(), + "--kernel-minor".as_ref(), "0".as_ref(), + "-n".as_ref(), package_name, + "-o".as_ref(), tab_path.as_os_str(), + "--protected-region-size".as_ref(), protected_size.as_ref(), + "--stack".as_ref(), stack_size.as_ref(), + elf, + ]); + if cli.verbose { + command.arg("-v"); + println!("elf2tab command: {:?}", command); + println!("Spawning elf2tab"); + } + let mut child = command.spawn().expect("failed to spawn elf2tab"); + let status = child.wait().expect("failed to wait for elf2tab"); + if cli.verbose { + println!("elf2tab finished. {}", status); + } + assert!(status.success(), "elf2tab returned an error. {}", status); + + // Verify that elf2tab created the TBF file, and that it is a file. + match metadata(&tbf_path) { + Err(io_error) => { + if io_error.kind() == ErrorKind::NotFound { + panic!("elf2tab did not create {}", tbf_path.display()); + } + panic!( + "Unable to query metadata for {}: {}", + tbf_path.display(), + io_error + ); + } + Ok(metadata) => { + assert!(metadata.is_file(), "{} is not a file", tbf_path.display()); + } + } + + OutFiles { tab_path, tbf_path } +} + +// Paths to the files output by elf2tab. +pub struct OutFiles { + pub tab_path: PathBuf, + pub tbf_path: PathBuf, +} + +// The amount of space to reserve for the TBF header. This must match the +// TBF_HEADER_SIZE value in the layout file for the platform, which is currently +// 0x48 for all platforms. +const TBF_HEADER_SIZE: u32 = 0x48; + +// Reads the stack size, and returns it as a String for use on elf2tab's command +// line. +fn read_stack_size(cli: &Cli) -> String { + let file = elf::File::open_path(&cli.elf).expect("Unable to open ELF"); + for section in file.sections { + // This section name comes from runtime/libtock_layout.ld, and it + // matches the size (and location) of the process binary's stack. + if section.shdr.name == ".stack" { + let stack_size = section.shdr.size.to_string(); + if cli.verbose { + println!("Found .stack section, size: {}", stack_size); + } + return stack_size; + } + } + + panic!("Unable to find the .stack section in {}", cli.elf.display()); +} diff --git a/runner/src/main.rs b/runner/src/main.rs new file mode 100644 index 00000000..be3ff581 --- /dev/null +++ b/runner/src/main.rs @@ -0,0 +1,57 @@ +mod elf2tab; +mod output_processor; +mod qemu; +mod tockloader; + +use clap::{ArgEnum, Parser}; +use std::env::{var, VarError}; +use std::path::PathBuf; + +/// Converts ELF binaries into Tock Binary Format binaries and runs them on a +/// Tock system. +#[derive(Debug, Parser)] +pub struct Cli { + /// Where to deploy the process binary. If not specified, runner will only + /// make a TBF file and not attempt to run it. + #[clap(arg_enum, long, short)] + deploy: Option, + + /// The executable to convert into Tock Binary Format and run. + elf: PathBuf, + + /// Whether to output verbose debugging information to the console. + #[clap(long, short)] + verbose: bool, +} + +#[derive(ArgEnum, Clone, Copy, Debug)] +pub enum Deploy { + Qemu, + Tockloader, +} + +fn main() { + let cli = Cli::parse(); + let paths = elf2tab::convert_elf(&cli); + let deploy = match cli.deploy { + None => return, + Some(deploy) => deploy, + }; + let platform = match var("LIBTOCK_PLATFORM") { + Err(VarError::NotPresent) => { + panic!("LIBTOCK_PLATFORM must be specified to deploy") + } + Err(VarError::NotUnicode(platform)) => { + panic!("Non-UTF-8 LIBTOCK_PLATFORM value: {:?}", platform) + } + Ok(platform) => platform, + }; + if cli.verbose { + println!("Detected platform {}", platform); + } + let child = match deploy { + Deploy::Qemu => qemu::deploy(&cli, platform, paths.tbf_path), + Deploy::Tockloader => tockloader::deploy(&cli, platform, paths.tab_path), + }; + output_processor::process(&cli, child); +} diff --git a/runner/src/output_processor.rs b/runner/src/output_processor.rs new file mode 100644 index 00000000..71cdceca --- /dev/null +++ b/runner/src/output_processor.rs @@ -0,0 +1,129 @@ +use super::Cli; +use libc::{kill, pid_t, SIGINT}; +use std::io::{stderr, stdin, stdout, BufRead, BufReader, ErrorKind, Stdout, Write}; +use std::process::Child; +use std::thread::spawn; +use termion::raw::{IntoRawMode, RawTerminal}; + +/// Reads the console messages from `child`'s standard output, sending SIGTERM +/// to the child when the process is terminated. +pub fn process(cli: &Cli, mut child: Child) { + let raw_mode = forward_stdin_if_piped(&mut child); + forward_stderr_if_piped(&mut child, raw_mode.is_some()); + let mut to_print = Vec::new(); + let mut reader = BufReader::new(child.stdout.as_mut().expect("Child's stdout not piped.")); + loop { + let buffer = reader + .fill_buf() + .expect("Unable to read from child process."); + if buffer.is_empty() { + // The child process has closed its stdout, likely by exiting. + break; + } + // Print the bytes received over stdout. If the terminal is in raw mode, + // translate '\n' into '\r\n'. + for &byte in buffer { + if raw_mode.is_some() && byte == b'\n' { + to_print.push(b'\r'); + } + to_print.push(byte); + } + stdout() + .write_all(&to_print) + .expect("Unable to echo child's stdout."); + to_print.clear(); + + let buffer_len = buffer.len(); + reader.consume(buffer_len); + } + if cli.verbose { + println!("Waiting for child process.\r"); + } + let status = child.wait().expect("Unable to wait for child process"); + drop(raw_mode); + assert!( + status.success(), + "Child process did not exit successfully. {}", + status + ); +} + +// If child's stdin is piped, this sets the terminal to raw mode and spawns a +// thread that forwards our stdin to child's stdin. The thread sends SIGINT to +// the child if Ctrl+C is pressed. Returns a RawTerminal, which reverts the +// terminal to its previous configuration on drop. +fn forward_stdin_if_piped(child: &mut Child) -> Option> { + let mut child_stdin = child.stdin.take()?; + let child_id = child.id(); + spawn(move || { + let our_stdin = stdin(); + let mut our_stdin = our_stdin.lock(); + loop { + let buffer = our_stdin.fill_buf().expect("Failed to read stdin."); + if buffer.is_empty() { + // Our stdin was closed. We interpret this as a signal to exit, + // because pressing Ctrl+C to trigger an exit is no longer + // possible. + break; + } + // In raw mode, pressing Ctrl+C will send a '3' byte to stdin ("end + // of message" ASCII value) instead of sending SIGINT. Identify that + // case, and exit if it occurs. + if buffer.contains(&3) { + break; + } + match child_stdin.write(buffer) { + // A BrokenPipe error occurs when the child has exited. Exit + // without sending SIGINT. + Err(error) if error.kind() == ErrorKind::BrokenPipe => return, + + Err(error) => panic!("Failed to forward stdin: {}", error), + Ok(bytes) => our_stdin.consume(bytes), + } + } + // Send SIGINT to the child, telling it to exit. After the child exits, + // the main loop will detect the exit and we will shut down cleanly. + // + // Safety: Sending SIGINT to a process is a safe operation -- kill is + // marked unsafe because it is a FFI function. + unsafe { + kill(child_id as pid_t, SIGINT); + } + }); + Some( + stdout() + .into_raw_mode() + .expect("Failed to set terminal to raw mode."), + ) +} + +// Forwards child's stderr to our stderr if child's stderr is piped, converting +// line endings to CRLF if raw_mode is true. +fn forward_stderr_if_piped(child: &mut Child, raw_mode: bool) { + let child_stderr = match child.stderr.take() { + None => return, + Some(child_stderr) => child_stderr, + }; + spawn(move || { + let mut to_print = Vec::new(); + let mut reader = BufReader::new(child_stderr); + loop { + let buffer = reader.fill_buf().expect("Unable to read child's stderr."); + if buffer.is_empty() { + return; + } + for &byte in buffer { + if raw_mode && byte == b'\n' { + to_print.push(b'\r'); + } + to_print.push(byte); + } + stderr() + .write_all(&to_print) + .expect("Unable to echo child's stderr."); + to_print.clear(); + let buffer_len = buffer.len(); + reader.consume(buffer_len); + } + }); +} diff --git a/runner/src/qemu.rs b/runner/src/qemu.rs new file mode 100644 index 00000000..af342a65 --- /dev/null +++ b/runner/src/qemu.rs @@ -0,0 +1,67 @@ +use super::Cli; +use std::path::PathBuf; +use std::process::{Child, Command, Stdio}; + +// Spawns a QEMU VM with a simulated Tock system and the process binary. Returns +// the handle for the spawned QEMU process. +pub fn deploy(cli: &Cli, platform: String, tbf_path: PathBuf) -> Child { + let platform_args = get_platform_args(platform); + let device = format!( + "loader,file={},addr={}", + tbf_path + .into_os_string() + .into_string() + .expect("Non-UTF-8 path"), + platform_args.process_binary_load_address, + ); + let mut qemu = Command::new("tock2/tools/qemu/build/qemu-system-riscv32"); + qemu.args(["-device", &device, "-nographic", "-serial", "mon:stdio"]); + qemu.args(platform_args.fixed_args); + // If we let QEMU inherit its stdin from us, it will set it to raw mode, + // which prevents Ctrl+C from generating SIGINT. QEMU will not exit when + // Ctrl+C is entered, making our runner hard to close. Instead, we forward + // stdin to QEMU ourselves -- see output_processor.rs for more details. + qemu.stdin(Stdio::piped()); + qemu.stdout(Stdio::piped()); + // Because we set the terminal to raw mode while running QEMU, but QEMU's + // stdin is not connected to a terminal, QEMU does not know it needs to use + // CRLF line endings when printing to stderr. To convert, we also pipe + // QEMU's stderr through us and output_processor converts the line endings. + qemu.stderr(Stdio::piped()); + if cli.verbose { + println!("QEMU command: {:?}", qemu); + println!("Spawning QEMU") + } + qemu.spawn().expect("failed to spawn QEMU") +} + +// Returns the command line arguments for the given platform to qemu. Panics if +// an unknown platform is passed. +fn get_platform_args(platform: String) -> PlatformConfig { + match platform.as_str() { + "hifive1" => PlatformConfig { + #[rustfmt::skip] + fixed_args: &[ + "-kernel", "tock2/target/riscv32imac-unknown-none-elf/release/hifive1", + "-M", "sifive_e,revb=true", + ], + process_binary_load_address: "0x20040000", + }, + "opentitan" => PlatformConfig { + #[rustfmt::skip] + fixed_args: &[ + "-bios", "tock2/tools/qemu-runner/opentitan-boot-rom.elf", + "-kernel", "tock2/target/riscv32imc-unknown-none-elf/release/earlgrey-cw310", + "-M", "opentitan", + ], + process_binary_load_address: "0x20030000", + }, + _ => panic!("Cannot deploy to platform {} via QEMU.", platform), + } +} + +// QEMU configuration information that is specific to each platform. +struct PlatformConfig { + fixed_args: &'static [&'static str], + process_binary_load_address: &'static str, +} diff --git a/runner/src/tockloader.rs b/runner/src/tockloader.rs new file mode 100644 index 00000000..9b74b531 --- /dev/null +++ b/runner/src/tockloader.rs @@ -0,0 +1,104 @@ +use super::Cli; +use std::path::PathBuf; +use std::process::{Child, Command, Stdio}; + +// Uses tockloader to deploy the provided TAB file to a Tock system. Returns the +// handle for the spawned 'tockloader listen' process. +// Note: This function is untested, as its author does not have hardware that +// works with tockloader. If you use it, please report back on how it works so +// we can fix it or remove this notice! +pub fn deploy(cli: &Cli, platform: String, tab_path: PathBuf) -> Child { + let flags: &[_] = match platform.as_str() { + "hail" => &[], + "microbit_v2" => &["--bundle-apps"], + "nrf52" | "nrf52840" => &[ + "--jlink", + "--arch", + "cortex-m4", + "--board", + "nrf52dk", + "--jtag-device", + "nrf52", + ], + _ => panic!("Cannot deploy to platform {} via tockloader", platform), + }; + if cli.verbose { + println!("Tockloader flags: {:?}", flags); + } + + // Tockloader listen's ability to receive every message from the Tock system + // varies from platform to platform. We look up the platform, and if it is + // not satisfactorily reliable we output a warning for the user. + let reliable_listen = match platform.as_str() { + // tockloader listen will reset the Hail, allowing it to capture all + // printed messages. + "hail" => true, + + // Microbit uses CDC over USB, which buffers messages so that tockloader + // listen can receive messages sent before it was started. As long as + // tockloader listen launches before the timeout, there will not be + // dropped messages. This is good enough for our purposes. + "microbit_v2" => true, + + // tockloader listen doesn't reset the nrf52, and there's no message + // queueing mechanism. Therefore, tockloader listen will likely miss + // messages printed quickly after the process binary is deployed. + "nrf52" | "nrf52840" => false, + + // We shouldn't hit this case, because the flag determination code above + // should error out on unknown platforms. + _ => panic!("Unknown reliability for {}", platform), + }; + if !reliable_listen { + println!( + "Warning: tockloader listen may miss early messages on platform {}", + platform + ); + } + + // Invoke tockloader uninstall to remove the process binary, if present. + let mut uninstall = Command::new("tockloader"); + uninstall.arg("uninstall"); + uninstall.args(flags); + if cli.verbose { + println!("tockloader uninstall command: {:?}", uninstall); + } + let mut child = uninstall + .spawn() + .expect("failed to spawn tockloader uninstall"); + let status = child + .wait() + .expect("failed to wait for tockloader uninstall"); + if cli.verbose { + println!("tockloader uninstall finished. {}", status); + } + + // Invoke tockloader install to deploy the new process binary. + let mut install = Command::new("tockloader"); + install.arg("install"); + install.args(flags); + install.arg(tab_path); + if cli.verbose { + println!("tockloader install command: {:?}", install); + } + let mut child = install.spawn().expect("failed to spawn tockloader install"); + let status = child.wait().expect("failed to wait for tockloader install"); + if cli.verbose { + println!("tockloader install finished. {}", status); + } + assert!( + status.success(), + "tockloader install returned unsuccessful status {}", + status + ); + + // Invoke tockloader listen to receive messages from the Tock system. + let mut listen = Command::new("tockloader"); + listen.arg("listen"); + listen.args(flags); + listen.stdout(Stdio::piped()); + if cli.verbose { + println!("tockloader listen command: {:?}", listen); + } + listen.spawn().expect("failed to spawn tockloader listen") +} diff --git a/tock2 b/tock2 index 17e698e8..935755eb 160000 --- a/tock2 +++ b/tock2 @@ -1 +1 @@ -Subproject commit 17e698e8fb2c9628624398435f75db0f5a50dbcd +Subproject commit 935755eb392030e31468545f6c27fed4d5340683