Split the system contract into boot-handoff / abi / device-abi
The `system` module (formerly `danos`) had become a grab-bag: it held the
loader<->kernel handoff *and* the kernel<->user ABI *and* the device wire types, in
one module three different audiences imported. Usage proved the seam — the
bootloader never touched the syscall/device ABI, and user space never touched the
boot handoff — so split it by audience, one module per contract:
system/boot-handoff.zig loader <-> kernel: BootInformation, Framebuffer,
MemoryMap, the VM layout + physicalToVirtual, kernel_abi
system/abi.zig kernel <-> user, core: SystemCall, mmap prot flags,
page_size, notify_badge_bit, ServiceId
system/devices/device-abi.zig kernel <-> user, devices: DeviceDescriptor,
DeviceClass, ResourceDescriptor, ResourceKind, ...
device-abi is the devices sub-project's public interface, exposed as its own module
the way vfs exposes vfs-protocol — importable by user space, unlike the
kernel-internal device model it also feeds. That collapses a real duplication:
DeviceClass and ResourceKind were defined twice (device-model.zig and the contract,
kept "in sync by hand"); device-model now re-exports them from device-abi, so the
enum a driver matches on and the one the kernel classifies with are one type.
Each import now declares which contract it speaks: the bootloader imports only
boot-handoff; a driver only abi + device-abi (via the runtime); the kernel all
three. This also retires the `system` / `runtime.system` name overlap. page_size
lands in abi (it's part of the mmap contract user space aligns to); the bootloader
keeps its own local 4 KiB constant so it depends on nothing but the handoff.
All 21 importers rewired, docs updated to keep /system mapping to source. Build,
host tests, and the QEMU suite (36/36) all green.
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
//! The **loader ↔ kernel** contract: everything a bootloader (boot/, e.g. efi.zig
|
||||
//! built as BOOTX64.efi) and the kernel (system/kernel/kernel.zig) must agree on to
|
||||
//! hand control over — the handoff structures the loader fills in, plus the kernel's
|
||||
//! virtual-memory layout and the physical↔virtual addressing both sides use.
|
||||
//!
|
||||
//! Both binaries import this as the `boot-handoff` module, so the layout is defined
|
||||
//! in exactly one place. **User space never sees this** — the kernel↔user contract is
|
||||
//! [[abi]] (system/abi.zig); device types are [[device-abi]] (system/devices/device-abi.zig).
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
/// Calling convention for the bootloader→kernel jump. Pinned to SystemV so it does
|
||||
/// not depend on each binary's target default: the UEFI bootloader's C
|
||||
/// convention is Microsoft x64 (first arg in RCX), the freestanding kernel's is
|
||||
/// SystemV (first arg in RDI). Both reference this to agree on where `*BootInformation`
|
||||
/// is passed.
|
||||
pub const kernel_abi: std.builtin.CallingConvention = .{ .x86_64_sysv = .{} };
|
||||
|
||||
/// Pixel byte order of the linear framebuffer the firmware handed us.
|
||||
pub const PixelFormat = enum(u32) {
|
||||
/// Byte 0 = Red, 1 = Green, 2 = Blue, 3 = reserved.
|
||||
rgbx,
|
||||
/// Byte 0 = Blue, 1 = Green, 2 = Red, 3 = reserved.
|
||||
bgrx,
|
||||
};
|
||||
|
||||
/// A linear framebuffer: `width`x`height` pixels, each a 32-bit value, with
|
||||
/// `pitch` bytes between the start of one row and the next (which may be larger
|
||||
/// than `width * 4` due to hardware padding).
|
||||
///
|
||||
/// A `base` of 0 means **no framebuffer** — the firmware exposed no Graphics
|
||||
/// Output Protocol (a headless server, say). The kernel must treat on-screen
|
||||
/// output as optional and never assume a framebuffer exists.
|
||||
pub const Framebuffer = extern struct {
|
||||
base: usize, // the memory address where pixel data starts (0 = none)
|
||||
width: u32, // visible pixels per row (e.g. 1920)
|
||||
height: u32, // visible rows (e.g. 1080)
|
||||
pitch: u32, // bytes from the start of one row to the start of the next
|
||||
format: PixelFormat,
|
||||
|
||||
/// Whether a usable framebuffer was handed over.
|
||||
pub fn present(self: Framebuffer) bool {
|
||||
return self.base != 0 and self.width != 0 and self.height != 0;
|
||||
}
|
||||
};
|
||||
|
||||
/// The kernel's virtual-memory layout (higher-half). The kernel is linked at
|
||||
/// `kernel_virt_base` but loaded at a low physical address; all of RAM (and the
|
||||
/// device MMIO windows) is also mapped at `physmap_base + physical`, so the kernel
|
||||
/// can reach any physical address by adding a constant. The low half is left
|
||||
/// entirely to user space.
|
||||
///
|
||||
/// user image + stack : 0x0000_7000_0000_0000 (PML4[224], low half)
|
||||
/// kernel heap : 0xFFFF_8000_0000_0000 (PML4[256])
|
||||
/// physmap : 0xFFFF_8800_0000_0000 (PML4[272]) + physical
|
||||
/// kernel image : 0xFFFF_FFFF_8000_0000 (PML4[511])
|
||||
pub const physmap_base: u64 = 0xFFFF_8800_0000_0000;
|
||||
pub const kernel_virt_base: u64 = 0xFFFF_FFFF_8000_0000;
|
||||
|
||||
/// Physical address -> its virtual address in the physmap. The single way the
|
||||
/// kernel dereferences a physical address once paging is up.
|
||||
///
|
||||
/// **Hazard:** valid only once the (bootstrap or final) page tables are live.
|
||||
/// The bootloader may use the *constant* `physmap_base` to build those tables,
|
||||
/// but must not call this to dereference memory before its own CR3 is loaded —
|
||||
/// it runs under the firmware's identity map, where these addresses are unmapped.
|
||||
pub inline fn physicalToVirtual(physical: u64) u64 {
|
||||
return physical + physmap_base;
|
||||
}
|
||||
|
||||
/// Physmap virtual address -> physical. Inverse of `physicalToVirtual`; for producing
|
||||
/// the physical address of something the kernel holds a physmap pointer to
|
||||
/// (e.g. a page-table frame for CR3, a post-mortem breadcrumb's RAM location).
|
||||
pub inline fn virtualToPhysical(virtual: u64) u64 {
|
||||
return virtual - physmap_base;
|
||||
}
|
||||
|
||||
/// danos's own classification of a span of physical memory — deliberately not
|
||||
/// UEFI's vocabulary. Each boot path (UEFI now, device tree later) translates its
|
||||
/// native memory description into these kinds, so the kernel never learns what
|
||||
/// booted it. [[architecture]] keeps the same discipline for CPU code.
|
||||
pub const MemoryKind = enum(u32) {
|
||||
/// Free RAM the kernel may allocate. Each boot path folds its own transient
|
||||
/// memory into this once it's genuinely free (e.g. the UEFI loader classifies
|
||||
/// boot-services memory as usable after ExitBootServices), so the kernel never
|
||||
/// has to know about boot-protocol-specific "reclaimable" states.
|
||||
usable,
|
||||
/// Firmware, MMIO, the kernel image, our own boot buffers, the boot stack —
|
||||
/// never hand out.
|
||||
reserved,
|
||||
/// ACPI tables: parse, then reclaim.
|
||||
acpi_tables,
|
||||
/// ACPI non-volatile storage: preserve across sleep, do not allocate.
|
||||
acpi_nvs,
|
||||
/// Not backed by RAM: memory-mapped device registers or a reserved
|
||||
/// address-space window (e.g. PCIe configuration space). Kept distinct from
|
||||
/// `reserved` so RAM accounting doesn't count device address space.
|
||||
mmio,
|
||||
};
|
||||
|
||||
/// One contiguous span of physical memory. Because danos defines this layout
|
||||
/// itself (unlike the UEFI descriptor it's built from), `@sizeOf` is
|
||||
/// authoritative — the kernel walks a plain `[]MemoryRegion`, with none of the
|
||||
/// firmware's variable descriptor-stride to worry about.
|
||||
pub const MemoryRegion = extern struct {
|
||||
base: u64, // physical start address
|
||||
pages: u64, // length in 4 KiB pages (the [[abi]] `page_size` unit)
|
||||
kind: MemoryKind,
|
||||
_pad: u32 = 0,
|
||||
};
|
||||
|
||||
/// The physical memory layout handed to the kernel: a pointer to an array of
|
||||
/// `len` `MemoryRegion`s, in a buffer that outlives the loader.
|
||||
pub const MemoryMap = extern struct {
|
||||
regions: usize, // address of a `[len]MemoryRegion`
|
||||
len: usize,
|
||||
};
|
||||
|
||||
/// One PT_LOAD segment of the kernel image, so the kernel can re-map itself with
|
||||
/// correct permissions (code R+X, rodata R, data R+W+NX). `flags` are raw ELF
|
||||
/// segment flags: PF_X=1, PF_W=2, PF_R=4. `virtual` is the higher-half link address;
|
||||
/// `physical` is where the loader actually placed the segment (they differ once the
|
||||
/// kernel links high — the loader records the real load address here).
|
||||
pub const KernelSegment = extern struct {
|
||||
virtual: u64,
|
||||
physical: u64,
|
||||
pages: u64,
|
||||
flags: u32,
|
||||
_pad: u32 = 0,
|
||||
};
|
||||
|
||||
/// Handoff structure the bootloader fills in and passes to the kernel's
|
||||
/// `_start` in RDI (the first argument under the SystemV AMD64 C ABI).
|
||||
pub const BootInformation = extern struct {
|
||||
framebuffer: Framebuffer,
|
||||
memory_map: MemoryMap,
|
||||
/// The kernel's own PT_LOAD segments (it has three: text, rodata, data).
|
||||
kernel_segments: [8]KernelSegment,
|
||||
kernel_segment_count: u32,
|
||||
/// Physical address of the ACPI RSDP the firmware exposed, or 0 if none. The
|
||||
/// kernel's device layer parses the ACPI tables from here to discover hardware.
|
||||
/// A device-tree boot path leaves this 0 and (later) fills a `device_tree_blob`
|
||||
/// field instead, so the kernel discovers devices without knowing what booted it.
|
||||
acpi_rsdp: u64 = 0,
|
||||
/// The raw `/sbin/init` ELF image, read off the boot volume by the loader
|
||||
/// into memory that survives the handoff (classified reserved, so the kernel
|
||||
/// identity-maps it and never allocates over it). 0/0 = no init found — the
|
||||
/// kernel boots without user space. Grows into a full initial_ramdisk handoff later.
|
||||
init_base: u64 = 0,
|
||||
init_len: u64 = 0,
|
||||
/// The initial_ramdisk image (a bundle of extra user binaries — the VFS server and
|
||||
/// device drivers), read off the boot volume into memory that survives the
|
||||
/// handoff, same as `init` above. 0/0 = no initial_ramdisk. See system/initial-ramdisk.zig.
|
||||
initial_ramdisk_base: u64 = 0,
|
||||
initial_ramdisk_len: u64 = 0,
|
||||
};
|
||||
Reference in New Issue
Block a user