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

标签 assembly x86 x86-64 cpu-registers calling-convention

有一个问题困扰着我。

所以 ... 为什么在 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 版本将 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 传递寄存器。 (请参阅 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/

相关文章:

assembly - 将 ARM 程序集转换为 gnu 程序集

assembly - 为 x86 程序集绘制堆栈框架

Linux 上的 C 内联汇编,将字符串从堆栈写入标准输出

assembly - 在 x86 上执行 cli 后丢失中断会发生什么?

c - rdtsc,循环太多

assembly - NASM printf 打印 64 位双段错误

x86-64 - CPU 缓存条目包含物理地址还是虚拟地址?

gcc - 基于 x86-64 汇编的索引寻址和段错误

performance - 现代 x86 成本模型

c++ - 使用 Intel Pin 时跟踪不匹配的 CALL 和 RET 指令数