1.栈的介绍-C语言调用函数(一)_双层小牛堡的博客-CSDN博客
接着上面
函数调用的约定
在栈帧中 主要的是主调函数如何存入实参 让被调用函数能够访问
这种是通过函数见的调用规定来规范的
并且 调用规定还规范了 函数执行完后应该由主函数实现 清除参数 还是 被调用函数清除
被调用函数和主函数之间 是存在一个调用约定来保证程序的完整性
调用规定
调用规定通常规定了几个方面
(1)函数参数的传递顺序和方式
最常见的就是堆栈 主函数把实参压入栈内 被调用函数通过 相对ebp的偏移量 来访问函数的实参
如果有很多参数 调用规定还要确定参数入栈的顺序 (一些规定允许使用寄存器传参来提高速度)
(2)栈的维护
主函数调用被调用函数的时候 把参数压入 并且把控制器交给函数
函数结束的时候 要把返回值弹出 并且清空栈帧 恢复到调用前的样子
这个通过约定来保证是 主函数或被调用函数进行清空
(3)名字修饰策略
又叫做函数名修饰策略 编译器对于不同函数 要进行确定名字修饰
如果函数之间的调用约定不匹配 那么就会出现错误
常见的调用约定
cdecl约定
这个约定是在C++/C语言编译器 默认的函数调用方式
所有非C/C++的函数成员和不是使用 stdcall/fastcall定义的函数 都默认使用该调用约定
函数参数按照从从右到左入栈
void stack_test1(int a, int b, int c)
c 先压入 b 再压入 最后压入 a
并且主函数负责清空栈帧
可以使用于参数不固定的函数 例如printf
返回值放入EAX中
C语言的函数名修饰规则 是在函数名前加一个_
C++的规则是 除非特别使用 return "C" 否则使用不同的名字来修饰
stdcall约定
Pascall程序调用约定 WinApi也都是调用这个约定
约定函数参数从右往左入栈
除了指针和引用类型外 全部使用传值的方式传递
被调用函数负责清除栈帧
返回值返回到EAX中
只能适用于 参数固定的函数
函数修饰 是 在函数名前面加_ 在函数后加@ 并且加入大小
例如
_functionname@number
number是函数参数的大小
fastcall
是stdcall的变形
使用 ECX EDX 传递前两个 四字节双字或更少字节的参数
参数从右往左入栈
被调用函数清空栈帧
返回到EAX 中
因为两个寄存器存储 不用全部入栈 所以速度比stdcall更快
函数名修饰
运用 @@修饰函数名 后面加上十进制数表示参数列表大小
@function_name@number
thiscall约定
C++中 非静态函数必须要接受一个指向主函数对象的类的指针 (this指针)并且频繁使用
主函数的对象地址必须要被调用函数提供
并且在调用被调用函数的时候 必须要把指针作为参数传递给被调用函数
编译器默认使用这个约定来应对非静态函数
函数参数从右往左入栈
如果参数是固定数目 那么this指针通过ECX传递给被调用函数
被调用函数清空栈帧
如果参数不是固定数目的 那么this指针在所有参数入栈后再继续入栈
主函数清空栈帧
thiscall只能编译器使用 这个不是函数
naked call
该约定 不能 没有保存和恢复寄存器的代码 并且不能使用ret来返回值
只能通过内嵌汇编返回
这个约定是在特殊情况下使用
声明处于非C/C++上下文中的函数,并由程序员自行编写初始化和清栈的内嵌汇编指令。
pascal约定
函数参数从左往右入栈
只能是固定参数
被调用函数自己清空栈帧
函数名修饰 只有全变为大写
我们继续给出图片来理解
这张图我们就能清晰的看到每个约定的内容 和不同
现在我们给出代码 例子 来演示 如果我们在设立代码的时候 确定约定 主函数和被调用函数的汇编
int __attribute__((__cdecl__)) CalleeFunc(int i, int j, int k){
// int __attribute__((__stdcall__)) CalleeFunc(int i, int j, int k){
//int __attribute__((__fastcall__)) CalleeFunc(int i, int j, int k){
return i+j+k;
}
void CallerFunc(void){
CalleeFunc(0x11, 0x22, 0x33);
}
int main(void){
CallerFunc();
return 0;
}
调用约定影响
如果函数导出被其他程序员执行的时候 应该遵循主调用约定 如果只是在内部 就根据函数的调用约定
x86函数参数传递方法
通过堆栈完成 并且存储顺序是从右往左存入
当主函数向被调用函数传参的时候
参数应该作为一个数组传入
因为是从右往左输入
那么数组的下标 0,1,2.....n-1
就和函数参数声明是一样的 例如 int x(1,2,3,4)
各个类型的传参
整型和指针参数的传递
整型参数和指针参数传递的方式是一样的 因为在32位x86上 大小是一样的(四个字节)
对于被调用函数的栈帧
浮点参数的传递
其实和整型差不多 就是浮点型多占4个字节 总共占8个字节
结构体和联合体的参数
结构体和联合体都是c语言自定义的数据类型
两者最大的区别就是 对内存的处理不同
结构体 struct
可以包含很多成员 每个成员又可以有自己的名称和类型 互不干涉 遵循内存对齐原则
一个struct的长度为所有成员的总和
联合体 union
所有成员共用一块内存 并且同一时刻 只能有一个成员得到内存的控制
这两个的区别
在内存中 就是 结构体是存放了所有成员 联合体存放的只是需要的成员/被选中的
他们的参数存放和整型,浮点型差不多 只是存放的大小 传递在内存中的指针 来传递参数
x86返回值的传递方法
函数的返回值可以通过寄存器返回
(1)返回值不超过四字节(int,float,char,指针等) 通过EAX返回
(2)如果超过四字节小于八字节(longlong _int64等) 通过EAX+EDX返回 EDX返回值高4字节,EAX返回值低4字节
(3)如果返回值是浮点型(float,double) 就通过专用的协处理器浮点型栈的栈顶返回
浮点运算是需要专门的浮点处理器 所以需要专门的浮点栈返回
(4)如果是结构体和联合体 主函数会向被调用函数传入一个指向返回地址的参数
foo(p1,p2)-->foo(&p,p1,p2)
变为引用型参数返回
具有一下步骤
1.主函数将显示的实参逆序入栈
2.将接受返回值的结构体变量地址作为隐藏变量入栈 -->临时变量
3.被调用函数的返回值 拷贝到指向隐藏变量的地址中
4.把隐藏变量的地址存入EAX中
不同的编译器有不同的方法
(5)不返回指向栈内存空间的指针 因为函数执行完毕后 会释放内存 如果返回了指针 会无法保证准确
函数返回值 通过寄存器返回 不需要分配空间来操作
所以如果不写返回的类型 就按照int类型返回 这会带来很大的安全隐患