i386 和 x86-64 上 UNIX 和 Linux 系统调用(和用户空间函数)的调用约定是什么

What are the calling conventions for UNIX & Linux system calls (and user-space functions) on i386 and x86-64

提问人:claws 提问时间:3/29/2010 最后编辑:Peter Cordesclaws 更新时间:12/7/2020 访问量:175511

问:

以下链接解释了 UNIX(BSD 风格)和 Linux 的 x86-32 系统调用约定:

但是 UNIX 和 Linux 上的 x86-64 系统调用约定是什么?

Linux 程序集 x86-64 调用约 定 abi x86

评论

0赞 Earlz 3/29/2010
Unix 调用约定没有“标准”。当然是 linux,但我敢肯定 Solaris、OpenBSD、Linux 和 Minix 可能有不同的,至少略有不同的调用约定,它们都是 unix。
2赞 Jonathan Leffler 3/29/2010
这并不完全正确 - 有一组 UNIX ABI 可用于大多数机器类型,它允许 C 编译器实现互操作性。C++ 编译器有一个更大的问题。
1赞 claws 3/29/2010
你们俩都是对的。我正在寻找FreeBSD和Linux。
0赞 Albert van der Horst 1/22/2016
如果答案包含有关跨系统调用保留哪些寄存器的信息,我将不胜感激。当然,堆栈指针是,(除非在__NR_clone调用中以受控方式更改),但它们的其他指针是吗?
1赞 Peter Cordes 2/2/2016
另请参阅 stackoverflow.com/a/2709009/224132entry_64.S的这篇文章

答:

16赞 Jonathan Leffler 3/29/2010 #1

也许您正在寻找 ABI x86_64?

如果这不是您想要的,请在您喜欢的搜索引擎中使用“x86_64 abi”来查找替代参考。

评论

5赞 claws 3/29/2010
实际上,我只想要系统调用约定。esp for UNIX (FreeBSD)
3赞 Jonathan Leffler 3/29/2010
@claws:系统调用约定是 ABI 的一部分。
1赞 claws 3/29/2010
是的。我去过每个操作系统的内核开发 irc 并询问他们。他们告诉我要调查来源并找出答案。我不明白,如果不记录东西,他们怎么能开始开发?因此,我从收集到的信息中添加了一个答案,希望其他人能够填写其余的详细信息。
0赞 Ajay Brahmakshatriya 11/25/2018
@JonathanLeffler链接现在似乎不起作用。如果您在访问链接时也遇到问题,请您更新一下吗?
0赞 Jonathan Leffler 11/25/2018
@AjayBrahmakshatriya:谢谢你的提醒;我添加了指向 Wayback Machine 记录的链接。整个 x86-64.org 网站没有提供任何数据。
298赞 20 revs, 9 users 49%claws #2

有关任何主题的进一步阅读,请访问:Linux 系统调用权威指南


我在 Linux 上使用 GNU 汇编程序 (gas) 验证了这些。

内核接口

x86-32 又名 i386 Linux 系统调用约定:

在 x86-32 中,Linux 系统调用的参数使用寄存器传递。 为了syscall_number。%ebx、%ecx、%edx、%esi、%edi、%ebp 用于将 6 个参数传递给系统调用。%eax

返回值位于 中。所有其他寄存器(包括 EFLAGS)都保留在 .%eaxint $0x80

我从 Linux Assembly 教程中获取了以下片段,但我对此表示怀疑。如果有人能举个例子,那就太好了。

如果有超过六个参数,则必须包含内存 参数列表的位置 被存储 - 但不要担心这个 因为你不太可能使用 具有六个以上的系统呼叫 参数。%ebx

有关示例和更多阅读内容,请参阅 http://www.int80h.org/bsdasm/#alternate-calling-convention。i386 Linux 的 Hello World 的另一个示例使用:Hello, world in assembly language with Linux system calls?int 0x80

