Skip to content
Closed
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
118 changes: 106 additions & 12 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 @@ -164,16 +168,37 @@ 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 := min(1, int(ty.Size()/unsafe.Sizeof(uintptr(0))))
if isARMPaddingNeeded(ty, ints+stack) {
usesSlots++
}

if ints < numOfIntegerRegisters() {
ints++
ints += usesSlots
} else {
stack++
stack += usesSlots
}
case reflect.Float32, reflect.Float64:
usesSlots := int(ty.Size() / unsafe.Sizeof(uintptr(0)))
if isARMSoftFloat() {
// float64 for arm with softfloat uses same rules as int64
if isARMPaddingNeeded(ty, ints+stack) {
usesSlots++
}
if ints+usesSlots < numOfIntegerRegisters() {
ints += usesSlots
} else {
stack += usesSlots
}
continue
}

if floats < floatArgRegs {
floats++
} else if isARMPaddingNeeded(ty, floats+stack) {
stack += usesSlots + 1
} else {
stack++
stack += usesSlots
}
case reflect.Struct:
ensureStructSupported()
Expand Down Expand Up @@ -347,9 +372,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)
Expand All @@ -373,6 +411,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 @@ -381,13 +420,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 @@ -415,9 +463,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
Expand All @@ -438,16 +502,30 @@ 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()))))
}
case reflect.Float64:
if is32bit {
bits := math.Float64bits(v.Float())
bits := math.Float64bits(v.Float())
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 {
addFloat(uintptr(math.Float64bits(v.Float())))
addFloat(uintptr(bits))
}
case reflect.Struct:
keepAlive = addStruct(v, numInts, numFloats, numStack, addInt, addFloat, addStack, keepAlive)
Expand Down Expand Up @@ -554,6 +632,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 Expand Up @@ -592,18 +671,25 @@ func estimateStackBytes(ty reflect.Type) int {
var numInts, numFloats int
var stackBytes int

ptrSize := int(unsafe.Sizeof(uintptr(0)))
for i := 0; i < ty.NumIn(); i++ {
arg := ty.In(i)
size := int(arg.Size())

// Check if this goes to register or stack
usesInt := arg.Kind() != reflect.Float32 && arg.Kind() != reflect.Float64
usesInt := (arg.Kind() != reflect.Float32 && arg.Kind() != reflect.Float64) || isARMSoftFloat()
if usesInt && numInts < numOfIntegerRegisters() {
if isARMPaddingNeeded(arg, numInts) {
numInts++
}
numInts++
} else if !usesInt && numFloats < numOfFloatRegisters() {
numFloats++
} else {
// Goes to stack - accumulate total bytes
if isARMPaddingNeeded(arg, stackBytes/ptrSize) {
stackBytes += ptrSize
}
stackBytes += size
}
}
Expand All @@ -613,3 +699,11 @@ func estimateStackBytes(ty reflect.Type) int {
}
return stackBytes
}

func isARMPaddingNeeded(ty reflect.Type, numValues 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
return runtime.GOARCH == "arm" && ty.Size() == 8 && numValues%2 != 0
}
50 changes: 50 additions & 0 deletions func_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import (
"bytes"
"errors"
"fmt"
"math"
"os"
"os/exec"
"path/filepath"
"reflect"
"runtime"
"strconv"
"strings"
"sync"
"testing"
Expand Down Expand Up @@ -402,6 +404,54 @@ 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 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 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 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
Loading