From 275664219ec4fc9bf0a4e4b51d5ecba8a211441b Mon Sep 17 00:00:00 2001 From: Achilleas Anagnostopoulos Date: Sun, 25 Jun 2017 19:51:44 +0100 Subject: [PATCH 1/4] Implement tool to detect redirects and to populate the redirect table The tool scans all go sources (excluding tests) in the "kernel" package and its subpackages looking for functions with a "go:redirect-from symbol_name" comment. The go:redirect-from directive implies that a function serves as a redirect target for s symbol name. For example, the following block: //go:redirect-from runtime.gopanic func foo(_ interface{}){ ... } specifies that calls to "runtime.gopanic" should be redirected to "foo". The tool provides two commands: - count: prints the count of redirections - populate-table: resolve redirect symbols and populate the _rt0_rediret_table entries in the kernel image. As the final virtual addresses for the symbols are only known after linking, populating this table is a 2-step process. At first, the "count" command is used to allocate enough space for 2 x NUM_REDIRECTS pointers. The table itself is placed with the help of the linker script in a separate section making it easy to find its offset in the ELF image. After the kernel is linked, the "populate-table" command use the debug/elf package to scan the image file and resolve the addresses for the src and dst redirection symbols. The tool will then open the image file in RW mode, seek to the location of the table and write the symbol addresses for each (src, dst) tuple. --- tools/redirects/redirects.go | 233 +++++++++++++++++++++++++++++++++++ 1 file changed, 233 insertions(+) create mode 100644 tools/redirects/redirects.go diff --git a/tools/redirects/redirects.go b/tools/redirects/redirects.go new file mode 100644 index 0000000..5e6d3b4 --- /dev/null +++ b/tools/redirects/redirects.go @@ -0,0 +1,233 @@ +package main + +import ( + "debug/elf" + "encoding/binary" + "errors" + "flag" + "fmt" + "go/ast" + "go/parser" + "go/token" + "io" + "os" + "path/filepath" + "strings" +) + +type redirect struct { + src string + dst string + + srcVMA uint64 + dstVMA uint64 +} + +func exit(err error) { + fmt.Fprintf(os.Stderr, "[redirects] error: %s\n", err.Error()) + os.Exit(1) +} + +func pkgPrefix() (string, error) { + goPath := os.Getenv("GOPATH") + "/src/" + cwd, err := os.Getwd() + if err != nil { + return "", err + } + + return strings.TrimPrefix(cwd, goPath), nil +} + +func collectGoFiles(root string) ([]string, error) { + var goFiles []string + err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if info.IsDir() { + return err + } + + if filepath.Ext(path) == ".go" && !strings.Contains(path, "_test") { + goFiles = append(goFiles, path) + } + + return err + }) + if err != nil { + return nil, err + } + + return goFiles, nil +} + +func findRedirects(goFiles []string) ([]*redirect, error) { + var redirects []*redirect + + prefix, err := pkgPrefix() + if err != nil { + return nil, err + } + + for _, goFile := range goFiles { + fset := token.NewFileSet() + + f, err := parser.ParseFile(fset, goFile, nil, parser.ParseComments) + if err != nil { + return nil, fmt.Errorf("%s: %s", goFile, err) + } + + cmap := ast.NewCommentMap(fset, f, f.Comments) + cmap.Filter(f) + for astNode, commentGroups := range cmap { + fnDecl, ok := astNode.(*ast.FuncDecl) + if !ok { + continue + } + + for _, commentGroup := range commentGroups { + for _, comment := range commentGroup.List { + if !strings.Contains(comment.Text, "go:redirect-from") { + continue + } + + // build qualified name to fn + fqName := fmt.Sprintf("%s/%s.%s", + prefix, + goFile[:strings.LastIndexByte(goFile, '/')], + fnDecl.Name, + ) + + fields := strings.Fields(comment.Text) + if len(fields) != 2 || fields[0] != "//go:redirect-from" { + return nil, fmt.Errorf("malformed go:redirect-from syntax for %q", fqName) + } + + redirects = append(redirects, &redirect{ + src: fields[1], + dst: fqName, + }) + } + } + } + } + + return redirects, nil +} + +func elfRedirectTableOffset(imgFile string) (uint64, error) { + f, err := elf.Open(imgFile) + if err != nil { + return 0, err + } + defer f.Close() + + redirectsSection := f.Section(".goredirectstbl") + if redirectsSection == nil { + return 0, fmt.Errorf("%s: missing .goredirectstbl section", imgFile) + } + + return redirectsSection.Offset, nil +} + +func elfWriteRedirectTable(redirects []*redirect, imgFile string) error { + redirectTableOffset, err := elfRedirectTableOffset(imgFile) + if err != nil { + return err + } + + // Open kernel image file and seek to table offset + f, err := os.OpenFile(imgFile, os.O_WRONLY, os.ModeType) + if err != nil { + return err + } + defer f.Close() + + if _, err = f.Seek(int64(redirectTableOffset), io.SeekStart); err != nil { + return err + } + + for _, redirect := range redirects { + binary.Write(f, binary.LittleEndian, redirect.srcVMA) + binary.Write(f, binary.LittleEndian, redirect.dstVMA) + } + + return nil +} + +func elfResolveRedirectSymbols(redirects []*redirect, imgFile string) error { + f, err := elf.Open(imgFile) + if err != nil { + return err + } + defer f.Close() + + symbols, err := f.Symbols() + if err != nil { + return err + } + + for _, redirect := range redirects { + for _, symbol := range symbols { + if symbol.Name == redirect.src { + redirect.srcVMA = symbol.Value + } + if symbol.Name == redirect.dst { + redirect.dstVMA = symbol.Value + } + } + + switch { + case redirect.srcVMA == 0: + return fmt.Errorf("%s: could not locate address of %q", imgFile, redirect.src) + case redirect.dstVMA == 0: + return fmt.Errorf("%s: could not locate address of %q", imgFile, redirect.dst) + } + } + + return nil +} + +func main() { + flag.Parse() + if matches, _ := filepath.Glob("kernel/"); len(matches) != 1 { + exit(errors.New("this tool must be run from the kernel root folder")) + } + + if len(flag.Args()) == 0 { + exit(errors.New("missing command")) + } + + cmd := flag.Arg(0) + var imgFile string + switch cmd { + case "count": + case "populate-table": + if len(flag.Args()) != 2 { + exit(errors.New("populate-table requires the path to the kernel image as an argument")) + } + imgFile = flag.Arg(1) + default: + exit(fmt.Errorf("unknown command %q", cmd)) + } + + goFiles, err := collectGoFiles("kernel/") + if err != nil { + exit(err) + } + + redirects, err := findRedirects(goFiles) + if err != nil { + exit(err) + } + + if cmd == "count" { + fmt.Printf("%d", len(redirects)) + return + } + + if err = elfResolveRedirectSymbols(redirects, imgFile); err != nil { + exit(err) + } + + if err = elfWriteRedirectTable(redirects, imgFile); err != nil { + exit(err) + } +} From d17f582c0b4445c071ef78acee10161297eb2747 Mon Sep 17 00:00:00 2001 From: Achilleas Anagnostopoulos Date: Sun, 25 Jun 2017 20:00:05 +0100 Subject: [PATCH 2/4] Reserve space for redirect table and install trampolines The rt0_64 code reserves space for _rt0_redirect_table using the output from the redirect tool's "count" command as a hint to the size of the table. The table itself is located in the .goredirectstbl section which the linker moves to a dedicated section in the final ELF image. When the kernel boots, the _rt0_install_redirect_trampolines function iterates the _rt0_redirect_table entries (populated as a post-link step) and overwrite the original function code with a trampoline that redirects control to the destination function. The trampoline is implemented as a 14-byte instruction that exploits rip-relative addressing to ensure that no registers are made dirty. The actual trampoline code looks like this: jmp [rip+0] ; 6-bytes dq abs_address_to_jump_to ; 8-bytes The _rt0_install_redirect_trampolines function sets up the abs_address to "dst" for each (src, dst) tuple and then copies the trampoline to "src". After the trampoline is installed, any calls to "src" will be transparently redirected to "dst". This hack (modifying code in the .text section) is only possible because the code runs in supervisor mode before memory protection is enabled. --- arch/x86_64/asm/rt0_64.s | 68 ++++++++++++++++++++++++++++++++- arch/x86_64/script/linker.ld.in | 9 +++++ 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/arch/x86_64/asm/rt0_64.s b/arch/x86_64/asm/rt0_64.s index a1ea0d9..83c2077 100644 --- a/arch/x86_64/asm/rt0_64.s +++ b/arch/x86_64/asm/rt0_64.s @@ -40,6 +40,7 @@ section .text ;------------------------------------------------------------------------------ global _rt0_64_entry _rt0_64_entry: + call _rt0_install_redirect_trampolines call _rt0_64_load_idt ; According to the x86_64 ABI, the fs:0 should point to the address of @@ -239,7 +240,6 @@ _rt0_64_gate_dispatcher_with_code: 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: @@ -307,3 +307,69 @@ write_string: .done: ret + +;------------------------------------------------------------------------------ +; Install redirect trampolines. This hack allows us to redirect calls to Go +; runtime functions to the kernel's own implementation without the need to +; export/globalize any symbols. This works by first setting up a redirect table +; (populated by a post-link step) that contains the addresses of the symbol to +; hook and the address where calls to that symbol should be redirected. +; +; This function iterates the redirect table entries and for each entry it +; sets up a trampoline to the dst symbol and overwrites the code in src with +; the 14-byte long _rt0_redirect_trampoline code. +; +; Note: this code modification is only possible because we are currently +; operating in supervisor mode with no memory protection enabled. Under normal +; conditions the .text section should be flagged as read-only. +;------------------------------------------------------------------------------ +_rt0_install_redirect_trampolines: + mov rax, _rt0_redirect_table + mov rdx, NUM_REDIRECTS + +_rt0_install_redirect_rampolines.next: + mov rdi, [rax] ; the symbol address to hook + mov rbx, [rax+8] ; the symbol to redirect to + + ; setup trampoline target and copy it to the hooked symbol + mov rsi, _rt0_redirect_trampoline + mov qword [rsi+6], rbx + mov rcx, 14 + rep movsb ; copy rcx bytes from rsi to rdi + + add rax, 16 + dec rdx + jnz _rt0_install_redirect_rampolines.next + + ret + +;------------------------------------------------------------------------------ +; This trampoline exploits rip-relative addressing to allow a jump to a +; 64-bit address without the need to touch any registers. The generated +; code is equivalent to: +; +; jmp [rip+0] +; dq abs_address_to_jump_to +;------------------------------------------------------------------------------ +_rt0_redirect_trampoline: + db 0xff ; the first 6 bytes encode a "jmp [rip+0]" instruction + db 0x25 + dd 0x00 + dq 0x00 ; the absolute address to jump to + +;------------------------------------------------------------------------------ +; The redirect table is placed in a dedicated section allowing us to easily +; find its offset in the kernel image file. As the VMA addresses of the src +; and target symbols for the redirect are now known in advance we just reserve +; enough space space for the src and dst addresses using the NUM_REDIRECTS +; define which is calculated by the Makefile and passed to nasm. +;------------------------------------------------------------------------------ +section .goredirectstbl + +_rt0_redirect_table: + %rep NUM_REDIRECTS + dq 0 ; src: address of the symbol we want to redirect + dq 0 ; dst: address of the symbol where calls to src are redirected to + %endrep + + diff --git a/arch/x86_64/script/linker.ld.in b/arch/x86_64/script/linker.ld.in index 31a8ae0..b12e292 100644 --- a/arch/x86_64/script/linker.ld.in +++ b/arch/x86_64/script/linker.ld.in @@ -38,6 +38,15 @@ SECTIONS { *(COMMON) *(.bss) } + + /* Go function redirection table. This table is used for hooking + * Go runtime function symbols so that calls to them are redirected to + * functions provided by the kernel. + */ + .goredirectstbl ALIGN(4K): AT(ADDR(.goredirectstbl) - PAGE_OFFSET) + { + *(.goredirectstbl) + } _kernel_end = ALIGN(4K); } From b238442ccc6d5a3c3225b32babed9d20e1a48e66 Mon Sep 17 00:00:00 2001 From: Achilleas Anagnostopoulos Date: Sun, 25 Jun 2017 20:40:36 +0100 Subject: [PATCH 3/4] Use redirects tool with nasm and as a post-link step --- Makefile | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 46c5f20..259bad3 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,7 @@ GOARCH := amd64 GOROOT := $(shell $(GO) env GOROOT) LD_FLAGS := -n -T $(BUILD_DIR)/linker.ld -static --no-ld-generated-unwind-info -AS_FLAGS := -g -f elf64 -F dwarf -I arch/$(ARCH)/asm/ +AS_FLAGS := -g -f elf64 -F dwarf -I arch/$(ARCH)/asm/ -dNUM_REDIRECTS=$(shell $(GO) run tools/redirects/redirects.go count) MIN_OBJCOPY_VERSION := 2.26.0 HAVE_VALID_OBJCOPY := $(shell objcopy -V | head -1 | awk -F ' ' '{print "$(MIN_OBJCOPY_VERSION)\n" $$NF}' | sort -ct. -k1,1n -k2,2n && echo "y") @@ -31,8 +31,12 @@ asm_obj_files := $(patsubst arch/$(ARCH)/asm/%.s, $(BUILD_DIR)/arch/$(ARCH)/asm/ .PHONY: kernel iso clean binutils_version_check -kernel: binutils_version_check $(kernel_target) +kernel: binutils_version_check kernel_image +kernel_image: $(kernel_target) + @echo "[tools:redirects] populating kernel image redirect table" + @$(GO) run tools/redirects/redirects.go populate-table $(kernel_target) + $(kernel_target): $(asm_obj_files) linker_script go.o @echo "[$(LD)] linking kernel-$(ARCH).bin" @$(LD) $(LD_FLAGS) -o $(kernel_target) $(asm_obj_files) $(BUILD_DIR)/go.o @@ -88,7 +92,7 @@ $(BUILD_DIR)/arch/$(ARCH)/asm/%.o: arch/$(ARCH)/asm/%.s iso: $(iso_target) -$(iso_target): iso_prereq $(kernel_target) +$(iso_target): iso_prereq kernel_image @echo "[grub] building ISO kernel-$(ARCH).iso" @mkdir -p $(BUILD_DIR)/isofiles/boot/grub From 5fc6ce188e78d4de23d4157812f12b705f1345d7 Mon Sep 17 00:00:00 2001 From: Achilleas Anagnostopoulos Date: Sun, 25 Jun 2017 20:42:35 +0100 Subject: [PATCH 4/4] Use go:redirect-from directive to map panic to kernel.Panic All calls (but one) to kernel.Panic have been replaced by calls to panic. A call to kernel.Panic is still required to prevent the compiler from treating kernel.Panic as dead code and eliminating it. --- kernel/kmain/kmain.go | 12 +++- kernel/mem/vmm/vmm.go | 9 +-- kernel/mem/vmm/vmm_test.go | 138 +++++++++++++++---------------------- kernel/panic.go | 21 +++++- kernel/panic_test.go | 39 ++++++++++- 5 files changed, 129 insertions(+), 90 deletions(-) diff --git a/kernel/kmain/kmain.go b/kernel/kmain/kmain.go index 97347fe..2c59342 100644 --- a/kernel/kmain/kmain.go +++ b/kernel/kmain/kmain.go @@ -8,6 +8,10 @@ import ( "github.com/achilleasa/gopher-os/kernel/mem/vmm" ) +var ( + errKmainReturned = &kernel.Error{Module: "kmain", Message: "Kmain returned"} +) + // Kmain is the only Go symbol that is visible (exported) from the rt0 initialization // code. This function is invoked by the rt0 assembly code after setting up the GDT // and setting up a a minimal g0 struct that allows Go code using the 4K stack @@ -27,8 +31,12 @@ func Kmain(multibootInfoPtr, kernelStart, kernelEnd uintptr) { var err *kernel.Error if err = allocator.Init(kernelStart, kernelEnd); err != nil { - kernel.Panic(err) + panic(err) } else if err = vmm.Init(); err != nil { - kernel.Panic(err) + panic(err) } + + // Use kernel.Panic instead of panic to prevent the compiler from + // treating kernel.Panic as dead-code and eliminating it. + kernel.Panic(errKmainReturned) } diff --git a/kernel/mem/vmm/vmm.go b/kernel/mem/vmm/vmm.go index b698f5c..9044489 100644 --- a/kernel/mem/vmm/vmm.go +++ b/kernel/mem/vmm/vmm.go @@ -16,9 +16,10 @@ var ( // the following functions are mocked by tests and are automatically // inlined by the compiler. - panicFn = kernel.Panic handleExceptionWithCodeFn = irq.HandleExceptionWithCode readCR2Fn = cpu.ReadCR2 + + errUnrecoverableFault = &kernel.Error{Module: "vmm", Message: "page/gpf fault"} ) // FrameAllocatorFn is a function that can allocate physical frames. @@ -78,7 +79,7 @@ func pageFaultHandler(errorCode uint64, frame *irq.Frame, regs *irq.Regs) { } } - nonRecoverablePageFault(faultAddress, errorCode, frame, regs, nil) + nonRecoverablePageFault(faultAddress, errorCode, frame, regs, errUnrecoverableFault) } func nonRecoverablePageFault(faultAddress uintptr, errorCode uint64, frame *irq.Frame, regs *irq.Regs, err *kernel.Error) { @@ -107,7 +108,7 @@ func nonRecoverablePageFault(faultAddress uintptr, errorCode uint64, frame *irq. frame.Print() // TODO: Revisit this when user-mode tasks are implemented - panicFn(err) + panic(err) } func generalProtectionFaultHandler(_ uint64, frame *irq.Frame, regs *irq.Regs) { @@ -117,7 +118,7 @@ func generalProtectionFaultHandler(_ uint64, frame *irq.Frame, regs *irq.Regs) { frame.Print() // TODO: Revisit this when user-mode tasks are implemented - panicFn(nil) + panic(errUnrecoverableFault) } // reserveZeroedFrame reserves a physical frame to be used together with diff --git a/kernel/mem/vmm/vmm_test.go b/kernel/mem/vmm/vmm_test.go index f4ecbb0..030c922 100644 --- a/kernel/mem/vmm/vmm_test.go +++ b/kernel/mem/vmm/vmm_test.go @@ -2,6 +2,7 @@ package vmm import ( "bytes" + "fmt" "strings" "testing" "unsafe" @@ -17,18 +18,16 @@ import ( func TestRecoverablePageFault(t *testing.T) { var ( - frame irq.Frame - regs irq.Regs - panicCalled bool - pageEntry pageTableEntry - origPage = make([]byte, mem.PageSize) - clonedPage = make([]byte, mem.PageSize) - err = &kernel.Error{Module: "test", Message: "something went wrong"} + frame irq.Frame + regs irq.Regs + pageEntry pageTableEntry + origPage = make([]byte, mem.PageSize) + clonedPage = make([]byte, mem.PageSize) + err = &kernel.Error{Module: "test", Message: "something went wrong"} ) defer func(origPtePtr func(uintptr) unsafe.Pointer) { ptePtrFn = origPtePtr - panicFn = kernel.Panic readCR2Fn = cpu.ReadCR2 frameAllocator = nil mapTemporaryFn = MapTemporary @@ -58,97 +57,87 @@ func TestRecoverablePageFault(t *testing.T) { mockTTY() - panicFn = func(_ *kernel.Error) { - panicCalled = true - } - ptePtrFn = func(entry uintptr) unsafe.Pointer { return unsafe.Pointer(&pageEntry) } readCR2Fn = func() uint64 { return uint64(uintptr(unsafe.Pointer(&origPage[0]))) } unmapFn = func(_ Page) *kernel.Error { return nil } flushTLBEntryFn = func(_ uintptr) {} for specIndex, spec := range specs { - mapTemporaryFn = func(f pmm.Frame) (Page, *kernel.Error) { return Page(f), spec.mapError } - SetFrameAllocator(func() (pmm.Frame, *kernel.Error) { - addr := uintptr(unsafe.Pointer(&clonedPage[0])) - return pmm.Frame(addr >> mem.PageShift), spec.allocError - }) + t.Run(fmt.Sprint(specIndex), func(t *testing.T) { + defer func() { + err := recover() + if spec.expPanic && err == nil { + t.Error("expected a panic") + } else if !spec.expPanic { + if err != nil { + t.Error("unexpected panic") + return + } - for i := 0; i < len(origPage); i++ { - origPage[i] = byte(i % 256) - clonedPage[i] = 0 - } - - panicCalled = false - pageEntry = 0 - pageEntry.SetFlags(spec.pteFlags) - - pageFaultHandler(2, &frame, ®s) - - if spec.expPanic != panicCalled { - t.Errorf("[spec %d] expected panic %t; got %t", specIndex, spec.expPanic, panicCalled) - } - - if !spec.expPanic { - for i := 0; i < len(origPage); i++ { - if origPage[i] != clonedPage[i] { - t.Errorf("[spec %d] expected clone page to be a copy of the original page; mismatch at index %d", specIndex, i) + for i := 0; i < len(origPage); i++ { + if origPage[i] != clonedPage[i] { + t.Errorf("expected clone page to be a copy of the original page; mismatch at index %d", i) + } + } } + }() + + mapTemporaryFn = func(f pmm.Frame) (Page, *kernel.Error) { return Page(f), spec.mapError } + SetFrameAllocator(func() (pmm.Frame, *kernel.Error) { + addr := uintptr(unsafe.Pointer(&clonedPage[0])) + return pmm.Frame(addr >> mem.PageShift), spec.allocError + }) + + for i := 0; i < len(origPage); i++ { + origPage[i] = byte(i % 256) + clonedPage[i] = 0 } - } + + pageEntry = 0 + pageEntry.SetFlags(spec.pteFlags) + + pageFaultHandler(2, &frame, ®s) + }) } } func TestNonRecoverablePageFault(t *testing.T) { - defer func() { - panicFn = kernel.Panic - }() - 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, }, } @@ -157,58 +146,45 @@ func TestNonRecoverablePageFault(t *testing.T) { frame irq.Frame ) - panicCalled := false - panicFn = func(_ *kernel.Error) { - panicCalled = true - } - for specIndex, spec := range specs { - fb := mockTTY() - panicCalled = false + t.Run(fmt.Sprint(specIndex), func(t *testing.T) { + defer func() { + if err := recover(); err != errUnrecoverableFault { + t.Errorf("expected a panic with errUnrecoverableFault; got %v", err) + } + }() + fb := mockTTY() - nonRecoverablePageFault(0xbadf00d000, spec.errCode, &frame, ®s, nil) - 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) - } + nonRecoverablePageFault(0xbadf00d000, spec.errCode, &frame, ®s, errUnrecoverableFault) + if got := readTTY(fb); !strings.Contains(got, spec.expReason) { + t.Errorf("expected reason %q; got output:\n%q", spec.expReason, got) + } + }) } } 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 - } + defer func() { + if err := recover(); err != errUnrecoverableFault { + t.Errorf("expected a panic with errUnrecoverableFault; got %v", err) + } + }() + mockTTY() 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) { diff --git a/kernel/panic.go b/kernel/panic.go index b94cfae..b05e198 100644 --- a/kernel/panic.go +++ b/kernel/panic.go @@ -8,11 +8,28 @@ import ( var ( // cpuHaltFn is mocked by tests and is automatically inlined by the compiler. cpuHaltFn = cpu.Halt + + errRuntimePanic = &Error{Module: "rt", Message: "unknown cause"} ) // Panic outputs the supplied error (if not nil) to the console and halts the -// CPU. Calls to Panic never return. -func Panic(err *Error) { +// CPU. Calls to Panic never return. Panic also works as a redirection target +// for calls to panic() (resolved via runtime.gopanic) +//go:redirect-from runtime.gopanic +func Panic(e interface{}) { + var err *Error + + switch t := e.(type) { + case *Error: + err = t + case string: + errRuntimePanic.Message = t + err = errRuntimePanic + case error: + errRuntimePanic.Message = t.Error() + err = errRuntimePanic + } + early.Printf("\n-----------------------------------\n") if err != nil { early.Printf("[%s] unrecoverable error: %s\n", err.Module, err.Message) diff --git a/kernel/panic_test.go b/kernel/panic_test.go index 0339129..6e1e6bb 100644 --- a/kernel/panic_test.go +++ b/kernel/panic_test.go @@ -2,6 +2,7 @@ package kernel import ( "bytes" + "errors" "testing" "unsafe" @@ -20,7 +21,7 @@ func TestPanic(t *testing.T) { cpuHaltCalled = true } - t.Run("with error", func(t *testing.T) { + t.Run("with *kernel.Error", func(t *testing.T) { cpuHaltCalled = false fb := mockTTY() err := &Error{Module: "test", Message: "panic test"} @@ -38,6 +39,42 @@ func TestPanic(t *testing.T) { } }) + t.Run("with error", func(t *testing.T) { + cpuHaltCalled = false + fb := mockTTY() + err := errors.New("go error") + + Panic(err) + + exp := "\n-----------------------------------\n[rt] unrecoverable error: go error\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("with string", func(t *testing.T) { + cpuHaltCalled = false + fb := mockTTY() + err := "string error" + + Panic(err) + + exp := "\n-----------------------------------\n[rt] unrecoverable error: string error\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()