让我们带着问题来阅读本篇文章
- 型参在哪里开辟内存?
- 型参的入栈顺序?
- 函数返回值怎么带出来?
- 函数的返回值为什么会回退到栈里?
- 函数调用结束为什么会沿着调用点继续执行?
我们先来了解一下堆与栈是怎样的一种存在
什么是栈?
栈用于维护函数调用的上下文,离开栈,函数就没有办法实现。栈通常在用户空间的最高地址处分配,通常有数兆字节大小。
栈在程序运行中具有举足轻重的地位。最重要的是,栈保存了一个函数调用所需要的的维护信息,这常常被称为堆帧栈或者活动记录。堆栈帧一般包括以下几个内容:
- 帧栈是一个main函数的活动空间范围。
- 函数的返回地址和参数。
- 临时变量:包括函数的非静态局部变量以及编辑器自动生成的其他临时变量。
- 保存的上下文:包括在函数调用前后需要保持不变寄存器。
什么是堆?
堆是用来容纳应用程序动态分配内存的内存区域,当程序使用malloc或new分配内存时,得到的内存来自堆里,堆也可能没有固定统一的存储区域。堆一般比栈大很多,可能有几十数百兆自己的容量。
认识函数堆栈调用的一些简单指令
mov 移值指令
lea 移地址
push 用栈
pop 出栈
call
- 压入下一行指令地址
- jump到被调用方函数
认识寄存器的存在
寄存器是CPU内部的元件,寄存器拥有非常高的读写速度,所以在寄存器之间的数据传送非常快。
寄存器的用途:
1.可将寄存器内的数据执行算术及逻辑运算。
2.存于寄存器内的地址可用来指向内存的某个位置,即寻址。
3.可以用来读写数据到电脑的周边设备。
eax 是"累加器"(accumulator), 它是很多加法乘法指令的缺省寄存器。
add eax,4; eax+=4
sub eax,4; eax-=4
ebx 是"基地址"(base)寄存器, 在内存寻址时存放基地址。
ecx 是计数器(counter), 是重复(REP)前缀指令和LOOP指令的内定计数器。
edx 总是被用来放整数除法产生的余数。
ebp 栈底指针寄存器
esp 栈顶指针寄存器
pc 下一行指令寄存器
在i386中,一个函数的活动记录用ebp(栈底指针寄存器)和esp(栈顶指针寄存器)这两个寄存器划定范围。esp寄存器始终指向栈的顶部,同时也就指向了当前函数的活动记录的顶部。而相对的,ebp的寄存器指向了函数活动记录的一个固定范围。
函数堆栈调用的实质
用下边的简单代码举例了解一下
int Add(int a,int b)
{
int c = a + b;
return c;
}
int main()
{
int a=10;
int b=20;
int c = Add(a,b);
printf("%d",c);
return 0;
}
型参开辟内存:调用方(如下代码)
main 函数栈布局:
int main()
{
011943A0 push ebp
011943A1 mov ebp,esp
011943A3 sub esp,0E4h
011943A9 push ebx
011943AA push esi
011943AB push edi
011943AC lea edi,[ebp-0E4h]
011943B2 mov ecx,39h
011943B7 mov eax,0CCCCCCCCh
011943BC rep stos dword ptr es:[edi]
int a=10;
011943BE mov dword ptr [a],0Ah //在栈上开辟空间,存放[a]
int b=20;
011943C5 mov dword ptr [b],14h //开辟空间,存放[b]
int c = Add(a,b);
011943CC mov eax,dword ptr [b] //将[b]移入到寄存器eax中
011943CF push eax //将eax压栈
011943D0 mov ecx,dword ptr [a] //将a的值移入到寄存器ecx中,
011943D3 push ecx //将ecx压栈
011943D4 call Add (0119110Eh) //call:调用被调用方函数(Add函数),压入下一行指令地址
011943D9 add esp,8 //栈顶指针移动
011943DC mov dword ptr [c],eax
printf("%d",c);
011943DF mov esi,esp
011943E1 mov eax,dword ptr [c] //将[c]的值放入eax中
011943E4 push eax //eax压栈
011943E5 push 119CC70h
011943EA call dword ptr ds:[11A03B8h]
011943F0 add esp,8
011943F3 cmp esi,esp
011943F5 call __RTC_CheckEsp (011912D5h)
return 0;
011943FA xor eax,eax
}
被调用方:(如下代码)
Add函数栈帧开辟:
int Add(int a,int b)
{
011928F3 sub esp,0CCh
011928F9 push ebx
011928FA push esi
011928FB push edi
011928FC lea edi,[ebp-0CCh]
01192902 mov ecx,33h
01192907 mov eax,0CCCCCCCCh
0119290C rep stos dword ptr es:[edi]
int c = a + b; //将运算结果保存到局部变量中
0119290E mov eax,dword ptr [a]
01192911 add eax,dword ptr [b]
01192914 mov dword ptr [c],eax
return c;
01192917 mov eax,dword ptr [c] //通过寄存器eax将返回值带回
}
0119291A pop edi
0119291B pop esi
0119291C pop ebx
0119291D mov esp,ebp //释放Add函数的栈帧空间
0119291F pop ebp
01192920 ret
函数调用过程
- 开辟型参的内存并初始化为0xcccccccc
- 压入下一行指令地址
- 压入调用方栈底指针的值
- 开辟局部变量所需要的栈空间并初始化
函数调用完成后的清栈
- 清理栈开辟的局部变量
- pop ebp(栈底指针寄存器) ebp回退到调用方栈底
- ret(pop pc)
ret包含两个动作:
1、出栈 ,栈顶指针向下移。出栈元素是下一行指令地址,赋给PC寄存器,PC寄存器永远存放下一行指令的地址。
2、ret运行完以跳转到下一行指令的地址
清栈的意义:告诉寄存器内存可以再次被分配。
函数的调用约定:
(1)thiscall: 类成员方法的调用约定,this指针存放于CX寄存器,参数从右到左压。
内置类型的调用约定(Calling convention):
(2)_cdecl:C标准的调用。型参由调用方开辟,被调用方清理。每一个调用它的函数都包含清空堆栈的代码,所以产生的可执行文件大小会比调用_stdcall函数的大。
(3)__stdcall:windows标准的调用约定,型参由调用方开辟,被调用方清理。
(4)__fastcall:快速调用约定,它是通过寄存器来传送参数的(实际上,它用ECX和EDX传送前两个双字(DWORD)或更小的参数,剩下的参数仍旧自右向左压栈传送,被调用的函数在返回前清理传送参数的内存栈)。
约定的意义:
1)约定了函数符号的生成。(不同的函数约定,函数符号的生成不一样)
2)约定了函数参数的压栈顺序。
3)约定了型参的开辟和清理方式。
接下来我们解决文章开头的问题???
1、型参在哪里开辟内存?
型参在栈上开辟内存,由调用方开辟。
2、型参的入栈顺序?
型参从右向左入栈,如果从左至右的话,不知道实参的传递个数。
3、函数返回值怎么带出来?
函数值存放到寄存器中,由寄存器带回
eax带回返回值:
(1)0<返回值<=4 由eax带回
(2)4<返回值<=8 由eax edx 两个寄存器带回
(3)8<返回值 由临时量带回(非类类型)
非类类型的返回方式:
- 临时量地址压栈
- 返回值拷到临时量里
- 临时量带回返回值
4、函数的返回值为什么会回退到栈里?
因为压栈压的是调用方的栈底指针
5、函数执行完成后怎么控制沿用点继续执行?
通过call指令继续。
call指令的作用:
(1)压入下一行指令的地址
(2)jump到被调用方函数
Tips:
实参与型参:
- 型参:用来接收调用该方法时传递的参数,被调用时分配空间,调用结束后就释放。
- 实参:传递给被调用方法的值,预先创建并赋予确定值。
int Add(int a,int b)//型参,用来接收调用该方法时传递的参数
{
int c = a + b;
return c;
}
int main()
{
int a=10;//实参,传递给被调用方法的值
int b=20;
int c = Add(a,b);
printf("%d",c);
return 0;
}
函数定义与函数说明:
- 声明:说明函数作用
int Add(int a,int b) //求两个整型数之和
- 定义:定义函数的实现功能,实现函数功能时,函数的名称,返回值,参数表必须要与此函数声明时一致。
int Add(int a,int b)//实现函数
{
int c = a + b;
return c;
}
- 当一个函数的定义在这个函数的调用之前,则不用声明,否则需要声明这个函数。
不需要声明
int Add(int a,int b)
{
int c = a + b;
return c;
}
int main()
{
int a=10;
int b=20;
int c = Add(a,b);
printf("%d",c);
return 0;
}
需要声明
int Add(int a,int b);//函数声明,需要加分号
int main()//被调用的函数在调用之后
{
int a=10;
int b=20;
int c = Add(a,b);
printf("%d",c);
return 0;
}int Add(int a,int b)
{
int c = a + b;
return c;
}