有时你想要更有效率,一次加载(或存储)多个值。为此我们可以使用LDM(load multiple)和STM(stroe multiple)指令。这些指令有各种变体,基本上只因访问初始地址的方式而异。这是我们本节将要使用的代码,将一步步地认识这些指令。
.data
array_buff:
.word 0x00000000 /* array_buff[0] */
.word 0x00000000 /* array_buff[1] */
.word 0x00000000 /* array_buff[2]. 此处是一个相对地址,等于array_buff+8 */
.word 0x00000000 /* array_buff[3] */
.word 0x00000000 /* array_buff[4] */
.text
.global _start
_start:
adr r0, words+12 /* address of words[3] -> r0 */
ldr r1, array_buff_bridge /* address of array_buff[0] -> r1 */
ldr r2, array_buff_bridge+4 /* address of array_buff[2] -> r2 */
ldm r0, {r4,r5} /* words[3] -> r4 = 0x03; words[4] -> r5 = 0x04 */
stm r1, {r4,r5} /* r4 -> array_buff[0] = 0x03; r5 -> array_buff[1] = 0x04 */
ldmia r0, {r4-r6} /* words[3] -> r4 = 0x03, words[4] -> r5 = 0x04; words[5] -> r6 = 0x05; */
stmia r1, {r4-r6} /* r4 -> array_buff[0] = 0x03; r5 -> array_buff[1] = 0x04; r6 -> array_buff[2] = 0x05 */
ldmib r0, {r4-r6} /* words[4] -> r4 = 0x04; words[5] -> r5 = 0x05; words[6] -> r6 = 0x06 */
stmib r1, {r4-r6} /* r4 -> array_buff[1] = 0x04; r5 -> array_buff[2] = 0x05; r6 -> array_buff[3] = 0x06 */
ldmda r0, {r4-r6} /* words[3] -> r6 = 0x03; words[2] -> r5 = 0x02; words[1] -> r4 = 0x01 */
ldmdb r0, {r4-r6} /* words[2] -> r6 = 0x02; words[1] -> r5 = 0x01; words[0] -> r4 = 0x00 */
stmda r2, {r4-r6} /* r6 -> array_buff[2] = 0x02; r5 -> array_buff[1] = 0x01; r4 -> array_buff[0] = 0x00 */
stmdb r2, {r4-r5} /* r5 -> array_buff[1] = 0x01; r4 -> array_buff[0] = 0x00; */
bx lr
words:
.word 0x00000000 /* words[0] */
.word 0x00000001 /* words[1] */
.word 0x00000002 /* words[2] */
.word 0x00000003 /* words[3] */
.word 0x00000004 /* words[4] */
.word 0x00000005 /* words[5] */
.word 0x00000006 /* words[6] */
array_buff_bridge:
.word array_buff /* array_buff的地址, 或者说是array_buff[0]的地址 */
.word array_buff+8 /* array_buff[2]的地址 */
开始之前,你一定要记住.word是指内存中的数据是32位,也就是4字节。这对理解地址偏移量很重要。程序中的.data段分配了一个空白的数组,有5个元素。我们将它作为可写内存来进行数据存储。.text段包含我们的代码,以及包含两个标签的只读数据段。一个标签是包含7个元素的数组,第二个标签用来桥接.text段和.date段,以便我们可以访问保存在.data中的array_buff。
adr r0, words+12 /* address of words[3] -> r0 */
使用ADR
指令(惰性方法)获取words
的第四个元素(words[3]
)的地址,存储到R0
。定位到words
数组的中间,以便接下来向前和向后操作。
gef> break _start
gef> run
gef> nexti
现在R0
存有wards[3]的地址0x80B8
,算一下words[0]地址,也就是数组words开始的地址:0x80AC ( 0x80B8 – 0xC)
。看一下内存值。
gef> x/7w 0x00080AC
0x80ac <words>: 0x00000000 0x00000001 0x00000002 0x00000003
0x80bc <words+16>: 0x00000004 0x00000005 0x00000006
在R1
和R2
中分别保存array_buff数组的第一(array_buff[0])和第三(array_buff[2])个元素的地址。
ldr r1, array_buff_bridge /* address of array_buff[0] -> r1 */
ldr r2, array_buff_bridge+4 /* address of array_buff[2] -> r2 */
执行完上面两条指令,看一下R1
和R2
中的值,分别是array_buff[0]
和array_buff[2]
的地址。
gef> info register r1 r2
r1 0x100d0 65744
r2 0x100d8 65752
下一条指令LDM
从R0
指向的words[3]
位置加载两个值到R4
和R5
,其中words[3]
给R4
,words[4]
给R5
。
ldm r0, {r4,r5} /* words[3]() -> r4 = 0x03; words[4] -> r5 = 0x04 */
我们一条指令就加载了两个数据,让R4=0x00000003
,R5 = 0x00000004
。
gef> info registers r4 r5
r4 0x3 3
r5 0x4 4
很好,现在再用STM
指令一次存储多条数据值。代码中STM
从R4
和R5
分别获取值0x03
和0x04
,然后依次存储到R1
指定的地址处。前面的指令让R1
通过array_buff_bridge
指向了数组array_buff
的开始位置,最终运行结果:array_buff[0] = 0x00000003 and array_buff[1] = 0x00000004
。如果没有特殊说明,LDM
和STM
操作的数据都是32位。
stm r1, {r4,r5} /* r4 -> array_buff[0] = 0x03; r5 -> array_buff[1] = 0x04 */
现在0x03
和0x04
应该分别被保存到了0x100D0
and 0x100D4
。下面的指令是产看地址0x000100D0
处的两个字长度的值。
gef> x/2w 0x000100D0
0x100d0 <array_buff>: 0x3 0x4
前面提到,LDM
和STM
有很多变种。其中一种指令后缀。如-IA(increase after)
、-IB(increase before)
、-DA(decrease after)
、-DB(decrease before)
。这些变种依据第一个操作数(保存源地址或目标地址的寄存器)指定的不同的内存访问方式而不同。在实践中,LDM
与LDMIA
相同,意思是第一个操作数(寄存器)内的地址随着元素的加载而不断增加。通过这种方式我们根据第一个操作数(保存了源地址的寄存器)获取一连串(正向)的数据。
ldmia r0, {r4-r6} /* words[3] -> r4 = 0x03, words[4] -> r5 = 0x04; words[5] -> r6 = 0x05; */
stmia r1, {r4-r6} /* r4 -> array_buff[0] = 0x03; r5 -> array_buff[1] = 0x04; r6 -> array_buff[2] = 0x05 */
执行完上面的指令后,寄存器R4-R6
以及地址0x000100D0
, 0x000100D4
和0x000100D8
的值应该是0x3
, 0x4
和0x5
。
gef> info registers r4 r5 r6
r4 0x3 3
r5 0x4 4
r6 0x5 5
gef> x/3w 0x000100D0
0x100d0 <array_buff>: 0x00000003 0x00000004 0x00000005
LDMIB
指令先将源地址加4个字节(一个字)然后再执行加载。这种方式下我们仍然会得到一串加载的数据,但是第一个元素是从源地址偏移4个字节开始的。这就是为什么例子中LDMIB
指令操作后R4
中的值是0x00000004
(words[4]
)而不是R0
所指的0x00000003
(words[3]
)的原因。
ldmib r0, {r4-r6} /* words[4] -> r4 = 0x04; words[5] -> r5 = 0x05; words[6] -> r6 = 0x06 */
stmib r1, {r4-r6} /* r4 -> array_buff[1] = 0x04; r5 -> array_buff[2] = 0x05; r6 -> array_buff[3] = 0x06 */
上面两条指令执行后,寄存器R4-R6
以及地址0x100D4
, 0x100D8
和0x100DC
的值应该是0x4
, 0x5
和0x6
。
gef> x/3w 0x100D4
0x100d4 <array_buff+4>: 0x00000004 0x00000005 0x00000006
gef> info register r4 r5 r6
r4 0x4 4
r5 0x5 5
r6 0x6 6
当使用LDMDA
指令所有的操作都是反向的。R0
当前指向words[3],当执行指令时反方向加载words[3]
,words[2]
,words[1]
到寄存器R6
,R5
,R4
。是的,寄存器也是按照反向顺序。执行完指令后R6 = 0x00000003,R5 = 0x00000002,R4 = 0x00000001
。这里的逻辑是,每次加载后都将源地址递减一次。加载时寄存器按照反方向是因为:每次加载时地址在减小,寄存器也跟着反方向,逻辑上保证了高地址上对应的是高寄存器中的值。再看一下LDMIA
(或LDM
)的例子,我们首先加载低寄存器是因为源地也是低地址,然后加载高寄存器是因为源地址也增加了。
加载多条值,后递减:
ldmda r0, {r4-r6} /* words[3] -> r6 = 0x03; words[2] -> r5 = 0x02; words[1] -> r4 = 0x01 */
执行后R4、R5和R6的值:
gef> info register r4 r5 r6
r4 0x1 1
r5 0x2 2
r6 0x3 3
加载多条值,前递减:
ldmdb r0, {r4-r6} /* words[2] -> r6 = 0x02; words[1] -> r5 = 0x01; words[0] -> r4 = 0x00 */
执行后R4、R5和R6的值:
gef> info register r4 r5 r6
r4 0x0 0
r5 0x1 1
r6 0x2 2
存储多条值,后递减:
stmda r2, {r4-r6} /* r6 -> array_buff[2] = 0x02; r5 -> array_buff[1] = 0x01; r4 -> array_buff[0] = 0x00 */
执行后array_buff[2],array_buff[1]和array_buff[0]地址处的值:
gef> x/3w 0x100D0
0x100d0 <array_buff>: 0x00000000 0x00000001 0x00000002
存储多条值,前递减:
stmda r2, {r4-r6} /* r6 -> array_buff[2] = 0x02; r5 -> array_buff[1] = 0x01; r4 -> array_buff[0] = 0x00 */
执行后array_buff[2],array_buff[1]和array_buff[0]地址处的值:
gef> x/2w 0x100D0
0x100d0 <array_buff>: 0x00000000 0x00000001
入栈和出栈
进程中有一个叫做栈的内存位置。栈指针(SP)寄存器总是指向栈内存中的地址。程序应用中通常使用栈来存储临时数据。前面讲的ARM中只能使用加载和存储来访问内存,就是只能使用LDR
/STR
指令或者他们的衍生指令(LDM
、STM
、LDMIA
、LDMDA
、STMDA
等等)进行内存操作。在x86
中使用PUSH和POP从栈内取或存,ARM中我们也可以使用这条指令。
当我们将数据PUSH入向下生长的栈(详见Part 7:堆栈与函数)时,会发生以下事情:
- 首先,SP中的地址减少4(译注:4字节=32位)。
- 然后,数据存储到SP的新地址值处。
当数据从栈中POP出时,发生以下事情:
- 当前SP中地址处的数据加载到指定寄存器中。
- SP中的地址值加4。
下面的例子中使用PUSH/POP
以及LDMIA/STMDB
:
.text
.global _start
_start:
mov r0, #3
mov r1, #4
push {r0, r1}
pop {r2, r3}
stmdb sp!, {r0, r1}
ldmia sp!, {r4, r5}
bkpt
反编译一下代码:
azeria@labs:~$ as pushpop.s -o pushpop.o
azeria@labs:~$ ld pushpop.o -o pushpop
azeria@labs:~$ objdump -D pushpop
pushpop: file format elf32-littlearm
Disassembly of section .text:
00008054 <_start>:
8054: e3a00003 mov r0, #3
8058: e3a01004 mov r1, #4
805c: e92d0003 push {r0, r1}
8060: e8bd000c pop {r2, r3}
8064: e92d0003 push {r0, r1}
8068: e8bd0030 pop {r4, r5}
806c: e1200070 bkpt 0x0000
可以看到LDMIA和STMDB被替换成了PUSH和POP。那是因为PUSH是STMDB的同语义指令,POP是LDMIA的同语义指令。
再GDB中调试运行一下:
gef> break _start
gef> run
gef> nexti 2
[...]
gef> x/w $sp
0xbefff7e0: 0x00000001
运行完头两条指令后先查看一下SP指向的地址以及地址处的数值。下一条PUSH指令会将SP减去8,并且将R1
和R0
中的值按顺序压入栈中。
gef> nexti
[...] ----- Stack -----
0xbefff7d8|+0x00: 0x3 <- $sp
0xbefff7dc|+0x04: 0x4
0xbefff7e0|+0x08: 0x1
[...]
gef> x/w $sp
0xbefff7d8: 0x00000003
接下来栈中的值0x03和0x04弹出到寄存器中。
gef> nexti
gef> info register r2 r3
r2 0x3 3
r3 0x4 4
gef> x/w $sp
0xbefff7e0: 0x00000001