为什么 x86_64 平台的汇编 syscall 参数不像 i386 平台那样按字母顺序排列

3

有一个问题一直困扰着我。

那就是...为什么在 x86_32 中,参数会按照我感觉上是字母表顺序eax, ecx, edx, esi)和等级顺序(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

他们这样做是有特定的原因吗?我有没有看漏什么?

1
在x86-64中,它匹配函数调用约定,因此系统调用包装函数是轻量级的。在i386中,我不知道为什么它使用那种不方便的设置(ebx是调用保留的,因此几乎每个系统调用包装器都需要保存/恢复ebx)。 - Peter Cordes
@PeterCordes,函数调用约定的设计旨在减少在使用rep movsb实现memcpy时的寄存器移动量等问题。 - fuz
函数调用约定?也许我会听起来像个新手,可能因为我确实是……但是什么是函数调用约定?这是汇编语言的事情吗? - Apostolis Anastasiou
我的回答在 https://dev59.com/6G855IYBdhLWcg3wSiM7#35619528 上提供了一些关于x86-64 SysV ABI设计的历史(包括由设计者Jan Hubicka的邮件列表档案链接)。 - Peter Cordes
@PeterCordes,如果我理解正确的话,使用寄存器的原因是它们的电子结构和机械操作吗?虽然没有人特别提到这一点,但Jan Habicka在这里:http://web.archive.org/web/20140414124645/http://www.x86-64.org/pipermail/discuss/2000-November/001257.html 表示其他寄存器产生的代码量较少?这怎么可能?因为5分钟之前我认为所有寄存器都是相同的,只是因为约定俗成而被不同地使用。现在一切都对我产生了影响... - Apostolis Anastasiou
显示剩余2条评论
1个回答

3
x86-64 System V ABI旨在最小化SPECint中的指令计数(在第一批AMD64 CPU销售之前使用的gcc版本编译),并在某种程度上减小代码大小。请参见this answer for some history and list-archive links

5分钟前,我认为所有寄存器都是相同的,但它们由于约定而被不同地使用。现在,对我来说一切都改变了。

x86-64不是完全正交的。某些指令隐含地使用特定的寄存器,例如push隐含使用rsp作为堆栈指针,shl edx, cl只能使用cl中的移位计数(直到BMI2 shlx)。
更少用的是:扩展mul rdi执行rdx:rax = rax*rdi。rep-string指令隐式使用RDI、RSI和RCX,尽管它们通常不值得使用。
事实证明,选择参数传递寄存器,使将其参数传递给memcpy的函数可以将其作为rep movs内联是有用的,因此选择了rdirsi作为前两个参数。但是,直到第四个参数才不使用rcx更好,因为变量计数移位需要cl。(大多数函数不会使用它们的第三个参数作为移位计数。)(可能旧版本的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实现时节省一些指令。(请参阅this answer,了解一些关于系统调用处理的内核方面内容。主要涉及int 0x80处理程序,但我认为我提到了64位syscall处理程序,并且它直接从asm分派到函数表。) syscall 指令本身破坏了 RCX 和 R11(以便在不需要微码设置内核堆栈的情况下保存用户空间 RIP 和 RFLAGS),因此,除非用户空间约定避免使用 RCX 和 R11,否则约定不能完全相同。但是,RCX 是一个方便的寄存器,其低半部分可以在没有 REX 前缀的情况下使用,因此将其保留为调用破坏的纯 Scratch,像 R11 一样,可能会更糟糕。此外,用户空间约定将 R10 用作具有一级嵌套函数(而不是 C/C++)的语言的“静态链”指针。

让前4个参数能够避免使用 REX 前缀可能是整体代码大小最好的选择,使用 RBX 或 RBP 替代 RCX 将很奇怪。具有几个不需要 REX 前缀(EBX/EBP)的调用保留寄存器是很好的。

参见UNIX和Linux系统调用在i386和x86-64上的函数调用和系统调用惯例


ebx是调用保留的,因此几乎每个系统调用包装器都需要保存/恢复ebx,除了像getpid这样没有参数的调用。 (对于这个你甚至不需要进入内核,只需调用vDSO:有关vDSO和大量其他信息,请参见The Definitive Guide to Linux System Calls (on x86)。)

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

还要注意,x86寄存器的“自然”顺序是EAX、ECX、EDX、EBX,这是它们在机器码中的数字代码的顺序,也是pusha/popa使用的顺序。请参见为什么前四个x86 GPR的命名如此不直观?

有任何线索为什么“syscall”约定在第4个参数上与用户空间约定('r10' vs 'rcx')不同吗?你提出了一个很好的观点,但是它们可能更快,除非有这种差异需要对具有4、5或6个参数的调用进行1、2或3个参数的洗牌。 - BeeOnRope
2
@BeeOnRope:syscall指令本身会使用保存的RIPRFLAGS值覆盖rcxr11寄存器。这在调用约定的选择中是一个非常小的因素,因此像需要REX高寄存器与无需REX的寄存器(如ecx)之类的考虑权重更大。如果约定使用rbx而不是rcx作为传递参数/调用破坏的“低”寄存器,则需要安全/恢复变量移位。您还需要一些调用保留的低寄存器(RBP和RBX,因为它是“最不特殊”的低寄存器,主要用于CPUID / CMPXCHG16B(最初甚至不存在))。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接