feat: Add chainboot boot loader

This commit is contained in:
Berkus Decker 2022-01-23 02:04:49 +02:00
parent 3c57c6e2df
commit cfe4a230de
13 changed files with 579 additions and 11 deletions

23
Cargo.lock generated
View File

@ -20,6 +20,23 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chainboot"
version = "0.0.1"
dependencies = [
"bit_field",
"bitflags",
"cfg-if",
"cortex-a",
"machine",
"r0",
"seahash",
"snafu",
"tock-registers",
"usize_conversions",
"ux",
]
[[package]]
name = "cortex-a"
version = "7.0.0"
@ -106,6 +123,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd7a31eed1591dcbc95d92ad7161908e72f4677f8fabf2a32ca49b4237cbf211"
[[package]]
name = "seahash"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "snafu"
version = "0.7.0"

View File

@ -1,6 +1,7 @@
[workspace]
members = [
"nucleus"
"nucleus",
"bin/chainboot"
]
[profile.dev]

View File

@ -6,6 +6,13 @@ zellij:
cargo make zellij-nucleus
zellij --layout-path emulation/layout.zellij
# Build and run chainboot in QEMU with serial port emulation
zellij-cb:
# Connect to it via chainofcommand to load an actual kernel
# TODO: actually run chainofcommand in a zellij session too
cargo make zellij-cb
zellij --layout-path emulation/layout.zellij
# Build and run kernel in QEMU
qemu:
cargo make qemu
@ -14,6 +21,11 @@ qemu:
qemu-gdb:
cargo make qemu-gdb
# Build and run chainboot in QEMU
qemu-cb:
# Connect to it via chainofcommand to load an actual kernel
cargo make qemu-cb
# Build and write kernel to an SD Card
device:
cargo make sdcard
@ -22,6 +34,11 @@ device:
device-eject:
cargo make sdeject
# Build and write chainboot to an SD Card, then eject the SD Card volume
cb-eject:
cd bin/chainboot
cargo make cb-eject
# Build default hw kernel
build:
cargo make build
@ -61,6 +78,10 @@ openocd:
gdb:
cargo make gdb
# Build and run chainboot in GDB using openocd or QEMU as target (gdb port 5555)
gdb-cb:
cargo make gdb-cb
# Build and print all symbols in the kernel
nm:
cargo make nm

View File

@ -95,6 +95,13 @@ env = { "TARGET_FEATURES" = "${QEMU_FEATURES}" }
command = "cargo"
args = ["build", "@@split(PLATFORM_TARGET, )", "--release"]
[tasks.qemu-runner]
dependencies = ["build-qemu", "kernel-binary"]
env = { "TARGET_FEATURES" = "${QEMU_FEATURES}" }
script = [
"${QEMU} ${QEMU_OPTS} ${QEMU_RUNNER_OPTS} -dtb ${TARGET_DTB} -kernel ${KERNEL_BIN}"
]
[tasks.expand]
env = { "TARGET_FEATURES" = "" }
command = "cargo"

39
bin/chainboot/Cargo.toml Normal file
View File

@ -0,0 +1,39 @@
[package]
name = "chainboot"
version = "0.0.1"
authors = ["Berkus Decker <berkus+vesper@metta.systems>"]
description = "Chain boot loader"
license = "BlueOak-1.0.0"
categories = ["no-std", "embedded", "os"]
publish = false
edition = "2021"
[badges]
maintenance = { status = "experimental" }
[features]
default = ["asm"]
# Build for running under QEMU with semihosting, so various halt/reboot options would for example quit QEMU instead.
qemu = ["machine/qemu"]
# Build for debugging it over JTAG/SWD connection - halts on first non-startup function start.
jtag = ["machine/jtag"]
# Dummy feature, ignored in this crate.
noserial = []
# Startup relocation code is implemented in assembly
asm = []
# Mutually exclusive features to choose a target board
rpi3 = ["machine/rpi3"]
rpi4 = ["machine/rpi4"]
[dependencies]
machine = { path = "../../machine" }
r0 = "1.0"
cortex-a = "7.0"
tock-registers = "0.7"
ux = { version = "0.1", default-features = false }
usize_conversions = "0.2"
bit_field = "0.10"
bitflags = "1.3"
cfg-if = "1.0"
snafu = { version = "0.7", default-features = false }
seahash = "4.1"

View File

