0x1

我最近在学习逆向,所以写写博客一来是增加文章数量,二来是加深记忆。我现在已学到堆栈,所以写这篇博文,记录一下自己学到的东西。

0x2

堆栈的优点是可以临时存储大量的数据,而且便于查找。这些数据储存在内存中,而不是储存在CPU里的寄存器中。实现一个堆栈结构需要使用两个寄存器,一个用来储存栈底地址,一个用来储存栈顶地址(我用ebp做栈底寄存器,用esp做栈顶寄存器,在很多地方都是用esp寄存器做堆栈指针的嘛,esp就是extended stack pointer的缩写,翻译过来就是栈指针的意思)。

数据压栈时,栈底指针不动,而栈顶指针永远指向栈顶的位置。在windows中,内存地址是从大到小的顺序排列的,那么栈底的内存编号必是大于栈顶的内存编号的。所以压栈时栈顶指针是减去一个单位,而出栈时栈顶指针是加上一个单位。通过计算栈顶到栈底的偏移量也可以计算出堆栈中储存了多少数据,这也是堆栈的另一个优点。

如果要取出堆栈中间某一个位置的数据,只需要使用栈顶或者栈底的位置加上偏移量就可以了。

0x3

我这里用汇编实现了一个堆栈,并且将1、2、3、4压入堆栈,栈底地址为0x12FFFC。

Ps: 我是直接用od打开一个exe文件,然后修改执行汇编代码的

首先就是把内存地址放进寄存器里面,栈底地址是0x12FFFC,那么就是0x12FFFC储存进ebp寄存器里面。现在还是一个空堆栈,所以栈顶的位置和栈底是一样的,也是12xFFFC。

1
2
mov ebp,0x12FFFC
mov esp,0x12FFFC

按下F8单步执行后,esp寄存器和ebp寄存器的值就都变成栈底地址0x12FFFC了。

堆栈已经创建好了,接下来就是压栈。我使用先压栈,后移动指针的方式。

1
mov dword ptr ds:[esp],0x1

执行后,内存0x12FFFC里面的值就变成1了。

Ps: 内存的最小单位是字节,1个字节等于8位,也就是8个0或者1。

除了DWORD,还有两种数据宽度:byte和word。

1
2
3
BYTE: 1字节 8位
WORD: 2字节 16位
DWORD: 4字节 32位

压栈以后紧接着就要移动堆栈指针,我刚才使用的数据宽度是DWORD,DWORD占用4个字节,那么堆栈指针就需要减去4个字节的位置。

1
sub esp,4

执行以后,esp寄存器里面的值就是0x12FFFC减去4的值,也就是0x12FFF8。

按照这个步骤继续把剩余的数字压入堆栈:

1
2
3
4
5
6
mov dword ptr ds:[esp],0x2
sub esp,4
mov dword ptr ds:[esp],0x3
sub esp,4
mov dword ptr ds:[esp],0x4
sub esp,4

F8单步执行以上几行汇编代码:

执行完以后可以看到栈底指针ebp指向0x12FFFC,栈顶指针esp指向0x12FFF0。

内存0x12FFF0里面的值为4

内存0x12FFF4里面的值为3

内存0x12FFF8里面的值为2

内存0x12FFFC里面的值为1

完整代码:

1
2
3
4
5
6
7
8
9
10
11
mov ebp,0x12FFFC
mov esp,0x12FFFC

mov dword ptr ds:[esp],0x1
sub esp,0x4
mov dword ptr ds:[esp],0x2
sub esp,0x4
mov dword ptr ds:[esp],0x3
sub esp,0x4
mov dword ptr ds:[esp],0x4
sub esp,0x4

当最后一个值压入堆栈以后,堆栈指针还是要移动,必须保证堆栈指针永远指向栈顶的位置。

0x4

从堆栈中取值时,只需要用栈底或栈顶地址加上偏移量就可以了。例如把堆栈中第3个元素放入eax寄存器里面:

1
mov eax,dword ptr ds:[ebp-8]

Ps: 堆栈是先进后出的结构,栈底的元素就是第一个元素,而栈顶的元素就是最后一个元素。第3个元素到第1个元素的距离是2个间隔,每一个间隔的数据宽度是4字节,那么第3个元素距离栈底的偏移量就是8个字节,所以栈底地址减去8就是第三个元素的地址,所以使用栈底指针ebp-8就可以找到第三个元素:

使用栈顶指针取值也是同理,堆栈中一共有4个元素,第3个元素到栈顶的距离就是1,每一个间隔的数据宽度是4字节,那么栈顶的位置加上4字节的偏移量就是第3个元素的地址,这次把值放入ecx寄存器。

1
mov ecx,dword ptr ds:[esp+4]

0x5

除了上面的手动压栈以外,可以使用更方便的push指令,push指令的作用就是把数据压入堆栈,并且自动移动栈顶指针。

操作系统默认是用ebp寄存器来存放栈底地址,esp寄存器来存放栈顶地址。为了便于观察,我先把现在这个程序的堆栈清空,然后把栈顶指针和栈底指针都放到0x12FFFC:

现在esp和ebp都是指向0x12FFFC的,并且0x12FFFC到0x12FFCC这段内存里储存的值都是0。

使用push指令压几个数据到堆栈里:

1
2
3
4
5
6
push 0x1
push 0x2
push 0x3
push 0x4
push 0x5
push 0x6

按F8单步执行这6行push以后,可以看到堆栈指针esp已经指向的0x12FFE4,栈底指针还是原来的位置,而1、2、3、4、5、6这6个数字已经被压入堆栈。

push一个立即数的时候,它的数据宽度是32位,也就是DWORD。

除了立即数,也可以push一个寄存器,或者push一个内存地址。

push寄存器或者内存地址的时候,可以push一个16位或者32位的寄存器或者内存,但是不能push一个8位的寄存器或者内存

这么写是可以的:

1
2
3
4
5
push eax
push ax

push dword ptr ds:[内存地址]
push word ptr ds:[内存地址]

eax是32位的寄存器,ax是16位的寄存器,dword的宽度是32位,word的宽度是16位。

但是这么写是错误的:

1
2
push al
push byte ptr ds:[内存地址]

al是一个8位的寄存器,byte的数据宽度是8位。

0x6

出栈

出栈和通过偏移量取值是不一样的

通过偏移量取值是读取出堆栈中间某一个位置的值,取值时堆栈指针是不动的。

而出栈是把数据弹出堆栈,堆栈指针是要移动的。弹出一个,堆栈指针就要回退一个,把内存让出来。

pop指令可以很方便的弹出数据。

为了便于观察我先清空eax、ecx、edx和ebx这四个寄存器:

然后使用pop指令把堆栈中后4个值弹出到这四个寄存器里面:

1
2
3
4
pop eax
pop ecx
pop edx
pop ebx

现在堆栈指针esp指向的位置是0x12FFE4。而且这4个寄存器里的值都是0,F8单步执行以上4行代码:

eax里面的值已经变成了6,ecx里面的值已经变成了5,edx里面的值已经变成了4,ebx里面的值已经变成了3。而堆栈指针esp已经自动回退到了0x12FFF4。

pop和push一样,可以pop到一个32位或者16位的寄存器或者内存,但是不能pop到一个8位的寄存器或内存