From d7933002795dcfd702e231c88141ada8501901ae Mon Sep 17 00:00:00 2001 From: Achilleas Anagnostopoulos Date: Wed, 21 Jun 2017 07:57:05 +0100 Subject: [PATCH 1/8] Move TLB-handling code to the cpu pkg --- kernel/cpu/cpu_amd64.go | 11 +++++++++++ kernel/{mem/vmm/tlb_amd64.s => cpu/cpu_amd64.s} | 11 ++++++----- kernel/mem/vmm/map.go | 3 ++- kernel/mem/vmm/pdt.go | 5 +++-- kernel/mem/vmm/tlb.go | 11 ----------- 5 files changed, 22 insertions(+), 19 deletions(-) create mode 100644 kernel/cpu/cpu_amd64.go rename kernel/{mem/vmm/tlb_amd64.s => cpu/cpu_amd64.s} (53%) delete mode 100644 kernel/mem/vmm/tlb.go diff --git a/kernel/cpu/cpu_amd64.go b/kernel/cpu/cpu_amd64.go new file mode 100644 index 0000000..4943fa2 --- /dev/null +++ b/kernel/cpu/cpu_amd64.go @@ -0,0 +1,11 @@ +package cpu + +// 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/cpu/cpu_amd64.s similarity index 53% rename from kernel/mem/vmm/tlb_amd64.s rename to kernel/cpu/cpu_amd64.s index 0f12a43..002c8cb 100644 --- a/kernel/mem/vmm/tlb_amd64.s +++ b/kernel/cpu/cpu_amd64.s @@ -1,15 +1,16 @@ - #include "textflag.h" - -TEXT ·flushTLBEntry(SB),NOSPLIT,$0 +#include "textflag.h" + +TEXT ·FlushTLBEntry(SB),NOSPLIT,$0 INVLPG virtAddr+0(FP) RET -TEXT ·switchPDT(SB),NOSPLIT,$0 +TEXT ·SwitchPDT(SB),NOSPLIT,$0 // loading CR3 also triggers a TLB flush MOVQ pdtPhysAddr+0(FP), CR3 RET -TEXT ·activePDT(SB),NOSPLIT,$0 +TEXT ·ActivePDT(SB),NOSPLIT,$0 MOVQ CR3, AX MOVQ AX, ret+0(FP) RET + 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 From cd3199199b9d1cce938f245c5bf629f7f886f908 Mon Sep 17 00:00:00 2001 From: Achilleas Anagnostopoulos Date: Wed, 21 Jun 2017 07:58:15 +0100 Subject: [PATCH 2/8] Add assemnly code for enabling/disabling insterrupts and halting the CPU --- kernel/cpu/cpu_amd64.go | 9 +++++++++ kernel/cpu/cpu_amd64.s | 13 +++++++++++++ 2 files changed, 22 insertions(+) diff --git a/kernel/cpu/cpu_amd64.go b/kernel/cpu/cpu_amd64.go index 4943fa2..7776082 100644 --- a/kernel/cpu/cpu_amd64.go +++ b/kernel/cpu/cpu_amd64.go @@ -1,5 +1,14 @@ 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) diff --git a/kernel/cpu/cpu_amd64.s b/kernel/cpu/cpu_amd64.s index 002c8cb..5ef45db 100644 --- a/kernel/cpu/cpu_amd64.s +++ b/kernel/cpu/cpu_amd64.s @@ -1,5 +1,18 @@ #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 From 827f1a171fb47ae318d73eaa9acc98a341c8009a Mon Sep 17 00:00:00 2001 From: Achilleas Anagnostopoulos Date: Wed, 21 Jun 2017 17:29:29 +0100 Subject: [PATCH 3/8] Load SS register value to DS_SEG when setting up GDT If not set then the CPU wil generate a GPF exception when returning from an interrupt handler --- arch/x86_64/asm/rt0_32.s | 1 + 1 file changed, 1 insertion(+) 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: From 5a2efb2bd358c614744e6f65c93aea066238b2be Mon Sep 17 00:00:00 2001 From: Achilleas Anagnostopoulos Date: Wed, 21 Jun 2017 17:45:42 +0100 Subject: [PATCH 4/8] Load IDT and define gate error-code-aware handlers (rt0/x86_64) The rt0_64 code will load a blank IDT with 256 entries (the max number of supported interrupts in the x86_64 architecture). Each IDT entry is set as *not present* but its handler is set to a dedicated gate entrypoint defined in the rt0 code. A gate entrypoint is defined for each interrupt number using a nasm macro. Each entrypoint will then use the interrupt number to index a list of pointers (defined and managed by the Go assembly code in the irq pkg) to the registered interrupt handlers and push its address on the stack before jumping to one of the two available gate dispatching functions (some interrupts also push an error code to the stack which must be popped before returning from the interrupt handler): - _rt0_64_gate_dispatcher_with_code - _rt0_64_gate_dispatcher_without_code Both dispatchers operate in the same way: - they save the original registers - they invoke the interrupt handler - they restore the original registers - ensure that the stack pointer (rsp) points to the exception frame pushed by the CPU The difference between the dispatchers is that the "with_code" variant will invoke a handler with signature `func(code, &frame, ®s)` and ensure that the code is popped off the stack before returning from the interrupt while the "without_code" variant will invoke a handler with signature `func(&frame, ®s)` --- arch/x86_64/asm/rt0_64.s | 209 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 208 insertions(+), 1 deletion(-) 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 ;------------------------------------------------------------------------------ From 9adde8f5c176318355edf40582eb1027cc9e86ad Mon Sep 17 00:00:00 2001 From: Achilleas Anagnostopoulos Date: Wed, 21 Jun 2017 17:57:15 +0100 Subject: [PATCH 5/8] Define and implement API for registering exception handlers --- Makefile | 1 + kernel/irq/handler_amd64.go | 41 +++++++++++++++ kernel/irq/handler_amd64.s | 36 +++++++++++++ kernel/irq/interrupt_amd64.go | 51 ++++++++++++++++++ kernel/irq/interrupt_amd64_test.go | 84 ++++++++++++++++++++++++++++++ 5 files changed, 213 insertions(+) create mode 100644 kernel/irq/handler_amd64.go create mode 100644 kernel/irq/handler_amd64.s create mode 100644 kernel/irq/interrupt_amd64.go create mode 100644 kernel/irq/interrupt_amd64_test.go diff --git a/Makefile b/Makefile index bc975ab..55c5c48 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: 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 +} From c5aa5fe05f931b324438ad917cd9387e90c0c515 Mon Sep 17 00:00:00 2001 From: Achilleas Anagnostopoulos Date: Wed, 21 Jun 2017 18:18:29 +0100 Subject: [PATCH 6/8] Add method for reading the CR2 register value This will allow us to figure out the virtual address that caused a page- or general protection fault. --- kernel/cpu/cpu_amd64.go | 3 +++ kernel/cpu/cpu_amd64.s | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/kernel/cpu/cpu_amd64.go b/kernel/cpu/cpu_amd64.go index 7776082..c757266 100644 --- a/kernel/cpu/cpu_amd64.go +++ b/kernel/cpu/cpu_amd64.go @@ -18,3 +18,6 @@ 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 index 5ef45db..b654195 100644 --- a/kernel/cpu/cpu_amd64.s +++ b/kernel/cpu/cpu_amd64.s @@ -27,3 +27,7 @@ TEXT ·ActivePDT(SB),NOSPLIT,$0 MOVQ AX, ret+0(FP) RET +TEXT ·ReadCR2(SB),NOSPLIT,$0 + MOVQ CR2, AX + MOVQ AX, ret+0(FP) + RET From 6e8d504ae8257ba4ee14287d952f023aa25cf201 Mon Sep 17 00:00:00 2001 From: Achilleas Anagnostopoulos Date: Wed, 21 Jun 2017 18:54:26 +0100 Subject: [PATCH 7/8] Install exception handlers for page faults/GPFs and provide kernel.Panic --- kernel/kmain/kmain.go | 10 ++- kernel/mem/vmm/vmm.go | 63 +++++++++++++- kernel/mem/vmm/vmm_test.go | 169 +++++++++++++++++++++++++++++++++++++ kernel/panic.go | 24 ++++++ kernel/panic_test.go | 84 ++++++++++++++++++ 5 files changed, 346 insertions(+), 4 deletions(-) create mode 100644 kernel/mem/vmm/vmm_test.go create mode 100644 kernel/panic.go create mode 100644 kernel/panic_test.go 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/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 +} From 56d031621d0018b54fca68829488a4a2222c4da0 Mon Sep 17 00:00:00 2001 From: Achilleas Anagnostopoulos Date: Wed, 21 Jun 2017 19:04:50 +0100 Subject: [PATCH 8/8] Make qemu log exceptions and prevent reboots in case of triple faults --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 55c5c48..46c5f20 100644 --- a/Makefile +++ b/Makefile @@ -111,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) &