ARM语法 Part 4[内存指令:加载和存储]

  目录

ARM使用加载(Load)/存储(Stroe)指令来读写内存,这意味着你只能使用LDR和STR指令访问内存。在ARM上数据必须从内存中加载到寄存器之后才能进行其他操作,而在x86上大部分指令都可以直接访问内存中的数据。如前所述,在ARM上增加内存里的一个32-bit数据值,需要三个指令(load,increment,store)。为了解释 ARM 上的 Load 和 Store 操作的基本原理,我们从一个基本示例开始,然后再使用三个基本偏移形式,每个偏移形式具有三种不同的寻址模式。为了简单化,每个示例,我们将在同一段汇编代码中使用不同 LDR/STR 偏移形式的。遵循这本段教程的最佳方法是在你的测试环境中用调试器(GDB)运行代码示例。

  1. 偏移形式:立即数作为偏移量
  • 寻址模式:立即寻址
  • 寻址模式:前变址寻址
  • 寻址模式:后变址寻址
  1. 偏移形式:寄存器作为偏移量
  • 寻址模式:立即寻址
  • 寻址模式:前变址寻址
  • 寻址模式:后变址寻址
  1. 偏移形式:缩放寄存器作为偏移量
  • 寻址模式:立即寻址
  • 寻址模式:前变址寻址
  • 寻址模式:后变址寻址

第一个例子:

LDR 将内存中的值加载到寄存器中,STR 将寄存器内的值存储到内存地址。

LDR R2, [R0]   @ [R0] - R0中保存的值是源地址。
STR R2, [R1]   @ [R1] - R1中保存的值是目标地址。

LDR : 把R0内保存的值作为地址值,将该地址处的值加载到寄存器R2中。

STR : 把R1内保存的值作为地址值,将寄存器R2中的值存储到该地址处。

汇编程序是这样:

.data          /*.data段是动态创建的,无法预测 */
var1: .word 3  /* 内存中的变量var1=3*/
var2: .word 4  /* 内存中的变量var2=4*/
​
.text          /* 代码段开始位置 */ 
.global _start
​
_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内的值(0x03)存储到R1中的地址处。 
    bkpt             
​
adr_var1: .word var1  /* 变量var1的地址位置 */
adr_var2: .word var2  /* 变量var2的地址位置 */

在程序底部有我们的文本池(在代码段用来存储常量、字符串或其他可以引用的位置无关的偏移量),使用adr_var1adr_va2两个标签来存储var1var2的内存地址。第一个LDRvar1的地址加载到R0,然后第二个LDRvar2的地址加载到·。之后将R0中的地址指向的值(0x03)加载到R2,最后将R2中的值(0x03)存储到R1中的地址处。

当加载数据到寄存器中时,使用[]符号意思时:取寄存器中的值作为地址值,然后再从该地址处加载数据到目标寄存器中,如果不加[]那就是将寄存器中保存的值直接加载到目标寄存器。

同样STR命令中也是一个意思。

这听起来比实际要复杂的多,没关系,下面是一个更直观的演示图:

下面我们看一下调试器中的这段代码:

