【问题标题】:Why Assembly x86_64 syscall parameters are not in alphabetical order like i386为什么 Assembly x86_64 系统调用参数不像 i386 那样按字母顺序排列
【发布时间】:2018-05-20 11:22:39
【问题描述】:

有一个问题困扰着我。

所以...为什么在 x86_32参数 是在我觉得 按字母顺序 中的寄存器中传递的(eax, @987654324 @、edxesi)和排名顺序(esiediebp

+---------+------+------+------+------+------+------+
| 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 中,它匹配函数调用约定,因此系统调用包装函数是轻量级的。在 i386 中,IDK 为什么它使用这种不方便的设置(ebx 是保留调用的,所以几乎每个系统调用包装器都需要保存/恢复 ebx。)
  • @PeterCordes 函数调用约定旨在减少在实现memcpyrep movsb 时的寄存器混洗量。
  • 函数调用约定?也许我听起来像个菜鸟,也许是因为我是……但是什么是函数调用约定?那是大会的事情吗?
  • @ApostolisAnastasiou:通常你只会说“调用约定”,但我们需要区分函数的调用约定和系统调用的调用约定。有关两者的信息,请参阅stackoverflow.com/questions/2535989/…。 (这个问题可能与此链接或我之前评论中的链接重复...)

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


【解决方案1】:

x86-64 System V ABI 旨在最大限度地减少 SPECint 中的指令数(并在一定程度上减少代码大小),该 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 rdirdx:rax = rax*rdi。 rep-string 指令隐式使用 RDI、RSI 和 RCX,尽管它们通常不值得使用。

事实证明,选择 arg 传递寄存器以便将其 args 传递给 memcpy 的函数可以内联它,因为 rep movs 在 Jan Hubicka 使用的度量标准中很有用,因此 rdirsi 被选为前两个参数。但是将rcx 留到第4 个参数之前使用会更好,因为变量计数移位需要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 分派到一个函数表.)

syscall 指令本身 destroys RCX and R11(以节省用户空间 RIP 和 RFLAGS 而无需微码来设置内核堆栈)因此约定不能相同,除非用户空间约定避免 RCX 和 R11。但是 RCX 是一个方便的寄存器,它的低半部分可以在没有 REX 前缀的情况下使用,因此这可能比将它作为像 R11 这样的调用破坏的纯刮擦更糟糕。此外,用户空间约定使用 R10 作为具有一流嵌套函数(不是 C/C++)的语言的“静态链”指针。

让前 4 个 args 能够避免使用 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:请参阅 The Definitive Guide to Linux System Calls (on x86) 了解有关 vDSO 和大量其他内容的更多信息。)

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

还要注意 x86 寄存器的“自然”顺序是 EAX、ECX、EDX、EBX,根据它们在机器码中的数字代码,以及 pusha / popa 使用的顺序。见Why are first four x86 GPRs named in such unintuitive order?

【讨论】:

  • 知道为什么syscall 约定在第 4 个参数处与用户空间约定不同(r10rcx)?您对简单的包装器提出了一个很好的观点,但它们可能会更快,除了这种差异需要为具有 4、5 或 6 个参数的调用改组 1、2 或 3 个参数。
  • @BeeOnRope:syscall 指令本身实际上破坏了 rcxr11 与保存的 RIPRFLAGS 值。在调用约定的选择中,这是一个非常次要的因素,因此诸如 REX 需要的高 regs 与无 REX 寄存器(如 ecx)之类的考虑超过了它。如果约定使用 rbx 而不是 rcx 作为 arg-passing/call-clobbered "low" reg,则需要使用变量移位来保护/恢复它。您还需要一些保留调用的低寄存器(RBP 和 RBX,因为它是“最不特殊”的低寄存器,主要是 CPUID / CMPXCHG16B(最初甚至不存在))
猜你喜欢
  • 2015-04-05
  • 1970-01-01
  • 1970-01-01
  • 2020-03-29
  • 1970-01-01
  • 1970-01-01
  • 2012-06-22
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多