@ -0,0 +1,52 @@
[env]
CHAINBOOT_ELF = "${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/target/${TARGET}/release/chainboot"
CHAINBOOT_BIN = "${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/target/chainboot.bin"
[tasks.kernel-binary]
env = { "BINARY_FILE" = "${CHAINBOOT_ELF}" }
run_task = "custom-binary"
[tasks.hopper]
disabled = true
[tasks.zellij-nucleus]
disabled = true
[tasks.zellij-cb]
env = { "KERNEL_BIN" = "${CHAINBOOT_BIN}", "QEMU_OPTS" = "${QEMU_OPTS} ${QEMU_DISASM_OPTS}" }
run_task = "zellij-config"
[tasks.zellij-cb-gdb]
env = { "KERNEL_BIN" = "${CHAINBOOT_BIN}", "QEMU_OPTS" = "${QEMU_OPTS} ${QEMU_DISASM_OPTS} ${QEMU_GDB_OPTS}", "TARGET_BOARD" = "rpi3", "TARGET_DTB" = "${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/targets/bcm2710-rpi-3-b-plus.dtb" }
run_task = "zellij-config"
[tasks.qemu]
disabled = true
[tasks.qemu-cb]
env = { "QEMU_RUNNER_OPTS" = "${QEMU_SERIAL_OPTS}", "KERNEL_BIN" = "${CHAINBOOT_BIN}", "TARGET_DTB" = "${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/targets/bcm2710-rpi-3-b-plus.dtb" }
extend = "qemu-runner"
[tasks.gdb]
disabled = true
[tasks.gdb-cb]
dependencies = ["build", "kernel-binary", "gdb-config"]
env = { "RUST_GDB" = "${GDB}" }
script = [
"rust-gdb -x ${GDB_CONNECT_FILE} ${CHAINBOOT_ELF}"
]
[tasks.sdcard]
dependencies = ["build", "kernel-binary"]
script_runner = "@duckscript"
script = [
'''
kernelImage = set "chain_boot_rpi4.img"
cp ${CHAINBOOT_BIN} ${VOLUME}/${kernelImage}
echo "Copied chainboot to ${VOLUME}/${kernelImage}"
'''
]
[tasks.cb-eject]
dependencies = ["sdeject"]

6
bin/chainboot/build.rs Normal file
View File

@ -0,0 +1,6 @@
const LINKER_SCRIPT: &str = "bin/chainboot/src/link.ld";
fn main() {
println!("cargo:rerun-if-changed={}", LINKER_SCRIPT);
println!("cargo:rustc-link-arg=--script={}", LINKER_SCRIPT);
}

67
bin/chainboot/src/boot.rs Normal file
View File

@ -0,0 +1,67 @@
// Assembly counterpart to this file.
#[cfg(feature = "asm")]
core::arch::global_asm!(include_str!("boot.s"));
// This is quite impossible - the linker constants are resolved to fully constant offsets in asm
// version, but are image-relative symbols in rust, and I see no way to force it otherwise.
#[no_mangle]
#[link_section = ".text._start"]
#[cfg(not(feature = "asm"))]
pub unsafe extern "C" fn _start() -> ! {
use {
cortex_a::registers::{MPIDR_EL1, SP},
machine::endless_sleep,
tock_registers::interfaces::{Readable, Writeable},
};
const CORE_0: u64 = 0;
const CORE_MASK: u64 = 0x3;
if CORE_0 == MPIDR_EL1.get() & CORE_MASK {
// if not core0, infinitely wait for events
endless_sleep()
}
// These are a problem, because they are not interpreted as constants here.
// Subsequently, this code tries to read values from not-yet-existing data locations.
extern "C" {
// Boundaries of the .bss section, provided by the linker script
static mut __bss_start: u64;
static mut __bss_end_exclusive: u64;
// Load address of the kernel binary
static mut __binary_nonzero_lma: u64;
// Address to relocate to and image size
static mut __binary_nonzero_vma: u64;
static mut __binary_nonzero_vma_end_exclusive: u64;
// Stack top
static mut __boot_core_stack_end_exclusive: u64;
}
// Set stack pointer.
SP.set(&mut __boot_core_stack_end_exclusive as *mut u64 as u64);
// Zeroes the .bss section
r0::zero_bss(&mut __bss_start, &mut __bss_end_exclusive);
// Relocate the code
core::ptr::copy_nonoverlapping(
&mut __binary_nonzero_lma as *const u64,
&mut __binary_nonzero_vma as *mut u64,
(&mut __binary_nonzero_vma_end_exclusive as *mut u64 as u64
- &mut __binary_nonzero_vma as *mut u64 as u64) as usize,
);
_start_rust();
}
//--------------------------------------------------------------------------------------------------
// Public Code
//--------------------------------------------------------------------------------------------------
/// The Rust entry of the `kernel` binary.
///
/// The function is called from the assembly `_start` function, keep it to support "asm" feature.
#[no_mangle]
pub unsafe fn _start_rust(max_kernel_size: u64) -> ! {
crate::kernel_init(max_kernel_size)
}

