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


147

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

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


Unix调用约定没有“标准”。对于Linux,当然可以,但是我确定Solaris,OpenBSD,Linux和Minix可能具有至少至少略有不同的调用约定,并且它们都是unix。
Earlz 2010年

2
这不是完全正确的-大多数机器类型都可以使用一组UNIX ABI,这使C编译器可以实现互操作性。C ++编译器有一个更大的问题。
乔纳森·莱夫勒

1
你们俩都是正确的。我正在寻找FreeBSD和Linux。
爪子

如果答案包含有关在整个系统调用中保留哪些寄存器的信息,我将不胜感激。当然,堆栈指针是(除非在__NR_clone调用中以受控方式更改),但是其他指针是吗?
艾伯特·范德霍斯特

@AlbertvanderHorst:是的,我刚刚用32位的详细信息更新了Wiki答案。64位已经很准确:rcx和r11因工作方式而被破坏sysret,并且rax被返回值替换。所有其他寄存器都保留在amd64上。
彼得·科德斯

Answers:


230

进一步阅读以下任何主题:Linux系统调用权威指南


我在Linux上使用GNU汇编器(气体)验证了这些。

内核接口

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

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

返回值为in %eax。所有其他寄存器(包括EFLAGS)都保留在内int $0x80

我从Linux Assembly Tutorial中摘录了以下片段,但对此感到怀疑。如果有人可以举一个例子,那就太好了。

如果有六个以上的参数,则 %ebx必须包含参数列表存储的内存位置-但不必担心,因为您不太可能使用超过六个参数的syscall。

有关示例和更多内容,请参见http://www.int80h.org/bsdasm/#alternate-calling-convention。i386 Linux的Hello World的另一个示例使用int 0x80Hello,Linux系统调用中的汇编语言世界吗?

有一种进行32位系统调用的更快方法:使用sysenter。内核将页面的内存映射到每个进程(vDSO)中,而舞动的用户空间端sysenter则必须与内核合作才能找到返回地址。用于寄存器映射的Arg与for相同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相似但不同。TODO:检查* BSD的功能。

