ABI(Application Binary Interface) 是指二进制模块之间的接口,比如用户程序调用库函数或系统调用。 通过约定ABI,达到在二进制级别的互操作。相比一般而言的API,ABI处于更底层。

ABI 主要包含wikipedia

  • 处理器架构相关,例如指令集,内存访问(分页,栈,虚拟内存,对齐等)
  • 调用约定:例如函数参数如何传递,寄存器如何使用
  • 系统调用
  • 完整的操作系统ABI还应该包括目标文件,二进制文件等的格式

不同架构下的ABI标准:

x86_64

Microsoft x64 ABI

System V ABI 下载

Calling Convention

调用约定是ABI的重要组成部分。例如当C语言用户程序调用库函数时,调用方与被调用方可以是在不同机器使用不同的编译器编译的。两者必须遵循一致的调用约定才能正确调用。 以x86_64为例,上述ms和sysv的ABI各自定义了调用约定。简单对比如下:

  sysv abi ms abi
参数传递 前六个整形或指针参数通过RDI, RSI, RDX, RCX, R8, R9传递;剩余参数通过栈 前四个参数通过寄存器传递,例如整型通过rcx, rdx, r8, r9;剩余参数通过栈
返回值 rax(最多64位)或rax和rdx(最多128位) rax(最多64位)
callee-saved registers RBP, RBX, RSP, R12, R13, R14, R15 RBX, RBP, RDI, RSI, RSP, R12, R13, R14, R15, XMM6:XMM15
caller-saved registers RAX, RCX, RDX, RSI, RDI, R8, R9, R10, R11, XMM0:XMM16 RAX, RCX, RDX, R8, R9, R10, R11, XMM0:XMM5

Note:

上述表格内容不完整,具体细节请参考ABI文档。

考虑如下C程序,__attribute__((sysv_abi))指定使用哪种ABI,gcc和clang都支持该语法。

__attribute__((sysv_abi)) int foo(int x, int y);

__attribute__((ms_abi)) void bar(int x, int y){
    int a = 3;
    foo(a,4);
}

void main(void) {
    bar(1, 2);
    foo(4, 5);
}

gcc 编译对应的汇编结果如下,可以看到参数传递,返回值等都按照上述calling convention生成了对应的汇编代码。

save-restore寄存器

一个值得注意的点是,生成的代码中,对于caller/callee-saved register处理时,为了提高效率,并不会save-restore所有寄存器,而只是保存必要的寄存器。 例如在main中,并没有保存sysv abi中定义的caller-saved register。 因为编译器可以推断出来,即使foo, bar中修改了这些寄存器(如RCX)的值,对程序的正确性也没有影响,因为:

  1. main函数中,在调用barfoo后没有使用到这些寄存器。
  2. main的caller如果使用了RCX,它需要自己save-restore RCX的值,因为RCX是caller-saved。

但是我们可以看到bar的汇编代码中在调用foo前后分别对RDI, RSI, XMM6:XMM15这些寄存器做了save和restore的操作。原因如下: 以RDI为例,RDI在sysv abi中是caller saved,但在ms abi中是callee saved。 所以如果foo中修改了RDI,它并不会在返回前恢复RDI的值,所以bar无法确定调用foo后RDI的值有没有被修改。 而对于bar来说,它本身要依照ms abi的约定,RDI是callee saved,main调用bar后预期是RDI没有修改。 为了保证RDI在bar返回时没有被修改,它就需要保存并恢复RDI的值。

同理其他寄存器。

bar:
        pushq   %rbp
        movq    %rsp, %rbp
        pushq   %rdi
        pushq   %rsi
        subq    $176, %rsp
        movaps  %xmm6, 16(%rsp)
        movaps  %xmm7, 32(%rsp)
        movaps  %xmm8, 48(%rsp)
        movaps  %xmm9, -128(%rbp)
        movaps  %xmm10, -112(%rbp)
        movaps  %xmm11, -96(%rbp)
        movaps  %xmm12, -80(%rbp)
        movaps  %xmm13, -64(%rbp)
        movaps  %xmm14, -48(%rbp)
        movaps  %xmm15, -32(%rbp)
        movl    %ecx, 16(%rbp)
        movl    %edx, 24(%rbp)
        movl    $3, -180(%rbp)
        movl    -180(%rbp), %eax
        movl    $4, %esi
        movl    %eax, %edi
        call    foo
        nop
        movaps  16(%rsp), %xmm6
        movaps  32(%rsp), %xmm7
        movaps  48(%rsp), %xmm8
        movaps  -128(%rbp), %xmm9
        movaps  -112(%rbp), %xmm10
        movaps  -96(%rbp), %xmm11
        movaps  -80(%rbp), %xmm12
        movaps  -64(%rbp), %xmm13
        movaps  -48(%rbp), %xmm14
        movaps  -32(%rbp), %xmm15
        addq    $176, %rsp
        popq    %rsi
        popq    %rdi
        popq    %rbp
        ret
main:
        pushq   %rbp
        movq    %rsp, %rbp
        subq    $32, %rsp
        movl    $2, %edx
        movl    $1, %ecx
        call    bar
        addq    $32, %rsp
        movl    $5, %esi
        movl    $4, %edi
        call    foo
        nop
        leave
        ret

其他

在kernel开发中,根据处理器规则,有些函数可能无法遵循上述调用约定,如:

  • 中断处理函数:因为中断可以在任何指令处发生和处理,所以中断处理函数返回前需要保证恢复所有可能被修改的寄存器
  • 系统调用:x86_64中如果使用syscall/sysret实现系统调用,rcx, r11的值会被修改。Linux下的实现可参考Linux下系统调用实现

References