From 0b9c5f166b0970d4c23ac518425c82919e995d33 Mon Sep 17 00:00:00 2001 From: Charlotte Fulham Date: Thu, 30 Jul 2026 14:41:32 +1000 Subject: [PATCH 1/2] Tool: Move kernel config out of main Moves the kernel configuration logic outside of main.rs into sel4.rs for reuse Signed-off-by: Charlotte Fulham --- tool/microkit/src/main.rs | 305 ++++++++------------------------------ tool/microkit/src/sel4.rs | 209 +++++++++++++++++++++++++- tool/microkit/src/util.rs | 11 ++ 3 files changed, 277 insertions(+), 248 deletions(-) diff --git a/tool/microkit/src/main.rs b/tool/microkit/src/main.rs index daaaefeb3..25539f390 100644 --- a/tool/microkit/src/main.rs +++ b/tool/microkit/src/main.rs @@ -8,32 +8,32 @@ #![allow(clippy::assertions_on_constants)] use microkit_tool::argparse; -use microkit_tool::argparse::{Args, ArgsError, RequestedImageType}; -use microkit_tool::capdl::allocation::{ - simulate_capdl_object_alloc_algorithm, CapDLAllocEmulationErrorLevel, +use microkit_tool::argparse::ArgsError; +use microkit_tool::sdf::parse; +use std::{ + collections::HashMap, + fs::{self, metadata}, + path::Path, }; -use microkit_tool::capdl::build_capdl_spec; -use microkit_tool::capdl::initialiser::CapDLInitialiser; -use microkit_tool::capdl::packaging::pack_spec_into_initial_task; -use microkit_tool::elf::ElfFile; -use microkit_tool::loader::Loader; -use microkit_tool::report::write_report; -use microkit_tool::sdf::{parse, SysMemoryRegion, SysMemoryRegionPaddr}; -use microkit_tool::sdk::{AvailableConfig, Sdk}; -use microkit_tool::sel4::{ - emulate_kernel_boot, emulate_kernel_boot_partial, AddressSpaceConstants, Arch, Config, - ObjectSizes, PlatformConfig, RiscvVirtualMemory, -}; -use microkit_tool::symbols::patch_symbols; -use microkit_tool::util::{ - get_full_path, human_size_strict, json_str, json_str_as_bool, json_str_as_u64, round_down, - round_up, + +use microkit_tool::{ + argparse::Args, + capdl::{ + allocation::{simulate_capdl_object_alloc_algorithm, CapDLAllocEmulationErrorLevel}, + build_capdl_spec, + initialiser::CapDLInitialiser, + packaging::pack_spec_into_initial_task, + }, + elf::ElfFile, + loader::Loader, + report::write_report, + sdf::{SysMemoryRegion, SysMemoryRegionPaddr}, + sdk::Sdk, + sel4::{emulate_kernel_boot, emulate_kernel_boot_partial, Arch, Config, ImageOutputType}, + symbols::patch_symbols, + util::{bail_if_not_exists, get_full_path, human_size_strict, round_down, round_up}, + viper, DisjointMemoryRegion, MemoryRegion, }; -use microkit_tool::viper; -use microkit_tool::{DisjointMemoryRegion, MemoryRegion}; -use std::collections::HashMap; -use std::fs::{self, metadata}; -use std::path::Path; const MAX_BUILD_ITERATION: usize = 3; @@ -45,67 +45,6 @@ const KERNEL_COPY_FILENAME: &str = "sel4.elf"; // also copy the 32-bit version that was prepared by build_sdk.py for convenience. const KERNEL32_COPY_FILENAME: &str = "sel4_32.elf"; -enum ImageOutputType { - Binary, - Elf, - Uimage, -} - -impl ImageOutputType { - fn default_from_arch_and_board(arch: &Arch, board_name: &str) -> Self { - match board_name { - "ariane" | "cheshire" | "serengeti" => ImageOutputType::Elf, - _ => match arch { - Arch::Aarch64 => ImageOutputType::Binary, - Arch::Riscv64 => ImageOutputType::Uimage, - Arch::X86_64 => ImageOutputType::Elf, - }, - } - } - - /// Resolve the optional user-specified image type with what is the default for the - /// platform. - /// Not all image types are supported for all platforms, so we check here. - fn resolve(requested: &RequestedImageType, arch: &Arch, board_name: &str) -> Option { - match requested { - RequestedImageType::Binary => match arch { - Arch::Aarch64 | Arch::Riscv64 => Some(Self::Binary), - Arch::X86_64 => None, - }, - RequestedImageType::Elf => Some(Self::Elf), - RequestedImageType::Uimage => match arch { - Arch::Riscv64 => Some(Self::Uimage), - Arch::X86_64 | Arch::Aarch64 => None, - }, - RequestedImageType::Unspecified => { - Some(Self::default_from_arch_and_board(arch, board_name)) - } - } - } -} - -fn bail_if_not_exists(description: &'static str, path: &Path) -> Result<(), String> { - if !path.exists() { - eprintln!( - "microkit: error: {description} '{}' does not exist", - path.display() - ); - std::process::exit(1); - } - Ok(()) -} - -fn parse_json_file( - file_name: &str, - file_description: &'static str, - current_config: &AvailableConfig, -) -> Result { - let path = current_config.config_dir.join(file_name); - bail_if_not_exists(file_description, &path)?; - serde_json::from_str(&fs::read_to_string(&path).expect("Error: Unable to read {path}")) - .map_err(|err| format!("Error: Unable to parse {file_name}: {err}")) -} - fn main() -> Result<(), String> { let sdk = match Sdk::discover() { Ok(discovered_info) => discovered_info, @@ -134,165 +73,43 @@ fn main() -> Result<(), String> { std::process::exit(1); } }; - args.search_paths.push(sdk.cwd.clone()); - // NB safe unwrap: argparse would already have bailed if the config did not - // exist. - let current_config = sdk.select(&args.board, &args.config).unwrap(); + args.search_paths.push(sdk.cwd.clone()); - // the real work begins here - let elf_path = current_config.config_dir.join("elf"); - let loader_elf_path = elf_path.join("loader.elf"); - let kernel_elf_path = match args.override_kernel { - Some(ref path) => path, - None => &elf_path.join("sel4.elf"), - }; - let monitor_elf_path = elf_path.join("monitor.elf"); - let capdl_init_elf_path = elf_path.join("initialiser.elf"); - let kernel_config_path = current_config - .config_dir - .join("include/kernel/gen_config.json"); - let invocations_all_path = current_config.config_dir.join("invocations_all.json"); - bail_if_not_exists("board ELF directory", &elf_path)?; - bail_if_not_exists("kernel ELF", kernel_elf_path)?; - bail_if_not_exists("monitor ELF", &monitor_elf_path)?; - bail_if_not_exists("CapDL initialiser ELF", &capdl_init_elf_path)?; - bail_if_not_exists("kernel configuration file", &kernel_config_path)?; - bail_if_not_exists("invocations JSON file", &invocations_all_path)?; + let kernel_config = Config::build_kernel_config(&args, &sdk).unwrap(); let system_path = &args.sdf_path; bail_if_not_exists("system description file", system_path)?; let xml: String = fs::read_to_string(system_path).unwrap(); - let kernel_config_json: serde_json::Value = - serde_json::from_str(&fs::read_to_string(kernel_config_path).unwrap()).unwrap(); - - let invocations_labels: serde_json::Value = - serde_json::from_str(&fs::read_to_string(invocations_all_path).unwrap()).unwrap(); - - let arch = match json_str(&kernel_config_json, "SEL4_ARCH")? { - "aarch64" => Arch::Aarch64, - "riscv64" => Arch::Riscv64, - "x86_64" => Arch::X86_64, - _ => panic!("Unsupported kernel config architecture"), - }; - - let image_output_type = match ImageOutputType::resolve( - &args.requested_image_type, - &arch, - args.board.as_str(), + let mut system = match parse( + system_path.as_path(), + &xml, + &kernel_config, + &args.search_paths, ) { - Some(image) => image, - None => { - eprintln!( - "microkit: error: building the output image as '{0}' is unsupported for target architecture '{arch}'", - args.requested_image_type - ); + Ok(system) => system, + Err(err) => { + eprintln!("{err}"); std::process::exit(1); } }; - let (device_regions, normal_regions) = match arch { - Arch::X86_64 => (None, None), - _ => { - let platform_gen_path = current_config.config_dir.join("platform_gen.json"); - bail_if_not_exists("kernel platform configuration file", &platform_gen_path)?; - let kernel_platform_config: PlatformConfig = - serde_json::from_str(&fs::read_to_string(platform_gen_path).unwrap()).unwrap(); - - ( - Some(kernel_platform_config.devices), - Some(kernel_platform_config.memory), - ) - } - }; - - let object_sizes: ObjectSizes = parse_json_file( - "object_sizes.json", - "kernel object sizes file", - current_config, - )?; - - let address_space_constants: AddressSpaceConstants = parse_json_file( - "address_space_constants.json", - "kernel address space constants file", - current_config, - )?; - - let hypervisor = match arch { - Arch::Aarch64 => json_str_as_bool(&kernel_config_json, "ARM_HYPERVISOR_SUPPORT")?, - Arch::X86_64 => json_str_as_bool(&kernel_config_json, "VTX")?, - // Hypervisor mode is not available on RISC-V - _ => false, - }; - - let iommu = match arch { - Arch::X86_64 => json_str_as_bool(&kernel_config_json, "IOMMU")?, - _ => false, - }; - - let arm_pa_size_bits = match arch { - Arch::Aarch64 => { - if json_str_as_bool(&kernel_config_json, "ARM_PA_SIZE_BITS_40")? { - Some(40) - } else if json_str_as_bool(&kernel_config_json, "ARM_PA_SIZE_BITS_44")? { - Some(44) - } else { - panic!("Expected ARM platform to have 40 or 44 physical address bits") - } - } - Arch::X86_64 | Arch::Riscv64 => None, - }; - - let arm_smc = match arch { - Arch::Aarch64 => Some(json_str_as_bool(&kernel_config_json, "ALLOW_SMC_CALLS")?), - _ => None, - }; - - let kernel_frame_size = match arch { - Arch::Aarch64 => 1 << 12, - Arch::Riscv64 => 1 << 21, - Arch::X86_64 => 1 << 12, + let current_config = sdk.select(&args.board, &args.config).unwrap(); + let elf_path = current_config.config_dir.join("elf"); + let loader_elf_path = elf_path.join("loader.elf"); + let kernel_elf_path = match args.override_kernel { + Some(ref path) => path, + None => &elf_path.join("sel4.elf"), }; + let monitor_elf_path = elf_path.join("monitor.elf"); + let capdl_init_elf_path = elf_path.join("initialiser.elf"); - let kernel_config = Config { - arch, - word_size: json_str_as_u64(&kernel_config_json, "WORD_SIZE")?, - minimum_page_size: 1 << object_sizes.small_page, - paddr_user_device_top: json_str_as_u64(&kernel_config_json, "PADDR_USER_DEVICE_TOP")?, - kernel_frame_size, - init_cnode_bits: json_str_as_u64(&kernel_config_json, "ROOT_CNODE_SIZE_BITS")?, - cap_address_bits: 64, - fan_out_limit: json_str_as_u64(&kernel_config_json, "RETYPE_FAN_OUT_LIMIT")?, - max_num_bootinfo_untypeds: json_str_as_u64( - &kernel_config_json, - "MAX_NUM_BOOTINFO_UNTYPED_CAPS", - )?, - hypervisor, - iommu, - benchmark: args.config == "benchmark", - num_cores: if json_str_as_bool(&kernel_config_json, "ENABLE_SMP_SUPPORT")? { - json_str_as_u64(&kernel_config_json, "MAX_NUM_NODES")? - .try_into() - .expect("number of cores fits in u8") - } else { - 1 - }, - num_domains: json_str_as_u64(&kernel_config_json, "NUM_DOMAINS")? - .try_into() - .unwrap(), - num_domain_schedules: json_str_as_u64(&kernel_config_json, "NUM_DOMAIN_SCHEDULES")?, - fpu: json_str_as_bool(&kernel_config_json, "HAVE_FPU")?, - arm_pa_size_bits, - arm_smc, - riscv_pt_levels: Some(RiscvVirtualMemory::Sv39), - invocations_labels, - device_regions, - normal_regions, - object_sizes, - address_space_constants, - }; + bail_if_not_exists("board ELF directory", &elf_path)?; + bail_if_not_exists("kernel ELF", kernel_elf_path)?; + bail_if_not_exists("monitor ELF", &monitor_elf_path)?; + bail_if_not_exists("CapDL initialiser ELF", &capdl_init_elf_path)?; if kernel_config.arch != Arch::X86_64 && !loader_elf_path.exists() { eprintln!( @@ -302,20 +119,17 @@ fn main() -> Result<(), String> { std::process::exit(1); } - assert!( - kernel_config.word_size == 64, - "Microkit tool has various assumptions about the word size being 64-bits." - ); - - let mut system = match parse( - system_path.as_path(), - &xml, - &kernel_config, - &args.search_paths, + let image_output_type = match ImageOutputType::resolve( + &args.requested_image_type, + &kernel_config.arch, + args.board.as_str(), ) { - Ok(system) => system, - Err(err) => { - eprintln!("{err}"); + Some(image) => image, + None => { + eprintln!( + "microkit: error: building the output image as '{0}' is unsupported for target architecture '{1}'", + args.requested_image_type, kernel_config.arch + ); std::process::exit(1); } }; @@ -718,14 +532,14 @@ fn main() -> Result<(), String> { } }; - if let Some(capdl_json) = args.capdl_json_path { + if let Some(capdl_json) = &args.capdl_json_path { let serialised = serde_json::to_string_pretty(&spec_container.spec).unwrap(); fs::write(capdl_json, &serialised).unwrap(); }; - if let Some(viper_output_dir) = args.viper_output_dir { + if let Some(viper_output_dir) = &args.viper_output_dir { // NB returns Ok if the directory already exists, that's fine - fs::create_dir_all(&viper_output_dir).unwrap_or_else(|source| { + fs::create_dir_all(viper_output_dir).unwrap_or_else(|source| { eprintln!( "ERROR: cannot write Viper output directory {}: {source}", &viper_output_dir.display() @@ -764,6 +578,5 @@ fn main() -> Result<(), String> { // Only reachable when there are setvar region_paddr that we keep selecting the wrong address. panic!("ERROR: fatal, failed to build system in {iteration} iterations"); } - Ok(()) } diff --git a/tool/microkit/src/sel4.rs b/tool/microkit/src/sel4.rs index d95084a74..aba618d19 100644 --- a/tool/microkit/src/sel4.rs +++ b/tool/microkit/src/sel4.rs @@ -3,11 +3,17 @@ // // SPDX-License-Identifier: BSD-2-Clause // -use std::{cmp::max, fmt::Display}; +use std::{cmp::max, fmt::Display, fs}; use serde::Deserialize; -use crate::{elf::ElfFile, util, DisjointMemoryRegion, MemoryRegion, UntypedObject}; +use crate::{ + argparse::{Args, RequestedImageType}, + elf::ElfFile, + sdk::{AvailableConfig, Sdk}, + util::{self, bail_if_not_exists, json_str, json_str_as_bool, json_str_as_u64}, + DisjointMemoryRegion, MemoryRegion, UntypedObject, +}; pub struct KernelPartialBootInfo { device_memory: DisjointMemoryRegion, @@ -25,6 +31,56 @@ pub struct BootInfo { pub first_available_cap: u64, } +pub enum ImageOutputType { + Binary, + Elf, + Uimage, +} + +impl ImageOutputType { + fn default_from_arch_and_board(arch: &Arch, board_name: &str) -> Self { + match board_name { + "ariane" | "cheshire" | "serengeti" => ImageOutputType::Elf, + _ => match arch { + Arch::Aarch64 => ImageOutputType::Binary, + Arch::Riscv64 => ImageOutputType::Uimage, + Arch::X86_64 => ImageOutputType::Elf, + }, + } + } + + /// Resolve the optional user-specified image type with what is the default for the + /// platform. + /// Not all image types are supported for all platforms, so we check here. + pub fn resolve(requested: &RequestedImageType, arch: &Arch, board_name: &str) -> Option { + match requested { + RequestedImageType::Binary => match arch { + Arch::Aarch64 | Arch::Riscv64 => Some(Self::Binary), + Arch::X86_64 => None, + }, + RequestedImageType::Elf => Some(Self::Elf), + RequestedImageType::Uimage => match arch { + Arch::Riscv64 => Some(Self::Uimage), + Arch::X86_64 | Arch::Aarch64 => None, + }, + RequestedImageType::Unspecified => { + Some(Self::default_from_arch_and_board(arch, board_name)) + } + } + } +} + +fn parse_json_file( + file_name: &str, + file_description: &'static str, + current_config: &AvailableConfig, +) -> Result { + let path = current_config.config_dir.join(file_name); + bail_if_not_exists(file_description, &path)?; + serde_json::from_str(&fs::read_to_string(&path).expect("Error: Unable to read {path}")) + .map_err(|err| format!("Error: Unable to parse {file_name}: {err}")) +} + fn kernel_self_mem(kernel_elf: &ElfFile) -> MemoryRegion { let segments = kernel_elf.loadable_segments(); let base = segments[0].phys_addr; @@ -330,6 +386,155 @@ pub struct Config { } impl Config { + pub fn build_kernel_config(args: &Args, sdk: &Sdk) -> Result { + // NB safe unwrap: argparse would already have bailed if the config did not + // exist. + let current_config = sdk.select(&args.board, &args.config).unwrap(); + + // the real work begins here + let kernel_config_path = current_config + .config_dir + .join("include/kernel/gen_config.json"); + let invocations_all_path = current_config.config_dir.join("invocations_all.json"); + if !kernel_config_path.exists() { + return Err(format!( + "microkit: error: kernel configuration file '{}' does not exist", + kernel_config_path.display() + )); + } + if !invocations_all_path.exists() { + return Err(format!( + "microkit: error: invocations JSON file '{}' does not exist", + invocations_all_path.display() + )); + } + + let kernel_config_json: serde_json::Value = + serde_json::from_str(&fs::read_to_string(kernel_config_path).unwrap()).unwrap(); + + let invocations_labels: serde_json::Value = + serde_json::from_str(&fs::read_to_string(invocations_all_path).unwrap()).unwrap(); + + let arch = match json_str(&kernel_config_json, "SEL4_ARCH")? { + "aarch64" => Arch::Aarch64, + "riscv64" => Arch::Riscv64, + "x86_64" => Arch::X86_64, + _ => panic!("Unsupported kernel config architecture"), + }; + + let (device_regions, normal_regions) = match arch { + Arch::X86_64 => (None, None), + _ => { + let platform_gen_path = current_config.config_dir.join("platform_gen.json"); + if !platform_gen_path.exists() { + return Err(format!( + "microkit: error: kernel platform configuration file '{}' does not exist", + platform_gen_path.display() + )); + } + let kernel_platform_config: PlatformConfig = + serde_json::from_str(&fs::read_to_string(platform_gen_path).unwrap()).unwrap(); + + ( + Some(kernel_platform_config.devices), + Some(kernel_platform_config.memory), + ) + } + }; + + let object_sizes: ObjectSizes = parse_json_file( + "object_sizes.json", + "kernel object sizes file", + current_config, + )?; + + let address_space_constants: AddressSpaceConstants = parse_json_file( + "address_space_constants.json", + "kernel address space constants file", + current_config, + )?; + + let hypervisor = match arch { + Arch::Aarch64 => json_str_as_bool(&kernel_config_json, "ARM_HYPERVISOR_SUPPORT")?, + Arch::X86_64 => json_str_as_bool(&kernel_config_json, "VTX")?, + // Hypervisor mode is not available on RISC-V + _ => false, + }; + + let iommu = match arch { + Arch::X86_64 => json_str_as_bool(&kernel_config_json, "IOMMU")?, + _ => false, + }; + + let arm_pa_size_bits = match arch { + Arch::Aarch64 => { + if json_str_as_bool(&kernel_config_json, "ARM_PA_SIZE_BITS_40")? { + Some(40) + } else if json_str_as_bool(&kernel_config_json, "ARM_PA_SIZE_BITS_44")? { + Some(44) + } else { + panic!("Expected ARM platform to have 40 or 44 physical address bits") + } + } + Arch::X86_64 | Arch::Riscv64 => None, + }; + + let arm_smc = match arch { + Arch::Aarch64 => Some(json_str_as_bool(&kernel_config_json, "ALLOW_SMC_CALLS")?), + _ => None, + }; + + let kernel_frame_size = match arch { + Arch::Aarch64 => 1 << 12, + Arch::Riscv64 => 1 << 21, + Arch::X86_64 => 1 << 12, + }; + + let kernel_config = Config { + arch, + word_size: json_str_as_u64(&kernel_config_json, "WORD_SIZE")?, + minimum_page_size: 1 << object_sizes.small_page, + paddr_user_device_top: json_str_as_u64(&kernel_config_json, "PADDR_USER_DEVICE_TOP")?, + kernel_frame_size, + init_cnode_bits: json_str_as_u64(&kernel_config_json, "ROOT_CNODE_SIZE_BITS")?, + cap_address_bits: 64, + fan_out_limit: json_str_as_u64(&kernel_config_json, "RETYPE_FAN_OUT_LIMIT")?, + max_num_bootinfo_untypeds: json_str_as_u64( + &kernel_config_json, + "MAX_NUM_BOOTINFO_UNTYPED_CAPS", + )?, + hypervisor, + iommu, + benchmark: args.config == "benchmark", + num_cores: if json_str_as_bool(&kernel_config_json, "ENABLE_SMP_SUPPORT")? { + json_str_as_u64(&kernel_config_json, "MAX_NUM_NODES")? + .try_into() + .expect("number of cores fits in u8") + } else { + 1 + }, + num_domains: json_str_as_u64(&kernel_config_json, "NUM_DOMAINS")? + .try_into() + .unwrap(), + num_domain_schedules: json_str_as_u64(&kernel_config_json, "NUM_DOMAIN_SCHEDULES")?, + fpu: json_str_as_bool(&kernel_config_json, "HAVE_FPU")?, + arm_pa_size_bits, + arm_smc, + riscv_pt_levels: Some(RiscvVirtualMemory::Sv39), + invocations_labels, + device_regions, + normal_regions, + object_sizes, + address_space_constants, + }; + + assert!( + kernel_config.word_size == 64, + "Microkit tool has various assumptions about the word size being 64-bits." + ); + Ok(kernel_config) + } + /// Refers to the 'PPTR_BASE' define in kernel source pub fn virtual_base(&self) -> u64 { match self.arch { diff --git a/tool/microkit/src/util.rs b/tool/microkit/src/util.rs index 6f2c0e275..dc832e166 100644 --- a/tool/microkit/src/util.rs +++ b/tool/microkit/src/util.rs @@ -214,6 +214,17 @@ pub fn get_full_path(path: &Path, search_paths: &Vec) -> Option Result<(), String> { + if !path.exists() { + eprintln!( + "microkit: error: {description} '{}' does not exist", + path.display() + ); + std::process::exit(1); + } + Ok(()) +} + #[cfg(test)] mod tests { // Note this useful idiom: importing names from outer (for mod tests) scope. From cc75dbbda3d3d1026778a7997f8e589c2ed12afb Mon Sep 17 00:00:00 2001 From: Charlotte Fulham Date: Thu, 30 Jul 2026 14:45:02 +1000 Subject: [PATCH 2/2] Tool: Move image generation out of main Creates a new `build.rs` file for system image generation related functions, and corresponding `build` function that is now invoked in `main` and can be used elsewhere. Signed-off-by: Charlotte Fulham --- tool/microkit/src/build.rs | 531 +++++++++++++++++++++++++++++++++++++ tool/microkit/src/lib.rs | 1 + tool/microkit/src/main.rs | 525 +----------------------------------- 3 files changed, 539 insertions(+), 518 deletions(-) create mode 100644 tool/microkit/src/build.rs diff --git a/tool/microkit/src/build.rs b/tool/microkit/src/build.rs new file mode 100644 index 000000000..f2e6e3112 --- /dev/null +++ b/tool/microkit/src/build.rs @@ -0,0 +1,531 @@ +// +// Copyright 2025, UNSW +// +// SPDX-License-Identifier: BSD-2-Clause +// + +use std::{ + collections::HashMap, + fs::{self, metadata}, + path::Path, +}; + +use crate::{ + argparse::Args, + capdl::{ + allocation::{simulate_capdl_object_alloc_algorithm, CapDLAllocEmulationErrorLevel}, + build_capdl_spec, + initialiser::CapDLInitialiser, + packaging::pack_spec_into_initial_task, + }, + elf::ElfFile, + loader::Loader, + report::write_report, + sdf::{SysMemoryRegion, SysMemoryRegionPaddr, SystemDescription}, + sdk::Sdk, + sel4::{emulate_kernel_boot, emulate_kernel_boot_partial, Arch, Config, ImageOutputType}, + symbols::patch_symbols, + util::{bail_if_not_exists, get_full_path, human_size_strict, round_down, round_up}, + viper, DisjointMemoryRegion, MemoryRegion, +}; + +const MAX_BUILD_ITERATION: usize = 3; + +// When building for x86, the kernel is copied from the SDK release package to the same +// directory as the output boot module image, as Multiboot want them as +// separate images. +const KERNEL_COPY_FILENAME: &str = "sel4.elf"; +// The `-kernel` argument of 'qemu-system-x86_64' doesn't accept a 64-bit image, so we +// also copy the 32-bit version that was prepared by build_sdk.py for convenience. +const KERNEL32_COPY_FILENAME: &str = "sel4_32.elf"; + +pub fn build_system( + args: &Args, + sdk: &Sdk, + kernel_config: &Config, + system: &mut SystemDescription, +) -> Result<(), String> { + let current_config = sdk.select(&args.board, &args.config).unwrap(); + let elf_path = current_config.config_dir.join("elf"); + let loader_elf_path = elf_path.join("loader.elf"); + let kernel_elf_path = match args.override_kernel { + Some(ref path) => path, + None => &elf_path.join("sel4.elf"), + }; + let monitor_elf_path = elf_path.join("monitor.elf"); + let capdl_init_elf_path = elf_path.join("initialiser.elf"); + + bail_if_not_exists("board ELF directory", &elf_path)?; + bail_if_not_exists("kernel ELF", kernel_elf_path)?; + bail_if_not_exists("monitor ELF", &monitor_elf_path)?; + bail_if_not_exists("CapDL initialiser ELF", &capdl_init_elf_path)?; + + if kernel_config.arch != Arch::X86_64 && !loader_elf_path.exists() { + eprintln!( + "Error: loader ELF '{}' does not exist", + loader_elf_path.display() + ); + std::process::exit(1); + } + + let image_output_type = match ImageOutputType::resolve( + &args.requested_image_type, + &kernel_config.arch, + args.board.as_str(), + ) { + Some(image) => image, + None => { + eprintln!( + "microkit: error: building the output image as '{0}' is unsupported for target architecture '{1}'", + args.requested_image_type, kernel_config.arch + ); + std::process::exit(1); + } + }; + + let capdl_initialiser_elf = ElfFile::from_path(&capdl_init_elf_path).unwrap_or_else(|e| { + eprintln!( + "ERROR: failed to parse initialiser ELF ({}): {}", + capdl_init_elf_path.display(), + e + ); + std::process::exit(1); + }); + + // Only relevant for ARM and RISC-V. + // Determine how much physical memory is available to the kernel after it boots but before dropping + // to userspace by partially emulating the kernel boot process. This is useful for two purposes: + // 1. To implement setvar region_paddr for memory regions that doesn't specify a phys address, where + // we must automatically select a suitable address inside the Microkit tool. + // 2. Post-spec generation sanity checks at a later point to ensure that there are sufficient memory + // to allocate all kernel objects. + let (kernel_elf_maybe, available_memory_maybe, kernel_boot_region_maybe) = + match kernel_config.arch { + Arch::X86_64 => (None, None, None), + Arch::Aarch64 | Arch::Riscv64 => { + let kernel_elf = ElfFile::from_path(kernel_elf_path).unwrap_or_else(|e| { + eprintln!( + "ERROR: failed to parse kernel ELF ({}): {}", + kernel_elf_path.display(), + e + ); + std::process::exit(1); + }); + + // Now determine how much memory we have after the kernel boots. + let (available_memory, kernel_boot_region) = + emulate_kernel_boot_partial(kernel_config, &kernel_elf); + ( + Some(kernel_elf), + Some(available_memory), + Some(kernel_boot_region), + ) + } + }; + + let monitor_elf = ElfFile::from_path(&monitor_elf_path).unwrap_or_else(|e| { + eprintln!( + "ERROR: failed to parse monitor ELF ({}): {}", + monitor_elf_path.display(), + e + ); + std::process::exit(1); + }); + + // This list refers to all PD ELFs as well as the Monitor ELF. + // The monitor is very similar to a PD so it is useful to pass around + // a list like this. + let mut system_elfs = Vec::with_capacity(system.protection_domains.len()); + // Get the elf files for each pd: + for pd in &system.protection_domains { + match get_full_path(&pd.program_image, &args.search_paths) { + Some(path) => { + let path_for_symbols = pd + .program_image_for_symbols + .as_ref() + .map(|path_suffix| { + get_full_path(path_suffix, &args.search_paths).ok_or_else(|| { + format!( + "unable to find program image for symbols: '{}'", + path_suffix.display() + ) + }) + }) + .transpose()?; + match ElfFile::from_split_paths(&path, path_for_symbols.as_deref()) { + Ok(elf) => system_elfs.push(elf), + Err(e) => { + eprintln!( + "ERROR: failed to parse ELF '{}' for PD '{}': {}", + path.display(), + pd.name, + e + ); + std::process::exit(1); + } + }; + } + None => { + return Err(format!( + "unable to find program image: '{}'", + pd.program_image.display() + )) + } + } + } + + // The monitor is just a special PD + system_elfs.push(monitor_elf); + + let capdl_initialiser_orig = CapDLInitialiser::new(capdl_initialiser_elf); + + // Now build the capDL spec and final image. We may need to do this in >1 iterations on ARM and RISC-V + // if there are Memory Regions without a paddr but subject to setvar region_paddr. + let mut iteration = 0; + let mut spec_need_refinement = true; + let mut system_built = false; + while spec_need_refinement && iteration < MAX_BUILD_ITERATION { + let mut capdl_initialiser = capdl_initialiser_orig.clone(); + spec_need_refinement = false; + + // Patch all the required symbols in the Monitor and PDs according to the Microkit's requirements + if let Err(err) = patch_symbols(kernel_config, &mut system_elfs, system) { + eprintln!("ERROR: {err}"); + std::process::exit(1); + } + + let mut spec_container = build_capdl_spec(kernel_config, &mut system_elfs, system)?; + pack_spec_into_initial_task( + kernel_config, + args.config.as_str(), + &spec_container, + &system_elfs, + &mut capdl_initialiser, + ); + + match kernel_config.arch { + Arch::X86_64 => { + // setvar region_paddr not supported on this architecture nor can we emulate the + // kernel boot process to statically check for issues due to unknown memory map, so nothing to do. + // Write out the capDL initialiser as an ELF boot module and we are done. + } + Arch::Aarch64 | Arch::Riscv64 => { + // Now that we have the CapDL initialiser ELF with embedded spec, + // we can determine exactly how much memory will be available statically when the kernel + // drops to userspace on ARM and RISC-V. This allow us to sanity check that: + // 1. There are enough memory to allocate all the objects required in the spec. + // 2. All frames with a physical attached reside in legal memory (device or normal). + // 3. Objects can be allocated from the free untyped list. For example, we detect + // situations where you might have a few frames with size bit 12 to allocate but + // only have untyped with size bit <12 remaining. + // This also allow the tool to automatically pick physical address of Memory Regions with out + // an explicit paddr in SDF but are subject to setvar region_paddr. + + // Determine how much memory the CapDL initialiser needs. + let initialiser_vaddr_range = capdl_initialiser.image_bound(); + let initial_task_size = initialiser_vaddr_range.end - initialiser_vaddr_range.start; + + // Reuse data from the partial kernel boot emulation previously done. + // .clone() as we need to mutate this for every iteration. + let mut available_memory = available_memory_maybe.clone().unwrap(); + let kernel_boot_region = kernel_boot_region_maybe.unwrap(); + + // The kernel relies on the initial task region being allocated above the kernel + // boot/ELF region, so we have the end of the kernel boot region as the lower + // bound for allocating the reserved region. + let initial_task_phys_base = + available_memory.allocate_from(initial_task_size, kernel_boot_region.end); + + let Some(initial_task_phys_base) = initial_task_phys_base else { + // Unlikely to happen on Microkit-supported platforms with multi gigabytes memory. + // But printing a helpful error in case we do run into this problem. + eprintln!( + "ERROR: cannot allocate memory for the initialiser, contiguous physical memory region of size {} not found", human_size_strict(initial_task_size) + ); + eprintln!("ERROR: physical memory regions the initialiser can be placed at:"); + for region in available_memory.regions { + eprintln!( + " [0x{:0>12x}..0x{:0>12x}), size: {}", + region.base, + region.end, + human_size_strict(region.size()) + ); + } + std::process::exit(1); + }; + + capdl_initialiser.set_phys_base(initial_task_phys_base); + let initial_task_phys_region = MemoryRegion::new( + initial_task_phys_base, + initial_task_phys_base + initial_task_size, + ); + let user_image_virt_region = MemoryRegion::new( + capdl_initialiser.elf.lowest_vaddr(), + initialiser_vaddr_range.end, + ); + + // With the initial task region determined the kernel boot can be emulated in full. This provides + // the boot info information (containing untyped objects) which is needed for the next steps + let kernel_boot_info = emulate_kernel_boot( + kernel_config, + kernel_elf_maybe.as_ref().unwrap(), + initial_task_phys_region, + user_image_virt_region, + ); + + if iteration == 0 { + // On the first iteration where the spec have not been refined, simulate the capDL allocation algorithm + // to double check that all kernel objects of the system as described by SDF can be successfully allocated. + if !simulate_capdl_object_alloc_algorithm( + &mut spec_container, + &kernel_boot_info, + kernel_config, + CapDLAllocEmulationErrorLevel::PrintStderr, + ) { + eprintln!("ERROR: could not allocate all required kernel objects. Please see report for more details."); + std::process::exit(1); + } + } else { + // Do the same thing for further iterations, at this point the simulation won't fail *except* for when we have picked a + // bad address for Memory Regions subject to setvar region_paddr. This can happen because after we have + // picked the address, we will update spec and patch it into the program's frame. Which will causes the + // spec to increase in size as the frames' data are compressed. So if the simulation fail, we need to + // pick another address as we now have a better idea of how large the spec is. + + // This is highly unlikely to happen unless the spec size increase causes the initial task size to cross + // a 4K page boundary. + if !simulate_capdl_object_alloc_algorithm( + &mut spec_container, + &kernel_boot_info, + kernel_config, + CapDLAllocEmulationErrorLevel::Suppressed, + ) { + // Encountered a problem, pick a better address. + for tool_allocate_mr in system.memory_regions.iter_mut().filter(|mr| { + matches!(mr.phys_addr, SysMemoryRegionPaddr::ToolAllocated(_)) + }) { + tool_allocate_mr.phys_addr = SysMemoryRegionPaddr::ToolAllocated(None); + } + spec_container.expected_allocations = HashMap::new(); + } + } + + // Now pick a physical address for any memory regions that are subject to setvar region_paddr. + // Doing something a bit unconventional here: converting the list of untypeds back to a DisjointMemoryRegion + // to give us a view of physical memory available after the kernel drops to user space. + // I.e. available memory after the initial task have been created. + { + let mut available_user_memory = DisjointMemoryRegion::default(); + for ut in kernel_boot_info + .untyped_objects + .iter() + .filter(|ut| !ut.is_device) + { + // Only take untypeds that can at least fit a page because some have been used to back the initial task's + // kernel object such as TCB, endpoint etc. + let start = round_up(ut.base(), kernel_config.minimum_page_size); + let end = round_down(ut.end(), kernel_config.minimum_page_size); + if end > start { + // will be automatically merged + available_user_memory.insert_region(ut.base(), ut.end()); + } + } + + // Then take away any memory ranges occupied by Memory Regions with a paddr specified in SDF. + for mr in system.memory_regions.iter() { + if let SysMemoryRegionPaddr::Specified(sdf_paddr) = mr.phys_addr { + let mr_end = sdf_paddr + mr.size; + + // MR may be device memory, which isn't covered in available_user_memory. + let is_normal_mem = + available_user_memory.regions.iter().any(|region| { + sdf_paddr >= region.base + && sdf_paddr < region.end + && mr_end <= region.end + }); + if is_normal_mem { + available_user_memory.remove_region(sdf_paddr, sdf_paddr + mr.size); + } + } + } + + let mut tool_allocated_mrs = Vec::new(); + for (mr_id, tool_allocate_mr) in system + .memory_regions + .iter_mut() + .enumerate() + .filter(|(_, mr)| { + matches!(mr.phys_addr, SysMemoryRegionPaddr::ToolAllocated(None)) + }) + { + spec_need_refinement = true; + + let target_paddr = available_user_memory + .allocate(tool_allocate_mr.size, tool_allocate_mr.page_size); + if target_paddr.is_none() { + eprintln!("ERROR: cannot auto-select a physical address for MR {} because there are no contiguous memory region of sufficient size.", tool_allocate_mr.name); + eprintln!("ERROR: MR {} needs to be physically contiguous as it is a subject of a setvar region_paddr.", tool_allocate_mr.name); + if !tool_allocated_mrs.is_empty() { + eprintln!("Previously auto-allocated memory regions:"); + for allocated_mr_id in tool_allocated_mrs { + let allocated_mr: &SysMemoryRegion = + &system.memory_regions[allocated_mr_id]; + eprintln!( + "name = '{}', paddr = 0x{:0>12x}, size = 0x{:0>12x}", + allocated_mr.name, + allocated_mr.paddr().unwrap(), + allocated_mr.size + ); + } + } + eprintln!("available physical memory regions:"); + for region in available_user_memory.regions { + eprintln!( + "[0x{:0>12x}..0x{:0>12x}), size: {}", + region.base, + region.end, + human_size_strict(region.size()) + ); + } + std::process::exit(1); + } + tool_allocated_mrs.push(mr_id); + tool_allocate_mr.phys_addr = + SysMemoryRegionPaddr::ToolAllocated(target_paddr); + } + } + + // Patch the list of untypeds we used to simulate object allocation into the initialiser. + // At runtime the initialiser will validate what we simulated against what the kernel gives it. If they deviate + // we will have problems! For example, if we simulated with more memory than what's actually available, the initialiser + // can crash. + capdl_initialiser.add_expected_untypeds(&kernel_boot_info.untyped_objects); + } + }; + + if !spec_need_refinement { + // All is well in the universe, write the image out. + println!( + "MICROKIT|CAPDL SPEC: number of root objects = {}, spec footprint = {}", + spec_container.spec.objects.len(), + human_size_strict( + capdl_initialiser + .spec_metadata() + .as_ref() + .unwrap() + .spec_size + ), + ); + let initialiser_vaddr_range = capdl_initialiser.image_bound(); + println!( + "MICROKIT|INITIAL TASK: memory size = {}", + human_size_strict(initialiser_vaddr_range.end - initialiser_vaddr_range.start), + ); + + let image_out_path = args.output_path.as_path(); + + match kernel_config.arch { + Arch::X86_64 => match capdl_initialiser.elf.reserialise(image_out_path) { + Ok(size) => { + // Copy the kernel to the build directory as well so users doesn't have to dig through the SDK. + if let Err(copy_err) = fs::copy( + kernel_elf_path, + image_out_path.parent().unwrap().join(KERNEL_COPY_FILENAME), + ) { + eprintln!("ERROR: couldn't copy the kernel to image's output directory: {copy_err}"); + std::process::exit(1); + } + if let Err(copy_err) = fs::copy( + kernel_elf_path + .parent() + .unwrap() + .join(KERNEL32_COPY_FILENAME), + image_out_path + .parent() + .unwrap() + .join(KERNEL32_COPY_FILENAME), + ) { + eprintln!("ERROR: couldn't copy the 32-bit kernel to image's output directory: {copy_err}"); + std::process::exit(1); + } + println!( + "MICROKIT|BOOT MODULE: image file size = {}", + human_size_strict(size) + ); + } + Err(err) => { + eprintln!("ERROR: couldn't write the boot module to filesystem: {err}"); + std::process::exit(1); + } + }, + Arch::Aarch64 | Arch::Riscv64 => { + let loader = Loader::new( + kernel_config, + Path::new(&loader_elf_path), + kernel_elf_maybe.as_ref().unwrap(), + &capdl_initialiser.elf, + capdl_initialiser.phys_base.unwrap(), + &initialiser_vaddr_range, + ); + + match image_output_type { + ImageOutputType::Binary => loader.write_image(image_out_path), + ImageOutputType::Elf => loader.write_elf(image_out_path), + ImageOutputType::Uimage => loader.write_uimage(image_out_path), + }; + + println!( + "MICROKIT|LOADER: image file size = {}", + human_size_strict(metadata(image_out_path).unwrap().len()) + ); + } + }; + + if let Some(capdl_json) = &args.capdl_json_path { + let serialised = serde_json::to_string_pretty(&spec_container.spec).unwrap(); + fs::write(capdl_json, &serialised).unwrap(); + }; + + if let Some(viper_output_dir) = &args.viper_output_dir { + // NB returns Ok if the directory already exists, that's fine + fs::create_dir_all(viper_output_dir).unwrap_or_else(|source| { + eprintln!( + "ERROR: cannot write Viper output directory {}: {source}", + &viper_output_dir.display() + ); + std::process::exit(1); + }); + for view in viper::get_combined_views(&spec_container, system) { + let mut output = format!( + "// exported invariants for PD {} in {}\n", + view.pd_name, + &args.sdf_path.display(), + ); + view.export(&mut output); + let path = viper_output_dir.join(format!("{}.vpr", view.pd_name)); + fs::write(&path, output).unwrap_or_else(|source| { + eprintln!( + "ERROR: cannot write Viper output file {}: {source}", + &path.display() + ); + std::process::exit(1); + }); + } + } + + write_report(&spec_container, kernel_config, &args.report_path); + system_built = true; + break; + } else { + // Some memory regions have had their physical address updated, rebuild the spec. + iteration += 1; + } + } + + if !system_built { + // Cannot build a reasonable spec, absurd. + // Only reachable when there are setvar region_paddr that we keep selecting the wrong address. + panic!("ERROR: fatal, failed to build system in {iteration} iterations"); + } + Ok(()) +} diff --git a/tool/microkit/src/lib.rs b/tool/microkit/src/lib.rs index 31c19178c..166c5b34b 100644 --- a/tool/microkit/src/lib.rs +++ b/tool/microkit/src/lib.rs @@ -12,6 +12,7 @@ use crate::{ }; pub mod argparse; +pub mod build; pub mod capdl; pub mod crc32; pub mod elf; diff --git a/tool/microkit/src/main.rs b/tool/microkit/src/main.rs index 25539f390..6f4f88bbf 100644 --- a/tool/microkit/src/main.rs +++ b/tool/microkit/src/main.rs @@ -8,42 +8,13 @@ #![allow(clippy::assertions_on_constants)] use microkit_tool::argparse; -use microkit_tool::argparse::ArgsError; +use microkit_tool::argparse::{Args, ArgsError}; +use microkit_tool::build::build_system; use microkit_tool::sdf::parse; -use std::{ - collections::HashMap, - fs::{self, metadata}, - path::Path, -}; - -use microkit_tool::{ - argparse::Args, - capdl::{ - allocation::{simulate_capdl_object_alloc_algorithm, CapDLAllocEmulationErrorLevel}, - build_capdl_spec, - initialiser::CapDLInitialiser, - packaging::pack_spec_into_initial_task, - }, - elf::ElfFile, - loader::Loader, - report::write_report, - sdf::{SysMemoryRegion, SysMemoryRegionPaddr}, - sdk::Sdk, - sel4::{emulate_kernel_boot, emulate_kernel_boot_partial, Arch, Config, ImageOutputType}, - symbols::patch_symbols, - util::{bail_if_not_exists, get_full_path, human_size_strict, round_down, round_up}, - viper, DisjointMemoryRegion, MemoryRegion, -}; - -const MAX_BUILD_ITERATION: usize = 3; - -// When building for x86, the kernel is copied from the SDK release package to the same -// directory as the output boot module image, as Multiboot want them as -// separate images. -const KERNEL_COPY_FILENAME: &str = "sel4.elf"; -// The `-kernel` argument of 'qemu-system-x86_64' doesn't accept a 64-bit image, so we -// also copy the 32-bit version that was prepared by build_sdk.py for convenience. -const KERNEL32_COPY_FILENAME: &str = "sel4_32.elf"; +use microkit_tool::sdk::Sdk; +use microkit_tool::sel4::Config; +use microkit_tool::util::bail_if_not_exists; +use std::fs; fn main() -> Result<(), String> { let sdk = match Sdk::discover() { @@ -96,487 +67,5 @@ fn main() -> Result<(), String> { } }; - let current_config = sdk.select(&args.board, &args.config).unwrap(); - let elf_path = current_config.config_dir.join("elf"); - let loader_elf_path = elf_path.join("loader.elf"); - let kernel_elf_path = match args.override_kernel { - Some(ref path) => path, - None => &elf_path.join("sel4.elf"), - }; - let monitor_elf_path = elf_path.join("monitor.elf"); - let capdl_init_elf_path = elf_path.join("initialiser.elf"); - - bail_if_not_exists("board ELF directory", &elf_path)?; - bail_if_not_exists("kernel ELF", kernel_elf_path)?; - bail_if_not_exists("monitor ELF", &monitor_elf_path)?; - bail_if_not_exists("CapDL initialiser ELF", &capdl_init_elf_path)?; - - if kernel_config.arch != Arch::X86_64 && !loader_elf_path.exists() { - eprintln!( - "Error: loader ELF '{}' does not exist", - loader_elf_path.display() - ); - std::process::exit(1); - } - - let image_output_type = match ImageOutputType::resolve( - &args.requested_image_type, - &kernel_config.arch, - args.board.as_str(), - ) { - Some(image) => image, - None => { - eprintln!( - "microkit: error: building the output image as '{0}' is unsupported for target architecture '{1}'", - args.requested_image_type, kernel_config.arch - ); - std::process::exit(1); - } - }; - - let capdl_initialiser_elf = ElfFile::from_path(&capdl_init_elf_path).unwrap_or_else(|e| { - eprintln!( - "ERROR: failed to parse initialiser ELF ({}): {}", - capdl_init_elf_path.display(), - e - ); - std::process::exit(1); - }); - - // Only relevant for ARM and RISC-V. - // Determine how much physical memory is available to the kernel after it boots but before dropping - // to userspace by partially emulating the kernel boot process. This is useful for two purposes: - // 1. To implement setvar region_paddr for memory regions that doesn't specify a phys address, where - // we must automatically select a suitable address inside the Microkit tool. - // 2. Post-spec generation sanity checks at a later point to ensure that there are sufficient memory - // to allocate all kernel objects. - let (kernel_elf_maybe, available_memory_maybe, kernel_boot_region_maybe) = - match kernel_config.arch { - Arch::X86_64 => (None, None, None), - Arch::Aarch64 | Arch::Riscv64 => { - let kernel_elf = ElfFile::from_path(kernel_elf_path).unwrap_or_else(|e| { - eprintln!( - "ERROR: failed to parse kernel ELF ({}): {}", - kernel_elf_path.display(), - e - ); - std::process::exit(1); - }); - - // Now determine how much memory we have after the kernel boots. - let (available_memory, kernel_boot_region) = - emulate_kernel_boot_partial(&kernel_config, &kernel_elf); - ( - Some(kernel_elf), - Some(available_memory), - Some(kernel_boot_region), - ) - } - }; - - let monitor_elf = ElfFile::from_path(&monitor_elf_path).unwrap_or_else(|e| { - eprintln!( - "ERROR: failed to parse monitor ELF ({}): {}", - monitor_elf_path.display(), - e - ); - std::process::exit(1); - }); - - // This list refers to all PD ELFs as well as the Monitor ELF. - // The monitor is very similar to a PD so it is useful to pass around - // a list like this. - let mut system_elfs = Vec::with_capacity(system.protection_domains.len()); - // Get the elf files for each pd: - for pd in &system.protection_domains { - match get_full_path(&pd.program_image, &args.search_paths) { - Some(path) => { - let path_for_symbols = pd - .program_image_for_symbols - .as_ref() - .map(|path_suffix| { - get_full_path(path_suffix, &args.search_paths).ok_or_else(|| { - format!( - "unable to find program image for symbols: '{}'", - path_suffix.display() - ) - }) - }) - .transpose()?; - match ElfFile::from_split_paths(&path, path_for_symbols.as_deref()) { - Ok(elf) => system_elfs.push(elf), - Err(e) => { - eprintln!( - "ERROR: failed to parse ELF '{}' for PD '{}': {}", - path.display(), - pd.name, - e - ); - std::process::exit(1); - } - }; - } - None => { - return Err(format!( - "unable to find program image: '{}'", - pd.program_image.display() - )) - } - } - } - - // The monitor is just a special PD - system_elfs.push(monitor_elf); - - let capdl_initialiser_orig = CapDLInitialiser::new(capdl_initialiser_elf); - - // Now build the capDL spec and final image. We may need to do this in >1 iterations on ARM and RISC-V - // if there are Memory Regions without a paddr but subject to setvar region_paddr. - let mut iteration = 0; - let mut spec_need_refinement = true; - let mut system_built = false; - while spec_need_refinement && iteration < MAX_BUILD_ITERATION { - let mut capdl_initialiser = capdl_initialiser_orig.clone(); - spec_need_refinement = false; - - // Patch all the required symbols in the Monitor and PDs according to the Microkit's requirements - if let Err(err) = patch_symbols(&kernel_config, &mut system_elfs, &system) { - eprintln!("ERROR: {err}"); - std::process::exit(1); - } - - let mut spec_container = build_capdl_spec(&kernel_config, &mut system_elfs, &system)?; - pack_spec_into_initial_task( - &kernel_config, - args.config.as_str(), - &spec_container, - &system_elfs, - &mut capdl_initialiser, - ); - - match kernel_config.arch { - Arch::X86_64 => { - // setvar region_paddr not supported on this architecture nor can we emulate the - // kernel boot process to statically check for issues due to unknown memory map, so nothing to do. - // Write out the capDL initialiser as an ELF boot module and we are done. - } - Arch::Aarch64 | Arch::Riscv64 => { - // Now that we have the CapDL initialiser ELF with embedded spec, - // we can determine exactly how much memory will be available statically when the kernel - // drops to userspace on ARM and RISC-V. This allow us to sanity check that: - // 1. There are enough memory to allocate all the objects required in the spec. - // 2. All frames with a physical attached reside in legal memory (device or normal). - // 3. Objects can be allocated from the free untyped list. For example, we detect - // situations where you might have a few frames with size bit 12 to allocate but - // only have untyped with size bit <12 remaining. - // This also allow the tool to automatically pick physical address of Memory Regions with out - // an explicit paddr in SDF but are subject to setvar region_paddr. - - // Determine how much memory the CapDL initialiser needs. - let initialiser_vaddr_range = capdl_initialiser.image_bound(); - let initial_task_size = initialiser_vaddr_range.end - initialiser_vaddr_range.start; - - // Reuse data from the partial kernel boot emulation previously done. - // .clone() as we need to mutate this for every iteration. - let mut available_memory = available_memory_maybe.clone().unwrap(); - let kernel_boot_region = kernel_boot_region_maybe.unwrap(); - - // The kernel relies on the initial task region being allocated above the kernel - // boot/ELF region, so we have the end of the kernel boot region as the lower - // bound for allocating the reserved region. - let initial_task_phys_base = - available_memory.allocate_from(initial_task_size, kernel_boot_region.end); - - let Some(initial_task_phys_base) = initial_task_phys_base else { - // Unlikely to happen on Microkit-supported platforms with multi gigabytes memory. - // But printing a helpful error in case we do run into this problem. - eprintln!( - "ERROR: cannot allocate memory for the initialiser, contiguous physical memory region of size {} not found", human_size_strict(initial_task_size) - ); - eprintln!("ERROR: physical memory regions the initialiser can be placed at:"); - for region in available_memory.regions { - eprintln!( - " [0x{:0>12x}..0x{:0>12x}), size: {}", - region.base, - region.end, - human_size_strict(region.size()) - ); - } - std::process::exit(1); - }; - - capdl_initialiser.set_phys_base(initial_task_phys_base); - let initial_task_phys_region = MemoryRegion::new( - initial_task_phys_base, - initial_task_phys_base + initial_task_size, - ); - let user_image_virt_region = MemoryRegion::new( - capdl_initialiser.elf.lowest_vaddr(), - initialiser_vaddr_range.end, - ); - - // With the initial task region determined the kernel boot can be emulated in full. This provides - // the boot info information (containing untyped objects) which is needed for the next steps - let kernel_boot_info = emulate_kernel_boot( - &kernel_config, - kernel_elf_maybe.as_ref().unwrap(), - initial_task_phys_region, - user_image_virt_region, - ); - - if iteration == 0 { - // On the first iteration where the spec have not been refined, simulate the capDL allocation algorithm - // to double check that all kernel objects of the system as described by SDF can be successfully allocated. - if !simulate_capdl_object_alloc_algorithm( - &mut spec_container, - &kernel_boot_info, - &kernel_config, - CapDLAllocEmulationErrorLevel::PrintStderr, - ) { - eprintln!("ERROR: could not allocate all required kernel objects. Please see report for more details."); - std::process::exit(1); - } - } else { - // Do the same thing for further iterations, at this point the simulation won't fail *except* for when we have picked a - // bad address for Memory Regions subject to setvar region_paddr. This can happen because after we have - // picked the address, we will update spec and patch it into the program's frame. Which will causes the - // spec to increase in size as the frames' data are compressed. So if the simulation fail, we need to - // pick another address as we now have a better idea of how large the spec is. - - // This is highly unlikely to happen unless the spec size increase causes the initial task size to cross - // a 4K page boundary. - if !simulate_capdl_object_alloc_algorithm( - &mut spec_container, - &kernel_boot_info, - &kernel_config, - CapDLAllocEmulationErrorLevel::Suppressed, - ) { - // Encountered a problem, pick a better address. - for tool_allocate_mr in system.memory_regions.iter_mut().filter(|mr| { - matches!(mr.phys_addr, SysMemoryRegionPaddr::ToolAllocated(_)) - }) { - tool_allocate_mr.phys_addr = SysMemoryRegionPaddr::ToolAllocated(None); - } - spec_container.expected_allocations = HashMap::new(); - } - } - - // Now pick a physical address for any memory regions that are subject to setvar region_paddr. - // Doing something a bit unconventional here: converting the list of untypeds back to a DisjointMemoryRegion - // to give us a view of physical memory available after the kernel drops to user space. - // I.e. available memory after the initial task have been created. - { - let mut available_user_memory = DisjointMemoryRegion::default(); - for ut in kernel_boot_info - .untyped_objects - .iter() - .filter(|ut| !ut.is_device) - { - // Only take untypeds that can at least fit a page because some have been used to back the initial task's - // kernel object such as TCB, endpoint etc. - let start = round_up(ut.base(), kernel_config.minimum_page_size); - let end = round_down(ut.end(), kernel_config.minimum_page_size); - if end > start { - // will be automatically merged - available_user_memory.insert_region(ut.base(), ut.end()); - } - } - - // Then take away any memory ranges occupied by Memory Regions with a paddr specified in SDF. - for mr in system.memory_regions.iter() { - if let SysMemoryRegionPaddr::Specified(sdf_paddr) = mr.phys_addr { - let mr_end = sdf_paddr + mr.size; - - // MR may be device memory, which isn't covered in available_user_memory. - let is_normal_mem = - available_user_memory.regions.iter().any(|region| { - sdf_paddr >= region.base - && sdf_paddr < region.end - && mr_end <= region.end - }); - if is_normal_mem { - available_user_memory.remove_region(sdf_paddr, sdf_paddr + mr.size); - } - } - } - - let mut tool_allocated_mrs = Vec::new(); - for (mr_id, tool_allocate_mr) in system - .memory_regions - .iter_mut() - .enumerate() - .filter(|(_, mr)| { - matches!(mr.phys_addr, SysMemoryRegionPaddr::ToolAllocated(None)) - }) - { - spec_need_refinement = true; - - let target_paddr = available_user_memory - .allocate(tool_allocate_mr.size, tool_allocate_mr.page_size); - if target_paddr.is_none() { - eprintln!("ERROR: cannot auto-select a physical address for MR {} because there are no contiguous memory region of sufficient size.", tool_allocate_mr.name); - eprintln!("ERROR: MR {} needs to be physically contiguous as it is a subject of a setvar region_paddr.", tool_allocate_mr.name); - if !tool_allocated_mrs.is_empty() { - eprintln!("Previously auto-allocated memory regions:"); - for allocated_mr_id in tool_allocated_mrs { - let allocated_mr: &SysMemoryRegion = - &system.memory_regions[allocated_mr_id]; - eprintln!( - "name = '{}', paddr = 0x{:0>12x}, size = 0x{:0>12x}", - allocated_mr.name, - allocated_mr.paddr().unwrap(), - allocated_mr.size - ); - } - } - eprintln!("available physical memory regions:"); - for region in available_user_memory.regions { - eprintln!( - "[0x{:0>12x}..0x{:0>12x}), size: {}", - region.base, - region.end, - human_size_strict(region.size()) - ); - } - std::process::exit(1); - } - tool_allocated_mrs.push(mr_id); - tool_allocate_mr.phys_addr = - SysMemoryRegionPaddr::ToolAllocated(target_paddr); - } - } - - // Patch the list of untypeds we used to simulate object allocation into the initialiser. - // At runtime the initialiser will validate what we simulated against what the kernel gives it. If they deviate - // we will have problems! For example, if we simulated with more memory than what's actually available, the initialiser - // can crash. - capdl_initialiser.add_expected_untypeds(&kernel_boot_info.untyped_objects); - } - }; - - if !spec_need_refinement { - // All is well in the universe, write the image out. - println!( - "MICROKIT|CAPDL SPEC: number of root objects = {}, spec footprint = {}", - spec_container.spec.objects.len(), - human_size_strict( - capdl_initialiser - .spec_metadata() - .as_ref() - .unwrap() - .spec_size - ), - ); - let initialiser_vaddr_range = capdl_initialiser.image_bound(); - println!( - "MICROKIT|INITIAL TASK: memory size = {}", - human_size_strict(initialiser_vaddr_range.end - initialiser_vaddr_range.start), - ); - - let image_out_path = args.output_path.as_path(); - - match kernel_config.arch { - Arch::X86_64 => match capdl_initialiser.elf.reserialise(image_out_path) { - Ok(size) => { - // Copy the kernel to the build directory as well so users doesn't have to dig through the SDK. - if let Err(copy_err) = fs::copy( - kernel_elf_path, - image_out_path.parent().unwrap().join(KERNEL_COPY_FILENAME), - ) { - eprintln!("ERROR: couldn't copy the kernel to image's output directory: {copy_err}"); - std::process::exit(1); - } - if let Err(copy_err) = fs::copy( - kernel_elf_path - .parent() - .unwrap() - .join(KERNEL32_COPY_FILENAME), - image_out_path - .parent() - .unwrap() - .join(KERNEL32_COPY_FILENAME), - ) { - eprintln!("ERROR: couldn't copy the 32-bit kernel to image's output directory: {copy_err}"); - std::process::exit(1); - } - println!( - "MICROKIT|BOOT MODULE: image file size = {}", - human_size_strict(size) - ); - } - Err(err) => { - eprintln!("ERROR: couldn't write the boot module to filesystem: {err}"); - std::process::exit(1); - } - }, - Arch::Aarch64 | Arch::Riscv64 => { - let loader = Loader::new( - &kernel_config, - Path::new(&loader_elf_path), - kernel_elf_maybe.as_ref().unwrap(), - &capdl_initialiser.elf, - capdl_initialiser.phys_base.unwrap(), - &initialiser_vaddr_range, - ); - - match image_output_type { - ImageOutputType::Binary => loader.write_image(image_out_path), - ImageOutputType::Elf => loader.write_elf(image_out_path), - ImageOutputType::Uimage => loader.write_uimage(image_out_path), - }; - - println!( - "MICROKIT|LOADER: image file size = {}", - human_size_strict(metadata(image_out_path).unwrap().len()) - ); - } - }; - - if let Some(capdl_json) = &args.capdl_json_path { - let serialised = serde_json::to_string_pretty(&spec_container.spec).unwrap(); - fs::write(capdl_json, &serialised).unwrap(); - }; - - if let Some(viper_output_dir) = &args.viper_output_dir { - // NB returns Ok if the directory already exists, that's fine - fs::create_dir_all(viper_output_dir).unwrap_or_else(|source| { - eprintln!( - "ERROR: cannot write Viper output directory {}: {source}", - &viper_output_dir.display() - ); - std::process::exit(1); - }); - for view in viper::get_combined_views(&spec_container, &system) { - let mut output = format!( - "// exported invariants for PD {} in {}\n", - view.pd_name, - &args.sdf_path.display(), - ); - view.export(&mut output); - let path = viper_output_dir.join(format!("{}.vpr", view.pd_name)); - fs::write(&path, output).unwrap_or_else(|source| { - eprintln!( - "ERROR: cannot write Viper output file {}: {source}", - &path.display() - ); - std::process::exit(1); - }); - } - } - - write_report(&spec_container, &kernel_config, &args.report_path); - system_built = true; - break; - } else { - // Some memory regions have had their physical address updated, rebuild the spec. - iteration += 1; - } - } - - if !system_built { - // Cannot build a reasonable spec, absurd. - // Only reachable when there are setvar region_paddr that we keep selecting the wrong address. - panic!("ERROR: fatal, failed to build system in {iteration} iterations"); - } - Ok(()) + build_system(&args, &sdk, &kernel_config, &mut system) }