本文根据 ARM 汇编课程笔记整理,主要围绕 编写 ARM 启动代码 展开。启动代码的目标是在处理器复位后,搭建 C 语言运行所需的最基本环境,最终引导程序进入
main函数。
目录
- [1. 环境准备](#1. 环境准备)
- [2. 启动代码的主要任务](#2. 启动代码的主要任务)
- [3. 汇编格式与伪操作](#3. 汇编格式与伪操作)
- [4. 常用 ARM 指令](#4. 常用 ARM 指令)
- [4.1 MOV 传送指令](#4.1 MOV 传送指令)
- [4.2 ADD / SUB 加减法指令](#4.2 ADD / SUB 加减法指令)
- [4.3 什么是 12 位立即数](#4.3 什么是 12 位立即数)
- [4.4 LDR / STR 加载与存储](#4.4 LDR / STR 加载与存储)
- [4.5 MVN / BIC / ORR 位操作](#4.5 MVN / BIC / ORR 位操作)
- [4.6 条件标志 NZCV 与 CMP](#4.6 条件标志 NZCV 与 CMP)
- [4.7 跳转指令 B / BL / BX](#4.7 跳转指令 B / BL / BX)
- [5. 循环结构](#5. 循环结构)
- [6. 函数定义与调用](#6. 函数定义与调用)
- [6.1 PC 与 LR](#6.1 PC 与 LR)
- [6.2 栈与保护现场](#6.2 栈与保护现场)
- [6.3 STMFD / LDMFD](#6.3 STMFD / LDMFD)
- [6.4 汇编调用 C 函数](#6.4 汇编调用 C 函数)
- [6.5 C 语言调用汇编函数](#6.5 C 语言调用汇编函数)
- [7. 修改工作模式](#7. 修改工作模式)
- [8. 异常向量表](#8. 异常向量表)
- [9. 作业与参考答案](#9. 作业与参考答案)
- [10. 总结](#10. 总结)
1. 环境准备
- 开发工具:Keil 4
- 参考文档:
ARMv7A_V7R_Architecture_Reference_Manual
学习 ARM 汇编的主要目的不是为了写纯汇编程序,而是为了编写启动代码。启动代码运行在 C 语言环境之前,负责初始化硬件和运行环境。
2. 启动代码的主要任务
处理器复位后,启动代码通常需要完成以下工作:
- 初始化异常向量表
- 初始化各工作模式的栈指针寄存器
- 开启 ARM 内核中断允许
- 将工作模式设置为 User 模式
- 引导程序进入 C 语言主函数执行
因此,汇编指令的学习主要围绕这几个目的展开。
3. 汇编格式与伪操作
伪操作不是 ARM 处理器实际执行的指令,而是写给汇编器的命令,用于指导汇编器如何工作。
asm
AREA reset, CODE, READONLY
CODE32
ENTRY
END
AREA:定义一个段。程序、数据、堆栈等都需要组织在不同的段中。reset:段名,通常表示复位向量段,即 CPU 上电后首先执行的第一段代码。CODE:指定该段属性为代码,包含可执行指令。READONLY:只读属性,代码段通常默认且必须只读。CODE32:后续指令使用 32 位 ARM 指令集。THUMB:后续指令使用 16 位 Thumb 指令集。ENTRY:指定程序入口。END:汇编文件结束。
4. 常用 ARM 指令
4.1 MOV 传送指令
asm
MOV{S}<c> <Rd>, #<const>
MOV{S}<c> <Rd>, <Rm>
MOV 也可以带移位操作:
| MOV 形式 | 等价规范形式 |
|---|---|
MOV{S} <Rd>, <Rm>, ASR #<n> |
ASR{S} <Rd>, <Rm>, #<n> |
MOV{S} <Rd>, <Rm>, LSL #<n> |
LSL{S} <Rd>, <Rm>, #<n> |
MOV{S} <Rd>, <Rm>, LSR #<n> |
LSR{S} <Rd>, <Rm>, #<n> |
MOV{S} <Rd>, <Rm>, ROR #<n> |
ROR{S} <Rd>, <Rm>, #<n> |
MOV{S} <Rd>, <Rm>, ASR <Rs> |
ASR{S} <Rd>, <Rm>, <Rs> |
MOV{S} <Rd>, <Rm>, LSL <Rs> |
LSL{S} <Rd>, <Rm>, <Rs> |
MOV{S} <Rd>, <Rm>, LSR <Rs> |
LSR{S} <Rd>, <Rm>, <Rs> |
MOV{S} <Rd>, <Rm>, ROR <Rs> |
ROR{S} <Rd>, <Rm>, <Rs> |
MOV{S} <Rd>, <Rm>, RRX |
RRX{S} <Rd>, <Rm> |
注意:
- 与 C 语言赋值运算对比(左值/右值),便于理解。
#<n>/<Rs>取值范围一般为 0 - 31。RRX是扩展右移,不需要移位量。- 计算机只识别二进制数据,没有有无符号、浮点等概念,这些是解释方式。
示例:
asm
MOV r0, #0x8
MOV r1, r0
MOV r3, #31
MOV r0, #1
MOV r6, r0, LSL #31
MOV r7, r0, LSL r3
4.2 ADD / SUB 加减法指令
ADD 指令格式:
asm
ADD{S}<c> <Rd>, <Rn>, #<const>
ADD{S}<c> <Rd>, <Rn>, <Rm>{, <shift>}
ADD{S}<c> <Rd>, <Rn>, <Rm>, <type> <Rs>
SUB 指令格式类似:
asm
SUB{S}<c> <Rd>, <Rn>, #<const>
SUB{S}<c> <Rd>, <Rn>, <Rm>{, <shift>}
SUB{S}<c> <Rd>, <Rn>, <Rm>, <type> <Rs>
示例:
asm
MOV r0, #0x0F
MOV r1, #0xF0
MOV r2, #1
ADD r6, r0, #0xF0
ADD r7, r0, r1
ADD r7, r0, r1, LSL #1
ADD r8, r0, r1, LSL r2
SUB r6, r0, #0xF0
SUB r7, r0, r1
SUB r7, r0, r1, LSL #1
SUB r8, r0, r1, LSL r2
注意:
{, <shift>}中{}表示可选,,表示使用时需要在Rm后添加逗号和移位量。- 为什么没有
ADD r0, #3, #2?因为 C 语言中int a = 1 + 2;会在编译阶段计算,不需要在机器指令中体现。
4.3 什么是 12 位立即数
ARM 数据处理指令中的立即数并不是任意 32 位数,而是 12 位立即数 imm12。
判断标准:
把某个数展开成二进制,该数必须存在一种 循环右移偶数位 的方式,使得移位后高 24 位全 0,低 8 位即为有效 imm8。
编码格式:
0000 (rotate) 0000 0000 (imm8)
N(0-15) 1000 0001
2N(0-30)
也就是说,一个 32 位数如果能由一个 8 位立即数循环右移偶数位得到,它就是合法立即数。
例如:
asm
MOV r0, #0x80000000 ; 合法,1 循环右移 2 位?
LDR sp, =0x40001000 ; 非法立即数,需要用 LDR 伪指令加载
4.4 LDR / STR 加载与存储
asm
LDR<c> <Rt>, <label>
STR<c> <Rt>, <label>
笔记中写的
sdr应为STR。
LDR 除了加载内存数据,还常用作伪指令加载 32 位立即数:
asm
LDR sp, =0x40001000
因为 0x40001000 不是合法 12 位立即数,不能直接 MOV sp, #0x40001000。
4.5 MVN / BIC / ORR 位操作
MVN:按位取反传送
asm
MVN{S}<c> <Rd>, #<const>{, <shift>}
MVN{S}<c> <Rd>, <Rm>
MVN{S}<c> <Rd>, <Rm>, <type> <Rs>
BIC:位清除,指定位置 0
asm
BIC{S}<c> <Rd>, <Rn>, #<const>
BIC{S}<c> <Rd>, <Rn>, <Rm>{, <shift>}
BIC{S}<c> <Rd>, <Rn>, <Rm>, <type> <Rs>
示例:
asm
MOV r0, #0xFFFFFFFF
MOV r1, #1
BIC r2, r0, r1, LSL #31
BIC r3, r0, #(1 << 31)
ORR:位置 1
asm
ORR{S}<c> <Rd>, <Rn>, #<const>
ORR{S}<c> <Rd>, <Rn>, <Rm>{, <shift>}
ORR{S}<c> <Rd>, <Rn>, <Rm>, <type> <Rs>
示例:
asm
MOV r0, #0x00
MOV r1, #1
MOV r2, #31
ORRS r6, r0, #0x80000000
ORRS r7, r0, #(1 << 31)
ORRS r8, r0, r1, LSL #31
ORRS r9, r0, r1, LSL r2
4.6 条件标志 NZCV 与 CMP
CPSR 寄存器中条件判断标志位:
- N:符号标志位。上条指令执行结果最高位 bit31 为 1,则 N = 1,作为有符号解释时为负值。
- Z:零值标志位。结果为 0,则 Z = 1。
- C:进位标志位。无符号解读,加法进位或减法无借位时 C = 1。
- V:溢出标志位。有符号解读,发生溢出时 V = 1(两个正数加得负数,两个负数加得正数)。
常见条件码:
| 条件码 | 含义 |
|---|---|
| EQ | 相等 |
| NE | 不相等 |
| GE | 有符号大于等于 |
| GT | 有符号大于 |
| LE | 有符号小于等于 |
| LT | 有符号小于 |
| AL | 无条件执行 |
CMP 比较指令
asm
CMP<c> <Rn>, #<const>
CMP<c> <Rn>, <Rm>{, <shift>}
CMP<c> <Rn>, <Rm>, <type> <Rs>
CMP r0, r1 等价于 SUBS r0, r1,只更新标志位,不保存结果。
练习:比较获取三个数中最大值。
4.7 跳转指令 B / BL / BX
| 指令 | 说明 | 是否保存返回地址 | 目标 |
|---|---|---|---|
B |
普通分支 | 否 | 标签 |
BL |
带链接分支,函数调用 | 是,保存到 LR | 标签 |
BX |
分支并交换指令集 | 否 | 寄存器 |
asm
B<c> <label>
b fun ; 等价于 ldr pc, =fun
BL<c> <label>
bl fun
BX<c> <Rm>
bx lr ; 等价于 mov pc, lr
5. 循环结构
循环三要素:
- 循环结束条件
- 推动循环趋向终结的语句
- 循环体
do...while 的汇编实现
C 语言:
c
int i = 0;
int sum = 0;
do {
sum += i;
i++;
} while (i <= 100);
汇编:
asm
MOV r0, #0 ; i
MOV r1, #0 ; sum
loop
ADD r1, r1, r0
ADD r0, r0, #1
CMP r0, #100
BLE loop
while / for 的汇编实现
C 语言:
c
int i = 0;
int sum = 0;
while (i <= 100) {
sum += i;
i++;
}
汇编:
asm
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. 函数定义与调用
6.1 PC 与 LR
PC是程序计数器。LR(R14)保存子程序返回地址。- 调用函数:
BL func会把下一条指令地址保存到LR,然后跳转。 - 返回:
BX LR或MOV PC, LR。
示例:
asm
B main
func
MOV r0, #1
MOV r1, #2
ADD r3, r0, r1
BX lr
main
MOV r0, #100
MOV r1, #200
BL func
MOV r3, #300
6.2 栈与保护现场
问题:
- 函数被调可能修改主调寄存器。
- 函数嵌套时,
LR只有一个,无法正确返回。
解决:压栈保护现场,弹栈恢复现场。
栈类型:
- 空栈:
*sp = xxxx,sp++ - 满栈:
sp++,*sp = xxxx - 增栈:
sp += 4 - 减栈:
sp -= 4
ARM 2440 通常采用 满减栈。
栈顶指针初始化:
asm
LDR sp, =0x40001000
Keil 中可在魔术棒 -> Target -> IRAM1 设置:#0x40000000 size:0x1000。
6.3 STMFD / LDMFD
保护现场:
asm
STMDB<c> <Rn>{!}, <registers>
恢复现场:
asm
LDMFD<c> <Rn>{!}, <registers>
示例:
asm
B main
func1
MOV r0, #10
MOV r1, #20
CMP r0, r1
MOVGE r2, r0
MOVLT r2, r1
BX lr
func0
MOV r0, #1
MOV r1, #2
ADD r3, r0, r1
STMFD sp!, {r0-r12, lr}
BL func1
LDMFD sp!, {r0-r12, pc}
; BX lr
main
LDR sp, =0x40001000
MOV r0, #100
MOV r1, #200
STMFD sp!, {r0-r12, lr}
BL func0
LDMFD sp!, {r0-r12, lr}
MOV r3, #300
finish
B finish
END
GNU 体系中保护现场、恢复现场通常由主调完成。
6.4 汇编调用 C 函数
步骤:
-
创建
main.c -
声明
extern void c_add(void); -
导入
IMPORT c_add(Keil 要求) -
保护现场,
BL调用,恢复现场 -
解决编译报错:
asm.axf: Error: L6238E: start.o(reset) contains invalid call from '~PRES8 ...' function to 'REQ8 ...' function c_add.
解决办法:栈对齐伪指令 PRESERVE8,确保函数调用时栈指针保持 8 字节对齐。
- 创建工程自动添加启动代码报错时:
- 重建覆盖工程
- 删除
.sct文件 - 添加
start.s、main.c - 重设软件配置
- 魔术棒 -> Debug -> Use Simulator -> Run to main(取消)
- 魔术棒 -> Linker -> Use Memory Layout from Target Dialog(勾选)
- 魔术棒 -> Target -> ROM1 -> Start:
0x0Size:0x2000 - 函数传参:
asm
STMFD sp!, {r0-r12, lr}
MOV r0, #1
MOV r1, #2
MOV r2, #3
MOV r3, #4
MOV r4, #5
STMFD sp!, {r4}
BL c_add
LDMFD sp!, {r4}
LDMFD sp!, {r0-r12, lr}
6.5 C 语言调用汇编函数
- 导出:
EXPORT func1 - 声明:
extern int func1(int a, int b);
7. 修改工作模式
CPSR 的 M 域控制工作模式。
-
MRS:读状态寄存器asmMRS<c> <Rd>, <spec_reg> -
MSR:写状态寄存器asmMSR<c> <spec_reg>, #<const> MSR<c> <spec_reg>, <Rn>
示例:切换到 User 模式
asm
MRS r0, cpsr
BIC r0, r0, #(0x1FF << 0)
ORR r0, r0, #(0x10 << 0)
MSR cpsr, r0
8. 异常向量表
异常向量表通常放在启动代码最前面。
软中断异常:
asm
SWI #7
通过 SWI 指令可以触发软件中断异常,进入对应的异常处理程序。
9. 作业与参考答案
1. 什么是立即数?如何判断某数是否是非法 12 位立即数?
立即数是在指令中直接给出的常量。ARM 数据处理指令支持 12 位立即数,编码为 8 位有效数循环右移偶数位。若一个 32 位数无法通过这种方式得到,就是非法 12 位立即数,需要用 LDR 伪指令加载。
2. B、BL、BX 指令的区别是什么?
B:普通跳转,不保存返回地址。BL:带链接跳转,保存下一条指令地址到LR,用于函数调用。BX:跳转到寄存器地址,并根据寄存器 bit0 切换 ARM/Thumb 状态,常用于函数返回。
3. ARM 内核采用的栈是哪种栈?
ARM 通常采用 满减栈 (Full Descending),即栈顶指针先减后存,STMDB / LDMFD 配合使用。
4. CPSR 中条件标志位分别在什么情况下被置位?
- N:结果最高位为 1。
- Z:结果为 0。
- C:加法进位或减法无借位。
- V:有符号运算溢出。
5. ARM 汇编调用 C 语言函数以及 C 语言调用汇编函数,参数和返回值如何处理?
- 汇编调用 C:前 4 个参数放入
r0-r3,更多参数压栈;返回值通常放在r0。调用前保护现场,调用后恢复现场,注意 8 字节栈对齐。 - C 调用汇编:汇编函数用
EXPORT导出,C 中用extern声明。参数同样通过r0-r3传递,返回值放在r0。
10. 总结
本文整理了 ARM 汇编启动代码相关的核心知识:
- 启动代码的任务与伪操作
- 常用数据处理指令:MOV、ADD、SUB、MVN、BIC、ORR
- 立即数判断
- 条件标志与 CMP
- 跳转指令 B、BL、BX
- 循环结构
- 函数调用、栈保护、STMFD/LDMFD
- 汇编与 C 混合编程
- 修改工作模式与异常向量表
掌握这些内容后,就可以编写一个基本的 ARM 启动代码,并顺利进入 C 语言环境运行。