From f548385fd648d90d5774b769849dc229d6b32b72 Mon Sep 17 00:00:00 2001 From: Vladimir Stolyarov Date: Mon, 27 Jul 2026 19:32:06 +0400 Subject: [PATCH] purego: properly handle 64-bit integers on 32-bit arm 1. Obey 8-byte alignment on stack and even-register placement required by AAPCS. 2. Use two registers (R0, R1) to make function return value. This will work for '386' architecture too. 3. Add test cases. Co-authored-by: Hirador <63920290+Hirador@users.noreply.github.com> --- func.go | 88 ++++++++++++++++++++++++++++++++---- func_test.go | 62 ++++++++++++++++++++++++- sys_unix_arm.s | 2 + syscall_notstackargs.go | 14 ++++++ syscall_stackargs_ppc64le.go | 8 ++++ syscall_stackargs_s390x.go | 8 ++++ syscall_unix.go | 23 ++++++++-- testdata/abitest/abi_test.c | 21 +++++++++ 8 files changed, 210 insertions(+), 16 deletions(-) diff --git a/func.go b/func.go index 6ddf3ce3..9bdb474f 100644 --- a/func.go +++ b/func.go @@ -144,6 +144,7 @@ func RegisterFunc(fptr any, cfn uintptr) { var ints int var floats int floatArgRegs := numOfFloatRegisters() + ptrSize := unsafe.Sizeof(uintptr(0)) var stack int for i := 0; i < ty.NumIn(); i++ { arg := ty.In(i) @@ -164,16 +165,25 @@ func RegisterFunc(fptr any, cfn uintptr) { case reflect.String, reflect.Uintptr, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Pointer, reflect.UnsafePointer, reflect.Slice, reflect.Bool: + usesSlots := max(1, int(arg.Size()/ptrSize)) + if isARMPaddingNeeded(arg, ints, stack) { + usesSlots++ + } + if ints < numOfIntegerRegisters() { - ints++ + ints += usesSlots } else { - stack++ + stack += usesSlots } case reflect.Float32, reflect.Float64: + usesSlots := max(1, int(arg.Size()/ptrSize)) + if isARMFloatPaddingNeeded(arg, floats, stack) { + usesSlots++ + } if floats < floatArgRegs { floats++ } else { - stack++ + stack += usesSlots } case reflect.Struct: ensureStructSupported() @@ -347,9 +357,22 @@ func RegisterFunc(fptr any, cfn uintptr) { outType := ty.Out(0) v := reflect.New(outType).Elem() switch outType.Kind() { - case reflect.Uintptr, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + case reflect.Uint64: + if is32bit { + // high-word is recorded at a2 for 32-bit platforms and 64-bit returns + v.SetUint(uint64(syscall.a1) | (uint64(syscall.a2) << 32)) + } else { + v.SetUint(uint64(syscall.a1)) + } + case reflect.Uintptr, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32: v.SetUint(uint64(syscall.a1)) - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + case reflect.Int64: + if is32bit { + v.SetInt(int64(syscall.a1) | (int64(syscall.a2) << 32)) + } else { + v.SetInt(int64(syscall.a1)) + } + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32: v.SetInt(int64(syscall.a1)) case reflect.Bool: v.SetBool(byte(syscall.a1) != 0) @@ -415,9 +438,25 @@ func addValue(v reflect.Value, keepAlive []any, addInt func(x uintptr), addFloat ptr := strings.CString(v.String()) keepAlive = append(keepAlive, ptr) addInt(uintptr(unsafe.Pointer(ptr))) - case reflect.Uintptr, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + case reflect.Uint64: + if isARMPaddingNeeded(v.Type(), *numInts, *numStack) { + addInt(0) + } + addInt(uintptr(v.Uint())) + if is32bit { + addInt(uintptr(v.Uint() >> 32)) // on 32bit we must add high word too + } + case reflect.Uintptr, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32: addInt(uintptr(v.Uint())) - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + case reflect.Int64: + if isARMPaddingNeeded(v.Type(), *numInts, *numStack) { + addInt(0) + } + addInt(uintptr(v.Int())) + if is32bit { + addInt(uintptr(v.Int() >> 32)) + } + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32: addInt(uintptr(v.Int())) case reflect.Pointer, reflect.UnsafePointer, reflect.Slice: // There is no need to keepAlive this pointer separately because it is kept alive in the args variable @@ -442,12 +481,16 @@ func addValue(v reflect.Value, keepAlive []any, addInt func(x uintptr), addFloat addFloat(uintptr(math.Float32bits(float32(v.Float())))) } case reflect.Float64: + bits := math.Float64bits(v.Float()) + if isARMFloatPaddingNeeded(v.Type(), *numFloats, *numStack) { + // if floats are spilled onto stack on ARM than we must follow AAPCS C.7 + addFloat(0) + } if is32bit { - bits := math.Float64bits(v.Float()) addFloat(uintptr(bits)) addFloat(uintptr(bits >> 32)) } else { - addFloat(uintptr(math.Float64bits(v.Float()))) + addFloat(uintptr(bits)) } case reflect.Struct: keepAlive = addStruct(v, numInts, numFloats, numStack, addInt, addFloat, addStack, keepAlive) @@ -603,7 +646,6 @@ func estimateStackBytes(ty reflect.Type) int { } else if !usesInt && numFloats < numOfFloatRegisters() { numFloats++ } else { - // Goes to stack - accumulate total bytes stackBytes += size } } @@ -613,3 +655,29 @@ func estimateStackBytes(ty reflect.Type) int { } return stackBytes } + +func isARMPaddingNeeded(ty reflect.Type, numInts, numStack int) bool { + // ARM EABI (AAPCS): 8-byte-aligned types (int64/uint64) start on an + // even core register (C.3); if they then spill, the stack slot is + // 8-byte aligned too (C.7). + // https://github.com/ARM-software/abi-aa/blob/main/aapcs32/aapcs32.rst#6111handling-values-larger-than-32-bits + if runtime.GOARCH != "arm" || ty.Size() != 8 { + return false + } + if numInts >= 0 && numInts < numOfIntegerRegisters() { + return numInts%2 != 0 + } + return numStack%2 != 0 +} + +func isARMFloatPaddingNeeded(ty reflect.Type, numFloats, numStack int) bool { + if runtime.GOARCH != "arm" || ty.Size() != 8 { + return false + } + if numFloats >= 0 && numFloats < numOfFloatRegisters() { + // float registers are 64bit so alignment never needed for args in registers + return false + } + // Only check if AAPCS C.7 is applicable here + return numStack%2 != 0 +} diff --git a/func_test.go b/func_test.go index 2c0cae4d..9b305ade 100644 --- a/func_test.go +++ b/func_test.go @@ -7,11 +7,13 @@ import ( "bytes" "errors" "fmt" + "math" "os" "os/exec" "path/filepath" "reflect" "runtime" + "strconv" "strings" "sync" "testing" @@ -402,6 +404,63 @@ func TestABI_ArgumentPassing(t *testing.T) { }, want: "1:2:3:4:5:6:7:8:9:10.0", }, + { + // check if unaligned 64bit argument and 64bit returned value is properly passed via registers + // arm-specific but must work everywhere + name: "arm_int64_unaligned_in_registers", + fn: new(func(uintptr, int64) int64), + cFn: "arm_int64_unaligned_in_registers", + call: func(f any) string { + fn := *(f).(*func(x uintptr, y int64) int64) + return strconv.FormatInt(fn(456, math.MaxInt32+1500), 10) + }, + want: strconv.FormatInt(456*123+math.MaxInt32+1500, 10), + }, + { + // check if unaligned 64bit argument and 64bit returned value is properly passed via stack + // arm-specific but must work everywhere + name: "arm_int64_unaligned_on_stack", + fn: new(func(uintptr, uintptr, uintptr, uintptr, uintptr, int64) int64), + cFn: "arm_int64_unaligned_on_stack", + call: func(f any) string { + fn := *(f).(*func(a1, a2, a3, a4, a5 uintptr, a6 int64) int64) + return strconv.FormatInt(fn(12, 34, 56, 78, 90, math.MaxInt32+1500), 10) + }, + want: strconv.FormatInt(12*1+34*2+56*3+78*4+90*5+math.MaxInt32+1500, 10), + }, + { + // check if unaligned 64bit argument and 64bit returned value is properly passed via stack when it's occupied by floats + // arm-specific but must work everywhere + name: "arm_int64_unaligned_on_stack_after_floats", + fn: new(func( + uintptr, uintptr, uintptr, uintptr, + float32, float32, float32, float32, + float32, float32, float32, float32, + float32, float32, float32, float32, + float32, float32, float32, float32, + float32, int64, + ) int64), + cFn: "arm_int64_unaligned_on_stack_after_floats", + call: func(f any) string { + fn := *(f).(*func( + a1, a2, a3, a4 uintptr, + f1, f2, f3, f4 float32, + f5, f6, f7, f8 float32, + f9, f10, f11, f12 float32, + f13, f14, f15, f16 float32, + f17 float32, a5 int64, + ) int64) + return strconv.FormatInt(fn( + 12, 34, 56, 78, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, math.MaxInt32+1500, + ), 10) + }, + want: strconv.FormatInt(12*1+34*2+56*3+78*4+math.MaxInt32+1500, 10), + }, } for _, tt := range tests { @@ -409,7 +468,8 @@ func TestABI_ArgumentPassing(t *testing.T) { if tt.name == "20_int32" && runtime.GOARCH == "ppc64le" { t.Skip("ppc64le retains the 15-argument limit") } - if tt.name == "10_float32" && (runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64" || runtime.GOARCH == "s390x") { + if (tt.name == "10_float32" || tt.name == "arm_int64_unaligned_on_stack_after_floats") && + (runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64" || runtime.GOARCH == "s390x") { t.Skip("float32 stack arguments not yet supported on this platform") } // Struct tests require Darwin ARM64 or AMD64 diff --git a/sys_unix_arm.s b/sys_unix_arm.s index a97e9437..2ba8c2bd 100644 --- a/sys_unix_arm.s +++ b/sys_unix_arm.s @@ -51,6 +51,7 @@ TEXT callbackasm1(SB), NOSPLIT|NOFRAME, $0 MOVW R4, 52(R13) MOVW $0, R4 MOVW R4, 56(R13) + MOVW R4, 60(R13) // high word of a 64-bit return // Call crosscall2(fn, frame, 0, ctxt) MOVW ·callbackWrap_call(SB), R0 @@ -63,6 +64,7 @@ TEXT callbackasm1(SB), NOSPLIT|NOFRAME, $0 // Get result MOVW 56(R13), R0 + MOVW 60(R13), R1 // high word of a 64-bit return // Restore float registers MOVD 64(R13), F0 diff --git a/syscall_notstackargs.go b/syscall_notstackargs.go index 8acb7ddb..dc206f8f 100644 --- a/syscall_notstackargs.go +++ b/syscall_notstackargs.go @@ -39,3 +39,17 @@ func (c *callbackArgs) stackFrame() unsafe.Pointer { func (c *callbackArgs) intFrame() unsafe.Pointer { return nil } + +func (c *callbackArgs) setInt64Result(result int64) { + c.result[0] = uintptr(result) + if unsafe.Sizeof(uintptr(0)) == 4 { + c.result[1] = uintptr(result >> 32) + } +} + +func (c *callbackArgs) setUint64Result(result uint64) { + c.result[0] = uintptr(result) + if unsafe.Sizeof(uintptr(0)) == 4 { + c.result[1] = uintptr(result >> 32) + } +} diff --git a/syscall_stackargs_ppc64le.go b/syscall_stackargs_ppc64le.go index 9eb41315..3d7468f4 100644 --- a/syscall_stackargs_ppc64le.go +++ b/syscall_stackargs_ppc64le.go @@ -28,3 +28,11 @@ func (c *callbackArgs) stackFrame() unsafe.Pointer { func (c *callbackArgs) intFrame() unsafe.Pointer { return nil } + +func (c *callbackArgs) setInt64Result(result int64) { + c.result[0] = uintptr(result) +} + +func (c *callbackArgs) setUint64Result(result uint64) { + c.result[0] = uintptr(result) +} diff --git a/syscall_stackargs_s390x.go b/syscall_stackargs_s390x.go index 399985a5..c1a79970 100644 --- a/syscall_stackargs_s390x.go +++ b/syscall_stackargs_s390x.go @@ -28,3 +28,11 @@ func (c *callbackArgs) stackFrame() unsafe.Pointer { func (c *callbackArgs) intFrame() unsafe.Pointer { return nil } + +func (c *callbackArgs) setInt64Result(result int64) { + c.result[0] = uintptr(result) +} + +func (c *callbackArgs) setUint64Result(result uint64) { + c.result[0] = uintptr(result) +} diff --git a/syscall_unix.go b/syscall_unix.go index 4cfe9a8f..7ba03211 100644 --- a/syscall_unix.go +++ b/syscall_unix.go @@ -162,11 +162,10 @@ func callbackWrap(a *callbackArgs) { stackByteOffset := uintptr(0) for i := range args { // slots is the number of pointer-sized slots the argument takes - var slots int inType := fnType.In(i) + slots := int((inType.Size() + ptrSize - 1) / ptrSize) switch inType.Kind() { case reflect.Float32, reflect.Float64: - slots = int((fnType.In(i).Size() + ptrSize - 1) / ptrSize) if floatsN+slots > numOfFloatRegisters() { if isDarwin && runtime.GOARCH == "arm64" { // Darwin ARM64: read from packed stack with proper alignment @@ -183,6 +182,10 @@ func callbackWrap(a *callbackArgs) { args[i] = reflect.NewAt(inType, unsafe.Pointer(&stackFrame[stackSlot])).Elem() } stackSlot += slots + } else if isARMFloatPaddingNeeded(inType, -1, stackSlot) { + stackSlot++ + args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[stackSlot])).Elem() + stackSlot += slots } else { args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[stackSlot])).Elem() stackSlot += slots @@ -211,7 +214,9 @@ func callbackWrap(a *callbackArgs) { args[i] = getCallbackStruct(inType, a.args, &floatsN, &intsN, &stackSlot, &stackByteOffset) continue default: - slots = int((inType.Size() + ptrSize - 1) / ptrSize) + if isARMPaddingNeeded(inType, -1, intsN) { + intsN++ + } if intsN+slots > numOfIntegerRegisters() { if isDarwin && runtime.GOARCH == "arm64" { // Darwin ARM64: read from packed stack with proper alignment @@ -225,6 +230,10 @@ func callbackWrap(a *callbackArgs) { args[i] = reflect.NewAt(inType, unsafe.Pointer(&stackFrame[stackSlot])).Elem() } stackSlot += slots + } else if isARMPaddingNeeded(inType, -1, stackSlot) { + stackSlot++ + args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[stackSlot])).Elem() + stackSlot += slots } else { args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[stackSlot])).Elem() stackSlot += slots @@ -249,9 +258,13 @@ func callbackWrap(a *callbackArgs) { ret := fn.Call(args) if len(ret) > 0 { switch k := ret[0].Kind(); k { - case reflect.Uint, reflect.Uint64, reflect.Uint32, reflect.Uint16, reflect.Uint8, reflect.Uintptr: + case reflect.Uint64: + a.setUint64Result(ret[0].Uint()) + case reflect.Uint, reflect.Uint32, reflect.Uint16, reflect.Uint8, reflect.Uintptr: a.result[0] = uintptr(ret[0].Uint()) - case reflect.Int, reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8: + case reflect.Int64: + a.setInt64Result(ret[0].Int()) + case reflect.Int, reflect.Int32, reflect.Int16, reflect.Int8: a.result[0] = uintptr(ret[0].Int()) case reflect.Bool: if ret[0].Bool() { diff --git a/testdata/abitest/abi_test.c b/testdata/abitest/abi_test.c index 824e453d..bc31eb9e 100644 --- a/testdata/abitest/abi_test.c +++ b/testdata/abitest/abi_test.c @@ -167,3 +167,24 @@ double stack_32_mixed_int_float( f9 * 25 + f10 * 26 + f11 * 27 + f12 * 28 + f13 * 29 + f14 * 30 + f15 * 31 + f16 * 32; } + +int64_t arm_int64_unaligned_in_registers(uintptr_t a1, int64_t a2) { + return (int64_t)a1 * 123 + a2; +} + +int64_t arm_int64_unaligned_on_stack(uintptr_t a1, uintptr_t a2, uintptr_t a3, uintptr_t a4, uintptr_t a5, int64_t a6) { + return (int64_t)a1 * 1 + (int64_t)a2 * 2 + (int64_t)a3 * 3 + (int64_t)a4 * 4 + + (int64_t)a5 * 5 + a6; +} + +int64_t arm_int64_unaligned_on_stack_after_floats( + uintptr_t a1, uintptr_t a2, uintptr_t a3, uintptr_t a4, /* these args are going to integer registers */ + float f1, float f2, float f3, float f4, /* these args are going to float registers */ + float f5, float f6, float f7, float f8, + float f9, float f10, float f11, float f12, + float f13, float f14, float f15, float f16, + float f17, /* this is a first arg on stack */ + int64_t a5 /* this arg must be padded */ +) { + return (int64_t)a1 * 1 + (int64_t)a2 * 2 + (int64_t)a3 * 3 + (int64_t)a4 * 4 + a5; +}