ARM V8R中汇编指令LDR的详细用法
好的,我们来全面、深入地解析 ARMv8-R 架构(通常运行在 AArch32 状态)中 LDR (Load Register) 指令的用法。这是 ARM 汇编中最为核心和重要的指令之一,负责将数据从内存加载到寄存器,是 CPU 与内存交互的关键入口。
🧠 核心功能与概述
LDR 指令的核心功能是:从内存中的一个指定地址读取数据,并将其加载到目标寄存器中。
- 操作定义:
Rt = Memory[Address] - 主要用途:
- 访问内存中的变量和数据。
- 读取硬件外设寄存器的值(通过对内存映射的寄存器进行读取)。
- 在函数调用中,从栈上恢复临时寄存器的值。
- 加载大立即数或地址值到寄存器(作为伪指令)。
为了让您快速概览 LDR 指令的各种形态,下表总结了其最常见的使用方式:
| 指令示例 | 名称/模式 | 功能描述 | 关键点 |
|---|---|---|---|
LDR Rt, [Rn] |
基址寻址 | 从 Rn 指向的地址加载数据到 Rt |
最基础的形式 |
LDR Rt, [Rn, #offset] |
基址+偏移 | 从 Rn + offset 的地址加载数据 |
Rn 不变 |
LDR Rt, [Rn, #offset]! |
前变基寻址 | 先从 Rn + offset 计算地址加载数据,然后更新 Rn |
Rn 会改变 |
LDR Rt, [Rn], #offset |
后变基寻址 | 先从 Rn 的地址加载数据,然后更新 Rn = Rn + offset |
Rn 会改变 |
LDR Rt, =value |
伪指令 | 将一个大立即数或地址加载到 Rt |
汇编器自动转换 |
LDRB Rt, [Rn] |
加载字节 | 从内存加载一个字节,零扩展到32位 | |
LDRH Rt, [Rn] |
加载半字 | 从内存加载一个半字,零扩展到32位 | |
LDRSB Rt, [Rn] |
加载有符号字节 | 从内存加载一个字节,符号扩展到32位 | |
LDRSH Rt, [Rn] |
加载有符号半字 | 从内存加载一个半字,符号扩展到32位 |
⚙️ 语法与操作数格式
LDR 指令的通用语法如下:
LDR{<size>}{<cond>} <Rt>, [<Rn>, <offset>]{!}
LDR{<size>}{<cond>} <Rt>, [<Rn>], <offset>
{<size>}:可选的数据大小指定符。- (空):默认,加载字 (Word, 32位)
B:加载字节 (Byte, 8位),零扩展。H:加载半字 (Halfword, 16位),零扩展。SB:加载有符号字节 (Signed Byte),符号扩展。SH:加载有符号半字 (Signed Halfword),符号扩展。
{<cond>}:可选的条件码后缀(如EQ,NE)。<Rt>:目标寄存器,内存中的数据将被加载到这里。<Rn>:基址寄存器,其值是内存访问的基地址。<offset>:偏移量。可以是:- 一个立即数(如
#12)。 - 一个寄存器(如
Rm)。 - 一个移位操作的寄存器(如
Rm, LSL #2)。
- 一个立即数(如
{!}:写回后缀。如果使用!,则操作完成后,计算后的新地址会被写回基址寄存器Rn。
🛠️ 详细用法与示例
1. 基本加载:从变量或绝对地址读取数据
这是最直接的用法,从另一个寄存器所指向的内存位置读取数据。
LDR R0, =my_variable @ 将变量 'my_variable' 的地址加载到 R0
LDR R1, [R0] @ 从 R0 所指向的内存地址读取数据,加载到 R1
@ 即:R1 = my_variable
2. 访问数组或结构体成员(基址+偏移)
通过基址加偏移的方式,可以高效地访问数组元素或结构体成员。
@ 假设 R0 是数组基地址,我们要读取 array[2] (假设每个元素占4字节)
LDR R1, [R0, #8] @ 地址 = R0 + 8 (2 * 4 bytes)。R0 的值保持不变。
@ R1 = array[2]
@ 访问结构体 (假设 R0 是结构体指针,第二个成员在偏移量 #12 处)
LDR R2, [R0, #12] @ R2 = 结构体的第二个成员
3. 后变基寻址:加载后自动移动指针
这种模式在连续读取内存块(如遍历数组、处理缓冲区)时极其高效。
MOV R0, =array_start @ R0 指向数组起始地址
LDR R1, [R0], #4 @ 步骤 1: 从 [R0] 加载数据到 R1 (array[0]), 然后将 R0 = R0 + 4 (指向array[1])
LDR R2, [R0], #4 @ 步骤 2: 从 [R0] 加载数据到 R2 (array[1]), 然后将 R0 = R0 + 4 (指向array[2])
@ 执行后:
@ R1 = array[0]
@ R2 = array[1]
@ R0 指向 array[2]
4. 前变基寻址:先移动指针,再加载
同样用于连续读取,但逻辑是“先准备好下一个地址,再读数据”。
MOV R0, =array_start
LDR R1, [R0, #4]! @ 步骤 1: 先将 R0 更新为 R0 + 4 (指向array[1]),然后从 [R0] 加载数据到 R1 (array[1])
LDR R2, [R0, #4]! @ 步骤 2: 先将 R0 更新为 R0 + 4 (指向array[2]),然后从 [R0] 加载数据到 R2 (array[2])
5. 加载不同大小和符号的数据
LDR R1, [R0] @ 从 [R0] 加载一个完整的 32 位字
LDRB R2, [R0] @ 从 [R0] 加载一个字节(无符号),零扩展到 32 位。 R2 = 0x000000XX
LDRH R3, [R0] @ 从 [R0] 加载一个半字(无符号),零扩展到 32 位。 R3 = 0x0000XXXX
LDRSB R4, [R0] @ 从 [R0] 加载一个字节(有符号),符号扩展到 32 位。若字节为0xFE,则R4=0xFFFFFFFE
LDRSH R5, [R0] @ 从 [R0] 加载一个半字(有符号),符号扩展到 32 位。
6. 读取硬件寄存器(内存映射I/O)
在嵌入式系统中,通过读取外设寄存器的内存映射地址来获取硬件状态。
@ 假设 UART 状态寄存器的地址是 0x40001000
LDR R0, =0x40001000 @ 将外设寄存器地址加载到 R0
LDR R1, [R0] @ 读取状态寄存器的值到 R1
TST R1, #0x01 @ 测试“数据就绪”位
BNE data_ready @ 如果就绪,则跳转处理
7. 伪指令:加载大立即数或地址(极其重要!)
这是 LDR 指令一个独特而强大的用法。当 MOV 指令无法加载一个大的立即数(如 32 位地址或常数)时,使用 LDR 伪指令。
MOV R0, #0x12345678 @ 错误!0x12345678 很可能是无效立即数,汇编失败
LDR R0, =0x12345678 @ 正确!汇编器会自动处理:
@ 1. 如果可能,转换为 MOV/MOVK 组合指令
@ 2. 否则,将该常数放入“文字池”,并生成一条 PC 相对的 LDR 指令来加载它
LDR R0, =my_variable @ 最常用的用法:直接将变量地址加载到寄存器
LDR R1, =0x55AA55AA @ 加载任意 32 位常数
⚠️ 重要注意事项与原理
-
地址对齐:
LDR:要求地址是 4 字节对齐的。非对齐访问可能会触发异常。LDRH:要求地址是 2 字节对齐的。LDRB:可以对任何地址进行操作。
-
偏移量范围:
- 立即数偏移量
offset有其取值范围(例如 ±4095 字节)。
- 立即数偏移量
-
写回操作 (
!):- 使用
!后缀会更新基址寄存器Rn。这在遍历数组时非常有用,可以节省一条ADD指令。
- 使用
-
与伪指令的区别:
LDR R1, [R0] @ 这是**指令**:从内存地址 [R0] 加载数据 LDR R1, =0x1234 @ 这是**伪指令**:将立即数 0x1234 加载到 R1汇编器会根据上下文区分它们的用途。
-
访问权限:你只能从具有读权限的内存区域进行加载操作。
💎 总结
LDR 指令是 ARM 汇编中将数据从内存系统输入到CPU核心的基础工具,其重要性无可替代。
它的核心价值在于:
- 数据访问:读取内存中的变量和数组元素。
- 硬件交互:通过内存映射I/O读取外设状态,这是嵌入式编程的核心。
- 高效数据处理:通过各种灵活的寻址模式,高效地遍历数据。
- 常量加载:作为伪指令,解决了加载大立即数的核心难题。
简单决策指南:
- 需要从内存加载32位数据 -> 使用
LDR - 需要加载8位/16位数据 -> 使用
LDRB/LDRH或LDRSB/LDRSH(注意符号扩展) - 需要连续读取(如数组处理)-> 使用前变基
[Rn, #offset]!或后变基[Rn], #offset寻址模式 - 需要加载一个大的立即数或地址 -> 使用
LDR Rt, =value伪指令
掌握 LDR 指令是进行任何有意义的 ARM 汇编编程的前提,无论是应用开发还是底层系统编程,都离不开它。
浙公网安备 33010602011771号