ARM汇编学习六

我们知道函数利用堆栈来保存局部变量,保存寄存器状态等。为了让所有事物有序运行,函数使用栈帧,即堆栈中的一片本地化内存区域,专用于特定的函数。栈帧是在函数的prologue中创建的。将帧指针(FP)设置到堆栈帧的底部,然后为栈帧分配的堆栈缓存会被开辟。栈帧(从它的底部开始)通常包含返回地址(之前的LR)、之前的帧指针、需要保存的任何寄存器、函数参数(如果函数允许大于4)、局部变量等。虽然堆栈帧的实际存储的内容可能有所不同,但之前概述的那些内容是最常见的。最后,堆栈帧在函数运行到结尾部分时被破坏。
栈中栈帧的抽象简图如下所示
ARM汇编学习六
我们写个c程序体验一下
程序如下,在pro.c
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;
}
可以看到main调用了max
ARM汇编学习六
设置好布局
ARM汇编学习六
我们就在max下断点
break max,run,如下所示
ARM汇编学习六
结合c源码可知,会传入两个参数给max,这反映在汇编里其实就是由我在上图红框框出来的两条指令实现的
看max的汇编
ARM汇编学习六
可以知道c源码中的if语句的比较操作就是这里的cmp r2,r3实现的,所以我们直接跳到这里来看看布局
所以我们一直执行到两条str执行之后,输入nexti 9即可
ARM汇编学习六
此时,FP(R11)指向0xBEFFF234,这里是栈帧的底部。堆栈上的地址(绿色地址)存储0x000 010418,这是返回地址。 0xBEFFF234 上面的4字节(0xBEFFF230)中存储了值0xBEFFF24c,它是上一个的帧指针的地址。地址0xBEFFF22C处的 0x1和0xBEFFF228处0x2是函数max执行过程中用到的局部变量。

接下来我们学习ARM函数
想理解ARM函数首先需要熟悉函数的结构组成,他们是:
1.Prologue 序言
2.Body 函数主体
3.Epilogue 尾声
Pologue序言的目的是保存程序的先前状态(通过将LR和R11的值存储到堆栈上)并为函数的局部变量开辟堆栈空间。虽然序言的实现可能取决于所使用的编译器,但通常通过使用PUSH/ADD/SUB指令来完成。典型用法如下:
push {r11, lr} /* 序言开始,保存FP并将LR入栈 /
add r11, sp, #0 /
设置栈帧的底部*/
sub sp, sp, #16 /* 序言的终止,在栈上分配一些缓存区,这样也为栈帧分配了一些内存空间*/

函数的主体部分通常负责执行某种特殊的和特定的任务。函数的这一部分可以包含多种指令、分支(跳转)到其他函数等。典型用法如下:
mov r0, #1 /* setting up local variables (a=1). This also serves as setting up the first parameter for the function max /
mov r1, #2 /
setting up local variables (b=2). This also serves as setting up the second parameter for the function max /
bl max /
Calling/branching to function max */
上面的片段设置局部变量,然后调用到另一个函数。这段代码还告诉我们,函数的参数(在这种情况下是函数max的参数)如何通过寄存器传递。在某些情况下,当有超过4个参数需要被传递时,我们将使用堆栈来存储剩余的参数。还需要注意的是,函数的结果是通过寄存器R0返回的。因此,无论函数(max)的结果究竟是什么,我们应该能够在函数返回之后,从寄存器R0中把它提取出来。还有一点要指出的是,在某些特定情况下,返回值的长度可能是64位的长度(超过32位寄存器的大小)。在这种情况下,我们可以使用R0与R1组合起来,来返回64位结果。

函数的最后一部分,尾声(epilogue),用来将程序恢复到初始状态(调用函数之前的状态),所以可以接着从函数被调用之前的位置继续往后执行。为了实现该目标,我们需要读取堆栈指针(SP)。这是通过使用帧指针寄存器(R11)作为参考并执行加法或者减法操作来完成的。当我们重新调整堆栈指针时,我们通过将它们从堆栈中弹出到各自的寄存器中来恢复先前(prologue)保存的寄存器值。POP指令可能是结尾部分的最后指令,这取决于函数的类型 。但是,在恢复寄存器值之后,我们可能会使用BX指令来离开函数。尾声(Epilogue)的一个例子是这样的:
sub sp, r11, #0 /* 尾声开始,调整SP寄存器*/
pop {r11, pc} /* 尾声的结束。从堆栈中恢复之前的FP,并把之前保存的LR载入PC,跳转到那里继续执行。函数的栈帧至此全部销毁完毕*/

简单的总结一下,就是:
序言部分建立了函数的运行环境;函数体部分实现函数的逻辑并将返回值存储进R0;尾声部分恢复了函数被调用之前的状态并继续运行。

接下来看看函数的类型:叶和非叶。叶函数是这样一类函数,它本身不调用/分支另一函数。非叶函数是一种函数,除了它自己的逻辑外,还得调用/分支到另一个函数。这两种函数的实现是相似的。然而,它们有一些不同之处。为了分析这些函数的差异,我们修改下之前的c源码
源码如下所示,在pro1.c里面
ARM汇编学习六
可以看出,main是非叶函数,max是叶函数,接下来编译链接生成Pro1
使用objdump查看汇编
ARM汇编学习六
直接看重点:main和max的
ARM汇编学习六
叶函数和非叶函数在序言和尾声处的实现方式有差异,先来看我上图圈出的序言的差异
非叶函数的序言需要将更多的寄存器保存在堆栈里。背后的原因在于,由于非叶函数的天然属性,在执行这样的函数期间LR被修改了,因此需要保存该寄存器的值,以便以后能够恢复。一般来说,如果必要的话,序言可以保存更多的寄存器。所以我们在上图可以看到main push了fp,lr;而max只push了fp
在看看尾声的差异
ARM汇编学习六
在max函数中可以看到,使用BL指令分支到叶函数。我们使用函数的标签作为参数来启动分支。在编译过程中,标签被替换为内存地址。在跳转到该位置之前,下一条指令的地址被保存到LR寄存器,这样我们就可以返回到函数max结束时离开的位置。
我们在max函数中看到:用于离开叶函数的BX指令以LR寄存器作为参数。如前所述,在跳转到函数max之前,BL指令将函数main的下一个指令的地址保存到LR寄存器中。由于叶函数不在执行期间改变LR寄存器的值,所以该寄存器现在可以用于返回父(main)函数。

至此,arm汇编学习已经结束。

参考:
azeria-labs 的arm教程
https://azeria-labs.com