欢迎来到 黑吧安全网 聚焦网络安全前沿资讯,精华内容,交流技术心得!

ARM 汇编基础速成7:栈与函数

来源:本站整理 作者:佚名 时间:2017-07-16 TAG: 我要投稿

在这部分我们将研究一篇独特的内存区域叫做栈,讲解栈的目的以及相关操作。除此之外,我们还会研究ARM架构中函数的调用约定。

一般来说,栈是一片在程序/进程中的内存区域。这部分内存是在进程创建的时候被创建的。我们利用栈来存储一些临时数据比如说函数的局部变量,环境变量等。在之前的文章中,我们讲了操作栈的相关指令PUSH和POP。
在我们开始之前,还是了解一下栈的相关知识以及其实现方式吧。首先谈谈栈的增长,即当我们把32位的数据放到栈上时候它的变化。栈可以向上增长(当栈的实现是负向增长时),或者向下增长(当栈的实现是正向增长时)。具体的关于下一个32位的数据被放到哪里是由栈指针来决定的,更精确的说是由SP寄存器决定。不过这里面所指向的位置,可能是当前(也就是上一次)存储的数据,也可能是下一次存储时的位置。如果SP当前指向上一次存放的数据在栈中的位置(满栈实现),SP将会递减(降序栈)或者递增(升序栈),然后再对指向的内容进行操作。而如果SP指向的是下一次要操作的数据的空闲位置(空栈实现),数据会先被存放,而后SP会被递减(降序栈)或递增(升序栈)。

不同的栈实现,可以用不同情形下的多次存取指令来表示(这里很绕...):

我们的例子中,使用的是满栈降序的栈实现。让我们看一个栈相关的例子。
/* azeria@labs:~$ as stack.s -o stack.o && gcc stack.o -o stack && gdb stack */
.global main
main:
     mov   r0, #2  /* 设置R0 */
     push  {r0}    /* 将R0存在栈上 */
     mov   r0, #3  /* 修改R0 */
     pop   {r0}    /* 恢复R0为初始值 */
     bx    lr      /* 程序结束 */
在一开始,栈指针指向地址0xbefff6f8,代表着上一次入栈数据的位置。可以看到当前位置存储了一些值。
gef> x/1x $sp
0xbefff6f8: 0xb6fc7000
在执行完第一条指令MOV后,栈没有改变。在只执行完下一条PUSH指令后,首先SP的值会被减4字节。之后存储在R0中的值会被存放到SP指向的位置中。现在我们在看看SP指向的位置以及其中的值。
gef> x/x $sp
0xbefff6f4: 0x00000002
之后的指令将R0的值修改为3。然后我们执行POP指令将SP中的值存放到R0中,并且将SP的值加4,指向当前栈顶存放数据的位置。z最终R0的值是2。
gef> info registers r0
r0       0x2          2
(下面的动图展示了低地址在顶部的栈的变化情况)

栈被用来存储局部变量,之前的寄存器状态。为了统一管理,函数使用了栈帧这个概念,栈帧是在栈内用于存储函数相关数据的特定区域。栈帧在函数开始时被创建。栈帧指针(FP)指向栈帧的底部元素,栈帧指针确定后,会在栈上申请栈帧所属的缓冲区。栈帧(从它的底部算起)一般包含着返回地址(之前说的LR),上一层函数的栈帧指针,以及任何需要被保存的寄存器,函数参数(当函数需要4个以上参数时),局部变量等。虽然栈帧包含着很多数据,但是这其中不少类型我们之前已经了解过了。最后,栈帧在函数结束时被销毁。
下图是关于栈帧的在栈中的位置的抽象描述(默认栈,满栈降序):

来一个例子来更具体的了解下栈帧吧:
/* 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
 {
 return b;
 }
 else
 {
 return a;
 }
}
int do_nothing()
{
 return 0;
}
在下面的截图中我们可以看到GDB中栈帧的相关信息:

可以看到上面的图片中我们即将离开函数max(最下面的反汇编中可以看到)。在此时,FP(R11)寄存器指向的0xbefff254就是当前栈帧的底部。这个地址对应的栈上(绿色地址区域)位置存储着0x00010418这个返回地址(LR)。再往上看4字节是0xbefff26c。可以看到这个值是上层函数的栈帧指针。在0xbefff24c和0xbefff248的0x1和0x2是函数max执行时产生的局部变量。所以栈帧包含着我们之前说过的LR,FP以及两个局部变量。
函数
在开始学习ARM下的函数前,我们需要先明白一个函数的结构:
序言准备(Prologue)
函数体
结束收尾(Epilogue)
序言的目的是为了保存之前程序的执行状态(通过存储LR以及R11到栈上)以及设定栈以及局部函数变量。这些的步骤的实现可能根据编译器的不同有差异。通常来说是用PUSH/ADD/SUB这些指令。举个例子:
push   {r11, lr}    /* 保存R11与LR */

[1] [2] [3]  下一页

【声明】:黑吧安全网(http://www.myhack58.com)登载此文出于传递更多信息之目的,并不代表本站赞同其观点和对其真实性负责,仅适于网络安全技术爱好者学习研究使用,学习中请遵循国家相关法律法规。如有问题请联系我们,联系邮箱admin@myhack58.com,我们会在最短的时间内进行处理。
  • 最新更新
    • 相关阅读
      • 本类热门
        • 最近下载