From 17400ccd7b0d6df23684cfb5fab73bda8112bd13 Mon Sep 17 00:00:00 2001
From: Ayke van Laethem <aykevanlaethem@gmail.com>
Date: Fri, 22 Nov 2024 15:17:37 +0100
Subject: [PATCH 1/2] windows: add windows/386 support

---
 .github/workflows/build-macos.yml  |  2 +-
 .github/workflows/windows.yml      |  2 +-
 builder/builder_test.go            |  1 +
 builder/mingw-w64.go               |  3 ++
 compileopts/target.go              | 18 ++++++---
 compiler/calls.go                  | 10 ++++-
 compiler/llvm.go                   | 15 ++++++++
 compiler/symbol.go                 |  6 +++
 compiler/syscall.go                | 25 ++++++++++++-
 main_test.go                       | 60 ++++++++++++++++++------------
 src/internal/task/task_stack_386.S | 14 +++++++
 src/runtime/asm_386.S              | 14 +++++++
 12 files changed, 136 insertions(+), 34 deletions(-)

diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml
index 46ec2cc7c9..20c5cdb5ed 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 692790bafe..a32117be45 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 ccccef30ba..6b84b10070 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 32cf58f531..6ea3560f73 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 3c5fd62390..c3d57a583a 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 6400e634bd..a44ac38a87 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 139c5a1cd8..de387b39c0 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 d3b8069a2a..944f36f0f6 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 aa40ad1a55..7fd6e354c6 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 f193f46799..d950340b24 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 402e9e50f0..f92dae0bf3 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 faaa7c3a3b..f463ffa0a0 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

From 13d6eeb68efc18233ac7064da18e4eace5dfcb6a Mon Sep 17 00:00:00 2001
From: Ayke van Laethem <aykevanlaethem@gmail.com>
Date: Mon, 7 Apr 2025 15:09:32 +0200
Subject: [PATCH 2/2] windows: use MSVCRT.DLL instead of UCRT on i386

This allows the binaries to run on Windows XP, without needing any extra
DLLs. Tested in an x86 Windows XP SP3 virtual machine.
---
 GNUmakefile                     |  12 +++-
 builder/builtins.go             |  10 +++
 builder/mingw-w64.go            | 115 +++++++++++++++++++++++---------
 compileopts/config.go           |  16 ++++-
 compileopts/target.go           |   2 +
 compiler/symbol.go              |   2 +-
 src/crypto/rand/rand_windows.go |  39 +++++------
 src/runtime/runtime_windows.go  |  75 ++++++++++++++++-----
 8 files changed, 196 insertions(+), 75 deletions(-)

