在嵌入式系统开发中,内存地址加载是最基础也是最重要的操作之一。LDR伪指令(LDR Rd, =label)提供了一种高效灵活的地址加载方式,其核心原理是通过文字池(Literal Pool)机制实现任意32位值的加载。
当汇编器遇到LDR r0, =start这样的伪指令时,实际会执行两步转换:
文字池分配:汇编器会在当前代码段附近创建一个文字池区域,并将标签地址(如start)存入其中。文字池通常位于代码段末尾或通过LTORG指令显式指定位置。
PC相对寻址转换:生成实际的LDR指令,通过PC相对寻址从文字池加载数据。例如:
assembly复制LDR r0, [pc, #offset_to_literal_pool] ; offset为当前PC到文字池的偏移量
关键细节:PC值在Arm架构中始终指向当前指令+8的位置(A32模式),计算偏移量时需要特别注意这个特性。
文字池的自动管理是编译器的重要功能,但开发者需要了解其规则以避免常见错误:
示例中展示了一个典型错误场景:
assembly复制func2
LDR r3, =Darea + 6000 ; 正常:使用Literal Pool 1
; LDR r4, =Darea + 6004 ; 错误:超出Literal Pool 1范围
BX lr
Darea SPACE 8000 ; 大内存块
; Literal Pool 2在END后生成(超出范围)
LDR伪指令在嵌入式开发中主要有三大应用:
assembly复制LDR r0, =0x40021000 ; STM32F4的GPIOA基地址
LDR r1, [r0, #0x08] ; 读取IDR寄存器
assembly复制LDR pc, [pc, r2, LSL #2] ; 根据r2值跳转到不同处理程序
.ltorg ; 确保跳转表在范围内
assembly复制LDR r0, =error_message ; 加载字符串地址
BL printf
...
error_message DCB "Error: invalid parameter",0
LDM(Load Multiple)和STM(Store Multiple)是Arm架构中高效的数据传输指令,通过单条指令完成多个寄存器的存取操作。
多寄存器指令支持四种基本寻址模式:
| 后缀 | 含义 | 地址变化方向 | 写回时机 |
|---|---|---|---|
| IA | Increment After | 递增 | 操作后更新 |
| IB | Increment Before | 递增 | 操作前更新 |
| DA | Decrement After | 递减 | 操作后更新 |
| DB | Decrement Before | 递减 | 操作前更新 |
栈操作专用别名:
assembly复制STMFD sp!, {r0-r5} ; 等价于STMDB(满递减栈)
LDMFD sp!, {r0-r5} ; 等价于LDMIA(满递减栈)
通过对比测试单寄存器传输与多寄存器传输的性能差异:
测试条件:
结果对比:
| 方法 | 周期数 | 代码大小 |
|---|---|---|
| LDR/STR循环 | 672 | 24字节 |
| LDM/STM块拷贝 | 96 | 12字节 |
性能提升达7倍,代码尺寸减少50%。实际工程中,8寄存器传输通常可获得最佳性价比。
最朴素的块拷贝实现:
assembly复制mov r2, #num ; 设置计数器
loop:
ldr r3, [r0], #4 ; 加载并更新指针
str r3, [r1], #4 ; 存储并更新指针
subs r2, r2, #1 ; 计数器递减
bne loop ; 循环直到完成
这种实现简单但效率低下,每个字需要3条指令(加载、存储、循环控制)。
改进后的实现采用分块策略:
assembly复制blockcopy:
movs r3, r2, lsr #3 ; 计算8字块数量
beq copywords ; 无完整块则跳转
push {r4-r11} ; 保存工作寄存器
octcopy:
ldm r0!, {r4-r11} ; 一次加载8字
stm r1!, {r4-r11} ; 一次存储8字
subs r3, r3, #1 ; 块计数器递减
bne octcopy ; 继续块拷贝
pop {r4-r11} ; 恢复寄存器
copywords:
ands r2, r2, #7 ; 剩余字数
beq done ; 无剩余则完成
wordcopy:
ldr r3, [r0], #4 ; 单字拷贝
str r3, [r1], #4
subs r2, r2, #1
bne wordcopy
done:
优化要点:
AAPCS规范要求使用满递减栈(Full Descending),典型函数入口/出口处理:
assembly复制function:
push {r4-r6, lr} ; 保存寄存器与返回地址
sub sp, sp, #locals ; 分配局部变量空间
... ; 函数体
add sp, sp, #locals ; 释放局部空间
pop {r4-r6, pc} ; 恢复寄存器并返回
关键细节:
支持嵌套调用的完整示例:
assembly复制main:
push {lr} ; 保存返回地址
bl function1 ; 第一次调用
bl function2 ; 第二次调用
pop {pc} ; 恢复返回地址
function1:
push {r4-r5, lr} ; 保存工作寄存器
... ; 可能调用其他函数
pop {r4-r5, pc} ; 恢复寄存器并返回
function2:
push {r4-r7, lr} ; 保存更多寄存器
... ; 复杂操作
pop {r4-r7, pc} ; 恢复寄存器并返回
当代码量较大时,需要合理控制文字池位置:
assembly复制 bl func1
.ltorg ; 确保文字池在范围内
bl func2
assembly复制 AREA Section1, CODE
... ; 代码段1
LTORG ; 文字池1
AREA Section2, CODE
... ; 代码段2
LTORG ; 文字池2
除了常规用途,LDM/STM还可用于:
assembly复制 stm r0, {r1-r2} ; 存储r1,r2到[r0],[r0+4]
ldm r0, {r2-r1} ; 交换r1和r2的值
assembly复制save_context:
stm sp!, {r0-r12, lr} ; 保存所有寄存器
restore_context:
ldm sp!, {r0-r12, pc} ; 恢复并返回
文字池超出范围:
栈不对齐:
寄存器覆盖:
写回冲突:
stm r0!, {r0-r3}(r0在列表中)经过多年嵌入式开发实践,总结出以下优化经验:
块大小选择:
LDM r0!, {r4-r7} vs LDM r0!, {r4-r11}存储器延迟处理:
assembly复制ldm r0!, {r4-r7} ; 第一次加载
ldm r0!, {r8-r11} ; 第二次加载(与第一次重叠执行)
stm r1!, {r4-r7} ; 存储第一次结果
stm r1!, {r8-r11} ; 存储第二次结果
缓存预取技巧:
assembly复制pld [r0, #128] ; 预取后续数据
ldm r0!, {r4-r11} ; 当前块加载
双缓冲技术:
assembly复制; 缓冲区A
ldm r0!, {r4-r11}
stm r1!, {r4-r11}
; 缓冲区B(并行处理)
ldm r2!, {r12-r14}
stm r3!, {r12-r14}
在Cortex-M7等带缓存处理器上,合理的块大小和预取策略可提升30%以上的拷贝性能。