summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJohannes Stoelp <johannes.stoelp@gmail.com>2025-04-24 00:21:02 +0200
committerJohannes Stoelp <johannes.stoelp@gmail.com>2025-04-24 00:21:02 +0200
commit23e0ccf7b5a0ccea545f231d35dbecc00011a9de (patch)
treefcabb2e084297bb51c2a12ee77404b891cca0df6
parent20a854354918735c3289c5576a28fad18ca21757 (diff)
downloadzig-playground-main.tar.gz
zig-playground-main.zip
multiboot: add multiboot example kernelHEADmain
-rw-r--r--x86-bare-metal/multiboot/Makefile25
-rw-r--r--x86-bare-metal/multiboot/grub/grub.cfg10
-rw-r--r--x86-bare-metal/multiboot/kern.ld27
-rw-r--r--x86-bare-metal/multiboot/kern.zig47
-rw-r--r--x86-bare-metal/multiboot/mb.zig220
-rw-r--r--x86-bare-metal/multiboot/scripts/check_mbhdr.awk36
-rw-r--r--x86-bare-metal/multiboot/scripts/check_sse.awk12
-rw-r--r--x86-bare-metal/multiboot/vga.zig117
8 files changed, 494 insertions, 0 deletions
diff --git a/x86-bare-metal/multiboot/Makefile b/x86-bare-metal/multiboot/Makefile
new file mode 100644
index 0000000..88772f5
--- /dev/null
+++ b/x86-bare-metal/multiboot/Makefile
@@ -0,0 +1,25 @@
+O := BUILD
+
+$(O)/disk.img: $(O)/kern grub
+ mkdir -p $(O)/iso/boot
+ rsync -rav $^ $(O)/iso/boot
+ grub-mkrescue -o '$@' $(O)/iso
+
+# When building the kernel we explicitly let the zig compiler emit code for a
+# very old cpu. This way we ensure it does not emit any sse instructions, which
+# first need to be enabled before the can be used (setting up ctrl registers and
+# sse state).
+$(O)/kern: kern.ld kern.zig
+ mkdir -p $(O)
+ zig build-exe -fno-unwind-tables -femit-bin=$@ -target x86-freestanding-none -mcpu i386 -O Debug -fno-strip --script $^
+ xxd -d -e -c4 $@ | awk -f scripts/check_mbhdr.awk
+ objdump -d $@ | awk -f scripts/check_sse.awk
+
+clean:
+ $(RM) -r $(O)
+
+run: $(O)/kern
+ qemu-system-i386 -kernel $< -append mode=raw-elf
+
+run-img: $(O)/disk.img
+ qemu-system-i386 -hda $<
diff --git a/x86-bare-metal/multiboot/grub/grub.cfg b/x86-bare-metal/multiboot/grub/grub.cfg
new file mode 100644
index 0000000..f940511
--- /dev/null
+++ b/x86-bare-metal/multiboot/grub/grub.cfg
@@ -0,0 +1,10 @@
+#set timeout=0
+#set default="0"
+
+menuentry "kern" {
+ multiboot /boot/kern
+}
+
+menuentry "kern w/ args" {
+ multiboot /boot/kern mode=grub-disk
+}
diff --git a/x86-bare-metal/multiboot/kern.ld b/x86-bare-metal/multiboot/kern.ld
new file mode 100644
index 0000000..5a615c9
--- /dev/null
+++ b/x86-bare-metal/multiboot/kern.ld
@@ -0,0 +1,27 @@
+ENTRY(_start)
+
+SECTIONS
+{
+ . = 1M;
+ .mbhdr : {
+ KEEP(*(.mbhdr))
+ }
+ .text.boot : {
+ *(.text.boot)
+ }
+ .text : {
+ *(.text*)
+ }
+ .rodata ALIGN (0x1000) : {
+ *(.rodata*)
+ }
+ .data ALIGN (0x1000) : {
+ *(.data*)
+ }
+ .bss : {
+ sbss = .;
+ *(COMMON)
+ *(.bss*)
+ ebss = .;
+ }
+}
diff --git a/x86-bare-metal/multiboot/kern.zig b/x86-bare-metal/multiboot/kern.zig
new file mode 100644
index 0000000..d8f9c96
--- /dev/null
+++ b/x86-bare-metal/multiboot/kern.zig
@@ -0,0 +1,47 @@
+const fmt = @import("std").fmt;
+const dbg = @import("std").debug;
+
+const vga = @import("vga.zig");
+const mb = @import("mb.zig");
+
+export const mbhdr align(4) linksection(".mbhdr") = mb.HEADER;
+
+var KERNEL_STACK: [32 * 4096]u8 align(16) = undefined;
+
+export fn _start() align(16) linksection(".text.boot") callconv(.naked) noreturn {
+ @setRuntimeSafety(false);
+ asm volatile (
+ \\mov %%eax, %%esp
+ // Push paddr of multiboot info structure on ask (per abi first integer argument to the fn).
+ \\pushl %%ebx
+ \\call kmain
+ \\1: hlt
+ \\jmp 1b
+ :
+ : [top] "{eax}" (@intFromPtr(&KERNEL_STACK) + KERNEL_STACK.len),
+ );
+}
+
+export fn kmain(ebx: u32) void {
+ var con = vga.vga(10, 0).init;
+ con.puts("Booting into kmain()\n\n");
+
+ mb.formatBootinfo(con.writer(), mb.bootinfo(ebx)) catch {};
+
+ dbg.panic("{s}:{}:{s}: done...", .{ @src().file, @src().line, @src().fn_name });
+}
+
+// -- PANIC HANDLER -------------------------------------------------------------
+
+pub const panic = dbg.FullPanic(panicHandler);
+
+fn panicHandler(msg: []const u8, first_trace_addr: ?usize) noreturn {
+ _ = first_trace_addr;
+
+ var con = vga.vga(11, 0).init;
+ fmt.format(con.writer(), "\nPANIC: {s}\n", .{msg}) catch {};
+
+ while (true) {
+ asm volatile ("hlt");
+ }
+}
diff --git a/x86-bare-metal/multiboot/mb.zig b/x86-bare-metal/multiboot/mb.zig
new file mode 100644
index 0000000..e18f905
--- /dev/null
+++ b/x86-bare-metal/multiboot/mb.zig
@@ -0,0 +1,220 @@
+// Multiboot [1]
+//
+// * OS image requiremets
+// - 32 bit executable file
+// - multiboot header in the first 8192 bytes, and 4 byte aligned
+//
+// * Machine state
+// - eax: contains the magic value 0x2BADB002
+// - ebx: 32 bit phys addr of the multiboot info structure
+// - cs: 32 bit rx segment [0..0xffff_ffff]
+// - ds/es/fs/gs/ss: 32 bit rw segment [0..0xffff_ffff]
+// - a20 gate enabled [3]
+// - cr0: paging (PG) disabled, protection (PE) enabled
+// - esp: os must setup its own stack when it needs one
+// - gdtr/idtr: os image must setup own tables
+//
+// [1] https://www.gnu.org/software/grub/manual/multiboot/multiboot.html
+// [2] https://www.gnu.org/software/grub/manual/multiboot/multiboot.pdf
+// [3] https://wiki.osdev.org/A20_Line
+
+// -- MULTIBOOT HEADER ----------------------------------------------------------
+
+const MBH_MEMALIGN = (1 << 0);
+const MBH_MEMINFO = (1 << 1);
+
+const MAGIC = 0x1BADB002;
+const FLAGS = MBH_MEMINFO;
+const CHECKSUM: i32 = -MAGIC - FLAGS;
+
+const MultibootHeader = extern struct {
+ magic: u32,
+ flags: u32,
+ checksum: i32,
+};
+
+pub const HEADER = MultibootHeader{
+ .magic = MAGIC,
+ .flags = FLAGS,
+ .checksum = CHECKSUM,
+};
+
+// -- MULTIBOOT INFO ------------------------------------------------------------
+
+const MultibootInfoFlag = enum(u32) {
+ MEMORY = (1 << 0),
+ BOOTDEV = (1 << 1),
+ CMDLINE = (1 << 2),
+ MODS = (1 << 3),
+ SYMAOUT = (1 << 4),
+ SYMELF = (1 << 5),
+ MEMMAP = (1 << 6),
+ DRIVE = (1 << 7),
+ CONFIG = (1 << 8),
+ BOOTLOADER = (1 << 9),
+ APM = (1 << 10),
+ VBE = (1 << 11),
+ FRAMEBUFFER = (1 << 12),
+};
+
+pub const MultibootInfo = extern struct {
+ flags: u32,
+
+ // Valid iff MEMORY flag set.
+ mem_lower: u32,
+ mem_upper: u32,
+
+ // Valid iff BOOTDEV flag set.
+ boot_device: u32,
+
+ // Valid iff CMDLINE flag set.
+ cmdline: u32,
+
+ // Valid iff MODS flag set.
+ mods_count: u32,
+ mods_addr: u32,
+
+ // Valid iff SYMAOUT or SYMELF flag set.
+ _sym_stub: [4]u32,
+
+ // Valid iff MEMMAP flag set.
+ mmap_length: u32,
+ mmap_addr: u32,
+
+ // Valid iff DRIVE flag set.
+ drives_length: u32,
+ drives_addr: u32,
+
+ // Valid iff CONFIG flag set.
+ config_table: u32,
+
+ // Valid iff BOOTLOADER flag set.
+ boot_loader_name: u32,
+
+ // Valid iff APM flag set.
+ apm_table: u32,
+
+ // Valid iff VBE flag set.
+ vbe_control_info: u32,
+ vbe_mode_info: u32,
+ vbe_mode: u16,
+ vbe_interface_seg: u16,
+ vbe_interface_off: u16,
+ vbe_interface_len: u16,
+
+ // Valid iff FB flag set.
+ framebuffer_addr: u64,
+ framebuffer_pitch: u32,
+ framebuffer_width: u32,
+ framebuffer_height: u32,
+ framebuffer_bpp: u8,
+ framebuffer_type: u8,
+ _framebuffer_palette_stub: [6]u8,
+
+ /// Check if given multiboot info flag is set.
+ fn has(self: @This(), flag: MultibootInfoFlag) bool {
+ return (self.flags & @intFromEnum(flag)) != 0;
+ }
+
+ /// If the cmdline is available return a null-terminated c string.
+ fn cmdlineStr(self: @This()) ?[*:0]const u8 {
+ return if (self.has(.CMDLINE)) cstr(self.cmdline) else null;
+ }
+
+ /// If the bootloader name is available return a null-terminated c string.
+ fn bootloaderStr(self: @This()) ?[*:0]const u8 {
+ return if (self.has(.BOOTLOADER)) cstr(self.boot_loader_name) else null;
+ }
+
+ /// If the mmap info is available, return an iterator over the mmap entries.
+ fn mmapIter(self: @This()) ?MultibootMmapEntry.Iter {
+ return if (self.has(.MEMMAP)) .{ .addr = self.mmap_addr, .len = self.mmap_length, .off = 0 } else null;
+ }
+};
+
+// This structure is defined as packed structure in the mb spec.
+pub const MultibootMmapEntry = extern struct {
+ size: u32 align(1),
+ addr: u64 align(1),
+ len: u64 align(1),
+ type: u32 align(1),
+
+ /// Get the `actual` size of an mmap entry.
+ fn getSize(self: @This()) usize {
+ // The `size` field expresses the actual size of the mmap structure
+ // (w/o the size field).
+ // The actual size can be larger as the struct definition.
+ return self.size + @sizeOf(@TypeOf(self.size));
+ }
+
+ /// Get the mmap type as string.
+ fn typeStr(self: @This()) []const u8 {
+ return switch (self.type) {
+ 1 => "available",
+ 2 => "reserved",
+ 3 => "acpi-reclaim",
+ 4 => "nvs",
+ 5 => "badram",
+ else => "<unknown>",
+ };
+ }
+
+ /// Iterator type over mmap entries.
+ const Iter = struct {
+ addr: u32,
+ len: u32,
+ off: u32,
+
+ /// Return the next mmap entry if the iterator is not exhausted.
+ fn next(self: *@This()) ?*const MultibootMmapEntry {
+ return if ((self.off + @sizeOf(MultibootMmapEntry)) > self.len)
+ null
+ else blk: {
+ const m: *const MultibootMmapEntry = @ptrFromInt(self.addr + self.off);
+ self.off += m.getSize();
+ break :blk m;
+ };
+ }
+ };
+};
+
+// -- MULTIBOOT UTILS -----------------------------------------------------------
+
+const format = @import("std").fmt.format;
+
+pub fn bootinfo(ebx: u32) *const MultibootInfo {
+ return @ptrFromInt(ebx);
+}
+
+pub fn formatBootinfo(w: anytype, mbi: *const MultibootInfo) !void {
+ try w.writeAll("multiboot info\n");
+
+ try format(w, "flags: 0b{b}\n", .{mbi.flags});
+
+ if (mbi.bootloaderStr()) |bootloader| {
+ try format(w, "bootloader: {s}\n", .{bootloader});
+ }
+
+ if (mbi.cmdlineStr()) |cmdline| {
+ try format(w, "cmdline: {s}\n", .{cmdline});
+ }
+
+ if (mbi.has(.MEMORY)) {
+ try format(w, "mem_lower: {x:08} - {x:08}\n", .{ 0, mbi.mem_lower * 1024 });
+ try format(w, "mem_upper: {x:08} - {x:08}\n", .{ 1024 * 1024, mbi.mem_upper * 1024 });
+ }
+
+ if (mbi.mmapIter()) |iter| {
+ // Rebind iter, cant bind mutable reference to temporary object.
+ var it = iter;
+
+ try format(w, "mmap: @ {x:08} - {x:08}\n", .{ it.addr, it.addr + it.len - 1 });
+ while (it.next()) |map| {
+ try format(w, "region: {x:08} - {x:08} | {s}\n", .{ map.addr, map.addr + map.len - 1, map.typeStr() });
+ }
+ }
+}
+
+fn cstr(addr: u32) [*:0]const u8 {
+ return @ptrFromInt(addr);
+}
diff --git a/x86-bare-metal/multiboot/scripts/check_mbhdr.awk b/x86-bare-metal/multiboot/scripts/check_mbhdr.awk
new file mode 100644
index 0000000..1471d69
--- /dev/null
+++ b/x86-bare-metal/multiboot/scripts/check_mbhdr.awk
@@ -0,0 +1,36 @@
+BEGIN {
+ # > xxd -d -e -c 4 IMG
+ # 00004096: 1badb002 ....
+ #
+ # Split at colon to easily extract file offset.
+ FS = ":"
+
+ # An OS image must contain an additional header called Multiboot header,
+ # besides the headers of the format used by the OS image. The Multiboot
+ # header must be contained completely within the first 8192 bytes of the OS
+ # image, and must be longword (32-bit) aligned. In general, it should come
+ # as early as possible, and may be embedded in the beginning of the text
+ # segment after the real executable header.
+ #
+ # https://www.gnu.org/software/grub/manual/multiboot/multiboot.html
+ MBHDR_LIMIT = 8192;
+ MBHDR_ALIGN = 4;
+
+ MBHDR_SIZE = 3 * 4;
+}
+
+/1badb002/ {
+ print $0" off="$1
+
+ if ($1 > MBHDR_LIMIT - MBHDR_SIZE) {
+ print "FAIL: multiboot header must be in the first "$MBHDR_SIZE" bytes of the image!"
+ exit 1
+ }
+
+ if ($1 % MBHDR_ALIGN != 0) {
+ print "FAIL: multiboot header must be 32bit aligned!"
+ exit 1
+ }
+
+ exit 0
+}
diff --git a/x86-bare-metal/multiboot/scripts/check_sse.awk b/x86-bare-metal/multiboot/scripts/check_sse.awk
new file mode 100644
index 0000000..91fb389
--- /dev/null
+++ b/x86-bare-metal/multiboot/scripts/check_sse.awk
@@ -0,0 +1,12 @@
+# Utility to check the disassembly for sse instructions. This uses a
+# simple heuristic by checking if there are any usaged of xmm, ymm or
+# zmm register.
+#
+# We want to build our kernel w/o sse instruction, as those first need
+# to be enabled in the cpus control register as well the sse state
+# needs to be initialized.
+
+/[xyz]mm[0-9]/ {
+ print "FAIL: no sse insns allowed in binary, found: "$0
+ exit 1
+}
diff --git a/x86-bare-metal/multiboot/vga.zig b/x86-bare-metal/multiboot/vga.zig
new file mode 100644
index 0000000..10d222c
--- /dev/null
+++ b/x86-bare-metal/multiboot/vga.zig
@@ -0,0 +1,117 @@
+/// VGA text mode console [1]
+///
+/// 8 colors + 8 bright variants:
+/// - 0x0 black (0x8 dark gray)
+/// - 0x1 blue (0x9 light blue)
+/// - 0x2 green (0xa light green)
+/// - 0x3 cyan (0xb light cyan)
+/// - 0x4 red (0xc light red)
+/// - 0x5 magenta (0xd pink)
+/// - 0x6 brown (0xe yellow)
+/// - 0x7 light gray (0xf white)
+///
+/// [1] https://en.wikipedia.org/wiki/VGA_text_mode
+pub fn vga(fg: u4, bg: u3) type {
+ return struct {
+ const Self = @This();
+
+ // Text mode dimensions (mode 3h text mode 80x25).
+ const COLS = 80;
+ const ROWS = 25;
+ // Slice into VGA video memory.
+ const VIDEO: []u16 = @as([*]u16, @ptrFromInt(0xB8000))[0 .. COLS * ROWS];
+
+ /// Current index into last line.
+ col: usize,
+
+ /// Default value.
+ pub const init: Self = .{
+ .col = 0,
+ };
+
+ /// Write string to screen and move cursor.
+ pub fn puts(self: *Self, msg: []const u8) void {
+ for (msg) |char| {
+ self.putcImpl(char);
+ }
+ self.updateCursor();
+ }
+
+ /// Write character to screen and move cursor.
+ pub fn putc(self: *Self, char: u8) void {
+ self.putcImpl(char);
+ self.updateCursor();
+ }
+
+ /// Write character to screen.
+ fn putcImpl(self: *Self, char: u8) void {
+ if (char == '\n') {
+ self.lf();
+ return;
+ }
+
+ const idx = (ROWS - 1) * COLS + self.col;
+ VIDEO[idx] = vgaEntry(char, fg, bg);
+
+ self.col += 1;
+ if (self.col == COLS) {
+ self.lf();
+ }
+ }
+
+ /// Handle linefeed (newline).
+ /// Move up all lines by one, shifting out the upper most line.
+ fn lf(self: *Self) void {
+ // Shift all rows one up, copy rows [1. ROWS[-> [0, ROWS-1[.
+ for (0..(ROWS - 1) * COLS) |idx| {
+ VIDEO[idx] = VIDEO[1 * COLS + idx];
+ }
+ // Clear ROWS-1 (last row).
+ for (0..COLS) |idx| {
+ VIDEO[(ROWS - 1) * COLS + idx] = vgaEntry(' ', fg, bg);
+ }
+ self.col = 0;
+ }
+
+ /// Update cursor to current position.
+ /// https://wiki.osdev.org/Text_Mode_Cursor#Without_the_BIOS
+ fn updateCursor(self: Self) void {
+ const idx = (ROWS - 1) * COLS + self.col;
+
+ outb(0x3d4, 0xf);
+ outb(0x3d5, @truncate(idx & 0xff));
+ outb(0x3d4, 0xe);
+ outb(0x3d5, @truncate(idx >> 8 & 0xff));
+ }
+
+ const Writer = @import("std").io.GenericWriter(*Self, anyerror, struct {
+ fn write(ctx: *Self, bytes: []const u8) anyerror!usize {
+ ctx.puts(bytes);
+ return bytes.len;
+ }
+ }.write);
+
+ pub fn writer(self: *Self) Writer {
+ return .{ .context = self };
+ }
+ };
+}
+
+// Each VGA (text mode) video buffer entry is 16bit wide.
+// [15] blink
+// [14:12] bg color (3 bit)
+// [11: 8] fg color (4 bit)
+// [ 7: 0] ascii character
+//
+// https://en.wikipedia.org/wiki/VGA_text_mode
+inline fn vgaEntry(char: u8, fg: u4, bg: u3) u16 {
+ return @as(u16, bg) << 12 | @as(u16, fg) << 8 | char;
+}
+
+inline fn outb(port: u16, val: u8) void {
+ _ = asm volatile ("outb %[al], %[dx]"
+ : // no outputs
+ : [al] "{al}" (val),
+ [dx] "{dx}" (port),
+ );
+}