x86 反汇编/堆栈
一般来说,堆栈是一种数据结构,它将数据值连续存储在内存中。但是,与数组不同的是,您只能访问(读取或写入)堆栈的“顶部”数据。从堆栈读取被称为“出栈”,写入堆栈被称为“入栈”。堆栈也称为 LIFO 队列(后进先出),因为值从堆栈中弹出的顺序与它们被推入堆栈的顺序相反(想想您是如何在一张桌子上堆放盘子的)。出栈的数据从堆栈中消失。
所有 x86 架构都使用堆栈作为 RAM 中的临时存储区域,允许处理器快速存储和检索内存中的数据。esp 寄存器指向堆栈的当前顶部。堆栈“向下”增长,从高内存地址到低内存地址,因此最近被推入堆栈的值位于高于 esp 指针的内存地址中。没有寄存器专门指向堆栈的底部,尽管大多数操作系统监控堆栈边界以检测“下溢”(弹出空堆栈)和“上溢”(将太多信息推入堆栈)条件。
当从堆栈中弹出值时,该值将保留在内存中,直到被覆盖。但是,您永远不要依赖 esp 之下内存地址的内容,因为其他函数可能会在您不知情的情况下覆盖这些值。
Windows ME、98、95、3.1(及更早版本)的用户可能仍然记得臭名昭著的“蓝屏死机”——有时是由堆栈溢出异常引起的。当写入堆栈的数据过多,堆栈“增长”超出其限制时,就会发生这种情况。现代操作系统使用更好的边界检查和错误恢复来减少堆栈溢出的发生,并在堆栈溢出后保持系统稳定性。
以下 ASM 代码行基本等效
push eax
|
sub esp, 4
mov DWORD PTR SS:[esp], eax
|
pop eax
|
mov eax, DWORD PTR SS:[esp]
add esp, 4
|
但单个命令的执行速度实际上比替代方法快得多。可以将其可视化为堆栈从右到左增长,并且 esp 在堆栈大小增长时减小。
压栈 | 出栈 |
---|---|
此代码示例使用 MASM 语法 |
假设我们想要快速丢弃之前推入堆栈的 3 个项目,而不保存这些值(换句话说,"清理"堆栈)。以下方法有效(注意它会覆盖 eax 寄存器)
pop eax
pop eax
pop eax
但是,有一种更快的方法,也不会影响除堆栈指针以外的任何寄存器。我们可以简单地对 esp 执行一些基本算术运算,使指针“超过”数据项,这样就不能再读取它们,并且可以在下一轮 push 命令中用它们覆盖。
add esp, 12 ; 12 is 3 DWORDs (4 bytes * 3)
同样,如果我们想要在堆栈上为大于 DWORD 的项目保留空间,我们可以使用减法来人工向前移动 esp。然后,我们可以直接将保留的内存访问为内存指针,或者我们可以将它间接访问为 esp 本身的偏移量值。
假设我们想要在堆栈上创建一个字节值数组,长度为 100 个项目。我们想要将指向该数组基址的指针存储在 edi 中。我们该怎么做?以下是一个示例
sub esp, 100 ; num of bytes in our array
mov edi, esp ; copy address of 100 bytes area to edi
要销毁该数组,我们只需编写以下指令
add esp, 100
要读取堆栈上的值而不将其从堆栈中弹出,可以使用 esp 以及偏移量。例如,要将堆栈顶部的 3 个 DWORD 值读取到 eax(但不使用 pop 指令),我们将使用以下指令
mov eax, DWORD PTR SS:[esp]
mov eax, DWORD PTR SS:[esp + 4]
mov eax, DWORD PTR SS:[esp + 8]
请记住,由于 esp 在堆栈增长时向下移动,因此可以使用正偏移量访问堆栈上的数据。永远不要使用负偏移量,因为堆栈“上方”的数据不能保证保持您离开时的状态。从堆栈读取而不弹出的操作通常被称为“窥视”,但由于这不是正式术语,因此本维基教科书不会使用它。
计算机内存中有两个区域可以存储程序数据。第一个是我们一直在讨论的堆栈。它是一个线性 LIFO 缓冲区,允许快速分配和释放,但大小有限。堆通常是一个非线性数据存储区域,通常使用链表、二叉树或其他更复杂的方法实现。堆比堆栈更难与之交互和维护,分配/释放执行得更慢。但是,堆可以随着数据的增长而增长,并且当数据量变得太大时,可以分配新的堆。
正如我们将在后面看到的那样,显式声明的变量是在堆栈上分配的。堆栈变量的数量有限,并且具有确定的尺寸。堆变量的数量和尺寸可以是可变的。我们将在后面更详细地讨论这些主题。