const std = @import("std"); const uefi = std.os.uefi; const elf = std.elf; 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. 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 { // `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), .memory_map = undefined, // filled by exitBootServices, just below .kernel_segments = undefined, // filled by loadKernel .kernel_segment_count = 0, }; const entry = try loadKernel(bs, &boot_info); log("danos: kernel loaded, exiting boot services\r\n"); 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 // default `.c` convention (Microsoft x64) would use. const kernel: *const fn (*const BootInfo) callconv(danos.kernel_abi) noreturn = @ptrFromInt(entry); kernel(&boot_info); } /// A display resolution in pixels. const Resolution = struct { width: u32, height: 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) !danos.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), }; } /// 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) !danos.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 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. 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; return .{ .width = w, .height = h }; } /// 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_info: *BootInfo) !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_info); } /// 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_info: *BootInfo) !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); // Record it (identity-loaded: virtual == physical) for the kernel's VMM. const n = boot_info.kernel_segment_count; if (n < boot_info.kernel_segments.len) { boot_info.kernel_segments[n] = .{ .virt = phdr.p_vaddr, .pages = pages, .flags = phdr.p_flags, }; boot_info.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) !danos.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_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(map_buf.ptr) catch {}; _ = bs.freePool(regions_buf.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_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); // 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 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 config 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. /// Among RAM regions, anything we don't recognise is `reserved` — the safe /// default; our own LoaderData (kernel image, these buffers) lands there too and /// stays reserved until the kernel reclaims it. fn classify(d: *const uefi.tables.MemoryDescriptor) danos.MemoryKind { if (!d.attribute.wb) return .mmio; return switch (d.@"type") { .conventional_memory => .usable, .boot_services_code, .boot_services_data => .reclaimable, .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 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 {}; }