我们来学习内存指令:加载和存储
ARM使用载入-存储模型来访问内存,意味着只有加载/存储(LDR和STR)指令才可以访问内存。在X86中,大多数指令允许直接操作内存中的数据,而在ARM中,在操作数据之前,必须把数据从内存移动到寄存器中。这意味着在ARM下,若要 增加特定内存地址里的32位的数值,将需要用到三种类型的指令(载入、增加和存储):首先将特定地址里的数值加载到寄存器中,然后在寄存器中增加它,最后将数据从寄存器返存回内存里
为了解释ARM的加载和存储操作的基本原理,我们接下来会使用几个示例,然后通过gdb调试进一步理解。
敲黑板,这一部分非常重要!!!
先来看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,下一条要执行的指令为str r2,[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, ]
STR Ra, [Rb, Rc, ]
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,nexti 3后查看情况
这条指令使用偏移寻址模式,将在r2中找到的值(0x3)存储在x中,x的计算方式为:
1.r2:x00000003(十六进制)=0000 0011(二进制)
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后使用下面命令验证
确实和我们计算的一样。
参考:
azeria-labs 的arm教程
https://azeria-labs.com