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..010ebe1 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: build + 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..1e4d397 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.9.0" +fdt = "0.1.3" diff --git a/README.md b/README.md index bd6efc0..1f829ae 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,9 @@ [![License-Apache](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE-APACHE) [![docs.rs](https://img.shields.io/docsrs/qemu-fw-cfg)](https://docs.rs/qemu-fw-cfg) -A Rust library for reading fw_cfg from QEMU. +A Rust library for reading [fw_cfg] from QEMU. + +[fw_cfg]: https://www.qemu.org/docs/master/specs/fw_cfg.html ## Usage diff --git a/src/lib.rs b/src/lib.rs index b5f8a75..d9d5a54 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,6 @@ -//! A Rust library for reading fw_cfg from QEMU. +//! A Rust library for reading [fw_cfg] from QEMU. +//! +//! [fw_cfg]: https://www.qemu.org/docs/master/specs/fw_cfg.html //! //! # Supported architectures //! @@ -29,7 +31,11 @@ extern crate alloc; #[cfg(feature = "alloc")] use alloc::vec::Vec; +use core::cell::UnsafeCell; +use core::convert::TryFrom; +use core::fmt; use core::mem::size_of; +use core::sync::atomic::{compiler_fence, Ordering}; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] #[path = "x86.rs"] @@ -37,25 +43,55 @@ mod arch; mod selector_keys { pub const SIGNATURE: u16 = 0x0000; + pub const FEATURE_BITMAP: u16 = 0x0001; pub const DIR: u16 = 0x0019; } const SIGNATURE_DATA: &[u8] = b"QEMU"; +mod feature_bitmasks { + pub const _HAS_TRADITIONAL_INTERFACE: u32 = 1 << 0; + pub const HAS_DMA: u32 = 1 << 1; +} + /// An enum type for [`FwCfg`] errors. -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] #[non_exhaustive] pub enum FwCfgError { /// Invalid signature returned from QEMU fw_cfg I/O port InvalidSignature, } +/// An enum type for [`FwCfg::write_file`] errors. +#[derive(Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum FwCfgWriteError { + /// This fw_cfg device does not support DMA access, + /// which is necessary for writing since QEMU v2.4. + /// + /// Note: writing through the data register for older QEMU versions + /// is not supported by this crate. + DmaNotAvailable, + /// Something went wrong during a DMA write + DmaFailed, +} + /// A struct for accessing QEMU fw_cfg. #[derive(Debug)] -pub struct FwCfg(()); +pub struct FwCfg { + mode: Mode, + feature_bitmap: Option, +} + +#[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 +100,51 @@ 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, + feature_bitmap: None, + }; + 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 the "feature" configuration item, + /// reading it from the device if necessary and caching it. + fn feature_bitmap(&mut self) -> u32 { + self.feature_bitmap.unwrap_or_else(|| { + let mut buffer = [0u8; 4]; + self.select(selector_keys::FEATURE_BITMAP); + self.read(&mut buffer); + let value = u32::from_le_bytes(buffer); + self.feature_bitmap = Some(value); + value + }) } /// Return an iterator of all files in the fw_cfg directory @@ -162,15 +233,47 @@ impl FwCfg { buf } + /// Write provided `data` into a file, starting at file offset 0. + /// + /// This requires the DMA interface, which QEMU supports since version 2.9. + pub fn write_to_file(&mut self, file: &FwCfgFile, data: &[u8]) -> Result<(), FwCfgWriteError> { + let has_dma = (self.feature_bitmap() & feature_bitmasks::HAS_DMA) != 0; + if !has_dma { + return Err(FwCfgWriteError::DmaNotAvailable); + } + let control = (file.key() as u32) << 16 | FwCfgDmaAccess::WRITE | FwCfgDmaAccess::SELECT; + let access = FwCfgDmaAccess::new(control, data.as_ptr() as _, data.len()); + // `data` and `access` initialization must not be reordered to after this: + compiler_fence(Ordering::Release); + match &mut self.mode { + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + Mode::IOPort => unsafe { arch::start_dma(&access) }, + Mode::MemoryMapped(device) => device.start_dma(&access), + } + loop { + let control = access.read_control(); + if (control & FwCfgDmaAccess::ERROR) != 0 { + return Err(FwCfgWriteError::DmaFailed); + } + if control == 0 { + return Ok(()); + } + } + } + fn select(&mut self, key: u16) { - unsafe { - Self::write_selector(key); + match &mut self.mode { + #[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.mode { + #[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 +281,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 +328,99 @@ 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()]); + } + } + + fn start_dma(&self, access: &FwCfgDmaAccess) { + let address = access as *const FwCfgDmaAccess as u64; + // https://gitlab.com/qemu-project/qemu/-/blob/v7.0.0/docs/specs/fw_cfg.txt#L89 + let offset = 16; + let dma_address_register: *mut u32 = self.register(offset); + unsafe { + // https://gitlab.com/qemu-project/qemu/-/blob/v7.0.0/docs/specs/fw_cfg.txt#L167 + // The DMA address register is 64-bit and big-endian. + // Writing its lower half is what triggers DMA, + // so write these half separately to control their order: + let register_high = dma_address_register; + let register_low = dma_address_register.add(1); // One u32 + let address_high = (address >> 32) as u32; + let address_low = address as u32; + register_high.write_volatile(address_high.to_be()); + compiler_fence(Ordering::AcqRel); + register_low.write_volatile(address_low.to_be()); + } + } +} + +#[derive(Debug)] +// 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#L177-181 +#[repr(C)] +struct FwCfgDmaAccess { + control_be: UnsafeCell, + length_be: u32, + address_be: u64, +} + +impl FwCfgDmaAccess { + const ERROR: u32 = 1 << 0; + const _READ: u32 = 1 << 1; + const _SKIP: u32 = 1 << 2; + const SELECT: u32 = 1 << 3; + const WRITE: u32 = 1 << 4; + + fn new(control: u32, ptr: *mut (), length: usize) -> Self { + Self { + control_be: UnsafeCell::new(control.to_be()), + length_be: u32::try_from(length).unwrap().to_be(), + address_be: u64::try_from(ptr as usize).unwrap().to_be(), + } + } + + fn read_control(&self) -> u32 { + u32::from_be(unsafe { self.control_be.get().read_volatile() }) + } +} diff --git a/src/x86.rs b/src/x86.rs index d3af3f9..5b8ee89 100644 --- a/src/x86.rs +++ b/src/x86.rs @@ -1,8 +1,9 @@ -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; +const IO_PORT_DMA_ADDRESS: u16 = 0x514; unsafe fn in_u8(address: u16) -> u8 { let ret: u8; @@ -22,14 +23,34 @@ unsafe fn out_u16(address: u16, data: u16) { ); } -impl FwCfg { - pub(crate) unsafe fn write_selector(key: u16) { - out_u16(IO_PORT_SELECTOR, key); - } +unsafe fn out_u32(address: u16, data: u32) { + asm!( + "out dx, eax", + in("dx") address, + in("eax") data, + ); +} - pub(crate) unsafe fn read_data(buffer: &mut [u8]) { - for i in buffer { - *i = in_u8(IO_PORT_DATA); - } +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 start_dma(access: &crate::FwCfgDmaAccess) { + let address = access as *const crate::FwCfgDmaAccess as u64; + // https://gitlab.com/qemu-project/qemu/-/blob/v7.0.0/docs/specs/fw_cfg.txt#L167 + // The DMA address register is 64-bit and big-endian, + // but I/O ports only support 32-bit writes. + let port_high = IO_PORT_DMA_ADDRESS; + let port_low = IO_PORT_DMA_ADDRESS + 4; + let address_high = (address >> 32) as u32; + let address_low = address as u32; + out_u32(port_high, address_high.to_be()); + // Write the lower bits last as this is what triggers DMA, do it last + out_u32(port_low, address_low.to_be()); +} diff --git a/tests/main.rs b/tests/main.rs index eeaf772..addd6c6 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,10 @@ 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); + + // This file is not writeable + let result = fw_cfg.write_to_file(&file_input_txt, b" "); + assert_eq!(result, Err(qemu_fw_cfg::FwCfgWriteError::DmaFailed)); + + writeln!(shared::Writer, "✅ Test sucessful").unwrap(); } diff --git a/tests/runner.sh b/tests/runner.sh old mode 100644 new mode 100755 index 33d89fa..b8b9b6d --- a/tests/runner.sh +++ b/tests/runner.sh @@ -1,21 +1,33 @@ -#!/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 +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" - status=$(($? >> 1)) +if [[ "$ARCH" == i686 ]]; then + qemu-system-i386 $QEMU_OPTS \ + -device isa-debug-exit \ + -kernel "$@" +elif [[ "$ARCH" == riscv32* ]]; then + if [[ -z "$GDB" ]]; then + qemu-system-riscv32 $QEMU_OPTS \ + -machine virt \ + -bios none \ + -kernel "$@" + else + riscv32-elf-gdb -ex 'target remote :1234' "$@" + fi +else + echo Unsupported TARGET=$TARGET fi +status=$(($? >> 1)) + if [ $status -gt 0 ]; then exit $status fi diff --git a/tests/shared/i686/boot.asm b/tests/shared/i686/boot.asm index f47a57f..5a4586e 100644 --- a/tests/shared/i686/boot.asm +++ b/tests/shared/i686/boot.asm @@ -21,5 +21,5 @@ _start: .bss .align 4096 -.skip 16 * 1024 +.skip 128 * 1024 stack_top: 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() +}