【计算机系统】函数调用约定详解

本文最后更新于: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; };
// mytype c 将存入RDI和RSI中,a和b分别存入RDX、RCX
// 返回的mytype分别放入RAX和RDX中
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);
// a 放入 RCX, b 放入 XMM1, c 放入 R8, d 放入 XMM3, f 和 e 压入栈中(f先压栈)

未完全原型化的函数(可变参数)

仅对于浮点值,整数寄存器和浮点寄存器都包含浮点值,以防被调用者期望整数寄存器中的值。

1
2
3
4
func1();
func2() { // RCX = 2, RDX = XMM1 = 1.0, and R8 = 7
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);
// a 放入 RCX, b 放入 XMM1, c 放入 R8, d 放入 R9, e 压栈,
// 返回 __int64 在 RAX 中
  • 通过 XMM0 返回值:非标量类型,包括浮点型、双精度型和向量类型,例如 __m128, __m128i, __m128d
1
2
3
__m128 func2(float a, double b, int c, __m64 d);
// a 放入 XMM0, b 放入 XMM1, c 放入 R8, d 放入 R9,
// 返回 __m128 result 在 XMM0 中.

RAX 或 XMM0 返回值中未被使用的bit的状态则未定义。

返回用户定义类型

  • 在 RAX 中按值返回用户定义类型:

    该类型的长度必须为 1、2、4、8、16、32 或 64 位。它还必须没有用户定义的构造函数、析构函数或复制赋值运算符。它不能有私有或受保护的非静态数据成员,也不能有引用类型的非静态数据成员。 它不能有基类或虚函数。并且,它只能具有也满足这些要求的数据成员。

1
2
3
4
5
6
struct Struct2 {
int j, k; // Struct2 为 64 bits
};
Struct2 func4(int a, double b, int c, float d);
// 将 a 放入 RCX, b 放入 XMM1, c 放入 R8, d 放入 XMM3;
// 返回 Struct2 的值,在 RAX.
  • 在 RAX 中返回用户定义类型的指针:

    不符合按值返回的要求的时候,则为返回的用户定义类型分配内存,并将其指针作为函数的第1个参数传入,占用 RCX 寄存器,剩余参数则往右移动一个参数的位置。

    最后在 RAX 中返回该相同的指针。

1
2
3
4
5
6
7
struct Struct1 {
int j, k, l; // Struct1 超过了 64 bits.
};
Struct1 func3(int a, double b, int c, float d);
// 为 Struct1 分配内存,其指针存入 RCX
// a 存入 RDX, b 存入 XMM2, c 存入 R9, d 压入栈中;
// 返回值为 Struct1 的指针,放在 RAX 中

【计算机系统】函数调用约定详解
https://qalxry.github.io/2024/04/16/【计算机系统】函数调用约定详解/
作者
しずり雪
发布于
2024年4月16日
更新于
2024年4月16日
许可协议