Here is Go's undocumented Syscall function:
func Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno)
And here is the C definition:
long syscall(long number, ...);
Pretty different. So it's fairly obvious that trap
is number
, and a1
, a2
, and a3
allow for three arguments. I also worked out that r1
is the return value, and err
is errno
. But what is r2
? The syscall man page doesn't mention multiple return values.
It does give the actual calling conventions (still only one retval):
arch/ABI instruction syscall # retval error Notes
────────────────────────────────────────────────────────────────────
alpha callsys v0 a0 a3 [1]
arc trap0 r8 r0 -
arm/OABI swi NR - a1 - [2]
arm/EABI swi 0x0 r7 r0 -
arm64 svc #0 x8 x0 -
blackfin excpt 0x0 P0 R0 -
i386 int $0x80 eax eax -
ia64 break 0x100000 r15 r8 r10 [1]
m68k trap #0 d0 d0 -
microblaze brki r14,8 r12 r3 -
mips syscall v0 v0 a3 [1]
nios2 trap r2 r2 r7
parisc ble 0x100(%sr2, %r0) r20 r28 -
powerpc sc r0 r3 r0 [1]
s390 svc 0 r1 r2 - [3]
s390x svc 0 r1 r2 - [3]
superh trap #0x17 r3 r0 - [4]
sparc/32 t 0x10 g1 o0 psr/csr [1]
sparc/64 t 0x6d g1 o0 psr/csr [1]
tile swint1 R10 R00 R01 [1]
x86_64 syscall rax rax - [5]
x32 syscall rax rax - [5]
xtensa syscall a2 a2 -
But on x86 this is the implementation
#define INVOKE_SYSCALL INT $0x80
TEXT ·Syscall(SB),NOSPLIT,$0-28
CALL runtime·entersyscall(SB)
MOVL trap+0(FP), AX // syscall entry
MOVL a1+4(FP), BX
MOVL a2+8(FP), CX
MOVL a3+12(FP), DX
MOVL $0, SI
MOVL $0, DI
INVOKE_SYSCALL
CMPL AX, $0xfffff001
JLS ok
MOVL $-1, r1+16(FP)
MOVL $0, r2+20(FP)
NEGL AX
MOVL AX, err+24(FP)
CALL runtime·exitsyscall(SB)
RET
ok:
MOVL AX, r1+16(FP)
MOVL DX, r2+20(FP)
MOVL $0, err+24(FP)
CALL runtime·exitsyscall(SB)
RET
Now, I don't read assembly too well, but I'm pretty sure it is returning EDX in r2. Why?
I think they have multiple return values for consistency. As you can see from that table, some architectures return multiple values and if you check a few of the other assembly files from that directory you'll see they move register values to r2.
But why DX? This part is still puzzling. Scattered across the web are docs mentioning on i386 a function is allowed to use both EAX and EDX for return values. For example System V Application Binary Interface Intel386 Architecture Processor Supplement:
%edx scratch register; also used to return the upper 32bits of some 64bit return types
Later it goes on to say:
The most significant 32 bits are returned in %edx. The least unsigned long long significant 32 bits are returned in %eax.
Let's try this:
uint64_t some_function() {
return 18446744073709551614LLU;
}
Clang ends up producing:
pushl %ebp
movl %esp, %ebp
movl $-2, %eax
movl $-1, %edx
popl %ebp
ret
Interestingly, asm_linux_amd64.s seems to do the same thing, giving us a pretext to look at the System V ABI for AMD64. This also doc mentions in passing, about RDX:
used to pass 3rd argument to functions; 2nd return register
But Appendix A deals with Linux Conventions specifically.
The interface between the C library and the Linux kernel is the same as for the user-level applications with the following differences:
Returning from the syscall, register %rax contains the result of the system-call. A value in the range between -4095 and -1 indicates an error, it is -errno.
No mention of RDX for the system call.
I won't put my hand in the fire for this (or in general) but I suspect taking DX is not necessary for Linux which doesn't make use of such large return values that they spill out of AX.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With