预备知识
1.关于ARM架构
ARM架构,过去称作高级精简指令集机器(英语:Advanced RISC Machine,更早称作Acorn精简指令集机器,AcornRISC Machine),是一个精简指令集(RISC)处理器架构家族,其广泛地使用在许多嵌入式系统设计。由于节能的特点,其在其他领域上也有很多作为。ARM处理器非常适用于移动通信领域,匹配其主要设计目标为低成本、高性能、低耗电的特性。另一方面,超级计算机消耗大量电能,ARM同样被视作更高效的选择。安谋控股开发此架构并授权其他公司使用,以供他们实现ARM的某一个架构,开发自主的系统单片机和系统模块(system-on-module,SoC)。
2.关于汇编语言
汇编语言(英语:assembly language)是一种用于电子计算机、微处理器、微控制器,或其他可编程器件的低级语言。在不同的设备中,汇编语言对应着不同的机器语言指令集。 一种汇编语言专用于某种计算机系统结构,而不像许多高级语言,可以在不同系统平台之间移植。
3.树莓派安装参考
实验目的
通过该实验了解ARM汇编基础语法,这是我们学习ARM下的漏洞利用程序编写的基础。
实验环境
服务器:Ubuntu IP地址:随机分配
测试文件请在实验机内下载使用:http://tools.hetianlab.com/tools/T052.zip
启动树莓派命令:
$ qemu-system-arm -kernel ~/qemu_vms/qemu-rpi-kernel/kernel-qemu-4.4.34-jessie -cpu arm1176 -m 256 -M versatilepb -serial stdio -append "root=/dev/sda2 rootfstype=ext4 rw" -hda ~/qemu_vms/rasbian.img -redir tcp:5022::22 -no-reboot
SSH连接树莓派:
$ ssh pi@127.0.0.1 -p 5022
pi账户密码raspberry。
实验步骤一
我们知道函数利用堆栈来保存局部变量,保存寄存器状态等。为了让所有事物有序运行,函数使用栈帧,即堆栈中的一片本地化内存区域,专用于特定的函数。栈帧是在函数的prologue中创建的。将帧指针(FP)设置到堆栈帧的底部,然后为栈帧分配的堆栈缓存会被开辟。栈帧(从它的底部开始)通常包含返回地址(之前的LR)、之前的帧指针、需要保存的任何寄存器、函数参数(如果函数允许大于4)、局部变量等。虽然堆栈帧的实际存储的内容可能有所不同,但之前概述的那些内容是最常见的。最后,堆栈帧在函数运行到结尾部分时被破坏。
栈中栈帧的抽象简图如下所示:

我们写个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。
我们直接编译进入调试:

break max在max下断点,run,如下所示:

结合c源码可知,会传入两个参数给max,这反映在汇编里其实就是由我在上图红框框出来的两条指令实现的。
看max的汇编:

可以知道c源码中的if语句的比较操作就是这里的cmp r2,r3实现的,所以我们直接跳到这里来看看布局。
所以我们一直执行到两条str执行之后,输入nexti 9即可:

