目录
预备知识
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
实验步骤一
任务描述:学习内存指令,加载和存储。
ARM使用载入-存储模型来访问内存,意味着只有加载/存储(LDR和STR)指令才可以访问内存。在X86中,大多数指令允许直接操作内存中的数据,而在ARM中,在操作数据之前,必须把数据从内存移动到寄存器中。这意味着在ARM下,若要增加特定内存地址里的32位的数值,将需要用到三种类型的指令(载入、增加和存储):首先将特定地址里的数值加载到寄存器中,然后在寄存器中增加它,最后将数据从寄存器返存回内存里。
为了解释ARM的加载和存储操作的基本原理,我们接下来会使用几个示例,然后通过gdb调试进一步理解。
敲黑板,这一部分非常重要!!!
1.先来看LDR,STR
通常,LDR用于将内存数据加载到寄存器中,STR用于从寄存器的值存储到内存地址对应的内存中。如下图所示:
格式如下:
LDR R2, [R0] @ [R0] - 原始地址是R0里的数值
STR R2, [R1] @[R1] - 目标地址是R1里的数值
LDR操作:将在R0中找到的地址的值加载到目标寄存器R2。
STR操作:将R2中找到的值存储在R1中找到的内存地址中。
我们先来看第一种偏移形式:立即数用作偏移。这里我们使用一个立即数(整数)作为偏移量。这个值通过与基址寄存器(下面的例子中的R1)相加或相减来访问数据。
示例代码如下,在test2.s中。
在其中已经写好注释了:
中文注释为:
_start:
ldr r0, adr_var1 将变量var1的内存地址通过标签adr_var1载入R0
ldr r1, adr_var2 将变量var2的内存地址通过标签adr_var2载入R1
ldr r2, [r0] @ 将r0里的值作为地址取出里面的值(0x03)存入寄存器R2
str r2, [r1, #2] 寻址模式:偏移模式。将存储在R2里的值(0x03)存放在以r1+2为地址指向的内存空间中。基址寄存器(R1)的值不变
str r2, [r1, #4]! 寻址模式:先索引模式。将存储在R2里的值(0x03)存放在以r1+4为地址指向的内存空间中,然后基址寄存器(R1)被修改为R1=R1+4
ldr r3, [r1], #4 寻址模式:后索引模式。将存储在R1里的值作为内存地址取出里面的值存放在以r3中(而非R3+4),然后基址寄存器(R1)被修改为R1=R1+4
bkpt 中断,暂停程序
主要看_start就行了。
我们使用adr_var1和adr_var2来存储var1变量和var2变量(在顶部的数据段中定义的)的内存地址。第一个LDR指令将var1的地址加载到寄存器R0中。第二个LDR指令对var2做了同样的事并将其加载到R1。然后,将存储在R0中的内存地址加载到R2里,并将R2中找到的值存储在R1中找到的内存地址中。
同样编译好test2:
载入gdb调试,在_start下断点:
然后run。
前三条都是ldr指令,用于给r0,r1,r2寄存器赋值,我们不关心这个过程,使用nexti 3直接跳到str r2,[r1,#2]前:
可以看到此时r2已经被赋值为0x3了,下一步要执行的指令为str r2,[r1,#2]。
按照注释的说法,它会将r2(0x3)存储到值为(r1(0x2009c)+2)所指向的内存空间中。
意思即0x2009e指向的内存空间的内容为0x3,我们使用nexti执行这条指令,然后使用下面命令查看,果然和我们计算的相符合:
下一步指令为:
这是先索引寻址模式,这个模式下基址寄存器将被最终的内存地址更新,这个例子中基址寄存器为r1。执行后,会将r2的值(0x3)存储到(r1(0x2009c)+4)即0x200a0所指向的内存空间中,同时将r1更新为0x200a0。
同样使用nexti执行。
然后使用下面的命令验证:
接下来要执行的指令是:
这条指令使用的后索引寻址模式。指令执行之后,取出r1(0x200a0)地址的内容,赋给r3,然后r1(0x200a0)+4=0x200a4会更新r1。
使用nexti执行这条指令,然后使用下面的命令验证:
实验步骤二
任务描述:学习第二种偏移形式,寄存器的值用作偏移。
代码如下,写在test3.s中:
关键指令的中文注释如下:
ldr r0, adr_var1 透过adr_var1标签,将var1变量的内存地址加载进R0
ldr r1, adr_var2 透过adr_var2标签,将var2变量的内存地址加载进R1
ldr r2, [r0] 将R0里的值作为内存地址,把地址里的数值(0x03)载入r2
str r2, [r1, r2] 寻址模式:偏移寻址。将R2里的值存入:R1+R2(0x03,偏移)的结果所指向的内存空间中。基址寄存器不更新
str r2, [r1, r2]! 寻址模式:先索引模式。将R2的值(0x03)存入:R1+R2(0x03,用作偏移)得出的结果所指向的内存空间中。基址寄存器R1更新为R1 = R1+R2
ldr r3, [r1], r2 寻址模式:后索引模式。将R2的值作为地址取出里面的值,并将其存入寄存器R3。基址寄存器R1更新为R1=R1+R2
编译:
同样使用gdb调试,在_start下断点,跳过前三条ldr指令,然后看看此时的情况:
此时r1为0x2009c,r2为0x3,下一条要执行的指令为strr2,[r1,r2]。
这是偏移寻址模式,执行之后,r2的值(0x3)会被存储在0x2009c+0x3=0x2009f指向的内存空间中。
我们使用nexti执行后使用下面的命令查看:
符合我们的计算,同时看到下一条要执行的指令。
这是先索引模式。与上一条指令相比,这条指令执行后会用0x2009c+0x3=0x2009f的值更新r1。
我们使用nexti执行后使用下面的命令查看:
符合我们的计算,同时看到了下一条要执行的指令。
这是后索引寻址模式。执行后会将r1(0x2009f)地址中的值取出来赋给r3,然后更新r1为0x2009f+0x3=0x200a2。
我们使用nexti执行后使用下面的命令查看:
实验步骤三
任务描述:学习第三种偏移形式,移位寄存器作为偏移。
格式为:
LDR Ra, [Rb, Rc, <shifter>]
STR Ra, [Rb, Rc, <shifter>]
Rb是基寄存器,Rc里面是立即数偏移量(或是寄存器里的一个立即数),用于左/右移位(<移位寄存器>)。这意味着移位寄存器被用来存放移位的偏移量。
代码如下,写在test4.s:
关键指令中文注释如下:
_start:
ldr r0, adr_var1 透过adr_var1标签,将var1变量的内存地址加载进R0
ldr r1, adr_var2 透过adr_var2标签,将var2变量的内存地址加载进R1
ldr r2, [r0] 将R0里的值作为内存地址,把里面的值(0x03)载入r2
str r2, [r1, r2, LSL#2] 寻址模式:偏移寻址。将R2里的值(0x03)存入:R1的值+偏移量(R2左移2位后的值)的结果所指向的内存空间中。基址寄存器(R1)不更新
str r2, [r1, r2, LSL#2]! 寻址模式:先索引寻址。将R2里的值(0x03)存入:R1的值+偏移量(R2左移2位后的值)的结果所指向的内存空间中。基址寄存器(R1)更新为R1 = R1 + R2<<2
ldr r3, [r1], r2, LSL#2 寻址模式:后索引寻址。将R1的值作为地址取出里面的值,并将其存入寄存器R3。基址寄存器R1更新为R1=R1+R2<<2
bkpt
编译后载入gdb,在_start下断点,run,nexti3后查看情况:
这条指令使用偏移寻址模式,将在r2中找到的值(0x3)存储在x中,x的计算方式为:
1.r2:x00000003(十六进制)=00000011(二进制)。
2.0000 0011 左移两位得到 0000 1100。
3.0000 1100(二进制) = 0xc(十六进制)。
4.然后r1(0x2009c)作为基址,加上0xc(作为偏移)得到0x00200a8。
使用nexti执行,使用下面命令验证:
下一条要执行的指令为先索引地址模式。执行后会更新基址寄存器r1。它会首先给r1(0x2009c)向左偏移两位得到0x200a8,把r2(0x3)赋给0x200a8,同时用0x200a8更新r1。
使用nexti执行后使用下列命令查看:
符合我们的计算,下一个指令是后索引寻址模式。会将r1(0x200a8)取出作为地址,取出该地址里的值(0x3)赋给r3,然后使用x更新r1。
x的值等于r1(x0200a8)加上r2(0x3)左移两位后得到的(0xc),即0x200a8+0x3=0x200b4。
nexti后使用下面命令验证:
确实和我们计算的一样。