ARM语法 Part 5[加载和存储多个值]

  目录

有时你想要更有效率,一次加载(或存储)多个值。为此我们可以使用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

R1R2中分别保存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 */

执行完上面两条指令,看一下R1R2中的值,分别是array_buff[0]array_buff[2]的地址。

gef> info register r1 r2
r1      0x100d0     65744
r2      0x100d8     65752

下一条指令LDMR0指向的words[3]位置加载两个值到R4R5,其中words[3]R4words[4]R5

ldm r0, {r4,r5}              /* words[3]() -> r4 = 0x03; words[4] -> r5 = 0x04 */

我们一条指令就加载了两个数据,让R4=0x00000003R5 = 0x00000004

gef> info registers r4 r5
r4      0x3      3
r5      0x4      4

很好,现在再用STM指令一次存储多条数据值。代码中STMR4R5分别获取值0x030x04,然后依次存储到R1指定的地址处。前面的指令让R1通过array_buff_bridge指向了数组array_buff的开始位置,最终运行结果:array_buff[0] = 0x00000003 and array_buff[1] = 0x00000004。如果没有特殊说明,LDMSTM操作的数据都是32位。

stm r1, {r4,r5}              /* r4 -> array_buff[0] = 0x03; r5 -> array_buff[1] = 0x04 */

现在0x030x04应该分别被保存到了0x100D0 and 0x100D4。下面的指令是产看地址0x000100D0处的两个字长度的值。

gef> x/2w 0x000100D0
0x100d0 <array_buff>:  0x3   0x4

前面提到,LDMSTM有很多变种。其中一种指令后缀。如-IA(increase after)-IB(increase before)-DA(decrease after)-DB(decrease before)。这些变种依据第一个操作数(保存源地址或目标地址的寄存器)指定的不同的内存访问方式而不同。在实践中,LDMLDMIA相同,意思是第一个操作数(寄存器)内的地址随着元素的加载而不断增加。通过这种方式我们根据第一个操作数(保存了源地址的寄存器)获取一连串(正向)的数据。

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以及地址0x000100D00x000100D40x000100D8的值应该是0x30x40x5

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中的值是0x00000004words[4])而不是R0所指的0x00000003words[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以及地址0x100D40x100D80x100DC的值应该是0x40x50x6

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]到寄存器R6R5R4。是的,寄存器也是按照反向顺序。执行完指令后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指令或者他们的衍生指令(LDMSTMLDMIALDMDASTMDA等等)进行内存操作。在x86中使用PUSH和POP从栈内取或存,ARM中我们也可以使用这条指令。

当我们将数据PUSH入向下生长的栈(详见Part 7:堆栈与函数)时,会发生以下事情:

  1. 首先,SP中的地址减少4(译注:4字节=32位)。
  2. 然后,数据存储到SP的新地址值处。

当数据从栈中POP出时,发生以下事情:

  1. 当前SP中地址处的数据加载到指定寄存器中。
  2. 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,并且将R1R0中的值按顺序压入栈中。

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

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