【ARM汇编学习笔记】ARMv7‑A汇编伪指令、常用指令、循环函数栈、C与汇编混合编程
📖今日学习:Keil4环境下ARM汇编,伪操作、数据处理指令、移位、比较跳转、循环、函数调用、栈操作、MRS/MSR读写CPSR,汇编和C语言互相调用。
🔍文章大纲
- ARM汇编学习目的(启动代码作用)
- 汇编伪操作(AREA、CODE32、ENTRY、END、PRESERVE8、IMPORT、EXPORT)
- 常用汇编指令详解
- CPSR状态寄存器 NZCV条件标志与条件码
- 汇编实现循环(do‑while / while / for)
- 函数调用、栈操作 STMFD / LDMFD(满递减栈)
- C与汇编互相调用(AAPCS调用规约)
- MRS / MSR读写程序状态寄存器,修改处理器工作模式
- ✨学习重点汇总
- ⚠️易错点踩坑记录
- 📝今日学习小结
1. ARM汇编学习目的
学习ARM汇编,主要是为了编写ARM启动代码 。
处理器复位之后,C语言环境还没有建立,CPU只能执行汇编指令。启动代码要完成如下工作:
- 初始化异常向量表
- 初始化各个处理器工作模式下的栈指针SP
- 设置内核中断开关
- 切换处理器工作模式为User用户模式
- 搭建好C运行环境,跳转执行C语言main函数
我们学习汇编指令,全部围绕启动代码的上述需求展开。开发环境:Keil4,参考手册
armV7A_V7R_architecture_reference_manual。
2. 汇编伪操作
伪操作不是CPU可执行指令,是给汇编器(编译器)看的命令,指导汇编器如何处理代码。
| 伪指令 | 作用说明 |
|---|---|
AREA reset,CODE,READONLY |
定义段;段名reset,代码段,只读;复位代码放在该段 |
CODE32 |
后续代码编译为32位ARM指令集 |
THUMB |
后续代码编译为16位Thumb指令集 |
ENTRY |
标记程序入口点,复位从此处开始执行 |
END |
汇编源文件结束标记,写在整个文件末尾 |
PRESERVE8 |
保持栈8字节对齐;汇编调用C函数必须加,解决L6238E链接报错 |
IMPORT |
导入外部符号(外部C函数) |
EXPORT |
导出本汇编的符号,供C语言调用 |
示例段框架:
PRESERVE8
AREA reset, CODE, READONLY
CODE32
ENTRY
;此处写汇编代码
END
3. 常用汇编指令
指令后缀说明:
{S}:写S代表运算后更新CPSR的NZCV标志位 ;不带S不更新标志。
{<c>}:条件执行后缀,如BEQ、BNE,满足条件才执行指令。
3.1 MOV 数据传送指令
MOV{S}<c> <Rd>, #<const> ;立即数传送到寄存器
MOV{S}<c> <Rd>, <Rm> ;寄存器拷贝
MOV{S}<c> <Rd>,<Rm>,LSL #n ;移位操作,移位0‑31位
;移位类型 LSL逻辑左移,LSR逻辑右移,ASR算术右移,ROR循环右移,RRX扩展循环右移
示例
mov r0,#0x8
mov r1,r0
mov r6,r0,lsl #31
注意:
mov的立即数是12位立即数(imm12) ,不是任意数字都可以直接用#。
12位立即数判断规则 :
把数字二进制展开,该数字能够通过8位原始数据,偶数次循环右移 得到。不能满足则为非法立即数,不能直接用mov,改用LDR Rd,=立即数。
3.2 ADD 加法指令
ADD{S}<c> <Rd>,<Rn>,#<const> ;立即数
ADD{S}<c> <Rd>,<Rn>,<Rm>{,shift} ;寄存器+移位寄存器
示例
mov r0,#0x0F
mov r1,#0xF0
add r6,r0,#0xF0
add r7,r0,r1
add r7,r0,r1,lsl #1
❌错误写法:
add r0,#3,#2;ARM汇编不允许两个立即数相加,编译阶段由汇编器计算。
3.3 SUB 减法指令
SUB{S}<c> <Rd>,<Rn>,#<const>
SUB{S}<c> <Rd>,<Rn>,<Rm>{,shift}
3.4 MVN 按位取反传送
把源操作数按位取反后送入目标寄存器。
MVN{S}<c> <Rd>, #<const>
MVN{S}<c> <Rd>, <Rm>
3.5 BIC 位清除(把指定bit置0)
BIC{S}<c> <Rd>,<Rn>,#<const>
BIC{S}<c> <Rd>,<Rn>,<Rm>{,shift}
示例,把r0的bit31清零
mov r0,#0xFFFFFFFF
mov r1,#1
bic r2,r0,r1,lsl #31
3.6 ORR 按位或(指定bit置1)
ORR{S}<c> <Rd>,<Rn>,#<const>
ORR{S}<c> <Rd>,<Rn>,<Rm>{,shift}
示例,把bit31置1
mov r0,#0
mov r1,#1
orr r6,r0,r1,lsl #31
3.7 CMP 比较指令
CMP Rn,Rm 等价于执行SUBS Rn,Rm,只更新CPSR标志,不保存运算结果。
CMP<c> <Rn>,#<const>
CMP<c> <Rn>,<Rm>{,shift}
3.8 跳转指令 B / BL / BX
| 指令 | 功能 |
|---|---|
B label |
无条件跳转,不保存返回地址 |
BL label |
带链接跳转;把返回地址存入LR(R14),用于函数调用 |
BX Rm |
跳转到寄存器地址,可切换ARM/Thumb状态;BX LR实现函数返回 |
BL用于函数调用;BX LR用于函数返回。
3.9 LDR 伪加载指令
LDR Rd,=value,把任意32位立即数/标签地址加载到寄存器;解决mov立即数限制。
ldr sp,=0x40001000 ;设置栈指针,不能写 mov sp,#0x40001000(非法立即数)
3.10 批量内存访问 STMFD / LDMFD(栈操作)
ARM7默认使用FD满递减栈Full‑Descending,栈向低地址方向增长,SP指向有效栈顶。
-
STMFD SP!,{r0‑r12,lr}:压栈,保存寄存器现场(!代表SP自动更新) -
LDMFD SP!,{r0‑r12,pc}:弹栈,恢复现场,直接跳转返回;保护现场
stmfd sp!,{r0‑r12,lr};恢复现场
ldmfd sp!,{r0‑r12,pc}
3.11 MRS / MSR 读写状态寄存器CPSR/SPSR
普通指令不能直接访问CPSR,需要专用指令
MRS<c> Rd, spec_reg ;读状态寄存器 CPSR/SPSR → Rd
MSR<c> spec_reg, Rn ;把Rn写入状态寄存器
示例修改处理器模式位:
mrs r0,cpsr
bic r0,r0,#0x1F
orr r0,r0,#0x10
msr cpsr,r0
4. CPSR条件标志位 NZCV
CPSR的高4位N Z C V,算术指令带S后缀会更新标志,条件跳转指令依靠标志判断。
| 标志 | 位置 | 置1条件 |
|---|---|---|
| N(负号) | bit31 | 运算结果为负数(有符号,结果最高位为1) |
| Z(零) | bit30 | 运算结果等于0 |
| C(进位) | bit29 | 无符号加法产生进位;减法无借位;移位移出bit=1 |
| V(溢出) | bit28 | 有符号数运算溢出(正数相加变负数,负数相加变正数) |
常用条件码后缀:
EQ:相等(Z=1)NE:不相等(Z=0)GT:有符号大于GE:有符号大于等于LT:有符号小于LE:有符号小于等于AL:无条件执行,指令默认
5. 汇编实现循环
do‑while循环(先执行后判断)
C语言
int i=0,sum=0;
do{
sum += i;
i++;
}while(i <=100);
ARM汇编
mov r0,#0
mov r1,#0
loop
add r1,r1,r0
add r0,r0,#1
cmp r0,#100
ble loop
while / for循环(先判断后执行)
C语言
int i=0,sum=0;
while(i<=100){
sum +=i;
i++;
}
ARM汇编
mov r0,#0
mov r1,#0
loop
cmp r0,#100
bgt finish
add r1,r1,r0
add r0,r0,#1
b loop
finish
b finish
6. 函数调用,栈帧与现场保护
BL func调用函数,自动把返回地址保存到LR(R14)- 如果函数内部还会调用别的子函数,LR会被覆盖,必须压栈保存LR
STMFD压栈保护用到的通用寄存器,函数返回前LDMFD弹栈恢复现场。- AAPCS调用规范:
R0‑R3用作函数参数,R0存放返回值。
简单嵌套函数示例:
func0
stmfd sp!,{r0‑r12,lr} ;保护现场
bl func1 ;调用子函数
ldmfd sp!,{r0‑r12,pc} ;恢复现场,返回
func1
mov r0,#10
mov r1,#20
bx lr ;返回
7. C语言和汇编互相调用
7.1 汇编调用C函数
-
汇编文件开头伪指令:
PRESERVE8保证栈8字节对齐,解决L6238E链接报错 -
IMPORT c_add导入外部C函数符号 -
R0‑R3传递前4个参数;多于4个参数压入栈
-
BL跳转调用C函数;调用前做好寄存器现场保护。
PRESERVE8
AREA reset,CODE,READONLY
CODE32
ENTRY
IMPORT c_addstart
ldr sp,=0x40001000
stmfd sp!,{r0‑r12,lr}
mov r0,#1
mov r1,#2
mov r2,#3
mov r3,#4
mov r4,#5
stmfd sp!,{r4} ;第5个参数压栈
bl c_add
ldmfd sp!,{r4}
ldmfd sp!,{r0‑r12,pc}
END
7.2 C调用汇编函数
- 汇编中使用
EXPORT func_name导出函数名 - C语言中使用
extern int func(int a,int b);声明外部汇编函数。
AAPCS调用约定
- R0、R1、R2、R3传递前4个函数参数
- 返回值存放在R0寄存器
- 超过4个参数,参数从栈上传递。
Keil工程配置要点
- Target选项卡:ROM1起始地址
0x0,Size0x2000;设置IRAM内存地址大小。 - Linker:勾选
Use Memory Layout from Target Dialog,不使用scatter脚本。 - Debug:
Use Simulator,取消Run to main。
✨学习重点
- 汇编伪操作和指令区分:伪操作给汇编器看;指令是CPU执行。重点
AREA、CODE32、ENTRY、END、PRESERVE8、IMPORT、EXPORT。 - MOV立即数受12位imm12限制,非法立即数使用
LDR Rd,=xxx;不能直接mov sp,#立即数,要用ldr sp,=地址。 - 移位指令LSL/LSR/ASR/ROR/RRX;S后缀指令会更新CPSR的NZCV标志位。
- CMP比较本质是SUBS,只改标志,不保存运算结果;B、BL、BX跳转指令区别,BL保存LR用于函数调用,BX LR函数返回。
- ARM7栈:满递减FD栈,SP向低地址增长 ;STMFD压栈、LDMFD弹栈,
!表示自动更新SP。 - CPSR寄存器N/Z/C/V四个标志位含义;条件码EQ/NE/GT/GE/LT/LE。
- MRS读CPSR,MSR写CPSR,用来修改处理器工作模式。
- 汇编循环:do‑while先执行再判断;while先判断后执行。
- 函数调用:BL保存返回地址到LR;嵌套调用必须压栈保存LR防止被覆盖。
- AAPCS调用规范:R0‑R3传参,R0存返回值;汇编调用C必须写
PRESERVE8保证栈8字节对齐。 - 汇编<‑>C互相调用:IMPORT导入C函数,EXPORT导出汇编函数。
⚠️易错点&踩坑记录
- ❌混淆伪操作和指令,把AREA、ENTRY当成CPU指令。
- ❌mov直接写大立即数,遇到非法12位立即数,编译报错;改用
ldr rd,=立即数。 - ❌
mov sp,#0x40001000;立即数非法,必须使用ldr sp,=0x40001000。 - ❌忘记S后缀:不加S的ADD/SUB不会更新CPSR标志,后面条件跳转判断失效。
- ❌B、BL、BX混淆:B跳转不保存LR,不能用于函数调用返回;函数调用用BL,返回用BX LR。
- ❌栈操作忘记写
!,STMFD/LDMFD不会自动更新SP,栈指针错乱。 - ❌汇编调用C函数不加PRESERVE8,报L6238E链接错误。
- ❌函数嵌套调用,没有压栈保护LR;子函数调用覆盖LR,函数返回直接跑飞。
- ❌AAPCS规约记错,函数参数不用R0‑R3,随便乱用寄存器传参,和C交互结果错乱。
- ❌MRS/MSR写错,直接
mov cpsr,r0;CPSR不能用普通MOV读写,只能MRS/MSR。 - ❌汇编调用C函数,超过4个参数忘记压栈;C调用汇编忘记EXPORT导出符号。
- ❌Keil工程scatter脚本干扰,勾选
Use Memory Layout from Target Dialog。
📝今日学习小结
今天学习ARMv7‑A汇编,主要面向启动代码开发。掌握伪操作AREA、CODE32、ENTRY、PRESERVE8;掌握MOV、ADD、SUB、MVN、BIC、ORR、CMP、B/BL/BX等核心指令;理解CPSR的NZCV条件标志。
学会汇编实现do‑while、while循环;掌握满递减栈STMFD/LDMFD做函数现场保护;理解BL保存LR实现函数调用,BX‑LR返回;掌握MRS/MSR读写状态寄存器修改处理器模式。
重点掌握汇编与C语言互相调用,AAPCS调用规约,解决Keil链接报错。
练习方向:汇编实现1‑100求和;汇编写一个函数供C调用;汇编调用C函数。
后续学习:ARM异常向量表编写,完整启动代码。