有一种更快的方法可以进行 32 位系统调用:使用 。内核将内存页映射到每个进程(vDSO)中,用户空间端必须与内核合作才能找到返回地址。寄存器映射的 Arg 与 的 相同。通常应调用 vDSO,而不是直接使用。(有关链接和调用 vDSO 的信息,以及有关 的详细信息以及与系统调用相关的所有其他信息,请参阅 Linux 系统调用权威指南sysentersysenterint $0x80sysentersysenter

x86-32 [免费|打开|网络|DragonFly]BSD UNIX 系统调用约定:

参数在堆栈上传递。将参数(最后一个参数先推送到堆栈)。然后推送额外的 32 位虚拟数据(它实际上不是虚拟数据,有关详细信息,请参阅以下链接),然后给出系统调用指令int $0x80

http://www.int80h.org/bsdasm/#default-calling-convention


x86-64 Linux 系统调用约定:

(注意:x86-64 Mac OS X 与 Linux 类似但不同。TODO:检查 *BSD 是做什么的)

请参阅 System V 应用程序二进制接口 AMD64 体系结构处理器补充的“A.2 AMD64 Linux 内核约定”部分。最新版本的 i386 和 x86-64 System V psABI 可以从此页面的 ABI 维护者存储库中找到链接。(另请参阅 标签 wiki,了解最新的 ABI 链接和许多其他关于 x86 asm 的好东西。

以下是本节的片段:

  1. 用户级应用程序用作整数寄存器,用于传递 序列 %rdi、%rsi、%rdx、%rcx、 %r8 和 %r9。内核接口使用 %rdi、%rsi、%rdx、%r10、%r8 和 %r9。
  2. 系统调用是通过 syscall 指令完成的。这会破坏 %rcx 和 %r11 以及 %rax 返回值,但会保留其他寄存器。
  3. 系统调用的编号必须在寄存器 %rax 中传递。
  4. 系统调用限制为 6 个参数,不传递任何参数 直接在堆栈上。
  5. 从 syscall 返回后,寄存器 %rax 包含 系统调用。介于 -4095 和 -1 之间的值表示 一个错误,它是.-errno
  6. 只有类 INTEGER 或类 MEMORY 的值才会传递给内核。

请记住,这是来自 ABI 的特定于 Linux 的附录,即使对于 Linux,它也是信息性的,而不是规范性的。(但事实上它是准确的。

此 32 位 ABI 可用于 64 位代码(但强烈建议不推荐)。如果在 64 位代码中使用 32 位 int 0x80 Linux ABI,会发生什么情况?它仍然将其输入截断为 32 位,因此它不适合指针,并且它将 r8-r11 归零。int $0x80

用户界面:函数调用

x86-32 函数调用约定:

在 x86-32 中,参数在堆栈上传递。最后一个参数首先被推送到堆栈,直到所有参数都完成,然后执行指令。这用于从程序集调用 Linux 上的 C 库 (libc) 函数。call

现代版本的 i386 System V ABI(在 Linux 上使用)需要 16 字节对齐 ,就像 x86-64 System V ABI 一直要求的那样。被调用者可以假设并使用 SSE 16 字节加载/存储,该错误在未对齐时出现。但从历史上看,Linux 只需要 4 字节的堆栈对齐,因此即使是 8 字节或其他东西,也需要额外的工作来保留自然对齐的空间。%espcalldouble

其他一些现代 32 位系统仍然不需要超过 4 字节的堆栈对齐。


x86-64 System V 用户空间函数调用约定:

x86-64 System V 在寄存器中传递 args,这比 i386 System V 的堆栈 args 约定更有效。它避免了将参数存储到内存(缓存)然后将它们重新加载到被调用方中的延迟和额外指令。这很有效,因为有更多的寄存器可用,并且更适合延迟和无序执行很重要的现代高性能 CPU。(i386 ABI 非常老旧)。

在这种机制中:首先,参数被划分为类。每个参数的类决定了它传递给被调用函数的方式。

有关完整信息,请参阅:System V 应用程序二进制接口 AMD64 架构处理器补充的“3.2 函数调用序列”,其中部分内容如下:

对参数进行分类后,寄存器将被分配(在 从左到右的顺序)进行如下传递:

  1. 如果类是 MEMORY,则在堆栈上传递参数。
  2. 如果该类是 INTEGER,则 使用序列 %rdi、%rsi、%rdx、%rcx、%r8 和 %r9

寄存器的顺序也是如此,用于将整数/指针(即 INTEGER 类)参数从汇编传递给任何 libc 函数。%rdi 用于第一个 INTEGER 参数。%rsi 表示第 2 个,%rdx 表示第 3 个,依此类推。然后应该给出指导。堆栈 () 在执行时必须与 16B 对齐。%rdi, %rsi, %rdx, %rcx, %r8 and %r9call%rspcall

如果 INTEGER 参数超过 6 个,则在堆栈上传递第 7 个 INTEGER 参数及更高版本。(呼叫者弹出,与 x86-32 相同。

前 8 个浮点参数在 %xmm0-7 中传递,稍后在堆栈中传递。没有调用保留的向量寄存器。(混合了 FP 和整数参数的函数可以有 8 个以上的寄存器参数。

可调参数函数(printf)始终需要 = FP 寄存器参数的数量。%al

对于何时将结构打包到寄存器中(返回时)和内存中,有规则。有关详细信息,请参阅 ABI,并检查编译器输出以确保您的代码与编译器在如何传递/返回某些内容方面达成一致。rdx:rax


请注意,Windows x64 函数调用约定与 x86-64 System V 存在多个显著差异,例如调用方必须保留的阴影空间(而不是红色区域)和调用保留的 xmm6-xmm15。对于哪个 arg 进入哪个寄存器的规则非常不同。

评论

1赞 Albert van der Horst 3/16/2016
在 linux 32 中,“除了 ax bx cd dx si di bp 之外的所有寄存器都被保留”。我想不出任何......
1赞 Peter Cordes 9/3/2017
@Nicolás:调用方清理堆栈。我更新了答案,提供了有关函数调用约定的更多详细信息。
2赞 Peter Cordes 9/7/2017
如果您在 64 位代码中使用 Linux 的 ABI,则会发生以下情况:stackoverflow.com/questions/46087730/...。它将 r8-r11 归零,工作方式与在 32 位进程中运行时完全相同。在该问答中,我有一个示例,显示它工作或因截断指针而失败。我还深入研究了内核源代码,以说明它为什么会以这种方式运行。int 0x80
1赞 Michael Petch 10/26/2018
@EvanCarroll:代码片段(引用的文本)位于 Linux Assembly Tutorial 的链接中,特别是在第 4.3 节 Linux 系统调用
1赞 JCWasmx86 9/15/2020
@r0ei 它与 64 位寄存器相同。它是 ax 而不是 rax,它是 bx 而不是 rbx 等等。除非您有 16 位调用约定,否则还有其他方法可以传递参数。
13赞 Peter Teoh 6/12/2011 #3

调用约定定义在调用或被其他程序调用时如何在寄存器中传递参数。这些约定的最佳来源是为每个硬件定义的 ABI 标准。为了便于编译,用户空间和内核程序也使用相同的 ABI。Linux/Freebsd 在 x86-64 上遵循相同的 ABI,在 32 位上遵循另一组 ABI。但是 x86-64 ABI for Windows 与 Linux/FreeBSD 不同。通常,ABI 不会区分系统调用与普通的“函数调用”。 也就是说,这里是x86_64调用约定的一个特定示例,对于 Linux 用户空间和内核都是相同的:http://eli.thegreenplace.net/2011/09/06/stack-frame-layout-on-x86-64/(注意参数的顺序 a、b、c、d、e、f):

A good rendering of calling conventions vs registers usage

性能是这些 ABI 的原因之一(例如,通过寄存器传递参数而不是保存到内存堆栈中)

对于 ARM,有各种 ABI:

http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.subset.swdev.abi/index.html

https://developer.apple.com/library/ios/documentation/Xcode/Conceptual/iPhoneOSABIReference/iPhoneOSABIReference.pdf

ARM64 约定:

http://infocenter.arm.com/help/topic/com.arm.doc.ihi0055b/IHI0055B_aapcs64.pdf

对于 PowerPC 上的 Linux:

http://refspecs.freestandards.org/elf/elfspec_ppc.pdf

http://www.0x04.net/doc/elf/psABI-ppc64.pdf

对于嵌入式,有 PPC EABI:

http://www.freescale.com/files/32bit/doc/app_note/PPCEABI.pdf

本文档很好地概述了所有不同的约定:

http://www.agner.org/optimize/calling_conventions.pdf

评论

1赞 Albert van der Horst 1/22/2016
完全不是重点。这个问题的发布者不会要求 linux 中的 64 位 syscall 调用约定,如果它与一般的 ABI 转换相同。
17赞 Ciro Santilli OurBigBook.com 3/2/2019 #4

Linux 内核 5.0 源代码注释

我知道 x86 的细节在 下,而 syscall 的东西在 .因此,该目录中的快速操作将我引导至arch / x86 / entry / entry_64.Sarch/x86arch/x86/entrygit grep rdi

/*
 * 64-bit SYSCALL instruction entry. Up to 6 arguments in registers.
 *
 * This is the only entry point used for 64-bit system calls.  The
 * hardware interface is reasonably well designed and the register to
 * argument mapping Linux uses fits well with the registers that are
 * available when SYSCALL is used.
 *
 * SYSCALL instructions can be found inlined in libc implementations as
 * well as some other programs and libraries.  There are also a handful
 * of SYSCALL instructions in the vDSO used, for example, as a
 * clock_gettimeofday fallback.
 *
 * 64-bit SYSCALL saves rip to rcx, clears rflags.RF, then saves rflags to r11,
 * then loads new ss, cs, and rip from previously programmed MSRs.
 * rflags gets masked by a value from another MSR (so CLD and CLAC
 * are not needed). SYSCALL does not save anything on the stack
 * and does not change rsp.
 *
 * Registers on entry:
 * rax  system call number
 * rcx  return address
 * r11  saved rflags (note: r11 is callee-clobbered register in C ABI)
 * rdi  arg0
 * rsi  arg1
 * rdx  arg2
 * r10  arg3 (needs to be moved to rcx to conform to C ABI)
 * r8   arg4
 * r9   arg5
 * (note: r12-r15, rbp, rbx are callee-preserved in C ABI)
 *
 * Only called from user space.
 *
 * When user can change pt_regs->foo always force IRET. That is because
 * it deals with uncanonical addresses better. SYSRET has trouble
 * with them due to bugs in both AMD and Intel CPUs.
 */

对于 arch/x86/entry/entry_32.S 的 32 位:

/*
 * 32-bit SYSENTER entry.
 *
 * 32-bit system calls through the vDSO's __kernel_vsyscall enter here
 * if X86_FEATURE_SEP is available.  This is the preferred system call
 * entry on 32-bit systems.
 *
 * The SYSENTER instruction, in principle, should *only* occur in the
 * vDSO.  In practice, a small number of Android devices were shipped
 * with a copy of Bionic that inlined a SYSENTER instruction.  This
 * never happened in any of Google's Bionic versions -- it only happened
 * in a narrow range of Intel-provided versions.
 *
 * SYSENTER loads SS, ESP, CS, and EIP from previously programmed MSRs.
 * IF and VM in RFLAGS are cleared (IOW: interrupts are off).
 * SYSENTER does not save anything on the stack,
 * and does not save old EIP (!!!), ESP, or EFLAGS.
 *
 * To avoid losing track of EFLAGS.VM (and thus potentially corrupting
 * user and/or vm86 state), we explicitly disable the SYSENTER
 * instruction in vm86 mode by reprogramming the MSRs.
 *
 * Arguments:
 * eax  system call number
 * ebx  arg1
 * ecx  arg2
 * edx  arg3
 * esi  arg4
 * edi  arg5
 * ebp  user stack
 * 0(%ebp) arg6
 */

glibc 2.29 Linux x86_64系统调用实现

现在让我们通过查看主要的 libc 实现来作弊,看看它们在做什么。

在我写这个答案时,还有什么比研究我现在使用的 glibc 更好的呢?:-)

glibc 2.29 定义了 sysdeps/unix/sysv/linux/x86_64/sysdep.h x86_64系统调用,其中包含一些有趣的代码,例如:

/* The Linux/x86-64 kernel expects the system call parameters in
   registers according to the following table:

    syscall number  rax
    arg 1       rdi
    arg 2       rsi
    arg 3       rdx
    arg 4       r10
    arg 5       r8
    arg 6       r9

    The Linux kernel uses and destroys internally these registers:
    return address from
    syscall     rcx
    eflags from syscall r11

    Normal function call, including calls to the system call stub
    functions in the libc, get the first six parameters passed in
    registers and the seventh parameter and later on the stack.  The
    register use is as follows:

     system call number in the DO_CALL macro
     arg 1      rdi
     arg 2      rsi
     arg 3      rdx
     arg 4      rcx
     arg 5      r8
     arg 6      r9

    We have to take care that the stack is aligned to 16 bytes.  When
    called the stack is not aligned since the return address has just
    been pushed.


    Syscalls of more than 6 arguments are not supported.  */

和:

/* Registers clobbered by syscall.  */
# define REGISTERS_CLOBBERED_BY_SYSCALL "cc", "r11", "cx"

#undef internal_syscall6
#define internal_syscall6(number, err, arg1, arg2, arg3, arg4, arg5, arg6) \
({                                  \
    unsigned long int resultvar;                    \
    TYPEFY (arg6, __arg6) = ARGIFY (arg6);              \
    TYPEFY (arg5, __arg5) = ARGIFY (arg5);              \
    TYPEFY (arg4, __arg4) = ARGIFY (arg4);              \
    TYPEFY (arg3, __arg3) = ARGIFY (arg3);              \
    TYPEFY (arg2, __arg2) = ARGIFY (arg2);              \
    TYPEFY (arg1, __arg1) = ARGIFY (arg1);              \
    register TYPEFY (arg6, _a6) asm ("r9") = __arg6;            \
    register TYPEFY (arg5, _a5) asm ("r8") = __arg5;            \
    register TYPEFY (arg4, _a4) asm ("r10") = __arg4;           \
    register TYPEFY (arg3, _a3) asm ("rdx") = __arg3;           \
    register TYPEFY (arg2, _a2) asm ("rsi") = __arg2;           \
    register TYPEFY (arg1, _a1) asm ("rdi") = __arg1;           \
    asm volatile (                          \
    "syscall\n\t"                           \
    : "=a" (resultvar)                          \
    : "0" (number), "r" (_a1), "r" (_a2), "r" (_a3), "r" (_a4),     \
      "r" (_a5), "r" (_a6)                      \
    : "memory", REGISTERS_CLOBBERED_BY_SYSCALL);            \
    (long int) resultvar;                       \
})

我觉得这是不言自明的。请注意,这似乎被设计为与常规 System V AMD64 ABI 函数的调用约定完全匹配: https://en.wikipedia.org/wiki/X86_calling_conventions#List_of_x86_calling_conventions

快速提醒 clobbers:

有关从头开始的显式最小可运行示例,请参阅以下答案:如何在内联程序集中通过 syscall 或 sysenter 调用系统调用?

手动在程序集中进行一些系统调用

不是很科学,但很有趣:

  • x86_64.S

    .text
    .global _start
    _start:
    asm_main_after_prologue:
        /* write */
        mov $1, %rax    /* syscall number */
        mov $1, %rdi    /* stdout */
        mov $msg, %rsi  /* buffer */
        mov $len, %rdx  /* len */
        syscall
    
        /* exit */
        mov $60, %rax   /* syscall number */
        mov $0, %rdi    /* exit status */
        syscall
    msg:
        .ascii "hello\n"
    len = . - msg
    

    GitHub 上游

从 C 进行系统调用

下面是一个带有寄存器约束的示例:如何在内联程序集中通过 syscall 或 sysenter 调用系统调用?

aarch64的

我已经展示了一个最小可运行的用户空间示例: https://reverseengineering.stackexchange.com/questions/16917/arm64-syscalls-table/18834#18834 TODO grep 内核代码在这里,应该很容易。

评论

1赞 Peter Cordes 3/3/2019
clobber 是不必要的:Linux 系统调用 save/restore RFLAGS(/ 指令使用 R11 执行此操作,内核不会修改保存的 R11 / RFLAGS,除非通过调试器系统调用。这并不重要,因为在 GNU C Extended asm 中,x86 / x86-64 隐含了一个 clobber,所以你不能通过省略它来获得任何东西。"cc"syscallsysretptrace"cc"