93
bin/chainboot/src/boot.s Normal file
View File

@ -0,0 +1,93 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
//
// Copyright (c) 2021 Andre Richter <andre.o.richter@gmail.com>
// Modifications
// Copyright (c) 2021- Berkus <berkus+github@metta.systems>
//--------------------------------------------------------------------------------------------------
// Definitions
//--------------------------------------------------------------------------------------------------
// Load the address of a symbol into a register, PC-relative.
//
// The symbol must lie within +/- 4 GiB of the Program Counter.
//
// # Resources
//
// - https://sourceware.org/binutils/docs-2.36/as/AArch64_002dRelocations.html
.macro ADR_REL register, symbol
adrp \register, \symbol
add \register, \register, #:lo12:\symbol
.endm
// Load the address of a symbol into a register, absolute.
//
// # Resources
//
// - https://sourceware.org/binutils/docs-2.36/as/AArch64_002dRelocations.html
.macro ADR_ABS register, symbol
movz \register, #:abs_g2:\symbol
movk \register, #:abs_g1_nc:\symbol
movk \register, #:abs_g0_nc:\symbol
.endm
//--------------------------------------------------------------------------------------------------
// Public Code
//--------------------------------------------------------------------------------------------------
.section .text._start
//------------------------------------------------------------------------------
// fn _start()
//------------------------------------------------------------------------------
_start:
// Only proceed on the boot core. Park it otherwise.
mrs x1, MPIDR_EL1
and x1, x1, 0b11 // core id mask
cmp x1, 0 // boot core id
b.ne .L_parking_loop
// If execution reaches here, it is the boot core.
// Initialize bss.
ADR_ABS x0, __bss_start
ADR_ABS x1, __bss_end_exclusive
.L_bss_init_loop:
cmp x0, x1
b.eq .L_relocate_binary
stp xzr, xzr, [x0], #16
b .L_bss_init_loop
// Next, relocate the binary.
.L_relocate_binary:
ADR_REL x0, __binary_nonzero_lma // The address the binary got loaded to.
ADR_ABS x1, __binary_nonzero_vma // The address the binary was linked to.
ADR_ABS x2, __binary_nonzero_vma_end_exclusive
sub x4, x1, x0 // Get difference between vma and lma as max size
.L_copy_loop:
ldr x3, [x0], #8
str x3, [x1], #8
cmp x1, x2
b.lo .L_copy_loop
// Prepare the jump to Rust code.
// Set the stack pointer.
ADR_ABS x0, __rpi_phys_binary_load_addr
mov sp, x0
// Pass maximum kernel size as an argument to Rust init function.
mov x0, x4
// Jump to the relocated Rust code.
ADR_ABS x1, _start_rust
br x1
// Infinitely wait for events (aka "park the core").
.L_parking_loop:
wfe
b .L_parking_loop
.size _start, . - _start
.type _start, function
.global _start

98
bin/chainboot/src/link.ld Normal file
View File

