diff --git a/Makefile b/Makefile index a35d2b2..8d861c0 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,7 @@ GOROOT := $(shell $(GO) env GOROOT) GC_FLAGS ?= 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/ -dNUM_REDIRECTS=$(shell $(GO) run tools/redirects/redirects.go count) +AS_FLAGS := -g -f elf64 -F dwarf -I $(BUILD_DIR)/ -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") @@ -37,8 +37,8 @@ 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 + +$(kernel_target): asm_files linker_script go.o @echo "[$(LD)] linking kernel-$(ARCH).bin" @$(LD) $(LD_FLAGS) -o $(kernel_target) $(asm_obj_files) $(BUILD_DIR)/go.o @@ -65,6 +65,9 @@ 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 \ + --globalize-symbol runtime.g0 \ + --globalize-symbol runtime.m0 \ + --globalize-symbol runtime.physPageSize \ $(BUILD_DIR)/go.o $(BUILD_DIR)/go.o binutils_version_check: @@ -86,11 +89,19 @@ linker_script: -E -x \ c arch/$(ARCH)/script/linker.ld.in | grep -v "^#" > $(BUILD_DIR)/linker.ld +$(BUILD_DIR)/go_asm_offsets.inc: + @mkdir -p $(BUILD_DIR) + + @echo "[tools:offsets] calculating OS/arch-specific offsets for g, m and stack structs" + @$(GO) run tools/offsets/offsets.go -target-os $(GOOS) -target-arch $(GOARCH) -go-binary $(GO) -out $@ + $(BUILD_DIR)/arch/$(ARCH)/asm/%.o: arch/$(ARCH)/asm/%.s @mkdir -p $(shell dirname $@) @echo "[$(AS)] $<" @$(AS) $(AS_FLAGS) $< -o $@ +asm_files: $(BUILD_DIR)/go_asm_offsets.inc $(asm_obj_files) + iso: $(iso_target) $(iso_target): iso_prereq kernel_image diff --git a/arch/x86_64/asm/rt0_64.s b/arch/x86_64/asm/rt0_64.s index 83c2077..40e9a0d 100644 --- a/arch/x86_64/asm/rt0_64.s +++ b/arch/x86_64/asm/rt0_64.s @@ -20,12 +20,9 @@ _rt0_idt_desc: ; 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 -r0_g_stack_hi: resq 1 -r0_g_stackguard0: resq 1 ; rsp compared to this value in go stack growth prologue -r0_g_stackguard1: resq 1 ; rsp compared to this value in C stack growth prologue +; The FS register is loaded with the address of r0_g_ptr. fs:0x00 should contain +; a pointer to the currently active g struct (in this case runtime.g0) +r0_g_ptr: resq 1 section .text @@ -42,34 +39,7 @@ 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 - ; the user-space thread structure. The actual TLS structure is located - ; just before that (aligned). Go code tries to fetch the address to the - ; active go-routine's g struct by accessing fs:-8. What we need to do - ; is to setup a mock g0 struct, populate its stack_lo/hi/guard fields - ; and then use wrmsr to update the FS register - extern stack_top - extern stack_bottom - - ; Setup r0_g - mov rax, stack_bottom - mov rbx, stack_top - mov rsi, r0_g - mov qword [rsi+0], rax ; stack_lo - mov qword [rsi+8], rbx ; stack_hi - mov qword [rsi+16], rax ; stackguard0 - mov rax, r0_g_ptr - mov qword [rax], rsi - - ; Load 64-bit FS register address - ; rax -> lower 32 bits - ; rdx -> upper 32 bits - mov ecx, 0xc0000100 ; fs_base - mov rax, rsi ; lower 32 bits - shr rsi, 32 - mov rdx, rsi ; high 32 bits - wrmsr + call _rt0_64_setup_go_runtime_structs ; Call the kernel entry point passing a pointer to the multiboot data ; copied by the 32-bit entry code @@ -93,6 +63,58 @@ _rt0_64_entry: cli hlt +;------------------------------------------------------------------------------ +; Setup m0, g0 and other symbols required for bootstrapping the Go runtime. +; For the definitions of g and m see the Go runtime src: src/runtime/runtime2.go +;------------------------------------------------------------------------------ +_rt0_64_setup_go_runtime_structs: + %include "go_asm_offsets.inc" ; generated by tools/offsets + + ; The Go allocator expects this symbol to be set to the system page size + ; As the kernel bypass osinit() this needs to be set here. + extern runtime.physPageSize + mov rax, runtime.physPageSize + mov qword [rax], 0x1000 ; 4096 + + ; Setup r0_g stack limits using the reserved stack + extern stack_top + extern stack_bottom + extern runtime.g0 + mov rax, stack_bottom + mov rbx, stack_top + mov rsi, runtime.g0 + mov qword [rsi+GO_G_STACK+GO_STACK_LO], rax ; g.stack.lo + mov qword [rsi+GO_G_STACK+GO_STACK_HI], rbx ; g.stack.hi + mov qword [rsi+GO_G_STACKGUARD0], rax ; g.stackguard0 + + ; Link m0 to the g0 + extern runtime.m0 + mov rbx, runtime.m0 + mov qword [rbx+GO_M_G0], rsi ; m.g0 = g0 + mov qword [rsi+GO_G_M], rbx ; g.m = m + + ; Store the address of g0 in r0_g_ptr + mov rax, r0_g_ptr + mov qword [rax], rsi + + ; According to the x86_64 ABI, the fs register should contain the + ; address after the pointer to the pointer to the user-space thread + ; structure. This allows the Go runtime to retrieve the address of + ; the currently active g structure by accessing fs:-0x8. + ; + ; Load 64-bit FS register address + ; eax -> lower 32 bits + ; edx -> upper 32 bits + mov ecx, 0xc0000100 ; fs_base + mov rsi, r0_g_ptr + add rsi, 8 ; fs -> r0_g_ptr + 0x8 + mov rax, rsi ; lower 32 bits + shr rsi, 32 + mov rdx, rsi ; high 32 bits + wrmsr + + ret + ;------------------------------------------------------------------------------ ; Setup and load IDT. We preload each IDT entry with a pointer to a gate handler diff --git a/tools/offsets/offsets.go b/tools/offsets/offsets.go new file mode 100644 index 0000000..c749906 --- /dev/null +++ b/tools/offsets/offsets.go @@ -0,0 +1,192 @@ +package main + +import ( + "errors" + "flag" + "fmt" + "io/ioutil" + "os" + "os/exec" + "strconv" + "strings" + "time" +) + +func exit(err error) { + fmt.Fprintf(os.Stderr, "[offsets] error: %s\n", err.Error()) + os.Exit(1) +} + +func genBuildScript(targetOS, targetArch, goBinary, workDir string) ([]byte, error) { + // Write a dummy program in workDir so "go build" does not complain + dummyGoProgram := []byte("package main\n func main(){}") + err := ioutil.WriteFile(fmt.Sprintf("%s/main.go", workDir), dummyGoProgram, os.ModePerm) + if err != nil { + return nil, err + } + + // Run "go build -a -n" in workDir and capture the output. The -a flag + // ensures that the generated build script includes steps to always + // rebuild the runtime packages. + cmd := exec.Command(goBinary, "build", "-a", "-n") + cmd.Dir = workDir + cmd.Env = append(cmd.Env, fmt.Sprintf("GOOS=%s", targetOS)) + cmd.Env = append(cmd.Env, fmt.Sprintf("GOARCH=%s", targetArch)) + out, err := cmd.CombinedOutput() + if err != nil { + return nil, fmt.Errorf("failed to generate build script\nMore info:\n%s", out) + } + + return out, nil +} + +func patchBuildScript(script []byte, workDir, targetOS, targetArch, goBinary string) ([]byte, error) { + lines := strings.Split(string(script), "\n") + + // Inject os/arch and workdir to the top of the build file + header := []string{ + fmt.Sprintf("export GOOS=%s", targetOS), + fmt.Sprintf("export GOARCH=%s", targetArch), + fmt.Sprintf("WORK=%q", workDir), + fmt.Sprintf("alias pack='%s tool pack'", goBinary), + } + lines = append(header, lines...) + + // We are only interested in building the runtime as this block generates + // the asm headers. Scan the lines till we find "# runtime" comment and + // stop at next comment + var stopOnNextComment bool + for lineIndex := 0; lineIndex < len(lines); lineIndex++ { + // Ignore empty comments + if lines[lineIndex] == "#" { + continue + } + + if stopOnNextComment && strings.HasPrefix(lines[lineIndex], "#") { + return []byte(strings.Join(lines[:lineIndex], "\n")), nil + } + + if lines[lineIndex] == "# runtime" { + stopOnNextComment = true + } + } + + return nil, errors.New("generated build file does not specify -asmhdr when building the runtime") +} + +func execBuildScript(script []byte, workDir string) error { + f, err := os.Create(fmt.Sprintf("%s/build.sh", workDir)) + if err != nil { + return err + } + + _, err = f.Write(script) + if err != nil { + f.Close() + return err + } + f.Close() + + cmd := exec.Command("sh", f.Name()) + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("failed to execute build script\nMore info:\n%s", out) + } + + return nil +} + +func genAsmIncludes(workDir string) ([]byte, error) { + headers, err := ioutil.ReadFile(fmt.Sprintf("%s/runtime/_obj/go_asm.h", workDir)) + if err != nil { + return nil, err + } + + var includes []string + includes = append(includes, "; vim: set ft=nasm :\n") + includes = append(includes, fmt.Sprintf("; generated by tools/offsets at %v\n", time.Now())) + + for _, line := range strings.Split(string(headers), "\n") { + line = strings.TrimPrefix(line, "#define ") + + // We are only interested in the offsets for the g, m and stack structures + if strings.HasPrefix(line, "g_") || strings.HasPrefix(line, "m_") || strings.HasPrefix(line, "stack_") { + tokens := strings.Fields(line) + if len(tokens) != 2 { + continue + } + + offset, err := strconv.ParseInt(tokens[1], 10, 32) + if err != nil { + continue + } + + includes = append(includes, + fmt.Sprintf("GO_%s equ 0x%x ; %d", + strings.ToUpper(tokens[0]), + offset, + offset, + ), + ) + } + } + + return []byte(strings.Join(includes, "\n")), nil +} + +func runTool() error { + targetOS := flag.String("target-os", "", "a valid GOOS value for generating the asm offsets") + targetArch := flag.String("target-arch", "", "a valid GOARCH value for generating the asm offsets") + goBinary := flag.String("go-binary", "go", "the Go binary to use") + asmOutput := flag.String("out", "-", "a file to write the asm headers or - to output to STDOUT") + flag.Parse() + + switch { + case *targetOS == "": + exit(errors.New("-target-os parameter missing")) + case *targetArch == "": + exit(errors.New("-target-arch parameter missing")) + } + + workDir, err := ioutil.TempDir("", "offsets-tool") + if err != nil { + return err + } + defer os.RemoveAll(workDir) + + buildScript, err := genBuildScript(*targetOS, *targetArch, *goBinary, workDir) + if err != nil { + return err + } + + buildScript, err = patchBuildScript(buildScript, workDir, *targetOS, *targetArch, *goBinary) + if err != nil { + return err + } + + if err = execBuildScript(buildScript, workDir); err != nil { + return err + } + + asmIncludes, err := genAsmIncludes(workDir) + if err != nil { + return err + } + + switch *asmOutput { + case "-": + fmt.Printf("%s\n", string(asmIncludes)) + default: + if err = ioutil.WriteFile(*asmOutput, asmIncludes, os.ModePerm); err != nil { + return err + } + } + + return nil +} + +func main() { + if err := runTool(); err != nil { + exit(err) + } +}