gef> disassemble _start
Dump of assembler code for function _start:
 0x00008074 <+0>:      ldr  r0, [pc, #12]   ; 0x8088 <adr_var1>
 0x00008078 <+4>:      ldr  r1, [pc, #12]   ; 0x808c <adr_var2>
 0x0000807c <+8>:      ldr  r2, [r0]
 0x00008080 <+12>:     str  r2, [r1]
 0x00008084 <+16>:     bx   lr
End of assembler dump.

开头的两个LDR操作中的第二操作数被替换成了[pc, #12]。这被叫做PC相对寻址。因为我们使用了标签,所以编译器可以计算出文本池中标签的地址相对位置(pc+12)。您可以使用这种精确的方法自行计算位置,也可以像前面一样使用标签。唯一的区别是,相较于使用标签,你需要计算值在文本池中的确切位置。在这种情况下,它距离有效的PC位置有3个跳转(4+4+4=12)。本章稍后将介绍有关PC相对寻址的介绍。

如果你忘了为什么有效PC指向当前指位置后两个指令,在第二部介绍了[…在执行过程中,在ARM模式下,PC将当前指令的地址加上8(两个ARM指令)作为最终值存储起来,在Thumb模式下,将当前指令加上 4(两个Thumb指令)作为最终值存储起来。而x86中PC始终指向要执行的下一个指令…]

1.偏移模式:立即数作为偏移量

STR    Ra, [Rb, imm]
LDR    Ra, [Rc, imm]

这里,我们使用立即(整数)作为偏移量。从基寄存器(以下示例中的 R1)中增加或减去此值,在编译时可以用已知的偏移量访问数据。

.data
var1: .word 3
var2: .word 4
​
.text
.global _start
​
_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, #2]  @ 以R1中的值为基准加上立即数2作为最终地址,将R2中的值(0x03)存储到该地址处,其中R1中的值不会被修改。 
    str r2, [r1, #4]! @ 前变址寻址:以R1中的值为基准加上立即数4作为最终地址,将R2中的值(0x03)存储到该地址处,其中R1中的值被修改为:R1+4。 
    ldr r3, [r1], #4  @ 后变址寻址:将R1中的值作为最终地址,获取该地址处的数据加载到R3,其中R1中的值被修改为:R1+4。
    bkpt
​
adr_var1: .word var1
adr_var2: .word var2

假设以上程序文件为ldr.s,编译并用GDB允许,看看会发生什么。

$ as ldr.s -o ldr.o
$ ld ldr.o -o ldr
$ gdb ldr

GDB(包含gef)中,在_start处设置断点,运行程序。

gef> break _start
gef> run
...
gef> nexti 3     /* 运行后3条指令 */

系统上的寄存器现在填充了以下值(注意,这些地址在你的系统上可能有所不同):

$r0 : 0x00010098 -> 0x00000003
$r1 : 0x0001x009c -> 0x00000004
$r2 : 0x00000003
$r3 : 0x00000000
$r4 : 0x00000000
$r5 : 0x00000000
$r6 : 0x00000000
$r7 : 0x00000000
$r8 : 0x00000000
$r9 : 0x00000000
$r10 : 0x00000000
$r11 : 0x00000000
$r12 : 0x00000000
$sp : 0xbefff7e0 -> 0x00000001
$lr : 0x00000000
$pc : 0x00010080 -> <_start+12> str r2, [r1]
$cpsr : 0x00000010

下一条指令将在偏移地址模式下执行STR指令。它将把R2中的值(0x00000003)存储在:R10x0001x009c)+偏移(#2)= 0x1009e地址处,运行完该条指令后用x/w命令查看0x0001x009c处的值为0x3,完全正确。

gef> nexti
gef> x/w 0x1009e 
0x1009e <var2+2>: 0x3

再下一条~指令是前变址寻址。可以根据“!”来识别该模式。唯一区别是,基准寄存器会被更新为最终访问地址。这意味着,我们将R20x3) 中的值存储到 地址:R10x1009c)+ 偏移量(#4) = 0x100A0,并使用此地址更新 R1。运行完命令查看0x100A0地址处的值,然后使用命令info register r1查看R1的值。

gef> nexti
gef> x/w 0x100A0
0x100a0: 0x3
gef> info register r1
r1     0x100a0     65696

最后一条LDR指令是后变址寻址。意思是R1中的值作为最终访问地址,获取最终访问地址处的值加载到R3。然后将R10x100A0)更新为R1(0x100A0)+ 偏移(#4)= 0x100a4。运行完该命令看看寄存器R1R3的值。

gef> info register r1
r1      0x100a4   65700
gef> info register r3
r3      0x3       3

下图是实际发生的事情:

2.偏移模式:寄存器作为偏移量(寄存器基址变址寻址

STR    Ra, [Rb, Rc]
LDR    Ra, [Rb, Rc]

这种偏移是使用寄存器作为偏移量。下面的示例是,代码在运行时计算要访问的数组索引。

.data
var1: .word 3
var2: .word 4
​
.text
.global _start_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]  @ 以R1中的值为基准地址,R2中的值(0x03)为偏移量,获得最终访问地址,将R2中的值(0x03)存储到该地址处,基准寄存器R1中的值保存不变。
    str r2, [r1, r2]! @ 前变址寻址:以R1中的值为基准地址,R2中的值(0x03)为偏移量,获得最终访问地址,将R2中的值(0x03)存储到该地址处,基准寄存器R1中的值更新为R1+R2。 
    ldr r3, [r1], r2  @ 后变址寻址:以R1中的值为最终访问地址,获取该地址处的数据并加载到R3,基准寄存器R1中的值更新为R1+R2bx lr
​
adr_var1: .word var1
adr_var2: .word var2

当执行第一条STR指令时,R2中的值(0x00000003)被存储到地址:0x0001009c + 0x00000003 = 0x0001009F

gef> x/w 0x0001009F
 0x1009f <var2+3>: 0x00000003

第二条STR指令操作是前变址寻址,做了同样的操作,不同的一点是R1的值会被更新:R1=R1+R2

gef> info register r1
 r1     0x1009f      65695

最后一条LDR指令操作是后变址寻址。以R1中的值为访问地址,获取该地址处的数据并加载到R3,然后更新R1的值:R1 = R1 + R2 = 0x1009f + 0x3 = 0x100a2

gef> info register r1
 r1      0x100a2     65698
gef> info register r3
 r3      0x3       3

3.偏移模式:缩放寄存器作为偏移量(寄存器基址变址寻址)

LDR    Ra, [Rb, Rc, <shifter>]
STR    Ra, [Rb, Rc, <shifter>]

第三中偏移形式是缩放寄存器作为偏移量。这种情况下,Rb是基地址寄存器,Rc是一个被左移或右移(<shifter>位移操作)缩放过的立即数(Rc中保存的值)。意思是桶型位移操作用来缩放偏移量。下面是一个在数组上循环遍历的例子,可以在GDB中运行看一下:

.data
var1: .word 3
var2: .word 4
​
.text
.global _start
​
_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中的值左移2位(相当于乘以4)为偏移量,加上R1中的基准地址获得最终访问地址,将R2中的值(0x03)存储到该地址,基准寄存器R1中的值不变。
    str r2, [r1, r2, LSL#2]! @ 以R2中的值左移2位(相当于乘以4)为偏移量,加上R1中的基准地址获得最终结果地址,将R2中的值(0x03)存储到该地址,基准寄存器R1中的值被修改: R1 = R1 + R2<<2
    ldr r3, [r1], r2, LSL#2  @ 以R1中的值为访问地址,加载该地址处的数据到R3,基准寄存器R1中的值被修改: R1 = R1 + R2<<2
    bkpt
​
adr_var1: .word var1
adr_var2: .word var2

下面是程序运行时的样子:

第一条不多赘述,第二条STR指令操作使用了前变址寻址,也就是:R1的值0x1009c+R2中的值左移2位(0x03<<2=0xc)= 0x100a8,并更新R1的值为0x100a8R1 = R1 + 0x03<<2 = 0x100a8 + 0xc = 0x100b4

gef> info register r1
r1      0x100a8      65704

最后一条LDR指令操作使用了后变址寻址。意思是,加载R1中的值0x100a8地址处的数据到寄存器R3,然后将R2中的值左移两位(0x03<<2=0xc)得到值0xC,再加上R1中的值0x100a8得到0x100b4,最后R1的值更新为0x100a8R1 = R1 + 0x03<<2 = 0x100a8 + 0xc = 0x100b4

gef> info register r1
r1      0x100b4      65716

总结

记住LDRSTR中有三种偏移形式:

  1. 立即数作为偏移量
  • ldr r3, [r1, #4]
  1. 寄存器作为偏移量
  • ldr r3, [r1, r2]
  1. 带有位移操作的寄存器作为偏移量
  • ldr r3, [r1, r2, LSL#2]

如何记住LDRSTR这些寻址模式:

  • 如果带有!,就是前变址寻址

  • ldr r3, [r1, #4]!

  • ldr r3, [r1, r2]!

  • ldr r3, [r1, r2, LSL#2]!

  • 如果基地值寄存器(R1)带中括号,就是后变址寻址

  • ldr r3, [r1], #4

  • ldr r3, [r1], r2

  • ldr r3, [r1], r2, LSL#2

  • 其他的都是带偏移量的寄存器间接寻址

  • ldr r3, [r1, #4]

  • ldr r3, [r1, r2]

  • ldr r3, [r1, r2, LSL#2]

LDR中的PC相对寻址

LDR是唯一用来加载数据到寄存器中的指令。语法如下:

.section .text
.global _start
​
_start:
   ldr r0, =jump        /* 加载函数标签jump的地址到R0 */
   ldr r1, =0x68DB00AD  /* 加载值0x68DB00AD到R1 */
jump:
   ldr r2, =511         /* 加载值511到R2 */ 
   bkpt

这些指令被称为伪指令,我们可以使用此语法来引用文本池中的数据。在上面的示例中,我们使用这些伪指令引用一个函数的偏移量,在指令中将一个32位常量加载到寄存器中。我需要使用此语法在一个指令中将 32 位常量移动到寄存器中的原因是,ARM 只能一次加载 8 位值。什么?要了解原因,您需要了解 ARM 上如何处理立即数的。

ARM中的立即数

在ARM上加载一个立即数到寄存器中并不像x86上那么简单,ARM对于立即数有很多限制。这些限制是什么以及如何处理它们并不是ARM汇编所关心的,这只是为了有助于你理解,其实有一些技巧可以绕过这些限制(提示:LDR)。

我们知道ARM指令长度是32位,并且所有指令都是可条件执行指令。其中有16种条件码,就要占用4位(2^4=16),然后还要2位代指目标寄存器,2位代指操作寄存器,1位作为状态标志,加起其他一些操作码占用的位。到这里分配完指令类型,寄存器以及其他位段,最后只剩下12位用来操作立即数,最多只能表示4096个数。

这意味着ARM中MOV指令只能操作一定范围内的立即数,如果不能直接被调用,就必须被分割成多个部分,用众多小数字拼起来。

还没完,这12位还不全是用来表示一个整数,其中8位用来表示0-255范围的数n,4位表示旋转循环右移(其实ARM中只有一种位移,就是旋转循环右移,左移也是通过旋转循环右移得到)的次数r(范围0-30)。所以一个立即数的表示形式是:v = n ror 2*r。也就是说,只能以偶数进行旋转循环右移,一次移动两位,n组成的有效位图必须能放到一个字节(8位)中。

下面是一些有效和无效的立即数:

Valid values:
#256        // 1 ror 24 --> 256  循环右移12次,每次两位(注意数据是32位长度)。
#384        // 6 ror 26 --> 384  循环右移13次,每次两位。
#484        // 121 ror 30 --> 484
#16384      // 1 ror 18 --> 16384
#2030043136 // 121 ror 8 --> 2030043136
#0x06000000 // 6 ror 8 --> 100663296 (0x06000000 in hex)
​
Invalid values:
#370        // 185 ror 31 --> 循环右移31位,但超出了(0 – 30)范围,因此不是有效立即数。
#511        // 1 1111 1111 --> 有效位图无法放到一个字节(8位)中。
#0x06010000 // 110 0000 0001.. --> 有效位图无法放到一个字节(8位)中。

译注:1.以上立即数都是32位长度。2.旋转循环右移:每位都向右移动,末位不断放到最前位,类似首尾相连。3.有效位图要能放到一个字节中:例子中*#511*的二进制为*0000 0000 0000 0000 0000 0001 1111 1111*,有效位图为*1 1111 1111*,超过一个字节。*#0x06010000*的二进制位‭*0110 0000 0001 0000 0000 0000 0000*‬,有效位图*110 0000 0001*超过一个字节。

其结果是无法一次加载完整的 32 位地址。我们可以通过使用以下两个选项之一来绕过此限制:

  1. 用较小的值构造较大的值

  2. 不要使用 MOV r0, #511

  3. 分成两部分: MOV r0, #256ADD r0, #255

  4. 使用加载方式“ldr r1, =value”,编译器会很乐意将其转换位MOV指令,或者是PC相对寻址来加载。

  5. LDR r1, = 511

如果你加载了一个无效的立即数,那么编译器会报错:“Error: invalid constant”。如果遇到这种问题你应该知道怎么做。

.section .text
.global _start
​
_start:
    mov     r0, #511
    bkpt

如果尝试编译,编译器会输出类似以下错误:

azeria@labs:~$ as test.s -o test.o
test.s: Assembler messages:
test.s:5: Error: invalid constant (1ff) after fixup

你应该把511拆成几个小数值,或者用前面介绍的LDR方式。

.section .text
.global _start
​
_start:
 mov r0, #256   /* 1 ror 24 = 256, so it's valid */
 add r0, #255   /* 255 ror 0 = 255, valid. r0 = 256 + 255 = 511 */
 ldr r1, =511   /* load 511 from the literal pool using LDR */
 bkpt

如果你想判断一个立即数是否是有效的立即数,你可以用我写的python脚本rotator.py

azeria@labs:~$ python rotator.py
Enter the value you want to check: 511
​
Sorry, 511 cannot be used as an immediate number and has to be split.
​
azeria@labs:~$ python rotator.py
Enter the value you want to check: 256
​
The number 256 can be used as a valid immediate number.
1 ror 24 --> 256

文章作者: 改变世界
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 改变世界 !
评论