λ

lumine

UEFI x86_64 bootloader for linux kernel images

settings

diff

f1caee61 to 88026b83

+287 -62

.gitignore modified +3 -0

diff --git a/.gitignore b/.gitignore
index c4a0218398f5391b8b66d5e765cef165c6d0c96a..d12c95dd869e4019286e7e9fb892874c3ec3054c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,4 +2,7 @@ zig-pkg
 zig-out
 .zig-cache
 
+res/*
+!res/lumine.zon
+
 .ignore

build.zig modified +65 -32

diff --git a/build.zig b/build.zig
index 869a3590671e19e6a3b9309fa46efa6c02b4f459..c5de48379c847934e040c7dcc1e797a6f2739879 100644
--- a/build.zig
+++ b/build.zig
@@ -4,67 +4,100 @@ const Arch = enum {
     x86_64,
     aarch64,
 
-    pub fn stdlib(self: Arch) std.Target.Cpu.Arch {
+    pub fn target(self: Arch) std.Target.Cpu.Arch {
         return switch (self) {
             .x86_64 => .x86_64,
             .aarch64 => .aarch64,
         };
     }
 
-    pub fn efiName(self: Arch, b: *std.Build) []const u8 {
-        const suffix = switch (self) {
-            .x86_64 => "X64",
-            .aarch64 => "AA64",
+    pub const EfiName = enum { X64, AA64 };
+
+    pub fn efi(self: Arch) EfiName {
+        return switch (self) {
+            .x86_64 => .X64,
+            .aarch64 => .AA64,
         };
-        return b.fmt("BOOT{s}", .{suffix});
     }
 };
 
-pub fn build(b: *std.Build) void {
-    const optimize = b.standardOptimizeOption(.{});
-
-    const cpu_arch = b.option(Arch, "arch", "cpu architecture") orelse .x86_64;
+fn lumineExecutable(b: *std.Build, cpu_arch: Arch, optimize: std.builtin.OptimizeMode) *std.Build.Step.Compile {
+    const target = b.resolveTargetQuery(.{
+        .cpu_arch = cpu_arch.target(),
+        .cpu_model = .baseline,
+        .os_tag = .uefi,
+    });
 
-    const efi_name = cpu_arch.efiName(b);
-
-    const lumine_exe = b.addExecutable(.{
-        .name = efi_name,
+    return b.addExecutable(.{
+        .name = b.fmt("BOOT{t}", .{cpu_arch.efi()}),
         .root_module = b.createModule(.{
-            .root_source_file = b.path("src/main.zig"),
-            .target = b.resolveTargetQuery(.{
-                .os_tag = .uefi,
-                .cpu_arch = cpu_arch.stdlib(),
-            }),
+            .target = target,
             .optimize = optimize,
+            .root_source_file = b.path("src/main.zig"),
         }),
     });
+}
+
+pub fn build(b: *std.Build) void {
+    const optimize = b.standardOptimizeOption(.{});
+
+    const cpu_arch = b.option(Arch, "arch", "cpu architecture") orelse .x86_64;
 
     const install_step = b.getInstallStep();
 
+    const lumine_exe = lumineExecutable(b, cpu_arch, optimize);
     const lumine_install = b.addInstallArtifact(lumine_exe, .{});
     install_step.dependOn(&lumine_install.step);
 
-    const qemu_exe = b.option([]const u8, "qemu", "path to qemu binary") orelse b.fmt("qemu-system-{t}", .{cpu_arch});
-    const qemu_cmd = b.addSystemCommand(&.{ qemu_exe, "-m", "512M", "-serial", "mon:stdio", "-nographic", "-no-reboot", "-no-shutdown" });
+    const all_step = b.step("all", "build for all supported architectures");
+
+    inline for (@typeInfo(Arch).@"enum".fields) |field| {
+        const arch_exe = lumineExecutable(b, @field(Arch, field.name), optimize);
+
+        const arch_install = b.addInstallArtifact(arch_exe, .{});
+        all_step.dependOn(&arch_install.step);
+    }
 
-    {
-        const ovmf_dep = b.dependency("ovmf", .{});
-        const ovmf_code = ovmf_dep.path(b.fmt("ovmf-code-{t}.fd", .{cpu_arch}));
+    const qemu_exe = b.option([]const u8, "qemu", "ovveride qemu executable") orelse b.fmt("qemu-system-{t}", .{cpu_arch});
+    const qemu_cmd = b.addSystemCommand(&.{ qemu_exe, "-m", "512M", "-no-reboot", "-no-shutdown", "-serial", "mon:stdio", "-nographic" });
 
-        switch (cpu_arch) {
-            .aarch64 => {
-                qemu_cmd.addArgs(&.{ "-M", "virt", "-cpu", "max" });
-            },
-            .x86_64 => {},
-        }
+    switch (cpu_arch) {
+        .aarch64 => qemu_cmd.addArgs(&.{ "-M", "virt", "-cpu", "cortex-a57" }),
+        .x86_64 => {},
+    }
 
+    const ovmf_override = b.option(std.Build.LazyPath, "ovmf", "override ovmf firmware dependency");
+    const ovmf_use_dep = b.option(bool, "ovmf-dep", "use ovmf dependency instead") orelse false;
+
+    if (ovmf_use_dep) {
+        if (b.lazyDependency("ovmf", .{})) |ovmf_dep| {
+            const ovmf_code = ovmf_dep.path(b.fmt("ovmf-code-{t}.fd", .{cpu_arch}));
+
+            qemu_cmd.addArg("-bios");
+            qemu_cmd.addFileArg(ovmf_code);
+        }
+    } else if (ovmf_override) |override| {
         qemu_cmd.addArg("-bios");
-        qemu_cmd.addFileArg(ovmf_code);
+        qemu_cmd.addFileArg(override);
+    } else {
+        const fail = b.addFail("ovmf not specified, either use -Dovmf=PATH_TO_OVMF_CODE_BIN or use -Dovmf-dep to download as dependency");
+        qemu_cmd.step.dependOn(&fail.step);
     }
 
     {
         const boot_dir = b.addTempFiles();
-        _ = boot_dir.addCopyFile(lumine_exe.getEmittedBin(), b.fmt("EFI/BOOT/{s}.EFI", .{efi_name}));
+        _ = boot_dir.addCopyFile(lumine_exe.getEmittedBin(), b.fmt("EFI/BOOT/BOOT{t}.EFI", .{cpu_arch.efi()}));
+
+        const config_file = b.option([]const u8, "config", "path to config file") orelse "res/lumine.zon";
+
+        _ = boot_dir.addCopyFile(b.path(config_file), "lumine.zon");
+
+        // https://codeberg.org/ziglang/zig/issues/32041
+        // https://codeberg.org/dragsbruh/minimux/releases
+        const linux_dir = b.option([]const u8, "linux", "dir containing `vmlinuz` and `initrd.xz`") orelse b.fmt("res/{t}", .{cpu_arch});
+
+        _ = boot_dir.addCopyFile(b.path(linux_dir).path(b, "vmlinuz"), "linux/vmlinuz");
+        _ = boot_dir.addCopyFile(b.path(linux_dir).path(b, "initrd.xz"), "linux/initrd.xz");
 
         qemu_cmd.addArg("-drive");
         qemu_cmd.addPrefixedDirectoryArg("format=raw,file=fat:rw:", boot_dir.getDirectory());

build.zig.zon modified +1 -0

diff --git a/build.zig.zon b/build.zig.zon
index 451bc0dc58e73f1f5d0960cd3d61e20f4bcf5daf..9582bf697757be39217a4ca57c21e9c48ce7c5cb 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -7,6 +7,7 @@     .dependencies = .{
         .ovmf = .{
             .url = "https://github.com/osdev0/edk2-ovmf-nightly/releases/download/20260531T041444Z/edk2-ovmf.tar.gz",
             .hash = "N-V-__8AAAAAjAyHNKDbnzj1LQizupozkEzL21pfQiTzt8NQ",
+            .lazy = true,
         },
     },
     .paths = .{""},

config.zon deleted +0 -15

diff --git a/config.zon b/config.zon
deleted file mode 100644
index a50892956f699929983f43bfbd03a66d3c1af082..0000000000000000000000000000000000000000
--- a/config.zon
+++ /dev/null
@@ -1,15 +0,0 @@
-// example config, since this is unimplemented
-// for now, paths must be absolute path and in boot partition
-.{
-    .boot = .{
-        .linux = .{
-            .kernel_path = "\\linux\\vmlinuz",
-            .kernel_cmdline = "root=/dev/vda1 rw",
-
-            .module_paths = .{
-                "\\linux\\initramfs.img",
-                "\\linux\\intel-ucode.img",
-            },
-        },
-    },
-}

res/lumine.zon added +10 -0

diff --git a/res/lumine.zon b/res/lumine.zon
new file mode 100644
index 0000000000000000000000000000000000000000..16b572fd33fd13d1fad195d3a0d902e1fb76e705
--- /dev/null
+++ b/res/lumine.zon
@@ -0,0 +1,10 @@
+.{
+    .boot = .{
+        .linux = .{
+            .kernel_path = .{
+                .boot = "/linux/vmlinuz",
+            },
+            .kernel_cmdline = "console=ttyS0",
+        },
+    },
+}

src/Config.zig added +23 -0

diff --git a/src/Config.zig b/src/Config.zig
new file mode 100644
index 0000000000000000000000000000000000000000..39d11ee4531fb6b62c43b850b1484e8259350fae
--- /dev/null
+++ b/src/Config.zig
@@ -0,0 +1,23 @@
+const std = @import("std");
+const Io = std.Io;
+
+pub const Path = union(enum) {
+    /// file in boot partition
+    boot: []const u8,
+
+    pub fn format(self: Path, w: *Io.Writer) !void {
+        switch (self) {
+            // limine mention :(
+            .boot => |path| try w.print("boot():{s}", .{path}),
+        }
+    }
+};
+
+pub const BootOption = union(enum) {
+    linux: struct {
+        kernel_path: Path,
+        kernel_cmdline: []const u8 = &.{},
+    },
+};
+
+boot: BootOption,

src/linux.zig added +1 -0

diff --git a/src/linux.zig b/src/linux.zig
new file mode 100644
index 0000000000000000000000000000000000000000..34738dea0ed7fcbbef052723f925a0daeb87c5ce
--- /dev/null
+++ b/src/linux.zig
@@ -0,0 +1 @@
+pub const Kernel = @import("./linux/Kernel.zig");

src/linux/Kernel.zig added +63 -0

diff --git a/src/linux/Kernel.zig b/src/linux/Kernel.zig
new file mode 100644
index 0000000000000000000000000000000000000000..8fd8289e9b1a6ee6df727940ba9b8e94f61fce87
--- /dev/null
+++ b/src/linux/Kernel.zig
@@ -0,0 +1,63 @@
+const std = @import("std");
+const uefi = std.os.uefi;
+
+const luefi = @import("../luefi.zig");
+const Path = @import("../Config.zig").Path;
+
+const log = std.log.scoped(.linux);
+
+const Kernel = @This();
+
+kernel_pages: usize,
+kernel_buf: []align(4096) u8,
+
+pub fn load(path: Path) !Kernel {
+    const boot_svc = uefi.system_table.boot_services.?;
+
+    log.info("loading kernel from {f}", .{path});
+    const file = try luefi.fileOpen(path);
+    defer file.close() catch unreachable;
+
+    const size = try luefi.fileSize(file);
+    const page_count = try std.math.divCeil(usize, size, 4096);
+
+    const pages = try boot_svc.allocatePages(.any, .loader_data, page_count);
+    errdefer boot_svc.freePages(pages) catch unreachable;
+
+    const slice = @as([*]align(4096) u8, @ptrCast(pages))[0..size];
+    try luefi.fileReadSlice(file, slice);
+
+    return .{
+        .kernel_buf = slice,
+        .kernel_pages = size,
+    };
+}
+
+pub fn deinit(self: Kernel) void {
+    const boot_svc = uefi.system_table.boot_services.?;
+    const pages = @as([*]align(4096) [4096]u8, @ptrCast(self.kernel_buf));
+    boot_svc.freePages(pages[0..self.kernel_pages]) catch unreachable;
+}
+
+pub fn start(self: Kernel, cmdline: []const u8) !void {
+    const boot_svc = uefi.system_table.boot_services.?;
+    const allocator = uefi.pool_allocator;
+
+    const cmdline_utf16 = try std.unicode.utf8ToUtf16LeAllocZ(allocator, cmdline);
+    defer allocator.free(cmdline_utf16);
+
+    const img_handle = try boot_svc.loadImage(false, uefi.handle, .{ .buffer = self.kernel_buf });
+    defer _ = boot_svc.unloadImage(img_handle) catch unreachable;
+
+    const loaded_img = try boot_svc.openProtocol(uefi.protocol.LoadedImage, img_handle, .{
+        .get_protocol = .{ .agent = uefi.handle },
+    }) orelse unreachable;
+
+    loaded_img.load_options = @ptrCast(cmdline_utf16);
+    loaded_img.load_options_size = @intCast(cmdline_utf16.len * @sizeOf(u16));
+
+    const exit_data = try boot_svc.startImage(img_handle);
+    log.err("kernel image exited with exit code {t}", .{exit_data.code});
+
+    return error.KernelExited;
+}

src/luefi.zig added +65 -0

diff --git a/src/luefi.zig b/src/luefi.zig
new file mode 100644
index 0000000000000000000000000000000000000000..0be53d5070e981bba11affc72cda848680711e1a
--- /dev/null
+++ b/src/luefi.zig
@@ -0,0 +1,65 @@
+//! lumine uefi abstractions
+const std = @import("std");
+const uefi = std.os.uefi;
+
+const Path = @import("./Config.zig").Path;
+
+pub fn posixPathToUefi(path: []u16) void {
+    for (0..path.len) |i|
+        switch (path[i]) {
+            '/' => path[i] = '\\',
+            else => {},
+        };
+}
+
+pub fn fileOpenBoot(path: []const u8) !*uefi.protocol.File {
+    const boot_svc = uefi.system_table.boot_services.?;
+    const allocator = uefi.pool_allocator;
+
+    const loaded_img = try boot_svc.openProtocol(uefi.protocol.LoadedImage, uefi.handle, .{
+        .get_protocol = .{ .agent = uefi.handle },
+    }) orelse unreachable;
+
+    const device_handle = loaded_img.device_handle orelse unreachable;
+    const simple_fs = try boot_svc.openProtocol(uefi.protocol.SimpleFileSystem, device_handle, .{
+        .get_protocol = .{ .agent = uefi.handle },
+    }) orelse unreachable;
+
+    const root_fs = try simple_fs.openVolume();
+    defer root_fs.close() catch unreachable;
+
+    // is allocating a good idea?
+    const utf16_path = try std.unicode.utf8ToUtf16LeAllocZ(allocator, path);
+    defer allocator.free(utf16_path);
+
+    posixPathToUefi(utf16_path);
+
+    return root_fs.open(utf16_path, .read, .{});
+}
+
+pub fn fileOpen(path: Path) !*uefi.protocol.File {
+    return switch (path) {
+        .boot => |boot_path| fileOpenBoot(boot_path),
+    };
+}
+
+pub fn fileSize(file: *uefi.protocol.File) !usize {
+    const allocator = uefi.pool_allocator;
+
+    const info_size = try file.getInfoSize(.file);
+    const info_buf = try allocator.alignedAlloc(u8, .@"8", info_size);
+    defer allocator.free(info_buf);
+
+    const info = try file.getInfo(.file, info_buf);
+    return info.file_size;
+}
+
+pub fn fileReadSlice(file: *uefi.protocol.File, slice: []u8) !void {
+    var read: usize = 0;
+
+    while (read < slice.len) {
+        const chunk = try file.read(slice[read..]);
+        if (chunk == 0) return error.ReadShort;
+        read += chunk;
+    }
+}

src/main.zig modified +47 -6

diff --git a/src/main.zig b/src/main.zig
index 2614d4619b2066bcb6ccd1c99d65258e0d34f101..405011792adeb8f3714b0b3e80b12579cac5b977 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -1,13 +1,51 @@
 const std = @import("std");
 const uefi = std.os.uefi;
 const log = std.log;
+const Io = std.Io;
 
-const Writer = @import("./uefi/Writer.zig");
+const Config = @import("./Config.zig");
+const Console = @import("./Console.zig");
+const linux = @import("./linux.zig");
+const luefi = @import("./luefi.zig");
+
+const config_paths: []const Config.Path = &.{
+    .{ .boot = "/lumine.zon" },
+    .{ .boot = "/lumine/lumine.zon" },
+    .{ .boot = "/lumine/config.zon" },
+};
 
 pub fn bmain() !void {
-    log.info("hello lumine", .{});
-    while (true)
-        std.atomic.spinLoopHint();
+    const allocator = uefi.pool_allocator;
+
+    const config: Config = try config: {
+        var file = reader: {
+            for (config_paths) |path| {
+                break :reader luefi.fileOpen(path) catch continue;
+            }
+
+            log.err("failed to detect config, tried paths:", .{});
+            for (config_paths) |path| log.err("\t{f}", .{path});
+            return error.NoConfig;
+        };
+        defer file.close() catch unreachable;
+
+        const source = try allocator.allocSentinel(u8, try luefi.fileSize(file), 0);
+        defer allocator.free(source);
+
+        try luefi.fileReadSlice(file, source);
+
+        break :config std.zon.parse.fromSliceAlloc(Config, allocator, source, null, .{});
+    };
+    defer std.zon.parse.free(allocator, config);
+
+    switch (config.boot) {
+        .linux => |linux_conf| {
+            const kernel: linux.Kernel = try .load(linux_conf.kernel_path);
+            defer kernel.deinit();
+
+            try kernel.start(linux_conf.kernel_cmdline);
+        },
+    }
 }
 
 pub fn main() uefi.Status {
@@ -16,7 +54,8 @@         log.err("unrecoverable error in bmain: {}", .{err});
         return .aborted;
     };
 
-    return .success;
+    log.err("bootloader exited successfully, this shouldnt happen", .{});
+    while (true) std.atomic.spinLoopHint();
 }
 
 pub fn logFn(
@@ -27,9 +66,11 @@     args: anytype,
 ) void {
     // stderr should be equivalent to stdout?
     // but i dont see logs on my hardware if i use stderr
-    var stderr: Writer = .stdout();
+    var stderr_buf: [512]u8 = undefined;
+    var stderr: Console = .stdout(&stderr_buf);
     stderr.interface.print("{t}({}): ", .{ level, scope }) catch return;
     stderr.interface.print(format ++ "\r\n", args) catch return;
+    stderr.interface.flush() catch return;
 }
 
 pub fn panicFn(msg: []const u8, _: ?usize) noreturn {

src/uefi/Writer.zig => src/Console.zig renamed +9 -9

diff --git a/src/uefi/Writer.zig b/src/Console.zig
rename from src/uefi/Writer.zig
rename to src/Console.zig
index 43fd9726616a8b9e46aa4008c7cd5e33ff2fb13b..7f3805640480bdb8e3ccbfd5e520360b1be15813 100644
--- a/src/uefi/Writer.zig
+++ b/src/Console.zig
@@ -2,12 +2,12 @@ const std = @import("std");
 const Io = std.Io;
 const uefi = std.os.uefi;
 
-const Writer = @This();
+const Console = @This();
 
 interface: Io.Writer,
 protocol: *uefi.protocol.SimpleTextOutput,
 
-pub fn init(buffer: []u8, protocol: *uefi.protocol.SimpleTextOutput) Writer {
+pub fn init(buffer: []u8, protocol: *uefi.protocol.SimpleTextOutput) Console {
     return .{
         .protocol = protocol,
         .interface = .{
@@ -17,16 +17,16 @@         },
     };
 }
 
-pub fn stdout() Writer {
-    return .init(&.{}, uefi.system_table.con_out.?);
+pub fn stdout(buffer: []u8) Console {
+    return .init(buffer, uefi.system_table.con_out.?);
 }
 
-pub fn stderr() Writer {
-    return .init(&.{}, uefi.system_table.std_err.?);
+pub fn stderr(buffer: []u8) Console {
+    return .init(buffer, uefi.system_table.std_err.?);
 }
 
-pub fn write(self: *Writer, data: []const u8) !void {
-    var buffer: [64:0]u16 = undefined;
+pub fn write(self: *Console, data: []const u8) !void {
+    var buffer: [64:0]u16 = @splat(undefined);
     var buflen: usize = 0;
     for (data) |b| {
         buffer[buflen] = @intCast(b);
@@ -43,7 +43,7 @@     }
 }
 
 fn drainFn(io_w: *Io.Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize {
-    const self: *Writer = @fieldParentPtr("interface", io_w);
+    const self: *Console = @fieldParentPtr("interface", io_w);
 
     {
         const buf = io_w.buffered();