diff --git a/GNUmakefile b/GNUmakefile
index 58568d3eca..a685c08487 100644
--- a/GNUmakefile
+++ b/GNUmakefile
@@ -940,8 +940,9 @@ build/release: tinygo gen-device $(if $(filter 1,$(USE_SYSTEM_BINARYEN)),,binary
 	@mkdir -p build/release/tinygo/lib/clang/include
 	@mkdir -p build/release/tinygo/lib/CMSIS/CMSIS
 	@mkdir -p build/release/tinygo/lib/macos-minimal-sdk
+	@mkdir -p build/release/tinygo/lib/mingw-w64/mingw-w64-crt/crt
+	@mkdir -p build/release/tinygo/lib/mingw-w64/mingw-w64-crt/math
 	@mkdir -p build/release/tinygo/lib/mingw-w64/mingw-w64-crt/lib-common
-	@mkdir -p build/release/tinygo/lib/mingw-w64/mingw-w64-crt/stdio
 	@mkdir -p build/release/tinygo/lib/mingw-w64/mingw-w64-headers/defaults
 	@mkdir -p build/release/tinygo/lib/musl/arch
 	@mkdir -p build/release/tinygo/lib/musl/crt
@@ -997,10 +998,17 @@ endif
 	@cp -rp lib/musl/src/time            build/release/tinygo/lib/musl/src
 	@cp -rp lib/musl/src/unistd          build/release/tinygo/lib/musl/src
 	@cp -rp lib/musl/src/process         build/release/tinygo/lib/musl/src
+	@cp -rp lib/mingw-w64/mingw-w64-crt/crt/pseudo-reloc.c          build/release/tinygo/lib/mingw-w64/mingw-w64-crt/crt
 	@cp -rp lib/mingw-w64/mingw-w64-crt/def-include                 build/release/tinygo/lib/mingw-w64/mingw-w64-crt
+	@cp -rp lib/mingw-w64/mingw-w64-crt/gdtoa                       build/release/tinygo/lib/mingw-w64/mingw-w64-crt
+	@cp -rp lib/mingw-w64/mingw-w64-crt/include                     build/release/tinygo/lib/mingw-w64/mingw-w64-crt
 	@cp -rp lib/mingw-w64/mingw-w64-crt/lib-common/api-ms-win-crt-* build/release/tinygo/lib/mingw-w64/mingw-w64-crt/lib-common
+	@cp -rp lib/mingw-w64/mingw-w64-crt/lib-common/advapi32.def.in  build/release/tinygo/lib/mingw-w64/mingw-w64-crt/lib-common
 	@cp -rp lib/mingw-w64/mingw-w64-crt/lib-common/kernel32.def.in  build/release/tinygo/lib/mingw-w64/mingw-w64-crt/lib-common
-	@cp -rp lib/mingw-w64/mingw-w64-crt/stdio/ucrt_*                build/release/tinygo/lib/mingw-w64/mingw-w64-crt/stdio
+	@cp -rp lib/mingw-w64/mingw-w64-crt/lib-common/msvcrt.def.in    build/release/tinygo/lib/mingw-w64/mingw-w64-crt/lib-common
+	@cp -rp lib/mingw-w64/mingw-w64-crt/math/x86                    build/release/tinygo/lib/mingw-w64/mingw-w64-crt/math
+	@cp -rp lib/mingw-w64/mingw-w64-crt/misc                        build/release/tinygo/lib/mingw-w64/mingw-w64-crt
+	@cp -rp lib/mingw-w64/mingw-w64-crt/stdio                       build/release/tinygo/lib/mingw-w64/mingw-w64-crt
 	@cp -rp lib/mingw-w64/mingw-w64-headers/crt/                    build/release/tinygo/lib/mingw-w64/mingw-w64-headers
 	@cp -rp lib/mingw-w64/mingw-w64-headers/defaults/include        build/release/tinygo/lib/mingw-w64/mingw-w64-headers/defaults
 	@cp -rp lib/mingw-w64/mingw-w64-headers/include                 build/release/tinygo/lib/mingw-w64/mingw-w64-headers
diff --git a/builder/builtins.go b/builder/builtins.go
index b493b6680a..5e91c78915 100644
--- a/builder/builtins.go
+++ b/builder/builtins.go
@@ -3,6 +3,7 @@ package builder
 import (
 	"os"
 	"path/filepath"
+	"strings"
 
 	"github.com/tinygo-org/tinygo/compileopts"
 	"github.com/tinygo-org/tinygo/goenv"
@@ -201,6 +202,11 @@ var avrBuiltins = []string{
 	"avr/udivmodqi4.S",
 }
 
+// Builtins needed specifically for windows/386.
+var windowsI386Builtins = []string{
+	"i386/chkstk.S", // also _alloca
+}
+
 // libCompilerRT is a library with symbols required by programs compiled with
 // LLVM. These symbols are for operations that cannot be emitted with a single
 // instruction or a short sequence of instructions for that target.
@@ -229,6 +235,10 @@ var libCompilerRT = Library{
 			builtins = append(builtins, avrBuiltins...)
 		case "x86_64", "aarch64", "riscv64": // any 64-bit arch
 			builtins = append(builtins, genericBuiltins128...)
+		case "i386":
+			if strings.Split(target, "-")[2] == "windows" {
+				builtins = append(builtins, windowsI386Builtins...)
+			}
 		}
 		return builtins, nil
 	},
diff --git a/builder/mingw-w64.go b/builder/mingw-w64.go
index 6ea3560f73..6bbced8211 100644
--- a/builder/mingw-w64.go
+++ b/builder/mingw-w64.go
@@ -30,25 +30,62 @@ var libMinGW = Library{
 	sourceDir: func() string { return filepath.Join(goenv.Get("TINYGOROOT"), "lib/mingw-w64") },
 	cflags: func(target, headerPath string) []string {
 		mingwDir := filepath.Join(goenv.Get("TINYGOROOT"), "lib/mingw-w64")
-		return []string{
+		flags := []string{
 			"-nostdlibinc",
+			"-isystem", mingwDir + "/mingw-w64-crt/include",
 			"-isystem", mingwDir + "/mingw-w64-headers/crt",
+			"-isystem", mingwDir + "/mingw-w64-headers/include",
 			"-I", mingwDir + "/mingw-w64-headers/defaults/include",
 			"-I" + headerPath,
 		}
+		if strings.Split(target, "-")[0] == "i386" {
+			flags = append(flags,
+				"-D__MSVCRT_VERSION__=0x700", // Microsoft Visual C++ .NET 2002
+				"-D_WIN32_WINNT=0x0501",      // target Windows XP
+				"-D_CRTBLD",
+				"-Wno-pragma-pack",
+			)
+		}
+		return flags
 	},
 	librarySources: func(target string) ([]string, error) {
 		// These files are needed so that printf and the like are supported.
-		sources := []string{
-			"mingw-w64-crt/stdio/ucrt_fprintf.c",
-			"mingw-w64-crt/stdio/ucrt_fwprintf.c",
-			"mingw-w64-crt/stdio/ucrt_printf.c",
-			"mingw-w64-crt/stdio/ucrt_snprintf.c",
-			"mingw-w64-crt/stdio/ucrt_sprintf.c",
-			"mingw-w64-crt/stdio/ucrt_vfprintf.c",
-			"mingw-w64-crt/stdio/ucrt_vprintf.c",
-			"mingw-w64-crt/stdio/ucrt_vsnprintf.c",
-			"mingw-w64-crt/stdio/ucrt_vsprintf.c",
+		var sources []string
+		if strings.Split(target, "-")[0] == "i386" {
+			// Old 32-bit x86 systems use msvcrt.dll.
+			sources = []string{
+				"mingw-w64-crt/crt/pseudo-reloc.c",
+				"mingw-w64-crt/gdtoa/dmisc.c",
+				"mingw-w64-crt/gdtoa/gdtoa.c",
+				"mingw-w64-crt/gdtoa/gmisc.c",
+				"mingw-w64-crt/gdtoa/misc.c",
+				"mingw-w64-crt/math/x86/exp2.S",
+				"mingw-w64-crt/math/x86/trunc.S",
+				"mingw-w64-crt/misc/___mb_cur_max_func.c",
+				"mingw-w64-crt/misc/lc_locale_func.c",
+				"mingw-w64-crt/misc/mbrtowc.c",
+				"mingw-w64-crt/misc/strnlen.c",
+				"mingw-w64-crt/misc/wcrtomb.c",
+				"mingw-w64-crt/misc/wcsnlen.c",
+				"mingw-w64-crt/stdio/acrt_iob_func.c",
+				"mingw-w64-crt/stdio/mingw_lock.c",
+				"mingw-w64-crt/stdio/mingw_pformat.c",
+				"mingw-w64-crt/stdio/mingw_vfprintf.c",
+				"mingw-w64-crt/stdio/mingw_vsnprintf.c",
+			}
+		} else {
+			// Anything somewhat modern (amd64, arm64) uses UCRT.
+			sources = []string{
+				"mingw-w64-crt/stdio/ucrt_fprintf.c",
+				"mingw-w64-crt/stdio/ucrt_fwprintf.c",
+				"mingw-w64-crt/stdio/ucrt_printf.c",
+				"mingw-w64-crt/stdio/ucrt_snprintf.c",
+				"mingw-w64-crt/stdio/ucrt_sprintf.c",
+				"mingw-w64-crt/stdio/ucrt_vfprintf.c",
+				"mingw-w64-crt/stdio/ucrt_vprintf.c",
+				"mingw-w64-crt/stdio/ucrt_vsnprintf.c",
+				"mingw-w64-crt/stdio/ucrt_vsprintf.c",
+			}
 		}
 		return sources, nil
 	},
@@ -63,27 +100,41 @@ var libMinGW = Library{
 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
-	// .lib file. But to simplify things, we're going to leave them as separate
-	// files.
-	for _, name := range []string{
-		"kernel32.def.in",
-		"api-ms-win-crt-conio-l1-1-0.def",
-		"api-ms-win-crt-convert-l1-1-0.def.in",
-		"api-ms-win-crt-environment-l1-1-0.def",
-		"api-ms-win-crt-filesystem-l1-1-0.def",
-		"api-ms-win-crt-heap-l1-1-0.def",
-		"api-ms-win-crt-locale-l1-1-0.def",
-		"api-ms-win-crt-math-l1-1-0.def.in",
-		"api-ms-win-crt-multibyte-l1-1-0.def",
-		"api-ms-win-crt-private-l1-1-0.def.in",
-		"api-ms-win-crt-process-l1-1-0.def",
-		"api-ms-win-crt-runtime-l1-1-0.def.in",
-		"api-ms-win-crt-stdio-l1-1-0.def",
-		"api-ms-win-crt-string-l1-1-0.def",
-		"api-ms-win-crt-time-l1-1-0.def",
-		"api-ms-win-crt-utility-l1-1-0.def",
-	} {
+	var libs []string
+	if goarch == "386" {
+		libs = []string{
+			// x86 uses msvcrt.dll instead of UCRT for compatibility with old
+			// Windows versions.
+			"advapi32.def.in",
+			"kernel32.def.in",
+			"msvcrt.def.in",
+		}
+	} else {
+		// Use the modernized UCRT on new systems.
+		// Normally all the api-ms-win-crt-*.def files are all compiled to a
+		// single .lib file. But to simplify things, we're going to leave them
+		// as separate files.
+		libs = []string{
+			"advapi32.def.in",
+			"kernel32.def.in",
+			"api-ms-win-crt-conio-l1-1-0.def",
+			"api-ms-win-crt-convert-l1-1-0.def.in",
+			"api-ms-win-crt-environment-l1-1-0.def",
+			"api-ms-win-crt-filesystem-l1-1-0.def",
+			"api-ms-win-crt-heap-l1-1-0.def",
+			"api-ms-win-crt-locale-l1-1-0.def",
+			"api-ms-win-crt-math-l1-1-0.def.in",
+			"api-ms-win-crt-multibyte-l1-1-0.def",
+			"api-ms-win-crt-private-l1-1-0.def.in",
+			"api-ms-win-crt-process-l1-1-0.def",
+			"api-ms-win-crt-runtime-l1-1-0.def.in",
+			"api-ms-win-crt-stdio-l1-1-0.def",
+			"api-ms-win-crt-string-l1-1-0.def",
+			"api-ms-win-crt-time-l1-1-0.def",
+			"api-ms-win-crt-utility-l1-1-0.def",
+		}
+	}
+	for _, name := range libs {
 		outpath := filepath.Join(tmpdir, filepath.Base(name)+".lib")
 		inpath := filepath.Join(root, "lib/mingw-w64/mingw-w64-crt/lib-common/"+name)
 		job := &compileJob{
diff --git a/compileopts/config.go b/compileopts/config.go
index 61efa439fe..d05111f2b0 100644
--- a/compileopts/config.go
+++ b/compileopts/config.go
@@ -398,15 +398,25 @@ func (c *Config) LibcCFlags() []string {
 	case "mingw-w64":
 		root := goenv.Get("TINYGOROOT")
 		path := c.LibraryPath("mingw-w64")
-		return []string{
+		cflags := []string{
 			"-nostdlibinc",
 			"-isystem", filepath.Join(path, "include"),
 			"-isystem", filepath.Join(root, "lib", "mingw-w64", "mingw-w64-headers", "crt"),
 			"-isystem", filepath.Join(root, "lib", "mingw-w64", "mingw-w64-headers", "include"),
 			"-isystem", filepath.Join(root, "lib", "mingw-w64", "mingw-w64-headers", "defaults", "include"),
-			"-D_UCRT",
-			"-D_WIN32_WINNT=0x0a00", // target Windows 10
 		}
+		if c.GOARCH() == "386" {
+			cflags = append(cflags,
+				"-D__MSVCRT_VERSION__=0x700", // Microsoft Visual C++ .NET 2002
+				"-D_WIN32_WINNT=0x0501",      // target Windows XP
+			)
+		} else {
+			cflags = append(cflags,
+				"-D_UCRT",
+				"-D_WIN32_WINNT=0x0a00", // target Windows 10
+			)
+		}
+		return cflags
 	case "":
 		// No libc specified, nothing to add.
 		return nil
diff --git a/compileopts/target.go b/compileopts/target.go
index c3d57a583a..7fb3906d38 100644
--- a/compileopts/target.go
+++ b/compileopts/target.go
@@ -437,6 +437,8 @@ func defaultTarget(options *Options) (*TargetSpec, error) {
 		case "386":
 			spec.LDFlags = append(spec.LDFlags,
 				"-m", "i386pe",
+				"--major-os-version", "4",
+				"--major-subsystem-version", "4",
 			)
 			// __udivdi3 is not present in ucrt it seems.
 			spec.RTLib = "compiler-rt"
diff --git a/compiler/symbol.go b/compiler/symbol.go
index 944f36f0f6..c56e0792f2 100644
--- a/compiler/symbol.go
+++ b/compiler/symbol.go
@@ -208,7 +208,7 @@ 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":
+	case "GetModuleHandleExA", "GetProcAddress", "GetSystemInfo", "GetSystemTimeAsFileTime", "LoadLibraryExW", "QueryPerformanceCounter", "QueryPerformanceFrequency", "QueryUnbiasedInterruptTime", "SetEnvironmentVariableA", "Sleep", "SystemFunction036", "VirtualAlloc":
 		// On Windows we need to use a special calling convention for some
 		// external calls.
 		if c.GOOS == "windows" && c.GOARCH == "386" {
diff --git a/src/crypto/rand/rand_windows.go b/src/crypto/rand/rand_windows.go
index 16266198fe..d8c9173c1e 100644
--- a/src/crypto/rand/rand_windows.go
+++ b/src/crypto/rand/rand_windows.go
@@ -1,6 +1,9 @@
 package rand
 
-import "errors"
+import (
+	"errors"
+	"unsafe"
+)
 
 func init() {
 	Reader = &reader{}
@@ -16,28 +19,22 @@ func (r *reader) Read(b []byte) (n int, err error) {
 		return
 	}
 
-	var randomByte uint32
-	for i := range b {
-		// Call rand_s every four bytes because it's a C int (always 32-bit in
-		// Windows).
-		if i%4 == 0 {
-			errCode := libc_rand_s(&randomByte)
-			if errCode != 0 {
-				// According to the documentation, it can return an error.
-				return n, errRandom
-			}
-		} else {
-			randomByte >>= 8
-		}
-		b[i] = byte(randomByte)
+	// Use the old RtlGenRandom, introduced in Windows XP.
+	// Even though the documentation says it is deprecated, it is widely used
+	// and probably won't go away anytime soon.
+	// See for example: https://github.com/golang/go/issues/33542
+	// For Windows 7 and newer, we might switch to ProcessPrng in the future
+	// (which is a documented function and might be a tiny bit faster).
+	ok := libc_RtlGenRandom(unsafe.Pointer(&b[0]), len(b))
+	if !ok {
+		return 0, errRandom
 	}
-
 	return len(b), nil
 }
 
-// Cryptographically secure random number generator.
-// https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/rand-s?view=msvc-170
-// errno_t rand_s(unsigned int* randomValue);
+// This function is part of advapi32.dll, and is called SystemFunction036 for
+// some reason. It's available on Windows XP and newer.
+// See: https://learn.microsoft.com/en-us/windows/win32/api/ntsecapi/nf-ntsecapi-rtlgenrandom
 //
-//export rand_s
-func libc_rand_s(randomValue *uint32) int32
+//export SystemFunction036
+func libc_RtlGenRandom(buf unsafe.Pointer, len int) bool
diff --git a/src/runtime/runtime_windows.go b/src/runtime/runtime_windows.go
index 17ee2ce721..c4df8dec36 100644
--- a/src/runtime/runtime_windows.go
+++ b/src/runtime/runtime_windows.go
@@ -42,10 +42,24 @@ func __p___argc() *int32
 //export __p___argv
 func __p___argv() **unsafe.Pointer
 
+type startupInfo struct {
+	newMode int32
+}
+
+//export __getmainargs
+func __getmainargs(argc *int32, argv, env **unsafe.Pointer, doWildcard int, startInfo *startupInfo) int32
+
+var performanceFrequency int64
+
 //export mainCRTStartup
 func mainCRTStartup() int {
 	preinit()
 
+	// Obtain the (constant) performance frequency when needed.
+	if GOARCH == "386" {
+		_QueryPerformanceFrequency(&performanceFrequency)
+	}
+
 	// Obtain the initial stack pointer right before calling the run() function.
 	// The run function has been moved to a separate (non-inlined) function so
 	// that the correct stack pointer is read.
@@ -78,9 +92,19 @@ var args []string
 func os_runtime_args() []string {
 	if args == nil {
 		// Obtain argc/argv from the environment.
-		_configure_narrow_argv(2)
-		argc := *__p___argc()
-		argv := *__p___argv()
+		var argc int32
+		var argv *unsafe.Pointer
+		if GOARCH == "386" {
+			// MSVCRT.DLL
+			var env *unsafe.Pointer
+			startInfo := startupInfo{newMode: 0}
+			__getmainargs(&argc, &argv, &env, 1, &startInfo)
+		} else {
+			// UCRT
+			_configure_narrow_argv(2)
+			argc = *__p___argc()
+			argv = *__p___argv()
+		}
 
 		// Make args slice big enough so that it can store all command line
 		// arguments.
@@ -146,10 +170,30 @@ func sleepTicks(d timeUnit) {
 	}
 }
 
+//export QueryPerformanceFrequency
+func _QueryPerformanceFrequency(*int64) bool
+
+//export QueryPerformanceCounter
+func _QueryPerformanceCounter(*int64) bool
+
 func ticks() timeUnit {
-	var unbiasedTime uint64
-	_QueryUnbiasedInterruptTime(&unbiasedTime)
-	return timeUnit(unbiasedTime)
+	if GOARCH == "386" {
+		// Unfortunately QueryUnbiasedInterruptTime is only available starting
+		// with Windows 7.
+
+		// Obtain counter (that runs at a fixed frequency).
+		var counter int64
+		_QueryPerformanceCounter(&counter)
+		// Convert this counter to ticks of 100ns (just like
+		// QueryUnbiasedInterruptTime).
+		// (We could also change the definition of ticks on GOOS=386 but that
+		// seems messy).
+		return timeUnit((counter * 10000000) / performanceFrequency)
+	} else {
+		var unbiasedTime uint64
+		_QueryUnbiasedInterruptTime(&unbiasedTime)
+		return timeUnit(unbiasedTime)
+	}
 }
 
 //go:linkname now time.now
@@ -237,17 +281,16 @@ func procUnpin() {
 }
 
 func hardwareRand() (n uint64, ok bool) {
-	var n1, n2 uint32
-	errCode1 := libc_rand_s(&n1)
-	errCode2 := libc_rand_s(&n2)
-	n = uint64(n1)<<32 | uint64(n2)
-	ok = errCode1 == 0 && errCode2 == 0
+	// Use the old RtlGenRandom, introduced in Windows XP.
+	// See the rationale in src/crypto/rand/rand_windows.go for why we use this
+	// one.
+	ok = _RtlGenRandom(unsafe.Pointer(&n), 8)
 	return
 }
 
-// Cryptographically secure random number generator.
-// https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/rand-s?view=msvc-170
-// errno_t rand_s(unsigned int* randomValue);
+// This function is part of advapi32.dll, and is called SystemFunction036 for
+// some reason. It's available on Windows XP and newer.
+// See: https://learn.microsoft.com/en-us/windows/win32/api/ntsecapi/nf-ntsecapi-rtlgenrandom
 //
-//export rand_s
-func libc_rand_s(randomValue *uint32) int32
+//export SystemFunction036
+func _RtlGenRandom(buf unsafe.Pointer, len int) bool