Skip to content

Commit dea7e41

Browse files
author
Johnathan Van Why
committed
Add a runner for libtock2 examples.
This replaces tools/flash.sh. In the future, it will have functionality to process test output, and will therefore replace test_runner as well. It can currently deploy to Hifive1 on QEMU, and has untested logic to deploy via `tockloader`. This PR does not attempt to keep Tock 1.0 support working -- instead, it disables the Tock 1.0 CI components that break. It would take significantly more effort to continue supporting the Tock 1.0 crates, so I think it is time to remove the Tock 1.0 crates. What do you think? Additional changes: 1. added a `setup-qemu-2` build target to build the tock2 submodule's QEMU module. I intend to rename this to `setup-qemu` after the Tock 1.0 targets and submodules are removed. 2. I updated the tock2 submodule to a newer kernel. [ci skip]
1 parent 85242dc commit dea7e41

File tree

10 files changed

+381
-3
lines changed

10 files changed

+381
-3
lines changed

.cargo/config

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ rustflags = [
1212
"-C", "relocation-model=static",
1313
"-C", "link-arg=-Tlayout.ld",
1414
]
15-
runner = "./tools/flash.sh"
15+
runner = ["cargo", "run", "-p", "runner", "--release"]

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ members = [
9090
"libtock2",
9191
"panic_handlers/small_panic",
9292
"platform",
93+
"runner",
9394
"runtime",
9495
"syscalls_tests",
9596
"test_runner",

Makefile

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ usage:
2626
@echo " Set the DEBUG flag to enable the debug build"
2727
@echo " Set the FEATURES flag to enable features"
2828
@echo "Run 'make flash-<board> EXAMPLE=<>' to flash EXAMPLE to that board"
29+
@echo "Run 'make qemu-example EXAMPLE=<>' to run EXAMPLE in QEMU"
2930
@echo "Run 'make test' to test any local changes you have made"
3031
@echo "Run 'make print-sizes' to print size data for the example binaries"
3132

@@ -38,7 +39,7 @@ release=--release
3839
endif
3940

4041
.PHONY: setup
41-
setup: setup-qemu
42+
setup: setup-qemu setup-qemu-2
4243
cargo install elf2tab
4344
cargo install stack-sizes
4445
cargo miri setup
@@ -49,6 +50,12 @@ setup: setup-qemu
4950
setup-qemu:
5051
CI=true $(MAKE) -C tock ci-setup-qemu
5152

53+
# Sets up QEMU in the tock2/ directory. We use Tock's QEMU which may contain
54+
# patches to better support boards that Tock supports.
55+
.PHONY: setup-qemu-2
56+
setup-qemu-2:
57+
CI=true $(MAKE) -C tock2 ci-setup-qemu
58+
5259
# Builds a Tock kernel for the HiFive board for use by QEMU tests.
5360
.PHONY: kernel-hifive
5461
kernel-hifive:
@@ -68,6 +75,12 @@ kernel-hifive-2:
6875
print-sizes: examples
6976
cargo run --release -p print_sizes
7077

78+
# Runs a libtock2 example in QEMU on a simulated HiFive board.
79+
.PHONY: qemu-example
80+
qemu-example: kernel-hifive-2
81+
LIBTOCK_PLATFORM="hifive1" cargo run --example "$(EXAMPLE)" -p libtock2 \
82+
--release --target=riscv32imac-unknown-none-elf -- --deploy qemu
83+
7184
# Runs the libtock_test tests in QEMU on a simulated HiFive board.
7285
.PHONY: test-qemu-hifive
7386
test-qemu-hifive: kernel-hifive

runner/Cargo.toml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[package]
2+
authors = ["Tock Project Developers <[email protected]>"]
3+
description = """Tool used to run libtock-rs process binaries."""
4+
edition = "2018"
5+
license = "Apache-2.0 OR MIT"
6+
name = "runner"
7+
publish = false
8+
repository = "https://www.github.com/tock/libtock-rs"
9+
version = "0.1.0"
10+
11+
[dependencies]
12+
clap = { features = ["derive"], version = "3.0.10" }
13+
elf = "0.0.10"
14+
signal-hook = "0.3.13"

runner/src/elf2tab.rs

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
use super::Cli;
2+
use std::fs::{metadata, remove_file};
3+
use std::io::ErrorKind;
4+
use std::path::PathBuf;
5+
use std::process::Command;
6+
7+
// Converts the ELF file specified on the command line into TBF and TAB files,
8+
// and returns the paths to those files.
9+
pub fn convert_elf(cli: &Cli) -> OutFiles {
10+
let package_name = cli.elf.file_stem().expect("ELF must be a file");
11+
let mut tab_path = cli.elf.clone();
12+
tab_path.set_extension("tab");
13+
let protected_size = TBF_HEADER_SIZE.to_string();
14+
if cli.verbose {
15+
println!("Package name: {:?}", package_name);
16+
println!("TAB path: {}", tab_path.display());
17+
println!("Protected region size: {}", protected_size);
18+
}
19+
let stack_size = read_stack_size(cli);
20+
let elf = cli.elf.as_os_str();
21+
let mut tbf_path = cli.elf.clone();
22+
tbf_path.set_extension("tbf");
23+
if cli.verbose {
24+
println!("ELF file: {:?}", elf);
25+
println!("TBF path: {}", tbf_path.display());
26+
}
27+
28+
// If elf2tab returns a successful status but does not write to the TBF
29+
// file, then we run the risk of using an outdated TBF file, creating a
30+
// hard-to-debug situation. Therefore, we delete the TBF file, forcing
31+
// elf2tab to create it, and later verify that it exists.
32+
if let Err(io_error) = remove_file(&tbf_path) {
33+
// Ignore file-no-found errors, panic on any other error.
34+
assert_eq!(
35+
io_error.kind(),
36+
ErrorKind::NotFound,
37+
"Unable to remove the TBF file"
38+
);
39+
}
40+
41+
let mut command = Command::new("elf2tab");
42+
#[rustfmt::skip]
43+
command.args([
44+
// TODO: libtock-rs' crates are designed for Tock 2.1's Allow interface,
45+
// so we should increment this as soon as the Tock kernel will accept a
46+
// 2.1 app.
47+
"--kernel-major".as_ref(), "2".as_ref(),
48+
"--kernel-minor".as_ref(), "0".as_ref(),
49+
"-n".as_ref(), package_name,
50+
"-o".as_ref(), tab_path.as_os_str(),
51+
"--protected-region-size".as_ref(), protected_size.as_ref(),
52+
"--stack".as_ref(), stack_size.as_ref(),
53+
elf,
54+
]);
55+
if cli.verbose {
56+
command.arg("-v");
57+
println!("elf2tab command: {:?}", command);
58+
println!("Spawning elf2tab");
59+
}
60+
let mut child = command.spawn().expect("failed to spawn elf2tab");
61+
let status = child.wait().expect("failed to wait for elf2tab");
62+
if cli.verbose {
63+
println!("elf2tab finished. {}", status);
64+
}
65+
assert!(status.success(), "elf2tab returned an error. {}", status);
66+
67+
// Verify that elf2tab created the TBF file, and that it is a file.
68+
match metadata(&tbf_path) {
69+
Err(io_error) => {
70+
if io_error.kind() == ErrorKind::NotFound {
71+
panic!("elf2tab did not create {}", tbf_path.display());
72+
}
73+
panic!(
74+
"Unable to query metadata for {}: {}",
75+
tbf_path.display(),
76+
io_error
77+
);
78+
}
79+
Ok(metadata) => {
80+
assert!(metadata.is_file(), "{} is not a file", tbf_path.display());
81+
}
82+
}
83+
84+
OutFiles { tab_path, tbf_path }
85+
}
86+
87+
// Paths to the files output by elf2tab.
88+
pub struct OutFiles {
89+
pub tab_path: PathBuf,
90+
pub tbf_path: PathBuf,
91+
}
92+
93+
// The amount of space to reserve for the TBF header.
94+
const TBF_HEADER_SIZE: u32 = 0x48;
95+
96+
// Reads the stack size, and returns it as a String for use on elf2tab's command
97+
// line.
98+
fn read_stack_size(cli: &Cli) -> String {
99+
let file = elf::File::open_path(&cli.elf).expect("Unable to open ELF");
100+
for section in file.sections {
101+
// This section name comes from runtime/libtock_layout.ld, and it
102+
// matches the size (and location) of the process binary's stack.
103+
if section.shdr.name == ".stack" {
104+
let stack_size = section.shdr.size.to_string();
105+
if cli.verbose {
106+
println!("Found .stack section, size: {}", stack_size);
107+
}
108+
return stack_size;
109+
}
110+
}
111+
112+
panic!("Unable to find the .stack section in {}", cli.elf.display());
113+
}

runner/src/main.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
mod elf2tab;
2+
mod output_processor;
3+
mod qemu;
4+
mod tockloader;
5+
6+
use clap::{ArgEnum, Parser};
7+
use std::path::PathBuf;
8+
9+
/// Converts ELF binaries into Tock Binary Format binaries and runs them on a
10+
/// Tock system.
11+
#[derive(Debug, Parser)]
12+
pub struct Cli {
13+
/// Where to deploy the process binary. If not specified, runner will only
14+
/// make a TBF file and not attempt to run it.
15+
#[clap(arg_enum, long, short)]
16+
deploy: Option<Deploy>,
17+
18+
/// The executable to convert into Tock Binary Format and run.
19+
elf: PathBuf,
20+
21+
/// Whether to output verbose debugging information to the console.
22+
#[clap(long, short)]
23+
verbose: bool,
24+
}
25+
26+
#[derive(ArgEnum, Clone, Debug)]
27+
pub enum Deploy {
28+
Qemu,
29+
Tockloader,
30+
}
31+
32+
fn main() {
33+
let cli = Cli::parse();
34+
let paths = elf2tab::convert_elf(&cli);
35+
let child = match cli.deploy {
36+
None => return,
37+
Some(Deploy::Qemu) => qemu::deploy(&cli, paths.tbf_path),
38+
Some(Deploy::Tockloader) => tockloader::deploy(&cli, paths.tab_path),
39+
};
40+
output_processor::process(&cli, child);
41+
}

runner/src/output_processor.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
use super::Cli;
2+
use signal_hook::flag::{register, register_conditional_default};
3+
use std::io::{stdout, BufReader, Read, Write};
4+
use std::process::Child;
5+
use std::sync::atomic::AtomicBool;
6+
use std::sync::Arc;
7+
8+
/// Reads the console messages from `child`'s standard output, sending SIGTERM
9+
/// to the child when the process is terminated.
10+
pub fn process(cli: &Cli, mut child: Child) {
11+
// When Ctrl+C is pressed, Bash sends SIGINT to both us and our child
12+
// process. If we finish before the child process does, then bash will print
13+
// the shell prompt before the child process prints its final messages,
14+
// which is annoying for the user.
15+
//
16+
// So instead, we ignore the first SIGINT we receive. Ctrl+C should
17+
// therefore result in a clean shutdown: the child process exits, then we
18+
// see that it finished and terminates. As a fail-safe in case we fail to
19+
// terminate, this combination will make us exit if SIGINT is received a
20+
// second time.
21+
let sigint_received = Arc::new(AtomicBool::new(false));
22+
register_conditional_default(signal_hook::consts::SIGINT, sigint_received.clone())
23+
.expect("Unable to register SIGINT conditional handler.");
24+
register(signal_hook::consts::SIGINT, sigint_received)
25+
.expect("Unable to register SIGINT handler.");
26+
27+
let reader = BufReader::new(child.stdout.as_mut().expect("Child's stdout not piped."));
28+
for byte in reader.bytes() {
29+
let byte = byte.expect("Unexpected IO error.");
30+
stdout()
31+
.write_all(&[byte])
32+
.expect("Failed to write to stdout.");
33+
}
34+
if cli.verbose {
35+
println!("Waiting for child process to exit");
36+
}
37+
let status = child.wait().expect("Unable to wait for child process");
38+
assert!(
39+
status.success(),
40+
"Child process did not exit successfully. {}",
41+
status
42+
);
43+
}

runner/src/qemu.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
use super::Cli;
2+
use std::path::PathBuf;
3+
use std::process::{Child, Command, Stdio};
4+
5+
// Spawns a QEMU VM with a simulated Tock system and the process binary. Returns
6+
// the handle for the spawned QEMU process.
7+
pub fn deploy(cli: &Cli, tab_path: PathBuf) -> Child {
8+
let device = format!(
9+
"loader,file={},addr=0x20040000",
10+
tab_path
11+
.into_os_string()
12+
.into_string()
13+
.expect("Non-UTF-8 path")
14+
);
15+
let mut qemu = Command::new("tock2/tools/qemu/build/qemu-system-riscv32");
16+
#[rustfmt::skip]
17+
qemu.args([
18+
"-device", &device,
19+
"-kernel", "tock2/target/riscv32imac-unknown-none-elf/release/hifive1",
20+
"-M", "sifive_e,revb=true",
21+
"-nographic",
22+
]);
23+
// QEMU does something to its stdin that prevents Ctrl+C from generating
24+
// SIGINT. If we set QEMU's stdin to be our stdin, then Ctrl+C will not
25+
// close us. To prevent that, we set QEMU's stdin to null.
26+
qemu.stdin(Stdio::null());
27+
qemu.stdout(Stdio::piped());
28+
if cli.verbose {
29+
println!("QEMU command: {:?}", qemu);
30+
println!("Spawning QEMU")
31+
}
32+
qemu.spawn().expect("failed to spawn QEMU")
33+
}

0 commit comments

Comments
 (0)