病理网,正月,房间,合唱团,景观
当前位置:主页 > 文章 > 正文

宝宝体检说心脏有杂音,为什么一些人说堆栈是程序的心脏?

时间:

堆栈就是一块内存,操作系统启动时已经分配好的供程序使用的。在这里说明一下,这里介绍的堆栈与数据结构中的堆栈无关。为什么要有堆栈?堆栈是程序执行过程中必须使用的一块内存,任何一个程序需要用到的关键数据、临时数据等,都会在堆栈中有一定的体现,所以我们可以说堆栈是程序的心脏。堆栈不平衡程序就会报错,没办法执行下一个函数,比如有时会报错C0000005错误。

我们知道每个应用程序都有独立的4GB内存空间,是不是当前运行的应用程序拥有的4GB内存空间都可以使用哪?当然不是,虽然说名义上有4GB内存空间,但是在你真正要用某一块内存时,一定要向操作系统申请,换句话说你要告诉操作系统,我们把这个过程称为内存申请。

不同的语言,在申请内存时写法是不同的,但它们的本质是相同的,都是申请内存。至于怎么申请,怎么实现,这就和C语言相关了,这里暂不介绍了。

示例:我们回顾一下DTDebug.exe界面,首先打开DTDebug.exe软件,将飞鸽软件拖到DTDebug.exe软件中,如图2-10-1所示,DTDebug.exe界面包括汇编窗口、寄存器窗口、内存窗口、堆栈窗口,本节主要介绍堆栈窗口和寄存器窗口中的两个寄存器,这两个寄存器分别为ESP、EBP。

看图2-10-1堆栈窗口中的内存,都是已经向操作系统申请过的。那么我们怎么知道申请了多少堆栈哪?看图2-10-1寄存器窗口中有FS位,它对应的数据是0x00272000,在DTDebug.exe软件中找到命令窗口,输入dd 0x00272000,如图2-10-2所示:

点击键盘上的Enter键,如图2-10-3所示:

在内存窗口中,内存地址0x00272004对应的数据是0x001A0000 (Top of thread's stack),内存地址0x00272008对应的数据是0x0019D000 (Bottom of thread's stack)。这就是操作系统为我们分配的堆栈地址,从0x0019D000到0x001A0000,堆栈窗口中的内存地址就在这个范围的。不同的系统分配的大小会有差异。由于堆栈是操作系统专门给程序用的,程序在执行过程中必须使用堆栈,所以它又可以看做是一块特殊的内存。

堆栈窗口中的内存地址是从高内存地址到低内存地址用的,也就是从内存地址0x001A0000开始使用到内存地址0x0019D000结束,如果用完了,就会报错,提示堆栈溢出。那我们怎么知道当前程序堆栈使用到哪里了?比如图2-10-3汇编窗口中,当前程序停在了0x77068E34,我们看寄存器窗口中,ESP寄存器存储的数据为0x0019FFF0,则0x0019FFF0为当前程序堆栈使用到的地方。那么从内存地址0x001A0000到内存地址0x0019FFF0都已经被使用过了如图2-10-4所示:

为什么是ESP寄存器哪?为什么不是别的寄存器?在之前我们介绍过,32位汇编中8个通用寄存器都有各自的用途,ESP为栈指针,用于指向栈的栈顶,也就是说当前使用到的存储数据的内存地址。有指向栈顶的指针,肯定有指向栈底的指针,那就是EBP寄存器,EBP为帧指针,指向当前活动记录的底部。

我们知道了堆栈的知识,那我们该怎么使用堆栈哪?接下来介绍堆栈的使用

2.10.2【堆栈的使用】

我们当前程序执行到某个阶段时,中间会产生大量的数据,而这些数据将会暂时保存在堆栈中。如果我们现在要使用堆栈,假如有临时数据0x00000001和临时数据0x00000002,我们怎么能让这些临时数据暂时保存在堆栈中哪?如图2-10-4寄存器窗口中ESP存储的当前数据为0x0019FFF0,说明堆栈已经使用到了内存地址为0x0019FFF0,临时数据只能存储在内存地址0x0019FFEC往上的地方。知道存储在哪了,接下来就是写汇编指令将临时数据保存在堆栈中了。

我们可以用已经学过的指令:

第一步:输入指令,如图2-10-5所示:

MOV DWORD PTR DS:[0x0019FFEC],0x00000001

MOV DWORD PTR DS:[0x0019FFE8],0x00000002

第二步:按F8执行,并观察如图2-10-6、2-10-7所示。

两次按F8执行后,临时数据0x00000001和临时数据0x00000002都已经暂时存储在了堆栈中。但ESP寄存器存储的数据并没有发生变化,这时我们要告诉堆栈当前已经使用到内存地址为0x0019FFE8的地方,若不告诉堆栈,则下一次再存储数据时可能会覆盖掉之前存储的临时数据,那么该如何告诉堆栈哪?

第一步:输入指令,如图2-10-8所示:

SUB ESP,4

SUB ESP,4

因为向堆栈中写入了2次数据且堆栈中的数据是从高地址到低地址,所以将esp的值减少8个字节,也可写成SUB ESP,8。

第二步:按F8执行并观察ESP寄存器存储的数据变化及堆栈窗口的变化,如图2-10-9、2-10-10所示:

图2-10-9堆栈窗口中的黑色定位光标显示在了内存地址0x0019FFEC中,而ESP存储的数据为0x0019FFEC,说明堆栈已经记录了数据0x00000001。我们接着按F8观察。