@ -0,0 +1,98 @@
/* SPDX-License-Identifier: MIT OR Apache-2.0
*
* Copyright (c) 2018-2021 Andre Richter <andre.o.richter@gmail.com>
* Copyright (c) 2021- Berkus <berkus+github@metta.systems>
*/
/*
* Information from:
* [Output Section Address](https://sourceware.org/binutils/docs/ld/Output-Section-Address.html)
* [Output Section LMA](https://sourceware.org/binutils/docs/ld/Output-Section-LMA.html)
* [Output Section Attributes](https://sourceware.org/binutils/docs/ld/Output-Section-Attributes.html#Output-Section-Attributes)
*/
/* The physical address at which the the kernel binary will be loaded by the Raspberry's firmware */
__rpi_phys_binary_load_addr = 0x80000;
ENTRY(__rpi_phys_binary_load_addr)
/* Flags:
* 4 == R
* 5 == RX
* 6 == RW
*
* Segments are marked PT_LOAD below so that the ELF file provides virtual and physical addresses.
* It doesn't mean all of them need actually be loaded.
*/
PHDRS
{
segment_boot_core_stack PT_LOAD FLAGS(6);
segment_start_code PT_LOAD FLAGS(5);
segment_code PT_LOAD FLAGS(5);
segment_data PT_LOAD FLAGS(6);
}
SECTIONS
{
/***********************************************************************************************
* Boot Core Stack
***********************************************************************************************/
.boot_core_stack (NOLOAD) :
{
/* ^ */
/* | stack */
. += __rpi_phys_binary_load_addr; /* | growth */
/* | direction */
__boot_core_stack_end_exclusive = .; /* | */
} :segment_boot_core_stack
. = __rpi_phys_binary_load_addr;
.text :
{
KEEP(*(.text._start))
/* *(text.memcpy) -- only relevant for Rust relocator impl which is currently impossible */
} :segment_start_code
/* Align to 8 bytes, b/c relocating the binary is done in u64 chunks */
. = ALIGN(8);
__binary_nonzero_lma = .;
/* Set the link address to 32 MiB */
/* This dictates the max size of the loadable kernel. */
. += 0x2000000;
/***********************************************************************************************
* Code + RO Data + Global Offset Table
***********************************************************************************************/
__binary_nonzero_vma = .;
.text : AT (ADDR(.text) + SIZEOF(.text))
{
*(.text._start_rust) /* The Rust entry point */
/* *(text.memcpy) -- only relevant for Rust relocator impl which is currently impossible */
*(.text*) /* Everything else */
} :segment_code
.rodata : ALIGN(8) { *(.rodata*) } :segment_code
.got : ALIGN(8) { *(.got) } :segment_code
/***********************************************************************************************
* Data + BSS
***********************************************************************************************/
.data : { *(.data*) } :segment_data
/* Fill up to 8 bytes, b/c relocating the binary is done in u64 chunks */
. = ALIGN(8);
__binary_nonzero_vma_end_exclusive = .;
/* Section is zeroed in pairs of u64. Align start and end to 16 bytes */
.bss (NOLOAD) : ALIGN(16)
{
__bss_start = .;
*(.bss*);
. = ALIGN(16);
__bss_end_exclusive = .;
} :segment_data
}

153
bin/chainboot/src/main.rs Normal file
View File

@ -0,0 +1,153 @@
// Based on miniload by @andre-richter
#![feature(format_args_nl)]
#![feature(custom_test_frameworks)]
#![test_runner(machine::tests::test_runner)]
#![reexport_test_harness_main = "test_main"]
#![no_main]
#![no_std]
use {
core::{hash::Hasher, panic::PanicInfo},
cortex_a::asm::barrier,
machine::{
devices::SerialOps,
platform::rpi3::{gpio::GPIO, pl011_uart::PL011Uart, BcmHost},
print, println, CONSOLE,
},
seahash::SeaHasher,
};
mod boot;
/// Early init code.
///
/// # Safety
///
/// - Only a single core must be active and running this function.
/// - The init calls in this function must appear in the correct order.
unsafe fn kernel_init(max_kernel_size: u64) -> ! {
#[cfg(feature = "jtag")]
machine::arch::jtag::wait_debugger();
let gpio = GPIO::default();
let uart = PL011Uart::default();
let uart = uart.prepare(&gpio).expect("What could go wrong?");
CONSOLE.lock(|c| {
// Move uart into the global CONSOLE.
c.replace_with(uart.into());
});
// println! is usable from here on.
// Transition from unsafe to safe.
kernel_main(max_kernel_size)
}
// https://onlineasciitools.com/convert-text-to-ascii-art (FIGlet) with `cricket` font
const LOGO: &str = r#"
__ __ __ __
.----| |--.---.-|__.-----| |--.-----.-----| |_
| __| | _ | | | _ | _ | _ | _|
|____|__|__|___._|__|__|__|_____|_____|_____|____|
"#;
fn read_u64() -> u64 {
CONSOLE.lock(|c| {
let mut val: u64 = u64::from(c.read_byte());
val |= u64::from(c.read_byte()) << 8;
val |= u64::from(c.read_byte()) << 16;
val |= u64::from(c.read_byte()) << 24;
val |= u64::from(c.read_byte()) << 32;
val |= u64::from(c.read_byte()) << 40;
val |= u64::from(c.read_byte()) << 48;
val |= u64::from(c.read_byte()) << 56;
val
})
}
/// The main function running after the early init.
fn kernel_main(max_kernel_size: u64) -> ! {
#[cfg(test)]
test_main();
print!("{}", LOGO);
println!("{:>51}\n", BcmHost::board_name());
println!("[<<] Requesting kernel image...");
let kernel_addr: *mut u8 = BcmHost::kernel_load_address() as *mut u8;
loop {
CONSOLE.lock(|c| c.flush());
// Discard any spurious received characters before starting with the loader protocol.
CONSOLE.lock(|c| c.clear_rx());
// Notify `chainofcommand` to send the binary.
for _ in 0..3 {
CONSOLE.lock(|c| c.write_byte(3u8));
}
// Read the binary's size.
let size = read_u64();
// Check the size to fit RAM
if size > max_kernel_size {
println!("ERR Kernel image too big (over {} bytes)", max_kernel_size);
continue;
}
print!("OK");
// We use seahash, simple and with no_std implementation.
let mut hasher = SeaHasher::new();
// Read the kernel byte by byte.
for i in 0..size {
let val = CONSOLE.lock(|c| c.read_byte());
unsafe {
core::ptr::write_volatile(kernel_addr.offset(i as isize), val);
}
let written = unsafe { core::ptr::read_volatile(kernel_addr.offset(i as isize)) };
hasher.write_u8(written);
}
// Read the binary's checksum.
let checksum = read_u64();
let valid = hasher.finish() == checksum;
if !valid {
println!("ERR Kernel image checksum mismatch");
continue;
}
print!("OK");
break;
}
println!(
"[<<] Loaded! Executing the payload now from {:p}\n",
kernel_addr
);
CONSOLE.lock(|c| c.flush());
// Use black magic to create a function pointer.
let kernel: fn() -> ! = unsafe { core::mem::transmute(kernel_addr) };
// Force everything to complete before we jump.
unsafe { barrier::isb(barrier::SY) };
// Jump to loaded kernel!
kernel()
}
#[cfg(not(test))]
#[panic_handler]
fn panicked(info: &PanicInfo) -> ! {
machine::panic::handler(info)
}
#[cfg(test)]
#[panic_handler]
fn panicked(info: &PanicInfo) -> ! {
machine::panic::handler_for_tests(info)
}