此时,FP(R11)指向0xBEFFF234,这里是栈帧的底部。堆栈上的地址(绿色地址)存储0x000010418,这是返回地址。 0xBEFFF234上面的4字节(0xBEFFF230)中存储了值0xBEFFF24c,它是上一个的帧指针的地址。地址0xBEFFF22C处的0x1和0xBEFFF228处0x2是函数max执行过程中用到的局部变量。
实验步骤二
任务描述:学习ARM函数。
想理解ARM函数首先需要熟悉函数的结构组成,他们是:
1.Prologue 序言
Pologue序言的目的是保存程序的先前状态(通过将LR和R11的值存储到堆栈上)并为函数的局部变量开辟堆栈空间。虽然序言的实现可能取决于所使用的编译器,但通常通过使用PUSH/ADD/SUB指令来完成。典型用法如下:
push {r11, lr} /* 序言开始,保存FP并将LR入栈 */
add r11, sp, #0 /* 设置栈帧的底部*/
sub sp, sp, #16 /* 序言的终止,在栈上分配一些缓存区,这样也为栈帧分配了一些内存空间*/
2.Body 函数主体
函数的主体部分通常负责执行某种特殊的和特定的任务。函数的这一部分可以包含多种指令、分支(跳转)到其他函数等。典型用法如下:
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/branchingto function max */
上面的片段设置局部变量,然后调用到另一个函数。这段代码还告诉我们,函数的参数(在这种情况下是函数max的参数)如何通过寄存器传递。在某些情况下,当有超过4个参数需要被传递时,我们将使用堆栈来存储剩余的参数。还需要注意的是,函数的结果是通过寄存器R0返回的。因此,无论函数(max)的结果究竟是什么,我们应该能够在函数返回之后,从寄存器R0中把它提取出来。还有一点要指出的是,在某些特定情况下,返回值的长度可能是64位的长度(超过32位寄存器的大小)。在这种情况下,我们可以使用R0与R1组合起来,来返回64位结果。
3.Epilogue 尾声
函数的最后一部分,尾声(epilogue),用来将程序恢复到初始状态(调用函数之前的状态),所以可以接着从函数被调用之前的位置继续往后执行。为了实现该目标,我们需要读取堆栈指针(SP)。这是通过使用帧指针寄存器(R11)作为参考并执行加法或者减法操作来完成的。当我们重新调整堆栈指针时,我们通过将它们从堆栈中弹出到各自的寄存器中来恢复先前(prologue)保存的寄存器值。POP指令可能是结尾部分的最后指令,这取决于函数的类型。但是,在恢复寄存器值之后,我们可能会使用BX指令来离开函数。尾声(Epilogue)的一个例子是这样的:
sub sp, r11, #0 /* 尾声开始,调整SP寄存器*/
pop {r11, pc} /* 尾声的结束。从堆栈中恢复之前的FP,并把之前保存的LR载入PC,跳转到那里继续执行。函数的栈帧至此全部销毁完毕*/
简单的总结一下,就是:
序言部分建立了函数的运行环境;函数体部分实现函数的逻辑并将返回值存储进R0;尾声部分恢复了函数被调用之前的状态并继续运行。
实验步骤三
任务描述:函数的类型:叶和非叶。
叶函数是这样一类函数,它本身不调用/分支另一函数。非叶函数是一种函数,除了它自己的逻辑外,还得调用/分支到另一个函数。这两种函数的实现是相似的。然而,它们有一些不同之处。为了分析这些函数的差异,我们修改下之前的c源码。
源码如下所示,在pro1.c里面:

可以看出,main是非叶函数,max是叶函数,接下来编译链接生成Pro1。
使用objdump查看汇编:

直接看重点:main和max的。

叶函数和非叶函数在序言和尾声处的实现方式有差异,先来看我上图圈出的序言的差异。
非叶函数的序言需要将更多的寄存器保存在堆栈里。背后的原因在于,由于非叶函数的天然属性,在执行这样的函数期间LR被修改了,因此需要保存该寄存器的值,以便以后能够恢复。一般来说,如果必要的话,序言可以保存更多的寄存器。所以我们在上图可以看到main push了fp,lr;而max只push了fp。
再看看尾声的差异:

在max函数中可以看到,使用BL指令分支到叶函数。我们使用函数的标签作为参数来启动分支。在编译过程中,标签被替换为内存地址。在跳转到该位置之前,下一条指令的地址被保存到LR寄存器,这样我们就可以返回到函数max结束时离开的位置。
我们在max函数中看到:用于离开叶函数的BX指令以LR寄存器作为参数。如前所述,在跳转到函数max之前,BL指令将函数main的下一个指令的地址保存到LR寄存器中。由于叶函数不在执行期间改变LR寄存器的值,所以该寄存器现在可以用于返回父(main)函数。
至此,arm汇编教程已经结束。

本文是ARM汇编教程的一部分,旨在帮助读者理解ARM架构和汇编语言的基本概念。通过树莓派的实验环境,介绍了函数的三个关键部分:Prologue(序言)、Body(函数主体)和Epilogue(尾声),特别是如何在ARM汇编中实现函数调用、局部变量和返回地址。实验中,读者将学习如何通过汇编查看和理解C程序的执行过程,包括参数传递、函数调用和返回值的处理,同时区分叶函数和非叶函数的特性。

4万+

被折叠的 条评论
为什么被折叠?



