ARM汇编学习三

本文详细介绍了ARM汇编中LDM和STM指令的使用,包括不同形式的指令如何操作内存,如LDMIA、LDMIB、LDMDA等,以及如何通过gdb调试观察寄存器和内存变化。同时,还探讨了PUSH和POP指令在ARM中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

有时,一次性加载(或存储)多个值更有效率。因此,我们需要使用LDM(载入多个值)和STM(存储多个值)。这些指令基于起始地址的不同,有不同的形式。下面是我们将在本节中将会使用的代码。我们将一步一步地完成每一个指令.代码在test5.s中
.data

array_buff:
.word 0x00000000 /* array_buff[0] /
.word 0x00000000 /
array_buff[1] /
.word 0x00000000 /
array_buff[2]. This element has a relative address of 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 /* address of array_buff, or in other words - array_buff[0] /
.word array_buff+8 /
address of array_buff[2] */
在这里插入图片描述
.word指向了一个32位共计4字节的数据(内存)区块,这对于理解代码中的偏移很重要。程序中包含了.data区段,在此我们开辟了一个有5个元素的空数组array_buff。.text段包含了由内存操作指令构成的代码,和一个包含了两个标签的只读数据池,其中一个标签规定了一个具有7个元素的数组,另一个标签起到了“桥接”.text区段和.data区段的作用,我们可以用它访问.data区段的array_buffer。

编译链接
在这里插入图片描述
载入gdb调试,在_start下断点
在这里插入图片描述
run,可以看到将要执行的指令
在这里插入图片描述
adr r0, words+12 /* 将word[3]的地址传送给r0*/
我们使用ADR指令(偷懒的办法)将数组第四部分(word[3])的地址传送给R0。我们用R0指向word数组的中间位置,所以我们可以从这开始向前或者向后移动指针
nexti执行
在这里插入图片描述
可以看到此时r0的地址为0x100b8,这是word[3]的地址,说明数组首地址word[0]为0x100b8-0xc(12的16进制)=0x100ac,使用下图命令验证,确实如此
在这里插入图片描述

在上上张图可以看到接下来要执行的两条指令
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]的地址
我们使用nexti 2来执行
在这里插入图片描述
可以看到此时r1为0x200d0,r2为0x200d8,它们分别是array_buff[0]的地址和 array_buff[2]的地址
下一条指令为
ldm r0, {r4,r5} /
words[3] -> r4 = 0x03; words[4] -> r5 = 0x04 /
使用LDM指令,将R0指向的内存中,取两个字的数据出来。由于之前我们让R0指向了word[3],因此 word[3]的值会被存入R4, word[4]的会被存入R5
也就是说,使用一条指令加载了多个数据(2个数据块),并将R4设置为0x00000003 ,R5设置为 0x00000004
使用nexti执行后如图所示
在这里插入图片描述
符合我们的推测
下一条指令是
stm r1, {r4,r5} /
r4 -> array_buff[0] = 0x03; r5 -> array_buff[1] = 0x04 /
使用STM指令将多个值写入内存。STM指令将R4,R5寄存器的值0x3和0x4存入R1指向的内存空间中。由于之前我们让R1指向 array_buff的第一个元素,所以指令执行后 array_buff[0] = 0x00000003 ,array_buff[1] = 0x00000004。如果没有特别说明,LDM和STM指令操作的基本单位是一个字(32位等于4个字节)
使用nexti执行后可以看到符合我们的推测
在这里插入图片描述
LDM和STM有多种不同的使用形式。具体是哪一种使用形式由指令的后缀所决定。这个示例列出了后缀的几种形式:-IA (之后增加), -IB (之前增加), -DA (之后减少), -DB (之前减少)。这么多种类型,他们之间是如何区分的呢?由指令的第一个字段:运算指令规定了访问内存的方式(寄存器作为源地址还是目标地址)。实际上,由于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分别包含了 0x3,0x4,0x5 ,内存地址 0x000100D0, 0x000100D4,和 0x000100D8中也包含了0x3,0x4,0x5
nexti 2执行
在这里插入图片描述
可以看到r4-r6确实符合,再看看0x200d0及其后的地址
在这里插入图片描述
同样验证了
接下来的两条指令为
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 */
LDMIB指令先将源地址增加4字节(一个字),然后开始载入数据。该方法仍能让我们载入一串前向数据,但第一个数据的起始地址相较源地址有4个字节的偏移。这就是为什么我们用LDMIB指令将内存中的第一个元素载入R4后,R4是0x04(word[4]),而不是R0指向的0x3(word[3])
执行之后,寄存器R4-R6存储了0x4,0x5,0x6, 0x100D4, 0x100D8, 和0x100DC也存储了 0x4,0x5,0x6。
在这里插入图片描述
再看看对应的地址
在这里插入图片描述
符合我们的推测
接下来执行的是

ldmda r0, {r4-r6} /* words[3] -> r6 = 0x03; words[2] -> r5 = 0x02; words[1] -> r4 = 0x01 /
使用LDMDA向相反方向执行运算。R0指向word[3],载入操作开始后,我们向后将 words[3], words[2]和words[1]载入R6, R5, R4。注意,连寄存器都是反着被载入的。所以指令将 R6赋值为0x00000003, R5赋值为0x00000002, R4赋值为0x00000001。这里的逻辑是,每次载入操作完成后向后减少内存地址所以是反向赋值。
Nexti后验证
在这里插入图片描述
符合推测
下一条执行的是
ldmdb r0, {r4-r6} /
words[2] -> r6 = 0x02; words[1] -> r5 = 0x01; words[0] -> r4 = 0x00 */
同样的道理,执行后r6位0x2,r5为0x1,r4为0x0
在这里插入图片描述
接着执行的是

