在这一部分中,我们将研究称为堆栈的进程的一个特殊内存区域。本章介绍了Stack的用途和相关操作。此外,我们还将介绍ARM中函数的实现、类型和差异。
STACK
一般来说,堆栈是程序/进程中的一个内存区域。这部分内存是在创建进程时分配的。我们使用Stack存储临时数据,如某些函数的局部变量、帮助我们在函数之间转换的环境变量等。我们使用PUSH和POP指令与堆栈交互。正如第4部分:内存指令中所解释的:加载和存储PUSH和POP是其他一些与内存相关的指令的别名,而不是真正的指令,但出于简单的原因,我们使用PUSH和POP。
在我们研究一个实际的例子之前,我们必须知道Stack可以用各种方式实现。首先,当我们说Stack增长时,我们的意思是一个项目(32位数据)被放在Stack上。堆栈可以向上(当堆栈以降序实现时)或向下(当堆栈按升序实现时)增长。下一条(32位)信息将被放入的实际位置由堆栈指针定义,或者准确地说,由存储在SP寄存器中的存储器地址定义。在这里,地址可以再次指向堆栈中的当前(最后)项或该项的下一个可用内存插槽。如果SP当前指向堆栈中的最后一个项目(全堆栈实现),则SP将减少(如果是降序堆栈)或增加(如果是升序堆栈),然后该项目才会放在堆栈中。如果SP当前指向堆栈中的下一个空插槽,则会先放置数据,然后SP才会减少(降序堆栈)或增加(升序堆栈)。
作为不同Stack实现的总结,我们可以使用下表,该表描述了在不同情况下使用的存储多条/加载多条指令。
在我们的示例中,我们将使用全降序堆栈。让我们快速了解一个简单的练习,它处理这样一个堆栈,它就是堆栈指针。
/* azeria@labs:~$ as stack.s -o stack.o && gcc stack.o -o stack && gdb stack */
.global main
main:
mov r0, #2 /* set up r0 */
push {r0} /* save r0 onto the stack */
mov r0, #3 /* overwrite r0 */
pop {r0} /* restore r0 to it's initial state */
bx lr /* finish the program */
一开始,堆栈指针指向地址0xbefff6f8(在您的情况下可能不同),它表示堆栈中的最后一项。此时,我们看到它存储了一些值(同样,在您的情况下,值可能不同):
gef> x/1x $sp
0xbefff6f8: 0xb6fc7000
在执行第一条(MOV)指令后,堆栈方面没有任何变化。当我们执行PUSH指令时,会发生以下情况:首先,SP的值减少4(4字节=32位)。然后,R0的内容被存储到SP指定的新地址。当我们现在检查SP引用的更新的内存位置时,我们看到整数2的32位值存储在该位置:
gef> x/x $sp
0xbefff6f4: 0x00000002
我们示例中的指令(MOV r0,#3)用于模拟r0的损坏。然后,我们使用POP来恢复以前保存的R0值。因此,当POP被执行时,会发生以下情况:首先,从SP中地址当前指向的存储位置(0xbefff6f4)读取32位数据。然后,SP寄存器的值增加4(再次变为0xbefff6 f8)。作为结果,寄存器R0包含整数值2。
gef> info registers r0
r0 0x2 2
(请注意,下面的gif显示的堆栈顶部有较低的地址,底部有较高的地址,而不是像第一幅不同堆栈变体的插图中那样相反。这样做的原因是让它看起来像你在GDB中看到的堆栈视图)
我们将看到函数利用Stack来保存局部变量、保留寄存器状态等。为了保持一切有序,函数使用Stack Frames,这是堆栈中专用于特定函数的本地化内存部分。在函数的序言中创建了一个堆栈框架(下一节将对此进行详细介绍)。帧指针(FP)被设置到堆栈帧的底部,然后为堆栈帧分配堆栈缓冲区。堆栈帧(从底部开始)通常包含返回地址(前一个LR)、前一个帧指针、需要保留的任何寄存器、函数参数(如果函数接受4个以上)、局部变量等。虽然堆栈帧的实际内容可能有所不同,但前面概述的内容是最常见的。最后,堆栈帧在函数的尾声中被销毁。
以下是堆栈中堆栈帧的抽象说明:
作为Stack Frame可视化的一个快速示例,让我们使用以下代码:
/* azeria@labs:~$ gcc func.c -o func && gdb func */
int main()
{
int res = 0;
int a = 1;
int b = 2;
res = max(a, b);
return res;
}
int max(int a,int b)
{
do_nothing();
if(a<b)
{
return b;
}
else
{
return a;
}
}
int do_nothing()
{
return 0;
}
在下面的屏幕截图中,我们可以通过GDB调试器的视角看到堆栈帧的简单说明
我们可以在上图中看到,目前我们即将离开函数max(见底部反汇编中的箭头)。在这种状态下,FP(R11)指向0xbefff254,它是堆栈帧的底部。堆栈上的此地址(绿色地址)存储0x00010418,它是返回地址(以前的LR)。在此之上4个字节(在0xbefff250处),我们有一个值0xbefff26c,它是前一个帧指针的地址。地址0xbefff24c和0xbefff248处的0x1和0x2是在执行函数max期间使用的局部变量。因此,我们刚刚分析的堆栈帧只有LR、FP和两个局部变量。