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 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); } 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() 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) + } +}