Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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: |
Expand Down
46 changes: 44 additions & 2 deletions func.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}}
Expand Down Expand Up @@ -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++
}
Expand Down Expand Up @@ -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)))
Expand All @@ -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)))
Expand Down Expand Up @@ -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()))))
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
24 changes: 24 additions & 0 deletions func_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
6 changes: 6 additions & 0 deletions go_runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
10 changes: 9 additions & 1 deletion internal/fakecgo/asm_arm.s
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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
Expand Down
9 changes: 9 additions & 0 deletions internal/fakecgo/trampolines_arm.s
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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
Expand Down
9 changes: 9 additions & 0 deletions sys_arm.s
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
10 changes: 9 additions & 1 deletion sys_unix_arm.s
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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
Expand Down
20 changes: 20 additions & 0 deletions syscall_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions testdata/abitest/abi_test.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}