请参阅《System V应用程序二进制接口AMD64体系结构处理器补充》中的“ A.2 AMD64 Linux内核约定”部分。可从ABI维护人员的存储库中的此页面链接找到最新版本的i386和x86-64 System V psABI。。(另请参见 标记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. 系统调用仅限于六个参数,没有参数直接传递到堆栈上。
  5. 从系统调用返回,寄存器%rax包含系统调用的结果。-4095到-1之间的值表示错误,它是-errno
  6. 仅将类INTEGER或类MEMORY的值传递给内核。

请记住,这是从ABI特定于Linux的附录中获得的,即使对于Linux来说,它的信息内容也不是规范性的。(但实际上是准确的。)

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

用户界面:函数调用

x86-32函数调用约定:

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

i386 System V ABI的现代版本(在Linux上使用)要求%espa之前的16字节对齐call,就像x86-64 System V ABI始终需要的一样。允许被调用者假定并使用SSE 16字节加载/存储在未对齐时发生故障。但是从历史上看,Linux只需要4字节堆栈对齐,因此即使8字节也要保留自然对齐的空间,这需要额外的工作。double东西。

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


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

x86-64 System V在寄存器中传递了args,这比i386 System V的堆栈args约定效率更高。它避免了等待时间和将args存储到内存(高速缓存)然后再将它们重新加载到被调用方中的额外指令。因为有更多可用的寄存器,所以这种方法行之有效,并且对于延迟和无序执行至关重要的现代高性能CPU更好。(i386 ABI很旧)。

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

有关完整的信息,请参考:System V应用程序二进制接口AMD64体系结构处理器补充的 “ 3.2函数调用序列” ,部分内容为:

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

  1. 如果该类是MEMORY,则在堆栈上传递参数。
  2. 如果该类是INTEGER,则使用序列%rdi,%rsi,%rdx,%rcx,%r8和%r9的下一个可用寄存器

用来按顺序将整数/指针(即INTEGER类)参数传递给汇编中任何libc函数%rdi, %rsi, %rdx, %rcx, %r8 and %r9的寄存器也是如此。%rdi用于第一个INTEGER参数。%rsi代表第二,%rdx代表第三,依此类推。然后call应该给出指示。执行时,堆栈(%rsp)必须对齐16B call

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

前8个浮点args在%xmm0-7中传递,随后在堆栈中传递。没有保留呼叫的向量寄存器。(一个包含FP和整数参数的函数的寄存器总数可以超过8个。)

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

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


请注意,Windows x64函数调用约定与x86-64 System V有多个显着不同,例如必须由调用者保留的阴影空间(而不是红色区域)和保留了呼叫的xmm6-xmm15。arg进入哪个寄存器的规则也非常不同。


1
在linux 32中,“保留除ax bx cd dx si di bp之外的所有寄存器”。我
阿尔伯特·

在amd64上,如果有6个以上的参数并将它们传递到堆栈上,则谁负责在调用,调用方或被调用方之后清理堆栈?
尼古拉斯

1
@Nicolás:调用方清理堆栈。我用有关函数调用约定的更多详细信息更新了答案。
彼得·科德斯

1
如果您int 0x80在64位代码中使用Linux的ABI,则会发生以下情况:stackoverflow.com/questions/46087730/…。它将r8-r11归零,并且工作原理与在32位进程中运行时完全相同。在该问答中,我有一个示例说明它正常工作,或者由于截断指针而失败。而且,我还深入研究了内核源代码,以显示其为何如此运行。
彼得·科德斯

1
@EvanCarroll:该代码段(带引号的文字)在给出的链接上给出了Linux汇编教程,该链接专门在第4.3
Michael Petch,

14

也许您正在寻找x86_64 ABI?

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


5
实际上,我只想要系统调用约定。esp for UNIX(FreeBSD)
抓取

3
@claws:系统调用约定是ABI的一部分。
乔纳森·莱夫勒

1
是的 我已经进入每个操作系统的内核开发中心,并询问了他们。他们告诉我要调查源并找出答案。我不了解没有记录的东西,他们如何才能开始开发?因此,我从收集到的信息中添加了一个答案,希望其他人填写其余详细信息。
爪子

@JonathanLeffler链接现在似乎不起作用。如果您在访问链接时也遇到问题,可以更新一下吗?
Ajay Brahmakshatriya

@AjayBrahmakshatriya:感谢大家的注意;我已经添加了指向Wayback Machine记录的链接。整个x86-64.org网站没有任何数据响应。
乔纳森·莱夫勒

11

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


完全不重要。如果问题的发布者与一般的ABI转换相同,则不会要求Linux中的64位syscall调用约定。
阿尔伯特·范德霍斯特

6

Linux内核5.0源注释

我知道x86细节在下面arch/x86,而syscall东西在下面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.
 */

对于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在处定义了x86_64 syscalls,sysdeps/unix/sysv/linux/x86_64/sysdep.h其中包含一些有趣的代码,例如:

/* 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 : //zh.wikipedia.org/wiki/X86_calling_conventions#List_of_x86_calling_conventions

快速提醒您:

有关从头开始的最小的可运行示例,请参见以下答案:如何通过内联汇编中的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上游

aarch64

我在以下位置显示了一个最小的可运行用户态示例:https: //reverseengineering.stackexchange.com/questions/16917/arm64-syscalls-table/18834#18834 TODO grep内核代码应该很容易。


1
"cc"撞是不必要的:Linux的系统调用保存/恢复RFLAGS(在syscall/ sysret指令做,使用R11,内核不修改保存R11 / RFLAGS不是通过其他ptrace调试器的系统调用。)不,它曾经重要,因为"cc"撞的对于GNU C扩展asm中的x86 / x86-64是隐式的,因此将其遗漏将无法获得任何收益。
彼得·科德斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.