ARM使用load-store指令进行内存访问,这意味着只有LDR和STR指令才能访问内存,虽然在X86上,大多数指令都可以对内存中的数据进行操作,但在ARM上,数据在进行操作之前必须从内存移动到寄存器中。这意味着,在ARM上的特定内存地址增加32位值需要三种指令(加载、增量和存储)才能首先将特定的地址的值加载到寄存器中,在寄存器中递增,然后将其从寄存器存储回内存。
汇编程序例子:
.data /* the .data section is dynamically created and its addresses cannot be easily predicted */
var1: .word 3 /* variable 1 in memory */
var2: .word 4 /* variable 2 in memory */
.text /* start of the text (code) section */
.global _start
_start:
ldr r0, adr_var1 @ load the memory address of var1 via label adr_var1 into R0
ldr r1, adr_var2 @ load the memory address of var2 via label adr_var2 into R1
ldr r2, [r0] @ load the value (0x03) at memory address found in R0 to register R2
str r2, [r1] @ store the value found in R2 (0x03) to the memory address found in R1
bkpt
adr_var1: .word var1 /* address to var1 stored here */
adr_var2: .word var2 /* address to var2 stored here */
在底部,我们有我们的Literal Pool(同一代码段中的一个内存区域,用于存储常量、字符串或偏移量,其他人可以以独立于位置的方式引用),在这里,我们使用标签adr_var1和adr_var2存储var1和var2的内存地址(在顶部的数据段中定义)。第一个LDR将var1的地址加载到寄存器R0中。第二个LDR对var2执行相同的操作,并将其加载到R1。然后,我们将存储在R0中找到的存储器地址处的值加载到R2,并将在R2中找到的值存储到在R1中找到的存储地址。
当我们将某个东西加载到寄存器中时,括号([])的意思是:在这些括号之间的寄存器中找到的值是我们想要从中加载某个东西的内存地址。
当我们将东西存储到内存位置时,括号([])的意思是:在这些括号之间的寄存器中找到的值是我们想要存储东西的内存地址。
现在具体分析下上面ldr 和str 四行汇编代码,在内存中和寄存器中的体现:
ldr r0, adr_var1
ldr r1, adr_var2
ldr r2, [r0]
str r2, [r1]
我们在前两个LDR操作中指定的标签更改为[pc,#12]。这被称为PC相对寻址。因为我们使用了标签,编译器计算了我们在Literal Pool(PC+12)中指定的值的位置。你可以使用这种精确的方法自己计算位置,也可以像我们以前那样使用标签。唯一的区别是,您不需要使用标签,而是需要计算值在Literal Pool中的确切位置。在这种情况下,它距离有效PC位置有3跳(4+4+4=12)。本章稍后将介绍有关PC相对寻址的更多信息。
偏移形式:立即值作为偏移
STR Ra, [Rb, imm]
LDR Ra, [Rc, imm]
这里我们使用立即数(整数)作为偏移量。该值从基址寄存器(以下示例中的R1)中添加或减去,以在编译时以已知的偏移量访问数据。
.data
var1: .word 3
var2: .word 4
.text
.global _start
_start:
ldr r0, adr_var1 @ load the memory address of var1 via label adr_var1 into R0
ldr r1, adr_var2 @ load the memory address of var2 via label adr_var2 into R1
ldr r2, [r0] @ load the value (0x03) at memory address found in R0 to register R2
str r2, [r1, #2] @ address mode: offset. Store the value found in R2 (0x03) to the memory address found in R1 plus 2. Base register (R1) unmodified.
str r2, [r1, #4]! @ address mode: pre-indexed. Store the value found in R2 (0x03) to the memory address found in R1 plus 4. Base register (R1) modified: R1 = R1+4
ldr r3, [r1], #4 @ address mode: post-indexed. Load the value at memory address found in R1 to register R3. Base register (R1) modified: 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 /* to run the next 3 instructions */
我的系统上的寄存器现在填充了以下值(请记住,这些地址在您的系统上可能不同):
$r0 : 0x00010098 -> 0x00000003
$r1 : 0x0001009c -> 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)到R1中指定的存储器地址的值(0x0001009c)+偏移量(#2)=0x1009e。
gef> nexti
gef> x/w 0x1009e
0x1009e <var2+2>: 0x3
下一个STR操作使用预索引地址模式。你可以通过感叹号(!)来识别这种模式。唯一的区别是基址寄存器将被更新为存储R2值的最终存储器地址。这意味着,我们将在R2(0x3)中找到的值存储到R1(0x1009c)中指定的内存地址+偏移量(#4)=0x100A0,并用这个确切的地址更新R1。
gef> nexti
gef> x/w 0x100A0
0x100a0: 0x3
gef> info register r1
r1 0x100a0 65696
最后一个LDR操作使用后索引地址模式。这意味着基址寄存器(R1)被用作最终地址,然后用R1+4计算的偏移量进行更新。换句话说,它取在R1(而不是R1+4)中找到的值,即0x100A0,并将其加载到R3,然后将R1更新为R1(0x100A0)+偏移量(#4)=0x100a4。
gef> info register r1
r1 0x100a4 65700
gef> info register r3
r3 0x3 3
以下是汇编语句的执行效果:
str r2, [r1, #2]
str r2, [r1, #4]!
ldr r3, [r1], #4