diff options
author | Ayke van Laethem <[email protected]> | 2023-01-29 03:33:37 -0800 |
---|---|---|
committer | Ron Evans <[email protected]> | 2023-01-30 21:42:47 +0100 |
commit | df0f5ae1da0fdb7b136e70ca9bd1e3217c04dbcd (patch) | |
tree | d9156f0ebe67d883f9fe2195d48cf9b183fd39e4 | |
parent | 45c8817ddd6818ec96d4f180dc15237e945e5122 (diff) | |
download | tinygo-df0f5ae1da0fdb7b136e70ca9bd1e3217c04dbcd.tar.gz tinygo-df0f5ae1da0fdb7b136e70ca9bd1e3217c04dbcd.zip |
windows: add ARM64 support
This was actually surprising once I got TinyGo to build on Windows 11
ARM64. All the changes are exactly what you'd expect for a new
architecture, there was no special weirdness just for arm64.
Actually getting TinyGo to build was kind of involved though. The very
short summary is: install arm64 versions of some pieces of software
(like golang, cmake) instead of installing them though choco. In
particular, use the llvm-mingw[1] toolchain instead of using standard
mingw.
[1]: https://github.com/mstorsjo/llvm-mingw/releases
-rw-r--r-- | Makefile | 1 | ||||
-rw-r--r-- | builder/build.go | 2 | ||||
-rw-r--r-- | builder/builder_test.go | 1 | ||||
-rw-r--r-- | builder/mingw-w64.go | 18 | ||||
-rw-r--r-- | builder/tools-builtin.go | 2 | ||||
-rw-r--r-- | compileopts/target.go | 13 | ||||
-rw-r--r-- | compiler/defer.go | 4 | ||||
-rw-r--r-- | src/internal/task/task_stack_arm64.go | 2 | ||||
-rw-r--r-- | src/internal/task/task_stack_arm64_windows.S | 65 | ||||
-rw-r--r-- | src/internal/task/task_stack_arm64_windows.go | 72 | ||||
-rw-r--r-- | src/runtime/asm_arm64_windows.S | 36 |
11 files changed, 206 insertions, 10 deletions
@@ -738,6 +738,7 @@ endif @$(MD5SUM) test.hex GOOS=linux GOARCH=arm $(TINYGO) build -size short -o test.elf ./testdata/cgo GOOS=windows GOARCH=amd64 $(TINYGO) build -size short -o test.exe ./testdata/cgo + GOOS=windows GOARCH=arm64 $(TINYGO) build -size short -o test.exe ./testdata/cgo GOOS=darwin GOARCH=amd64 $(TINYGO) build -size short -o test ./testdata/cgo GOOS=darwin GOARCH=arm64 $(TINYGO) build -size short -o test ./testdata/cgo ifneq ($(OS),Windows_NT) diff --git a/builder/build.go b/builder/build.go index 26395d53f..78a9dc662 100644 --- a/builder/build.go +++ b/builder/build.go @@ -151,7 +151,7 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe return BuildResult{}, err } unlock() - libcDependencies = append(libcDependencies, makeMinGWExtraLibs(tmpdir)...) + libcDependencies = append(libcDependencies, makeMinGWExtraLibs(tmpdir, config.GOARCH())...) case "": // no library specified, so nothing to do default: diff --git a/builder/builder_test.go b/builder/builder_test.go index 3b0ae8bcf..f8bdd5134 100644 --- a/builder/builder_test.go +++ b/builder/builder_test.go @@ -59,6 +59,7 @@ func TestClangAttributes(t *testing.T) { {GOOS: "darwin", GOARCH: "amd64"}, {GOOS: "darwin", GOARCH: "arm64"}, {GOOS: "windows", GOARCH: "amd64"}, + {GOOS: "windows", GOARCH: "arm64"}, } { name := "GOOS=" + options.GOOS + ",GOARCH=" + options.GOARCH if options.GOARCH == "arm" { diff --git a/builder/mingw-w64.go b/builder/mingw-w64.go index 289a85489..1e7701d47 100644 --- a/builder/mingw-w64.go +++ b/builder/mingw-w64.go @@ -1,6 +1,7 @@ package builder import ( + "fmt" "io" "os" "path/filepath" @@ -43,7 +44,7 @@ var MinGW = Library{ // // TODO: cache the result. At the moment, it costs a few hundred milliseconds to // compile these files. -func makeMinGWExtraLibs(tmpdir string) []*compileJob { +func makeMinGWExtraLibs(tmpdir, goarch string) []*compileJob { var jobs []*compileJob root := goenv.Get("TINYGOROOT") // Normally all the api-ms-win-crt-*.def files are all compiled to a single @@ -74,16 +75,27 @@ func makeMinGWExtraLibs(tmpdir string) []*compileJob { result: outpath, run: func(job *compileJob) error { defpath := inpath + var archDef, emulation string + switch goarch { + case "amd64": + archDef = "-DDEF_X64" + emulation = "i386pep" + case "arm64": + archDef = "-DDEF_ARM64" + emulation = "arm64pe" + default: + return fmt.Errorf("unsupported architecture for mingw-w64: %s", goarch) + } if strings.HasSuffix(inpath, ".in") { // .in files need to be preprocessed by a preprocessor (-E) // first. defpath = outpath + ".def" - err := runCCompiler("-E", "-x", "c", "-Wp,-w", "-P", "-DDEF_X64", "-DDATA", "-o", defpath, inpath, "-I"+goenv.Get("TINYGOROOT")+"/lib/mingw-w64/mingw-w64-crt/def-include/") + err := runCCompiler("-E", "-x", "c", "-Wp,-w", "-P", archDef, "-DDATA", "-o", defpath, inpath, "-I"+goenv.Get("TINYGOROOT")+"/lib/mingw-w64/mingw-w64-crt/def-include/") if err != nil { return err } } - return link("ld.lld", "-m", "i386pep", "-o", outpath, defpath) + return link("ld.lld", "-m", emulation, "-o", outpath, defpath) }, } jobs = append(jobs, job) diff --git a/builder/tools-builtin.go b/builder/tools-builtin.go index fbe6ef02f..2b3cba618 100644 --- a/builder/tools-builtin.go +++ b/builder/tools-builtin.go @@ -28,7 +28,7 @@ const hasBuiltinTools = true func RunTool(tool string, args ...string) error { linker := "elf" if tool == "ld.lld" && len(args) >= 2 { - if args[0] == "-m" && args[1] == "i386pep" { + if args[0] == "-m" && (args[1] == "i386pep" || args[1] == "arm64pe") { linker = "mingw" } else if args[0] == "-flavor" { linker = args[1] diff --git a/compileopts/target.go b/compileopts/target.go index 6c01f339e..92e9315ea 100644 --- a/compileopts/target.go +++ b/compileopts/target.go @@ -304,10 +304,19 @@ func defaultTarget(goos, goarch, triple string) (*TargetSpec, error) { // normally present in Go (without explicitly opting in). // For more discussion: // https://groups.google.com/g/Golang-nuts/c/Jd9tlNc6jUE/m/Zo-7zIP_m3MJ?pli=1 + switch goarch { + case "amd64": + spec.LDFlags = append(spec.LDFlags, + "-m", "i386pep", + "--image-base", "0x400000", + ) + case "arm64": + spec.LDFlags = append(spec.LDFlags, + "-m", "arm64pe", + ) + } spec.LDFlags = append(spec.LDFlags, - "-m", "i386pep", "-Bdynamic", - "--image-base", "0x400000", "--gc-sections", "--no-insert-timestamp", "--no-dynamicbase", diff --git a/compiler/defer.go b/compiler/defer.go index 27e903319..5ec3ef7e2 100644 --- a/compiler/defer.go +++ b/compiler/defer.go @@ -162,9 +162,9 @@ mov x0, #0 1: ` constraints = "={x0},{x1},~{x1},~{x2},~{x3},~{x4},~{x5},~{x6},~{x7},~{x8},~{x9},~{x10},~{x11},~{x12},~{x13},~{x14},~{x15},~{x16},~{x17},~{x19},~{x20},~{x21},~{x22},~{x23},~{x24},~{x25},~{x26},~{x27},~{x28},~{lr},~{q0},~{q1},~{q2},~{q3},~{q4},~{q5},~{q6},~{q7},~{q8},~{q9},~{q10},~{q11},~{q12},~{q13},~{q14},~{q15},~{q16},~{q17},~{q18},~{q19},~{q20},~{q21},~{q22},~{q23},~{q24},~{q25},~{q26},~{q27},~{q28},~{q29},~{q30},~{nzcv},~{ffr},~{vg},~{memory}" - if b.GOOS != "darwin" { + if b.GOOS != "darwin" && b.GOOS != "windows" { // These registers cause the following warning when compiling for - // MacOS: + // MacOS and Windows: // warning: inline asm clobber list contains reserved registers: // X18, FP // Reserved registers on the clobber list may not be preserved diff --git a/src/internal/task/task_stack_arm64.go b/src/internal/task/task_stack_arm64.go index 74b8bc2cb..4cf500bda 100644 --- a/src/internal/task/task_stack_arm64.go +++ b/src/internal/task/task_stack_arm64.go @@ -1,4 +1,4 @@ -//go:build scheduler.tasks && arm64 +//go:build scheduler.tasks && arm64 && !windows package task diff --git a/src/internal/task/task_stack_arm64_windows.S b/src/internal/task/task_stack_arm64_windows.S new file mode 100644 index 000000000..45c1ec4dd --- /dev/null +++ b/src/internal/task/task_stack_arm64_windows.S @@ -0,0 +1,65 @@ +.section .text.tinygo_startTask,"ax" +.global tinygo_startTask +tinygo_startTask: + .cfi_startproc + // Small assembly stub for starting a goroutine. This is already run on the + // new stack, with the callee-saved registers already loaded. + // Most importantly, x19 contains the pc of the to-be-started function and + // x20 contains the only argument it is given. Multiple arguments are packed + // into one by storing them in a new allocation. + + // Indicate to the unwinder that there is nothing to unwind, this is the + // root frame. It avoids the following (bogus) error message in GDB: + // Backtrace stopped: previous frame identical to this frame (corrupt stack?) + .cfi_undefined lr + + // Set the first argument of the goroutine start wrapper, which contains all + // the arguments. + mov x0, x20 + + // Branch to the "goroutine start" function. By using blx instead of bx, + // we'll return here instead of tail calling. + blr x19 + + // After return, exit this goroutine. This is a tail call. + b tinygo_pause + .cfi_endproc + + +.global tinygo_swapTask +tinygo_swapTask: + // This function gets the following parameters: + // x0 = newStack uintptr + // x1 = oldStack *uintptr + + // Save all callee-saved registers: + stp x19, x20, [sp, #-160]! + stp x21, x22, [sp, #16] + stp x23, x24, [sp, #32] + stp x25, x26, [sp, #48] + stp x27, x28, [sp, #64] + stp x29, x30, [sp, #80] + stp d8, d9, [sp, #96] + stp d10, d11, [sp, #112] + stp d12, d13, [sp, #128] + stp d14, d15, [sp, #144] + + // Save the current stack pointer in oldStack. + mov x8, sp + str x8, [x1] + + // Switch to the new stack pointer. + mov sp, x0 + + // Restore stack state and return. + ldp d14, d15, [sp, #144] + ldp d12, d13, [sp, #128] + ldp d10, d11, [sp, #112] + ldp d8, d9, [sp, #96] + ldp x29, x30, [sp, #80] + ldp x27, x28, [sp, #64] + ldp x25, x26, [sp, #48] + ldp x23, x24, [sp, #32] + ldp x21, x22, [sp, #16] + ldp x19, x20, [sp], #160 + ret diff --git a/src/internal/task/task_stack_arm64_windows.go b/src/internal/task/task_stack_arm64_windows.go new file mode 100644 index 000000000..c3c6e8f02 --- /dev/null +++ b/src/internal/task/task_stack_arm64_windows.go @@ -0,0 +1,72 @@ +//go:build scheduler.tasks && arm64 && windows + +package task + +import "unsafe" + +var systemStack uintptr + +// calleeSavedRegs is the list of registers that must be saved and restored +// when switching between tasks. Also see task_stack_arm64_windows.S that +// relies on the exact layout of this struct. +type calleeSavedRegs struct { + x19 uintptr + x20 uintptr + x21 uintptr + x22 uintptr + x23 uintptr + x24 uintptr + x25 uintptr + x26 uintptr + x27 uintptr + x28 uintptr + x29 uintptr + pc uintptr // aka x30 aka LR + + d8 uintptr + d9 uintptr + d10 uintptr + d11 uintptr + d12 uintptr + d13 uintptr + d14 uintptr + d15 uintptr +} + +// archInit runs architecture-specific setup for the goroutine startup. +func (s *state) archInit(r *calleeSavedRegs, fn uintptr, args unsafe.Pointer) { + // Store the initial sp for the startTask function (implemented in assembly). + s.sp = uintptr(unsafe.Pointer(r)) + + // Initialize the registers. + // These will be popped off of the stack on the first resume of the goroutine. + + // Start the function at tinygo_startTask (defined in src/internal/task/task_stack_arm64_windows.S). + // This assembly code calls a function (passed in x19) with a single argument + // (passed in x20). After the function returns, it calls Pause(). + r.pc = uintptr(unsafe.Pointer(&startTask)) + + // Pass the function to call in x19. + // This function is a compiler-generated wrapper which loads arguments out of a struct pointer. + // See createGoroutineStartWrapper (defined in compiler/goroutine.go) for more information. + r.x19 = fn + + // Pass the pointer to the arguments struct in x20. + r.x20 = uintptr(args) +} + +func (s *state) resume() { + swapTask(s.sp, &systemStack) +} + +func (s *state) pause() { + newStack := systemStack + systemStack = 0 + swapTask(newStack, &s.sp) +} + +// SystemStack returns the system stack pointer when called from a task stack. +// When called from the system stack, it returns 0. +func SystemStack() uintptr { + return systemStack +} diff --git a/src/runtime/asm_arm64_windows.S b/src/runtime/asm_arm64_windows.S new file mode 100644 index 000000000..243778997 --- /dev/null +++ b/src/runtime/asm_arm64_windows.S @@ -0,0 +1,36 @@ +.section .text.tinygo_scanCurrentStack,"ax" +.global tinygo_scanCurrentStack +tinygo_scanCurrentStack: + // Sources: + // * https://learn.microsoft.com/en-us/cpp/build/arm64-windows-abi-conventions?view=msvc-170 + // * https://godbolt.org/z/foc1xncvb + + // Save callee-saved registers. + stp x29, x30, [sp, #-160]! + stp x28, x27, [sp, #16] + stp x26, x25, [sp, #32] + stp x24, x23, [sp, #48] + stp x22, x21, [sp, #64] + stp x20, x19, [sp, #80] + stp d8, d9, [sp, #96] + stp d10, d11, [sp, #112] + stp d12, d13, [sp, #128] + stp d14, d15, [sp, #144] + + // Scan the stack. + mov x0, sp + bl tinygo_scanstack + + // Restore stack state and return. + ldp x29, x30, [sp], #160 + ret + + +.section .text.tinygo_longjmp,"ax" +.global tinygo_longjmp +tinygo_longjmp: + // Note: the code we jump to assumes x0 is set to a non-zero value if we + // jump from here (which is conveniently already the case). + ldp x1, x2, [x0] // jumpSP, jumpPC + mov sp, x1 + br x2 |