diff --git a/README.md b/README.md index d7941b2..b072078 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,9 @@ -DanOS -==== +# DanOS +Codename: Shodan +Version: 1 -Operating System written for me \ No newline at end of file +Operating System written for me + +## Logo + +San Serif Text "Dan OS" with a black karate belt around it. diff --git a/build.zig b/build.zig index 1076ea6..4659110 100644 --- a/build.zig +++ b/build.zig @@ -11,6 +11,13 @@ pub fn build(b: *std.Build) void { .root_source_file = b.path("src/root.zig"), }); + // Architecture-specific kernel code (CPU ops, entry, later GDT/IDT/paging). + // The generic kernel imports this as "arch" and never names x86_64, so a new + // architecture is a matter of pointing this module at a different directory. + const arch_mod = b.addModule("arch", .{ + .root_source_file = b.path("src/arch/x86_64/cpu.zig"), + }); + // --- Kernel: freestanding x86_64 ELF, jumped to by the bootloader --- // SSE2 is part of the x86_64 baseline and UEFI leaves it enabled at handoff, // so we keep it: disabling it forces soft-float and makes the compiler unable @@ -35,10 +42,11 @@ pub fn build(b: *std.Build) void { .stack_protector = false, .imports = &.{ .{ .name = "danos", .module = mod }, + .{ .name = "arch", .module = arch_mod }, }, }), }); - exe.setLinkerScript(b.path("src/linker.ld")); + exe.setLinkerScript(b.path("src/arch/x86_64/linker.ld")); exe.entry = .{ .symbol_name = "_start" }; // Physical address the bootloader loads the kernel to (identity-mapped under // UEFI). Overrides Zig's default image base so the linker script's layout is diff --git a/docs/README.md b/docs/README.md index 484ea3a..16ef352 100644 --- a/docs/README.md +++ b/docs/README.md @@ -13,22 +13,34 @@ rather than restate it. Roughly in the order things happen at runtime: 3. **[framebuffer.md](framebuffer.md) — the framebuffer.** What the linear framebuffer the loader hands over actually is, and what **pitch** (stride) means versus width — the detail you have to get right to avoid a skewed image. -4. **[halting.md](halting.md) — halting.** Why a kernel can't just "exit", and +4. **[memory-map.md](memory-map.md) — the memory map.** How the loader learns what + physical RAM exists and hands it to the kernel in danos's own neutral format, + rather than leaking UEFI's memory descriptors across the boundary. +5. **[halting.md](halting.md) — halting.** Why a kernel can't just "exit", and how `while (true) hlt` parks the CPU safely once there's nothing left to do. +Cutting across all of these: + +- **[arch.md](arch.md) — the architecture split.** How CPU-specific code is kept + behind a build-time `arch` module so the generic kernel never names x86_64, + leaving room for other systems (e.g. an AArch64 Raspberry Pi) later. + ## How the pieces relate The boot flow ties them together: UEFI runs the loader ([efi.md](efi.md)), which -queries the **GOP** to pick a graphics mode ([gop.md](gop.md)) and hands the -kernel a **framebuffer** to draw into ([framebuffer.md](framebuffer.md)); when the -kernel has finished — or panics — it **halts** ([halting.md](halting.md)). +queries the **GOP** to pick a graphics mode ([gop.md](gop.md)), hands the kernel a +**framebuffer** to draw into ([framebuffer.md](framebuffer.md)) and a **memory +map** of physical RAM ([memory-map.md](memory-map.md)); the kernel runs — its +CPU-specific bits behind the [arch](arch.md) boundary — and when it has finished, +or panics, it **halts** ([halting.md](halting.md)). ## Source map | Area | Code | |------|------| | Bootloader (UEFI app) | `src/efi.zig` → `BOOTX64.efi` | -| Kernel entry, panic, halt | `src/main.zig` | -| Shared loader↔kernel contract (`BootInfo`, `Framebuffer`, ABI) | `src/root.zig` | +| Kernel entry, panic, memory-map read-out | `src/main.zig` | +| Shared loader↔kernel contract (`BootInfo`, `Framebuffer`, `MemoryMap`, ABI) | `src/root.zig` | | Framebuffer text console | `src/console.zig` | +| Arch-specific kernel code (`halt`, linker script) | `src/arch/x86_64/` | | Build + `run-efi` (QEMU/OVMF) | `build.zig` | diff --git a/docs/arch.md b/docs/arch.md new file mode 100644 index 0000000..385ef33 --- /dev/null +++ b/docs/arch.md @@ -0,0 +1,91 @@ +# Architecture split + +danos targets x86_64 today, but is meant to grow onto other systems later — a +Raspberry Pi, say, which is AArch64 and has no UEFI. To keep that possible without +a rewrite, CPU-specific kernel code lives behind a boundary: the generic kernel +never names an architecture, and each architecture plugs in behind it. + +## The seam is a build-time module named `arch` + +The mechanism is deliberately boring — no vtables, no function-pointer tables, no +runtime dispatch. `build.zig` exposes one architecture's code as a module called +`arch`: + +```zig +const arch_mod = b.addModule("arch", .{ + .root_source_file = b.path("src/arch/x86_64/cpu.zig"), +}); +``` + +and the generic kernel imports it by that name: + +```zig +const arch = @import("arch"); +// ... +arch.halt(); // never says "x86_64" +``` + +Adding a second architecture is then a build-time choice: create +`src/arch/aarch64/`, and point the `arch` module at it when the target CPU is +AArch64. `main.zig` and `console.zig` don't change. **That compiler-checked module +boundary _is_ the architecture interface** — when a new arch is missing a function +the generic kernel calls, the build fails and names exactly what's missing. + +## What's arch-specific vs generic + +The split follows a simple test: does it name a CPU instruction, a hardware +register, or a memory-management structure? If so, it's arch-specific. + +| Arch-specific — `src/arch/x86_64/` | Generic — kernel core | +|---|---| +| `cpu.zig`: `halt()` (`hlt`), later GDT/IDT/paging | `console.zig` — pure pixel math, works anywhere | +| `linker.ld` — link layout, load address | `main.zig` — `kmain` orchestration, panic handler | +| (future) interrupt controller, MMU setup | `root.zig` — the neutral handoff contract | + +Notice the framebuffer console is *generic*: it just writes pixels into whatever +framebuffer it's handed, so it needs no per-arch version. Most of the kernel +should end up on the generic side; the arch module stays small. + +## Two axes, kept separate + +There are really two independent questions, and it's worth not conflating them: + +- **CPU architecture** (x86_64 vs AArch64): instructions, MMU, interrupts → + `src/arch//`. +- **Boot protocol** (UEFI vs Raspberry Pi firmware + device tree): handled + *separately*, because loaders are their own binaries. `src/efi.zig` builds + `BOOTX64.efi`, a distinct executable from the kernel ELF. On a Pi there is no + separate loader at all — the firmware jumps straight into the kernel with a + device-tree pointer, so that entry work would live in the AArch64 arch code. + Either path converges on the same neutral [`BootInfo`](memory-map.md). + +## Current x86_64 contents + +- **`src/arch/x86_64/cpu.zig`** — the `arch` module root. Exposes `halt()` (see + [halting.md](halting.md)); GDT, IDT and paging will join it here as the kernel + grows. +- **`src/arch/x86_64/linker.ld`** — the kernel link layout (fixed low load + address, one PT_LOAD per permission set). + +The kernel entry point `_start` currently still lives in the generic `main.zig` as +a thin trampoline into `kmain`. It's arch-adjacent (its calling convention is +x86_64 SysV, via the shared `danos.kernel_abi`), but it's three lines and mostly +generic, so it stays put for now. When AArch64 arrives — where entry means setting +up a stack and reading a device-tree pointer from a register — the entry work will +be substantial and per-arch, and *that* is when we extract an entry interface into +the arch modules. + +## The discipline + +The thing that makes this help rather than hurt: **only extract what's provably +architecture-specific, and let the interface emerge with the second +implementation.** With a single architecture you're guessing at the seam, and a +wrong guess encoded as elaborate abstraction is expensive to undo. So: + +- Move code into `arch/` only when it genuinely names CPU-specific machinery. +- Grow the `arch` surface one function at a time, as steps need it. +- Don't pre-design the interrupt or paging interfaces before writing them. + +Directory hygiene is cheap and reversible; premature abstraction is neither. When +arch #2 lands and something doesn't fit, reshaping a few hundred lines is nothing — +unwinding an abstraction empire is not. diff --git a/docs/halting.md b/docs/halting.md index 1b29caa..05e8b39 100644 --- a/docs/halting.md +++ b/docs/halting.md @@ -15,12 +15,14 @@ safely, until the machine is reset or powered off. ## The core of it: `hlt` -Everything comes down to one x86 instruction. In `src/main.zig`: +Everything comes down to one x86 instruction. It's CPU-specific, so it lives in +the arch module, `src/arch/x86_64/cpu.zig` (see [arch.md](arch.md)), and the +generic kernel calls it as `arch.halt()`: ```zig -/// 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 { +/// 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. +pub fn halt() noreturn { while (true) asm volatile ("hlt"); } ``` @@ -71,7 +73,7 @@ makes us robust to all of them.) ## `noreturn`: telling the compiler it's the end -`hang()` is typed `noreturn` — a real Zig type meaning "this function never gives +`halt()` is typed `noreturn` — a real Zig type meaning "this function never gives control back to its caller." That isn't decoration; it changes how the compiler treats the call: @@ -82,26 +84,27 @@ treats the call: jumps back out. You can see the chain in `src/main.zig`: `_start` is `noreturn`, it calls -`kmain` which is `noreturn`, which ends by calling `hang()` which is `noreturn`. -The "never returns" property is threaded all the way down. +`kmain` which is `noreturn`, which ends by calling `arch.halt()` which is +`noreturn`. The "never returns" property is threaded all the way down. ## Where danos halts There are three halt sites, and they're all the same idea: -1. **Normal end of kernel work** — `kmain` prints its status, then calls `hang()`: +1. **Normal end of kernel work** — `kmain` prints its status, then calls + `arch.halt()`: ```zig con.write("\nkernel initialised; nothing left to do, halting.\n"); - hang(); + arch.halt(); ``` There's genuinely nothing more to do yet, so the kernel parks itself. 2. **Kernel panic** — the freestanding panic handler has no OS to report to, so it prints the message in red (if the console is up) and halts via the same - `hang()`. A panic is unrecoverable here, so stopping the machine — rather than - limping on with corrupted state — is the safe response. + `arch.halt()`. A panic is unrecoverable here, so stopping the machine — rather + than limping on with corrupted state — is the safe response. 3. **Bootloader failure** — in `src/efi.zig`, if `boot()` fails *before* handing off to the kernel, `main` logs the error and parks the machine with the same @@ -116,8 +119,8 @@ There are three halt sites, and they're all the same idea: }; ``` - (Here it's an inline loop rather than `hang()` because `hang` lives in the - kernel, which the loader is a separate binary from.) + (Here it's an inline loop rather than `arch.halt()` because that lives in the + kernel's arch module, and the loader is a separate binary from the kernel.) ## Summary diff --git a/docs/memory-map.md b/docs/memory-map.md new file mode 100644 index 0000000..8036d81 --- /dev/null +++ b/docs/memory-map.md @@ -0,0 +1,144 @@ +# The memory map + +Before a kernel can manage memory, it has to *know what memory exists*: which +physical address ranges are real RAM it may use, and which are firmware, hardware +registers, or already occupied. That inventory is the **memory map**, and the +firmware is the only thing that knows it. This page covers how danos gets that map +from the firmware and hands it to the kernel — deliberately without dragging UEFI +into the kernel. + +## Why not just pass UEFI's map through? + +UEFI hands the loader a perfectly good memory map. The tempting shortcut is to +forward it to the kernel as-is. We don't, for two reasons: + +1. **It would tie the kernel to UEFI.** The kernel would compare against UEFI's + memory-type numbers and walk the array using UEFI's variable descriptor stride. + That's UEFI vocabulary bleeding across the handoff — and danos wants to boot on + systems that have no UEFI at all (a Raspberry Pi describes its memory with a + *device tree* instead). See [arch.md](arch.md) for the same "keep the kernel + platform-agnostic" principle applied to CPU code. +2. **We already established the better pattern.** The loader doesn't hand the + kernel a raw UEFI GOP either — [`queryFramebuffer`](gop.md) converts it to + danos's own `Framebuffer`. The memory map follows the same discipline. + +So the boundary is: **each boot path translates its native memory description into +danos's own neutral format, and the kernel only ever sees that.** + +## The neutral format + +Defined in `src/root.zig`, the shared loader↔kernel contract: + +```zig +pub const MemoryKind = enum(u32) { + usable, // free RAM the kernel may allocate + reserved, // firmware / MMIO / kernel image — never hand out + reclaimable, // usable once boot-time structures are done with + acpi_tables, // parse, then reclaim + acpi_nvs, // preserve across sleep +}; + +pub const MemoryRegion = extern struct { + base: u64, // physical start + pages: u64, // length in page_size (4 KiB) units + kind: MemoryKind, + _pad: u32 = 0, +}; + +pub const MemoryMap = extern struct { + regions: usize, // pointer to a [len]MemoryRegion + len: usize, +}; +``` + +`MemoryKind` is danos's *own* vocabulary — not UEFI's ~15 types, just the +distinctions the kernel actually acts on. And because danos defines `MemoryRegion` +itself, `@sizeOf` is authoritative: the kernel walks a plain `[]MemoryRegion` with +no variable-stride subtlety (that stride problem is a UEFI-ism, and it stays in the +loader). + +`BootInfo` carries it alongside the framebuffer: + +```zig +pub const BootInfo = extern struct { + framebuffer: Framebuffer, + memory_map: MemoryMap, +}; +``` + +## The loader side (UEFI) + +Two functions in `src/efi.zig`, called from `exitBootServices`: + +- **`classify`** maps each UEFI memory type to a `MemoryKind`: + `conventional_memory → usable`; `boot_services_code`/`boot_services_data → + reclaimable` (free once we've exited); `acpi_reclaim_memory → acpi_tables`; + `acpi_memory_nvs → acpi_nvs`; **everything else → reserved** (the safe default). + Our own `loader_data` — the kernel image and these buffers — falls into + `reserved`, so it won't be handed out until the kernel deliberately reclaims it. +- **`convertMemoryMap`** walks the UEFI descriptors (striding by + `descriptor_size`, *not* `@sizeOf`), classifies each, and writes danos + `MemoryRegion`s into an output buffer, coalescing adjacent same-kind regions. + +### The ordering that makes it correct + +This is the fiddly part, dictated by two UEFI rules: you can only allocate memory +*before* `ExitBootServices`, and the memory map is only final *at* the moment you +exit (its "key" proves you've seen the latest state). So `exitBootServices` does, +per attempt: + +1. `getMemoryMapInfo` to size things, then `allocatePool` **two** LoaderData + buffers — one for the raw UEFI map, one for the converted regions. Allocating + now, before exit, is mandatory. +2. `getMemoryMap` then `exitBootServices(key)`. If either fails (allocating can + perturb the map and invalidate the key), free both buffers and retry. +3. **After** the exit succeeds, convert. Conversion is pure computation on memory + we already hold — no boot-services calls — so it's safe once services are gone. + +Both buffers are `LoaderData`, which survives `ExitBootServices`, so the converted +array the kernel is pointed at stays valid. (The raw UEFI buffer is just scratch +for the conversion.) + +## The kernel side + +The kernel receives a plain array and reads it with zero UEFI knowledge: + +```zig +const mm = boot_info.memory_map; +const regions = @as([*]const danos.MemoryRegion, @ptrFromInt(mm.regions))[0..mm.len]; +for (regions) |r| { + if (r.kind == .usable) usable_pages += r.pages; +} +``` + +Today `kmain` just prints the region count and total usable RAM — enough to prove +the handoff works. Booted in QEMU with 128 MiB, it reports something like: + +``` +mem regions: 35 +usable RAM : 77 MiB +``` + +with the balance being `reclaimable` boot-services memory (~44 MiB) and a large +`reserved` span that is mostly MMIO address space, not RAM. Those figures summing +back to ~128 MiB is the sanity check that nothing was dropped. + +## How Raspberry Pi will fit + +No UEFI there, but the boundary is unchanged. The Pi's firmware jumps into the +kernel with a **device-tree blob**; the AArch64 entry code will parse its +`/memory` and `/reserved-memory` nodes and produce the *same* `MemoryRegion` +array. The kernel's memory code — the frame allocator and everything above it — +never knows the difference. + +## What's next (not done here) + +This is plumbing plus classification only. Still to come: + +- A **physical frame allocator** that consumes `usable` regions and hands out + 4 KiB frames — the foundation everything else stands on. +- Reclaiming `reclaimable` regions, and carefully freeing `reserved` `loader_data` + (kernel image, these buffers) once the kernel is done reading them. +- Paging / the kernel's own page tables, then a heap. + +See the roadmap in [efi.md](efi.md) for where this sits in the boot flow. diff --git a/src/arch/x86_64/cpu.zig b/src/arch/x86_64/cpu.zig new file mode 100644 index 0000000..375f656 --- /dev/null +++ b/src/arch/x86_64/cpu.zig @@ -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"); +} diff --git a/src/linker.ld b/src/arch/x86_64/linker.ld similarity index 100% rename from src/linker.ld rename to src/arch/x86_64/linker.ld diff --git a/src/efi.zig b/src/efi.zig index 8473572..b16308e 100644 --- a/src/efi.zig +++ b/src/efi.zig @@ -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 = ®ions[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; diff --git a/src/main.zig b/src/main.zig index 1e9c75e..c906a59 100644 --- a/src/main.zig +++ b/src/main.zig @@ -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); diff --git a/src/root.zig b/src/root.zig index 4fa4eec..e438c99 100644 --- a/src/root.zig +++ b/src/root.zig @@ -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, };