# 引言
调用约定(Calling Convention)是一种实现级别的方案,规定了被调函数如何从主调函数获取参数,以及如何将返回值传回主调函数。简单来说,就是参数和返回值如何传入传出。具体来讲,调用约定解决了以下问题:
- 参数、返回值、返回地址放在哪里?寄存器,调用栈,还是其他数据结构?
- 使用内存传递参数时,按照什么顺序传递实参?
- 如何将返回值传回主调函数?尤其是当返回时很复杂时,使用栈,还是寄存器,还是堆?
- 进行函数调用前以及函数调用结束后,由主调还是被调函数进行环境初始化和清理的工作?
- 用于描述参数的元数据是否传递?怎么传递?
- 在哪里保存帧指针?在栈帧,还是寄存器?
- 对于不属于函数局部变量的数据,在哪里保存静态域链接?
- 局部变量如何分配?
有时还有以下的区别:
- 哪些寄存器可以直接被被调函数使用,而不需要进行保护
- 哪些寄存器被认为是 volatile 的,如果是 volatile 的,不需要由被调函数恢复的
即便部分编程语言可能规定了调用约定,但是不同的实现方式可能仍然采用不同的调用约定。实现上可能提供多于一种的调用约定供选择。原因可能包括性能、和其他流行语言的频繁适配、技术或是非技术的原因等等。
# x86 架构微处理器调用约定
# stdcall 调用约定
# cdecl 调用约定
即 C declaration,起源于微软为 C 设计的编译器,被 x86 架构下很多 C 编译器使用。有以下特征:
- 由主调函数清理栈上的参数
- 使用栈进行参数传递
- 使用 EAX 寄存器返回整数和内存地址,使用 ST0 x87 寄存器返回浮点数
- EAX、ECX、EDX 寄存器由主调函数保存,其他寄存器由被调函数保存
- 调用新的函数时,x87 浮点寄存器从 ST0 到 ST7 必须是空的,退出函数时 ST1 到 ST7 必须是空的,ST0 不用作返回值时也必须是空的
- 函数传参时,从右向左压栈
对于如下的 C 源程序,
int callee(int, int, int); | |
int caller(void) | |
{ | |
return callee(1, 2, 3) + 5; | |
} |
将被转换为如下的 x86 汇编代码(intel 语法),
caller:
; make new call frame
; (some compilers may produce an 'enter' instruction instead)
push ebp ; save old call frame
mov ebp, esp ; initialize new call frame
; push call arguments, in reverse
; (some compilers may subtract the required space from the stack pointer,
; then write each argument directly, see below.
; The 'enter' instruction can also do something similar)
; sub esp, 12 : 'enter' instruction could do this for us
; mov [ebp-4], 3 : or mov [esp+8], 3
; mov [ebp-8], 2 : or mov [esp+4], 2
; mov [ebp-12], 1 : or mov [esp], 1
push 3
push 2
push 1
call callee ; call subroutine 'callee'
add esp, 12 ; remove call arguments from frame
add eax, 5 ; modify subroutine result
; (eax is the return value of our callee,
; so we don't have to move it into a local variable)
; restore old call frame
; (some compilers may produce a 'leave' instruction instead)
mov esp, ebp ; most calling conventions dictate ebp be callee-saved,
; i.e. it's preserved after calling the callee.
; it therefore still points to the start of our stack frame.
; we do need to make sure
; callee doesn't modify (or restores) ebp, though,
; so we need to make sure
; it uses a calling convention which does this
pop ebp ; restore old call frame
ret ; return
手动指定调用约定: return_type __cdecl func_name();
# fastcall 调用约定
# thiscall 调用约定
# nakedcall 调用约定
# 参考资料
- https://en.wikipedia.org/wiki/X86_calling_conventions
- https://en.wikipedia.org/wiki/Calling_convention