函数堆栈调用
我们先来写一个简单的代码,写一个相加的函数,然后在main函数中调用它,并把结果打印出来。我们知道它最终打印的结果是30,但是main函数(调用方函数)到底是怎么去调用sum函数(被调用方函数)的呢,它的内存布局又是怎么样的,我们可以转到反汇编去看一看它的真面目。
#include <stdio.h>
int sum(int left, int right)
{
int tmp = 0;
tmp = left + right;
return tmp;
}
int main()
{
int a = 10;
int b = 20;
int rt = 0;
rt = sum(a, b);
printf("rt:%d\n", rt);
return 0;
}
我们在查看反汇编之前,首先了解反汇编的指令都代表了什么意思:
寄存器
eax ebx ecx edx 寄存器,存放数据
esp,栈顶指针寄存器
ebp,栈底指针寄存器
ebp和esp组合起来形成一个栈帧
指令
linux的指令是自左向右看的
Windows(inter x86)的指令是自右向左看的
mov,数据传送指令,将一个数据从源地址传送到目标地址
lea,移地址指令,一个变量用[a]括起来表明这是一个单元的地址如果前面加ptr表明该内存单元
push,压栈操作,例如push 0ah:表示将10(0xa)压到栈顶上面
pop,出栈操作,将栈顶的元素拿出放到寄存器中
add,累加指令,例如add eax, 0a:eax + 0a ==> eax 等价于eax += 0a
sub,减法指令, 例如sub eax, 0a:eax -= 0a
call,保存当前指令的下一条指令并跳转到目标函数,分为两步:
1:压入下一行指令地址
2:jmp跳转到被调用方
接下来转到反汇编代码:
首先给main函数开辟空间并作初始化
8: int main()
10: int a = 10;
005418B8 mov dword ptr [ebp-8],0Ah //将A放入到ebp-8的位置,即A的内存块
11: int b = 20;
005418BF mov dword ptr [ebp-14h],14h //将B放到ebp-14的位置
12: int rt = 0;
005418C6 mov dword ptr [ebp-20h],0 //将rt放到ebp-20的位置
13: rt = sum(a, b);**//实参传递,传递顺序是自右向左的,因为存在可变参参数**
005418CD mov eax,dword ptr [ebp-14h]//通过ebp-14的位置即B的位置拿出来放到eax寄存器
005418D0 push eax //将eax压栈
005418D1 mov ecx,dword ptr [ebp-8] //通过ebp-8的位置即B的位置拿出来放到ecx寄存器
005418D4 push ecx //将ecx压栈
005418D5 call 0054107D //近址相对位移调用指令,用来调用SUM函数
005418DA add esp,8
这是sum函数的反汇编
2: int sum(int left, int right)
00541720 push ebp //将main函数栈底指针压栈
00541721 mov ebp,esp //让ebp指向esp指向的位置
00541723 sub esp,0CCh //esp-=cc,给sum开辟空间
00541729 push ebx //压入三个寄存器
0054172A push esi
0054172B push edi
0054172C lea edi,[ebp+FFFFFF34h]
00541732 mov ecx,33h
00541737 mov eax,0CCCCCCCCh //将cccccccc赋给寄存器
0054173C rep stos dword ptr es:[edi] //循环指令将开辟好的空间全部用eax来初始化
总结
开栈的过程:
1.压入实参,开辟形参并赋值
2.压入下一行指令,被调用放函数处理完能沿着调用方下一行指令继续执行
3.压入调用方栈底地址,方便被调用方函数处理完成退回到调用方
4.开辟被调用方的活动空间并初始化位0xcccccccc
清栈的过程:
1.清理被调用方函数开辟的空间
2.出栈ebp,ebp回退到调用方栈帧上
3.出栈下一行指令寄存器,函数调用完成,沿着下一行指令继续执行
4.清理形参
返回值处理:
当返回值<4个字节时,由eax寄存器将返回值带出
当返回值在4到8个字节时,由eax,edx寄存器将返回值带出
当返回值大于8个字节时,由我们的临时量带出。临时量由调用方开辟,临时量在表达式结束时结束,但是空间还在
其中返回值可以分为内置类型和自定义类型,内置类型是指系统类型如int,char,short,double。用户自定义类型像枚举类型,结构体,联合体,类类型
**还有一点,函数返回值不能返回局部变量的地址,**我们知道当被调用方函数回退到调用方函数时,它的空间并没有销毁,而是被回收了。这就意味着那块地址有可能被重新赋值,这时候我们再指向它,就有可能出现越界的情况。这也是临时量为什么要在调用方开辟内存的原因。
调用约定:
在c中由三种调用约定:_cdecl,_stdcall和_fastcall,其中_stdcall调用约定是windows平台的。
在c++中还有一种约定:_thiscall调用约定