栈描述的是代码运行过程中,操作系统为调度程序之间的相互调用关系,或临时存放操作数而设置的一种数据结构。
为了更好理解代码中调用函数时相关数据的流动过程,了解PE在运行时对临时变量的处理方法,我们先重新认识一下栈。栈是程序运行时,操作系统为调度程序之间相互调用关系或临时存放操作数而设置的一种数据结构,事实上,栈就是内存的一块区域。因为在这块区域中存取数据遵循一定的规则,所以叫做数据结构。
栈遵循的规则就是先进后出,可以简单把栈理解为一个有底的容器,先放进去的东西自然放在最底下,后放进去的东西一定是先被取出。
程序在运行的时候会为系统分配一块内存区作为栈,由栈选择子ss和栈顶指针(esp)来确定当前栈的大小,CPU则直接操作ebp来存取数据。
内存中栈的结构如下所示:
往栈里存放数据称为压栈,压栈时根据压入数据类型的大小的字节大小,将esp减少到相应的字节数,如压入一个双字,则esp-4 从栈里取出数据称为出栈,出栈时根据弹出的数据类型的字节大小,将esp增加到相应的字节数,如弹出一个双字,则esp+4
栈的应用场合
操作系统通过修改esp指令来完成对栈中数据的压栈和出栈。以下场合会用到栈
保存临时的值
保存程序现场
传递函数参数
存放过程中的局部变量
1 保存临时的值
栈可用于在程序中暂时保留寄存器的值,完成某些操作后再恢复。例如以下代码:
push eax
..............;进行某项操作
pop eax
当进行某项操作时用到寄存器eax,导致eax的值发生变化,操作完后还想用eax的值做其它处理,这时候就可以通过栈实现对寄存器的值的临时保存。进行操作前通过指令push 将eax压入栈保存。操作执行完,无论eax的值是否被修改,都会从栈里取回原始的值给eax.
栈还可以实现对寄存器的赋值,如下:
push eax
pop edx
通过指令push压到栈的eax的值最终被pop弹出,弹出的值存储在edx中,通过这样一种方式实现了对寄存器的赋值。等价于mov edx eax
在对栈进行操作时,一定要维系栈的平衡,即压入多少字节最后就要弹出多少字节,否则会导致程序运行出现错误。
2 保存程序现场
在调用子程序时,栈可用于保存当前现场,如下:
call_subPrg
当指令执行时,会将紧跟在call指令后面的下面一条指令压入栈,以便于程序在调用完之后,能正确返回到主程序继续运行。
对16位系统而言,有两种call指令,一种称为长调用,即跨段调用,另一种称为短调用。长调用和断掉用的区别在于:是否将cs压入栈。一旦用户的程序中使用lcall这条指令,计算机会将cs,ip依次压入栈;而当过程返回时,可以通过iret将cs,ip弹出。对于短过程调用计算机只将ip压入栈,程序返回时则调用ret指令将ip 弹出。
对32位系统而言,单独一个寄存器的长度就能访问到完整的4GB的虚拟内存空间,所以调用call指令时,不需要往栈里压入cs寄存器,当然也没有长短调用之说,32位汇编语言中的cs依然是16位,其中存放了段的描述符,通常称为段选择子。
扩展:什么是段选择子
在实模式下,程序通过 段地址+程序偏移地址的方式来定位一个数据,段地址有不同的段寄存器来指定,80X86的段寄存器包括:CS,DS,ES,FS,GS,SS等,在实模式下,一个数据所在的地址是这样计算出来的:
假设我们在SS存入0x1000.SP中存入0xFFFF,那么SS:SP=0x1000*0x10+0xFFFF=0x1FFFF,这就是左移4位加偏移。
在保护模式下,程序通过 段选择子+程序偏移地址 的方式来定位一个数据,段寄存器还存在,但意义已经发生了变化: 段寄存器被称为段选择子,其所标示的值不再与直接的地址有关系,它指向了保护模式中 全局 /局部描述表的某个位置,在这张表中记录了真正的段地址。
段选择子是一个2字节的数,共16位。其中最低两位表示RPL,第三位表示查那张表,是利用GDT(全局描述符表)还是LDT(局部描述符表),剩下的高16位给出了所需的描述符在描述表中的地址.(注意16位正好足够寻址8KB项)。
保护模式下的SS:SP是如何计算的?
0x1000=1000000000000b=10 0000 0000 0 00
SS:SP = 全局描述符表中的第0x200项描述符给出的段基址+0xFFFF
所以,实模式和保护模式下SS :SP的寻址方式是不一样的,但他们都是一种映射,只不过映射的规则略有不同而已。
传递函数参数
当调用某个函数的时候,可以使用栈传递参数,以下是masm32对调用对话框显示信息的汇编指令。
invoke MessageBox,NULL,offset szText,offset szCaption ,MB_OK
经过汇编以后形成的代码如下:
:0x401000 6A 00 push 00000000
...
...
..
..
.
调用API函数的指令invoke实际对栈进行了一下两步的设置:
首先:将参数按照从右往左的顺序压栈,(不同的程序设计语言其通过栈传递参数的顺序并不相同),其次,调用call指令,也就是将eip也压入栈。
当子程序结束以后,会调用ret函数返回,eip随之被弹出。为了平衡栈,需要调用者使用如下语句将传入的参数一一弹出:
add esp , 0010h; 4个整
4 存放过程中的局部变量
进入一个过程以后,会定义许多局部变量,而局部变量的存放值也是栈。为局部变量在栈中申请的内存区区域称为缓冲区。当过程结束后,局部变量将从栈中删除,恢复到进入过程中的最初状态。也就是说,局部变量在过程结束以后就自动被释放了,原因是CPU调整了栈的栈顶指针esp.