summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--x86-bare-metal/mbr-com-serial/Makefile32
-rw-r--r--x86-bare-metal/mbr-com-serial/com.zig106
-rw-r--r--x86-bare-metal/mbr-com-serial/mbr.ld24
-rw-r--r--x86-bare-metal/mbr-com-serial/mbr.zig50
4 files changed, 212 insertions, 0 deletions
diff --git a/x86-bare-metal/mbr-com-serial/Makefile b/x86-bare-metal/mbr-com-serial/Makefile
new file mode 100644
index 0000000..9020ff8
--- /dev/null
+++ b/x86-bare-metal/mbr-com-serial/Makefile
@@ -0,0 +1,32 @@
+O := BUILD
+
+COMMON_FLAGS := -fno-unwind-tables -fno-strip -target x86-freestanding-code16 -O ReleaseSmall
+
+# Enable serial on stdio, such that in graphics mode it is enabled by default.
+QEMU_ARGS ?= -serial stdio
+
+$(O)/boot: $(O)/boot.elf | check_ep dump_info
+ objcopy -O binary $< $@
+
+$(O)/boot.elf: mbr.ld mbr.zig | $(O)
+ zig build-exe -femit-bin=$@ $(COMMON_FLAGS) -fno-entry --script $^
+
+clean:
+ $(RM) -r $(O)
+
+$(O):
+ mkdir -p $(O)
+
+# Check entry point is exactly at 0x7c00.
+check_ep: $(O)/boot.elf
+ readelf -h $< | awk '/Entry point address:/ { print $$4 }' | grep 0x7c00 >& /dev/null
+
+# Dump some debug info (disasm, elf load segments, code size).
+dump_info: $(O)/boot.elf
+ objdump -Mintel -m i8086 --disassemble --visualize-jumps=extended-color $<
+ readelf -W -l $<
+ size $<
+ size $< | awk '/$(notdir $<)/ { print "MBR utilization " $$1/512 "%" }'
+
+run: $(O)/boot
+ qemu-system-i386 -hda $< $(QEMU_ARGS)
diff --git a/x86-bare-metal/mbr-com-serial/com.zig b/x86-bare-metal/mbr-com-serial/com.zig
new file mode 100644
index 0000000..d6137ea
--- /dev/null
+++ b/x86-bare-metal/mbr-com-serial/com.zig
@@ -0,0 +1,106 @@
+// Support for COM serial ports [1].
+//
+// COM ports are accessed via IO ports. Each port exposes a set of 1 byte
+// registers relative to the ports base address.
+// 0 Receive buffer (inb).
+// 0 Transmit buffer (outb).
+// 1 Interrupt enable.
+// 2 Interrupt identification.
+// 3 Line control.
+// 4 Modem control.
+// 5 Line status.
+// 6 Modem status.
+// 7 Scratch register.
+//
+// If DLAB bit in the line control register is '1', then offset 0/1 are
+// remapped, to setup a 2 byte divisor value.
+// 0 Low byte of the divisor.
+// 1 High byte of the divisor.
+// The COM port has an internal clock at 115200 Hz.
+//
+// The bios data area (DBA) may provide the base address of COM1-COM4 ports [2].
+// The first two ports have fairly standardized base addresses
+// - COM1 0x3f8
+// - COM2 0x2f8
+//
+// [1] https://wiki.osdev.org/Serial_Ports
+// [2] https://wiki.osdev.org/Memory_Map_(x86)
+
+// COM port register offsets relative to COM port base address.
+const REG_DATA: u16 = 0;
+const REG_INTR_EN: u16 = 1;
+const REG_LINE_CTRL: u16 = 3;
+const REG_LINE_STAT: u16 = 5;
+// Remapped iff LINE_CTRL.DLAB == 1.
+const REG_DIVLO: u16 = 0;
+const REG_DIVHI: u16 = 1;
+
+// Bit masks for LINE_CTRL register.
+const BIT_DLAB: u8 = (1 << 7);
+
+// Bit masks for LINE_STAT register.
+const BIT_RX_READY: u8 = (1 << 0);
+const BIT_TX_READY: u8 = (1 << 5);
+
+/// Type to interact with x86 serial COM ports [1].
+///
+/// [1] https://wiki.osdev.org/Serial_Ports
+pub const Com = struct {
+ // Widelysed standardized address for COM1 port (eg also in qemu).
+ pub const COM1: u16 = 0x3f8;
+
+ base: u16,
+
+ /// Initialize COM port at `base` address, with following configuration
+ /// - all interrupts disabled (polling)
+ /// - divisor = 1 (-> baud rate 115200)
+ /// - 8 data, 1 stop, no parity bits (8N1)
+ pub fn init(base: u16) Com {
+ // Disable all COM port interrupts.
+ outb(base + REG_INTR_EN, 0);
+
+ // Set DLAB bit, to enable access to divisor registers.
+ outb(base + REG_LINE_CTRL, BIT_DLAB);
+
+ // Set divisor=1, configuring a baud rate of 115200.
+ outb(base + REG_DIVLO, 1);
+ outb(base + REG_DIVHI, 0);
+
+ // Unset DLAB bit, no parity, 1 stop bit, 8 data bits (8n1).
+ outb(base + REG_LINE_CTRL, 0b11);
+
+ return .{ .base = base };
+ }
+
+ /// Write out `msg` to COM port.
+ pub fn puts(self: Com, msg: []const u8) void {
+ for (msg) |ch| {
+ self.putc(ch);
+ }
+ }
+
+ /// Write out `char` to COM port.
+ pub fn putc(self: Com, char: u8) void {
+ while ((inb(self.base + REG_LINE_STAT) & BIT_TX_READY) == 0) {}
+ outb(self.base, char);
+ }
+};
+
+// -- UTILS ---------------------------------------------------------------------
+
+inline fn outb(port: u16, val: u8) void {
+ _ = asm volatile ("outb %[al], %[dx]"
+ : // No outputs.
+ : [al] "{al}" (val),
+ [dx] "{dx}" (port),
+ );
+}
+
+inline fn inb(port: u16) u8 {
+ var val: u8 = 0;
+ _ = asm volatile ("inb %[dx], %[al]"
+ : [al] "={al}" (val),
+ : [dx] "{dx}" (port),
+ );
+ return val;
+}
diff --git a/x86-bare-metal/mbr-com-serial/mbr.ld b/x86-bare-metal/mbr-com-serial/mbr.ld
new file mode 100644
index 0000000..9ada8c4
--- /dev/null
+++ b/x86-bare-metal/mbr-com-serial/mbr.ld
@@ -0,0 +1,24 @@
+/*OUTPUT_FORMAT(binary)*/
+OUTPUT_FORMAT(elf32-i386)
+OUTPUT_ARCH(i386)
+ENTRY(_entry)
+
+SECTIONS {
+ . = 0x7c00;
+ .boot : { *(.boot) }
+ .text : { *(.text) }
+ .data : { *(.data) }
+ .rodata : { *(.rodata) *(.rodata.str*) }
+ _boot_end = .;
+
+ . = 0x7c00 + 510;
+ .mbr.magic : {
+ BYTE(0x55);
+ BYTE(0xaa);
+ }
+
+ /*/DISCARD/ : { *(.*) }*/
+ /*rest : { *(.*) }*/
+
+ ASSERT(_boot_end - 0x7c00 < 510, "boot sector must fit in 510 bytes")
+}
diff --git a/x86-bare-metal/mbr-com-serial/mbr.zig b/x86-bare-metal/mbr-com-serial/mbr.zig
new file mode 100644
index 0000000..6c6fb77
--- /dev/null
+++ b/x86-bare-metal/mbr-com-serial/mbr.zig
@@ -0,0 +1,50 @@
+const Com = @import("com.zig").Com;
+
+// -- ENTRY POINT ---------------------------------------------------------------
+
+export fn _entry() linksection(".boot") callconv(.naked) noreturn {
+ asm volatile (
+ // Disable interrupts.
+ \\cli
+ // Clear segment selectors.
+ \\xor %%ax, %%ax
+ \\mov %%ax, %%ds
+ \\mov %%ax, %%es
+ \\mov %%ax, %%ss
+ \\mov %%ax, %%fs
+ \\mov %%ax, %%gs
+ \\mov $0x7c00, %sp
+ // Long jump to set cs to 0x0000, as some BIOSes load the MBR
+ // to either 07c0:0000 or 0000:7c000.
+ \\ljmp $0x0, $main
+ );
+}
+
+// -- MAIN ----------------------------------------------------------------------
+
+// Global access to COM0.
+var com0: ?Com = null;
+
+// main should be "callconv(.naked)", once issue is fixed.
+// https://github.com/ziglang/zig/issues/18183
+export fn main() noreturn {
+ com0 = com0 orelse Com.init(Com.COM1);
+ com0.?.puts("\n\nBooted into mbr.zig\n");
+
+ @panic("end reached, crashing kernel");
+}
+
+pub const panic = @import("std").debug.FullPanic(panicHandler);
+
+fn panicHandler(msg: []const u8, first_trace_addr: ?usize) noreturn {
+ _ = first_trace_addr;
+
+ com0 = com0 orelse Com.init(Com.COM1);
+ com0.?.puts("\nPANIC: ");
+ com0.?.puts(msg);
+ com0.?.puts("\n");
+
+ while (true) {
+ asm volatile ("hlt");
+ }
+}