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 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()); 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 = .{""}, 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", - }, - }, - }, -} 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", + }, + }, +} 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, 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"); 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; +} 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; + } +} 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 { 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();