diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml index 5fbe79ddf..bda947432 100644 --- a/.github/workflows/build-macos.yml +++ b/.github/workflows/build-macos.yml @@ -92,7 +92,7 @@ jobs: - name: make gen-device run: make -j3 gen-device - name: Test TinyGo - run: make test GOTESTFLAGS="-short" + run: make test GOTESTFLAGS="-only-current-os" - name: Build TinyGo release tarball run: make release -j3 - name: Test stdlib packages diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 692790baf..a32117be4 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -105,7 +105,7 @@ jobs: run: make -j3 gen-device - name: Test TinyGo shell: bash - run: make test GOTESTFLAGS="-short" + run: make test GOTESTFLAGS="-only-current-os" - name: Build TinyGo release tarball shell: bash run: make build/release -j4 diff --git a/builder/builder_test.go b/builder/builder_test.go index ccccef30b..6b84b1007 100644 --- a/builder/builder_test.go +++ b/builder/builder_test.go @@ -67,6 +67,7 @@ func TestClangAttributes(t *testing.T) { {GOOS: "linux", GOARCH: "mipsle", GOMIPS: "softfloat"}, {GOOS: "darwin", GOARCH: "amd64"}, {GOOS: "darwin", GOARCH: "arm64"}, + {GOOS: "windows", GOARCH: "386"}, {GOOS: "windows", GOARCH: "amd64"}, {GOOS: "windows", GOARCH: "arm64"}, } { diff --git a/builder/mingw-w64.go b/builder/mingw-w64.go index 32cf58f53..6ea3560f7 100644 --- a/builder/mingw-w64.go +++ b/builder/mingw-w64.go @@ -93,6 +93,9 @@ func makeMinGWExtraLibs(tmpdir, goarch string) []*compileJob { defpath := inpath var archDef, emulation string switch goarch { + case "386": + archDef = "-DDEF_I386" + emulation = "i386pe" case "amd64": archDef = "-DDEF_X64" emulation = "i386pep" diff --git a/compileopts/target.go b/compileopts/target.go index 3c5fd6239..c3d57a583 100644 --- a/compileopts/target.go +++ b/compileopts/target.go @@ -433,14 +433,20 @@ func defaultTarget(options *Options) (*TargetSpec, error) { spec.Scheduler = "tasks" spec.Linker = "ld.lld" spec.Libc = "mingw-w64" - // Note: using a medium code model, low image base and no ASLR - // because Go doesn't really need those features. ASLR patches - // around issues for unsafe languages like C/C++ that are not - // 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 options.GOARCH { + case "386": + spec.LDFlags = append(spec.LDFlags, + "-m", "i386pe", + ) + // __udivdi3 is not present in ucrt it seems. + spec.RTLib = "compiler-rt" case "amd64": + // Note: using a medium code model, low image base and no ASLR + // because Go doesn't really need those features. ASLR patches + // around issues for unsafe languages like C/C++ that are not + // 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 spec.LDFlags = append(spec.LDFlags, "-m", "i386pep", "--image-base", "0x400000", diff --git a/compiler/calls.go b/compiler/calls.go index 6400e634b..a44ac38a8 100644 --- a/compiler/calls.go +++ b/compiler/calls.go @@ -76,7 +76,15 @@ func (b *builder) createCall(fnType llvm.Type, fn llvm.Value, args []llvm.Value, fragments := b.expandFormalParam(arg) expanded = append(expanded, fragments...) } - return b.CreateCall(fnType, fn, expanded, name) + call := b.CreateCall(fnType, fn, expanded, name) + if !fn.IsAFunction().IsNil() { + if cc := fn.FunctionCallConv(); cc != llvm.CCallConv { + // Set a different calling convention if needed. + // This is needed for GetModuleHandleExA on Windows, for example. + call.SetInstructionCallConv(cc) + } + } + return call } // createInvoke is like createCall but continues execution at the landing pad if diff --git a/compiler/llvm.go b/compiler/llvm.go index 139c5a1cd..de387b39c 100644 --- a/compiler/llvm.go +++ b/compiler/llvm.go @@ -452,6 +452,21 @@ func (b *builder) readStackPointer() llvm.Value { return b.CreateCall(stacksave.GlobalValueType(), stacksave, nil, "") } +// writeStackPointer emits a LLVM intrinsic call that updates the current stack +// pointer. +func (b *builder) writeStackPointer(sp llvm.Value) { + name := "llvm.stackrestore.p0" + if llvmutil.Version() < 18 { + name = "llvm.stackrestore" // backwards compatibility with LLVM 17 and below + } + stackrestore := b.mod.NamedFunction(name) + if stackrestore.IsNil() { + fnType := llvm.FunctionType(b.ctx.VoidType(), []llvm.Type{b.dataPtrType}, false) + stackrestore = llvm.AddFunction(b.mod, name, fnType) + } + b.CreateCall(stackrestore.GlobalValueType(), stackrestore, []llvm.Value{sp}, "") +} + // createZExtOrTrunc lets the input value fit in the output type bits, by zero // extending or truncating the integer. func (b *builder) createZExtOrTrunc(value llvm.Value, t llvm.Type) llvm.Value { diff --git a/compiler/symbol.go b/compiler/symbol.go index d3b8069a2..944f36f0f 100644 --- a/compiler/symbol.go +++ b/compiler/symbol.go @@ -208,6 +208,12 @@ func (c *compilerContext) getFunction(fn *ssa.Function) (llvm.Type, llvm.Value) // > circumstances, and should not be exposed to source languages. llvmutil.AppendToGlobal(c.mod, "llvm.compiler.used", llvmFn) } + case "GetModuleHandleExA", "GetProcAddress", "GetSystemInfo", "GetSystemTimeAsFileTime", "LoadLibraryExW", "QueryUnbiasedInterruptTime", "SetEnvironmentVariableA", "Sleep", "VirtualAlloc": + // On Windows we need to use a special calling convention for some + // external calls. + if c.GOOS == "windows" && c.GOARCH == "386" { + llvmFn.SetFunctionCallConv(llvm.X86StdcallCallConv) + } } // External/exported functions may not retain pointer values. diff --git a/compiler/syscall.go b/compiler/syscall.go index aa40ad1a5..7fd6e354c 100644 --- a/compiler/syscall.go +++ b/compiler/syscall.go @@ -268,6 +268,8 @@ func (b *builder) createSyscall(call *ssa.CallCommon) (llvm.Value, error) { // The signature looks like this: // func Syscall(trap, nargs, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno) + isI386 := strings.HasPrefix(b.Triple, "i386-") + // Prepare input values. var paramTypes []llvm.Type var params []llvm.Value @@ -285,11 +287,17 @@ func (b *builder) createSyscall(call *ssa.CallCommon) (llvm.Value, error) { if setLastError.IsNil() { llvmType := llvm.FunctionType(b.ctx.VoidType(), []llvm.Type{b.ctx.Int32Type()}, false) setLastError = llvm.AddFunction(b.mod, "SetLastError", llvmType) + if isI386 { + setLastError.SetFunctionCallConv(llvm.X86StdcallCallConv) + } } getLastError := b.mod.NamedFunction("GetLastError") if getLastError.IsNil() { llvmType := llvm.FunctionType(b.ctx.Int32Type(), nil, false) getLastError = llvm.AddFunction(b.mod, "GetLastError", llvmType) + if isI386 { + getLastError.SetFunctionCallConv(llvm.X86StdcallCallConv) + } } // Now do the actual call. Pseudocode: @@ -300,9 +308,24 @@ func (b *builder) createSyscall(call *ssa.CallCommon) (llvm.Value, error) { // Note that SetLastError/GetLastError could be replaced with direct // access to the thread control block, which is probably smaller and // faster. The Go runtime does this in assembly. - b.CreateCall(setLastError.GlobalValueType(), setLastError, []llvm.Value{llvm.ConstNull(b.ctx.Int32Type())}, "") + // On windows/386, we also need to save/restore the stack pointer. I'm + // not entirely sure why this is needed, but without it these calls + // change the stack pointer leading to a crash soon after. + setLastErrorCall := b.CreateCall(setLastError.GlobalValueType(), setLastError, []llvm.Value{llvm.ConstNull(b.ctx.Int32Type())}, "") + var sp llvm.Value + if isI386 { + setLastErrorCall.SetInstructionCallConv(llvm.X86StdcallCallConv) + sp = b.readStackPointer() + } syscallResult := b.CreateCall(llvmType, fnPtr, params, "") + if isI386 { + syscallResult.SetInstructionCallConv(llvm.X86StdcallCallConv) + b.writeStackPointer(sp) + } errResult := b.CreateCall(getLastError.GlobalValueType(), getLastError, nil, "err") + if isI386 { + errResult.SetInstructionCallConv(llvm.X86StdcallCallConv) + } if b.uintptrType != b.ctx.Int32Type() { errResult = b.CreateZExt(errResult, b.uintptrType, "err.uintptr") } diff --git a/main_test.go b/main_test.go index f193f4679..d950340b2 100644 --- a/main_test.go +++ b/main_test.go @@ -35,6 +35,8 @@ const TESTDATA = "testdata" var testTarget = flag.String("target", "", "override test target") +var testOnlyCurrentOS = flag.Bool("only-current-os", false, "") + var supportedLinuxArches = map[string]string{ "AMD64Linux": "linux/amd64", "X86Linux": "linux/386", @@ -158,20 +160,35 @@ func TestBuild(t *testing.T) { return } - t.Run("EmulatedCortexM3", func(t *testing.T) { - t.Parallel() - runPlatTests(optionsFromTarget("cortex-m-qemu", sema), tests, t) - }) + if !*testOnlyCurrentOS { + t.Run("EmulatedCortexM3", func(t *testing.T) { + t.Parallel() + runPlatTests(optionsFromTarget("cortex-m-qemu", sema), tests, t) + }) - t.Run("EmulatedRISCV", func(t *testing.T) { - t.Parallel() - runPlatTests(optionsFromTarget("riscv-qemu", sema), tests, t) - }) + t.Run("EmulatedRISCV", func(t *testing.T) { + t.Parallel() + runPlatTests(optionsFromTarget("riscv-qemu", sema), tests, t) + }) - t.Run("AVR", func(t *testing.T) { - t.Parallel() - runPlatTests(optionsFromTarget("simavr", sema), tests, t) - }) + t.Run("AVR", func(t *testing.T) { + t.Parallel() + runPlatTests(optionsFromTarget("simavr", sema), tests, t) + }) + + t.Run("WebAssembly", func(t *testing.T) { + t.Parallel() + runPlatTests(optionsFromTarget("wasm", sema), tests, t) + }) + t.Run("WASI", func(t *testing.T) { + t.Parallel() + runPlatTests(optionsFromTarget("wasip1", sema), tests, t) + }) + t.Run("WASIp2", func(t *testing.T) { + t.Parallel() + runPlatTests(optionsFromTarget("wasip2", sema), tests, t) + }) + } if runtime.GOOS == "linux" { for name, osArch := range supportedLinuxArches { @@ -191,18 +208,13 @@ func TestBuild(t *testing.T) { options := optionsFromOSARCH("linux/mipsle/softfloat", sema) runTest("cgo/", options, t, nil, nil) }) - t.Run("WebAssembly", func(t *testing.T) { - t.Parallel() - runPlatTests(optionsFromTarget("wasm", sema), tests, t) - }) - t.Run("WASI", func(t *testing.T) { - t.Parallel() - runPlatTests(optionsFromTarget("wasip1", sema), tests, t) - }) - t.Run("WASIp2", func(t *testing.T) { - t.Parallel() - runPlatTests(optionsFromTarget("wasip2", sema), tests, t) - }) + } else if runtime.GOOS == "windows" { + if runtime.GOARCH != "386" { + t.Run("Windows386", func(t *testing.T) { + t.Parallel() + runPlatTests(optionsFromOSARCH("windows/386", sema), tests, t) + }) + } } } diff --git a/src/internal/task/task_stack_386.S b/src/internal/task/task_stack_386.S index 402e9e50f..f92dae0bf 100644 --- a/src/internal/task/task_stack_386.S +++ b/src/internal/task/task_stack_386.S @@ -1,7 +1,12 @@ +#ifdef _WIN32 +.global _tinygo_startTask +_tinygo_startTask: +#else // Linux etc .section .text.tinygo_startTask .global tinygo_startTask .type tinygo_startTask, %function tinygo_startTask: +#endif .cfi_startproc // Small assembly stub for starting a goroutine. This is already run on the // new stack, with the callee-saved registers already loaded. @@ -24,12 +29,21 @@ tinygo_startTask: addl $4, %esp // After return, exit this goroutine. This is a tail call. + #ifdef _WIN32 + jmp _tinygo_task_exit + #else jmp tinygo_task_exit + #endif .cfi_endproc +#ifdef _WIN32 +.global _tinygo_swapTask +_tinygo_swapTask: +#else .global tinygo_swapTask .type tinygo_swapTask, %function tinygo_swapTask: +#endif // This function gets the following parameters: movl 4(%esp), %eax // newStack uintptr movl 8(%esp), %ecx // oldStack *uintptr diff --git a/src/runtime/asm_386.S b/src/runtime/asm_386.S index faaa7c3a3..f463ffa0a 100644 --- a/src/runtime/asm_386.S +++ b/src/runtime/asm_386.S @@ -1,7 +1,12 @@ +#ifdef _WIN32 +.global _tinygo_scanCurrentStack +_tinygo_scanCurrentStack: +#else .section .text.tinygo_scanCurrentStack .global tinygo_scanCurrentStack .type tinygo_scanCurrentStack, %function tinygo_scanCurrentStack: +#endif // Sources: // * https://stackoverflow.com/questions/18024672/what-registers-are-preserved-through-a-linux-x86-64-function-call // * https://godbolt.org/z/q7e8dn @@ -15,7 +20,11 @@ tinygo_scanCurrentStack: // Scan the stack. subl $8, %esp // adjust the stack before the call to maintain 16-byte alignment pushl %esp + #ifdef _WIN32 + calll _tinygo_scanstack + #else calll tinygo_scanstack + #endif // Restore the stack pointer. Registers do not need to be restored as they // were only pushed to be discoverable by the GC. @@ -23,9 +32,14 @@ tinygo_scanCurrentStack: retl +#ifdef _WIN32 +.global _tinygo_longjmp +_tinygo_longjmp: +#else .section .text.tinygo_longjmp .global tinygo_longjmp tinygo_longjmp: +#endif // Note: the code we jump to assumes eax is set to a non-zero value if we // jump from here. movl 4(%esp), %eax