图2-10-10堆栈窗口中的黑色定位光标显示在了内存地址0x0019FFE8中,而ESP存储的数据为0x0019FFE8,说明堆栈已经记录了数据0x00000002。

我们以后再使用数据时,可以先提升栈顶,再存入数据。比如再向堆栈中存入临时数据0x33333333,我们先提升栈顶, 在输入数据。

第一步:先提升栈顶,如图2-10-11,ESP存储的数据为0x0019FFE8。

SUB ESP,4

第二步:按F8执行并观察数据变化如图2-10-12所示。

图2-10-12堆栈窗口中的黑色定位光标显示在了内存地址0x0019FFE4中,而ESP存储的数据为0x0019FFE4,说明堆栈已经提升了。

第三步:向堆栈中写入数据,如图2-10-13所示,当前ESP存储的数据为0x0019FFE4,内存地址0x0019FFE4存储的数据为0x00000000。

MOV DWORD PTR SS:[ESP],0x33333333

第四步:按F8执行并观察数据变化,如图2-10-14。

图2-10-14寄存器窗口中ESP存储的数据为0x0019FFE4,内存地址0x0019FFE4存储的数据为0x33333333。

假如我们存储的临时数据0x00000001、0x00000002和0x33333333,使用后不需要再用,那我们该如何释放被临时数据占用的内存哪?由于我们使用的内存地址总是栈顶指针ESP的相对位置,所以我们只要修改ESP的值,就可以将这一块内存释放出来供下次使用,所以用完这3个临时数据后,我们恢复原来的堆栈。

第一步:由于是存放了3个临时数据,输入指令,如图2-10-15所示。

ADD ESP,4

ADD ESP,4

ADD ESP,4

或者ADD ESP,0xC

图2-10-15中,ESP存储的数据为0x0019FFE4,当前栈顶为内存地址0x0019FFE4。

第二步:按F8单步执行并观察堆栈窗口中黑色光标区域栈顶的变动,如图2-10-16所示。

图2-10-16中,堆栈窗口中的黑色定位光标区域为内存地址0x0019FFF0,寄存器窗口中ESP存储的数据为0x0019FFF0,所以成功恢复到一开始没有存储临时数据的内存地址。

大家肯定看到图2-10-16中我们存储的临时数据还存在,这里我们已经恢复了栈顶,至于之前存储的临时数据对我们来说毫无影响了,下次存储临时数据时可以直接覆盖,对我们调试程序没有影响。

【拓展:我们在使用C语言或其他语言时,为什么要给局部变量赋初始值,原因就在这,如果没有初始化,只是申请变量空间,那么这里面的值就会影响我们的结果。】

以上我们是用基础指令来演示堆栈变化,汇编语言还给我们提供了一些简化指令。

【PUSH指令】

PUSH指令它的功能是:

1、向堆栈中压入数据

2、修改栈顶指针ESP寄存器的值

PUSH 指令的格式:

PUSH r32

PUSH r16

PUSH m16

PUSH m32

PUSH imm

我们push立即数的时候,默认为4个字节,这里不能使用8位寄存器或者内存。

例:

我们动手做实验,输入以下指令,如图2-10-17所示:

PUSH 0x33333333

第二步:按F8执行并观察数据变化,如图2-10-18所示。

看图2-10-18寄存器窗口ESP存储的数据为0x0019FFEC,堆栈窗口中黑色定位光标区域为内存地址0x0019FFEC,为当前栈顶,内存地址0x0019FFEC存储的数据正是我们压入的数据0x33333333,所以可以总结出,PUSH 0x33333333这条指令相当于:

1、MOV DWORD PTR SS:[ESP],0x33333333

2、SUB ESP,4

学汇编我们不要记汇编的格式,要真正掌握它的工作原理,当我们能用其他的指令实现相同的功能才能真正学会了汇编。接下来我们学习另一个堆栈指令,我们记住它的功能,并能用其它指令代替,才能算掌握。

【POP指令】

POP指令功能:

1、将栈顶数据存储到寄存器/内存

2、修改栈顶指针ESP寄存器的值

POP指令格式:

POP r32

POP r16

POP m16

POP m32

POP指令是将栈顶指针指向的数据取出,所以必须要有一个容器去接收POP指令弹出来的值,所以POP 后面不能是立即数

例:

我们动手做实验,输入以下指令,如图2-10-19所示,当前ESP存储的数据为0x009FFF0,内存地址0x009FFF0存储的数据为0x11111111,也是栈顶:

POP ECX

第二步:按F8执行后观察数据变化,如图2-10-20所示:

看图2-10-20寄存器窗口中,ESP存储的数据为0x0019FFF4,堆栈窗口中黑色定位光标区域为内存地址0x0019FFF4,为当前栈顶,ECX存储的数据正是我们取出的数据0x11111111,所以可以总结出,POP这条指令相当于:

1、MOV DWORD PTR SS:[ESP]

2、ADD ESP,4

总结:PUSH、POP是堆栈相关的指令,PUSH表示将数据压入堆栈中,同时栈顶提升相应宽度的字节,POP表示将栈顶的数据取出来放到某个容器里,同时栈顶减少相应宽度的字节。


思考一下:如何使用3种方式实现:PUSH ECX

版权保护: 本文原创,转载请保留链接: 为什么一些人说堆栈是程序的心脏?