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

198

2
这并不完全正确 - 大多数机器类型都有一组UNIX ABIs可用,允许C编译器实现互操作性。C ++编译器有更大的问题。 - Jonathan Leffler
1
你们两个都是正确的。我正在寻找FreeBSD和Linux。 - claws
如果答案包含有关系统调用中保留哪些寄存器的信息,我会很感激。当然,堆栈指针是被保留的(除非在__NR_clone调用中以受控方式更改),但还有其他的吗? - Albert van der Horst
@AlbertvanderHorst:是的,我刚刚更新了维基百科上有关32位的细节。64位已经很准确了:由于“sysret”工作的方式,rcx和r11会被破坏,同时rax会被替换为返回值。在amd64上,所有其他寄存器都将被保留。 - Peter Cordes
1
请参阅https://dev59.com/9XE85IYBdhLWcg3wl0jF#2709009和[entry_64.S的此篇文章](https://github.com/0xAX/linux-insides/blob/master/SysCall/syscall-2.md)。 - Peter Cordes
显示剩余2条评论
4个回答

298

任何这里涉及到的主题的进一步阅读材料:Linux系统调用权威指南


我在Linux上使用GNU汇编器(gas)进行了验证。

内核接口

x86-32也称i386 Linux系统调用约定:

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

返回值在%eax中。 所有其他寄存器(包括EFLAGS)在int $0x80之间保留。

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

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

例如,有更多的阅读材料,请参考http://www.int80h.org/bsdasm/#alternate-calling-convention。另一个使用int 0x80在i386 Linux上实现Hello World的示例:Hello, world in assembly language with Linux system calls?

