函数调用约定和返回值"/>
C函数调用约定和返回值
文章目录
- 一、函数调用约定
- 1. 影响函数生成的符号名
- 2. 影响形参内存的释放者
- _stdcall
- _fastcall
- _thiscall
- 二、函数的返回值
- 1. 0 < 返回值 <= 4字节
- 2. 4字节 < 返回值 <= 8字节
- 3. 返回值 > 8字节
一、函数调用约定
- _cdecl:C调用约定
- _stdcall:Windows标准的调用约定
- _fastcall:快速调用约定
- _thiscall:C++的成员函数调用约定
以上的函数调用约定入参都是从右向左,只有PASCAL从左向右。参数为什么要从右向左压栈呢?
主要是因为C/C++要支持可变参函数,如果不用支持可变参函数,从左向右压栈也是可以的。可变参函数就是支持参数数量和类型都可变的函数,比如printf的原型如下:
printf(char const* format, ...)
我们现在有一个fun函数,有一个已经确定的参数a,声明如下:
void fun(int a, ...);
调用方式如下:
fun(10,20,30);
fun(10,20,30,40.1,50);
我们知道在C/C++中通过栈底指针ebp负向偏移访问局部变量,通过ebp正向偏移访问形参
如果从左向右压栈,压栈的实参个数运行时才能确定,编译时期也就无法通过ebp+4访问已经确定的形参a,无法正确的生成指令
如果从右向左压栈,编译器生成指令时永远可以通过ebp+4访问形参a,这样在编译阶段就可以顺利生成指令
函数左侧确定的参数在栈上的位置必须是确定的!!即编译器必须知道ebp偏移多少访问已经确定的参数,才能保证函数正确执行
函数调用约定不同,会影响函数生成的符号名,函数入参顺序,形参内存的清理者
1. 影响函数生成的符号名
在一个文件中写_cdecl的函数声明
在另一个文件中写_stdcall的函数定义
我们编译链接一下
由于定义处__stdcall sum生成的函数符号和声明处__cdecl sum生成的函数符号 不一致,所以找不到__cdecl sum函数的定义
链接器找不到__cdecl sum这个函数调用的定义,将声明的地方改成__stdcall就可以链接成功
2. 影响形参内存的释放者
开辟形参内存 | 释放形参内存 | |
---|---|---|
_cdecl | 调用方 | 调用方 |
_stdcall | 调用方 | 被调用方 |
_fastcall | 调用方 | 被调用方 |
_stdcall
形参内存还是由调用方开辟
ret
表示把栈顶元素的值(调用处下一条指令的地址)赋给PC寄存器,并出栈栈顶元素(修改esp)
ret 8
表示在ret操作的基础上,执行执行指令add esp, 8
_fastcall
可以看到在_fastcall调用约定中,call指令前面并没有push操作,而是通过寄存器把实参传递到形参(没有压栈出栈,速度很快),实参在调用方栈帧上,形参在被调用方栈帧上
在_fastcall调用约定中,最多只能通过寄存器将最左边8字节的实参带给形参,多于8字节的实参还是通过push的方式带给调用方的形参
我们给sum传入三个参数,看看是谁释放形参内存
这就很清楚了,一共3个参数,左边的2个参数通过寄存器传递不需要清理内存,只有一个形参内存需要释放,所以显示ret 4
对于sum函数的第一个局部变量temp,在_cdecl和_stdcall中都是通过ebp-4访问的,形参是通过ebp正向偏移访问,因为形参内存在调用方的栈帧上
而在_fastcall中是通过寄存器把左边的8字节实参带给sum的形参,并存放在sum函数的栈帧上:
mov edx, dword ptr [ebp-8]
mov ecx, dword ptr [ebp-4]
所以对于sum函数的第一个局部变量temp,只能通过ebp-0Ch访问
_thiscall
对参数个数不确定的,调用者清理堆栈,否则被调用者清理堆栈
二、函数的返回值
函数的返回值分为内置类型(char、short、int、long、float、double等)、结构体类型、union、enum等
1. 0 < 返回值 <= 4字节
通过eax寄存器带出
2. 4字节 < 返回值 <= 8字节
#include <stdio.h>typedef struct {int a;int b;
}Data;Data sum(Data a, Data b) {Data temp = { 0 };temp.a = a.a + b.a;return temp;
}int main() {Data a = { 10 }; Data b = {20}; Data ret = { 0 }; ret = sum(a, b);return 0;
}
可以看到,4字节 < 返回值 <= 8字节时,通过eax和edx寄存器带出
3. 返回值 > 8字节
#include <stdio.h>typedef struct {int a[20];
}Data;Data sum(Data a, Data b) {Data temp = { 0 };temp.a[0] = a.a[0] + b.a[0];return temp;
}int main() {Data a = { 10 };Data b = {20};Data ret = { 0 };ret = sum(a, b);return 0;
}
压参数的时候,没有使用push指令,因为寄存器不够用,故使用了循环拷贝的方法,从实参的空间拷贝到形参的空间
产生临时量有三个地方:函数调用前,函数调用时return的地方,函数调用完成时。在接收大于8字节返回值时,是在函数调用前产生临时量,并把临时量内存的地址压栈,而这个临时量是用来接收返回值的
我们看到不仅仅压栈了实参a、b,还压栈了临时量的地址(临时量一般在调用双方栈帧之间),当函数返回值大于8字节时,可以把sum函数简单理解为如下形式:
Data sum(void* tmp_address, Data a, Data b);
我们看一下sum函数中return时的汇编指令是如何待会80字节的返回值的
最后通过eax把临时量的地址带出来,调用函数就可以通过eax拿到sum函数的返回值了
如果临时量在函数调用前产生,那被调用函数返回的时候,肯定是通过ebp+8
访问临时量并写入返回值。因为ebp指向的空间保存了调用函数的栈底地址,ebp+4指向的空间保存了call指令下一条指令的地址,ebp+8指向最后一个压栈的实参,即用于带出返回值的临时量的地址
返回方式:
- 返回值空间在[1,4],用eax寄存器
- 返回值空间在[5,8],用eax、edx寄存器
- 返回值空间大于8字节时,函数调用前产生临时量用于存储返回值,并把这个临时量的地址作为最后一个实参压栈,在被调用函数中通过
ebp+8
访问该临时量的地址。最终用该临时量给调用方的接收变量ret赋值
更多推荐
C函数调用约定和返回值
发布评论