Skip to content

Commit

Permalink
Barebones EFI implementation for mach-virt/arm64
Browse files Browse the repository at this point in the history
This is a minimal implementation in Rust of those parts of the EFI spec
that are actually needed to boot a arm64 Linux kernel straight from QEMU's
mach-virt.

It uses the efiloader crate which implements the bare minimum of EFI's
boot and runtime services that are needed to boot a Linux kernel, UKI
image or GRUB/shim loader stage.

It is intended to be minimal and secure:
- file data is never copied around - the kernel image and initrd are
  DMA'd into their final location using QEMU's fwcfg device,
- the MMU is enabled straight out of reset, and all memory accesses and
  code execution occurs with the appropriate memory and permission
  attributes,
- written in Rust,
- only a reduced subset of boot and runtime services is implemented,
- ACPI and SMBIOS tables are obtained from QEMU fwcfg directly and not
  modified,

How to use:

- build using 'cargo build'
  (ELF file ends up in target/aarch64-unknown-none/.../efilite)

- use 'objcopy -O binary <in> <out>' to convert the ELF file into a raw binary

- run using something like the below

$ qemu-system-aarch64 \
    -M virt -cpu max -smp 4 \
    -net none -nographic -m 900m -bios efilite.bin -kernel path/to/Image \
    -drive if=virtio,file=path/to/hda.xxx,format=xxx -initrd <initrd>
  • Loading branch information
ardbiesheuvel committed Nov 8, 2023
1 parent f624ec4 commit 4b72a3d
Show file tree
Hide file tree
Showing 16 changed files with 1,914 additions and 2 deletions.
12 changes: 12 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[target.aarch64-unknown-none]
rustflags = [
"-C", "relocation-model=static",
"-C", "linker-flavor=ld.lld",
"-C", "link-arg=-Tsrc/efilite.lds",
"-C", "link-arg=--orphan-handling=error",
"-C", "target-feature=+rand,+bti",
# "-Z", "branch-protection=bti"
]

[build]
target = "aarch64-unknown-none"
20 changes: 20 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: Rust

on:
push:
branches: [ "mach-virt" ]

env:
CARGO_TERM_COLOR: always

jobs:
build:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- run: rustup target add aarch64-unknown-none
- run: sudo apt-get update && sudo apt-get install -y lld
- name: Build
run: cargo build --verbose
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
/target
*.bin
.*.swp
146 changes: 146 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,18 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
linked_list_allocator = "0.10.5"
log = { version = "0.4.14", features = ["max_level_debug", "release_max_level_warn"] }
mmio = "2.1.0"
fdt = "0.1.5"
spinning_top = "0.2.5"
once_cell = { version = "1.18.0", default-features = false, features = ["alloc"] }
efiloader = "0.0.1"
aarch64-paging = "0.5.0"

[profile.dev]
panic = "abort"

[profile.release]
panic = "abort"
#lto = true # breaks BTI
84 changes: 84 additions & 0 deletions src/console.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// SPDX-License-Identifier: GPL-2.0
// Copyright 2022-2023 Google LLC
// Author: Ard Biesheuvel <[email protected]>

use core::fmt::Write;
use core::mem::MaybeUninit;
use core::ops::Range;
use fdt::node::FdtNode;
use log::{Metadata, Record};
use mmio::{Allow, Deny, VolBox};
use spinning_top::Spinlock;

pub struct DumbSerialConsole {
pub base: usize,
out: Spinlock<VolBox<u32, Deny, Allow>>,
}

struct DumbSerialConsoleWriter<'a> {
console: &'a DumbSerialConsole,
}

pub fn init(base: &Range<usize>) -> &'static DumbSerialConsole {
// Statically allocated so we can init the console before the heap
static mut CON: MaybeUninit<DumbSerialConsole> = MaybeUninit::uninit();

unsafe {
let v = VolBox::<u32, Deny, Allow>::new(base.start as *mut u32);
CON.write(DumbSerialConsole {
base: base.start,
out: Spinlock::new(v),
})
}
}

pub fn init_from_fdt_node(node: FdtNode) -> Option<&'static DumbSerialConsole> {
let reg = node.reg()?.nth(0)?;
let base = reg.starting_address as usize;
let size = reg.size?;
Some(init(&(base..base + size)))
}

