
原文链接:azeria-labs.com/load-and-store-multiple-part-5/
翻译:vvizz
有时候一次加载(或存储)多个值更有效率。为了达到这个目的,我们使用LDM(加载多个值 vvizz:LoaD Multiple)和STM(加载多个值 vvizz:STore 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 /* words[3]的地址 -> r0 */
ldr r1, array_buff_bridge /* array_buff[0]的地址 -> r1 */
ldr r2, array_buff_bridge+4 /* 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个元素的空数组(array_buff),用于存储数据的一块可写内存空间。.text区段中包含内存操作的代码和一个只读数据池,数据池中有两个标签:一个指向7个元素的数组,另一个是.text段和.data段的连通桥梁,方便我们直接访问.data段的array_buf数组。
adr r0, words+12 /* words[3]的地址 -> r0 */
我们使用ADR指令(懒方法,vvizz:原文 lazy approach)获取数组第4个元素(words[3])存放到R0中,之所以定位到数组的中间位置,是因为我们将要从这里进行向前和向后的移动操作。
gef> break _start gef> run gef> nexti
如上之后,R0中存放的是words[3]的地址,也就是0x80B8。计算可得words数组的起始地址words[0]:0x80AC (0x80B8 – 0xC). (vvizz:每个人的机器地址不一定的哈~ 其实不用提醒,嘿嘿)
gef> x/7w 0x00080AC 0x80ac <words>: 0x00000000 0x00000001 0x00000002 0x00000003 0x80bc <words+16>: 0x00000004 0x00000005 0x00000006
我们为array_buff数组的第一个(array_buff[0])和第三个(array_buff[2])元素准备了R1和R2来存放使用,一旦数组的地址获取之后,就可以对它们进行操作了。
ldr r1, array_buff_bridge /* array_buff[0]的地址 -> r1 */ ldr r2, array_buff_bridge+4 /* 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内存地址处开始的两个word值,因为我们之前已经把words[3]的地址存放到R0中,所以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(0x03)和R5(0x04)寄存器的值保存到R1存放的内存地址处。我们之前的操作完成了:R1指向array_buff的第一个元素。所以,在执行完STM指令之后,array_buff[0] = 0x00000003, array_buff[1] = 0x00000004.如果没有特殊的使用标记,LDM和STM指令的每一次操作的数据长度都是一个word类型(32 bits = 4 byte)。
stm r1, {r4,r5} /* r4 -> array_buff[0] = 0x03; r5 -> array_buff[1] = 0x04 */
0x03 和 0x04 两个值将被存放在地址 0x100D0 和 0x100D4 处。下面的指令将对 0x000100D0 地址处的两个word空间进行检查:
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相同,都是在加载动作完成之后再进行自增操作。这样我们就通过一个顺序(地址增大的方向 vvizz:原文是 forward,就是向前的,也就是地址增大的方向)将第一个操作数(存储源地址的寄存器)开始往后的数据加载到第二个操作数序列中。
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个字节(一个word类型)的源地址进行自增,然后执行加载操作。虽然我们依然是地址正向(地址增长方向)的顺序加载数据,但是操作的第一个元素已经与源地址有4字节的偏移了。(vvizz:虽然我们还坐在一起,但是心已经。。。哎。。。比以前更近啦~哈哈,开森开森)这就是为啥在我们示例中通过LDMIB指令将R0指向的内存地址值加载到R4寄存器,第一个元素是 0x00000004 (words[4])而不是 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指令的时候,所有操作的顺序都变成向后顺序了(vvizz:就是地址递减顺序)。R0指向words[3],指令执行的时候,将会将 words[3], words[2] 和 words[1] 的值加载到 R6, R5, R4 中。你没有看错,寄存器被加载的顺序也是反的。指令执行之后结果就是:R6 = 0x00000003, R5 = 0x00000002, R4 = 0x00000001. 上述操作的逻辑:因为我们每次在加载动作完成之后就会进行源地址的自减操作,所以移动的方向也是反向的。同理,寄存器编号大小和内存地址的大小是对应的,每次加载完成后进行自减操作,所以寄存器编号的顺序也是反向的。示例中的LDMIA(或者LDM),我们首先加载的源地址是低地址,然后每次操作之后的自增操作就会让寄存器编号升高。(vvizz:最后一句说的是LDMIA,不是LDMDA)
先加载多个值,然后自减:
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
先进行自减操作,然后存储多个值:
stmdb r2, {r4-r5} /* r5 -> array_buff[1] = 0x01; r4 -> array_buff[0] = 0x00; */
执行完成后,array_buff[1] 和 array_buff[0] 的值如下:
gef> x/2w 0x100D0 0x100d0 <array_buff>: 0x00000000 0x00000001
入栈 和 出栈
进程中有一个内存区间,叫做栈。栈指针(SP)正常情况下,总是指向栈空间的一个地址。程序一般会将临时变量存储到栈区中,并且前边已经讲过了,ARM是通过加载/存储模式进行内存访问的,也就是说,LDR / STR 或他们的衍生指令(LDM../STM..)被用来内存操作。在X86平台,我们使用PUSH 和 POP 指令对栈区进行读写操作;在ARM平台,我们也可以使用这两个指令:
当我们PUSH一些东西到递减栈(vvizz:原文 Full Descending, 更多栈的信息请查阅 第七章:https://azeria-labs.com/functions-and-the-stack-part-7/)之后会发生如下情况:
1. 首先,SP的值减4;
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 sp!, reglist 是同义词,POP 和 LDMIA sp!, reglist 是同义词(查看ARM Manual:http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0489e/Babefbce.html)(vvizz:reglist 是寄存器列表的意思)
看看GDB里边是啥样:
gef> break _start gef> run gef> nexti 2 [...] gef> x/w $sp 0xbefff7e0: 0x00000001
前两条指令执行完之后,查看内存地址和SP指向的内存地址的值,下一条 PUSH 指令,SP的值减8,然后把R0 和 R1 (按这个顺序)的值入栈。
gef> nexti [...] ----- Stack ----- 0xbefff7d8|+0x00: 0x3 <- $sp 0xbefff7dc|+0x04: 0x4 0xbefff7e0|+0x08: 0x1 [...] gef> x/w $sp 0xbefff7d8: 0x00000003
接下来,这两个值(0x3 和 0x4) 出栈到寄存器中,结果是 R2 = 0x3, R3 = 0x4,SP 加 8.
gef> nexti gef> info register r2 r3 r2 0x3 3 r3 0x4 4 gef> x/w $sp 0xbefff7e0: 0x00000001


相关文章本文地址:https://blog.sxx1314.com/sdk-android/556.html
版权声明:若无注明,本文皆为“unix 软硬件 技术宅 ”原创,转载请保留文章出处。百度已收录















