/*
* 2018/12/17 9:53 qing
*/
/*
* ldr, str
*
* https://www.jianshu.com/p/cd090a2e5164 (动画)
* https://azeria-labs.com/memory-instructions-load-and-store-part-4/
*/
地址模式:用作偏移
地址模式:前向索引
地址模式:后向索引
.date /* 数据段是在内存中动态创建的,所以它的在内存中的地址不可预测*/
var1: .word 3 /* 内存中的第一个变量 */
var2: .word 4 /* 内存中的第二个变量 */
.text /* 代码段开始 */
.global _start
_start:
ldr r0, adr_var1 @ 将存放var1值的地址adr_var1加载到寄存器R0中
ldr r1, adr_var2 @ 将存放var2值的地址adr_var2加载到寄存器R1中
ldr r2, [r0] @ 将R0所指向地址中存放的0x3加载到寄存器R2中
str r2, [r1] @ 将R2中的值0x3存放到R1做指向的地址
bkpt
adr_var1: .word var1 /* var1的地址助记符 */
adr_var2: .word var2 /* var2的地址助记符 */
在底部我们有我们的文字标识池(在代码段中用来存储常量,字符串,或者偏移等的内存,可以通过位置无关的方式引用),
分别用adr_var1和adr_var2存储着变量var1和var2的内存地址(var1和var2的值在数据段定义)。第一条LDR指令将变量var1的地址加载到寄存器R0。
第二条LDR指令同样将var2的地址加载到寄存器R1。之后我们将存储在R0指向的内存地址中的值加载到R2,最后将R2中的值存储到R1指向的内存地址中。
当我们加载数据到寄存器时,方括号"[]"意味着:将其中的值当做内存地址,并取这个内存地址中的值加载到对应寄存器。
当我们存储数据到内存时,方括号"[]"意味着:将其中的值当做内存地址,并向这个内存地址所指向的位置存入对应的值。
Memory (ldr and str-01.gif)
--------------
|___________|
0x00010098 | ... |
0x00010094 | 0x4 | <var2>
0x00010090 | 0x3 | <var1>
| ... |
ldr r0, adr_var1 @ r0 = 0x00010090
ldr r1, adr_var2 @ r1 = 0x00010094
ldr r2, [r0] @ r2 = 0x3
同样的再来看看的这段代码在调试器中的样子:
gef> disassemble _start
Dump of assembler code for function _start:
0x00008074 <+0>: ldr r0, [pc, #12] @ 0x8088 <add_var1>
0x00008078 <+4>: ldr r1, [pc, #12] @ 0x808c <add_var2>
0x0000807c <+8>: ldr r2, [r0]
0x00008080 <+12>: str r2, [r1]
0x00008084 <+16>: bx lr
End if assembler dump.
可以看到此时的反汇编代码和我们编写的汇编代码有出入了。前两个LDR操作的源寄存器被改成了[pc,#12]。这种操作叫做PC相对地址。
因为我们在汇编代码中使用的只是数据的标签,所以在编译时候编译器帮我们计算出来了与我们想访问的文字标识池的相对偏移,即PC+12。
你也可以看汇编代码中手动计算验证这个偏移是正确的,以adr_var1为例,执行到8074时,其当前有效PC与数据段还有三个四字节的距离,所以要加12。
PS:PC是指向当前执行指令之后第二条指令所在位置的,
在32位ARM模式下是当前执行位置加偏移值8,在Thumb模式下是加偏移值4。这也是与X86架构PC的区别之所在。
.data
var1: word 3
var2: word 4
.section .text
.global _start
_start:
ldr r0, [pc, #12] @ <value> must be 4 bytes aligned. 编译器帮我们计算出来了与我们想访问的文字标识池的相对偏移
ldr r1, [pc, #12]
ldr r2, [r0] @ effective PC (PC relative)
str r2, [r1] @ +4
bx lr @ +8
.word var1 @ +12
.word var2
/*
* 第一种偏移形式:立即数作偏移
*
* ldr and str-02.gif
*/
STR Ra, [Rb, imm]
LDR Ra, [Rc, imm]
使用立即数作为偏移量。这个立即数被用来与一个寄存器中存放的地址做加减操作,以访问对应地址偏移处的数据。
.data
var1: .word 3
var2: .word 4
.text
.global _start
_start:
ldr r0, adr_var1 @ 将存放var1值的地址adr_var1加载到寄存器R0中
ldr r1, adr_var2 @ 将存放var2值的地址adr_var2加载到寄存器R1中
ldr r2, [r0] @ 将R0所指向地址中存放的0x3加载到寄存器R2中
str r2, [r1, #2] @ 取址模式:基于偏移量。R2寄存器中的值0x3被存放到R1寄存器的值加2所指向地址处。
str r2, [r1, #4]! @ 取址模式:基于索引前置修改。R2寄存器中的值0x3被存放到R1寄存器的值加4所指向地址处,
@ 之后R1寄存器中存储的值加4,也就是R1=R1+4。
ldr r3, [r1], #4 @ 取址模式:基于索引后置修改。R3寄存器中的值是从R1寄存器的值所指向的地址中加载的,
@ 加载之后R1寄存器中存储的值加4,也就是R1=R1+4。
bkpt
adr_var1: .word var1
adr_var2: .word var2
/*
* 第二种偏移形式:寄存器作偏移
*
* ldr and str-03.gif
*/
STR Ra, [Rb, Rc]
LDR Ra, [Rb, Rc]
在这个偏移模式中,寄存器的值被用作偏移。下面的样例代码展示了当试着访问数组的时候是如何计算索引值的。
.data
var1: .word 3
var2: .word 4
.text
.global _start
_start:
ldr r0, adr_var1 @ 将存放var1值的地址adr_var1加载到寄存器R0中
ldr r1, adr_var2 @ 将存放var2值的地址adr_var2加载到寄存器R1中
ldr r2, [r0] @ 将R0所指向地址中存放的0x3加载到寄存器R2中
str r2, [r1, r2] @ 取址模式:基于偏移量。R2寄存器中的值0x3被存放到R1寄存器的值加R2寄存器的值所指向地址处。R1寄存器不会被修改。
str r2, [r1, r2]! @ 取址模式:基于索引前置修改。R2寄存器中的值0x3被存放到R1寄存器的值加R2寄存器的值所指向地址处,
@ 之后R1寄存器中的值被更新,也就是R1=R1+R2。
ldr r3, [r1], r2 @ 取址模式:基于索引后置修改。R3寄存器中的值是从R1寄存器的值所指向的地址中加载的,
@ 加载之后R1寄存器中的值被更新也就是R1 = R1+R2。
bx lr
adr_var1: .word var1
adr_var2: .word var2
/*
* 第三种偏移形式:寄存器缩放值作偏移
*
* ldr and str-04.png
*/
LDR Ra, [Rb, Rc, <shifter>]
STR Ra, [Rb, Rc, <shifter>]
在这种偏移形式下,第三个偏移量还有一个寄存器做支持。
Rb是基址寄存器,Rc中的值作为偏移量,或者是要被左移或右移的<shifter>次的值。这意味着移位器shifter被用来用作缩放Rc寄存器中存放的偏移量。
下面的样例代码展示了对一个数组的循环操作。
.data
var1: .word 3
var2: .word 4
.text
.global _start
_start:
ldr r0, adr_var1 @ 将存放var1值的地址adr_var1加载到寄存器R0中
ldr r1, adr_var2 @ 将存放var2值的地址adr_var2加载到寄存器R1中
ldr r2, [r0] @ 将R0所指向地址中存放的0x3加载到寄存器R2中
str r2, [r1, r2, LSL#2] @ 取址模式:基于偏移量。 R2寄存器中的值0x3被存放到R1寄存器的值加(左移两位后的R2寄存器的值)所指向地址处。
@ R1寄存器不会被修改。
str r2, [r1, r2, LSL#2]! @ 取址模式:基于索引前置修改。R2寄存器中的值0x3被存放到R1寄存器的值加(左移两位后的R2寄存器的值)所指向地址处,
@ 之后R1寄存器中的值被更新,也就R1 = R1 + R2<<2。
ldr r3, [r1], r2, LSL#2 @ 取址模式:基于索引后置修改。R3寄存器中的值是从R1寄存器的值所指向的地址中加载的,
@ 加载之后R1寄存器中的值被更新也就是R1 = R1 + R2<<2。
bkpt
adr_var1: .word var1
adr_var2: .word var2
/*
* 关于PC相对取址的LDR指令
*/
有时候LDR并不仅仅被用来从内存中加载数据。还有如下这操作:
.section .text
.global _start
_start:
ldr r0, =jump /* 加载jump标签所在的内存位置到R0 */
ldr r1, =0x68DB00AD /* 加载立即数0x68DB00AD到R1 */
jump:
ldr r2, =511 /* 加载立即数511到R2 */
bkpt
这些指令学术上被称作伪指令。
但我们在编写ARM汇编时可以用这种格式的指令去引用我们文字标识池中的数据。
在上面的例子中我们用一条指令将一个32位的常量值放到了一个寄存器中。
为什么我们会这么写是因为ARM每次仅仅能加载8位的值,原因需了解立即数在ARM架构下的处理
/*
* 单寄存器传输
*/
先看第一个,很简单:把单一的数据传入(LDR) 或传出(STR)寄存器,对内存的访问可以是DWORD(32-bit), WORD(16-bit)和BYTE(8-bit)。指令的格式如下:
DWORD:
Rd, addressing1
WORD:
H Rd, addressing2 无符号版
SH Rd, addressing2 有符号版
BYTE:
B Rd, addressing1 无符号版
SB Rd, addressing2 有符号版
addressing1 和addressing2 的分类下面再说,现在理解成某种寻址方式就可以了。
在单寄存器传输方面,还有以下三种变址模式,他们是:
◆ preindex
这种变址方式和x86的寻址机制是很类似的,先对寄存器进行运算,然后寻址,但是在寻之后,基址寄存器的内容并不发生改变,例如:
ldr r0, [r1, #4]
的含义就是把r1+4 这个地址处的DOWRD 加载到r0,而寻址后,r1 的内容并不改变。
◆ preindex with writeback
这种变址方式有点类似于++i的含义,寻址前先对基地址寄存器进行运算,然后寻址. 其基本的语法是在寻址符[]后面加上一个"!" 来表示.例如:
ldr r0, [r1, #4]!
就可以分解成:
add r1, r1, #4
ldr r0, [r1, #0]
◆ postindex
自然这种变址方式和i++的方式就很类似了,先利用基址寄存器进行寻址,然后对基址寄存器进行运算,其基本语法是把offset 部分放到[]外面,例如:
ldr r0, [r1], #4
就可以分解成:
ldr r0, [r1, #0]
add r1, r1, #4
如 果你还记得x86 的SIB 操作的话,那么你一定想ARM是否也有,答案是有也没有。在ss上面提到的addressing1 和addressing2的区别就是比例寄存器
的使用,addressing1可以使用[base, scale, 桶形移位器]来实现SB 的效果,或者通过[base,offset](这里的offset 可以是立即数或者寄存器)来
实现SI 的效果,而addressing2则只能用后者了。
于是每一种变址方式最多可以有3 种寻址方式,这样一来,最多可以有9种用来寻址的指令形式。例如:
ldr r0, [r1, r2, LSR #0x04]!
ldr r0, [r1, -#0x04]
ldr r0, [r1], LSR #0x04
每样找了一种,大概就是这个意思。到此,单寄存器传输就结束了,掌握这些足够应付差事了。下面来看看多寄存器传输吧。
/*
* 多寄存器传输
*/
通过一条指令同时把多个寄存器的内容写到内存或者从内存把数据写到寄存器中,效率高的代价是会增加系统的延迟,
所以armcc 提供了一个编译器选项来控制寄存器的个数。指令的格式有些复杂:
<寻址模式> Rn{!}, {r^}
我们先来搞明白寻址模式,多寄存器传输模式有4 种:
也就是说以A开头的都是在Rn的原地开始操作,而B开头的都是以Rn的下一个位置开始操作。如果你仍然感到困惑,我们不妨看个例子。
所有的示例指令执行前:
mem32[0x1000C] = 0x04
mem32[0x10008] = 0x03
mem32[0x10004] = 0x02
mem32[0x10000] = 0x01
r0 = 0x00010000
r1 = 0x00000000
r3 = 0x00000000
r4 = 0x00000000
1) ldmia r0!, {r1-r3}
2) ldmib r0!, {r1-r3}
执行后: 执行后:
r0 = 0x00010000 r0 = 0x00010000
r1 = 0x01 r1 = 0x02
r2 = 0x02 r2 = 0x03
r3 = 0x03 r3 = 0x04
至于 DA 和 DB 的模式,和 IA / IB 是类似的,不多说了。
最后要说的是,使用ldm 和stm指令对进行寄存器组的保护是很常见和有效的功能。配对方案:
stmia / ldmdb
stmib / ldmda
stmda / ldmib
stmdb / ldmia
继续来看两个例子:
执行前:
r0 = 0x00001000
r1 = 0x00000003
r2 = 0x00000002
r3 = 0x00000001
执行的指令:
stmib r0!, {r1-r3}
mov r1, #1 ; These regs have been modified
mov r2, #2
mov r3, #3
当前寄存器状态:
r0 = 0x00001000
r1 = 0x00000001
r2 = 0x00000002
r3 = 0x00000003
ldmia r0!, {r1-r3}
最后的结果:
r0 = 0x00001000
r1 = 0x00000003
r2 = 0x00000002
r3 = 0x00000001
另外,我们还可以利用这个指令对完成内存块的高效copy:
loop
ldmia r9!, {r0-r7}
stmia r10!, {r0-r7}
cmp r9, r11
bne loop