impl DumbSerialConsole {
fn puts(&self, s: &str) {
let mut out = self.out.lock();

for b in s.as_bytes().iter() {
if *b == b'\n' {
out.write(b'\r' as u32);
}
out.write(*b as u32)
}
}
}

impl efiloader::SimpleConsole for DumbSerialConsole {
fn write_string(&self, s: &str) {
self.puts(s)
}

fn read_byte(&self) -> Option<u8> {
None
}
}

impl Write for DumbSerialConsoleWriter<'_> {
fn write_str(&mut self, s: &str) -> core::fmt::Result {
Ok(self.console.puts(s))
}
}

impl log::Log for DumbSerialConsole {
fn enabled(&self, metadata: &Metadata) -> bool {
metadata.level() <= log::max_level()
}

fn log(&self, record: &Record) {
if self.enabled(record.metadata()) {
let mut out = DumbSerialConsoleWriter { console: &self };
write!(&mut out, "efilite {} - {}", record.level(), record.args()).ok();
}
}

fn flush(&self) {}
}
109 changes: 109 additions & 0 deletions src/efilite.lds
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright 2022 Google LLC
* Author: Ard Biesheuvel <[email protected]>
*/

MEMORY
{
flash : ORIGIN = 0, LENGTH = 2M
ram : ORIGIN = 0x40000000, LENGTH = 4M
}

PROVIDE(_init_base = 0x40000000);
PROVIDE(_init_size = 0x400000);

PROVIDE(rust_eh_personality = 0x0);

ENTRY(_entry)

SECTIONS
{
.text : {
_entry = .;
KEEP(*(.text.entry))
*(.rodata.idmap)

/*
* Omit the first 64k flash region from the runtime mapping
* so that the NOR flash control registers are not mapped
* by the OS
*/
. = ALIGN(0x10000);
RTSCODE = .;
_rtcode_start = .;
*(SORT_BY_ALIGNMENT(.text*))
*(SORT_BY_ALIGNMENT(.rodata*))
*(.got .got.plt)
*(.igot .iplt .igot.plt)
*(.gcc_except_table.*)
. = ALIGN(0x10000);
_rtcode_end = .;
} >flash

/*
* QEMU passes the DT blob by storing it at the base of DRAM
* before starting the guest
*/
.dtb (NOLOAD) : {
_bsdata_start = .;
_dtb_start = .;
. += 0x200000;
_dtb_end = .;
} >ram

/*
* put the stack first so we will notice if we overrun and
* hit the R/O mapping of the DT blob
*/
.stack (NOLOAD) : {
. += 0x20000;
_stack_end = .;
} >ram

.data : ALIGN(32) {
_data = .;
*(SORT_BY_ALIGNMENT(.data*))
. = ALIGN(32);
_edata = .;
} >ram AT >flash

data_lma = LOADADDR(.data);

.bss : ALIGN (32) {
_bss_start = .;
*(SORT_BY_ALIGNMENT(.bss*))
. = ALIGN(32);
_bss_end = .;
_end = .;
} >ram

.rtdata _init_base + _init_size - 0x10000 (NOLOAD) : {
_bsdata_end = .;
_rtdata_start = .;
_avail = ABSOLUTE(.) - _end;
*(.rtdata .rtdata*)
. = ALIGN(0x10000);
_rtdata_end = .;
} >ram

.debug_abbrev 0 : { *(.debug_abbrev) }
.debug_info 0 : { *(.debug_info) }
.debug_aranges 0 : { *(.debug_aranges) }
.debug_ranges 0 : { *(.debug_ranges) }
.debug_str 0 : { *(.debug_str) }
.debug_pubnames 0 : { *(.debug_pubnames) }
.debug_pubtypes 0 : { *(.debug_pubtypes) }
.debug_line 0 : { *(.debug_line) }
.debug_frame 0 : { *(.debug_frame) }
.debug_loc 0 : { *(.debug_loc) }

.symtab 0 : { *(.symtab) }
.strtab 0 : { *(.strtab) }
.shstrtab 0 : { *(.shstrtab) }

/DISCARD/ : {
*(.note* .comment* .rela.* .eh_frame_hdr .eh_frame .interp)
}
}
ASSERT(SIZEOF(.rtsdata) != 0x10000, ".rtsdata section too big")
Loading

0 comments on commit 4b72a3d

Please sign in to comment.