diff options
Diffstat (limited to 'x86-bare-metal/multiboot')
-rw-r--r-- | x86-bare-metal/multiboot/Makefile | 25 | ||||
-rw-r--r-- | x86-bare-metal/multiboot/grub/grub.cfg | 10 | ||||
-rw-r--r-- | x86-bare-metal/multiboot/kern.ld | 27 | ||||
-rw-r--r-- | x86-bare-metal/multiboot/kern.zig | 47 | ||||
-rw-r--r-- | x86-bare-metal/multiboot/mb.zig | 220 | ||||
-rw-r--r-- | x86-bare-metal/multiboot/scripts/check_mbhdr.awk | 36 | ||||
-rw-r--r-- | x86-bare-metal/multiboot/scripts/check_sse.awk | 12 | ||||
-rw-r--r-- | x86-bare-metal/multiboot/vga.zig | 117 |
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), + ); +} |