有一个问题困扰着我。
所以 ... 为什么在 x86_32 中,参数 在我认为是字母顺序 的寄存器中传递(eax
, ecx
, edx
, esi
) and ranked order (esi
, edi
, ebp
)
+---------+------+------+------+------+------+------+
| syscall | arg0 | arg1 | arg2 | arg3 | arg4 | arg5 |
+---------+------+------+------+------+------+------+
| %eax | %ebx | %ecx | %edx | %esi | %edi | %ebp |
+---------+------+------+------+------+------+------+
section .text
global _start
_start:
mov eax, 1 ; x86_64 opcode for sys_exit
mov ebx, 0 ; first argument
int 0x80
虽然在 x86_64 中,系统调用的参数在看起来有点随机排列的寄存器中传递:
+---------+------+------+------+------+------+------+
| syscall | arg0 | arg1 | arg2 | arg3 | arg4 | arg5 |
+---------+------+------+------+------+------+------+
| %rax | %rdi | %rsi | %rdx | %r10 | %r8 | %r9 |
+---------+------+------+------+------+------+------+
section .text
global _start
_start:
mov eax, 1 ; x86_64 opcode for sys_exit
mov edi, 0 ; first argument
syscall
他们这样做是出于特定原因吗?我在这里没看到什么吗?
最佳答案
x86-64 System V ABI 旨在最大限度地减少 SPECint 中的指令数(以及某种程度上的代码大小),该代码由第一批 AMD64 CPU 销售之前的最新 gcc 版本编译。参见 this answer for some history and list-archive links .
Since 5 minutes before I thought all registers were the same but they were used differently because of a convention. Now all things changed for me
x86-64 不是完全正交的。一些指令隐含地使用特定的寄存器。例如push
隐式使用 rsp
作为堆栈指针,shl edx, cl
仅可用于 cl
中的移位计数(直到 BMI2 shlx
)。
很少使用:加宽 mul rdi
会产生 rdx:rax = rax*rdi
。 rep-string 指令隐式使用 RDI、RSI 和 RCX,尽管它们通常不值得使用。
事实证明,选择 arg 传递寄存器以便将其 args 传递给 memcpy 的函数可以内联它,因为 rep movs
在 Jan Hubicka 使用的指标中很有用,因此 rdi
和 rsi
被选为前两个参数。但是,在第 4 个 arg 之前保留 rcx
未使用更好,因为变量计数转换需要 cl
。 (而且大多数函数不会碰巧使用它们的第 3 个参数作为移位计数。)(可能是较旧的 GCC 版本将 memcpy
或 memset
内联为 rep movs
更积极;对于如今的小型阵列,与 SIMD 相比,它通常不值得。)
x86-64 System V ABI 使用与系统调用几乎相同的函数调用约定。这不是巧合:这意味着像 mmap
这样的 libc 包装函数的实现可以是:
mmap:
mov r10, rcx ; syscall destroys rcx and r11; 4th arg passed in r10 for syscalls
mov eax, __NR_mmap
syscall
cmp rax, -4096
ja .set_errno_and_stuff
ret
这是一个微小的优势,但确实没有理由不这样做。它还在内核中保存了一些指令,这些指令在调度到内核中系统调用的 C 实现之前设置了 arg 传递寄存器。 (请参阅 this answer 查看系统调用处理的一些内核方面。主要是关于 int 0x80
处理程序,但我想我提到了 64 位 syscall
处理程序和它直接从 asm 分派(dispatch)到函数表。)
syscall
指令本身 destroys RCX and R11 (为了保存用户空间 RIP 和 RFLAGS 而无需微代码来设置内核堆栈)所以约定不能完全相同,除非用户空间约定避免了 RCX 和 R11。但是 RCX 是一个方便的寄存器,它的低半部分可以在没有 REX 前缀的情况下使用,所以这可能比将它作为像 R11 这样的调用破坏的纯划痕更糟糕。此外,用户空间约定将 R10 用作具有一流嵌套函数(而非 C/C++)的语言的“静态链”指针。
让前 4 个参数能够避免 REX 前缀可能对整体代码大小来说是最好的,使用 RBX 或 RBP 而不是 RCX 会很奇怪。有几个不需要 REX 前缀的调用保留寄存器 (EBX/EBP) 很好。
参见 What are the calling conventions for UNIX & Linux system calls on i386 and x86-64用于函数调用和系统调用约定。
i386 系统调用约定笨重且不方便:ebx
是调用保留的,因此几乎每个系统调用包装器都需要保存/恢复 ebx
,除了没有像 getpid
这样的参数的调用。 (为此,您甚至不需要进入内核,只需调用 vDSO:有关 vDSO 和大量其他内容的更多信息,请参阅 The Definitive Guide to Linux System Calls (on x86)。)
但是 i386 函数调用约定传递堆栈上的所有参数,因此 glibc 包装函数仍然需要 mov
每个参数。
另请注意,x86 寄存器的“自然”顺序是 EAX、ECX、EDX、EBX,根据它们在机器代码中的数字代码,以及 pusha
/popa 的顺序
使用。参见 Why are first four x86 GPRs named in such unintuitive order? .
关于assembly - 为什么 Assembly x86_64 系统调用参数不像 i386 那样按字母顺序排列,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/47676657/