diff --git a/Cargo.lock b/Cargo.lock index 7182cb2..3982c1c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index a47b552..3be9524 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ - "nucleus" + "nucleus", + "bin/chainboot" ] [profile.dev] diff --git a/Justfile b/Justfile index 8e8d0d8..cdd02b7 100644 --- a/Justfile +++ b/Justfile @@ -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 diff --git a/Makefile.toml b/Makefile.toml index 02ae498..606904d 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -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" diff --git a/bin/chainboot/Cargo.toml b/bin/chainboot/Cargo.toml new file mode 100644 index 0000000..070cdac --- /dev/null +++ b/bin/chainboot/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "chainboot" +version = "0.0.1" +authors = ["Berkus Decker "] +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" diff --git a/bin/chainboot/Makefile.toml b/bin/chainboot/Makefile.toml new file mode 100644 index 0000000..7e7a4cf --- /dev/null +++ b/bin/chainboot/Makefile.toml @@ -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"] diff --git a/bin/chainboot/build.rs b/bin/chainboot/build.rs new file mode 100644 index 0000000..33a3d71 --- /dev/null +++ b/bin/chainboot/build.rs @@ -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); +} diff --git a/bin/chainboot/src/boot.rs b/bin/chainboot/src/boot.rs new file mode 100644 index 0000000..63a0069 --- /dev/null +++ b/bin/chainboot/src/boot.rs @@ -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) +} diff --git a/bin/chainboot/src/boot.s b/bin/chainboot/src/boot.s new file mode 100644 index 0000000..c2ef874 --- /dev/null +++ b/bin/chainboot/src/boot.s @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// +// Copyright (c) 2021 Andre Richter +// Modifications +// Copyright (c) 2021- Berkus + +//-------------------------------------------------------------------------------------------------- +// 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 diff --git a/bin/chainboot/src/link.ld b/bin/chainboot/src/link.ld new file mode 100644 index 0000000..3d8b693 --- /dev/null +++ b/bin/chainboot/src/link.ld @@ -0,0 +1,98 @@ +/* SPDX-License-Identifier: MIT OR Apache-2.0 + * + * Copyright (c) 2018-2021 Andre Richter + * Copyright (c) 2021- Berkus + */ + +/* + * 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 +} diff --git a/bin/chainboot/src/main.rs b/bin/chainboot/src/main.rs new file mode 100644 index 0000000..641800c --- /dev/null +++ b/bin/chainboot/src/main.rs @@ -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) +} diff --git a/machine/Cargo.toml b/machine/Cargo.toml index b5ec72d..bc6c63e 100644 --- a/machine/Cargo.toml +++ b/machine/Cargo.toml @@ -2,7 +2,7 @@ name = "machine" version = "0.0.1" authors = ["Berkus Decker "] -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" diff --git a/nucleus/Makefile.toml b/nucleus/Makefile.toml index c7d8fe6..3beaf26 100644 --- a/nucleus/Makefile.toml +++ b/nucleus/Makefile.toml @@ -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