本文已参与「新人创作礼」活动,一起开启掘金创作之路
一.什么是“栈”
“栈”的概念,是指它的访问规则。 “栈”的定义是,最后存入的东西,总是第一个被取走。 如下图,我们放入一个整形数据18
再放入一个整形数据20
注意,我们放的顺序是18 20 ,可当拿出数据时,先出来的却是20,后出来的是18。 就像是我们向木桶里放苹果,最后放进去的苹果是最先拿出的。 这是栈区别于其他结构的主要特点,即“后入先出”((last in first out)。
二.“栈”在内存中的实现
在各种计算机系统中,最常见的栈实现方式如图所示。
它是由一段连续内存空间和一个寄存器(栈指针)组成。
栈指针是一个寄存器,它始终指向栈的顶部(即最近被压入的元素)。
每个被压入栈中的元素,在内存空间里占据一个独立的位置。
我们把放入数据成为压栈(一个苹果压在另一个苹果上面)。
首先,我们压入(PUSH)数据18,栈指针TOP移动,指向最后压入的值。
我们把弹出(POP)数据成为出栈(把一个苹果从另一个苹果上面拿走)。
上图中,我们压栈三次,出栈两次,TOP指针下移两次,指向最后压入的值。
三.函数栈帧的创建和销毁
1.我们先了解寄存器的概念
2.让我们以以下代码为例
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 20;
int c = 0;
c = Add(a, b);
printf("%d", c);
}
每一个函数调用,都要在栈区上创建一块空间。 正在调用哪个函数,ebp和esp就维护哪个函数的函数栈帧
进入main函数,ebp和esp维护main函数的函数栈帧
我们按下F10,右键转到反汇编,可看到以下代码
由于main函数是由__tmainCRTStartup调用的,
先分配__tmainCRTStartup的函数栈帧。
观察反汇编,第一行有push ebp
我们把ebp压入栈中,sep顺势上移指向ebp
我们看下一行
mov ebp,esp
mov是把后面的值给前面, esp存的是地址,因此ebp上移到esp的位置上
sub esp,0e4h
esp地址减小了0e4h,向上移到低地址的某个位置
如下图所示:
而他们之间的空间就是为main函数开辟的
我们继续向下走:
遇到三个PUSH,在顶上压进三个元素。
继续向下:
lea edi ebp-04eh
lea即 Load effective address 诶嘿,好熟悉的04eh
mov ecx 39h mov eax 0CCCCCCCCh rep stos dword ptr es :[edi]
要把从刚刚edi开始向下的39h次double word 初始化为 0CCCCCCCCh
我们继续:
ebp-8处放置a
经过验证,b和a隔了两个字节
到这里,我们就明白了局部变量是如何创建的。
首先我们为函数调用创建函数栈帧,在函数栈帧里找到一些空间,把变量放进去。
继续向下:
mov eax,dword ptr [ebp-14h] push eax
ebp-14h,这不是b吗?
把b的值放进eax里
压栈eax
同理得:
call指令调用函数,又把下一条指令的地址压栈
进入Add函数
进行同样的处理,可得:
把ebp+8的值给eax,这不是之前压栈的a吗
把ebp+12的值和ebp+8的值求和给eax,这不是a+b吗
最后把ebp-8的值(z)给寄存器eax
pop三次
mov esp,ebp
把ebp赋给esp,esp指向ebp
pop ebp
pop之后,ebp回到main函数的函数栈帧底部
esp向下移动一位,回到main函数的函数栈帧顶部
这块空间又由esp和ebp维护。
ret
ret返回call指令下一条指令的地址。
add esp,8
至此,esp+8,a和b的空间被销毁 并把z传出。