Skip to content

Add a runner for libtock2 examples. #365

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Feb 3, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .cargo/config
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ rustflags = [
"-C", "relocation-model=static",
"-C", "link-arg=-Tlayout.ld",
]
runner = "./tools/flash.sh"
runner = ["cargo", "run", "-p", "runner", "--release"]
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,15 @@ lto = true
debug = true

[workspace]
exclude = [ "tock" ]
exclude = ["tock", "tock2"]
members = [
"apis/low_level_debug",
"codegen",
"core",
"libtock2",
"panic_handlers/small_panic",
"platform",
"runner",
"runtime",
"syscalls_tests",
"test_runner",
Expand Down
20 changes: 17 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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-<board> 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"

Expand All @@ -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
Expand All @@ -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:
Expand All @@ -68,6 +75,12 @@ kernel-hifive-2:
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
Expand Down Expand Up @@ -100,7 +113,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
Expand All @@ -112,7 +125,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.
Expand Down Expand Up @@ -238,3 +251,4 @@ flash-msp432:
clean:
cargo clean
$(MAKE) -C tock clean
$(MAKE) -C tock2 clean
14 changes: 14 additions & 0 deletions runner/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
authors = ["Tock Project Developers <[email protected]>"]
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"
signal-hook = "0.3.13"
113 changes: 113 additions & 0 deletions runner/src/elf2tab.rs
Original file line number Diff line number Diff line change
@@ -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.
assert_eq!(
io_error.kind(),
ErrorKind::NotFound,
"Unable to remove the TBF file"
);
}

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.
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());
}
41 changes: 41 additions & 0 deletions runner/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
mod elf2tab;
mod output_processor;
mod qemu;
mod tockloader;

use clap::{ArgEnum, Parser};
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<Deploy>,

/// 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, Debug)]
pub enum Deploy {
Qemu,
Tockloader,
}

fn main() {
let cli = Cli::parse();
let paths = elf2tab::convert_elf(&cli);
let child = match cli.deploy {
None => return,
Some(Deploy::Qemu) => qemu::deploy(&cli, paths.tbf_path),
Some(Deploy::Tockloader) => tockloader::deploy(&cli, paths.tab_path),
};
output_processor::process(&cli, child);
}
43 changes: 43 additions & 0 deletions runner/src/output_processor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
use super::Cli;
use signal_hook::flag::{register, register_conditional_default};
use std::io::{stdout, BufReader, Read, Write};
use std::process::Child;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;

/// 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) {
// When Ctrl+C is pressed, Bash sends SIGINT to both us and our child
// process. If we finish before the child process does, then bash will print
// the shell prompt before the child process prints its final messages,
// which is annoying for the user.
//
// So instead, we ignore the first SIGINT we receive. Ctrl+C should
// therefore result in a clean shutdown: the child process exits, then we
// see that it finished and terminates. As a fail-safe in case we fail to
// terminate, this combination will make us exit if SIGINT is received a
// second time.
let sigint_received = Arc::new(AtomicBool::new(false));
register_conditional_default(signal_hook::consts::SIGINT, sigint_received.clone())
.expect("Unable to register SIGINT conditional handler.");
register(signal_hook::consts::SIGINT, sigint_received)
.expect("Unable to register SIGINT handler.");

let reader = BufReader::new(child.stdout.as_mut().expect("Child's stdout not piped."));
for byte in reader.bytes() {
let byte = byte.expect("Unexpected IO error.");
stdout()
.write_all(&[byte])
.expect("Failed to write to stdout.");
}
if cli.verbose {
println!("Waiting for child process to exit");
}
let status = child.wait().expect("Unable to wait for child process");
assert!(
status.success(),
"Child process did not exit successfully. {}",
status
);
}
33 changes: 33 additions & 0 deletions runner/src/qemu.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
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, tab_path: PathBuf) -> Child {
let device = format!(
"loader,file={},addr=0x20040000",
tab_path
.into_os_string()
.into_string()
.expect("Non-UTF-8 path")
);
let mut qemu = Command::new("tock2/tools/qemu/build/qemu-system-riscv32");
#[rustfmt::skip]
qemu.args([
"-device", &device,
"-kernel", "tock2/target/riscv32imac-unknown-none-elf/release/hifive1",
"-M", "sifive_e,revb=true",
"-nographic",
]);
// QEMU does something to its stdin that prevents Ctrl+C from generating
// SIGINT. If we set QEMU's stdin to be our stdin, then Ctrl+C will not
// close us. To prevent that, we set QEMU's stdin to null.
qemu.stdin(Stdio::null());
qemu.stdout(Stdio::piped());
if cli.verbose {
println!("QEMU command: {:?}", qemu);
println!("Spawning QEMU")
}
qemu.spawn().expect("failed to spawn QEMU")
}
Loading