基础提要:栈结构
ARM内存中的栈区域是满递减的,由高地址向低地址增长,SP指针始终指向最后一个压入栈的地址,即栈顶地址。如图所示:
为什么栈向下增长?
每一个可执行C程序,从低地址到高地址依次是:text,data,bss,堆,栈,环境参数变量;其中堆和栈之间有很大的地址空间空闲着,在需要分配空间的时候,堆向上涨,栈往下涨。
这样设计可以使得堆和栈能够充分利用空闲的地址空间。如果栈向上涨的话,我们就必须得指定栈和堆的一个严格分界线,但这个分界线怎么确定呢?平均分?但是有的程序使用的堆空间比较多,而有的程序使用的栈空间比较多。
所以就可能出现这种情况:一个程序因为栈溢出而崩溃的时候,其实它还有大量闲置的堆空间呢,但是我们却无法使用这些闲置的堆空间。
所以呢,最好的办法就是让堆和栈一个向上涨,一个向下涨,这样它们就可以最大程度地共用这块剩余的地址空间,达到利用率的最大化!!
—— 引用自判断栈和堆的生长方向
如何判断栈的增长方向?
很简单,我们可以通过两个函数的调用来确定。我们知道,执行一个函数时,这个函数的相关信息都会出现栈之中,比如参数、返回地址和局部变量。
那么,当它调用另一个函数时,在它栈信息保持不变的情况下,会把被调用函数的信息放到栈中。两个函数的相对信息位置是固定的,肯定是先调用的函数其信息先入栈,后调用的函数其信息后入栈。只需要判断这两个地址,就可以判断栈的增长方向了。
比如设计两个函数fun1()
和fun2()
,将fun1()
中某参数的地址传给fun2()
,且在fun1()
中调用fun2()
,最后在fun2()
中打印出两个函数参数的地址,则大功告成。
|
|
我这边测试打印出来分别是5fbff91c
5fbff8f4
(我用的Xcode
的 command line
测试的)。即a
的地址 > b
的地址,先分配的是高地址,因此是高地址向低地址增长。
如何区分栈底和栈顶?
很容易将分不清高地址和低地址到底谁才是栈底。我们可以想象一个桶,这个桶内的空间就是栈区,桶底(栈底)是确定了的,不会改变。往桶内加水,即使入栈操作。水面即是栈顶,也就SP
指针所在的位置。我们不断加水,只会使得水面(栈顶SP
指针)增长,而水底(栈底指针)仍旧不变。而栈区又是高地址向低地址增长,因此栈区的最高地址是为栈低,SP
指针是栈顶指针。
只不过,在实际应用情况中,我们经常习惯将栈底指针(最高地址处)放在最上面,SP
指针在最下面,也就是一个倒扣着的桶,在失重情况下,往倒扣的桶里加水,水底仍然是栈底指针,水面仍然是栈顶指针,水面随着水的加入而增长。
实例
在逆向过程中,我一直对栈的数据排列非常迷惑,现在让我们一起来解决它吧!
以下是我逆向遇到的一个小实例:
在图中我们可以看到目前运行到第三行代码stp x9, x10, [sp, #8]
处,stp
指令是将寄存器中的值依次存入后面的地址处。我们先打印一下x12
x8
以及sp
的值:
那么也就是说第二条指令stp x12, x8, [sp, #24]
已经将x12
和x8
的值存入了sp+24
地址处。
那么我们仔细想想,究竟这两个值在栈上是如何排列的呢?先存入的是x12
还是x8
呢?第二参数是在sp+24
的高地址处还是低地址处呢?
我们打印一下内存上的信息看一下就知道了:
/4xb: 就是说从该地址开始,按照1个字节,16进制的方式打印4个单位。
/4xw: 按照4个字节,16进制的方式打印4个单位更多有关打印格式的请看gdb查看内存区命令
跟上面x12
和x8
比较后,我们可以发现,内存在打印的时候,每个打印单位(这里是以w
格式打印,即4个字节)我们要从右往左看(从高地址到低地址),单位内部还是从左到右为高地址到低地址。也就是说,第一个打印单位的最右边是当前打印地址(最低地址),最后一个打印单位的最左边是最高地址。如下图所示,图中箭头为从高地址指向低地址:
那么,也就是说,x12
存储在从sp+24 (0x16fd11cc8)
到sp+31 (0x16fd11ccf)
之间x8
存储在从sp+32 (0x16fd11cd0)
到sp+39 (0x16fd11cd7)
之间
则他们之间的具体排列如下图所示
小测试(第3、4行命令)
现在我们大概对数据的排布有一个更深入的理解了,那么,就以第三行命令来测试一下我们是否真的理解了吧!
在执行第三条命令之前,我们先看一下x9
,x10
的值:
那么,按照上一条命令的方式,我们来猜测一下内存排布吧。stp x9, x10, [sp, #8]
这条命令的意思是,将x9
,x10
中的值依次放入sp+8
所在的位置。那么,到底是存储在sp+8
的高地址处,还是低地址处呢?
其实很好理解,因为栈是向低地址处增长的,如果我们往sp
到sp+8
处写入这两个寄存器的值,很明显是不够的,这里只有8个字节的空间,而我们需要16个字节,因此sp
指针就会往低地址处移动,则栈空间就增大了,但是sp
指针并没有改变,这些指令都没有关于要改变栈顶指针的,所以这个想法是错误的。
也就是说,在往栈内存储数据时,都在高地址到给定的地址之间存入,即向栈中已分配的空间存入。
那么,x9
,x10
依次存储在sp+8 ~ sp+15
,sp+16 ~ sp+23
之间。
按照之前讲的打印单位与单位之间是从右往左为从高地址到低地址,单位内的顺序是从左往右,1个地址存储1个字节(8位,两个字符)。
我们按照一个字节一个字节的打印(16进制),则
- 在
sp+8 ~ sp+15
处的数据应为:64 c3 af 0b 00 00 00 00
; - 在
sp+16 ~ sp+31
处的数据应为:86 dd a9 b1 00 00 00 00
。
如果我们按照4xw
的格式打印,则应该是sp+8: 0x0bafc364 0x00000000
,sp+16: 0xb1a9dd86 0x00000000
我们打印一下看看猜测是否正确:
完全正确!
我们继续往下执行第4条命令:str x11, [sp]
这条命令的意思是,将x11
中的值放入sp
所指的地方。我们知道sp
是栈顶指针,是栈的最后一个元素所在的位置,所以x11
肯定是存储在sp
到sp+8
之间。
打印一下x11
的值:
那么,依旧按照之前的方法,每个打印单位,要从右往左看:
- 那么
sp
的地址0x16fd11cb0
到sp+8 (0x16fd11cb8)
之间依次应该为ef e1 0f db 00 00 00 00
; - 按照
4xw
的打印格式则应该为:0x16fd11cb0: 0xdbofelef 0x00000000 0x0bafc364 0x00000000
(sp+8 ~ sp+15
是刚刚我们执行过的x9
的值)
打印一下:
完全正确!
好了,现在关于内存栈的数据排列,你是不是有更清晰的图像印在脑海里了呢?