tommwq的博客

调用约定笔记

· [tommwq@126.com]

对于常见的指令集,在指令层面没有所谓的“函数”概念,只有“子程序”概念。子程序是存储在“主程序”之外的一段指令。子程序通过call指令调用,通过ret指令返回。子程序可以使用内存、堆栈和寄存器。通常主程序会传递参数给子程序,子程序将执行结果返回给主程序。这些参数和返回值如何传递,可以由开发者决定。不过如果程序中同时使用了高级语言和汇编语言,为了让编译器生成的汇编代码可以正确的汇编和连接,必须采用一个双方都遵守的传递参数和返回值的方法。这就是调用约定。换言之,调用约定是为了保证了不同函数可以正确汇编和链接而设计的,主程序和子程序之间传递数据的方式。

从上面的说明可以看到,调用约定涉及参数和返回值两部分。早期的高级语言(比如C)只有一个返回值,因此返回值的传递也较为简单。下表总结了几个平台上返回值的传递方法:

平台 整型 结构体 浮点型
x86 eax eax st(0)
x86-64 rax rax xmm0
ARM R0 R0 R0
ARM64 R0 R0 R0

调用约定中比较复杂的是参数传递方法,其中又以x86平台的调用约定种类繁多。

x86参数传递

在32位x86系统上,由于寄存器数量较少,参数主要通过栈传递,也产生了比较多的调用方式。MSVC和GCC支持的32位调用约定有:

关键字 清理堆栈 参数传递
__cdecl caller 从右向左将参数压栈。
__clrcall n/a 将参数从左向右加入CLR表达式栈。
__stdcall callee 从右向左将参数压栈。
__fastcall callee 优先使用寄存器ecx和edx传递参数,然后才使用堆栈。
__thiscall callee 通过ecx传递this指针,其他参数通过栈传递。
__vectorcall callee 从右向左传递参数,优先使用寄存器ecx和edx,然后才使用堆栈。

通过栈传递参数时,栈的结构如下:

16(%ebp) third function parameter
12(%ebp) second function parameter
8(%ebp) first function parameter
4(%ebp) old %EIP (the function’s “return address”)
0(%ebp) old %EBP (previous function’s base pointer)
-4(%ebp) first local variable
-8(%ebp) second local variable
-12(%ebp) third local variable

x86_64 MSVC 调用约定

x86-64拥有比较多的寄存器,因此主要通过寄存器传递参数,调用约定也较为统一。MSVC编译器通过rcx、rdx、r8、r9传递前4个参数,其余参数同过栈传递,正如下面的例子:

void func1(int a, int b, int c, int d, int e);
// a: rcx, b: rdx, c: r8, d: r9, e: stack

如果函数的参数是结构体,编译器将这个结构体的指针作为实际参数传递。如果参数是浮点数,将通过寄存器xmm0、xmm1、xmm2和xmm3传递。如果参数中既有整数又有浮点数,编译器将按照下面的例子传递参数:

void func2(int a, fouble b, int c, float d);
// a: rcx b: xmm1 c: r8 d: xmm3

如果参数通过栈传递,调用者(caller)负责清理堆栈。C++程序的this指针通常作为第一个参数,通过rcx传递。

参数类型 第1个参数(从左边数) 第2个参数 第3个参数 第4个参数 第5个参数及后续参数
浮点数 XMM0 XMM1 XMM2 XMM3 栈(从右向左压栈)
整数 RCX RDX R8 R9 栈(从右向左压栈)
聚合(8-64位和__m64) RCX RDX R8 R9 栈(从右向左压栈)
聚合(其他,比如指针) RCX RDX R8 R9 栈(从右向左压栈)
__m128(作为指针) RCX RDX R8 R9 栈(从右向左压栈)

x86_64 Linux/Mac OSX 调用约定

寄存器 用途 保存方
rdi 保存第一个入参 调用方
rsi 保存第二个入参 调用方
rdx 保存第三个入参 调用方
rcx 保存第四个入参 调用方
r8 保存第五个入参 调用方
r9 保存第六个入参 调用方
rax 保存子程序返回值 调用方(caller)
rbx 基指针(可选) 子程序(callee)
rsp 栈指针
rbp frame 指针 子程序
r10 临时寄存器 调用方
r11 临时寄存器 调用方
r12 临时寄存器 子程序
r13 临时寄存器 子程序
r14 临时寄存器 子程序
r15 临时寄存器 子程序
地址 内容 栈帧
8n+16(%rbp) 内存参数字节 n 调用者栈帧
16(%rbp) 内存参数字节 0 调用者栈帧
8(%rbp) 返回地址 当前栈帧
0(%rsp) 父程序 %rbp 值 当前栈帧

在Linux上,GCC优先通过rdi、rsi、rdx、rcx、r8、r9传递参数。对于浮点数参数,GCC使用xmm0-xmm7寄存器。和MSVC一样,GCC也要求调用者清理堆栈。同时,GCC也将C++的this指针作为第一个参数。因此在GCC中,this指针通过rdi传递。

int f1(int a, int b) double f2(double a, double b) struct foo_t f3() // C++ C1::f4()

参考资料

https://blog.csdn.net/shenjianxz/article/details/71078227 https://msdn.microsoft.com/zh-cn/library/6xa169sk.aspx https://en.wikipedia.org/wiki/X86%5C_calling%5C_conventions#Caller%5C_clean-up https://en.wikipedia.org/wiki/Calling%5C_convention http://xldoc.xl7.xunlei.com/0000000018/00000000180001000010.html https://courses.cs.washington.edu/courses/cse378/10au/sections/Section1_recap.pdf

编辑记录

  • 2019年10月30日 建立文档。
  • 2019年11月13日 修改部分文字。