为什么 Assembly x86_64 系统调用参数不像 i386 那样按字母顺序排列

Why Assembly x86_64 syscall parameters are not in alphabetical order like i386

有一个问题困扰着我。

那么...为什么在 x86_32 参数 在寄存器中传递,我觉得在 按字母顺序eaxecxedxesi)和排名顺序(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 使用的指标中很有用,因此 rdirsi 被选为前两个参数。但是在第 4 个 arg 之前让 rcx 未使用更好,因为变量计数移位需要 cl。 (而且大多数函数不会碰巧使用它们的第 3 个参数作为移位计数。)(可能较旧的 GCC 版本更积极地内联 memcpymemset 作为 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 传递寄存器。 (请参阅 查看系统调用处理的一些内核方面。主要是关于 int 0x80 处理程序,但我想我提到了 64 位 syscall 处理程序并且它分派到table 个直接来自 asm 的函数。)

syscall 指令本身 (保存用户-space RIP 和 RFLAGS,而不需要微代码来设置内核堆栈)所以约定不能完全相同,除非user-space 约定避免了 RCX 和 R11。但是 RCX 是一个方便的寄存器,它的低半部分可以在没有 REX 前缀的情况下使用,所以这可能比将它作为像 R11 这样的调用破坏的纯划痕更糟糕。此外,user-space 约定使用 R10 作为 "static chain" 指针,用于具有 first-class 嵌套函数(不是 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 是调用保留的,因此几乎每个系统调用包装器都需要 save/restore ebx,除了没有像 getpid 这样的参数的调用。 (为此,您甚至不需要进入内核,只需调用 vDSO:有关 vDSO 和大量其他内容的更多信息,请参阅 The Definitive Guide to Linux System Calls (on x86)。)

但是 i386 函数调用约定传递堆栈上的所有参数,因此 glibc 包装函数仍然需要 mov 每个参数。

另请注意,x86寄存器的"natural"顺序是EAX、ECX、EDX、EBX,根据它们在机器码中的数字代码,还有pusha / [=35的顺序=] 使用。参见 Why are first four x86 GPRs named in such unintuitive order?