From 6e8d504ae8257ba4ee14287d952f023aa25cf201 Mon Sep 17 00:00:00 2001 From: Achilleas Anagnostopoulos Date: Wed, 21 Jun 2017 18:54:26 +0100 Subject: [PATCH] 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 +}