danos/boot/efi.zig

813 lines
37 KiB
Zig
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const std = @import("std");
const uefi = std.os.uefi;
const elf = std.elf;
const boot_handoff = @import("boot-handoff");
const initial_ramdisk = @import("initial-ramdisk");
const build_options = @import("build_options");
const BootInformation = boot_handoff.BootInformation;
const GraphicsOutput = uefi.protocol.GraphicsOutput;
const EdidActive = uefi.protocol.edid.Active;
const MemoryMapSlice = uefi.tables.MemoryMapSlice;
// The boot volume is the FHS-shaped zig-out (see build.zig / docs/README.md), so the
// loader reads each artifact from its addressed FHS path. UEFI paths use backslashes;
// the FAT driver walks the components itself, so no per-directory dance is needed.
/// The kernel image: /system/kernel.
const kernel_file_name = std.unicode.utf8ToUtf16LeStringLiteral("system\\kernel");
/// The user binaries: everything under /system except the kernel itself, plus
/// the test fixtures under /test. The loader walks both trees and packs them
/// into the in-RAM initial_ramdisk image — the volume's file structure is the
/// single source of truth (no packed image artifact on disk).
const system_directory_name = std.unicode.utf8ToUtf16LeStringLiteral("system");
const test_directory_name = std.unicode.utf8ToUtf16LeStringLiteral("test");
/// Physical page size, and the sentinel UEFI uses to seek to end-of-file.
const page_size = 4096;
const seek_end = 0xffff_ffff_ffff_ffff;
pub fn main() uefi.Status {
// `boot` never returns on success — it jumps into the kernel. If it fails,
// report the reason (boot services are still up) and park the machine so the
// message stays on screen.
boot() catch |err| {
log("\r\nEFI: boot failed: ");
logBytes(@errorName(err));
log("\r\n");
while (true) asm volatile ("hlt");
};
unreachable;
}
fn boot() !noreturn {
const bs = uefi.system_table.boot_services orelse return error.NoBootServices;
// Everything the kernel needs must be gathered *before* we exit boot
// services, since afterwards none of these calls are usable.
var boot_information: BootInformation = .{
// A missing GOP (a headless machine) is not fatal — hand the kernel a
// "no framebuffer" descriptor (base 0) and let it log to serial instead.
.framebuffer = queryFramebuffer(bs) catch boot_handoff.Framebuffer{
.base = 0,
.width = 0,
.height = 0,
.pitch = 0,
.format = .bgrx,
},
.memory_map = undefined, // filled by exitBootServices, just below
.kernel_segments = undefined, // filled by loadKernel
.kernel_segment_count = 0,
// Read the ACPI RSDP from the UEFI configuration table now, while boot
// services are still up. The pointer lives in ACPI reclaim memory, which
// the kernel identity-maps, so the physical address stays valid afterward.
.acpi_rsdp = if (acpiRootSystemDescriptorPointer()) |p| @intFromPtr(p) else 0,
};
const entry = try loadKernel(bs, &boot_information);
// Best effort: a volume without a /system tree of user binaries still boots
// (kernel-only). The tree — init included — becomes the initial_ramdisk.
loadSystemTree(bs, &boot_information) catch |err| {
log("EFI: no /system binaries (");
logBytes(@errorName(err));
log(") - booting without user space\r\n");
};
// Build the page tables the kernel starts life on: identity + a physmap of
// low RAM, plus the higher-half kernel image once it links high. Allocated
// now, while boot services (and the memory map) are still stable — nothing
// is allocatable after ExitBootServices, and any allocation between fetching
// the map and exiting would invalidate the map key.
const cr3 = try buildBootstrapTables(bs, &boot_information);
progress("EFI: kernel loaded, exiting boot services\r\n");
boot_information.memory_map = try exitBootServices(bs);
// Switch onto our tables and jump to the kernel in one uninterruptible step.
// We load RDI explicitly (SystemV first arg) rather than trusting this UEFI
// binary's Microsoft-x64 default, and jump straight to the (possibly
// higher-half) entry — the bootstrap tables map both the low loader code
// executing this and the kernel's link address.
handoff(cr3, entry, &boot_information);
}
/// A display resolution in pixels.
const Resolution = struct { width: u32, height: u32, refresh_hz: u32 };
/// Switch the GPU to the monitor's native resolution (when we can determine it)
/// and read the resulting graphics mode into our own framebuffer description.
fn queryFramebuffer(bs: *uefi.tables.BootServices) !boot_handoff.Framebuffer {
// Enumerate the handles carrying the Graphics Output Protocol. We go through
// handles (rather than locateProtocol) so we can also ask them for their EDID,
// which is what tells us the panel's native resolution.
const handles = (try bs.locateHandleBuffer(.{ .by_protocol = &GraphicsOutput.guid })) orelse
return error.NoGraphicsOutput;
defer _ = bs.freePool(@ptrCast(handles.ptr)) catch {};
const gop = (try bs.handleProtocol(GraphicsOutput, handles[0])) orelse
return error.NoGraphicsOutput;
// Best effort: the monitor's preferred (native) timing from its EDID.
const native = nativeResolution(bs, handles);
// Select the mode and switch to it if it isn't already current. setMode
// updates gop.mode (info and frame_buffer_base) to describe the new mode.
const target = pickMode(gop, native);
if (target != gop.mode.mode) try gop.setMode(target);
const info = gop.mode.info;
return .{
.base = @intCast(gop.mode.frame_buffer_base),
.width = info.horizontal_resolution,
.height = info.vertical_resolution,
// Each pixel is 32 bits, so the byte pitch is 4 * pixels-per-row.
.pitch = info.pixels_per_scan_line * 4,
.format = try pixelFormat(info.pixel_format),
// The refresh rate rides the EDID preferred timing. If the firmware kept a
// non-native mode it may not describe that mode exactly — but it is the panel's
// own clock, a far better frame-clock seed than a hardcoded 60 Hz.
.refresh_hz = if (native) |n| n.refresh_hz else 0,
};
}
/// Map a GOP pixel format to ours. bit_mask / blt_only have no linear 32bpp
/// layout we can paint into, so they're rejected.
fn pixelFormat(fmt: GraphicsOutput.PixelFormat) !boot_handoff.PixelFormat {
return switch (fmt) {
.red_green_blue_reserved_8_bit_per_color => .rgbx,
.blue_green_red_reserved_8_bit_per_color => .bgrx,
else => error.UnsupportedPixelFormat,
};
}
/// Choose the graphics mode to boot with. If we learned the monitor's native
/// resolution from EDID and a mode offers it (with a layout we can paint into),
/// use that. Otherwise keep whatever mode the firmware already selected: with a
/// valid EDID present the firmware normally defaults to the native mode itself,
/// so its default is a far safer bet than second-guessing it with, say, the
/// largest advertised mode (which is often a huge non-native surface).
fn pickMode(gop: *GraphicsOutput, native: ?Resolution) u32 {
const n = native orelse return gop.mode.mode;
var id: u32 = 0;
while (id < gop.mode.max_mode) : (id += 1) {
const info = gop.queryMode(id) catch continue;
_ = pixelFormat(info.pixel_format) catch continue; // must be paintable
if (info.horizontal_resolution == n.width and
info.vertical_resolution == n.height) return id;
}
return gop.mode.mode; // native not on offer; trust the firmware's default
}
/// The monitor's native resolution, read from an EDID's preferred timing. We try
/// every GOP handle and both EDID protocols (Active first, then Discovered),
/// since firmware installs them inconsistently — and many, including OVMF with
/// QEMU's stdvga, don't expose them at all. Returns null when none is found, in
/// which case pickMode keeps the firmware's default mode.
fn nativeResolution(bs: *uefi.tables.BootServices, handles: []uefi.Handle) ?Resolution {
for (handles) |h| {
if (bs.handleProtocol(EdidActive, h) catch null) |e| {
if (e.edid) |p| if (edidNative(p[0..e.size_of_edid])) |r| return r;
}
if (bs.handleProtocol(uefi.protocol.edid.Discovered, h) catch null) |e| {
if (e.edid) |p| if (edidNative(p[0..e.size_of_edid])) |r| return r;
}
}
return null;
}
/// Parse the native resolution and refresh rate from a raw EDID block. The first
/// Detailed Timing Descriptor (at byte 54) is the preferred — i.e. native — mode by
/// convention; its active pixel counts are split across low bytes and the high nibbles
/// of later bytes. The refresh rate is derived, not stored: the descriptor carries the
/// pixel clock (10 kHz units) and the active+blanking extents, and
/// refresh = clock / (horizontal total × vertical total).
fn edidNative(edid: []const u8) ?Resolution {
if (edid.len < 128) return null;
// Every EDID begins with this fixed 8-byte header.
const header = [_]u8{ 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00 };
if (!std.mem.eql(u8, edid[0..8], &header)) return null;
const dtd = edid[54..][0..18];
// A zero pixel clock marks a display (not timing) descriptor: no resolution.
if (dtd[0] == 0 and dtd[1] == 0) return null;
const w = @as(u32, dtd[2]) | (@as(u32, dtd[4] & 0xf0) << 4);
const h = @as(u32, dtd[5]) | (@as(u32, dtd[7] & 0xf0) << 4);
if (w == 0 or h == 0) return null;
const clock_hz = (@as(u64, dtd[0]) | (@as(u64, dtd[1]) << 8)) * 10_000;
const h_blank = @as(u64, dtd[3]) | (@as(u64, dtd[4] & 0x0f) << 8);
const v_blank = @as(u64, dtd[6]) | (@as(u64, dtd[7] & 0x0f) << 8);
const total = (@as(u64, w) + h_blank) * (@as(u64, h) + v_blank);
const refresh: u32 = if (total == 0) 0 else @intCast((clock_hz + total / 2) / total);
return .{ .width = w, .height = h, .refresh_hz = refresh };
}
/// Open the kernel on the volume we booted from, read it into a pool buffer,
/// load its segments, and return the physical entry-point address.
fn loadKernel(bs: *uefi.tables.BootServices, boot_information: *BootInformation) !usize {
const loaded = (try bs.handleProtocol(uefi.protocol.LoadedImage, uefi.handle)) orelse
return error.NoLoadedImage;
const device = loaded.device_handle orelse return error.NoBootDevice;
const fs = (try bs.handleProtocol(uefi.protocol.SimpleFileSystem, device)) orelse
return error.NoFileSystem;
const root = try fs.openVolume();
defer _ = root.close() catch {};
const file = try root.open(kernel_file_name, .read, .{});
defer _ = file.close() catch {};
// Seek to the end to learn the size, then rewind.
try file.setPosition(seek_end);
const size: usize = @intCast(try file.getPosition());
try file.setPosition(0);
const image = try bs.allocatePool(.loader_data, size);
defer _ = bs.freePool(image.ptr) catch {};
// `read` may return short; loop until the whole file is in memory.
var read_total: usize = 0;
while (read_total < size) {
const n = try file.read(image[read_total..]);
if (n == 0) return error.UnexpectedEof;
read_total += n;
}
return loadElf(bs, image, boot_information);
}
// --- bootstrap page tables -------------------------------------------------
// The kernel is (or will be) linked in the higher half but loaded low; the
// firmware's identity map doesn't cover the higher half, so the loader builds
// the first set of real page tables and switches CR3 before jumping in. They
// carry: an identity map of low RAM (so the loader's own code/stack executing
// the switch stays valid, and the low-linked kernel keeps working during the
// staged move), a physmap at boot_handoff.physmap_base (the kernel's permanent way to
// reach physical memory), and 4 KiB mappings of any higher-half kernel segment.
// The kernel later builds its own precise tables (paging.init) and abandons
// these; they leak as reserved LoaderData (~a handful of frames).
const pte_present: u64 = 1 << 0;
const pte_write: u64 = 1 << 1;
const pte_ps: u64 = 1 << 7; // page-size: a 2 MiB leaf at the PD level
const pte_address: u64 = 0x000F_FFFF_FFFF_F000;
const gib: u64 = 1 << 30;
/// A bump allocator over a pre-reserved block of zeroed frames, for page tables.
const TablePool = struct {
base: usize,
next: usize,
cap: usize,
fn alloc(self: *TablePool) !u64 {
if (self.next >= self.cap) return error.OutOfBootstrapFrames;
const frame = self.base + self.next * page_size;
self.next += 1;
@memset(@as(*[512]u64, @ptrFromInt(frame)), 0);
return frame;
}
fn table(physical: u64) *[512]u64 {
return @ptrFromInt(physical);
}
/// Return the next-level table an entry points at, creating it if absent.
fn descend(self: *TablePool, entry: *u64) !u64 {
if (entry.* & pte_present != 0) return entry.* & pte_address;
const frame = try self.alloc();
entry.* = frame | pte_present | pte_write;
return frame;
}
fn map2M(self: *TablePool, pml4: u64, virtual: u64, physical: u64) !void {
const pml4e = &table(pml4)[(virtual >> 39) & 0x1FF];
const pdpt = try self.descend(pml4e);
const pdpte = &table(pdpt)[(virtual >> 30) & 0x1FF];
const pd = try self.descend(pdpte);
table(pd)[(virtual >> 21) & 0x1FF] = (physical & ~@as(u64, 0x1F_FFFF)) | pte_present | pte_write | pte_ps;
}
fn map4K(self: *TablePool, pml4: u64, virtual: u64, physical: u64) !void {
const pml4e = &table(pml4)[(virtual >> 39) & 0x1FF];
const pdpt = try self.descend(pml4e);
const pdpte = &table(pdpt)[(virtual >> 30) & 0x1FF];
const pd = try self.descend(pdpte);
const pde = &table(pd)[(virtual >> 21) & 0x1FF];
const pt = try self.descend(pde);
table(pt)[(virtual >> 12) & 0x1FF] = (physical & pte_address) | pte_present | pte_write;
}
};
/// Build the bootstrap tables and return the physical PML4 address (for CR3).
/// No NX bits are set anywhere, so EFER.NXE (still off here) is irrelevant.
fn buildBootstrapTables(bs: *uefi.tables.BootServices, boot_information: *const BootInformation) !u64 {
// 64 frames (256 KiB) — comfortably covers a PML4, two PDPTs, eight PDs for
// the 4 GiB identity+physmap ranges, plus the kernel image's PTs.
const pool_pages = 64;
const block = try bs.allocatePages(.any, .loader_data, pool_pages);
var pool = TablePool{ .base = @intFromPtr(block.ptr), .next = 0, .cap = pool_pages };
const pml4 = try pool.alloc();
// Identity + physmap for low RAM. 4 GiB covers all of QEMU's RAM and MMIO
// (LAPIC/IOAPIC/HPET/ECAM/framebuffer under q35); a machine with RAM or a
// framebuffer above 4 GiB would extend this — see the fb window below.
var address: u64 = 0;
while (address < 4 * gib) : (address += 2 << 20) {
try pool.map2M(pml4, address, address); // identity
try pool.map2M(pml4, boot_handoff.physicalToVirtual(address), address); // physmap
}
// A framebuffer above the 4 GiB window needs its own identity + physmap
// pages (the kernel touches fb.base before it builds its own tables).
const fb = boot_information.framebuffer;
if (fb.present() and fb.base + @as(u64, fb.pitch) * fb.height > 4 * gib) {
var p: u64 = fb.base & ~@as(u64, 0x1F_FFFF);
const fb_end = fb.base + @as(u64, fb.pitch) * fb.height;
while (p < fb_end) : (p += 2 << 20) {
try pool.map2M(pml4, p, p);
try pool.map2M(pml4, boot_handoff.physicalToVirtual(p), p);
}
}
// Higher-half kernel segments (virtual != physical). While the kernel still links
// low its segments sit in the identity range and need no separate mapping
// (and 4 KiB-mapping them would collide with the 2 MiB identity leaves), so
// only map segments that actually live in the higher half.
for (boot_information.kernel_segments[0..boot_information.kernel_segment_count]) |seg| {
if (seg.virtual < boot_handoff.kernel_virt_base) continue;
var off: u64 = 0;
while (off < seg.pages * page_size) : (off += page_size) {
try pool.map4K(pml4, seg.virtual + off, seg.physical + off);
}
}
return pml4;
}
/// Switch onto `cr3` and jump to the kernel `entry` with `boot_information` in RDI,
/// interrupts off, in one block so nothing runs between the CR3 load and the
/// jump. The identity mapping keeps this low loader code valid across the CR3
/// load; the jump target is mapped (identity while low, higher-half once high).
fn handoff(cr3: u64, entry: usize, boot_information: *const BootInformation) noreturn {
asm volatile (
\\cli
\\movq %[cr3], %%cr3
\\movq %[bi], %%rdi
\\callq *%[entry]
:
: [cr3] "r" (cr3),
[bi] "r" (boot_information),
[entry] "r" (entry),
: .{ .memory = true });
unreachable;
}
// --- the /system and /test trees -> initial_ramdisk --------------------------
/// Cap on bundled binaries. Generous: the tree carries ~30 today.
const maximum_bundled = 64;
/// How deep the walk goes below a tree root ("/system/services/x" is depth 1,
/// "/test/system/services/x" is depth 2).
const maximum_tree_depth = 3;
/// One binary discovered under a walked tree: its FHS path (UTF-8,
/// '/'-separated, NUL-free) and its contents in a transient pool buffer.
const Bundled = struct {
path: [initial_ramdisk.maximum_name]u8,
path_len: usize,
data: []align(8) u8,
};
/// Gather the boot volume's user binaries into an in-RAM v2 initial_ramdisk
/// image, entries named by full FHS path — the volume's file structure is the
/// single source of truth (no packed ramdisk artifact; init travels in the
/// table like everything else).
///
/// Two strategies, most portable first:
/// 1. /system/manifest (written by the build): each listed path is opened BY
/// NAME — the case-insensitive lookup every firmware FAT driver gets
/// right, and the only file access the pre-tree loader ever used.
/// 2. No manifest: ENUMERATE the /system and /test trees. Portable in
/// principle, but firmware differs in what names enumeration returns
/// (bare 8.3 entries come back uppercase on some drivers), so this is
/// the fallback for hand-assembled sticks, not the primary path.
fn loadSystemTree(bs: *uefi.tables.BootServices, boot_information: *BootInformation) !void {
const loaded = (try bs.handleProtocol(uefi.protocol.LoadedImage, uefi.handle)) orelse
return error.NoLoadedImage;
const device = loaded.device_handle orelse return error.NoBootDevice;
const fs = (try bs.handleProtocol(uefi.protocol.SimpleFileSystem, device)) orelse
return error.NoFileSystem;
const root = try fs.openVolume();
defer _ = root.close() catch {};
// Unconditional breadcrumb (con_out, independent of -Dserial): this phase
// is where a slow firmware stalls, and a silent black screen here already
// cost a real-hardware debugging session.
log("EFI: loading the system...\r\n");
// The capsule (boot\system.img) first: one open + one sequential read is
// the only firmware file I/O shape that is fast everywhere. It is already
// the kernel's wire format — hand it over as-is.
if (loadCapsule(bs, root, boot_information)) {
log("EFI: system image loaded, starting the kernel\r\n");
return;
}
var list: [maximum_bundled]Bundled = undefined;
var count: usize = 0;
loadByManifest(bs, root, &list, &count) catch {
count = 0; // a torn manifest read leaves partial entries; start over
};
if (count == 0) {
const system_directory = try root.open(system_directory_name, .read, .{});
defer _ = system_directory.close() catch {};
try walkDirectory(bs, system_directory, "/system", 0, &list, &count);
// The /test tree is optional: a stick without fixtures still boots.
if (root.open(test_directory_name, .read, .{})) |test_directory| {
defer _ = test_directory.close() catch {};
try walkDirectory(bs, test_directory, "/test", 0, &list, &count);
} else |_| {}
}
if (count == 0) return error.NoBinaries;
// Assemble the v2 image: header, entry table, then the blobs.
const table_end = @sizeOf(initial_ramdisk.Header) + count * @sizeOf(initial_ramdisk.Entry);
var total: usize = table_end;
for (list[0..count]) |e| total += e.data.len;
const image = try bs.allocatePool(.loader_data, total); // survives the handoff
std.mem.bytesAsValue(initial_ramdisk.Header, image[0..@sizeOf(initial_ramdisk.Header)]).* = .{
.magic = initial_ramdisk.magic,
.count = @intCast(count),
};
var offset: usize = table_end;
for (list[0..count], 0..) |e, i| {
var record = initial_ramdisk.Entry{ .name = @splat(0), .offset = offset, .len = e.data.len };
@memcpy(record.name[0..e.path_len], e.path[0..e.path_len]);
const slot = image[@sizeOf(initial_ramdisk.Header) + i * @sizeOf(initial_ramdisk.Entry) ..][0..@sizeOf(initial_ramdisk.Entry)];
std.mem.bytesAsValue(initial_ramdisk.Entry, slot).* = record;
@memcpy(image[offset..][0..e.data.len], e.data);
offset += e.data.len;
_ = bs.freePool(e.data.ptr) catch {};
}
boot_information.initial_ramdisk_base = @intFromPtr(image.ptr);
boot_information.initial_ramdisk_len = total;
log("EFI: boot tree loaded, starting the kernel\r\n");
}
/// The boot capsule: the bundled binaries as one v2 initial_ramdisk image.
const capsule_file_name = std.unicode.utf8ToUtf16LeStringLiteral("boot\\system.img");
/// Load boot\system.img whole and hand it to the kernel unmodified — it is
/// already the initial_ramdisk wire format. Returns false (capsule absent or
/// unreadable or wrong magic) to let the caller fall back to per-file loading.
fn loadCapsule(bs: *uefi.tables.BootServices, root: *uefi.protocol.File, boot_information: *BootInformation) bool {
const file = root.open(capsule_file_name, .read, .{}) catch return false;
defer _ = file.close() catch {};
const image = readWholeFile(bs, file) catch return false;
if (image.len < @sizeOf(initial_ramdisk.Header) or
std.mem.bytesToValue(initial_ramdisk.Header, image[0..@sizeOf(initial_ramdisk.Header)]).magic != initial_ramdisk.magic)
{
_ = bs.freePool(image.ptr) catch {};
return false;
}
boot_information.initial_ramdisk_base = @intFromPtr(image.ptr);
boot_information.initial_ramdisk_len = image.len;
return true;
}
/// The manifest path, and a scratch limit for its UTF-16 conversion.
const manifest_file_name = std.unicode.utf8ToUtf16LeStringLiteral("system\\manifest");
/// Load every binary the manifest lists, opening each path by name from the
/// volume root. A listed-but-unopenable file is skipped (the kernel reports the
/// absence); a missing manifest errors so the caller falls back to the walk.
fn loadByManifest(bs: *uefi.tables.BootServices, root: *uefi.protocol.File, list: *[maximum_bundled]Bundled, count: *usize) !void {
const manifest_handle = try root.open(manifest_file_name, .read, .{});
var manifest_open = true;
defer if (manifest_open) {
_ = manifest_handle.close() catch {};
};
const manifest = try readWholeFile(bs, manifest_handle);
_ = manifest_handle.close() catch {};
manifest_open = false;
defer _ = bs.freePool(manifest.ptr) catch {};
var lines = std.mem.tokenizeAny(u8, manifest, "\r\n");
while (lines.next()) |line| {
if (line.len < 2 or line[0] != '/') continue;
if (line.len >= initial_ramdisk.maximum_name) continue;
if (count.* == maximum_bundled) return;
// "/system/services/init" -> UTF-16 "system\services\init".
var name16: [initial_ramdisk.maximum_name]u16 = undefined;
var i: usize = 0;
for (line[1..]) |c| {
name16[i] = if (c == '/') '\\' else c;
i += 1;
}
name16[i] = 0;
const file = root.open(@ptrCast(name16[0..i :0]), .read, .{}) catch continue;
defer _ = file.close() catch {};
const data = readWholeFile(bs, file) catch continue;
var entry: *Bundled = &list[count.*];
@memcpy(entry.path[0..line.len], line);
entry.path_len = line.len;
entry.data = data;
count.* += 1;
}
}
/// Recursively collect the regular files below `directory` into `list`. Top-level
/// files (depth 0) are skipped: the only one is /system/kernel, which loadKernel
/// has already consumed and which is not a spawnable user binary (/test has no
/// top-level files, so the skip is a no-op there).
fn walkDirectory(
bs: *uefi.tables.BootServices,
directory: *uefi.protocol.File,
prefix: []const u8,
depth: usize,
list: *[maximum_bundled]Bundled,
count: *usize,
) !void {
// Each read() on a directory yields one EFI_FILE_INFO; zero bytes means done.
var info_buffer: [1024]u8 align(8) = undefined;
while (true) {
const n = try directory.read(&info_buffer);
if (n == 0) return;
const info: *const uefi.protocol.File.Info.File = @ptrCast(@alignCast(&info_buffer));
const name16 = info.getFileName();
// Convert the (ASCII in practice) UTF-16 name. A hostile-shaped entry
// (too long, non-ASCII) is SKIPPED, never fatal — one odd file on a
// hand-written stick must not cost the whole boot. Names are lowered:
// the danos tree is canonically lowercase and FAT lookups are
// case-insensitive, but firmware ENUMERATION returns whatever the
// directory stores — an 8.3 short entry comes back uppercase ("INIT"),
// which would otherwise poison every path comparison downstream.
var name_buffer: [initial_ramdisk.maximum_name]u8 = undefined;
var name_length: usize = 0;
var name_ok = true;
while (name16[name_length] != 0) : (name_length += 1) {
if (name_length == name_buffer.len) {
name_ok = false;
break;
}
const c = name16[name_length];
if (c > 0x7F) {
name_ok = false;
break;
}
name_buffer[name_length] = std.ascii.toLower(@intCast(c));
}
if (!name_ok) continue;
const name = name_buffer[0..name_length];
// Skip dot entries: "." / ".." and host-OS litter (macOS "._*" AppleDouble
// resource forks, ".fseventsd", ".Spotlight-V100") a copied-onto stick
// accumulates — none of it is a danos binary.
if (name.len == 0 or name[0] == '.') continue;
if (info.attribute.directory) {
if (depth == maximum_tree_depth) continue;
var child_prefix: [initial_ramdisk.maximum_name]u8 = undefined;
const child = try std.fmt.bufPrint(&child_prefix, "{s}/{s}", .{ prefix, name });
const child_directory = try directory.open(name16, .read, .{});
defer _ = child_directory.close() catch {};
try walkDirectory(bs, child_directory, child, depth + 1, list, count);
continue;
}
if (depth == 0) continue; // /system/kernel — already loaded, not bundled
if (count.* == maximum_bundled) return error.TooManyBinaries;
var entry: *Bundled = &list[count.*];
const path = std.fmt.bufPrint(&entry.path, "{s}/{s}", .{ prefix, name }) catch continue; // path too long: skip the file, keep the boot
entry.path_len = path.len;
const file = directory.open(name16, .read, .{}) catch continue;
defer _ = file.close() catch {};
entry.data = readWholeFile(bs, file) catch continue; // unreadable/empty: skip
count.* += 1;
}
}
/// Read an open file completely into a fresh pool buffer that survives the
/// handoff (LoaderData is classified reserved, so the kernel identity-maps it).
fn readWholeFile(bs: *uefi.tables.BootServices, file: *uefi.protocol.File) ![]align(8) u8 {
try file.setPosition(seek_end);
const size: usize = @intCast(try file.getPosition());
try file.setPosition(0);
if (size == 0) return error.EmptyFile;
const buffer = try bs.allocatePool(.loader_data, size);
var read_total: usize = 0;
while (read_total < size) {
const n = try file.read(buffer[read_total..]);
if (n == 0) return error.UnexpectedEof;
read_total += n;
}
return buffer[0..size];
}
/// Validate the ELF, copy every PT_LOAD segment to its physical address, and
/// record each segment's layout so the kernel can re-map itself with the right
/// permissions.
fn loadElf(bs: *uefi.tables.BootServices, image: []u8, boot_information: *BootInformation) !usize {
if (image.len < @sizeOf(elf.Elf64_Ehdr)) return error.NotElf;
const ehdr: *const elf.Elf64_Ehdr = @ptrCast(@alignCast(image.ptr));
if (ehdr.e_ident[0] != 0x7f or ehdr.e_ident[1] != 'E' or
ehdr.e_ident[2] != 'L' or ehdr.e_ident[3] != 'F') return error.NotElf;
if (ehdr.e_machine != .X86_64) return error.WrongArchitecture;
var i: usize = 0;
while (i < ehdr.e_phnum) : (i += 1) {
const phdr: *const elf.Elf64_Phdr = @ptrCast(@alignCast(
image.ptr + ehdr.e_phoff + i * ehdr.e_phentsize,
));
if (phdr.p_type != elf.PT_LOAD) continue;
// Reserve the exact physical pages this segment is linked at. This
// requires the segment's p_paddr to be free in the firmware memory map;
// if it collides, adjust `image_base` in build.zig. (Once the kernel
// links high — M2 step 4 — p_paddr becomes a separate low load address
// via the linker's AT(), and this stays a valid physical allocation.)
const mem_sz: usize = @intCast(phdr.p_memsz);
const pages = (mem_sz + page_size - 1) / page_size;
const dest: [*]align(page_size) uefi.Page = @ptrFromInt(phdr.p_paddr);
_ = try bs.allocatePages(.{ .address = dest }, .loader_data, pages);
// Copy the file-backed part, then zero the .bss tail (memsz > filesz).
const bytes: [*]u8 = @ptrFromInt(phdr.p_paddr);
const file_sz: usize = @intCast(phdr.p_filesz);
const off: usize = @intCast(phdr.p_offset);
@memcpy(bytes[0..file_sz], image[off..][0..file_sz]);
@memset(bytes[file_sz..mem_sz], 0);
// Record the virtual link address and the physical load address so the
// kernel can map itself with the right permissions post-switch. They're
// equal while the kernel links low; they diverge once it links high.
const n = boot_information.kernel_segment_count;
if (n < boot_information.kernel_segments.len) {
boot_information.kernel_segments[n] = .{
.virtual = phdr.p_vaddr,
.physical = phdr.p_paddr,
.pages = pages,
.flags = phdr.p_flags,
};
boot_information.kernel_segment_count = n + 1;
}
}
return @intCast(ehdr.e_entry);
}
/// 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) !boot_handoff.MemoryMap {
var attempts: usize = 0;
while (attempts < 8) : (attempts += 1) {
const info = try bs.getMemoryMapInfo();
// Spare descriptors to absorb the growth from the allocations below.
const cap = info.len + 8;
const map_buffer = try bs.allocatePool(.loader_data, cap * info.descriptor_size);
const regions_buffer = try bs.allocatePool(.loader_data, cap * @sizeOf(boot_handoff.MemoryRegion));
const map = bs.getMemoryMap(map_buffer) catch {
_ = bs.freePool(map_buffer.ptr) catch {};
_ = bs.freePool(regions_buffer.ptr) catch {};
continue;
};
bs.exitBootServices(uefi.handle, map.info.key) catch {
_ = bs.freePool(map_buffer.ptr) catch {};
_ = bs.freePool(regions_buffer.ptr) catch {};
continue;
};
// 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_buffer);
}
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) boot_handoff.MemoryMap {
const regions: [*]boot_handoff.MemoryRegion = @ptrCast(@alignCast(out.ptr));
// We're about to call boot-services memory `usable`, but our own stack lives
// in it and the kernel starts out running on it. Keep the region holding the
// current stack pointer reserved so it's never handed out.
const rsp = asm volatile ("mov %%rsp, %[out]"
: [out] "=r" (-> usize),
);
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;
var kind = classify(d);
// The descriptor we're executing on stays reserved (see rsp above).
const region_end = d.physical_start + d.number_of_pages * page_size;
if (kind == .usable and rsp >= d.physical_start and rsp < region_end) kind = .reserved;
// Coalesce with the previous region if it's the same kind and contiguous.
if (count > 0) {
const previous = &regions[count - 1];
if (previous.kind == kind and
previous.base + previous.pages * page_size == d.physical_start)
{
previous.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 descriptor to danos's neutral kind. A region that isn't
/// writeback-cacheable (`wb`) isn't backed by real RAM — it's device registers or
/// a reserved address-space window (e.g. PCIe configuration space) — so it's `mmio`
/// regardless of type. UEFI overloads `reserved_memory_type` for both reserved RAM
/// and such holes, and the cache attribute is what actually tells them apart.
///
/// Boot-services memory is folded straight into `usable`: we've already called
/// ExitBootServices, so it's free RAM now — the kernel never needs to know it was
/// ever the firmware's (the one live piece, our stack, is reserved by the caller).
/// Anything unrecognised is `reserved` — the safe default; our own LoaderData (the
/// kernel image and these buffers) lands there and stays reserved.
fn classify(d: *const uefi.tables.MemoryDescriptor) boot_handoff.MemoryKind {
if (!d.attribute.wb) return .mmio;
return switch (d.type) {
.conventional_memory, .boot_services_code, .boot_services_data => .usable,
.acpi_reclaim_memory => .acpi_tables,
.acpi_memory_nvs => .acpi_nvs,
.memory_mapped_io, .memory_mapped_io_port_space => .mmio,
else => .reserved,
};
}
/// Write a compile-time string to the console (best effort).
fn log(comptime message: []const u8) void {
const out = uefi.system_table.con_out orelse return;
_ = out.outputString(std.unicode.utf8ToUtf16LeStringLiteral(message)) catch {};
}
/// A boot-progress breadcrumb: like `log`, but compiled out unless `-Dserial`
/// (off by default), so a real-hardware boot stays silent. Fatal errors use
/// `log` directly and always show, so a failed boot still explains itself.
fn progress(comptime message: []const u8) void {
if (!build_options.serial) return;
log(message);
}
/// Write a runtime ASCII byte string (e.g. an @errorName) by widening to UTF-16.
fn logBytes(bytes: []const u8) void {
const out = uefi.system_table.con_out orelse return;
var buffer: [128]u16 = undefined;
var i: usize = 0;
for (bytes) |b| {
if (i + 1 >= buffer.len) break;
buffer[i] = b;
i += 1;
}
buffer[i] = 0;
_ = out.outputString(buffer[0..i :0].ptr) catch {};
}
fn acpiRootSystemDescriptorPointer() ?*const anyopaque {
const table_entries = uefi.system_table.number_of_table_entries;
const configuration_tables = uefi.system_table.configuration_table;
const acpi2 = uefi.tables.ConfigurationTable.acpi_20_table_guid;
const acpi1 = uefi.tables.ConfigurationTable.acpi_10_table_guid;
for (0..table_entries) |i| {
const entry = configuration_tables[i];
if (entry.vendor_guid.eql(acpi2) or entry.vendor_guid.eql(acpi1)) {
return entry.vendor_table;
}
}
return null;
}