有一种更快的方法来进行32位系统调用:使用sysenter。内核将一个内存页面映射到每个进程中(vDSO),其中包括用户空间的sysenter,需要与内核协作以便它能够找到返回地址。寄存器和参数映射与int $0x80相同。通常应该调用vDSO而不是直接使用sysenter。(有关链接和调用vDSO的信息,请参见Linux系统调用权威指南,了解更多有关sysenter和系统调用的其他信息。) x86-32 [Free|Open|Net|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 Application Binary Interface AMD64 Architecture Processor Supplement的“A.2 AMD64 Linux Kernel Conventions”部分。最新版本的i386和x86-64 System V psABIs可以在ABI维护者的repo中链接到此页面找到。(还请参阅标签wiki,了解有关x86汇编的最新ABI链接和其他有用信息。)

以下是该部分的摘录:

  1. 用户级应用程序使用整数寄存器传递顺序为%rdi,%rsi,%rdx,%rcx,%r8和%r9的参数。内核接口使用%rdi,%rsi,%rdx,%r10,%r8和%r9。
  2. 系统调用通过syscall指令完成。这会破坏%rcx和%r11以及%rax返回值,但其他寄存器将被保留。
  3. 系统调用的编号必须在寄存器%rax中传递。
  4. 系统调用仅限于六个参数,不会直接将任何参数传递到栈上。
  5. 从系统调用返回,寄存器%rax包含系统调用的结果。范围在-4095和-1之间的值表示错误,它是-errno
  6. 只有整数或内存的类值才会传递给内核。
记住,这是ABI的Linux专用附录,即使对于Linux来说,它也是信息性的而不是规范性的。(但事实上它是准确的。)
这个32位的int $0x80 ABI可以在64位代码中使用(但强烈不建议)。如果在64位代码中使用32位int 0x80 Linux ABI会发生什么? 它仍然将其输入截断为32位,因此不适合指针,并且将r8-r11清零。
用户界面:函数调用 x86-32函数调用约定: 在x86-32中,参数是通过堆栈传递的。最后一个参数首先被推到堆栈上,直到所有参数都完成,然后执行call指令。这用于从汇编语言调用Linux上的C库(libc)函数。
现代版本的i386 System V ABI(用于Linux)要求在call之前对%esp进行16字节对齐,就像x86-64 System V ABI一直要求的那样。被调用者可以假设并使用SSE 16字节加载/存储,这会导致未对齐的错误。但是历史上,Linux仅需要4字节的堆栈对齐,因此即使是8字节的double等自然对齐空间也需要额外的工作来保留。
其他一些现代32位系统仍然不需要超过4字节的堆栈对齐。

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

x86-64 System V在寄存器中传递参数,这比i386 System V的堆栈参数约定更有效。它避免了将参数存储到内存(缓存)中,然后在被调用方再次加载它们所需的延迟和额外指令。这很好地运作是因为有更多可用的寄存器,并且对于现代高性能CPU来说更好,其中延迟和乱序执行很重要。(i386 ABI非常古老)。

在这个新机制中:首先将参数分成类。每个参数的类别确定了它被传递到被调用函数的方式。

有关完整信息,请参考System V Application Binary Interface AMD64 Architecture Processor Supplement 的“3.2函数调用序列”,其中部分内容如下:

一旦参数被分类,寄存器按照从左到右的顺序分配如下(用于传递):
  1. 如果类是MEMORY,则在堆栈上传递参数。
  2. 如果类是INTEGER,则使用序列%rdi、%rsi、%rdx、%rcx、%r8和%r9中的下一个可用寄存器。
因此,%rdi、%rsi、%rdx、%rcx、%r8和%r9是按顺序用于将整数/指针(即INTEGER类)参数从汇编语言传递给任何libc函数的寄存器。 %rdi用于第一个INTEGER参数。%rsi用于第二个,%rdx用于第三个,以此类推。然后应该给出“call”指令。当“call”执行时,堆栈(%rsp)必须对齐16B。
如果有超过6个INTEGER参数,则第7个INTEGER参数及更高的参数将在堆栈上传递。(调用者弹出,与x86-32相同。)
前8个浮点参数通过%xmm0-7传递,后续参数通过栈传递。没有被调用保留的向量寄存器。(既有FP又有整数参数的函数可能会有超过8个总寄存器参数。)
可变参数函数(如printf)始终需要%al= FP 寄存器参数的数量。
有关何时将结构体打包到寄存器(rdx:rax返回)与内存中的规则。请参阅ABI以获取详细信息,并检查编译器输出以确保您的代码与编译器一致,了解应该如何传递/返回某些内容。
请注意,Windows x64函数调用约定与x86-64 System V有多个显着的区别,例如必须由调用者保留影子空间(而不是红色区域),以及保持调用的xmm6-xmm15。并且对于哪个参数放在哪个寄存器中有非常不同的规则。

1
在 Linux 32 中,“除了 ax、bx、cd、dx、si、di 和 bp 之外的所有寄存器都被保留”。我想不出任何... - Albert van der Horst
1
@Nicolás:调用者清理堆栈。我更新了答案,更详细地介绍了函数调用约定。 - Peter Cordes
2
如果您在64位代码中使用Linux的int 0x80 ABI,那么这正是发生的情况:https://dev59.com/fFYO5IYBdhLWcg3wRfd-。它会将r8-r11清零,并且在32位进程中运行时完全像原来一样。在那个问答中,我有一个示例展示了它的工作方式,或者失败并截断指针。我还深入研究了内核源代码,以展示它为什么会表现出这种行为。 - Peter Cordes
1
@EvanCarroll:引用文本的代码片段在链接中给出Linux汇编教程,具体在第4.3节“Linux系统调用”中。 - Michael Petch
1
@r0ei,与64位寄存器相同。它是ax而不是rax,它是bx而不是rbx等等。除非您有16位调用约定,否则还有其他传递参数的方式。 - JCWasmx86
显示剩余6条评论

17

Linux内核5.0源码注释

我知道x86的专属内容在arch/x86下,系统调用内容在arch/x86/entry中。因此,在该目录下快速执行git grep rdi指令,就可以找到arch/x86/entry/entry_64.S

/*
 * 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.
 */

对于32位系统,请参考arch/x86/entry/entry_32.S文件:

/*
 * 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系统调用实现

现在我们可以通过查看主要的C库实现来作弊,看看它们在做什么。

有什么比查看我正在编写答案时正在使用的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 的含义:

  • cc 表示标志寄存器。但 Peter Cordes 的评论 指出这在此处是不必要的。
  • memory 表示可以传递一个指针并在汇编中使用它来访问内存。

有关从头开始进行明确且最小化的可运行示例,请参见此答案: 如何通过内联汇编调用系统调用?

手动在汇编中进行一些系统调用

虽然不太科学,但很有趣:

  • 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进行系统调用

以下是一个具有寄存器约束的示例: 如何通过内联汇编调用系统调用或Sysenter?

aarch64

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


1
"cc" clobber是不必要的:Linux系统调用通过R11保存/恢复RFLAGS(syscall/sysret指令使用R11执行此操作,内核除了通过ptrace调试器系统调用之外不会修改保存的R11 / RFLAGS)。尽管如此,因为在GNU C扩展asm中,x86 / x86-64的"cc" clobber是隐含的,所以省略它并不能获得任何好处。 - Peter Cordes

16

也许你正在寻找x86_64 ABI?

如果这不完全符合您的需求,请在首选搜索引擎中使用“x86_64 abi”查找备选参考资料。


5
实际上,我只需要系统调用规范,尤其是针对UNIX(FreeBSD)的规范。 - claws
3
系统调用约定是ABI的一部分。 - Jonathan Leffler
1
是的,我已经去了每个操作系统的内核开发IRC,并向他们询问了这个问题。他们告诉我要查看源代码并找出答案。我不明白如果没有记录东西,他们怎么能开始开发呢?因此,我根据我收集到的信息添加了一个答案,希望其他人可以填写其余的细节。 - claws
@JonathanLeffler,链接似乎现在无法使用。如果您访问链接时也遇到问题,请更新一下链接,好吗? - Ajay Brahmakshatriya
@AjayBrahmakshatriya:感谢您提供的信息;我已经添加了到Wayback Machine记录的链接。整个http://www.x86-64.org/网站没有响应任何数据。 - Jonathan Leffler

13

调用约定定义了在调用或被其他程序调用时,参数如何在寄存器中传递。这些约定的最佳来源是为每个硬件定义的ABI标准形式。为了便于编译,用户空间和内核程序也使用相同的ABI。Linux/Freebsd遵循x86-64的相同ABI以及另一组32位的ABI。但是Windows的x86-64 ABI与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的顺序):

调用约定与寄存器使用的良好渲染

性能是这些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
完全离题了。如果64位syscall调用约定与一般ABI转换相同,问题的发布者不会在Linux中询问它。 - Albert van der Horst

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