Move to multi arch support and added memory map

This commit is contained in:
2026-07-03 11:11:19 +01:00
parent c2435760c4
commit 628c4f6d57
11 changed files with 423 additions and 41 deletions
+12
View File
@@ -0,0 +1,12 @@
//! x86_64 CPU operations. This is the "arch" module: the generic kernel imports
//! it as `@import("arch")` and never names x86_64 directly, so a second
//! architecture is added by pointing that module at a different directory in
//! build.zig — no change to the generic code. Keep everything CPU-specific here
//! (halt now; GDT, IDT and paging will join it), and nothing generic.
/// Park the core forever. `hlt` drops it into a low-power idle until the next
/// interrupt; the loop re-halts on every wake so the stop is permanent. See
/// docs/halting.md for the full reasoning.
pub fn halt() noreturn {
while (true) asm volatile ("hlt");
}
+70 -10
View File
@@ -5,6 +5,7 @@ const danos = @import("danos");
const BootInfo = danos.BootInfo;
const GraphicsOutput = uefi.protocol.GraphicsOutput;
const EdidActive = uefi.protocol.edid.Active;
const MemoryMapSlice = uefi.tables.MemoryMapSlice;
/// Name of the kernel ELF on the boot volume (installed to the ESP root by
/// build.zig). UEFI wants a UTF-16, null-terminated path.
@@ -34,12 +35,13 @@ fn boot() !noreturn {
// services, since afterwards none of these calls are usable.
var boot_info: BootInfo = .{
.framebuffer = try queryFramebuffer(bs),
.memory_map = undefined, // filled by exitBootServices, just below
};
const entry = try loadKernel(bs);
log("danos: kernel loaded, exiting boot services\r\n");
try exitBootServices(bs);
boot_info.memory_map = try exitBootServices(bs);
// Hand control to the kernel. `danos.kernel_abi` is SysV, so the pointer is
// passed in RDI as the kernel expects — not RCX, which this UEFI binary's
@@ -217,27 +219,85 @@ fn loadElf(bs: *uefi.tables.BootServices, image: []u8) !usize {
return @intCast(ehdr.e_entry);
}
/// Fetch the memory map and exit boot services. Allocating the map buffer can
/// itself change the map (invalidating the key), so retry until it takes.
fn exitBootServices(bs: *uefi.tables.BootServices) !void {
/// Fetch the memory map, exit boot services, and hand back the map in danos's
/// neutral form. Allocating the buffers can itself change the map (invalidating
/// the key), so retry until it takes. Both buffers are LoaderData, which survives
/// the exit, so the returned map stays valid for the kernel.
fn exitBootServices(bs: *uefi.tables.BootServices) !danos.MemoryMap {
var attempts: usize = 0;
while (attempts < 8) : (attempts += 1) {
const info = try bs.getMemoryMapInfo();
// Spare descriptors to absorb the growth from the allocatePool below.
const buf = try bs.allocatePool(.loader_data, (info.len + 8) * info.descriptor_size);
const map = bs.getMemoryMap(buf) catch {
_ = bs.freePool(buf.ptr) catch {};
// Spare descriptors to absorb the growth from the allocations below.
const cap = info.len + 8;
const map_buf = try bs.allocatePool(.loader_data, cap * info.descriptor_size);
const regions_buf = try bs.allocatePool(.loader_data, cap * @sizeOf(danos.MemoryRegion));
const map = bs.getMemoryMap(map_buf) catch {
_ = bs.freePool(map_buf.ptr) catch {};
_ = bs.freePool(regions_buf.ptr) catch {};
continue;
};
bs.exitBootServices(uefi.handle, map.info.key) catch {
_ = bs.freePool(buf.ptr) catch {};
_ = bs.freePool(map_buf.ptr) catch {};
_ = bs.freePool(regions_buf.ptr) catch {};
continue;
};
return; // Boot services are gone; do not touch `bs` again.
// Boot services are gone; do not touch `bs` again. Converting the map is
// pure computation on memory we already hold, so it's safe here.
return convertMemoryMap(map, regions_buf);
}
return error.ExitBootServicesFailed;
}
/// Translate UEFI's memory map into danos's neutral `MemoryRegion` array, written
/// into `out` (sized for at least `map.info.len` regions). Adjacent regions of
/// the same kind are coalesced. This is the loader's job precisely so the kernel
/// never sees UEFI's vocabulary — the same seam the framebuffer already uses.
fn convertMemoryMap(map: MemoryMapSlice, out: []u8) danos.MemoryMap {
const regions: [*]danos.MemoryRegion = @ptrCast(@alignCast(out.ptr));
var count: usize = 0;
var i: usize = 0;
while (i < map.info.len) : (i += 1) {
// Stride by descriptor_size, NOT @sizeOf — firmware descriptors may be
// larger than the struct.
const d: *const uefi.tables.MemoryDescriptor =
@ptrCast(@alignCast(map.ptr + i * map.info.descriptor_size));
if (d.number_of_pages == 0) continue;
const kind = classify(d.@"type");
// Coalesce with the previous region if it's the same kind and contiguous.
if (count > 0) {
const prev = &regions[count - 1];
if (prev.kind == kind and
prev.base + prev.pages * danos.page_size == d.physical_start)
{
prev.pages += d.number_of_pages;
continue;
}
}
regions[count] = .{
.base = d.physical_start,
.pages = d.number_of_pages,
.kind = kind,
};
count += 1;
}
return .{ .regions = @intFromPtr(regions), .len = count };
}
/// Map a UEFI memory type to danos's neutral kind. Anything we don't explicitly
/// recognise is treated as `reserved` — the safe default. Our own loader data
/// (the kernel image, these buffers) is LoaderData, which falls here too and so
/// stays reserved until the kernel decides to reclaim it.
fn classify(t: uefi.tables.MemoryType) danos.MemoryKind {
return switch (t) {
.conventional_memory => .usable,
.boot_services_code, .boot_services_data => .reclaimable,
.acpi_reclaim_memory => .acpi_tables,
.acpi_memory_nvs => .acpi_nvs,
else => .reserved,
};
}
/// Write a compile-time string to the console (best effort).
fn log(comptime msg: []const u8) void {
const out = uefi.system_table.con_out orelse return;
+14 -8
View File
@@ -1,5 +1,6 @@
const std = @import("std");
const danos = @import("danos");
const arch = @import("arch");
const console = @import("console.zig");
const BootInfo = danos.BootInfo;
@@ -33,15 +34,20 @@ fn kmain(boot_info: *const BootInfo) noreturn {
con.print(" pitch : {d} bytes\n", .{fb.pitch});
con.print(" format : {s}\n", .{@tagName(fb.format)});
con.print(" framebuffer: 0x{x:0>16}\n", .{fb.base});
// Summarise the physical memory the loader handed us. The array is danos's
// own MemoryRegion, so this is a plain slice — no firmware layout in sight.
const regions = @as([*]const danos.MemoryRegion, @ptrFromInt(boot_info.memory_map.regions))[0..boot_info.memory_map.len];
var usable_pages: u64 = 0;
for (regions) |r| {
if (r.kind == .usable) usable_pages += r.pages;
}
con.print(" mem regions: {d}\n", .{regions.len});
con.print(" usable RAM : {d} MiB\n", .{usable_pages * danos.page_size / (1024 * 1024)});
con.write("\nkernel initialised; nothing left to do, halting.\n");
hang();
}
/// Stop the CPU. `hlt` in a loop parks the core at near-zero power until the
/// next interrupt; we loop because `hlt` returns when one arrives.
fn hang() noreturn {
while (true) asm volatile ("hlt");
arch.halt();
}
/// Freestanding has no OS to receive a panic. Print it to the console (if it is
@@ -55,6 +61,6 @@ pub const panic = std.debug.FullPanic(struct {
con.write(msg);
con.write("\n");
}
hang();
arch.halt();
}
}.panic);
+41
View File
@@ -32,8 +32,49 @@ pub const Framebuffer = extern struct {
format: PixelFormat,
};
/// Page size the memory map is measured in. 4 KiB on every architecture danos
/// targets so far.
pub const page_size = 4096;
/// 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. [[arch]] keeps the same discipline for CPU code.
pub const MemoryKind = enum(u32) {
/// Free RAM the kernel may allocate.
usable,
/// Firmware, MMIO, the kernel image, our own boot buffers — never hand out.
reserved,
/// Usable once the kernel is done with boot-time structures (e.g. UEFI boot
/// services memory, which is free after ExitBootServices).
reclaimable,
/// ACPI tables: parse, then reclaim.
acpi_tables,
/// ACPI non-volatile storage: preserve across sleep, do not allocate.
acpi_nvs,
};
/// 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 `page_size` units
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,
};
/// Handoff structure the bootloader fills in and passes to the kernel's
/// `_start` in RDI (the first argument under the SysV AMD64 C ABI).
pub const BootInfo = extern struct {
framebuffer: Framebuffer,
memory_map: MemoryMap,
};