这里说的栈不是数据结构中的栈,而是计算机内存中的一块存储区,它的访问方式是“先进后出”。大多数情况下,栈是从高地址向低地址增长的。
栈有很多单元格,通常情况下每个单元格是8位的(即可以存8个0或1),称为数据宽度,是用来存放数据的。每个单元格都会对应一个地址,地址一般是无符号32位的整数,因此可以表示4294967295(32位无符号整数可以表示的最大值)个单元格。
关于栈的操作涉及到两个寄存器,即ESP和EBP(什么是寄存器这里就不多说了),另外还有两个指令,POP和PUSH。
首先说一下两个寄存器,这两个寄存器分别称为栈指针寄存器和基址指针寄存器,因此这两个寄存器中存的是指针,即地址(这个地址就是前面说到的单元格对应的地址),更确切的说应该是栈帧顶部的地址(ESP)和栈帧底部的地址(EBP)(栈帧是什么后面会讲到)。
下面是一个栈的结构图:
可以看到,栈是从高地址向低地址增长的,高地址对应栈底,低地址对应栈顶,每个存放数据的单元格都有一个对应的地址,每个单元格的宽度为8位。
下面说一下POP和PUSH指令,这两个指令都只能操作栈顶,PUSH是向栈中推入一个数放在栈顶,POP是将栈顶的数据弹出。无论是推入新的数据还是弹出数据,ESP始终指向栈顶,也就意味着ESP会不断改变。
注意到上面我写了个栈帧,下面解释一下栈帧是什么。
2、栈帧
栈帧是一块连续的栈区(注意看上面的图),每个函数都有自己的栈区,这个栈区就称为栈帧。注意,函数调用所占用的栈区才称为栈帧,并且每个函数都会有自己的栈帧,包括main函数。当前栈帧的范围在EBP和ESP指向的区域之间。
当然栈帧不是固定不变的,由于推入新的数据和弹出数据ESP都会改变,因此栈帧的大小也会随之改变,但是EBP一般不会改变。
那么什么时候会改变EBP?
前面说到,每个函数都有自己的栈帧,也就是有自己的EBP和ESP,那么当一个函数调用另外一个函数的时候,第二个函数也应该有自己的栈帧,也有自己的EBP和ESP,但是系统中只有一个相应的寄存器(即只有一个ESP寄存器和一个EBP寄存器)。因此这个时候就要改变EBP的值,在改变EBP的值之前把旧的EBP的值保存一下就可以了(这个后面还会讲到)。
3、保存寄存器
为什么要保存寄存器?
首先,寄存器数量有限,因此寄存器是被所有的函数共享的。假设现在有A、B两个函数,A函数调用了B函数。A在调用过程中往EAX中存放了数据x,B在调用过程中往EAX存放了新的数据y,那么当B函数调用结束返回到A函数后,A继续使用EAX中的值,但这个时候EAX中的值已经不是最初存的x了。
怎么保护?
方法很简单,在B函数使用EAX寄存器之前,在准备阶段(刚进入B函数时)先将寄存器中的值保存到栈中,用完以后,在结束阶段再从栈中将值重新写入EAX中。
注:并不是所有通用寄存器中的值都由被调用函数保存,通常调用函数保存一部分,被调用函数保存一部分。IA-32规定,寄存器EAX、ECX、EDX是调用者保存寄存器,寄存器EBX、ESI、EDI是被调用者保存寄存器。
假设有两个函数A和B,A函数调用B函数,调用B函数需要传入两个参数x和y。
1:在A调用B函数之前,A先把需要传入B函数中的参数x,y推入栈中。(注意x,y被放在了A的栈帧中)
注:上面一句步骤没有说先把调用者保存寄存器推入栈中,事实上并不是每次调用函数都要把调用者保存寄存器推入栈中,只有在必要的时候才会进行保护(编译器自己决定)。
2:在执行函数调用的时候(即执行call指令),A把B函数的返回地址推入栈中(还是在A的栈帧中)。
3:在进入B函数后,推入旧的EBP的值,这时ESP指向的单元格中的内容是旧的EBP,然后令EBP等于当前的ESP,则这个EBP即为B函数的栈帧的栈底,EBP指向的单元格中的数据是旧的EBP。这里比较抽象,下面有一个图可以帮助理解。
(上图有个地方写错了,EBP应该是B函数栈帧的栈底)
4:开辟一块相对合适的空间用来存放非静态局部变量(存放非静态局部变量前会先置成cc)。
5:将被调用者保存寄存器中的内容推入栈中进行保护。
6:执行函数中的内容。
7:函数调用完毕,return。返回的时候将EDI、ESI、EBX依次弹出,然后让ESP指向EBP,将ESP指向的单元格中的内容(即旧的EBP)弹到EBP中,这样EBP又重新变成了A的栈帧的栈底,执行ret指令,弹出B的返回地址,然后ESP根据参数的个数加上相应的数使ESP指向原来A的栈帧的栈顶(一个参数加4,两个加8,以此类推)。这里有一点需要注意,ESP的值虽然改变了,但是栈中B的参数还存在,但是无所谓,在推入新的数据的时候就会被覆盖掉。
注意上图ESP = ESP+8后ESP指向了原来A栈帧的栈顶,但是B的参数还存在,不过当有新数据进来的时候就会被覆盖掉。
这里有个问题,B函数的参数在A函数的栈帧中,B函数应该怎么去读取使用呢?
注意推入参数和函数返回地址后就改变了EBP,因此EBP和函数的参数紧挨着(也就是说参数在B能够访问到的地方),那么怎么去访问呢?拿上面的例子讲,参数x在EBP+8中,参数y在EBP+12中。
以上内容来自https://blog.csdn.net/theLostLamb/article/details/79950815 感谢博主分享
下面我结合一个实际例子
int add(int a, int b) { int c = 0; c = a + b; return c; } int main() { add(1, 2); return 0; } /*** ## 下面是main的汇编代码 0x0040162d <+0>: push %ebp 0x0040162e <+1>: mov %esp,%ebp 0x00401630 <+3>: and $0xfffffff0,%esp 0x00401633 <+6>: sub $0x10,%esp 0x00401636 <+9>: call 0x401f40 <__main> [CS和IP 压入栈中 sep 下移8个字节] 1 => 0x0040163b <+14>: movl $0x2,0x4(%esp) [ebp 0x60feb8 esp 0x60fea0] 0x00401643 <+22>: movl $0x1,(%esp) 0x0040164a <+29>: call 0x401610 <add> [CS和IP 压入栈中 sep 下移8个字节] 6 => 0x0040164f <+34>: mov $0x0,%eax [ebp 0x60feb8 esp 0x60fea0] 0x00401654 <+39>: leave 0x00401655 <+40>: ret ## 下面是add函数的汇编代码 0x00401610 <+0>: push %ebp [ebp 0x60feb8 esp 0x60fe98] 0x00401611 <+1>: mov %esp,%ebp 0x00401613 <+3>: sub $0x10,%esp 2 => 0x00401616 <+6>: movl $0x0,-0x4(%ebp) [ebp 0x60fe98 esp 0x60fe88] 3 => 0x0040161d <+13>: mov 0x8(%ebp),%edx [ebp 0x60fe98 esp 0x60fe88] 0x00401620 <+16>: mov 0xc(%ebp),%eax 0x00401623 <+19>: add %edx,%eax 0x00401625 <+21>: mov %eax,-0x4(%ebp) 4=> 0x00401628 <+24>: mov -0x4(%ebp),%eax [ebp 0x60fe98 esp 0x60fe88] 5=> 0x0040162b <+27>: leave [ebp 0x60fe98 esp 0x60fe88] 0x0040162c <+28>: ret */