0%

栈·参数存储排布

基础提要:栈结构

ARM内存中的栈区域是满递减的,由高地址向低地址增长,SP指针始终指向最后一个压入栈的地址,即栈顶地址。如图所示:

为什么栈向下增长?

每一个可执行C程序,从低地址到高地址依次是:text,data,bss,堆,栈,环境参数变量;其中堆和栈之间有很大的地址空间空闲着,在需要分配空间的时候,堆向上涨,栈往下涨。

这样设计可以使得堆和栈能够充分利用空闲的地址空间。如果栈向上涨的话,我们就必须得指定栈和堆的一个严格分界线,但这个分界线怎么确定呢?平均分?但是有的程序使用的堆空间比较多,而有的程序使用的栈空间比较多。

所以就可能出现这种情况:一个程序因为栈溢出而崩溃的时候,其实它还有大量闲置的堆空间呢,但是我们却无法使用这些闲置的堆空间。

所以呢,最好的办法就是让堆和栈一个向上涨,一个向下涨,这样它们就可以最大程度地共用这块剩余的地址空间,达到利用率的最大化!!

—— 引用自判断栈和堆的生长方向

如何判断栈的增长方向?

很简单,我们可以通过两个函数的调用来确定。我们知道,执行一个函数时,这个函数的相关信息都会出现栈之中,比如参数、返回地址和局部变量。

那么,当它调用另一个函数时,在它栈信息保持不变的情况下,会把被调用函数的信息放到栈中。两个函数的相对信息位置是固定的,肯定是先调用的函数其信息先入栈,后调用的函数其信息后入栈。只需要判断这两个地址,就可以判断栈的增长方向了。

比如设计两个函数fun1()fun2(),将fun1()中某参数的地址传给fun2(),且在fun1()中调用fun2(),最后在fun2()中打印出两个函数参数的地址,则大功告成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void func2(int *a)
{
int b=0;
printf("%x\n%x\n",a,&b);
}
void func1()
{
int a=0;
func2(&a);
}
int main()
{
func1();
}

我这边测试打印出来分别是5fbff91c 5fbff8f4 (我用的Xcodecommand line 测试的)。即a 的地址 > b的地址,先分配的是高地址,因此是高地址向低地址增长。

如何区分栈底和栈顶?

很容易将分不清高地址和低地址到底谁才是栈底。我们可以想象一个桶,这个桶内的空间就是栈区,桶底(栈底)是确定了的,不会改变。往桶内加水,即使入栈操作。水面即是栈顶,也就SP指针所在的位置。我们不断加水,只会使得水面(栈顶SP指针)增长,而水底(栈底指针)仍旧不变。而栈区又是高地址向低地址增长,因此栈区的最高地址是为栈低,SP指针是栈顶指针。

只不过,在实际应用情况中,我们经常习惯将栈底指针(最高地址处)放在最上面,SP指针在最下面,也就是一个倒扣着的桶,在失重情况下,往倒扣的桶里加水,水底仍然是栈底指针,水面仍然是栈顶指针,水面随着水的加入而增长。

实例

在逆向过程中,我一直对栈的数据排列非常迷惑,现在让我们一起来解决它吧!
以下是我逆向遇到的一个小实例:

在图中我们可以看到目前运行到第三行代码stp x9, x10, [sp, #8]处,stp指令是将寄存器中的值依次存入后面的地址处。我们先打印一下x12 x8以及sp的值:

那么也就是说第二条指令stp x12, x8, [sp, #24]已经将x12x8的值存入了sp+24地址处。

那么我们仔细想想,究竟这两个值在栈上是如何排列的呢?先存入的是x12还是x8呢?第二参数是在sp+24的高地址处还是低地址处呢?

我们打印一下内存上的信息看一下就知道了:

/4xb: 就是说从该地址开始,按照1个字节,16进制的方式打印4个单位。
/4xw: 按照4个字节,16进制的方式打印4个单位

更多有关打印格式的请看gdb查看内存区命令

跟上面x12x8比较后,我们可以发现,内存在打印的时候,每个打印单位(这里是以w格式打印,即4个字节)我们要从右往左看(从高地址到低地址),单位内部还是从左到右为高地址到低地址。也就是说,第一个打印单位的最右边是当前打印地址(最低地址),最后一个打印单位的最左边是最高地址。如下图所示,图中箭头为从高地址指向低地址:

那么,也就是说,
x12存储在从sp+24 (0x16fd11cc8)sp+31 (0x16fd11ccf)之间
x8 存储在从sp+32 (0x16fd11cd0)sp+39 (0x16fd11cd7)之间

则他们之间的具体排列如下图所示

小测试(第3、4行命令)

现在我们大概对数据的排布有一个更深入的理解了,那么,就以第三行命令来测试一下我们是否真的理解了吧!
在执行第三条命令之前,我们先看一下x9x10的值:

那么,按照上一条命令的方式,我们来猜测一下内存排布吧。
stp x9, x10, [sp, #8] 这条命令的意思是,将x9x10中的值依次放入sp+8所在的位置。那么,到底是存储在sp+8的高地址处,还是低地址处呢?

其实很好理解,因为栈是向低地址处增长的,如果我们往spsp+8处写入这两个寄存器的值,很明显是不够的,这里只有8个字节的空间,而我们需要16个字节,因此sp指针就会往低地址处移动,则栈空间就增大了,但是sp指针并没有改变,这些指令都没有关于要改变栈顶指针的,所以这个想法是错误的。

也就是说,在往栈内存储数据时,都在高地址到给定的地址之间存入,即向栈中已分配的空间存入。

那么,x9x10依次存储在sp+8 ~ sp+15sp+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 0x00000000sp+16: 0xb1a9dd86 0x00000000

我们打印一下看看猜测是否正确:

完全正确!
我们继续往下执行第4条命令:
str x11, [sp]这条命令的意思是,将x11中的值放入sp所指的地方。我们知道sp是栈顶指针,是栈的最后一个元素所在的位置,所以x11肯定是存储在spsp+8之间。
打印一下x11的值:

那么,依旧按照之前的方法,每个打印单位,要从右往左看:

  • 那么sp的地址0x16fd11cb0sp+8 (0x16fd11cb8)之间依次应该为ef e1 0f db 00 00 00 00
  • 按照4xw的打印格式则应该为:0x16fd11cb0: 0xdbofelef 0x00000000 0x0bafc364 0x00000000sp+8 ~ sp+15是刚刚我们执行过的x9的值)

打印一下:

完全正确!
好了,现在关于内存栈的数据排列,你是不是有更清晰的图像印在脑海里了呢?

----------------------END END----------------------