每一个程序的执行都使用了栈,栈被定义为一个特殊的容器,用户可以将数据压入栈中(入栈,push),也可以将已经压入的数据弹出(pop,出栈),但栈这个容器必须遵守一条规则“先进后出”。
在操作系统中,栈是动态内存区域,程序可以将数据压入栈中,也可以将数据从栈顶弹出。在i386下,栈顶由称之为esp的寄存器进行定位。
栈在程序运行中具有举足轻重的地位。栈保存了一个函数调用所需要的维护信息,被称之为堆栈帧或活动记录。一般包括如下几个方面的内容:
1.函数的返回地址和参数;
2.临时变量:包括函数的非静态局部变量以及编译器自动生成的其他临时变量;
3.保存的上下文:包括在函数调用前后需要保持不变的寄存器。
具体写一个例子来进行分析:
#include<stdio.h>
int sum(int a,int b)
{
int tmp=0;
tmp=a+b;
return tmp;
}
int main()
{
int a=10;
int b=20;
int ret=0;
ret=sum(a,b);
printf("ret=%d\n",ret);
return 0;
}
首先,先从main函数开始,查看main()函数的反汇编代码,进去之后会发现在一开始就看到如下部分:
紧接着,继续看反汇编,如下图
画出在栈中的整个过程:
整个函数的调用,结合上面两张图就基本明白了。那现在说一下刚开始的问题,在第一张图中也做了一点标注。对于每个函数的刚开始都会出现基本类似的指令。这一大堆指令总结起来就干了四件事情:
第一:将调用方的栈底地址入栈。====》push ebp
第二:让原本指向调用方栈底的ebp指向当前函数的栈底。====》mov ebp,esp
第三:给当前函数开辟栈帧。====>sub esp,44h
第四:对开辟的栈帧进行初始化。初始化的大小不一定。====>rep stos
所以对于sum函数我们可以理解,但是在main函数刚开始也有这些指令,不由地,我们知道,main函数也是通过一个函数来进行调用的,所以也需要上面这四个步骤!!此时也就可以回答图二中我画??的地方咯,它一定存的是调用main函数的函数栈底地址。是不是很清楚呀^O^
自己写一下整个过程:
#include<stdio.h>
int sum(int a,int b)
{
/*
push ebp
mov ebp,esp
sub esp,44h
push ebx
push esi
push edi
lea edi,[ebp-44h]
mov ecx,11h
mov eax,0xccccccch
rep stos ===>[esp,ebp]=0xcccccccc
*/
int tmp=0;//mov dword ptr[ebp-4],0
tmp=a+b;
/*
mov eax,dword ptr[ebp+8]
add eax,dword ptr[ebp+0ch]
mov dword ptr[ebp-4],eax
*/
return tmp;//mov dword ptr[ebp-4],eax
}
/*
mov eax,dword ptr[ebp-4]
mov esp,ebp
pop ebp
ret
*/
int main()
{
/*
push ebp
mov ebp,esp
sub esp,44h
push ebx
push esi
push edi
lea edi,[ebp-44h]
mov ecx,11h
mov eax,0xccccccch
rep stos ===>[esp,ebp]=0xcccccccc
*/
int a=10;//mov dword ptr[ebp-4],0Ah
int b=20;//mov dword ptr[ebp-8],14h
int ret=0;//mov dword ptr[ebp-0Ch],0
ret=sum(a,b);
/*
mov eax,ptr[ebp-8]
push eax
mov ebx,ptr[ebp-4]
push ebx
push ecx
call sum
add esp,8
mov dword ptr[ebp-0ch],eax
*/
printf("ret=%d\n",ret);
return 0;
}
总结一下吧~
1、函数的运行都是在栈上开辟内存。
2、栈是通过esp(栈顶指针)、ebp(栈底指针)两个指针来标识的。
3、对于栈上的访问都是通过栈底指针的偏移来访问的。
4、在call一个函数时,有两件事情要做:先将调用函数的下一行指令的地址压入栈中;再进行跳转。
5、在函数调用时检查函数是否申明、函数名是否相同、函数的参数列表是否匹配、函数的返回值多大。
①如果 【函数的返回值<=4个字节】,则返回值通过寄存器eax带回。
②如果 【4<函数的返回值<=8个字节】,则返回值通过两个寄存器eax和edx带回。
③如果 【函数的返回值>8个字节】,则返回值通过产生的临时量带回。
6、函数结束ret指令干了两件事:先出栈;再将出栈的值放到CPU的PC寄存器中。因为PC寄存器中永远放的是下一次执行指令的地址,所以就顺理成章的在函数调用完之后依旧接着原来的代码继续执行。
本文部分为转载文章,吃水不忘挖井人,写的特别详细,收获良多!以下是原作者信息:
作者:zhuoya_
原文:https://blog.youkuaiyun.com/zhuoya_/article/details/80516246