1# vmbase 2 3This directory contains a Rust crate and static library which can be used to write `no_std` Rust 4binaries to run in an aarch64 VM under crosvm (via the VirtualizationService), such as for pVM 5firmware, a VM bootloader or kernel. 6 7In particular it provides: 8 9- An [entry point](entry.S) that initializes the MMU with a hard-coded identity mapping, enables the 10 cache, prepares the image and allocates a stack. 11- An [exception vector](exceptions.S) to call your exception handlers. 12- A UART driver and `println!` macro for early console logging. 13- Functions to shutdown or reboot the VM. 14 15Libraries are also available for heap allocation, page table manipulation and PSCI calls. 16 17## Usage 18 19The [example](example/) subdirectory contains an example of how to use it for a VM bootloader. 20 21### Build file 22 23Start by creating a `rust_ffi_static` rule containing your main module: 24 25```soong 26rust_ffi_static { 27 name: "libvmbase_example", 28 defaults: ["vmbase_ffi_defaults"], 29 crate_name: "vmbase_example", 30 srcs: ["src/main.rs"], 31 rustlibs: [ 32 "libvmbase", 33 ], 34} 35``` 36 37`vmbase_ffi_defaults`, among other things, specifies the stdlibs including the `compiler_builtins` 38and `core` crate. These must be explicitly specified as we don't want the normal set of libraries 39used for a C++ binary intended to run in Android userspace. 40 41### Entry point 42 43Your main module needs to specify a couple of special attributes: 44 45```rust 46#![no_main] 47#![no_std] 48``` 49 50This tells rustc that it doesn't depend on `std`, and won't have the usual `main` function as an 51entry point. Instead, `vmbase` provides a macro to specify your main function: 52 53```rust 54use vmbase::{logger, main}; 55use log::{info, LevelFilter}; 56 57main!(main); 58 59pub fn main(arg0: u64, arg1: u64, arg2: u64, arg3: u64) { 60 logger::init(LevelFilter::Info).unwrap(); 61 info!("Hello world"); 62} 63``` 64 65vmbase adds a wrapper around your main function to initialize the console driver first (with the 66UART at base address `0x3f8`, the first UART allocated by crosvm), and make a PSCI `SYSTEM_OFF` call 67to shutdown the VM if your main function ever returns. 68 69You can also shutdown the VM by calling `vmbase::power::shutdown` or 'reboot' by calling 70`vmbase::power::reboot`. Either will cause crosvm to terminate the VM, but by convention we use 71shutdown to indicate that the VM has finished cleanly, and reboot to indicate an error condition. 72 73### Exception handlers 74 75You must provide handlers for each of the 8 types of exceptions which can occur on aarch64. These 76must use the C ABI, and have the expected names. For example, to log sync exceptions and reboot: 77 78```rust 79use vmbase::{console::emergency_write_str, power::reboot}; 80 81extern "C" fn sync_exception_current() { 82 emergency_write_str("sync_exception_current\n"); 83 84 let mut esr: u64; 85 unsafe { 86 asm!("mrs {esr}, esr_el1", esr = out(reg) esr); 87 } 88 eprintln!("esr={:#08x}", esr); 89 90 reboot(); 91} 92``` 93 94The `println!` macro shouldn't be used in exception handlers, because it relies on a global instance 95of the UART driver which might be locked when the exception happens, which would result in deadlock. 96Instead you can use `emergency_write_str` and `eprintln!`, which will re-initialize the UART every 97time to ensure that it can be used. This should still be used with care, as it may interfere with 98whatever the rest of the program is doing with the UART. 99 100Note also that in some cases when the system is in a bad state resulting in the stack not working 101properly, `eprintln!` may hang. `emergency_write_str` may be more reliable as it seems to avoid 102any stack allocation. This is why the example above uses `emergency_write_str` first to ensure that 103at least something is logged, before trying `eprintln!` to print more details. 104 105See [example/src/exceptions.rs](examples/src/exceptions.rs) for a complete example. 106 107### Linker script and initial idmap 108 109The [entry point](entry.S) code expects to be provided a hardcoded identity-mapped page table to use 110initially. This must contain at least the region where the image itself is loaded, some writable 111DRAM to use for the `.bss` and `.data` sections and stack, and a device mapping for the UART MMIO 112region. See the [example/idmap.S](example/idmap.S) for an example of how this can be constructed. 113 114The addresses in the pagetable must map the addresses the image is linked at, as we don't support 115relocation. This can be achieved with a linker script, like the one in 116[example/image.ld](example/image.ld). The key part is the regions provided to be used for the image 117and writable data: 118 119```ld 120MEMORY 121{ 122 image : ORIGIN = 0x80200000, LENGTH = 2M 123 writable_data : ORIGIN = 0x80400000, LENGTH = 2M 124} 125``` 126 127### Building a binary 128 129To link your Rust code together with the entry point code and idmap into a static binary, you need 130to use a `cc_binary` rule: 131 132```soong 133cc_binary { 134 name: "vmbase_example", 135 defaults: ["vmbase_elf_defaults"], 136 srcs: [ 137 "idmap.S", 138 ], 139 static_libs: [ 140 "libvmbase_example", 141 ], 142 linker_scripts: [ 143 "image.ld", 144 ":vmbase_sections", 145 ], 146} 147``` 148 149This takes your Rust library (`libvmbase_example`), the vmbase library entry point and exception 150vector (`libvmbase_entry`) and your initial idmap (`idmap.S`) and builds a static binary with your 151linker script (`image.ld`) and the one provided by vmbase ([`sections.ld`](sections.ld)). This is an 152ELF binary, but to run it as a VM bootloader you need to `objcopy` it to a raw binary image instead, 153which you can do with a `raw_binary` rule: 154 155```soong 156raw_binary { 157 name: "vmbase_example_bin", 158 stem: "vmbase_example.bin", 159 src: ":vmbase_example", 160 enabled: false, 161 target: { 162 android_arm64: { 163 enabled: true, 164 }, 165 }, 166} 167``` 168 169The resulting binary can then be used to start a VM by passing it as the bootloader in a 170`VirtualMachineRawConfig`. 171