View File

@ -2,7 +2,7 @@
name = "machine"
version = "0.0.1"
authors = ["Berkus Decker <berkus+vesper@metta.systems>"]
description = "Vesper nanokernel shared code library."
description = "Vesper nanokernel shared code library, useful also for the chainboot loader."
documentation = "https://docs.metta.systems/vesper"
homepage = "https://github.com/metta-systems/vesper"
repository = "https://github.com/metta-systems/vesper"

View File

@ -7,25 +7,27 @@
env = { "BINARY_FILE" = "${KERNEL_ELF}" }
run_task = "custom-binary"
[tasks.qemu-runner]
dependencies = ["build-qemu", "kernel-binary"]
env = { "TARGET_FEATURES" = "${QEMU_FEATURES}" }
script = [
"${QEMU} ${QEMU_OPTS} ${QEMU_RUNNER_OPTS} -dtb ${TARGET_DTB} -kernel ${KERNEL_BIN}"
]
[tasks.qemu]
extend = "qemu-runner"
env = { "QEMU_RUNNER_OPTS" = "${QEMU_SERIAL_OPTS}", "TARGET_DTB" = "${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/targets/bcm2710-rpi-3-b-plus.dtb" }
extend = "qemu-runner"
[tasks.qemu-cb]
disabled = true
[tasks.qemu-gdb]
extend = "qemu-runner"
env = { "QEMU_RUNNER_OPTS" = "${QEMU_SERIAL_OPTS} ${QEMU_GDB_OPTS}", "TARGET_DTB" = "${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/targets/bcm2710-rpi-3-b-plus.dtb" }
extend = "qemu-runner"
[tasks.zellij-nucleus]
env = { "KERNEL_BIN" = "${KERNEL_BIN}" }
run_task = "zellij-config"
[tasks.zellij-cb]
disabled = true
[tasks.zellij-cb-gdb]
disabled = true
[tasks.gdb-config]
script_runner = "@duckscript"
script = [
@ -44,6 +46,9 @@ script = [
"rust-gdb -x ${GDB_CONNECT_FILE} ${KERNEL_ELF}"
]
[tasks.gdb-cb]
disabled = true
[tasks.nm]
dependencies = ["build", "kernel-binary"]
script = [
@ -62,6 +67,9 @@ script = [
'''
]
[tasks.cb-eject]
disabled = true
[tasks.hopper]
dependencies = ["build", "kernel-binary"]
# The cmd line below causes a bug in hopper, see https://www.dropbox.com/s/zyw5mfx0bepcjb1/hopperv4-RAW-bug.mov?dl=0