From 62aca2f2de7608d265792a95e899cda5bf5f7e72 Mon Sep 17 00:00:00 2001 From: Achilleas Anagnostopoulos Date: Tue, 27 Jun 2017 18:48:29 +0100 Subject: [PATCH 1/3] Implement tool for calculating offsets into g, m and stack structs The offsets tool is essentially a wrapper around "go build -a -n". It creates a temporary folder with a dummy go file and runs the above command using the target OS/ARCH for the kernel and captures the output. The use of the "-a" flag forces go build to generate a build script for rebuilding all packages including the runtime ones. As a by-product of building the runtime package, the compiler emits the "go_asm.h" file that contains (among other things) the offsets for each element of the g, m and stack structures (see src/runtime/runtime2.go). These offsets are used in Go assembly files instead of hardcoded offsets. For example the following snippet accesses the pointer to m in the g struct address stored at register CX: MOVQ TLS, CX MOVQ g_m(CX), BX The offsets tool modifies the captured output from the go build command so it only includes the steps up to building the runtime package, executes the build script and post-processes the generated go_asm.h file to retain the entries relevant to g, m and stack and then formats them so they are compatible with nasm definitions (name equ value). Depending on the value of the "-out" option, the tool outputs the generated definitions either to STDOUT (default value for -out) or to a file. --- tools/offsets/offsets.go | 192 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 tools/offsets/offsets.go 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) + } +} From 6820ffef2b18f5f717dc4cbf069543605dddc35f Mon Sep 17 00:00:00 2001 From: Achilleas Anagnostopoulos Date: Tue, 27 Jun 2017 18:51:39 +0100 Subject: [PATCH 2/3] Generate offset definitions as part of the build process The Makefile contains rules for invoking the offsets tool to generate the offset definitions for members of the g, m and stack structs. The definitions are stored in BUILD_DIR and BUILD_DIR is passed as an include target to nasm. --- Makefile | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) 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 From b733915536f3eb28df2c44cf42bb74e5ed248a12 Mon Sep 17 00:00:00 2001 From: Achilleas Anagnostopoulos Date: Tue, 27 Jun 2017 18:55:26 +0100 Subject: [PATCH 3/3] Use runtime.g0, runtime.m0 and replace hardcoded offsets The rt0 code implements a dedicated function for initializing the Go runtime structures. Instead of reserving space for a dummy g struct, the rt0 code now uses the g0 and m0 symbols defined by the runtime package. In addition to setting up g0, the rt0 also sets up the m0 struct and links it to g0. Setting up m0 is a requirement for properly bootstapping the malloc-related code in the following commits --- arch/x86_64/asm/rt0_64.s | 90 +++++++++++++++++++++++++--------------- 1 file changed, 56 insertions(+), 34 deletions(-) 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