summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--example-network/.gitignore2
-rw-r--r--example-network/Makefile3
-rw-r--r--example-network/pong.zig136
-rw-r--r--flake.nix19
-rw-r--r--x86-bare-metal/mbr-disk-lba/.gitignore1
-rw-r--r--x86-bare-metal/mbr-disk-lba/Makefile40
-rw-r--r--x86-bare-metal/mbr-disk-lba/mbr.S125
-rw-r--r--x86-bare-metal/mbr-disk-lba/mbr.ld26
-rw-r--r--x86-bare-metal/mbr-disk-lba/zmbr.zig66
-rw-r--r--x86-bare-metal/mbr-e820/Makefile2
-rw-r--r--x86-bare-metal/mbr-textmode/.gitignore1
-rw-r--r--x86-bare-metal/mbr-textmode/Makefile28
-rw-r--r--x86-bare-metal/mbr-textmode/mbr.S98
-rw-r--r--x86-bare-metal/mbr-textmode/mbr.ld25
-rw-r--r--x86-bare-metal/mbr-textmode/zmbr.zig39
15 files changed, 610 insertions, 1 deletions
diff --git a/example-network/.gitignore b/example-network/.gitignore
new file mode 100644
index 0000000..5263b7f
--- /dev/null
+++ b/example-network/.gitignore
@@ -0,0 +1,2 @@
+pong
+pong.o* \ No newline at end of file
diff --git a/example-network/Makefile b/example-network/Makefile
index 8fc8a1c..7b51314 100644
--- a/example-network/Makefile
+++ b/example-network/Makefile
@@ -2,3 +2,6 @@ s:
zig run basic.zig -- -s
c:
zig run basic.zig
+
+pong: pong.zig
+ zig build-exe $^ -O ReleaseFast
diff --git a/example-network/pong.zig b/example-network/pong.zig
new file mode 100644
index 0000000..3ef861c
--- /dev/null
+++ b/example-network/pong.zig
@@ -0,0 +1,136 @@
+const std = @import("std");
+
+pub const std_options: std.Options = .{
+ .log_level = .info,
+};
+
+const default_opts = struct {
+ /// Whether to start the server or the client side.
+ is_server: bool,
+ /// IPv4 address to listen (server) on or connect (client) to.
+ addr: [:0]const u8,
+ /// Message size send between client and server.
+ msg_size: usize,
+}{ .is_server = true, .addr = "127.0.0.1", .msg_size = 4096 };
+
+/// Parse command line arguments.
+fn parse_args() !?@TypeOf(default_opts) {
+ var opts = default_opts;
+ var args = std.process.args();
+
+ // Skip program name.
+ _ = args.skip();
+
+ return while (args.next()) |arg| {
+ if (std.mem.eql(u8, arg, "-c")) {
+ opts.is_server = false;
+ } else if (std.mem.eql(u8, arg, "-a")) {
+ opts.addr = args.next() orelse break error.MissingAddressValue;
+ } else if (std.mem.eql(u8, arg, "-m")) {
+ const bytes = args.next() orelse break error.MissingMessageSizeBytesValue;
+ opts.msg_size = try std.fmt.parseInt(usize, bytes, 10);
+ } else if (std.mem.eql(u8, arg, "-h")) {
+ std.debug.print("Usage: pong [args]\n", .{});
+ std.debug.print(" -c start client, rather than server\n", .{});
+ std.debug.print(" -a <addr> listen/connect IPv4 addr (default: {s})\n", .{default_opts.addr});
+ std.debug.print(" -m <bytes> message size in bytes, client only (default: {})\n", .{default_opts.msg_size});
+ std.debug.print(" -h this help message\n", .{});
+ break null;
+ } else {
+ std.log.err("Unknown argument '{s}'", .{arg});
+ break null;
+ }
+ } else opts;
+}
+
+fn run_server(alloc: std.mem.Allocator, addr: std.net.Address) !void {
+ // Start a new server.
+ var server = try addr.listen(.{ .reuse_address = true });
+ defer server.deinit();
+
+ while (true) {
+ // Accept a new client.
+ std.log.info("server listening on {}", .{addr});
+ const client = (try server.accept()).stream;
+ defer client.close();
+
+ // Allocate message buffer from client provided message size.
+ const buf = blk: {
+ // (1) Read message size from client (format: "<bytes>\n").
+ var msg_buf: [16]u8 = undefined;
+ const msg = try client.reader().readUntilDelimiter(&msg_buf, '\n');
+ const msg_size = try std.fmt.parseInt(usize, msg, 10);
+
+ // (2) Allocate actual message buffer.
+ std.log.info("allocate message buffer with {} bytes", .{msg_size});
+ break :blk try alloc.alloc(u8, msg_size);
+ };
+ defer alloc.free(buf);
+
+ // Function to receive and loop back message buffer.
+ const loop_message = struct {
+ fn call(c: std.net.Stream, b: []u8) !void {
+ _ = try c.readAll(b);
+ try c.writeAll(b);
+ }
+ }.call;
+
+ // Send message buffer in a loop, on error wait for a new client.
+ while (true) {
+ loop_message(client, buf) catch break;
+ }
+ }
+}
+
+fn run_client(alloc: std.mem.Allocator, addr: std.net.Address, msg_size: usize) !void {
+ // Connect to a server.
+ std.log.info("client connecting to {}", .{addr});
+ var con = try std.net.tcpConnectToAddress(addr);
+ defer con.close();
+
+ // Send message size and allocate message buffer.
+ const buf = blk: {
+ // (1) Send message size to server (format: "<bytes>\n").
+ var msg_buf: [16]u8 = undefined;
+ const msg = try std.fmt.bufPrint(&msg_buf, "{}\n", .{msg_size});
+ try con.writeAll(msg);
+
+ // (2) Allocate actual message buffer.
+ std.log.info("allocate message buffer with {} bytes", .{msg_size});
+ break :blk try alloc.alloc(u8, msg_size);
+ };
+ defer alloc.free(buf);
+
+ // Send message buffer in a loop, and periodically report throughput.
+ while (true) {
+ var count: usize = 0;
+ const start = try std.time.Instant.now();
+ var delta: u64 = 0;
+
+ while (delta < std.time.ns_per_s) : (delta = (try std.time.Instant.now()).since(start)) {
+ try con.writeAll(buf);
+ _ = try con.readAll(buf);
+ count += 1;
+ }
+
+ const bytes_per_sec = count * 2 * buf.len * (delta / std.time.ns_per_s);
+ std.log.info("{:.2}/sec ping-pong messages", .{std.fmt.fmtIntSizeBin(bytes_per_sec)});
+ }
+}
+
+pub fn main() !void {
+ // Command line options.
+ const opts = try parse_args() orelse return;
+ const addr = try std.net.Address.parseIp4(opts.addr, 8080);
+
+ // Allocator.
+ var gpa = std.heap.GeneralPurposeAllocator(.{}).init;
+ defer std.debug.assert(gpa.deinit() == .ok);
+ const alloc = gpa.allocator();
+
+ // Run server or client.
+ try if (opts.is_server)
+ run_server(alloc, addr)
+ else
+ run_client(alloc, addr, opts.msg_size);
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000..ab93306
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,19 @@
+{
+ description = "Zig development environment";
+
+ inputs = {
+ nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
+ };
+
+ outputs = { self, nixpkgs, ... }: let
+ system = "x86_64-linux";
+ pkgs = import nixpkgs { inherit system; };
+ in {
+ devShells.${system}.default = pkgs.mkShell {
+ buildInputs = [
+ pkgs.zig
+ pkgs.zls
+ ];
+ };
+ };
+}
diff --git a/x86-bare-metal/mbr-disk-lba/.gitignore b/x86-bare-metal/mbr-disk-lba/.gitignore
new file mode 100644
index 0000000..a92a67f
--- /dev/null
+++ b/x86-bare-metal/mbr-disk-lba/.gitignore
@@ -0,0 +1 @@
+BUILD/ \ No newline at end of file
diff --git a/x86-bare-metal/mbr-disk-lba/Makefile b/x86-bare-metal/mbr-disk-lba/Makefile
new file mode 100644
index 0000000..f897450
--- /dev/null
+++ b/x86-bare-metal/mbr-disk-lba/Makefile
@@ -0,0 +1,40 @@
+O := BUILD
+
+all: dump_elf dump_bin
+
+$(O)/boot: $(O)/boot.elf
+ # MBR 512 bytes (sector 1 - lba 0)
+ objcopy -O binary $< $@
+ # Craft 512 bytes (sector 2 - lba 1)
+ printf "aaaa" >> $@
+ dd if=/dev/zero bs=1 count=508 >> $@
+ # Craft 512 bytes (sector 3 - lba 2)
+ printf "bbbb" >> $@
+ dd if=/dev/zero bs=1 count=508 >> $@
+
+$(O)/boot.elf: mbr.ld $(O)/mbr.o $(O)/zmbr.o
+ ld -o $@ -nostdlib -T $^
+
+$(O)/mbr.o: mbr.S | $(O)
+ gcc -m32 -c -o $@ -ffreestanding mbr.S
+
+$(O)/zmbr.o: zmbr.zig | $(O)
+ zig build-obj -fno-strip -femit-bin=$@ -target x86-freestanding-none -O ReleaseSmall $<
+
+clean:
+ $(RM) -r $(O)
+
+$(O):
+ mkdir -p $(O)
+
+dump_elf: $(O)/boot.elf
+ @#objdump -Mintel --disassemble=kmain --visualize-jumps=extended-color $<
+ readelf -W -l $<
+ size $<
+ size $< | awk '/$(notdir $<)/ { print "MBR utilization " $$1/512 "%" }'
+
+dump_bin: $(O)/boot
+ hexdump -C $<
+
+run: $(O)/boot
+ qemu-system-i386 -hda $< $(QEMU_ARGS)
diff --git a/x86-bare-metal/mbr-disk-lba/mbr.S b/x86-bare-metal/mbr-disk-lba/mbr.S
new file mode 100644
index 0000000..df2cef1
--- /dev/null
+++ b/x86-bare-metal/mbr-disk-lba/mbr.S
@@ -0,0 +1,125 @@
+// -- BOOT TEXT SECTION ---------------------------------------------------------
+
+.code16
+.intel_syntax noprefix
+
+.section .boot, "ax", @progbits
+ // Disable interrupts.
+ cli
+
+ // Clear segment selectors.
+ xor ax, ax
+ mov ds, ax
+ mov es, ax
+ mov ss, ax
+ mov fs, ax
+ mov gs, ax
+
+ // Set cs to 0x0000, as some BIOSes load the MBR to either 07c0:0000 or 0000:7c000.
+ jmp 0x0000:entry_rm16
+
+// LBA - disk address packet.
+lba_pkt:
+ .byte 0x10 // Size of this disk packet in bytes (16).
+ .byte 0 // Reserved.
+ .2byte 2 // Number of blocks to read (sectors 512 bytes?) to read.
+ .4byte 0x7e00 // Destination address.
+ .8byte 1 // Starting lba block number (0 indexed, MBR is at 0).
+
+entry_rm16:
+ // Disk extended read.
+ // ah = 42h
+ // dl = drive number
+ // ds:si = address of disk packet
+ // Return
+ // cf = 0 (success) 1 (failed)
+ // ah = 0 (success) error code (failed)
+
+ // [1] http://www.ctyme.com/intr/rb-0708.htm
+ mov ah, 0x42
+ //mov dl, #drive // bios puts boot disk into dl
+ lea si, [lba_pkt]
+ int 0x13
+
+ jnc 2f
+1:
+ hlt
+ jmp 1b
+2:
+ // Get current video mode [1].
+ // Return:
+ // ah number of columns
+ // al display mode (see table in [2])
+ //
+ // [1] http://www.ctyme.com/intr/rb-0108.htm
+ // [2] http://www.ctyme.com/intr/rb-0069.htm
+ mov ah, 0xf
+ int 0x10
+
+ // Execpt that the bios initializes text mode 0x3.
+ // * 80x25 text mode (cols x rows)
+ // * 2 byte per character
+ // [15] blink [14:12] bg color [11:8] fg color [7:0] char
+ // * 0xB80000 screen address
+ cmp al, 0x3
+ // Else we indicate an error with a blue screen.
+ je 2f
+ // Set background color [1].
+ //
+ // [1] http://www.ctyme.com/intr/rb-0101.htm
+ mov ah, 0xb
+ mov bx, 1
+ int 0x10
+1:
+ hlt
+ jmp 1b
+2:
+
+ // Enable A20 address line.
+ in al, 0x92
+ or al, 2
+ out 0x92, al
+
+ // Load GDT descriptor.
+ lgdt [gdt_desc]
+
+ // Enable protected mode (set CR0.PE bit).
+ mov eax, cr0
+ or eax, (1 << 0)
+ mov cr0, eax
+
+ // Far jump which loads segment selector (0x0008) into cs.
+ // 0x0008 -> RPL=0, TI=0(GDT), I=1
+ jmp 0x0008:entry_pm32
+
+.code32
+entry_pm32:
+ // Select data segment selector (0x0010) for ds.
+ mov ax, gdt_data - gdt
+ mov ds, ax
+
+ // Initialize stack pointer.
+ // Real Mode memory (https://wiki.osdev.org/Memory_Map_(x86)
+ // 0x00000500 - 0x00007BFF | 29.75 KiB | conventional memory
+ mov esp, 0x7c00
+
+ // Enter zmbr.zig:kmain.
+ // Should not return, but for safety we emit a call rather than a jmp.
+ call kmain
+
+1:
+ hlt
+ jmp 1b
+
+// -- RODATA SECTION ------------------------------------------------------------
+
+.section .rodata, "a", @progbits
+.balign 8
+gdt:
+ .8byte 0x0000000000000000 // 0x00 | null descriptor
+ .8byte 0x00cf9a000000ffff // 0x08 | 32 bit, code (rx), present, dpl=0, g=4K, base=0, limit=fffff
+gdt_data:
+ .8byte 0x00cf92000000ffff // 0x10 | 32 bit, data (rw), present, dpl=0, g=4K, base=0, limit=fffff
+gdt_desc:
+ .2byte (. - gdt - 1) // size
+ .4byte gdt // address
diff --git a/x86-bare-metal/mbr-disk-lba/mbr.ld b/x86-bare-metal/mbr-disk-lba/mbr.ld
new file mode 100644
index 0000000..b93543a
--- /dev/null
+++ b/x86-bare-metal/mbr-disk-lba/mbr.ld
@@ -0,0 +1,26 @@
+/*OUTPUT_FORMAT(binary)*/
+OUTPUT_FORMAT(elf32-i386)
+OUTPUT_ARCH(i386)
+
+SECTIONS {
+ . = 0x7c00;
+ .boot : {
+ *(.boot)
+ }
+ .text : { *(.text) }
+ .data : { *(.data) }
+ .bss : { *(.bss) }
+ .rodata : { *(.rodata) }
+ _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-disk-lba/zmbr.zig b/x86-bare-metal/mbr-disk-lba/zmbr.zig
new file mode 100644
index 0000000..e5b06ea
--- /dev/null
+++ b/x86-bare-metal/mbr-disk-lba/zmbr.zig
@@ -0,0 +1,66 @@
+// Frambuffer limits.
+const COLS = 80;
+const ROWS = 25;
+
+// Frambuffer cursor.
+var col: u16 = 0;
+var row: u16 = 0;
+
+// Frambuffer.
+const fb: []u16 = @as([*]u16, @ptrFromInt(0xB8000))[0 .. COLS * ROWS];
+
+/// Clear screen (all black).
+fn clear_screen() void {
+ for (fb) |*ch| {
+ ch.* = 0;
+ }
+}
+
+/// Draw string to current cursor position.
+fn puts(str: []const u8) void {
+ // Each framebuffer entry in text mode 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
+ for (str) |ch| {
+ if (ch == '\n') {
+ col = 0;
+ row += 1;
+ } else {
+ const pos = (row * COLS + col);
+ // bg - black; fg - white;
+ fb[pos] = @as(u16, 15) << 8 | ch;
+ col += 1;
+ }
+ if (col == COLS) {
+ row += 1;
+ if (row == ROWS) {
+ row = 0;
+ }
+ }
+ }
+}
+
+// kmain should be "callconv(.naked)", once issue is fixed.
+// https://github.com/ziglang/zig/issues/18183
+export fn kmain() noreturn {
+ clear_screen();
+
+ // Print first bytes of LBA block 1 we loaded from disk (sector 2).
+ const lba1: []const u8 = @as([*]const u8, @ptrFromInt(0x7e00))[0..4];
+ puts("lba1: ");
+ puts(lba1);
+ puts("\n");
+
+ // Print first bytes of LBA block 2 we loaded from disk (sector 3).
+ const lba2: []const u8 = @as([*]const u8, @ptrFromInt(0x8000))[0..4];
+ puts("lba2: ");
+ puts(lba2);
+ puts("\n");
+
+ while (true) {
+ asm volatile ("hlt");
+ }
+}
diff --git a/x86-bare-metal/mbr-e820/Makefile b/x86-bare-metal/mbr-e820/Makefile
index 5dc465f..99ed9d8 100644
--- a/x86-bare-metal/mbr-e820/Makefile
+++ b/x86-bare-metal/mbr-e820/Makefile
@@ -1,6 +1,6 @@
O := BUILD
-COMMON_FLAGS := -fno-unwind-tables -fno-strip -target x86-freestanding-code16 -O ReleaseSmall -mcpu=i386
+COMMON_FLAGS := -fno-unwind-tables -fno-strip -target x86-freestanding-code16 -O ReleaseSmall -mcpu=i386 -fomit-frame-pointer
$(O)/boot: $(O)/boot.elf | check_ep dump_info
objcopy -O binary $< $@
diff --git a/x86-bare-metal/mbr-textmode/.gitignore b/x86-bare-metal/mbr-textmode/.gitignore
new file mode 100644
index 0000000..a92a67f
--- /dev/null
+++ b/x86-bare-metal/mbr-textmode/.gitignore
@@ -0,0 +1 @@
+BUILD/ \ No newline at end of file
diff --git a/x86-bare-metal/mbr-textmode/Makefile b/x86-bare-metal/mbr-textmode/Makefile
new file mode 100644
index 0000000..7642791
--- /dev/null
+++ b/x86-bare-metal/mbr-textmode/Makefile
@@ -0,0 +1,28 @@
+O := BUILD
+
+$(O)/boot: $(O)/boot.elf | dump_info
+ objcopy -O binary $< $@
+
+$(O)/boot.elf: mbr.ld $(O)/mbr.o $(O)/zmbr.o
+ ld -o $@ -nostdlib -T $^
+
+$(O)/mbr.o: mbr.S | $(O)
+ gcc -m32 -c -o $@ -ffreestanding mbr.S
+
+$(O)/zmbr.o: zmbr.zig | $(O)
+ zig build-obj -femit-bin=$@ -target x86-freestanding-none -O ReleaseSmall $<
+
+clean:
+ $(RM) -r $(O)
+
+$(O):
+ mkdir -p $(O)
+
+dump_info: $(O)/boot.elf
+ objdump -Mintel --disassemble=kmain --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-textmode/mbr.S b/x86-bare-metal/mbr-textmode/mbr.S
new file mode 100644
index 0000000..be41bed
--- /dev/null
+++ b/x86-bare-metal/mbr-textmode/mbr.S
@@ -0,0 +1,98 @@
+// -- BOOT TEXT SECTION ---------------------------------------------------------
+
+.code16
+.intel_syntax noprefix
+
+.section .boot, "ax", @progbits
+ // Disable interrupts.
+ cli
+
+ // Clear segment selectors.
+ xor ax, ax
+ mov ds, ax
+ mov es, ax
+ mov ss, ax
+ mov fs, ax
+ mov gs, ax
+
+ // Set cs to 0x0000, as some BIOSes load the MBR to either 07c0:0000 or 0000:7c000.
+ jmp 0x0000:entry_rm16
+
+entry_rm16:
+ // Get current video mode [1].
+ // Return:
+ // ah number of columns
+ // al display mode (see table in [2])
+ //
+ // [1] http://www.ctyme.com/intr/rb-0108.htm
+ // [2] http://www.ctyme.com/intr/rb-0069.htm
+ mov ah, 0xf
+ int 0x10
+
+ // Execpt that the bios initializes text mode 0x3.
+ // * 80x25 text mode (cols x rows)
+ // * 2 byte per character
+ // [15] blink [14:12] bg color [11:8] fg color [7:0] char
+ // * 0xB80000 screen address
+ cmp al, 0x3
+ // Else we indicate an error with a blue screen.
+ je 2f
+ // Set background color [1].
+ //
+ // [1] http://www.ctyme.com/intr/rb-0101.htm
+ mov ah, 0xb
+ mov bx, 1
+ int 0x10
+1:
+ hlt
+ jmp 1b
+2:
+
+ // Enable A20 address line.
+ in al, 0x92
+ or al, 2
+ out 0x92, al
+
+ // Load GDT descriptor.
+ lgdt [gdt_desc]
+
+ // Enable protected mode (set CR0.PE bit).
+ mov eax, cr0
+ or eax, (1 << 0)
+ mov cr0, eax
+
+ // Far jump which loads segment selector (0x0008) into cs.
+ // 0x0008 -> RPL=0, TI=0(GDT), I=1
+ jmp 0x0008:entry_pm32
+
+.code32
+entry_pm32:
+ // Select data segment selector (0x0010) for ds.
+ mov ax, gdt_data - gdt
+ mov ds, ax
+
+ // Initialize stack pointer.
+ // Real Mode memory (https://wiki.osdev.org/Memory_Map_(x86)
+ // 0x00000500 - 0x00007BFF | 29.75 KiB | conventional memory
+ mov esp, 0x7c00
+
+ // Enter zmbr.zig:kmain.
+ // Should not return, but for safety we emit a call rather than a jmp.
+ call kmain
+
+1:
+ hlt
+ jmp 1b
+
+// -- RODATA SECTION ------------------------------------------------------------
+
+.section .rodata, "a", @progbits
+.balign 8
+gdt:
+ .8byte 0x0000000000000000 // 0x00 | null descriptor
+ .8byte 0x00cf9a000000ffff // 0x08 | 32 bit, code (rx), present, dpl=0, g=4K, base=0, limit=fffff
+gdt_data:
+ .8byte 0x00cf92000000ffff // 0x10 | 32 bit, data (rw), present, dpl=0, g=4K, base=0, limit=fffff
+gdt_desc:
+ .2byte (. - gdt - 1) // size
+ .4byte gdt // address
diff --git a/x86-bare-metal/mbr-textmode/mbr.ld b/x86-bare-metal/mbr-textmode/mbr.ld
new file mode 100644
index 0000000..372ea42
--- /dev/null
+++ b/x86-bare-metal/mbr-textmode/mbr.ld
@@ -0,0 +1,25 @@
+/*OUTPUT_FORMAT(binary)*/
+OUTPUT_FORMAT(elf32-i386)
+OUTPUT_ARCH(i386)
+
+SECTIONS {
+ . = 0x7c00;
+ .boot : {
+ *(.boot)
+ }
+ .text : { *(.text) }
+ .data : { *(.data) }
+ .rodata : { *(.rodata) }
+ _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-textmode/zmbr.zig b/x86-bare-metal/mbr-textmode/zmbr.zig
new file mode 100644
index 0000000..2d95249
--- /dev/null
+++ b/x86-bare-metal/mbr-textmode/zmbr.zig
@@ -0,0 +1,39 @@
+const COLS = 80;
+const ROWS = 25;
+
+/// Clear screen (all black).
+fn clear_screen(video: []u16) void {
+ for (video) |*ch| {
+ ch.* = 0;
+ }
+}
+
+/// Draw the color palette.
+fn draw_palette(video: []u16) void {
+ // Each framebuffer entry in text mode 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
+
+ // Print each bg / fg combination once.
+ for (video, 0..0x80) |*ch, i| {
+ ch.* = @as(u16, @truncate(i)) << 8 | 'a';
+ }
+}
+
+// kmain should be "callconv(.naked)", once issue is fixed.
+// https://github.com/ziglang/zig/issues/18183
+export fn kmain() noreturn {
+ // Take a slice to VGA video memory (mode 3h text mode 80x25).
+ const video: []u16 = @as([*]u16, @ptrFromInt(0xB8000))[0 .. COLS * ROWS];
+
+ clear_screen(video);
+ draw_palette(video);
+
+ while (true) {
+ asm volatile ("hlt");
+ }
+}