diff options
Diffstat (limited to 'boot')
| -rw-r--r-- | boot/boot.zig | 43 | ||||
| -rw-r--r-- | boot/idt.zig | 115 | ||||
| -rw-r--r-- | boot/keyboard.zig | 42 | ||||
| -rw-r--r-- | boot/pic.zig | 22 | ||||
| -rw-r--r-- | boot/port.zig | 25 | ||||
| -rw-r--r-- | boot/serial.zig | 18 |
6 files changed, 223 insertions, 42 deletions
diff --git a/boot/boot.zig b/boot/boot.zig index 1f50d83..e6342ce 100644 --- a/boot/boot.zig +++ b/boot/boot.zig @@ -6,6 +6,7 @@ const gdt = @import("gdt.zig"); const idt = @import("idt.zig"); const pmm = @import("pmm.zig"); const heap = @import("heap.zig"); +const keyboard = @import("keyboard.zig"); const bridge = @import("bridge"); // We need a panic handler otherwise zig won't be happy, @@ -52,8 +53,8 @@ export fn _start() callconv(.c) noreturn { gdt.load(); idt.init(); - - + // NOTE: CPU interrupts stay off until the event loop's sti + // which is after the VM has run that\ serial.write("gdt + idt loaded\r\n"); // HHDM + memory map -> physical frame allocator -> kernel heap @@ -84,23 +85,29 @@ export fn _start() callconv(.c) noreturn { pmm.free(b); pmm.free(c); - // hand the kernel heap to revo and we're gucci - bridge.run(heap.allocator(), serial.write); + // hand the kernel heap + raw framebuffer to revo and bring up the VM + const fb: ?bridge.Fb = if (framebuffer.get()) |s| .{ + .ptr = s.address, + .width = s.width, + .height = s.height, + .pitch = s.pitch, + } else null; + bridge.boot(heap.allocator(), serial.write, fb); - if (framebuffer.get()) |screen| { - // XOR texture - var y: usize = 0; - while (y < screen.height) : (y += 1) { - var x: usize = 0; - while (x < screen.width) : (x += 1) { - const v: u32 = @intCast((x ^ y) & 0xFF); - framebuffer.putpixel(screen, x, y, (v << 16) | (v << 8) | v); - } + // - sleep until an interrupt + // - drain the keyboard + // - repeat. + // + // the cli/pop/sti-hlt bullshit closes the lost-wakeup race (a key + // arriving between "queue empty" and hlt would otherwise sit until + // the next keypress) + while (true) { + asm volatile ("cli"); + if (keyboard.pop()) |sc| { + asm volatile ("sti"); + bridge.onKey(sc); + } else { + asm volatile ("sti; hlt"); } - serial.write("drew to the framebuffer\r\n"); - } else { - serial.write("limine was a piece of shit.\r\n"); } - - while (true) asm volatile ("hlt"); } diff --git a/boot/idt.zig b/boot/idt.zig index 127fd8e..a9898d6 100644 --- a/boot/idt.zig +++ b/boot/idt.zig @@ -6,6 +6,8 @@ const gdt = @import("gdt.zig"); const serial = @import("serial.zig"); +const pic = @import("pic.zig"); +const keyboard = @import("keyboard.zig"); // 64bit interrupt gate const Gate = packed struct { @@ -26,7 +28,7 @@ const Idtr = packed struct { var idt = [_]Gate{@bitCast(@as(u128, 0))} ** 256; var idtr: Idtr = undefined; -// what the common stub leaves on the stack, low address -> high. +// what the common stub leaves on the stack, low address -> high. // `mov rdi, rsp` hands the handler a pointer to this const Frame = extern struct { r15: u64, @@ -55,17 +57,17 @@ const Frame = extern struct { }; const names = [_][]const u8{ - "divide by zero", "debug", - "non-maskable interrupt", "breakpoint", - "overflow", "bound range exceeded", - "invalid opcode", "device not available", - "double fault", "coprocessor segment overrun", - "invalid TSS", "segment not present", - "stack-segment fault", "general protection fault", - "page fault", "reserved", - "x87 floating-point", "alignment check", - "machine check", "SIMD floating-point", - "virtualization", "control protection", + "divide by zero", "debug", + "non-maskable interrupt", "breakpoint", + "overflow", "bound range exceeded", + "invalid opcode", "device not available", + "double fault", "coprocessor segment overrun", + "invalid TSS", "segment not present", + "stack-segment fault", "general protection fault", + "page fault", "reserved", + "x87 floating-point", "alignment check", + "machine check", "SIMD floating-point", + "virtualization", "control protection", }; fn name(vector: u64) []const u8 { @@ -134,23 +136,95 @@ fn writeHex(value: u64) void { export fn exceptionHandler(frame: *Frame) callconv(.c) noreturn { serial.write("\r\n!!! CPU DID AN OOPSIE: "); serial.write(name(frame.vector)); - serial.write("\r\n vector="); writeHex(frame.vector); - serial.write(" err="); writeHex(frame.error_code); - serial.write("\r\n rip="); writeHex(frame.rip); - serial.write(" cs="); writeHex(frame.cs); - serial.write("\r\n rflags="); writeHex(frame.rflags); - serial.write(" rsp="); writeHex(frame.rsp); + serial.write("\r\n vector="); + writeHex(frame.vector); + serial.write(" err="); + writeHex(frame.error_code); + serial.write("\r\n rip="); + writeHex(frame.rip); + serial.write(" cs="); + writeHex(frame.cs); + serial.write("\r\n rflags="); + writeHex(frame.rflags); + serial.write(" rsp="); + writeHex(frame.rsp); // page faults stash the offending address in CR2 if (frame.vector == 14) { const cr2 = asm volatile ("mov %%cr2, %[out]" : [out] "=r" (-> u64), ); - serial.write("\r\n cr2="); writeHex(cr2); + serial.write("\r\n cr2="); + writeHex(cr2); } serial.write("\r\n"); while (true) asm volatile ("hlt"); } +// hardware IRQ. +// unlike exceptions these must return so we acknowledge the PIC and iretq +// back to whatever we interrupted. the stub mirrors the exception one (dummy +// error code + vector for a uniform Frame) but jumps to the returning path. +fn irqStub(comptime vector: u8) fn () callconv(.naked) void { + return struct { + fn entry() callconv(.naked) void { + asm volatile ("pushq $0"); // dummy error code, keeps Frame uniform + asm volatile ("pushq %[v]\n jmp irqCommon" + : + : [v] "i" (@as(u32, vector)), + ); + } + }.entry; +} + +// same register save as isrCommon, but afterwards we restore everything, drop +// the pushed vector + error code, and iretq instead of halting. +export fn irqCommon() callconv(.naked) void { + asm volatile ( + \\ push %rax + \\ push %rbx + \\ push %rcx + \\ push %rdx + \\ push %rsi + \\ push %rdi + \\ push %rbp + \\ push %r8 + \\ push %r9 + \\ push %r10 + \\ push %r11 + \\ push %r12 + \\ push %r13 + \\ push %r14 + \\ push %r15 + \\ mov %rsp, %rdi + \\ call irqDispatch + \\ pop %r15 + \\ pop %r14 + \\ pop %r13 + \\ pop %r12 + \\ pop %r11 + \\ pop %r10 + \\ pop %r9 + \\ pop %r8 + \\ pop %rbp + \\ pop %rdi + \\ pop %rsi + \\ pop %rdx + \\ pop %rcx + \\ pop %rbx + \\ pop %rax + \\ add $16, %rsp + \\ iretq + ); +} + +export fn irqDispatch(frame: *Frame) callconv(.c) void { + switch (frame.vector) { + pic.MASTER_OFFSET + 1 => keyboard.onIrq(), // IRQ1: keyboard + else => {}, + } + pic.eoi(@intCast(frame.vector - pic.MASTER_OFFSET)); +} + fn setGate(vector: u8, handler: u64) void { idt[vector] = .{ .offset_low = @truncate(handler), @@ -166,6 +240,9 @@ fn setGate(vector: u8, handler: u64) void { // fill the exception vectors and load the IDT pub fn init() void { inline for (0..32) |v| setGate(v, @intFromPtr(&stub(v))); + // IRQ1 (keyboard) -> vector 0x21 + // the PIC is remapped separately in boot. + setGate(pic.MASTER_OFFSET + 1, @intFromPtr(&irqStub(pic.MASTER_OFFSET + 1))); idtr = .{ .limit = @sizeOf(@TypeOf(idt)) - 1, .base = @intFromPtr(&idt) }; asm volatile ("lidt (%[idtr])" : diff --git a/boot/keyboard.zig b/boot/keyboard.zig new file mode 100644 index 0000000..8e3a58f --- /dev/null +++ b/boot/keyboard.zig @@ -0,0 +1,42 @@ +// top half of the PS/2 keyboard driver. +// the IRQ1 handler calls onIrq and reads the raw scancode out +// of the controller and drops it in a ring buffer. thats ALLLLLL it does +// no decoding, no allocation, nothing that could (should) fault or reenter +// the only reason this exists is because the interrupt handler is not a safe +// spot to execute revo code and shit can break easily and i am NOT going to debug +// it so its just better to do it like this. +// +// since there is just one producer (ISR) and one consumer (the event loop) +// i dont need to make a lock, the ISR just moves the tail and the loop just +// moves the head + +const port = @import("port.zig"); + +const DATA_PORT: u16 = 0x60; +const SIZE: usize = 256; // u8 indices wrap mod 256 for free + +var buf: [SIZE]u8 = undefined; +var head: u8 = 0; // next slot the loop will read +var tail: u8 = 0; // next slot the ISR will write + +// called from the IRQ1 handler. read the scancode (which also ACKs the +// controller so it'll send the next one) and enqueue it, dropping it on the +// floor if the buffer is somehow full. +pub fn onIrq() void { + const code = port.inb(DATA_PORT); + const t: *volatile u8 = &tail; + const h: *volatile u8 = &head; + if (t.* +% 1 == h.*) return; // full, drop + buf[t.*] = code; + t.* +%= 1; +} + +// called from the event loop. next queued scancode, or null if empty. +pub fn pop() ?u8 { + const t: *volatile u8 = &tail; + const h: *volatile u8 = &head; + if (h.* == t.*) return null; + const code = buf[h.*]; + h.* +%= 1; + return code; +} diff --git a/boot/pic.zig b/boot/pic.zig new file mode 100644 index 0000000..0bec4a8 --- /dev/null +++ b/boot/pic.zig @@ -0,0 +1,22 @@ +// this is just a 8259 PIC thing in zig, kinda defines only the constants the +// IDT needs cause the actual remapping and masking is done in the kernel level +// which is pretty cool.EOI has to stay here cause it runs inside the ISR tho, +// so somewhere the VM cant really do much. +// +// see: https://wiki.osdev.org/8259_PIC + +const port = @import("port.zig"); + +const MASTER_CMD: u16 = 0x20; +const SLAVE_CMD: u16 = 0xA0; +const EOI: u8 = 0x20; // end-of-interrupt command + +pub const MASTER_OFFSET: u8 = 0x20; // IRQ0..7 -> vectors 0x20..0x27 +pub const SLAVE_OFFSET: u8 = 0x28; // IRQ8..15 -> vectors 0x28..0x2F + +// tell the PIC we're done. the slave's interrupts also need the master EOI'd +// since they arrive via the cascade. +pub fn eoi(irq: u4) void { + if (irq >= 8) port.outb(SLAVE_CMD, EOI); + port.outb(MASTER_CMD, EOI); +} diff --git a/boot/port.zig b/boot/port.zig new file mode 100644 index 0000000..05668d3 --- /dev/null +++ b/boot/port.zig @@ -0,0 +1,25 @@ +// x86 port I/O +// +// these are pretty important cause all drivers which rely +// on serial rely on these 2 instructions so + +pub inline fn outb(port: u16, val: u8) void { + asm volatile ("outb %[val], %[port]" + : + : [val] "{al}" (val), + [port] "N{dx}" (port), + ); +} + +pub inline fn inb(port: u16) u8 { + return asm volatile ("inb %[port], %[result]" + : [result] "={al}" (-> u8), + : [port] "N{dx}" (port), + ); +} + +// a throwaway write to an unused port, ~1us. some 8259s need a beat between +// back-to-back command writes on slow hardware; harmless on QEMU. +pub inline fn wait() void { + outb(0x80, 0); +} diff --git a/boot/serial.zig b/boot/serial.zig index 2e8c8d0..fab0e66 100644 --- a/boot/serial.zig +++ b/boot/serial.zig @@ -1,6 +1,6 @@ -// This is a minimal 16550 UART driver for COM1, this is mainly going -// to be used for debugging since its really simple to setup. -// Fun fact: i was drunk when i wrote this +// this is a minimal 16550 UART driver for COM1, this is mainly going +// to be used for debugging since its really simple to setup. +// Fun fact: i was drunk when i wrote this // // see: https://wiki.osdev.org/Serial_Ports @@ -38,15 +38,23 @@ pub fn init() void { outb(COM1 + 4, 0x0B); // RTS/DSR set } -// COM1+5 (bit 5) = transmit holding register and +// COM1+5 (bit 5) = transmit holding register and // spin until it's clear to send fn putc(c: u8) void { while (inb(COM1 + 5) & 0x20 == 0) {} outb(COM1, c); } +// a raw serial console needs \r\n, but revo (and most sane code) emits bare \n. +// inject the \r ourselves, skipping it when the \n already has one so existing +// "\r\n" strings don't turn into "\r\r\n". pub fn write(s: []const u8) void { - for (s) |c| putc(c); + var prev: u8 = 0; + for (s) |c| { + if (c == '\n' and prev != '\r') putc('\r'); + putc(c); + prev = c; + } } // dump a u64 as decimal. handy for "usable: N MiB" type logging. |
