为什么 Assembly x86_64 系统调用参数不像 i386 那样按字母顺序排列
Why Assembly x86_64 syscall parameters are not in alphabetical order like i386
有一个问题困扰着我。
那么...为什么在 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
他们这样做是出于特定原因吗?我在这里没看到什么吗?
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 传递寄存器。 (请参阅 查看系统调用处理的一些内核方面。主要是关于 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?。
有一个问题困扰着我。
那么...为什么在 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
他们这样做是出于特定原因吗?我在这里没看到什么吗?
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 传递寄存器。 (请参阅 int 0x80
处理程序,但我想我提到了 64 位 syscall
处理程序并且它分派到table 个直接来自 asm 的函数。)
syscall
指令本身
让前 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?。