在ARM架构的汇编编程中,LDM(Load Multiple)和STM(Store Multiple)指令是处理批量数据传输的核心工具。这些指令允许我们通过单条指令完成多个寄存器的加载或存储操作,相比单寄存器操作可以显著提升效率。
LDM和STM指令的标准语法格式如下:
code复制LDM{addr_mode}{cond} Rn{!}, reglist
STM{addr_mode}{cond} Rn{!}, reglist
其中关键组成部分:
addr_mode:指定寻址模式(IA/IB/DA/DB)cond:可选的条件执行后缀(如EQ、NE等)Rn:基址寄存器,存储内存操作的起始地址!:可选后缀,表示操作后更新基址寄存器reglist:要操作的寄存器列表,用大括号包围例如,以下指令将r1-r4和r7寄存器的值存储到r0指向的内存区域:
armasm复制STMIA r0!, {r1-r4, r7}
ARM提供了四种不同的寻址模式,通过后缀区分:
IA(Increment After):
IB(Increment Before):
DA(Decrement After):
DB(Decrement Before):
关键理解:这里的"Before/After"指的是内存地址更新相对于数据传输的时机。例如"Decrement Before"表示先递减地址指针,再进行数据存储。
在ARM体系结构中,栈的类型由两个关键属性决定:
增长方向:
栈指针位置:
这产生了四种可能的栈类型组合,ARM为每种组合提供了专用后缀:
| 栈类型 | 压栈指令 | 出栈指令 | 等效基本指令 |
|---|---|---|---|
| 满递减 | STMFD | LDMFD | STMDB / LDMIA |
| 满递增 | STMFA | LDMFA | STMIB / LDMDA |
| 空递减 | STMED | LDMED | STMDA / LDMIB |
| 空递增 | STMEA | LDMEA | STMIA / LDMDB |
ARM-Thumb Procedure Call Standard (ATPCS)明确规定使用满递减栈(Full Descending)。这是ARM架构中最常用的栈类型,也是C编译器的默认选择。
典型栈操作示例:
armasm复制; 压栈操作(保存r0-r5和lr)
STMFD sp!, {r0-r5, lr}
; 出栈操作(恢复r0-r5并返回)
LDMFD sp!, {r0-r5, pc}
这里有几个关键细节:
!后缀确保栈指针sp在操作后更新注意事项:在ARMv4T等支持Thumb的架构中,直接弹出到pc进行状态切换需要特别小心,可能引发不可预测的行为。
在子程序入口和出口使用LDM/STM可以高效地保存和恢复工作寄存器:
armasm复制subroutine:
STMFD sp!, {r4-r7, lr} ; 保存工作寄存器和返回地址
; 子程序主体代码
BL another_function ; 调用其他函数
; 更多代码
LDMFD sp!, {r4-r7, pc} ; 恢复寄存器并返回
这种模式的优势:
对比单寄存器拷贝,使用LDM/STM可以显著提升内存块拷贝效率。以下是优化前后的对比:
基础实现(单字拷贝):
armasm复制copy_single:
LDR r3, [r0], #4 ; 从源地址加载一个字
STR r3, [r1], #4 ; 存储到目标地址
SUBS r2, r2, #1 ; 计数器减1
BNE copy_single ; 循环直到完成
优化实现(八寄存器批量拷贝):
armasm复制copy_block:
MOVS r3, r2, LSR #3 ; 计算8字块的数量
BEQ copy_remainder ; 无完整块则跳转
STMFD sp!, {r4-r11} ; 保存额外寄存器
block_loop:
LDMIA r0!, {r4-r11} ; 一次加载8个字
STMIA r1!, {r4-r11} ; 一次存储8个字
SUBS r3, r3, #1 ; 块计数器减1
BNE block_loop ; 继续循环
LDMFD sp!, {r4-r11} ; 恢复寄存器
copy_remainder:
ANDS r2, r2, #7 ; 计算剩余字数
BEQ copy_done ; 无剩余则完成
remainder_loop:
LDR r3, [r0], #4 ; 单字加载
STR r3, [r1], #4 ; 单字存储
SUBS r2, r2, #1 ; 计数器减1
BNE remainder_loop ; 继续循环
copy_done:
MOV pc, lr ; 返回
性能关键点:
Thumb指令集中的LDM/STM有以下主要限制:
典型Thumb块拷贝示例:
armasm复制thumb_copy:
LSR r3, r2, #2 ; 计算4字块数量
BEQ copy_remainder
PUSH {r4-r7} ; 保存寄存器
block_loop:
LDMIA r0!, {r4-r7} ; 加载4个字
STMIA r1!, {r4-r7} ; 存储4个字
SUBS r3, #1 ; 计数器减1
BNE block_loop
POP {r4-r7} ; 恢复寄存器
copy_remainder:
ANDS r2, #3 ; 剩余字数
BEQ copy_done
remainder_loop:
LDMIA r0!, {r3} ; 单字加载
STMIA r1!, {r3} ; 单字存储
SUBS r2, #1
BNE remainder_loop
copy_done:
BX lr ; Thumb模式返回
Thumb还提供了专用的PUSH/POP指令用于栈操作:
armasm复制; 保存寄存器并调用子程序
PUSH {r0-r3, lr}
BL subroutine
POP {r0-r3, pc}
; 保存多个低寄存器
PUSH {r4-r7}
; ...
POP {r4-r7}
关键特点:
为了最大化LDM/STM的性能:
例如,在图像处理中,可以将像素的R、G、B分量分别放在r4、r5、r6,便于批量加载。
虽然ARM支持非对齐访问,但保持内存对齐能提升性能:
现代ARM处理器都有多级缓存,优化建议:
在支持状态切换的架构中:
寄存器列表顺序问题:
STMIA r0!, {r4,r2,r6}(未按编号顺序)STMIA r0!, {r2,r4,r6}(升序排列)基址寄存器更新遗漏:
LDMIA r0, {r1-r3}(忘记!后缀)LDMIA r0!, {r1-r3}栈不平衡:
在实际嵌入式项目中,我曾遇到一个典型问题:系统在启用中断后随机崩溃。最终发现是中断处理函数中使用了STMFD sp!, {r0-r3}但返回时误用了LDMIA sp!, {r0-r3, pc},导致栈指针错位。修正为匹配的LDMFD后问题解决。这提醒我们:在关键代码路径中,必须严格保持栈操作的对称性。