diff --git a/.cargo/config b/.cargo/config index 7b76c17..aa17488 100644 --- a/.cargo/config +++ b/.cargo/config @@ -6,4 +6,11 @@ target = "i686-unknown-none.json" [target.i686-unknown-none] rustflags = ["-C", "link-args=-T tests/shared/i686/link.ld"] -runner = "sh tests/runner.sh" +runner = "tests/runner.sh" + +[target.riscv32imac-unknown-none-elf] +rustflags = [ + "-C", "link-arg=-Ttests/shared/riscv32/memory.x", + "-C", "link-arg=-Tlink.x", +] +runner = "tests/runner.sh" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a6dab92..82e2cc6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,10 +25,14 @@ jobs: toolchain: ${{ matrix.rust }} override: true components: rust-src, rustfmt, clippy + target: riscv32imac-unknown-none-elf - - name: Setup QEMU + - name: Setup QEMU for RISC-V + run: sudo apt-get update && sudo apt-get install -y qemu-system-misc + + - name: Setup QEMU for i686 if: ${{ matrix.rust == 'nightly' }} - run: sudo apt-get update && sudo apt-get install -y qemu-system-x86 + run: sudo apt-get install -y qemu-system-x86 - name: Build (without testing) as x86_64 if: ${{ matrix.rust == 'nightly' }} # Tier 2 (precompiled libcore in rustup) since 1.62 @@ -44,6 +48,12 @@ jobs: command: test args: --target i686-unknown-none.json + - name: Build and test as RISC-V + uses: actions-rs/cargo@v1 + with: + command: test + args: --target riscv32imac-unknown-none-elf --no-default-features + - name: Rustfmt if: ${{ matrix.rust == 'nightly' }} uses: actions-rs/cargo@v1 diff --git a/Cargo.toml b/Cargo.toml index d1cc1cf..c325b59 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,3 +24,7 @@ test = false [[test]] name = "main" harness = false + +[target.'cfg(target_arch = "riscv32")'.dev-dependencies] +riscv-rt = "0.10.0" +fdt = "0.1.3" diff --git a/src/lib.rs b/src/lib.rs index b5f8a75..1b61012 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,6 +29,7 @@ extern crate alloc; #[cfg(feature = "alloc")] use alloc::vec::Vec; +use core::fmt; use core::mem::size_of; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] @@ -52,10 +53,17 @@ pub enum FwCfgError { /// A struct for accessing QEMU fw_cfg. #[derive(Debug)] -pub struct FwCfg(()); +pub struct FwCfg(Mode); + +#[derive(Debug)] +enum Mode { + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + IOPort, + MemoryMapped(MemoryMappedDevice), +} impl FwCfg { - /// Build `FwCfg` from the builder. + /// Build `FwCfg` for the x86/x86-64 I/O port. /// /// # Safety /// @@ -64,16 +72,35 @@ impl FwCfg { /// /// Only one `FwCfg` value may exist at the same time /// since it accesses a global shared stateful resource. - pub unsafe fn new() -> Result { + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + pub unsafe fn new_for_x86() -> Result { + Self::new_for_mode(Mode::IOPort) + } + + /// Build `FwCfg` for the device memory-mapped at the give base pointer. + /// + /// # Safety + /// + /// The pointer must point to a valid fw_cfg device. + /// + /// Only one `FwCfg` value may exist at the same time for that pointer. + pub unsafe fn new_memory_mapped(base_ptr: *mut ()) -> Result { + let device = MemoryMappedDevice::new(base_ptr); + Self::new_for_mode(Mode::MemoryMapped(device)) + } + + unsafe fn new_for_mode(mode: Mode) -> Result { + let mut fw_cfg = FwCfg(mode); + let mut signature = [0u8; SIGNATURE_DATA.len()]; - Self::write_selector(selector_keys::SIGNATURE); - Self::read_data(&mut signature); + fw_cfg.select(selector_keys::SIGNATURE); + fw_cfg.read(&mut signature); if signature != SIGNATURE_DATA { return Err(FwCfgError::InvalidSignature); } - Ok(FwCfg(())) + Ok(fw_cfg) } /// Return an iterator of all files in the fw_cfg directory @@ -163,14 +190,18 @@ impl FwCfg { } fn select(&mut self, key: u16) { - unsafe { - Self::write_selector(key); + match &mut self.0 { + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + Mode::IOPort => unsafe { arch::write_selector(key) }, + Mode::MemoryMapped(device) => device.write_selector(key), } } fn read(&mut self, buffer: &mut [u8]) { - unsafe { - Self::read_data(buffer); + match &mut self.0 { + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + Mode::IOPort => unsafe { arch::read_data(buffer) }, + Mode::MemoryMapped(device) => device.read_data(buffer), } } } @@ -178,7 +209,7 @@ impl FwCfg { const _: () = assert!(size_of::() == 64); /// A struct that contains information of a fw_cfg file. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Clone, PartialEq, Eq)] // NOTE: The memory layout of this struct must match this exactly: // https://gitlab.com/qemu-project/qemu/-/blob/v7.0.0/docs/specs/fw_cfg.txt#L132-137 #[repr(C)] @@ -225,3 +256,49 @@ impl FwCfgFile { unsafe { &mut *ptr } } } + +impl fmt::Debug for FwCfgFile { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + fmt.debug_struct("FwCfgFile") + .field("key", &self.key()) + .field("size", &self.size()) + .field("name", &self.name()) + .finish() + } +} + +#[derive(Debug)] +struct MemoryMappedDevice { + base_ptr: *mut (), +} + +impl MemoryMappedDevice { + unsafe fn new(base_ptr: *mut ()) -> Self { + Self { base_ptr } + } + + fn register(&self, offset_in_bytes: usize) -> *mut T { + let offset = offset_in_bytes / size_of::(); + unsafe { self.base_ptr.cast::().add(offset) } + } + + fn write_selector(&mut self, key: u16) { + // https://gitlab.com/qemu-project/qemu/-/blob/v7.0.0/docs/specs/fw_cfg.txt#L87 + let selector_offset = 8; + let selector_ptr = self.register::(selector_offset); + unsafe { selector_ptr.write_volatile(key.to_be()) } + } + + fn read_data(&mut self, data: &mut [u8]) { + // https://gitlab.com/qemu-project/qemu/-/blob/v7.0.0/docs/specs/fw_cfg.txt#L88 + let data_offset = 0; + let data_ptr = self.register::(data_offset); + for chunk in data.chunks_mut(size_of::()) { + let word = unsafe { data_ptr.read_volatile() }; + // https://gitlab.com/qemu-project/qemu/-/blob/v7.0.0/docs/specs/fw_cfg.txt#L53 + // "string-preserving" means native-endian + let bytes = word.to_ne_bytes(); + chunk.copy_from_slice(&bytes[..chunk.len()]); + } + } +} diff --git a/src/x86.rs b/src/x86.rs index d3af3f9..3758d2b 100644 --- a/src/x86.rs +++ b/src/x86.rs @@ -1,6 +1,6 @@ -use crate::FwCfg; use core::arch::asm; +// https://gitlab.com/qemu-project/qemu/-/blob/v7.0.0/docs/specs/fw_cfg.txt#L79 const IO_PORT_SELECTOR: u16 = 0x510; const IO_PORT_DATA: u16 = 0x511; @@ -22,14 +22,12 @@ unsafe fn out_u16(address: u16, data: u16) { ); } -impl FwCfg { - pub(crate) unsafe fn write_selector(key: u16) { - out_u16(IO_PORT_SELECTOR, key); - } +pub(crate) unsafe fn write_selector(key: u16) { + out_u16(IO_PORT_SELECTOR, key); +} - pub(crate) unsafe fn read_data(buffer: &mut [u8]) { - for i in buffer { - *i = in_u8(IO_PORT_DATA); - } +pub(crate) unsafe fn read_data(buffer: &mut [u8]) { + for i in buffer { + *i = in_u8(IO_PORT_DATA); } } diff --git a/tests/main.rs b/tests/main.rs index eeaf772..efb2ead 100644 --- a/tests/main.rs +++ b/tests/main.rs @@ -2,15 +2,15 @@ #![no_main] #![cfg_attr(feature = "alloc", feature(default_alloc_error_handler))] -use qemu_fw_cfg::FwCfg; +use core::fmt::Write; mod shared; const DATA_INPUT_TXT: &'static [u8] = include_bytes!("input.txt"); -#[no_mangle] +#[cfg_attr(not(target_arch = "riscv32"), no_mangle)] fn main() { - let mut fw_cfg = unsafe { FwCfg::new().unwrap() }; + let mut fw_cfg = unsafe { shared::fw_cfg() }; // File exist let file_input_txt = fw_cfg.find_file("opt/input.txt").unwrap(); @@ -54,4 +54,6 @@ fn main() { let mut buffer = [0u8; DATA_INPUT_TXT.len() / 2]; fw_cfg.read_file_to_buffer(&file_input_txt, &mut buffer); assert_eq!(DATA_INPUT_TXT[..buffer.len()], buffer); + + writeln!(shared::Writer, "✅ Test sucessful").unwrap(); } diff --git a/tests/runner.sh b/tests/runner.sh old mode 100644 new mode 100755 index 33d89fa..eaceb6c --- a/tests/runner.sh +++ b/tests/runner.sh @@ -1,21 +1,28 @@ -#!/bin/sh +#!/bin/bash TARGET=$(echo $1 | sed 's/.*target\/\([^\/]*\).*/\1/') ARCH=$(echo $TARGET | awk -F '-' '{ print $1 }') -if [ "$ARCH" = "i686" ]; then - qemu-system-i386 \ - -kernel $1 \ - -m 32M \ - -display none \ - -fw_cfg opt/input.txt,file=tests/input.txt \ - -fw_cfg opt/567890123456789012345678901234567890123456789012345,file=tests/input.txt \ - -device isa-debug-exit \ - -serial stdio - - status=$(($? >> 1)) -fi +QEMU_OPTS=" -m 32M" +QEMU_OPTS+=" -display none" +QEMU_OPTS+=" -fw_cfg opt/input.txt,file=tests/input.txt" +QEMU_OPTS+=" -fw_cfg opt/567890123456789012345678901234567890123456789012345,file=tests/input.txt" +QEMU_OPTS+=" -serial stdio" -if [ $status -gt 0 ]; then - exit $status +if [[ "$ARCH" == i686 ]]; then + exec qemu-system-i386 $QEMU_OPTS \ + -device isa-debug-exit \ + -kernel "$@" +elif [[ "$ARCH" == riscv32* ]]; then + if [[ -z "$GDB" ]]; then + exec qemu-system-riscv32 $QEMU_OPTS \ + -machine virt \ + -bios none \ + -kernel "$@" + else + exec riscv32-elf-gdb -ex 'target remote :1234' "$@" + fi +else + echo Unsupported TARGET=$TARGET + exit 1 fi diff --git a/tests/shared/i686/mod.rs b/tests/shared/i686/mod.rs index a0c0bea..4afea73 100644 --- a/tests/shared/i686/mod.rs +++ b/tests/shared/i686/mod.rs @@ -1,4 +1,5 @@ use core::arch::{asm, global_asm}; +use qemu_fw_cfg::FwCfg; global_asm!(include_str!("boot.asm")); @@ -31,3 +32,7 @@ impl core::fmt::Write for Writer { Ok(()) } } + +pub unsafe fn fw_cfg() -> FwCfg { + FwCfg::new_for_x86().unwrap() +} diff --git a/tests/shared/mod.rs b/tests/shared/mod.rs index 0889bf4..b08a29a 100644 --- a/tests/shared/mod.rs +++ b/tests/shared/mod.rs @@ -2,6 +2,10 @@ #[path = "i686/mod.rs"] mod arch; +#[cfg(target_arch = "riscv32")] +#[path = "riscv32/mod.rs"] +mod arch; + pub use arch::*; use core::{ diff --git a/tests/shared/riscv32/memory.x b/tests/shared/riscv32/memory.x new file mode 100644 index 0000000..2f985e6 --- /dev/null +++ b/tests/shared/riscv32/memory.x @@ -0,0 +1,12 @@ +MEMORY +{ + RAM : ORIGIN = 0x80000000, LENGTH = 16M +} + +REGION_ALIAS("REGION_TEXT", RAM); +REGION_ALIAS("REGION_RODATA", RAM); + +REGION_ALIAS("REGION_DATA", RAM); +REGION_ALIAS("REGION_BSS", RAM); +REGION_ALIAS("REGION_HEAP", RAM); +REGION_ALIAS("REGION_STACK", RAM); diff --git a/tests/shared/riscv32/mod.rs b/tests/shared/riscv32/mod.rs new file mode 100644 index 0000000..1965393 --- /dev/null +++ b/tests/shared/riscv32/mod.rs @@ -0,0 +1,57 @@ +use core::ptr::null_mut; +use core::sync::atomic::{AtomicPtr, Ordering}; +use qemu_fw_cfg::FwCfg; + +static EXIT: AtomicPtr = AtomicPtr::new(null_mut()); +static UART: AtomicPtr = AtomicPtr::new(null_mut()); +static FW_CFG: AtomicPtr<()> = AtomicPtr::new(null_mut()); + +#[riscv_rt::entry] +fn main(_hart_id: usize, fdt_address: usize) -> ! { + let fdt = unsafe { fdt::Fdt::from_ptr(fdt_address as _).unwrap() }; + find_compatible_reg(&fdt, "sifive,test1", &EXIT); + find_compatible_reg(&fdt, "ns16550a", &UART); + find_compatible_reg(&fdt, "qemu,fw-cfg-mmio", &FW_CFG); + crate::main(); + exit(0) +} + +fn find_compatible_reg(fdt: &fdt::Fdt, with: &str, ptr: &AtomicPtr) { + ptr.store( + fdt.find_compatible(&[with]) + .unwrap() + .reg() + .unwrap() + .next() + .unwrap() + .starting_address as _, + Ordering::Release, + ) +} + +#[no_mangle] +pub extern "C" fn exit(status: u8) -> ! { + unsafe { + let ptr = EXIT.load(Ordering::Acquire); + ptr.write_volatile((status as u32) << 16 | 0x3333); + } + loop {} +} + +pub struct Writer; + +impl core::fmt::Write for Writer { + fn write_str(&mut self, s: &str) -> core::fmt::Result { + let uart = UART.load(Ordering::Acquire); + for b in s.bytes() { + unsafe { + uart.write_volatile(b); + } + } + Ok(()) + } +} + +pub unsafe fn fw_cfg() -> FwCfg { + FwCfg::new_memory_mapped(FW_CFG.load(Ordering::Acquire)).unwrap() +}