diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 84a52639..2522e031 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -146,7 +146,9 @@ jobs: if: ${{ runner.os == 'Linux' && runner.arch == 'ARM64' }} run: | sudo apt-get update - sudo apt-get install -y gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf libc6-armhf-cross + sudo apt-get install -y \ + gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf libc6-armhf-cross \ + gcc-arm-linux-gnueabi g++-arm-linux-gnueabi libc6-armel-cross # Install the latest QEMU 10.x package from Debian. Ubuntu 24.04 ships QEMU 8.2 # which has a bug where inter-thread signal delivery (tgkill) hangs, @@ -177,6 +179,17 @@ jobs: go env -u CC go env -u CXX + # Repeat the same for ARMv5 target (soft-float) + sudo ln -sf /usr/arm-linux-gnueabi/lib/ld-linux.so.3 /lib/ld-linux.so.3 + go env -w CC=arm-linux-gnueabi-gcc + go env -w CXX=arm-linux-gnueabi-g++ + env GOOS=linux GOARCH=arm GOARM=5 CGO_ENABLED=0 go test -c -o=purego-test-nocgo . + env QEMU_LD_PREFIX=/usr/arm-linux-gnueabi qemu-arm ./purego-test-nocgo -test.shuffle=on -test.v -test.count=10 + env GOOS=linux GOARCH=arm GOARM=5 CGO_ENABLED=1 go test -c -o=purego-test-cgo . + env QEMU_LD_PREFIX=/usr/arm-linux-gnueabi qemu-arm ./purego-test-cgo -test.shuffle=on -test.v -test.count=10 + go env -u CC + go env -u CXX + - name: go test race (no Cgo) if: runner.os == 'macOS' run: | diff --git a/func.go b/func.go index ff154dc7..57afd93e 100644 --- a/func.go +++ b/func.go @@ -21,6 +21,10 @@ const ( align8ByteSize = 8 // 8-byte alignment boundary ) +func isARMSoftFloat() bool { + return runtime.GOARCH == "arm" && *(*uint8)(unsafe.Pointer(&runtime_goarmsoftfp)) != 0 +} + var thePool = sync.Pool{New: func() any { return new(syscallArgs) }} @@ -183,6 +187,19 @@ func RegisterFunc(fptr any, cfn uintptr) { } case reflect.Float32, reflect.Float64: usesSlots := max(1, int(arg.Size()/ptrSize)) + if isARMSoftFloat() { + // float64 for arm with softfloat uses same rules as int64 + if isARMPaddingNeeded(arg, ints, stack) { + usesSlots++ + } + if ints < numOfIntegerRegisters() { + ints += usesSlots + } else { + stack += usesSlots + } + continue + } + if isARMFloatPaddingNeeded(arg, floats, stack) { usesSlots++ } @@ -402,6 +419,7 @@ func RegisterFunc(fptr any, cfn uintptr) { // On 386, x87 FPU returns floats as float64 in ST(0), so we read as float64 and convert. // On PPC64LE, C ABI converts float32 to double in FPR, so we read as float64. // On S390X (big-endian), float32 is in upper 32 bits of the 64-bit FP register. + // On 32bit ARM with softfloat float32 returned as integer switch runtime.GOARCH { case "386": v.SetFloat(math.Float64frombits(uint64(syscall.f1) | (uint64(syscall.f2) << 32))) @@ -410,13 +428,22 @@ func RegisterFunc(fptr any, cfn uintptr) { case "s390x": // S390X is big-endian: float32 in upper 32 bits of 64-bit register v.SetFloat(float64(math.Float32frombits(uint32(syscall.f1 >> 32)))) + case "arm": + if isARMSoftFloat() { + v.SetFloat(float64(math.Float32frombits(uint32(syscall.a1)))) + } else { + v.SetFloat(float64(math.Float32frombits(uint32(syscall.f1)))) + } default: v.SetFloat(float64(math.Float32frombits(uint32(syscall.f1)))) } case reflect.Float64: // NOTE: syscall.r2 is only the floating return value on 64bit platforms. // On 32bit platforms syscall.r2 is the upper part of a 64bit return. - if is32bit { + if isARMSoftFloat() { + // a1,a2 are populated in this case + v.SetFloat(math.Float64frombits(uint64(syscall.a1) | (uint64(syscall.a2) << 32))) + } else if is32bit { v.SetFloat(math.Float64frombits(uint64(syscall.f1) | (uint64(syscall.f2) << 32))) } else { v.SetFloat(math.Float64frombits(uint64(syscall.f1))) @@ -483,6 +510,13 @@ func addValue(v reflect.Value, keepAlive []any, addInt func(x uintptr), addFloat case "s390x": // S390X big-endian: float32 goes in the upper 32 bits of the 64-bit FP register. addFloat(uintptr(math.Float32bits(float32(v.Float()))) << 32) + case "arm": + if isARMSoftFloat() { + // 32-bit ARM with softfloat: float32 goes as integer + addInt(uintptr(math.Float32bits(float32(v.Float())))) + } else { + addFloat(uintptr(math.Float32bits(float32(v.Float())))) + } default: addFloat(uintptr(math.Float32bits(float32(v.Float())))) } @@ -492,7 +526,14 @@ func addValue(v reflect.Value, keepAlive []any, addInt func(x uintptr), addFloat // if floats are spilled onto stack on ARM than we must follow AAPCS C.7 addFloat(0) } - if is32bit { + if isARMSoftFloat() { + // add as uint64 + if isARMPaddingNeeded(v.Type(), *numInts, *numStack) { + addInt(0) + } + addInt(uintptr(bits)) + addInt(uintptr(bits >> 32)) + } else if is32bit { addFloat(uintptr(bits)) addFloat(uintptr(bits >> 32)) } else { @@ -603,6 +644,7 @@ func numOfFloatRegisters() int { case "s390x": return 4 case "arm": + // 8 doubles (16 words) are always reserved by asm trampolines, even if softfloat is used return 16 case "386": // i386 SysV ABI passes all arguments on the stack, including floats diff --git a/func_test.go b/func_test.go index 9b305ade..f5190714 100644 --- a/func_test.go +++ b/func_test.go @@ -461,6 +461,30 @@ func TestABI_ArgumentPassing(t *testing.T) { }, want: strconv.FormatInt(12*1+34*2+56*3+78*4+math.MaxInt32+1500, 10), }, + { + // check if unaligned float 64bit argument and float 64bit returned value is properly passed via registers + // arm-softfloat-specific but must work everywhere + name: "arm_float64_unaligned_in_registers", + fn: new(func(uintptr, float64) float64), + cFn: "arm_float64_unaligned_in_registers", + call: func(f any) string { + fn := *(f).(*func(x uintptr, y float64) float64) + return strconv.FormatFloat(fn(456, math.MaxFloat32+1500), 'b', 10, 64) + }, + want: strconv.FormatFloat(456*123.5+math.MaxFloat32+1500, 'b', 10, 64), + }, + { + // check if unaligned float 64bit argument and float 64bit returned value is properly passed via stack + // arm-softfloat-specific but must work everywhere + name: "arm_float64_unaligned_on_stack", + fn: new(func(uintptr, uintptr, uintptr, uintptr, uintptr, float64) float64), + cFn: "arm_float64_unaligned_on_stack", + call: func(f any) string { + fn := *(f).(*func(a1, a2, a3, a4, a5 uintptr, a6 float64) float64) + return strconv.FormatFloat(fn(12, 34, 56, 78, 90, math.MaxFloat32+1500), 'b', 10, 64) + }, + want: strconv.FormatFloat(12*1+34*2+56*3+78*4+90*5+math.MaxFloat32+1500, 'b', 10, 64), + }, } for _, tt := range tests { diff --git a/go_runtime.go b/go_runtime.go index b327f786..0a332b6e 100644 --- a/go_runtime.go +++ b/go_runtime.go @@ -11,3 +11,9 @@ import ( //go:linkname runtime_cgocall runtime.cgocall func runtime_cgocall(fn uintptr, arg unsafe.Pointer) int32 // from runtime/sys_libc.go + +// from runtime/runtime2.go, exported via go:linkname for usage with cgo assembly +// pulled in as struct{} for proper linking, see https://github.com/golang/go/issues/72032 +// +//go:linkname runtime_goarmsoftfp runtime.goarmsoftfp +var runtime_goarmsoftfp struct{} diff --git a/internal/fakecgo/asm_arm.s b/internal/fakecgo/asm_arm.s index a45f1f83..1ebb7e4f 100644 --- a/internal/fakecgo/asm_arm.s +++ b/internal/fakecgo/asm_arm.s @@ -21,8 +21,11 @@ TEXT crosscall2(SB), NOSPLIT|NOFRAME, $0 // starting at 4(R13). MOVW.W R14, -4(R13) + // Skip floating point registers if runtime.goarmsoftfp!=0. + MOVB runtime·goarmsoftfp(SB), R11 + CMP $0, R11 + BNE skipfpsave // Save VFP callee-saved registers D8-D15 (same as S16-S31). - // Note: We always save these since we target hard-float ABI. MOVD F8, (13*4+8*1)(R13) MOVD F9, (13*4+8*2)(R13) MOVD F10, (13*4+8*3)(R13) @@ -32,9 +35,13 @@ TEXT crosscall2(SB), NOSPLIT|NOFRAME, $0 MOVD F14, (13*4+8*7)(R13) MOVD F15, (13*4+8*8)(R13) +skipfpsave: // We set up the arguments to cgocallback when saving registers above. BL runtime·cgocallback(SB) + MOVB runtime·goarmsoftfp(SB), R11 + CMP $0, R11 + BNE skipfprest MOVD (13*4+8*1)(R13), F8 MOVD (13*4+8*2)(R13), F9 MOVD (13*4+8*3)(R13), F10 @@ -44,6 +51,7 @@ TEXT crosscall2(SB), NOSPLIT|NOFRAME, $0 MOVD (13*4+8*7)(R13), F14 MOVD (13*4+8*8)(R13), F15 +skipfprest: MOVW.P 4(R13), R14 MOVM.IAW (R13), [R0, R1, R3, R4, R5, R6, R7, R8, R9, g, R11, R12] ADD $(8*9), R13 diff --git a/internal/fakecgo/trampolines_arm.s b/internal/fakecgo/trampolines_arm.s index c1cd0c92..d679f4c1 100644 --- a/internal/fakecgo/trampolines_arm.s +++ b/internal/fakecgo/trampolines_arm.s @@ -70,6 +70,10 @@ TEXT threadentry_trampoline(SB), NOSPLIT, $104-0 MOVW g, 32(R13) // R10 MOVW R11, 36(R13) + // Skip floating point registers if runtime.goarmsoftfp!=0. + MOVB runtime·goarmsoftfp(SB), R11 + CMP $0, R11 + BNE skipfpsave MOVD F8, 40(R13) MOVD F9, 48(R13) MOVD F10, 56(R13) @@ -79,10 +83,14 @@ TEXT threadentry_trampoline(SB), NOSPLIT, $104-0 MOVD F14, 88(R13) MOVD F15, 96(R13) +skipfpsave: MOVW ·threadentry_call(SB), R12 MOVW (R12), R12 CALL (R12) + MOVB runtime·goarmsoftfp(SB), R11 + CMP $0, R11 + BNE skipfprest MOVD 40(R13), F8 MOVD 48(R13), F9 MOVD 56(R13), F10 @@ -92,6 +100,7 @@ TEXT threadentry_trampoline(SB), NOSPLIT, $104-0 MOVD 88(R13), F14 MOVD 96(R13), F15 +skipfprest: MOVW 8(R13), R4 MOVW 12(R13), R5 MOVW 16(R13), R6 diff --git a/sys_arm.s b/sys_arm.s index f1ea44a2..fd2dcc22 100644 --- a/sys_arm.s +++ b/sys_arm.s @@ -41,6 +41,10 @@ TEXT syscallX(SB), NOSPLIT|NOFRAME, $0-0 MOVW syscallArgs_fn(R8), R5 MOVW R5, (PTR_ADDRESS-4)(R13) // save fn at offset 56 + // Skip floating point registers if runtime.goarmsoftfp!=0. + MOVB runtime·goarmsoftfp(SB), R11 + CMP $0, R11 + BNE skipfpload // Load floating point arguments // Each float64 spans 2 uintptr slots (8 bytes) on ARM32, so we skip by 2 MOVD syscallArgs_f1(R8), F0 // f1+f2 -> D0 @@ -52,6 +56,7 @@ TEXT syscallX(SB), NOSPLIT|NOFRAME, $0-0 MOVD syscallArgs_f13(R8), F6 // f13+f14 -> D6 MOVD syscallArgs_f15(R8), F7 // f15+f16 -> D7 +skipfpload: // Load integer arguments into registers (R0-R3 for ARM EABI) MOVW syscallArgs_a1(R8), R0 // a1 MOVW syscallArgs_a2(R8), R1 // a2 @@ -131,12 +136,16 @@ TEXT syscallX(SB), NOSPLIT|NOFRAME, $0-0 MOVW R0, syscallArgs_a1(R8) MOVW R1, syscallArgs_a2(R8) + MOVB runtime·goarmsoftfp(SB), R11 + CMP $0, R11 + BNE skipfpsave // save f0-f3 (each float64 spans 2 uintptr slots on ARM32) MOVD F0, syscallArgs_f1(R8) MOVD F1, syscallArgs_f3(R8) MOVD F2, syscallArgs_f5(R8) MOVD F3, syscallArgs_f7(R8) +skipfpsave: // Restore callee-saved registers and return MOVM.IA.W (R13), [R4, R5, R6, R7, R8, R9, R11] MOVW.P 4(R13), R15 // pop LR into PC (return) diff --git a/sys_unix_arm.s b/sys_unix_arm.s index 2ba8c2bd..0efd59e5 100644 --- a/sys_unix_arm.s +++ b/sys_unix_arm.s @@ -33,8 +33,11 @@ TEXT callbackasm1(SB), NOSPLIT|NOFRAME, $0 MOVW R2, 136(R13) MOVW R3, 140(R13) + // Skip floating point registers if runtime.goarmsoftfp!=0. + MOVB runtime·goarmsoftfp(SB), R11 + CMP $0, R11 + BNE skipfpsave // Save floating point registers F0-F7 at SP+64 (frame[0..15]) - // Note: We always save these since we target hard-float ABI. MOVD F0, 64(R13) MOVD F1, 72(R13) MOVD F2, 80(R13) @@ -44,6 +47,7 @@ TEXT callbackasm1(SB), NOSPLIT|NOFRAME, $0 MOVD F6, 112(R13) MOVD F7, 120(R13) +skipfpsave: // Set up callbackArgs at SP+48 MOVW 36(R13), R4 MOVW R4, 48(R13) @@ -66,6 +70,9 @@ TEXT callbackasm1(SB), NOSPLIT|NOFRAME, $0 MOVW 56(R13), R0 MOVW 60(R13), R1 // high word of a 64-bit return + MOVB runtime·goarmsoftfp(SB), R11 + CMP $0, R11 + BNE skipfprest // Restore float registers MOVD 64(R13), F0 MOVD 72(R13), F1 @@ -76,6 +83,7 @@ TEXT callbackasm1(SB), NOSPLIT|NOFRAME, $0 MOVD 112(R13), F6 MOVD 120(R13), F7 +skipfprest: // Restore callee-saved registers MOVW 0(R13), R4 MOVW 4(R13), R5 diff --git a/syscall_unix.go b/syscall_unix.go index 7ba03211..024e1073 100644 --- a/syscall_unix.go +++ b/syscall_unix.go @@ -166,6 +166,26 @@ func callbackWrap(a *callbackArgs) { slots := int((inType.Size() + ptrSize - 1) / ptrSize) switch inType.Kind() { case reflect.Float32, reflect.Float64: + if isARMSoftFloat() { + // we should restore from integer slot, can skip unnecessary branching here + if isARMPaddingNeeded(inType, -1, intsN) { + intsN++ + } + if intsN+slots <= numOfIntegerRegisters() { + // the integers begin after the floats in frame + args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[intsN+numOfFloatRegisters()])).Elem() + intsN += slots + continue + } + if isARMPaddingNeeded(inType, -1, stackSlot) { + stackSlot++ + } + args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[stackSlot])).Elem() + stackSlot += slots + intsN += slots + continue + } + if floatsN+slots > numOfFloatRegisters() { if isDarwin && runtime.GOARCH == "arm64" { // Darwin ARM64: read from packed stack with proper alignment diff --git a/testdata/abitest/abi_test.c b/testdata/abitest/abi_test.c index bc31eb9e..c129d26d 100644 --- a/testdata/abitest/abi_test.c +++ b/testdata/abitest/abi_test.c @@ -188,3 +188,12 @@ int64_t arm_int64_unaligned_on_stack_after_floats( ) { return (int64_t)a1 * 1 + (int64_t)a2 * 2 + (int64_t)a3 * 3 + (int64_t)a4 * 4 + a5; } + +double arm_float64_unaligned_in_registers(uintptr_t a1, double a2) { + return (double)a1 * 123.5 + a2; +} + +double arm_float64_unaligned_on_stack(uintptr_t a1, uintptr_t a2, uintptr_t a3, uintptr_t a4, uintptr_t a5, double a6) { + return (double)a1 * 1 + (double)a2 * 2 + (double)a3 * 3 + (double)a4 * 4 + + (double)a5 * 5 + a6; +}