本文最后更新于:2024年4月16日 上午 10:18
函数调用约定详解
大致内容源于
https://en.wikipedia.org/wiki/X86_calling_conventions
一、IA32 Architecture
714ac521e06e80553869c8adbfc98432
1 (Unix-like) cdecl
采用的操作系统:类Unix系统
全称:C Declaration (声明)
cdecl是C语言的默认调用约定。在这种约定下,调用者负责清理堆栈。这意味着函数可以有可变数量的参数。
参数传递
在 C
语言中,函数参数按从右到左的顺序压入堆栈,即最后一个参数首先压入。
返回值
- 如果返回值为整数值或是内存地址,则放入 EAX 中
- 如果返回值为浮点值,则放入 ST0 x87寄存器中
示例代码
C代码:
1 2 3 4
| int callee(int, int, int); int caller(void) { return callee(1, 2, 3) + 5; }
|
汇编代码(在godbolt网站上使用 x86-64 gcc 13.2,并启用
-m32
标志):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| caller(): pushl %ebp movl %esp, %ebp subl $8, %esp ; 通过向下调整栈指针,为局部变量分配8字节的空间(在栈上预留空间)。 subl $4, %esp ; 再为额外的局部变量或是准备压入栈的参数预留4字节的空间。 pushl $3 ; 将值3压入栈中,作为将要调用的函数的第三个参数。 pushl $2 ; 将值2压入栈中,作为将要调用的函数的第二个参数。 pushl $1 ; 将值1压入栈中,作为将要调用的函数的第一个参数。 ; call 指令将下一条指令的地址压栈,作为返回地址。 call callee(int, int, int) ; 调用callee),并传入之前压入栈的三个参数。 addl $16, %esp ; 函数调用返回后,调整栈指针以移除参数。 addl $5, %eax; 将`eax`寄存器的值增加5,这里对返回值进行了修改。 leave ; `leave`指令是`mov %ebp, %esp; pop %ebp` ret
|
在 Linux 中,从 GCC 4.5 版开始,调用函数时堆栈必须与 16
字节对齐(之前的版本只需要 4 字节对齐)。
2 (Microsoft)
cdecl、stdcall、fastcall、thiscall
采用的操作系统:Windows
2.1 __cdecl
__cdecl
是 C 和 C++ 程序的默认调用约定。
因为堆栈是由调用者清理的,所以它可以做可变参数功能。 这
__cdecl
调用约定创建比 __stdcall更大的可执行文件,因为它要求每个函数调用都包含堆栈清理代码。
参数传递顺序 |
右到左。 |
栈维护责任 |
调用函数从栈中弹出参数。 |
名称修饰约定 |
下划线字符 (_) 作为名称前缀,除非导出使用 C 链接的 __cdecl
函数。 |
示例:
1 2 3 4
| int __cdecl callee(int a, int b, int c) {return a + b + c;} int caller(void) { return callee(1, 2, 3) + 5; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| _a$ = 8 ; size = 4 _b$ = 12 ; size = 4 _c$ = 16 ; size = 4 _callee PROC push ebp mov ebp, esp mov eax, DWORD PTR _a$[ebp] add eax, DWORD PTR _b$[ebp] add eax, DWORD PTR _c$[ebp] pop ebp ret 0 ; 被调用者不清理栈 _callee ENDP
_caller PROC push ebp mov ebp, esp push 3 ; 参数全部从右到左依次压栈 push 2 push 1 call _callee ; 名称前缀为下划线_,无后缀 add esp, 12 ; 由调用函数清理栈,共12字节 add eax, 5 pop ebp ret 0 _caller ENDP
|
2.2 __stdcall
https://blog.csdn.net/hellokandy/article/details/54603055
stdcall 调用约定是 Pascal
调用约定的变体,其中被调用者负责清理堆栈,但参数按从右到左的顺序压入堆栈,类似于
_cdecl 调用约定。指定寄存器 EAX、ECX 和 EDX 在函数内使用。返回值存储在
EAX 寄存器中。
在Microsoft C++系列的C/C++编译器中,使用 PASCAL 宏,WINAPI 宏和
CALLBACK 宏来指定函数的调用方式都为 stdcall。
参数传递顺序 |
从右到左。 |
参数传递约定 |
按值传递,除非传递指针或引用类型。 |
栈维护责任 |
被调用的函数从栈中弹出它自己的参数。 |
名称修饰约定 |
下划线( _ ) 是名称的前缀。 该名称后跟 at 符号 (
@ ) 后跟参数列表中的字节数(十进制)。 因此,函数声明为
int func( int a, double b ) 装饰如下:
_func@12 |
示例:
1 2 3 4
| int __stdcall callee1(int a, int b, int c) {return a + b + c;} int caller(void) { return callee(1, 2, 3) + 5; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| _a$ = 8 ; size = 4 _b$ = 12 ; size = 4 _c$ = 16 ; size = 4 _callee@12 PROC push ebp mov ebp, esp mov eax, DWORD PTR _a$[ebp] add eax, DWORD PTR _b$[ebp] add eax, DWORD PTR _c$[ebp] pop ebp ret 12 ; 被调用者清理栈 _callee@12 ENDP
_caller PROC push ebp mov ebp, esp push 3 ; 参数直接压栈 push 2 push 1 call _callee@12 ; 下划线_为名称前缀,@为名称后缀 ; 后跟函数参数列表的字节数(十进制) ; 调用者无需清理栈中参数,由被调用者执行 add eax, 5 pop ebp ret 0 _caller ENDP
|
2.3 __fastcall
参数传递顺序 |
前两个 DWORD 或者在参数列表中从左到右找到的较小参数在
ECX 和 EDX 寄存器中传递; 所有其他参数都在堆栈上从右向左传递。 |
栈维护责任 |
被调用的函数从栈中弹出参数。 |
名称修饰约定 |
at 符号 (@) 是名称的前缀; 参数列表中的 at
符号后跟字节数(十进制)作为名称的后缀。 |
类、结构体和联合 |
被视为“多字节”类型(无论大小)并在堆栈上传递。 |
枚举和枚举类 |
如果它们的基础类型是通过寄存器传递的,则通过寄存器传递。
例如,如果基础类型是 int 或者
unsigned int 大小为 8、16 或 32 位。 |
示例:
1 2 3 4
| int __fastcall callee(int a, int b, int c) {return a + b + c;} int caller(void) { return callee2(1, 2, 3) + 5; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| _b$ = -8 ; size = 4 _a$ = -4 ; size = 4 _c$ = 8 ; size = 4 @callee@12 PROC push ebp mov ebp, esp sub esp, 8 mov DWORD PTR _b$[ebp], edx mov DWORD PTR _a$[ebp], ecx mov eax, DWORD PTR _a$[ebp] add eax, DWORD PTR _b$[ebp] add eax, DWORD PTR _c$[ebp] mov esp, ebp pop ebp ret 4 ; 被调用的函数清理栈中参数 @callee@12 ENDP
_caller PROC push ebp mov ebp, esp push 3 ; 多出的参数在栈中传递 mov edx, 2 ; 从左往右找到的第2个较小参数用EDX传递 mov ecx, 1 ; 从左往右找到的第2个较小参数用ECX传递 call @callee@12 ; 名称前后都有@符号,后跟函数参数列表的字节数(十进制) ; 调用者无需清理栈中参数,由被调用者执行 add eax, 5 pop ebp ret 0 _caller ENDP
|
2.4 __thiscall
用于 x86 体系结构上的 C++ 类成员函数。
这是不使用可变参数的成员函数使用的默认调用约定。
参数传递顺序 |
参数从右向左压入栈。
this 指针通过寄存器 ECX
传递,而不是在栈上传递。 |
栈维护责任 |
被调用的函数从栈中弹出参数。 |
示例:
1 2 3 4 5 6 7 8
| class Myclass { public: int a, b, c; int callee(int x, int y); }; int caller(Myclass mc) { return mc.callee(1, 2) + 5; }
|
1 2 3 4 5 6 7 8 9 10 11 12
| _mc$ = 8 ; size = 12 int caller(Myclass) PROC push ebp mov ebp, esp push 2 ; 参数从右向左压入栈 push 1 lea ecx, DWORD PTR _mc$[ebp] ; this 指针通过 ECX 传递 call int Myclass::callee(int,int) add eax, 5 pop ebp ret 0 int caller(Myclass) ENDP
|
二、x86-64 Architecture
64位架构下的Ubuntu和Windows的函数调用约定,当然,Ubuntu对应的是Linux
IA32下多种类的函数调用约定基本上被统一为了同一种方式。
1 System V AMD64 ABI
采用的操作系统:Linux、macOS、BSD、Solaris
参数传递
整数或指针(1~6) |
从左到右的整数分别放入:RDI, RSI, RDX, RCX, R8, R9 |
浮点数(1~8) |
从左到右的浮点数分别放入:XMM0 - XMM7 |
更多的参数 |
从右到左依次压入栈中 |
静态链指针(用于嵌套函数) |
R10 |
自定义结构体(不超过128且能分为两个64位,等等) |
RDI、RSI |
其他不满足要求的自定义结构体 |
指向调用者提供的空间的指针作为第1个参数添加到前面 |
返回值
- 不超过 64 位的整数返回值存储在 RAX 中
- 大于 64 位但不超过128 位的值存储在 RAX 和 RDX 中。
浮点返回值类似地存储在 XMM0 和 XMM1 中。
- 自定义结构体(不超过128且能拆为两个64位,还有其他要求)也能够存储在RAX和RDX中
- 其他的不满足要求的自定义结构体则返回保存在 RAX 的指针
示例代码1
C代码:
1 2 3 4
| int callee(int,float,int,int,float,int,int,int,int); int caller(void) { return callee(1, 2.0, 3, 4, 5.0, 6, 7, 8, 9) + 5; }
|
汇编代码(在godbolt网站上使用x86-64 gcc 13.2):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| caller(): pushq %rbp movq %rsp, %rbp subq $8, %rsp pushq $9 ; 多余的整数参数压入栈中 movl $8, %r9d ; 整数的第6个参数存入 R9 中 movl $7, %r8d ; 整数的第5个参数存入 R8 中 movl $6, %ecx ; 整数的第4个参数存入 RCX 中 movss .LC0(%rip), %xmm1 ; 浮点数的第2个参数存入 XMM1 中 movl $4, %edx ; 整数的第3个参数存入 RDX 中 movl $3, %esi ; 整数的第2个参数存入 RSI 中 movl .LC1(%rip), %eax ; 浮点数的第1个参数存入 XMM0 中 movd %eax, %xmm0 movl $1, %edi ; 整数的第1个参数存入 RDI 中 call callee(int, float, int, int, float, int, int, int, int) addq $16, %rsp addl $5, %eax leave ret .LC0: .long 1084227584 .LC1: .long 1073741824
|
示例代码2
C代码(嵌套的函数)
1 2 3 4 5 6 7
| int func() { int x = 10, y = 20; int nested(int a, int b, int c) { return a + b + c + x + y; } return nested(1, 2, 3) + 4; }
|
可以从下面的汇编代码中看到R10寄存器的作用:用于支持嵌套函数访问父级函数的局部变量,它指向父级局部变量所在栈的最低起始地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| nested.0: pushq %rbp movq %rsp, %rbp movl %edi, -4(%rbp) ; 将第一个参数a存入基址指针-4处 movl %esi, -8(%rbp) ; 将第二个参数b存入基址指针-8处 movl %edx, -12(%rbp) ; 将第三个参数c存入基址指针-12处 movq %r10, %rax ; 将r10寄存器的值(函数调用中的上下文)存入rax寄存器 movq %r10, -24(%rbp) ; 将r10寄存器的值存入基址指针-24处 movl -4(%rbp), %ecx ; 将基址指针-4处的值(a)存入ecx寄存器 movl -8(%rbp), %edx ; 将基址指针-8处的值(b)存入edx寄存器 addl %edx, %ecx ; ecx = edx + ecx(将b加到a上) movl -12(%rbp), %edx ; 将基址指针-12处的值(c)存入edx寄存器 addl %edx, %ecx ; ecx = edx + ecx(将c加到之前的结果上) movl 4(%rax), %edx ; 将rax+4指向的值(在此为x)存入edx寄存器 addl %ecx, %edx ; edx = edx + ecx(将之前的结果加到x上) movl (%rax), %eax ; 将rax指向的值(在此为y)存入eax寄存器 addl %edx, %eax ; eax = eax + edx(将之前的结果加到y上) popq %rbp ; 弹出当前栈帧的基址指针 ret ; 返回到调用nested函数的地方 func: pushq %rbp ; 将当前函数的基址指针压栈 movq %rsp, %rbp ; 将当前栈指针存入基址指针,建立新的栈帧 subq $16, %rsp ; 分配16字节的空间给局部变量 leaq 16(%rbp), %rax movq %rax, -8(%rbp) movl $10, %eax ; 局部变量 int x = 10 movl %eax, -12(%rbp) movl $20, %eax ; 局部变量 int y = 20 movl %eax, -16(%rbp) leaq -16(%rbp), %rax ; 计算局部变量起始地址 movq %rax, %r10 ; 将上述地址存入r10寄存器 movl $3, %edx ; 将值3存入edx寄存器 movl $2, %esi ; 将值2存入esi寄存器 movl $1, %edi ; 将值1存入edi寄存器 call nested.0 ; 调用nested函数 addl $4, %eax ; 将值4加到eax寄存器中的结果上 leave ; 释放当前栈帧,等效于movq %rbp, %rsp 和 popq %rbp ret ; 返回到函数调用点
|
更多示例
1 2 3 4 5 6 7 8
| struct mytype { int a, b, c, d; };
struct mytype callee(int a, int b, struct mytype c); void caller(void) { struct mytype tmp1, tmp2; tmp1 = callee(1,2,tmp2); }
|
2 Microsoft x64 calling
convention
采用的操作系统:Windows x64、UEFI
在Windows环境中为x64架构编译时(无论使用Microsoft还是非Microsoft工具),stdcall、thiscall、cdecl和fastcall都将解析为使用此约定。
https://learn.microsoft.com/en-us/cpp/build/x64-calling-convention
2.1 参数传递
默认情况下,x64
调用约定将前四个参数传递给寄存器中的函数。用于这些参数的寄存器取决于参数的位置和类型。剩余的参数按从右到左的顺序压入堆栈。
最左边四个位置的整数值参数分别在 RCX、RDX、R8 和 R9
中按从左到右的顺序传递。第五个和更高的参数如前所述在堆栈上传递。寄存器中的所有整数参数都是右对齐的,因此被调用者可以忽略寄存器的高位并仅访问寄存器的必要部分。
前四个参数中的任何浮点和双精度参数都在 XMM0 - XMM3
中传递,具体取决于位置。 当存在可变参数参数时,浮点值仅放置在整数寄存器
RCX、RDX、R8 和 R9 中。同样,当相应的参数是整数或指针类型时,XMM0 - XMM3
寄存器将被忽略。
浮点数 |
stack |
XMM3 |
XMM2 |
XMM1 |
XMM0 |
整数 |
stack |
R9 |
R8 |
RDX |
RCX |
聚合类型 (8, 16, 32, or 64 bits) |
stack |
R9 |
R8 |
RDX |
RCX |
其他聚合类型,比如指针 |
stack |
R9 |
R8 |
RDX |
RCX |
例子:
1 2
| func3(int a, double b, int c, float d, int e, float f);
|
未完全原型化的函数(可变参数)
仅对于浮点值,整数寄存器和浮点寄存器都包含浮点值,以防被调用者期望整数寄存器中的值。
1 2 3 4
| func1(); func2() { func1(2, 1.0, 7); }
|
Shadow Space
在Microsoft
x64调用约定中,调用者负责在调用函数之前在堆栈上分配32字节的“影子空间”(无论实际使用的参数数量如何),并在调用后弹出堆栈。影子空间用于备份RCX、RDX、R8和R9,但必须对所有函数(即使是参数少于四个的函数)都可用。
例如,采用 5
个整数参数的函数将采用寄存器中的第一个到第四个参数,第五个参数将被推送到影子空间上方的顶部。
因此,当进入被调用函数时,栈(按升序)将由返回地址、后面的影子空间(32
字节)和第五个参数组成。
2.2 返回值
返回内置类型
- 通过 RAX 返回值:可以容纳 64 位的标量返回值,包括
__m64
类型。
1 2 3
| __int64 func1(int a, float b, int c, int d, int e);
|
1 2 3
| __m128 func2(float a, double b, int c, __m64 d);
|
RAX 或 XMM0 返回值中未被使用的bit的状态则未定义。
返回用户定义类型
1 2 3 4 5 6
| struct Struct2 { int j, k; }; Struct2 func4(int a, double b, int c, float d);
|
1 2 3 4 5 6 7
| struct Struct1 { int j, k, l; }; Struct1 func3(int a, double b, int c, float d);
|