booting into the kernel
This commit is contained in:
parent
3c8656fb25
commit
ca3c3d131c
53
build.zig
53
build.zig
|
|
@ -4,22 +4,46 @@ pub fn build(b: *std.Build) void {
|
|||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
// Shared handoff definitions (BootInfo, Framebuffer, ...). No target is set,
|
||||
// so the module inherits the target of whichever binary imports it — the
|
||||
// freestanding kernel or the UEFI bootloader.
|
||||
const mod = b.addModule("danos", .{
|
||||
.root_source_file = b.path("src/root.zig"),
|
||||
.target = target,
|
||||
});
|
||||
|
||||
// --- 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
|
||||
// to encode the vector ops that std's formatting/runtime still emit.
|
||||
const kernel_target = b.resolveTargetQuery(.{
|
||||
.cpu_arch = .x86_64,
|
||||
.os_tag = .freestanding,
|
||||
.abi = .none,
|
||||
});
|
||||
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "danos",
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = target,
|
||||
.target = kernel_target,
|
||||
.optimize = optimize,
|
||||
.code_model = .small, // kernel is linked in the low 2 GiB (see image_base)
|
||||
.red_zone = false, // interrupts would corrupt the SysV red zone
|
||||
.single_threaded = true, // no scheduler yet; avoids pulling in TLS/atomics
|
||||
.sanitize_c = .off, // the UBSan runtime needs f128/SSE support we don't provide
|
||||
.stack_check = false, // stack-probe calls have no runtime to land in
|
||||
.stack_protector = false,
|
||||
.imports = &.{
|
||||
.{ .name = "danos", .module = mod },
|
||||
},
|
||||
}),
|
||||
});
|
||||
exe.setLinkerScript(b.path("src/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
|
||||
// honoured; adjust here if it collides with firmware-reserved memory.
|
||||
exe.image_base = 0x100000; // 1 MiB
|
||||
|
||||
b.installArtifact(exe);
|
||||
|
||||
|
|
@ -32,6 +56,9 @@ pub fn build(b: *std.Build) void {
|
|||
.os_tag = .uefi,
|
||||
}),
|
||||
.optimize = optimize,
|
||||
.imports = &.{
|
||||
.{ .name = "danos", .module = mod },
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
@ -55,6 +82,11 @@ pub fn build(b: *std.Build) void {
|
|||
const efi_install = b.addInstallArtifact(efiexe, .{
|
||||
.dest_dir = .{ .override = .{ .custom = "esp/EFI/BOOT" } },
|
||||
});
|
||||
// The bootloader loads the kernel by name from the volume root, so drop the
|
||||
// kernel ELF at esp/danos.
|
||||
const kernel_install = b.addInstallArtifact(exe, .{
|
||||
.dest_dir = .{ .override = .{ .custom = "esp" } },
|
||||
});
|
||||
|
||||
// The firmware needs to write NVRAM, so give it a writable copy of the vars.
|
||||
const vars_copy = b.addSystemCommand(&.{ "cp", "-f", ovmf_vars });
|
||||
|
|
@ -79,6 +111,7 @@ pub fn build(b: *std.Build) void {
|
|||
"none",
|
||||
});
|
||||
run_efi.step.dependOn(&efi_install.step);
|
||||
run_efi.step.dependOn(&kernel_install.step);
|
||||
|
||||
const run_efi_step = b.step("run-efi", "Boot the EFI app in QEMU (OVMF)");
|
||||
run_efi_step.dependOn(&run_efi.step);
|
||||
|
|
@ -92,19 +125,19 @@ pub fn build(b: *std.Build) void {
|
|||
// run_cmd.addArgs(args);
|
||||
// }
|
||||
|
||||
// Tests run on the host. The kernel and bootloader target freestanding/UEFI
|
||||
// and can't be executed natively, so only the shared module is unit-tested
|
||||
// here (compiled for the host rather than inheriting a freestanding target).
|
||||
const mod_tests = b.addTest(.{
|
||||
.root_module = mod,
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("src/root.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
}),
|
||||
});
|
||||
|
||||
const run_mod_tests = b.addRunArtifact(mod_tests);
|
||||
|
||||
const exe_tests = b.addTest(.{
|
||||
.root_module = exe.root_module,
|
||||
});
|
||||
|
||||
const run_exe_tests = b.addRunArtifact(exe_tests);
|
||||
|
||||
const test_step = b.step("test", "Run tests");
|
||||
test_step.dependOn(&run_mod_tests.step);
|
||||
test_step.dependOn(&run_exe_tests.step);
|
||||
}
|
||||
|
|
|
|||
182
src/efi.zig
182
src/efi.zig
|
|
@ -1,15 +1,177 @@
|
|||
const std = @import("std");
|
||||
const uefi = std.os.uefi;
|
||||
const elf = std.elf;
|
||||
const danos = @import("danos");
|
||||
const BootInfo = danos.BootInfo;
|
||||
|
||||
/// 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.
|
||||
const kernel_file_name = std.unicode.utf8ToUtf16LeStringLiteral("danos");
|
||||
|
||||
/// 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 {
|
||||
// std's start code populates these globals before calling main.
|
||||
const con_out = uefi.system_table.con_out orelse return .device_error;
|
||||
|
||||
// UEFI console output expects a null-terminated UTF-16 string.
|
||||
const message = std.unicode.utf8ToUtf16LeStringLiteral("Hello from Zig UEFI!\r\n");
|
||||
_ = con_out.outputString(message) catch {};
|
||||
|
||||
while (true) {}
|
||||
|
||||
return .success;
|
||||
// `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\ndanos: 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_info: BootInfo = .{
|
||||
.framebuffer = try queryFramebuffer(bs),
|
||||
};
|
||||
|
||||
const entry = try loadKernel(bs);
|
||||
|
||||
log("danos: kernel loaded, exiting boot services\r\n");
|
||||
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
|
||||
// default `.c` convention (Microsoft x64) would use.
|
||||
const kernel: *const fn (*const BootInfo) callconv(danos.kernel_abi) noreturn = @ptrFromInt(entry);
|
||||
kernel(&boot_info);
|
||||
}
|
||||
|
||||
/// Read the current graphics mode into our own framebuffer description.
|
||||
fn queryFramebuffer(bs: *uefi.tables.BootServices) !danos.Framebuffer {
|
||||
const gop = (try bs.locateProtocol(uefi.protocol.GraphicsOutput, null)) orelse
|
||||
return error.NoGraphicsOutput;
|
||||
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 = switch (info.pixel_format) {
|
||||
.red_green_blue_reserved_8_bit_per_color => .rgbx,
|
||||
.blue_green_red_reserved_8_bit_per_color => .bgrx,
|
||||
// bit_mask / blt_only have no linear 32bpp layout we can paint into.
|
||||
else => return error.UnsupportedPixelFormat,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// 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) !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);
|
||||
}
|
||||
|
||||
/// Validate the ELF and copy every PT_LOAD segment to its physical address.
|
||||
fn loadElf(bs: *uefi.tables.BootServices, image: []u8) !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.
|
||||
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);
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {};
|
||||
continue;
|
||||
};
|
||||
bs.exitBootServices(uefi.handle, map.info.key) catch {
|
||||
_ = bs.freePool(buf.ptr) catch {};
|
||||
continue;
|
||||
};
|
||||
return; // Boot services are gone; do not touch `bs` again.
|
||||
}
|
||||
return error.ExitBootServicesFailed;
|
||||
}
|
||||
|
||||
/// 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;
|
||||
_ = out.outputString(std.unicode.utf8ToUtf16LeStringLiteral(msg)) catch {};
|
||||
}
|
||||
|
||||
/// 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 buf: [128]u16 = undefined;
|
||||
var i: usize = 0;
|
||||
for (bytes) |b| {
|
||||
if (i + 1 >= buf.len) break;
|
||||
buf[i] = b;
|
||||
i += 1;
|
||||
}
|
||||
buf[i] = 0;
|
||||
_ = out.outputString(buf[0..i :0].ptr) catch {};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
/* Kernel link layout.
|
||||
*
|
||||
* The kernel is linked at a fixed low physical address (set by `image_base` in
|
||||
* build.zig). UEFI runs with memory identity-mapped, so the bootloader can load
|
||||
* each PT_LOAD segment to the physical address matching its virtual address and
|
||||
* jump straight to _start — no page tables to build yet. (Moving to a
|
||||
* higher-half virtual base is a later step, once the bootloader sets up paging.)
|
||||
*/
|
||||
|
||||
ENTRY(_start)
|
||||
|
||||
/* One loadable segment per permission set, so the loader can map .text as R+X,
|
||||
* .rodata as R, and .data/.bss as R+W. FLAGS bits: 1=X, 2=W, 4=R. */
|
||||
PHDRS {
|
||||
text PT_LOAD FLAGS(5); /* R + X */
|
||||
rodata PT_LOAD FLAGS(4); /* R */
|
||||
data PT_LOAD FLAGS(6); /* R + W */
|
||||
}
|
||||
|
||||
SECTIONS {
|
||||
.text ALIGN(4K) : {
|
||||
*(.text .text.*)
|
||||
} :text
|
||||
|
||||
.rodata ALIGN(4K) : {
|
||||
*(.rodata .rodata.*)
|
||||
} :rodata
|
||||
|
||||
.data ALIGN(4K) : {
|
||||
*(.data .data.*)
|
||||
} :data
|
||||
|
||||
/* .bss occupies memory but not file space. The loader zeroes it via the
|
||||
* gap between each PT_LOAD segment's file size and memory size, so no
|
||||
* boundary symbols are needed here. (Zig's self-hosted linker also does not
|
||||
* yet honour linker-script symbol assignments.) */
|
||||
.bss ALIGN(4K) : {
|
||||
*(.bss .bss.*)
|
||||
*(COMMON)
|
||||
} :data
|
||||
|
||||
/DISCARD/ : {
|
||||
*(.comment)
|
||||
*(.note .note.*)
|
||||
*(.eh_frame .eh_frame_hdr)
|
||||
}
|
||||
}
|
||||
48
src/main.zig
48
src/main.zig
|
|
@ -1,8 +1,48 @@
|
|||
const std = @import("std");
|
||||
const Io = std.Io;
|
||||
|
||||
const danos = @import("danos");
|
||||
const BootInfo = danos.BootInfo;
|
||||
|
||||
pub fn main() !void {
|
||||
/// The calling convention used to enter the kernel. Pinned to SysV explicitly:
|
||||
/// the bootloader is built for the UEFI target, whose C convention is Microsoft
|
||||
/// x64 (first argument in RCX), while the kernel is SysV (first argument in
|
||||
/// RDI). Both sides reference this so the `boot_info` pointer lands in the
|
||||
/// register the other expects. `danos.kernel_abi` re-exports it to the loader.
|
||||
pub const kernel_abi = danos.kernel_abi;
|
||||
|
||||
}
|
||||
/// Kernel entry point. The bootloader jumps here after `ExitBootServices` with a
|
||||
/// pointer to the handoff data. There is no runtime, no stack unwinding, and no
|
||||
/// caller to return to, so this never returns.
|
||||
export fn _start(boot_info: *const BootInfo) callconv(kernel_abi) noreturn {
|
||||
kmain(boot_info);
|
||||
}
|
||||
|
||||
fn kmain(boot_info: *const BootInfo) noreturn {
|
||||
// Paint the whole screen so it is visually obvious the kernel took over.
|
||||
// We honour `pitch` because it can exceed `width * 4`.
|
||||
const fb = boot_info.framebuffer;
|
||||
const base: [*]volatile u8 = @ptrFromInt(fb.base);
|
||||
var y: u32 = 0;
|
||||
while (y < fb.height) : (y += 1) {
|
||||
const row: [*]volatile u32 = @ptrCast(@alignCast(base + y * fb.pitch));
|
||||
var x: u32 = 0;
|
||||
while (x < fb.width) : (x += 1) row[x] = 0x0033_66cc;
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
/// Freestanding has no OS to receive a panic, so override std's default panic
|
||||
/// handler to simply halt. (Later this can print to the framebuffer/serial.)
|
||||
pub const panic = std.debug.FullPanic(struct {
|
||||
fn panic(msg: []const u8, first_trace_addr: ?usize) noreturn {
|
||||
_ = msg;
|
||||
_ = first_trace_addr;
|
||||
hang();
|
||||
}
|
||||
}.panic);
|
||||
|
|
|
|||
40
src/root.zig
40
src/root.zig
|
|
@ -1 +1,39 @@
|
|||
//! By convention, root.zig is the root source file when making a package.
|
||||
//! Shared definitions that form the contract between the bootloader
|
||||
//! (src/efi.zig, built as BOOTX64.efi) and the kernel (src/main.zig).
|
||||
//!
|
||||
//! Both binaries import this as the "danos" module, so the handoff layout is
|
||||
//! defined in exactly one place.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
/// Calling convention for the bootloader→kernel jump. Pinned to SysV 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
|
||||
/// SysV (first arg in RDI). Both reference this to agree on where `*BootInfo`
|
||||
/// 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).
|
||||
pub const Framebuffer = extern struct {
|
||||
base: usize,
|
||||
width: u32,
|
||||
height: u32,
|
||||
pitch: u32,
|
||||
format: PixelFormat,
|
||||
};
|
||||
|
||||
/// 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,
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue