diff --git a/Makefile b/Makefile index bc975ab..46c5f20 100644 --- a/Makefile +++ b/Makefile @@ -59,6 +59,7 @@ go.o: @echo "[objcopy] creating global symbol alias 'kernel.Kmain' for 'github.com/achilleasa/gopher-os/kernel.Kmain' in go.o" @objcopy \ --add-symbol kernel.Kmain=.text:0x`nm $(BUILD_DIR)/go.o | grep "kmain.Kmain$$" | cut -d' ' -f1` \ + --globalize-symbol _rt0_interrupt_handlers \ $(BUILD_DIR)/go.o $(BUILD_DIR)/go.o binutils_version_check: @@ -110,7 +111,7 @@ iso: endif run: iso - qemu-system-$(ARCH) -cdrom $(iso_target) + qemu-system-$(ARCH) -cdrom $(iso_target) -d int,cpu_reset -no-reboot gdb: iso qemu-system-$(ARCH) -M accel=tcg -s -S -cdrom $(iso_target) & diff --git a/arch/x86_64/asm/rt0_32.s b/arch/x86_64/asm/rt0_32.s index 59ffdc2..8480a1f 100644 --- a/arch/x86_64/asm/rt0_32.s +++ b/arch/x86_64/asm/rt0_32.s @@ -321,6 +321,7 @@ _rt0_enter_long_mode: mov es, ax mov fs, ax mov gs, ax + mov ss, ax jmp CS_SEG:.flush_gdt - PAGE_OFFSET .flush_gdt: diff --git a/arch/x86_64/asm/rt0_64.s b/arch/x86_64/asm/rt0_64.s index 19fd1b1..a1ea0d9 100644 --- a/arch/x86_64/asm/rt0_64.s +++ b/arch/x86_64/asm/rt0_64.s @@ -1,9 +1,25 @@ ; vim: set ft=nasm : %include "constants.inc" +bits 64 + section .bss align 8 +; Allocate space for the interrupt descriptor table (IDT). +; This arch supports up to 256 interrupt handlers +%define IDT_ENTRIES 0xff +_rt0_idt_start: + resq 2 * IDT_ENTRIES ; each 64-bit IDT entry is 16 bytes +_rt0_idt_end: + +_rt0_idt_desc: + resw 1 + resq 1 + +; Allocates space for the IRQ handlers pointers registered by the IRQ package +_rt0_irq_handlers resq IDT_ENTRIES + r0_g_ptr: resq 1 ; fs:0x00 is a pointer to the current g struct r0_g: r0_g_stack_lo: resq 1 @@ -12,7 +28,6 @@ r0_g_stackguard0: resq 1 ; rsp compared to this value in go stack growth prolog r0_g_stackguard1: resq 1 ; rsp compared to this value in C stack growth prologue section .text -bits 64 ;------------------------------------------------------------------------------ ; Kernel 64-bit entry point @@ -25,6 +40,8 @@ bits 64 ;------------------------------------------------------------------------------ global _rt0_64_entry _rt0_64_entry: + call _rt0_64_load_idt + ; According to the x86_64 ABI, the fs:0 should point to the address of ; the user-space thread structure. The actual TLS structure is located ; just before that (aligned). Go code tries to fetch the address to the @@ -75,6 +92,196 @@ _rt0_64_entry: cli hlt + +;------------------------------------------------------------------------------ +; Setup and load IDT. We preload each IDT entry with a pointer to a gate handler +; but set it as inactive. The code in irq_amd64 is responsible for enabling +; individual IDT entries when handlers are installed. +;------------------------------------------------------------------------------ +_rt0_64_load_idt: + mov rax, _rt0_idt_start + +%assign gate_num 0 +%rep IDT_ENTRIES + mov rbx, _rt0_64_gate_entry_%+ gate_num + mov word [rax], bx ; gate entry bits 0-15 + mov word [rax+2], 0x8 ; GDT descriptor + mov byte [rax+5], 0x0 ; Mark the entry as NOT present + shr rbx, 16 + mov word [rax+6], bx ; gate entry bits 16-31 + shr rbx, 16 + mov dword [rax+8], ebx ; gate entry bits 32-63 + + add rax, 16 ; size of IDT entry +%assign gate_num gate_num+1 +%endrep + mov rax, _rt0_idt_desc + mov word [rax], _rt0_idt_end - _rt0_idt_start - 1 ; similar to GDT this must be len(IDT) - 1 + mov rbx, _rt0_idt_start + mov qword [rax+2], rbx + lidt [rax] +ret + + +;------------------------------------------------------------------------------ +; Generate gate entries. Each gate handler pushes the address of the registered +; handler to the stack before jumping to a dispatcher function. +; +; Some exceptions push an error code to the stack after the stack frame. This +; code must be popped off the stack before calling iretq. The generated handlers +; are aware whether they need to deal with the code or not and jump to the +; appropriate get dispatcher. +;------------------------------------------------------------------------------ +%assign gate_num 0 +%rep IDT_ENTRIES +extern _rt0_interrupt_handlers +_rt0_64_gate_entry_%+ gate_num: + push rax + mov rax, _rt0_interrupt_handlers + add rax, 8*gate_num + mov rax, [rax] + xchg rax, [rsp] ; store handler address and restore original rax + + ; For a list of gate numbers that push an error code see: + ; http://wiki.osdev.org/Exceptions + %if (gate_num == 8) || (gate_num >= 10 && gate_num <= 14) || (gate_num == 17) || (gate_num == 30) + jmp _rt0_64_gate_dispatcher_with_code + %else + jmp _rt0_64_gate_dispatcher_without_code + %endif +%assign gate_num gate_num+1 +%endrep + +%macro save_regs 0 + push r15 + push r14 + push r13 + push r12 + push r11 + push r10 + push r9 + push r8 + push rbp + push rdi + push rsi + push rdx + push rcx + push rbx + push rax +%endmacro + +%macro restore_regs 0 + pop rax + pop rbx + pop rcx + pop rdx + pop rsi + pop rdi + pop rbp + pop r8 + pop r9 + pop r10 + pop r11 + pop r12 + pop r13 + pop r14 + pop r15 +%endmacro + +;------------------------------------------------------------------------------ +; This dispatcher is invoked by gate entries that expect a code to be pushed +; by the CPU to the stack. It performs the following functions: +; - save registers +; - push pointer to saved regs +; - push pointer to stack frame +; - read and push exception code +; - invoke handler(code, &frame, ®s) +; - restore registers +; - pop exception code from stack so rsp points to the stack frame +;------------------------------------------------------------------------------ +_rt0_64_gate_dispatcher_with_code: + ; This is how the stack looks like when entering this function: + ; (each item is 8-bytes wide) + ; + ;------------------ + ; handler address | <- pushed by gate_entry_xxx (RSP points here) + ;-----------------| + ; Exception code | <- needs to be removed from stack before calling iretq + ;-----------------| + ; RIP | <- exception frame + ; CS | + ; RFLAGS | + ; RSP | + ; SS | + ;----------------- + cld + + ; save regs and push a pointer to them + save_regs + mov rax, rsp ; rax points to saved rax + push rax ; push pointer to saved regs + + ; push pointer to exception stack frame (we have used 15 qwords for the + ; saved registers plus one qword for the data pushed by the gate entry + ; plus one extra qword to jump over the exception code) + add rax, 17*8 + push rax + + ; push exception code (located between the stack frame and the saved regs) + sub rax, 8 + push qword [rax] + + call [rsp + 18*8] ; call registered irq handler + + add rsp, 3 * 8 ; unshift the pushed arguments so rsp points to the saved regs + restore_regs + + add rsp, 16 ; pop handler address and exception code off the stack before returning + iretq + + +;------------------------------------------------------------------------------ +; This dispatcher is invoked by gate entries that do not use exception codes. +; It performs the following functions: +; - save registers +; - push pointer to saved regs +; - push pointer to stack frame +; - invoke handler(&frame, ®s) +; - restore registers +;------------------------------------------------------------------------------ +_rt0_64_gate_dispatcher_without_code: + ; This is how the stack looks like when entering this function: + ; (each item is 8-bytes wide) + ; + ;------------------ + ; handler address | <- pushed by gate_entry_xxx (RSP points here) + ;-----------------| + ; RIP | <- exception frame + ; CS | + ; RFLAGS | + ; RSP | + ; SS | + ;----------------- + cld + + ; save regs and push a pointer to them + save_regs + mov rax, rsp ; rax points to saved rax + push rax ; push pointer to saved regs + + ; push pointer to exception stack frame (we have used 15 qwords for the + ; saved registers plus one qword for the data pushed by the gate entry) + add rax, 16*8 + push rax + + call [rsp + 17*8] ; call registered irq handler + + add rsp, 2 * 8 ; unshift the pushed arguments so rsp points to the saved regs + restore_regs + + add rsp, 8 ; pop handler address off the stack before returning + iretq + ;------------------------------------------------------------------------------ ; Error messages ;------------------------------------------------------------------------------ diff --git a/kernel/cpu/cpu_amd64.go b/kernel/cpu/cpu_amd64.go new file mode 100644 index 0000000..c757266 --- /dev/null +++ b/kernel/cpu/cpu_amd64.go @@ -0,0 +1,23 @@ +package cpu + +// EnableInterrupts enables interrupt handling. +func EnableInterrupts() + +// DisableInterrupts disables interrupt handling. +func DisableInterrupts() + +// Halt stops instruction execution. +func Halt() + +// FlushTLBEntry flushes a TLB entry for a particular virtual address. +func FlushTLBEntry(virtAddr uintptr) + +// SwitchPDT sets the root page table directory to point to the specified +// physical address and flushes the TLB. +func SwitchPDT(pdtPhysAddr uintptr) + +// ActivePDT returns the physical address of the currently active page table. +func ActivePDT() uintptr + +// ReadCR2 returns the value stored in the CR2 register. +func ReadCR2() uint64 diff --git a/kernel/cpu/cpu_amd64.s b/kernel/cpu/cpu_amd64.s new file mode 100644 index 0000000..b654195 --- /dev/null +++ b/kernel/cpu/cpu_amd64.s @@ -0,0 +1,33 @@ +#include "textflag.h" + +TEXT ·EnableInterrupts(SB),NOSPLIT,$0 + STI + RET + +TEXT ·DisableInterrupts(SB),NOSPLIT,$0 + CLI + RET + +TEXT ·Halt(SB),NOSPLIT,$0 + CLI + HLT + RET + +TEXT ·FlushTLBEntry(SB),NOSPLIT,$0 + INVLPG virtAddr+0(FP) + RET + +TEXT ·SwitchPDT(SB),NOSPLIT,$0 + // loading CR3 also triggers a TLB flush + MOVQ pdtPhysAddr+0(FP), CR3 + RET + +TEXT ·ActivePDT(SB),NOSPLIT,$0 + MOVQ CR3, AX + MOVQ AX, ret+0(FP) + RET + +TEXT ·ReadCR2(SB),NOSPLIT,$0 + MOVQ CR2, AX + MOVQ AX, ret+0(FP) + RET diff --git a/kernel/irq/handler_amd64.go b/kernel/irq/handler_amd64.go new file mode 100644 index 0000000..b289ab7 --- /dev/null +++ b/kernel/irq/handler_amd64.go @@ -0,0 +1,41 @@ +package irq + +// ExceptionNum defines an exception number that can be +// passed to the HandleException and HandleExceptionWithCode +// functions. +type ExceptionNum uint8 + +const ( + // DoubleFault occurs when an exception is unhandled + // or when an exception occurs while the CPU is + // trying to call an exception handler. + DoubleFault = ExceptionNum(8) + + // GPFException is raised when a general protection fault occurs. + GPFException = ExceptionNum(13) + + // PageFaultException is raised when a PDT or + // PDT-entry is not present or when a privilege + // and/or RW protection check fails. + PageFaultException = ExceptionNum(14) +) + +// ExceptionHandler is a function that handles an exception that does not push +// an error code to the stack. If the handler returns, any modifications to the +// supplied Frame and/or Regs pointers will be propagated back to the location +// where the exception occurred. +type ExceptionHandler func(*Frame, *Regs) + +// ExceptionHandlerWithCode is a function that handles an exception that pushes +// an error code to the stack. If the handler returns, any modifications to the +// supplied Frame and/or Regs pointers will be propagated back to the location +// where the exception occurred. +type ExceptionHandlerWithCode func(uint64, *Frame, *Regs) + +// HandleException registers an exception handler (without an error code) for +// the given interrupt number. +func HandleException(exceptionNum ExceptionNum, handler ExceptionHandler) + +// HandleExceptionWithCode registers an exception handler (with an error code) +// for the given interrupt number. +func HandleExceptionWithCode(exceptionNum ExceptionNum, handler ExceptionHandlerWithCode) diff --git a/kernel/irq/handler_amd64.s b/kernel/irq/handler_amd64.s new file mode 100644 index 0000000..f2aa3a9 --- /dev/null +++ b/kernel/irq/handler_amd64.s @@ -0,0 +1,36 @@ +#include "textflag.h" + +// The maximum number of interrupt handlers is 256 so we need to allocate space +// for 256 x 8-byte pointers. This symbol is made global by the Makefile so it +// can be accessed by the gate entries defined in the rt0 assembly code. +GLOBL _rt0_interrupt_handlers(SB), NOPTR, $2048 + +// In 64-bit mode SIDT stores 8+2 bytes for the IDT address and limit +GLOBL _rt0_idtr<>(SB), NOPTR, $10 + +TEXT ·HandleException(SB),NOSPLIT,$0 + JMP ·HandleExceptionWithCode(SB) + RET + +TEXT ·HandleExceptionWithCode(SB),NOSPLIT,$0 + // Install the handler address in _rt0_interrupt_handlers + LEAQ _rt0_interrupt_handlers+0(SB), CX + MOVBQZX exceptionNum+0(FP), AX // exceptionNum is a uint8 so we zero-extend it to 64bits + MOVQ handler+8(FP), BX + MOVQ 0(BX), BX // dereference pointer to handler fn + MOVQ BX, (CX)(AX*8) + + // To enable the handler we need to lookup the appropriate IDT entry + // and modify its type/attribute byte. To acquire the IDT base address + // we use the SIDT instruction. + MOVQ IDTR, _rt0_idtr<>+0(SB) + LEAQ _rt0_idtr<>(SB), CX + MOVQ 2(CX), CX // CX points to IDT base address + SHLQ $4, AX // Each IDT entry uses 16 bytes so we multiply num by 16 + ADDQ AX, CX // and add it to CX to get the address of the IDT entry + // we want to tweak + + MOVB $0x8e, 5(CX) // 32/64-bit ring-0 interrupt gate that is present + // see: http://wiki.osdev.org/Interrupt_Descriptor_Table + + RET diff --git a/kernel/irq/interrupt_amd64.go b/kernel/irq/interrupt_amd64.go new file mode 100644 index 0000000..dcab9ef --- /dev/null +++ b/kernel/irq/interrupt_amd64.go @@ -0,0 +1,51 @@ +package irq + +import "github.com/achilleasa/gopher-os/kernel/kfmt/early" + +// Regs contains a snapshot of the register values when an interrupt occurred. +type Regs struct { + RAX uint64 + RBX uint64 + RCX uint64 + RDX uint64 + RSI uint64 + RDI uint64 + RBP uint64 + R8 uint64 + R9 uint64 + R10 uint64 + R11 uint64 + R12 uint64 + R13 uint64 + R14 uint64 + R15 uint64 +} + +// Print outputs a dump of the register values to the active console. +func (r *Regs) Print() { + early.Printf("RAX = %16x RBX = %16x\n", r.RAX, r.RBX) + early.Printf("RCX = %16x RDX = %16x\n", r.RCX, r.RDX) + early.Printf("RSI = %16x RDI = %16x\n", r.RSI, r.RDI) + early.Printf("RBP = %16x\n", r.RBP) + early.Printf("R8 = %16x R9 = %16x\n", r.R8, r.R9) + early.Printf("R10 = %16x R11 = %16x\n", r.R10, r.R11) + early.Printf("R12 = %16x R13 = %16x\n", r.R12, r.R13) + early.Printf("R14 = %16x R15 = %16x\n", r.R14, r.R15) +} + +// Frame describes an exception frame that is automatically pushed by the CPU +// to the stack when an exception occurs. +type Frame struct { + RIP uint64 + CS uint64 + RFlags uint64 + RSP uint64 + SS uint64 +} + +// Print outputs a dump of the exception frame to the active console. +func (f *Frame) Print() { + early.Printf("RIP = %16x CS = %16x\n", f.RIP, f.CS) + early.Printf("RSP = %16x SS = %16x\n", f.RSP, f.SS) + early.Printf("RFL = %16x\n", f.RFlags) +} diff --git a/kernel/irq/interrupt_amd64_test.go b/kernel/irq/interrupt_amd64_test.go new file mode 100644 index 0000000..3c2d22a --- /dev/null +++ b/kernel/irq/interrupt_amd64_test.go @@ -0,0 +1,84 @@ +package irq + +import ( + "bytes" + "testing" + "unsafe" + + "github.com/achilleasa/gopher-os/kernel/driver/video/console" + "github.com/achilleasa/gopher-os/kernel/hal" +) + +func TestRegsPrint(t *testing.T) { + fb := mockTTY() + regs := Regs{ + RAX: 1, + RBX: 2, + RCX: 3, + RDX: 4, + RSI: 5, + RDI: 6, + RBP: 7, + R8: 8, + R9: 9, + R10: 10, + R11: 11, + R12: 12, + R13: 13, + R14: 14, + R15: 15, + } + regs.Print() + + exp := "RAX = 0000000000000001 RBX = 0000000000000002\nRCX = 0000000000000003 RDX = 0000000000000004\nRSI = 0000000000000005 RDI = 0000000000000006\nRBP = 0000000000000007\nR8 = 0000000000000008 R9 = 0000000000000009\nR10 = 000000000000000a R11 = 000000000000000b\nR12 = 000000000000000c R13 = 000000000000000d\nR14 = 000000000000000e R15 = 000000000000000f" + + if got := readTTY(fb); got != exp { + t.Fatalf("expected to get:\n%q\ngot:\n%q", exp, got) + } +} + +func TestFramePrint(t *testing.T) { + fb := mockTTY() + frame := Frame{ + RIP: 1, + CS: 2, + RFlags: 3, + RSP: 4, + SS: 5, + } + frame.Print() + + exp := "RIP = 0000000000000001 CS = 0000000000000002\nRSP = 0000000000000004 SS = 0000000000000005\nRFL = 0000000000000003" + + if got := readTTY(fb); got != exp { + t.Fatalf("expected to get:\n%q\ngot:\n%q", exp, got) + } + +} + +func readTTY(fb []byte) string { + var buf bytes.Buffer + for i := 0; i < len(fb); i += 2 { + ch := fb[i] + if ch == 0 { + if i+2 < len(fb) && fb[i+2] != 0 { + buf.WriteByte('\n') + } + continue + } + + buf.WriteByte(ch) + } + + return buf.String() +} + +func mockTTY() []byte { + // Mock a tty to handle early.Printf output + mockConsoleFb := make([]byte, 160*25) + mockConsole := &console.Ega{} + mockConsole.Init(80, 25, uintptr(unsafe.Pointer(&mockConsoleFb[0]))) + hal.ActiveTerminal.AttachTo(mockConsole) + + return mockConsoleFb +} diff --git a/kernel/kmain/kmain.go b/kernel/kmain/kmain.go index 8d84538..97347fe 100644 --- a/kernel/kmain/kmain.go +++ b/kernel/kmain/kmain.go @@ -1,10 +1,11 @@ package kmain import ( + "github.com/achilleasa/gopher-os/kernel" "github.com/achilleasa/gopher-os/kernel/hal" "github.com/achilleasa/gopher-os/kernel/hal/multiboot" - "github.com/achilleasa/gopher-os/kernel/kfmt/early" "github.com/achilleasa/gopher-os/kernel/mem/pmm/allocator" + "github.com/achilleasa/gopher-os/kernel/mem/vmm" ) // Kmain is the only Go symbol that is visible (exported) from the rt0 initialization @@ -24,7 +25,10 @@ func Kmain(multibootInfoPtr, kernelStart, kernelEnd uintptr) { hal.InitTerminal() hal.ActiveTerminal.Clear() - if err := allocator.Init(kernelStart, kernelEnd); err != nil { - early.Printf("[%s] error: %s\n", err.Module, err.Message) + var err *kernel.Error + if err = allocator.Init(kernelStart, kernelEnd); err != nil { + kernel.Panic(err) + } else if err = vmm.Init(); err != nil { + kernel.Panic(err) } } diff --git a/kernel/mem/vmm/map.go b/kernel/mem/vmm/map.go index 4dfe64d..fc472da 100644 --- a/kernel/mem/vmm/map.go +++ b/kernel/mem/vmm/map.go @@ -4,6 +4,7 @@ import ( "unsafe" "github.com/achilleasa/gopher-os/kernel" + "github.com/achilleasa/gopher-os/kernel/cpu" "github.com/achilleasa/gopher-os/kernel/mem" "github.com/achilleasa/gopher-os/kernel/mem/pmm" ) @@ -18,7 +19,7 @@ var ( // flushTLBEntryFn is used by tests to override calls to flushTLBEntry // which will cause a fault if called in user-mode. - flushTLBEntryFn = flushTLBEntry + flushTLBEntryFn = cpu.FlushTLBEntry errNoHugePageSupport = &kernel.Error{Module: "vmm", Message: "huge pages are not supported"} ) diff --git a/kernel/mem/vmm/pdt.go b/kernel/mem/vmm/pdt.go index e60b336..68505cd 100644 --- a/kernel/mem/vmm/pdt.go +++ b/kernel/mem/vmm/pdt.go @@ -4,6 +4,7 @@ import ( "unsafe" "github.com/achilleasa/gopher-os/kernel" + "github.com/achilleasa/gopher-os/kernel/cpu" "github.com/achilleasa/gopher-os/kernel/mem" "github.com/achilleasa/gopher-os/kernel/mem/pmm" ) @@ -11,11 +12,11 @@ import ( var ( // activePDTFn is used by tests to override calls to activePDT which // will cause a fault if called in user-mode. - activePDTFn = activePDT + activePDTFn = cpu.ActivePDT // switchPDTFn is used by tests to override calls to switchPDT which // will cause a fault if called in user-mode. - switchPDTFn = switchPDT + switchPDTFn = cpu.SwitchPDT // mapFn is used by tests and is automatically inlined by the compiler. mapFn = Map diff --git a/kernel/mem/vmm/tlb.go b/kernel/mem/vmm/tlb.go deleted file mode 100644 index 703e597..0000000 --- a/kernel/mem/vmm/tlb.go +++ /dev/null @@ -1,11 +0,0 @@ -package vmm - -// flushTLBEntry flushes a TLB entry for a particular virtual address. -func flushTLBEntry(virtAddr uintptr) - -// switchPDT sets the root page table directory to point to the specified -// physical address and flushes the TLB. -func switchPDT(pdtPhysAddr uintptr) - -// activePDT returns the physical address of the currently active page table. -func activePDT() uintptr diff --git a/kernel/mem/vmm/tlb_amd64.s b/kernel/mem/vmm/tlb_amd64.s deleted file mode 100644 index 0f12a43..0000000 --- a/kernel/mem/vmm/tlb_amd64.s +++ /dev/null @@ -1,15 +0,0 @@ - #include "textflag.h" - -TEXT ·flushTLBEntry(SB),NOSPLIT,$0 - INVLPG virtAddr+0(FP) - RET - -TEXT ·switchPDT(SB),NOSPLIT,$0 - // loading CR3 also triggers a TLB flush - MOVQ pdtPhysAddr+0(FP), CR3 - RET - -TEXT ·activePDT(SB),NOSPLIT,$0 - MOVQ CR3, AX - MOVQ AX, ret+0(FP) - RET diff --git a/kernel/mem/vmm/vmm.go b/kernel/mem/vmm/vmm.go index 2ee2971..ba7cf37 100644 --- a/kernel/mem/vmm/vmm.go +++ b/kernel/mem/vmm/vmm.go @@ -2,10 +2,23 @@ package vmm import ( "github.com/achilleasa/gopher-os/kernel" + "github.com/achilleasa/gopher-os/kernel/cpu" + "github.com/achilleasa/gopher-os/kernel/irq" + "github.com/achilleasa/gopher-os/kernel/kfmt/early" "github.com/achilleasa/gopher-os/kernel/mem/pmm" ) -var frameAllocator FrameAllocatorFn +var ( + // frameAllocator points to a frame allocator function registered using + // SetFrameAllocator. + frameAllocator FrameAllocatorFn + + // the following functions are mocked by tests and are automatically + // inlined by the compiler. + panicFn = kernel.Panic + handleExceptionWithCodeFn = irq.HandleExceptionWithCode + readCR2Fn = cpu.ReadCR2 +) // FrameAllocatorFn is a function that can allocate physical frames. type FrameAllocatorFn func() (pmm.Frame, *kernel.Error) @@ -15,3 +28,51 @@ type FrameAllocatorFn func() (pmm.Frame, *kernel.Error) func SetFrameAllocator(allocFn FrameAllocatorFn) { frameAllocator = allocFn } + +func pageFaultHandler(errorCode uint64, frame *irq.Frame, regs *irq.Regs) { + early.Printf("\nPage fault while accessing address: 0x%16x\nReason: ", readCR2Fn()) + switch { + case errorCode == 0: + early.Printf("read from non-present page") + case errorCode == 1: + early.Printf("page protection violation (read)") + case errorCode == 2: + early.Printf("write to non-present page") + case errorCode == 3: + early.Printf("page protection violation (write)") + case errorCode == 4: + early.Printf("page-fault in user-mode") + case errorCode == 8: + early.Printf("page table has reserved bit set") + case errorCode == 16: + early.Printf("instruction fetch") + default: + early.Printf("unknown") + } + + early.Printf("\n\nRegisters:\n") + regs.Print() + frame.Print() + + // TODO: Revisit this when user-mode tasks are implemented + panicFn(nil) +} + +func generalProtectionFaultHandler(_ uint64, frame *irq.Frame, regs *irq.Regs) { + early.Printf("\nGeneral protection fault while accessing address: 0x%x\n", readCR2Fn()) + early.Printf("Registers:\n") + regs.Print() + frame.Print() + + // TODO: Revisit this when user-mode tasks are implemented + panicFn(nil) +} + +// Init initializes the vmm system and installs paging-related exception +// handlers. +func Init() *kernel.Error { + handleExceptionWithCodeFn(irq.PageFaultException, pageFaultHandler) + handleExceptionWithCodeFn(irq.GPFException, generalProtectionFaultHandler) + + return nil +} diff --git a/kernel/mem/vmm/vmm_test.go b/kernel/mem/vmm/vmm_test.go new file mode 100644 index 0000000..d1aedd3 --- /dev/null +++ b/kernel/mem/vmm/vmm_test.go @@ -0,0 +1,169 @@ +package vmm + +import ( + "bytes" + "strings" + "testing" + "unsafe" + + "github.com/achilleasa/gopher-os/kernel" + "github.com/achilleasa/gopher-os/kernel/cpu" + "github.com/achilleasa/gopher-os/kernel/driver/video/console" + "github.com/achilleasa/gopher-os/kernel/hal" + "github.com/achilleasa/gopher-os/kernel/irq" +) + +func TestPageFaultHandler(t *testing.T) { + defer func() { + panicFn = kernel.Panic + readCR2Fn = cpu.ReadCR2 + }() + + specs := []struct { + errCode uint64 + expReason string + expPanic bool + }{ + { + 0, + "read from non-present page", + true, + }, + { + 1, + "page protection violation (read)", + true, + }, + { + 2, + "write to non-present page", + true, + }, + { + 3, + "page protection violation (write)", + true, + }, + { + 4, + "page-fault in user-mode", + true, + }, + { + 8, + "page table has reserved bit set", + true, + }, + { + 16, + "instruction fetch", + true, + }, + { + 0xf00, + "unknown", + true, + }, + } + + var ( + regs irq.Regs + frame irq.Frame + ) + + readCR2Fn = func() uint64 { + return 0xbadf00d000 + } + + panicCalled := false + panicFn = func(_ *kernel.Error) { + panicCalled = true + } + + for specIndex, spec := range specs { + fb := mockTTY() + panicCalled = false + + pageFaultHandler(spec.errCode, &frame, ®s) + if got := readTTY(fb); !strings.Contains(got, spec.expReason) { + t.Errorf("[spec %d] expected reason %q; got output:\n%q", specIndex, spec.expReason, got) + continue + } + + if spec.expPanic != panicCalled { + t.Errorf("[spec %d] expected panic %t; got %t", specIndex, spec.expPanic, panicCalled) + } + } +} + +func TestGPtHandler(t *testing.T) { + defer func() { + panicFn = kernel.Panic + readCR2Fn = cpu.ReadCR2 + }() + + var ( + regs irq.Regs + frame irq.Frame + fb = mockTTY() + ) + + readCR2Fn = func() uint64 { + return 0xbadf00d000 + } + + panicCalled := false + panicFn = func(_ *kernel.Error) { + panicCalled = true + } + + generalProtectionFaultHandler(0, &frame, ®s) + + exp := "\nGeneral protection fault while accessing address: 0xbadf00d000\nRegisters:\nRAX = 0000000000000000 RBX = 0000000000000000\nRCX = 0000000000000000 RDX = 0000000000000000\nRSI = 0000000000000000 RDI = 0000000000000000\nRBP = 0000000000000000\nR8 = 0000000000000000 R9 = 0000000000000000\nR10 = 0000000000000000 R11 = 0000000000000000\nR12 = 0000000000000000 R13 = 0000000000000000\nR14 = 0000000000000000 R15 = 0000000000000000\nRIP = 0000000000000000 CS = 0000000000000000\nRSP = 0000000000000000 SS = 0000000000000000\nRFL = 0000000000000000" + if got := readTTY(fb); got != exp { + t.Errorf("expected output:\n%q\ngot:\n%q", exp, got) + } + + if !panicCalled { + t.Error("expected kernel.Panic to be called") + } +} + +func TestInit(t *testing.T) { + defer func() { + handleExceptionWithCodeFn = irq.HandleExceptionWithCode + }() + + handleExceptionWithCodeFn = func(_ irq.ExceptionNum, _ irq.ExceptionHandlerWithCode) {} + + if err := Init(); err != nil { + t.Fatal(err) + } +} + +func readTTY(fb []byte) string { + var buf bytes.Buffer + for i := 0; i < len(fb); i += 2 { + ch := fb[i] + if ch == 0 { + if i+2 < len(fb) && fb[i+2] != 0 { + buf.WriteByte('\n') + } + continue + } + + buf.WriteByte(ch) + } + + return buf.String() +} + +func mockTTY() []byte { + // Mock a tty to handle early.Printf output + mockConsoleFb := make([]byte, 160*25) + mockConsole := &console.Ega{} + mockConsole.Init(80, 25, uintptr(unsafe.Pointer(&mockConsoleFb[0]))) + hal.ActiveTerminal.AttachTo(mockConsole) + + return mockConsoleFb +} diff --git a/kernel/panic.go b/kernel/panic.go new file mode 100644 index 0000000..b94cfae --- /dev/null +++ b/kernel/panic.go @@ -0,0 +1,24 @@ +package kernel + +import ( + "github.com/achilleasa/gopher-os/kernel/cpu" + "github.com/achilleasa/gopher-os/kernel/kfmt/early" +) + +var ( + // cpuHaltFn is mocked by tests and is automatically inlined by the compiler. + cpuHaltFn = cpu.Halt +) + +// Panic outputs the supplied error (if not nil) to the console and halts the +// CPU. Calls to Panic never return. +func Panic(err *Error) { + early.Printf("\n-----------------------------------\n") + if err != nil { + early.Printf("[%s] unrecoverable error: %s\n", err.Module, err.Message) + } + early.Printf("*** kernel panic: system halted ***") + early.Printf("\n-----------------------------------\n") + + cpuHaltFn() +} diff --git a/kernel/panic_test.go b/kernel/panic_test.go new file mode 100644 index 0000000..0339129 --- /dev/null +++ b/kernel/panic_test.go @@ -0,0 +1,84 @@ +package kernel + +import ( + "bytes" + "testing" + "unsafe" + + "github.com/achilleasa/gopher-os/kernel/cpu" + "github.com/achilleasa/gopher-os/kernel/driver/video/console" + "github.com/achilleasa/gopher-os/kernel/hal" +) + +func TestPanic(t *testing.T) { + defer func() { + cpuHaltFn = cpu.Halt + }() + + var cpuHaltCalled bool + cpuHaltFn = func() { + cpuHaltCalled = true + } + + t.Run("with error", func(t *testing.T) { + cpuHaltCalled = false + fb := mockTTY() + err := &Error{Module: "test", Message: "panic test"} + + Panic(err) + + exp := "\n-----------------------------------\n[test] unrecoverable error: panic test\n*** kernel panic: system halted ***\n-----------------------------------" + + if got := readTTY(fb); got != exp { + t.Fatalf("expected to get:\n%q\ngot:\n%q", exp, got) + } + + if !cpuHaltCalled { + t.Fatal("expected cpu.Halt() to be called by Panic") + } + }) + + t.Run("without error", func(t *testing.T) { + cpuHaltCalled = false + fb := mockTTY() + + Panic(nil) + + exp := "\n-----------------------------------\n*** kernel panic: system halted ***\n-----------------------------------" + + if got := readTTY(fb); got != exp { + t.Fatalf("expected to get:\n%q\ngot:\n%q", exp, got) + } + + if !cpuHaltCalled { + t.Fatal("expected cpu.Halt() to be called by Panic") + } + }) +} + +func readTTY(fb []byte) string { + var buf bytes.Buffer + for i := 0; i < len(fb); i += 2 { + ch := fb[i] + if ch == 0 { + if i+2 < len(fb) && fb[i+2] != 0 { + buf.WriteByte('\n') + } + continue + } + + buf.WriteByte(ch) + } + + return buf.String() +} + +func mockTTY() []byte { + // Mock a tty to handle early.Printf output + mockConsoleFb := make([]byte, 160*25) + mockConsole := &console.Ega{} + mockConsole.Init(80, 25, uintptr(unsafe.Pointer(&mockConsoleFb[0]))) + hal.ActiveTerminal.AttachTo(mockConsole) + + return mockConsoleFb +}