stmda r2, {r4-r6} /* r6 -> array_buff[2] = 0x02; r5 -> array_buff[1] = 0x01; r4 -> array_buff[0] = 0x00 */
同样的道理,存储值之后,降低地址,由r6往r4走,执行之后,array_buff[0](0x200d0)为0x0,array_buff[1](0x200d4)为0x1,array_buff[2](0x200d8)为0x2
nexti执行
在这里插入图片描述
这一条执行的是

stmdb r2, {r4-r5} /* r5 -> array_buff[1] = 0x01; r4 -> array_buff[0] = 0x00; */
一样的道理,执行之后,array_buff[1](0x100d4)为0x1,array_buff[0](0x100d0)为0x0
nexti执行后使用下列命令查看
在这里插入图片描述
符合推测

接下来看看push和pop
进程中有使用一块内存区域叫堆栈。堆栈指针(SP)是一个寄存器,在正常情况下,它总是指向栈内存区域中的一个地址。应用程序通常使用堆栈进行临时数据存储。之前提过,ARM使用加载/存储模型进行内存访问,这意味着指令LDR/STR或它们的派生指令(LDM…/STM…)用于内存操作。在x86中,我们使用PUSH和POP从堆栈中加载和存储。在ARM中,我们也可以使用这两个指令:
当我们把数据压入降序分布的栈区(详见第7部分:栈和函数)后会发生:
首先,SP寄存器里的地址值减4
第二步,将信息储存在SP指向的新的地址空间中
当把数据弹出栈时会发生:
首先,当前SP地址所指向的内存里的值内载入到相应寄存器中
然后SP里的值加4
demo代码如下,在test6.s中
.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
在这里插入图片描述
编译,链接后可以查看其汇编代码
在这里插入图片描述
我们发现, LDMIA 和STMDB指令被转换成了 PUSH和POP。这是因为push有同义词stmdb SP!,而pop有同义词 LDMIA sp!
使用gdb调试,在_start下断点,使用nexti 2直接先执行前两条mov指令,然后看看情况
先看看sp
在这里插入图片描述
可以看到下一条push指令应该会把SP减8,并将R0和R1的值压入堆栈

我们使用nexti执行后看看
在这里插入图片描述
可以看到本来sp是0xbeff3a0,现在为0xbeff398,0xbeff398=0xbeff3a0-0x8
并且r0,r1被压栈了
下一条要执行的是pop
在这里插入图片描述
两个值 (0x3和0x4)会被弹出到相应寄存器里(pop {r2, r3}),所以 R2 = 0x3 ,R3 = 0x4。SP加8
执行nexti后查看
在这里插入图片描述
可以看到,此时sp+8,回到了0xbefff3a0,并且r2被赋为0x3,r3被赋为0x4,符合推测,后续的指令的分析也是同样的方法,不再演示。

参考:
azeria-labs 的arm教程
https://azeria-labs.com/

### 学习ARM汇编的入门指南 学习ARM汇编语言需要从基础概念入手,并结合实际操作进行深入理解。以下是一些关键点和资源推荐,帮助初学者快速掌握ARM汇编的基础知识。 #### 1. ARM汇编的基本概念 ARM汇编是一种低级编程语言,直接与硬件交互,用于编写高效、紧凑的代码。计算机通过电信号的形式接收指令,并将其解释为一系列0/1序列(bits)。为了便于人类理解和记忆,这些电信号被抽象为助记符,形成了汇编语言[^4]。例如,`MOV R0, #1` 是一条典型的ARM汇编指令,表示将值1加载到寄存器R0中。 #### 2. 学习资源推荐 - **官方文档**:ARM官方提供的参考手册是学习ARM汇编的重要资源。它详细描述了每条指令的功能和使用方法。 - **在线教程**:如《Whirlwind Tour of ARM Assembly》和《ARM assembler in Raspberry Pi》等文章提供了丰富的实例和解释,适合初学者逐步掌握。 - **书籍**:《Practical Reverse Engineering: x86, x64, ARM, Windows Kernel, Reversing Tools, and Obfuscation》是一本优秀的逆向工程书籍,其中包含了ARM汇编的相关内容[^4]。 - **集成开发环境(IDE)**:使用Keil MDK等工具可以方便地编写、调试ARM汇编程序[^5]。例如,在Keil中创建一个STM32项目,可以模拟真实的嵌入式开发环境。 #### 3. 实践案例 通过实际编写代码来加深对ARM汇编的理解是非常重要的。以下是一个简单的“Hello, World”程序示例,展示了如何在ARM架构上运行汇编代码[^3]: ```assembly @ 定义数据段 .data msg: .asciz "Hello, ARM\n" @ 定义代码段 .text .global _start _start: @ 将消息地址加载到寄存器R0 LDR R0, =msg @ 调用系统调用以打印字符串 MOV R7, #4 @ 系统调用号4 (sys_write) MOV R1, #1 @ 文件描述符1 (stdout) LDR R2, =len @ 消息长度 SWI 0 @ 触发软中断 @ 退出程序 MOV R7, #1 @ 系统调用号1 (sys_exit) SWI 0 @ 触发软中断 len = . - msg @ 计算消息长度 ``` 此代码片段展示了如何使用ARM汇编编写一个简单的程序,输出“Hello, ARM”字符串[^3]。 #### 4. 进阶学习建议 随着基础知识的掌握,可以进一步探索ARM架构的高级特性,例如异常处理、中断服务程序以及多核处理器的支持。同时,实践是学习汇编语言的关键,建议多尝试编写和调试小型程序,逐步积累经验[^1]。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值