From 5cba25fa30d95510ac6fd72a81152d1a014a9130 Mon Sep 17 00:00:00 2001 From: Evan Wies Date: Thu, 14 May 2026 22:38:31 -0400 Subject: [PATCH] compiler: implement syscall.Syscall* on darwin syscall.Syscall, syscall.Syscall6, syscall.RawSyscall and syscall.RawSyscall6 are declared bodyless on darwin in upstream Go and implemented in asm_darwin_{amd64,arm64}.s. TinyGo intentionally skipped them and never provided assembly, so any program calling them (directly or via modernc.org/memory and similar) failed to link: linker could not find symbol _syscall.Syscall Lower these calls in the compiler instead. Mirror upstream's ABI: - amd64: SYSCALL with trap + 0x2000000 in RAX, args in RDI/RSI/RDX/R10/R8/R9, primary in RAX, secondary in RDX, error indicated by the carry flag. Constraint: ={@ccc}. - arm64: SVC #0x80 with trap in X16, args in X0..X5, primary in X0, secondary in X1, error indicated by the carry flag (BCS). Constraint: ={@cccs}. LLVM 20 rejects an i1 result type from these flag-output constraints with "Glue output operand is of invalid type" on both backends, so the flag is received as i32 and truncated to i1 in IR. Clobber lists match the AArch64 caller-saved convention (x0..x7) and the SYSCALL clobbers on x86 (rcx, r11). The integration test exercises the three-arg form (SYS_GETPID happy path, SYS_CLOSE error path returning EBADF) and the six-arg form (anonymous SYS_MMAP, which uses all six argument registers and would fail if any register slot were swapped). Fixes #4794 --- compiler/compiler.go | 4 +- compiler/syscall.go | 119 ++++++++++++++++++++++++++++++++++++ main_test.go | 6 ++ testdata/syscall_darwin.go | 47 ++++++++++++++ testdata/syscall_darwin.txt | 3 + 5 files changed, 176 insertions(+), 3 deletions(-) create mode 100644 testdata/syscall_darwin.go create mode 100644 testdata/syscall_darwin.txt diff --git a/compiler/compiler.go b/compiler/compiler.go index 45e6c8a54b..7fd34f0b99 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -1981,9 +1981,7 @@ func (b *builder) createFunctionCall(instr *ssa.CallCommon) (llvm.Value, error) case strings.HasPrefix(name, "(device/riscv.CSR)."): return b.emitCSROperation(instr) case (strings.HasPrefix(name, "syscall.Syscall") || strings.HasPrefix(name, "syscall.RawSyscall") || strings.HasPrefix(name, "golang.org/x/sys/unix.Syscall") || strings.HasPrefix(name, "golang.org/x/sys/unix.RawSyscall")) && name != "syscall.SyscallN": - if b.GOOS != "darwin" { - return b.createSyscall(instr) - } + return b.createSyscall(instr) case name == "syscall.syscalln": if b.GOOS == "windows" { return b.createSyscalln(instr) diff --git a/compiler/syscall.go b/compiler/syscall.go index 946acdecb0..7ac36c6cbb 100644 --- a/compiler/syscall.go +++ b/compiler/syscall.go @@ -269,6 +269,24 @@ func (b *builder) createSyscall(call *ssa.CallCommon) (llvm.Value, error) { retval = b.CreateInsertValue(retval, zero, 1, "") retval = b.CreateInsertValue(retval, errResult, 2, "") return retval, nil + case "darwin": + r1, r2, errFlag, err := b.createDarwinRawSyscall(call) + if err != nil { + return llvm.Value{}, err + } + // Darwin returns (r1, r2, err) where err is the raw errno on + // failure (carry flag set) and r1=-1 in that case. On success, + // err=0 and r1/r2 carry the syscall return values. + zero := llvm.ConstInt(b.uintptrType, 0, false) + minusOne := llvm.ConstInt(b.uintptrType, ^uint64(0), false) // -1 as uintptr + finalR1 := b.CreateSelect(errFlag, minusOne, r1, "") + finalR2 := b.CreateSelect(errFlag, zero, r2, "") + finalErr := b.CreateSelect(errFlag, r1, zero, "syscallError") + retval := llvm.Undef(b.ctx.StructType([]llvm.Type{b.uintptrType, b.uintptrType, b.uintptrType}, false)) + retval = b.CreateInsertValue(retval, finalR1, 0, "") + retval = b.CreateInsertValue(retval, finalR2, 1, "") + retval = b.CreateInsertValue(retval, finalErr, 2, "") + return retval, nil case "windows": // On Windows, syscall.Syscall* is basically just a function pointer // call. This is complicated in gc because of stack switching and the @@ -546,3 +564,104 @@ func (b *builder) createDarwinFuncPCABI0Call(instr *ssa.CallCommon) llvm.Value { // abi.FuncPCABI0 returns). return b.CreatePtrToInt(llvmFn, b.uintptrType, "") } + +// createDarwinRawSyscall emits a raw kernel syscall for darwin and returns +// (r1, r2, errFlag). errFlag is an i1 derived from the carry flag. It is +// called only from createSyscall's darwin branch. +// +// References (upstream Go): +// +// src/syscall/asm_darwin_amd64.s +// src/syscall/asm_darwin_arm64.s +func (b *builder) createDarwinRawSyscall(call *ssa.CallCommon) (r1, r2, errFlag llvm.Value, err error) { + num := b.getValue(call.Args[0], getPos(call)) + switch b.GOARCH { + case "amd64": + // AMD64 darwin syscall ABI: + // - syscall number in RAX, ORed with 0x2000000 (BSD class) + // - args in RDI, RSI, RDX, R10, R8, R9 + // - SYSCALL instruction + // - primary return in RAX, secondary in RDX + // - carry flag set on error + args := []llvm.Value{ + b.CreateAdd(num, llvm.ConstInt(b.uintptrType, 0x2000000, false), ""), + } + argTypes := []llvm.Type{b.uintptrType} + constraints := "={rax},={rdx},={@ccc},0" + for i, arg := range call.Args[1:] { + constraints += "," + [...]string{ + "{rdi}", + "{rsi}", + "{rdx}", + "{r10}", + "{r8}", + "{r9}", + }[i] + llvmValue := b.getValue(arg, getPos(call)) + args = append(args, llvmValue) + argTypes = append(argTypes, llvmValue.Type()) + } + constraints += ",~{rcx},~{r11}" + // LLVM's x86 backend requires the flag-output (={@ccc}) to be at least + // i32; passing i1 directly triggers "Glue output operand is of invalid + // type" during instruction selection. Receive i32 and truncate. + retType := b.ctx.StructType([]llvm.Type{b.uintptrType, b.uintptrType, b.ctx.Int32Type()}, false) + fnType := llvm.FunctionType(retType, argTypes, false) + target := llvm.InlineAsm(fnType, "syscall", constraints, true, false, llvm.InlineAsmDialectIntel, false) + result := b.CreateCall(fnType, target, args, "") + r1 = b.CreateExtractValue(result, 0, "syscall.r1") + r2 = b.CreateExtractValue(result, 1, "syscall.r2") + errFlagWide := b.CreateExtractValue(result, 2, "syscall.errFlagWide") + errFlag = b.CreateTrunc(errFlagWide, b.ctx.Int1Type(), "syscall.errFlag") + return r1, r2, errFlag, nil + + case "arm64": + // ARM64 darwin syscall ABI: + // - syscall number in X16 + // - args in X0..X5 + // - SVC #0x80 + // - primary return in X0, secondary in X1 + // - carry flag set on error (BCS) + var args []llvm.Value + var argTypes []llvm.Type + constraints := "={x0},={x1},={@cccs}" + for i, arg := range call.Args[1:] { + constraints += "," + [...]string{ + "0", // tie to first output (X0) + "{x1}", + "{x2}", + "{x3}", + "{x4}", + "{x5}", + }[i] + llvmValue := b.getValue(arg, getPos(call)) + args = append(args, llvmValue) + argTypes = append(argTypes, llvmValue.Type()) + } + args = append(args, num) + argTypes = append(argTypes, b.uintptrType) + constraints += ",{x16}" // syscall number (also implicitly clobbered) + // Mark X0-X7 clobbered if not used as inputs. The kernel may + // clobber any of these per the AArch64 caller-saved convention. + // Unlike linux/arm64 (which uses x8 for the syscall number and + // has x16/x17 as scratch), darwin's ABI uses x16 directly, so + // no extra scratch-register clobbers are needed. + for i := len(call.Args) - 1; i < 8; i++ { + constraints += ",~{x" + strconv.Itoa(i) + "}" + } + // AArch64's flag-output constraint requires an i32 result, not i1. + // Truncate to i1 after extraction. + retType := b.ctx.StructType([]llvm.Type{b.uintptrType, b.uintptrType, b.ctx.Int32Type()}, false) + fnType := llvm.FunctionType(retType, argTypes, false) + target := llvm.InlineAsm(fnType, "svc #0x80", constraints, true, false, 0, false) + result := b.CreateCall(fnType, target, args, "") + r1 = b.CreateExtractValue(result, 0, "syscall.r1") + r2 = b.CreateExtractValue(result, 1, "syscall.r2") + errFlagWide := b.CreateExtractValue(result, 2, "syscall.errFlagWide") + errFlag = b.CreateTrunc(errFlagWide, b.ctx.Int1Type(), "syscall.errFlag") + return r1, r2, errFlag, nil + + default: + return llvm.Value{}, llvm.Value{}, llvm.Value{}, b.makeError(call.Pos(), "system calls are not supported on darwin/"+b.GOARCH) + } +} diff --git a/main_test.go b/main_test.go index 6ac0d0f596..788c1cde82 100644 --- a/main_test.go +++ b/main_test.go @@ -348,6 +348,12 @@ func runPlatTests(options compileopts.Options, tests []string, t *testing.T) { runTest("env.go", options, t, []string{"first", "second"}, []string{"ENV1=VALUE1", "ENV2=VALUE2"}) }) } + if options.GOOS == "darwin" { + t.Run("syscall_darwin.go", func(t *testing.T) { + t.Parallel() + runTest("syscall_darwin.go", options, t, nil, nil) + }) + } if isWebAssembly { t.Run("alias.go-scheduler-none", func(t *testing.T) { t.Parallel() diff --git a/testdata/syscall_darwin.go b/testdata/syscall_darwin.go new file mode 100644 index 0000000000..06e11a54fa --- /dev/null +++ b/testdata/syscall_darwin.go @@ -0,0 +1,47 @@ +//go:build darwin + +package main + +import ( + "fmt" + "os" + "syscall" +) + +func main() { + // Happy path: SYS_GETPID returns the current pid; no errno. + r1, _, errno := syscall.Syscall(syscall.SYS_GETPID, 0, 0, 0) + if errno != 0 { + fmt.Println("getpid errno:", errno) + return + } + if int(r1) != os.Getpid() { + fmt.Println("getpid mismatch:", r1, os.Getpid()) + return + } + fmt.Println("getpid ok") + + // Error path: close(99999) should return EBADF. + _, _, errno = syscall.Syscall(syscall.SYS_CLOSE, 99999, 0, 0) + if errno == syscall.EBADF { + fmt.Println("close ebadf ok") + } else { + fmt.Println("close errno unexpected:", errno) + } + + // Syscall6 path: anonymous mmap exercises all 6 argument + // registers (RDI/RSI/RDX/R10/R8/R9 on amd64; X0..X5 on arm64). + // A swap of any register slot would make the kernel reject the + // call, so successful return is meaningful coverage. + const ( + PROT_READ_WRITE = 0x3 // PROT_READ | PROT_WRITE + MAP_PRIVATE = 0x0002 + MAP_ANON = 0x1000 + ) + addr, _, errno := syscall.Syscall6(syscall.SYS_MMAP, 0, 4096, PROT_READ_WRITE, MAP_PRIVATE|MAP_ANON, ^uintptr(0), 0) + if errno != 0 || addr == 0 { + fmt.Println("mmap6 failed:", errno, "addr=", addr) + return + } + fmt.Println("mmap6 ok") +} diff --git a/testdata/syscall_darwin.txt b/testdata/syscall_darwin.txt new file mode 100644 index 0000000000..bc34881680 --- /dev/null +++ b/testdata/syscall_darwin.txt @@ -0,0 +1,3 @@ +getpid ok +close ebadf ok +mmap6 ok