栈
顾名思义,“栈”就是相当于”客栈”的意思,在计算机系统中,栈就是数据暂存的一片动态内存。栈可以用来实现函数参数的传递,局部变量的分配和释放,也可以用来储存返回信息,保存寄存器以供恢复调用前处理机状态。
每当调用一个函数,系统会为这个函数分配栈空间,为单个函数分配
的栈空间为栈帧(stack frame)。
图中寄存器%ebp为帧指针,寄存器%esp为栈指针
假设有一段代码:
#include<stdio.h>
int sum(int a,int b){
int c=a+b;
return c;
}
int main(){
int num1=10;
int num2=20;
int s=sum(num1,num2);
return 0;
}
我们用VS2015进行反汇编得出:
//sum函数的部分指令
int sum(int a, int b) {
010D1690 push ebp
· · ·
010D16C0 ret
//main函数的部分指令
int main()
{
· · ·
010D1704 call sum (010D1023h)
010D1709 add esp,8
· · ·
我们从上面的反汇编代码可以看到当main函数调用sum函数时执行
call指令我们从后面的地址中可以知道call指令并不能直接跳转到
sum函数,用vs2015按F11跳转到一个新的语句:
00A91023 jmp sum (0A91690h)
jmp指令将跳转到sum函数的第一条指令
我们可以知道call指令的效果是将返回地址(紧跟着call指令的下一条指令的地址(0x010D1709))压入栈和跳转到被调用函数的栈帧。
进入被调用函数的栈帧的时候,首先将调用函数的ebp的值压入栈
当被调用函数一直执行到ret指令时,ret指令弹出返回地址,并跳转到该地址。
我们发现在调用函数和被调用函数都有以下语句:
012D1690 push ebp
012D1691 mov ebp,esp
012D1693 sub esp,0CCh
012D1699 push ebx
012D169A push esi
012D169B push edi
012D169C lea edi,[ebp-0CCh]
012D16A2 mov ecx,33h
012D16A7 mov eax,0CCCCCCCCh
012D16AC rep stos dword ptr es:[edi]
012D1691 mov ebp,esp
将栈底下移
012D1693 sub esp,0CCh
0cch大小的空间可用于在调试时编辑代码时可能添加的局部变量。
012D1699 push ebx
012D169A push esi
012D169B push edi
保存调用函数中寄存器的值
012D169C lea edi,[ebp-0CCh]
下移edi到栈顶
012D16A2 mov ecx,33h
33h=0cch/4,刚才额外的空间占用33h个4字节空间
012D16A7 mov eax,0CCCCCCCCh
012D16AC rep stos dword ptr es:[edi]
初始化刚才的额外的空间
012D16BA pop edi
012D16BB pop esi
012D16BC pop ebx
012D16BD mov esp,ebp
012D16BF pop ebp
012D16C0 ret
当ret指令之前,ebx,esi,edi恢复原来的值,栈顶上移,恢复原来栈帧的ebp。
还有个问题就是,当我们在传递参数时,参数列表是从右往左顺序压栈的,这是为什么呢?
012D16FC mov eax,dword ptr [num2]
012D16FF push eax
012D1700 mov ecx,dword ptr [num1]
012D1703 push ecx
这就引出c语言的一个特性了——可变长参数
c语言入栈从右往左,栈底的元素就是参数列表中最右边的一个,我们只需知道第一个参数的位置,剩下到栈底都是可变参数,假若从左往右,我们则不能知道最右边的参数位置,也就无法实现可变参数了。
c语言中的printf就是可变参数函数。