一、ARM 常用指令集速查
ARM 汇编指令格式: 指令{S}{条件码} 目标寄存器, 操作数1, 操作数2
加 S 后缀 → 运算完成后更新 CPSR 的条件标志位(N/Z/C/V)
1.1 数据处理指令
|
指令 |
操作 |
示例 |
|
MOV |
Rd = Operand2 |
MOV R0, #5 ; R0 = 5 |
|
MVN |
Rd = NOT Operand2(取反后存入) |
MVN R0, #0 ; R0 = 0xFFFFFFFF |
|
ADD |
Rd = Rn + Op2 |
ADD R0, R1, R2 ; R0 = R1+R2 |
|
ADDS |
Rd = Rn + Op2(更新CPSR) |
ADDS R0, R0, R1 |
|
ADC |
Rd = Rn + Op2 + C(使用进位C) |
ADC R1, R1, R3 ; 64位加法高32位 |
|
SUB |
Rd = Rn - Op2 |
SUB R0, R1, #1 |
|
AND |
Rd = Rn & Op2 |
AND R0, R0, #0xF0 |
|
ORR |
Rd = Rn | Op2 |
ORR R0, R0, #0x01 |
|
EOR |
Rd = Rn XOR Op2 |
EOR R0, R1, #0xFF |
|
BIC |
Rd = Rn & (~Op2) |
BIC R0, R0, #0x1F |
|
CMP |
执行 Rn - Op2,仅更新CPSR,不保存结果 |
CMP R0, #10 |
|
CMN |
执行 Rn + Op2,仅更新CPSR |
CMN R0, R1 |
|
TST |
执行 Rn & Op2,仅更新CPSR |
TST R0, #0x01 |
|
TEQ |
执行 Rn XOR Op2,仅更新CPSR |
TEQ R0, R1 |
1.2 EOR(按位异或)详解
|
用途 |
说明 |
示例 |
|
翻转特定位 |
Op2对应为1的位会被翻转,为0的位保持不变 |
EOR R0, R1, #0xFF → R1低8位全部取反 |
|
判断两值是否相等 |
EOR后若结果为0,说明两寄存器完全相同 |
EOR R0, R1, R2 → R0=0表示R1==R2 |
|
加密/校验 |
CRC校验和XOR加密常用 |
— |
1.3 BIC(位清除)详解
操作:Rd = Rn AND (NOT Op2) —— 先将 Op2 取反,再与 Rn 做按位与
效果:Op2 中为1的位 → 强制将 Rn 对应位清0;Op2 中为0的位 → 保留 Rn 原值
BIC R0, R1, #0x0F ; 将 R1 低4位清零,其余位不变,结果存 R0
BIC R0, R0, #0x1F ; 常用于清除 CPSR 的工作模式位(低5位)
; 经典用法(改工作模式):先BIC清零目标位,再ORR写入新值
BIC R0, R0, #0x1F ; 清除低5位(模式位)
ORR R0, R0, #0x10 ; 写入 User 模式 (10000)
二、立即数限制与 LDR 伪指令
ARM 指令固定32位,既要编码操作码和寄存器,留给立即数的只有12位(8位数值 + 4位移位量)。
2.1 MOV 立即数限制规则
- 合法立即数:8位常数经过偶数次循环右移能得到的值
- 合法:0xFF、0x100(0x01<<8)、0xFF00、0x1FC(0x7F<<1)
- 非法:0x40001000、0x123456 等不能由8位循环移位得到的32位数
MOV R0, #0xFF ; 合法,直接编码
MOV R0, #0x100 ; 合法(1 左移8位)
MOV R0, #0x40001000 ; 非法!汇编器会报错
2.2 LDR 伪指令 —— 加载任意32位立即数
LDR Rd, =constant 是一条伪指令,汇编器将其自动转换为:
- 若值较小且合法 → 直接转换为 MOV 指令
- 若值较大无法用 MOV 表示 → 在代码段附近生成一个「文字池(Literal Pool)」,将常数存入文字池,再用 LDR Rd, [PC, #offset] 从文字池读取
LDR SP, =0x40001000 ; 伪指令,加载任意32位地址到SP
LDR R0, =0x12345678 ; 汇编器自动放入文字池并替换为内存读取
|
💡 为什么PC步进是4 |
code32 模式下,每条指令固定32位(4字节),所以 PC 每步进一次加4。跳转指令(B/BL)会改变PC为目标地址,但跳转后继续顺序执行时仍每次加4。 |
三、64位加法 —— ADDS + ADC
ARM 是32位处理器,要计算64位数据的加法,需要两条指令配合进位标志位 C。
|
指令 |
说明 |
|
ADDS |
加法并更新 CPSR 标志位(包括进位C),但自身不使用C |
|
ADC |
加法并使用 CPSR 中的进位C(用于高32位相加时加上低32位产生的进位) |
// 计算 0x1FFFFFFFF + 0x4(64位加法)
// 低32位存 R0,高32位存 R1
MOV R0, #0xFFFFFFFF ; 低32位
MOV R1, #0x1 ; 高32位(合起来是 0x1_FFFFFFFF)
MOV R2, #0x4 ; 加数
MOV R3, #0x0
ADDS R0, R0, R2 ; 低32位相加,若有进位则 CPSR.C = 1
ADC R1, R1, R3 ; 高32位相加 + 进位C,得到正确结果
; 结果:R0 = 0x00000003,R1 = 0x00000002(即 0x2_00000003)
四、条件码与汇编中的判断语句
ARM 的独特设计:几乎所有指令都可以加条件后缀,CPU 根据 CPSR 的 N/Z/C/V 标志决定是否执行。
4.1 常用条件码
|
后缀 |
含义 |
判断条件(CPSR位) |
|
EQ |
Equal(相等) |
Z = 1 |
|
NE |
Not Equal(不等) |
Z = 0 |
|
GT |
Greater Than(大于,有符号) |
Z = 0 且 N = V |
|
GE |
Greater or Equal(大于等于,有符号) |
N = V |
|
LT |
Less Than(小于,有符号) |
N ≠ V |
|
LE |
Less or Equal(小于等于,有符号) |
Z = 1 或 N ≠ V |
|
HI |
Higher(高于,无符号) |
C = 1 且 Z = 0 |
|
LS |
Lower or Same(低于等于,无符号) |
C = 0 或 Z = 1 |
|
CS/HS |
Carry Set(进位) |
C = 1 |
|
CC/LO |
Carry Clear(无进位) |
C = 0 |
|
AL |
Always(总是执行,默认) |
任何情况 |
4.2 用汇编实现 if-else
// 计算 0x1FFFFFFFF + 0x4(64位加法)
// 低32位存 R0,高32位存 R1
MOV R0, #0xFFFFFFFF ; 低32位
MOV R1, #0x1 ; 高32位(合起来是 0x1_FFFFFFFF)
MOV R2, #0x4 ; 加数
MOV R3, #0x0
ADDS R0, R0, R2 ; 低32位相加,若有进位则 CPSR.C = 1
ADC R1, R1, R3 ; 高32位相加 + 进位C,得到正确结果
; 结果:R0 = 0x00000003,R1 = 0x00000002(即 0x2_00000003)
4.3 用汇编实现 while 循环
// C语言:while (R0 != 0) { R0--; }
loop
CMP R0, #0 ; 判断 R0 是否为0
BEQ end_loop ; 若 R0 == 0,跳出循环
SUB R0, R0, #1 ; R0--
B loop ; 无条件跳回循环开始
end_loop
; B 无条件跳转
; BEQ 相等时跳转(Z=1)
; BNE 不相等时跳转(Z=0)
五、栈操作 —— STMFD / LDMFD
ARM 使用满递减栈(Full Descending,FD):SP 指向最后压入的数据,压栈时向低地址增长。
5.1 STMFD —— 入栈(保护现场)
|
要素 |
说明 |
|
语法 |
STMFD SP!, {寄存器列表} |
|
动作 |
从寄存器列表中编号最高的开始,依次 SP-=4,将寄存器内容存入 SP 地址 |
|
感叹号 ! |
Write-Back,操作完成后将新 SP 值写回 SP 寄存器(必须有!) |
|
效果 |
SP 向低地址移动 4×N 字节(N = 寄存器个数) |
5.2 LDMFD —— 出栈(恢复现场)
|
要素 |
说明 |
|
语法 |
LDMFD SP!, {寄存器列表} |
|
动作 |
从 SP 当前位置依次读取数据到寄存器,每次 SP+=4 |
|
感叹号 ! |
操作完成后将新 SP 值写回 SP 寄存器 |
|
效果 |
SP 向高地址移动(还原到压栈前的位置) |
5.3 实际压栈/出栈过程(图解)
// 函数入口:保护现场
STMFD SP!, {R4-R12, LR}
// 作用:将 R4~R12 和 LR 共10个寄存器压栈
// SP 向低地址移动 10×4 = 40 字节
// 函数出口:恢复现场并返回
LDMFD SP!, {R4-R12, PC} ; 注意:弹出到PC而不是LR!
// 将栈中LR的值直接弹出到PC,实现函数返回
// ^符号(用于异常返回时同时恢复CPSR):LDMFD SP!, {R4-R12, PC}^
|
没有 ! 会怎样 |
STMFD SP, {R0-R3} → 数据压入栈,但 SP 不更新! 下次再压栈时会覆盖刚才的数据,造成栈破坏。 正确写法必须带 !:STMFD SP!, {R0-R3} |
六、ARM 调用约定 ATPCS
ATPCS(ARM-Thumb Procedure Call Standard)规定了函数调用时寄存器的使用规则,保证C和汇编可以互相调用。
6.1 寄存器使用规则
|
寄存器 |
角色 |
规则 |
|
R0 ~ R3 |
参数寄存器 / 返回值 |
前4个整型参数通过 R0~R3 传递;函数返回值存入 R0(64位返回值用 R0+R1);被调用者可以随意修改 |
|
R4 ~ R11 |
保存寄存器 |
被调用的函数不能改变这些寄存器的值!若要使用,必须先压栈保存,返回前恢复 |
|
R12 (IP) |
内部调用临时寄存器 |
编译器使用,不可信赖 |
|
R13 (SP) |
栈指针 |
始终指向当前栈顶,调用前后必须保持一致(平衡栈) |
|
R14 (LR) |
链接寄存器 |
调用函数时由 BL 指令自动设为返回地址;若函数内再调用函数,需先压栈保存 LR |
|
R15 (PC) |
程序计数器 |
不能直接操作(只能通过跳转指令改变) |
6.2 超过4个参数时的传递方式
前4个参数:用 R0~R3 传递(快速,不占栈空间)
第5个及以后的参数:调用者(Caller)负责在调用前将参数压栈,被调用者从栈中读取。
// C语言函数:int c_add(int a, int b, int c, int d, int e, int f)
// 对应汇编调用:
MOV R0, #1 ; 参数 a → R0
MOV R1, #2 ; 参数 b → R1
MOV R2, #3 ; 参数 c → R2
MOV R3, #4 ; 参数 d → R3
MOV R4, #5 ; 参数 e(超过4个,放入R4临时存储)
MOV R5, #6 ; 参数 f
STMFD SP!, {R4, R5} ; 将 e 和 f 压栈!调用者负责压栈
BL c_add ; 调用函数
; c_add 内部:R0~R3读前4个参数,再从SP读第5、6个
|
为什么不直接用R4/R5传参 |
因为 R4~R11 是被保护寄存器,被调用函数内部会保存并恢复它们,如果用来传参被调用者会恢复原值,参数就丢失了。所以规定第5个以后的参数必须通过栈传递。 |
七、C 与汇编混合编程
7.1 基本规则
- 头部必须加 PRESERVE8 ← 声明使用8字节对齐栈(AAPCS标准要求)
- 汇编导出函数:EXPORT 函数名 ← 让C文件可以调用此汇编函数
- 汇编导入C函数:IMPORT 函数名 ← 在汇编中调用C语言函数
- C文件声明汇编函数:int asm_add(int x, int y); ← 参数类型随便定义,功能由汇编实现
7.2 start.s 完整代码注解
PRESERVE8 ; 声明8字节栈对齐
AREA reset, CODE, READONLY ; 定义只读代码段,段名reset
CODE32 ; ARM 32位指令集
ENTRY ; 程序入口点
; ─── 异常向量表(必须放在代码最开头,对应 0x00~0x1C)───
B start ; 0x00 Reset → 跳转到 start 初始化
NOP ; 0x04 Undef(未定义指令异常,暂不处理)
B deal_swi ; 0x08 SWI(软件中断)→ 跳转到 deal_swi 处理
NOP ; 0x0C Prefetch Abort
NOP ; 0x10 Data Abort
NOP ; 0x14 Reserved
NOP ; 0x18 IRQ
NOP ; 0x1C FIQ
; ─── SWI 软中断服务函数 ───
deal_swi
STMFD SP!, {R4-R12, LR} ; 保护现场(把R4~R12和LR全部压栈)
SUB R0, LR, #4 ; LR指向SWI指令的下一条,减4得到SWI指令地址
LDR R1, [R0] ; 读取 SWI 指令的机器码
BIC R0, R1, #(0xFF<<24); 清除高8位(opcode),留下低24位的SWI号
IMPORT c_deal_swi
BL c_deal_swi ; 调用C函数处理SWI号
LDMFD SP!, {R4-R12, PC}^ ; 恢复现场,^ 表示同时恢复 CPSR
; ─── 系统初始化 start ───
start
LDR SP, =0x40001000 ; 设置管理模式(SVC)的栈指针(伪指令)
; === 切换到 User 模式 ===
MRS R0, CPSR ; 读取当前 CPSR
BIC R0, R0, #0x1F ; 清除低5位(模式位)
ORR R0, R0, #0x10 ; 写入 User 模式(10000)
MSR CPSR_C, R0 ; 写入CPSR(只更新控制域)
LDR SP, =0x40000C00 ; 设置 User 模式的栈指针(独立!)
; === 测试函数调用(6个参数)===
MOV R0, #1 ; MOV R1, #2 ; MOV R2, #3 ; MOV R3, #4
MOV R4, #5 ; MOV R5, #6
STMFD SP!, {R4, R5} ; 第5、6个参数压栈
IMPORT c_add
BL c_add ; 调用 C 函数 c_add
SWI #7 ; 触发软中断,SWI号为7
NOP
B start ; 死循环
; ─── 汇编实现的加法函数 asm_add ───
EXPORT asm_add ; 导出供 C 调用
asm_add
STMFD SP!, {R4-R12, LR} ; 保护 R4~R12 和 LR
ADD R0, R0, R1 ; R0 = R0 + R1(利用 ATPCS:参数在R0/R1)
LDMFD SP!, {R4-R12, PC} ; 恢复寄存器,PC = LR(函数返回)
END
7.3 main.c 代码注解
// 声明汇编中实现的函数(必须在C中声明才能调用)
int asm_add(int x, int y);
// SWI 软中断的 C 处理函数
// 参数 num 就是 SWI 指令中的立即数(由汇编提取后传入)
void c_deal_swi(unsigned int num)
{
switch(num)
{
case 7: // SWI #7 → 执行case 7的操作
break;
default:
break;
}
}
// 6个参数的加法函数
// a,b,c,d 通过 R0~R3 传入;e,f 通过栈传入(由调用者压栈)
int c_add(int a, int b, int c, int d, int e, int f)
{
int sum = a + b + c + d + e + f;
return sum; // 返回值放在 R0
}
八、工作模式切换(MSR / MRS)
ARM 处理器有7种工作模式(User/FIQ/IRQ/SVC/Abort/Undef/System),通过操作 CPSR 寄存器切换。
8.1 MRS 和 MSR 指令
|
指令 |
格式 |
作用 |
|
MRS |
MRS Rd, CPSR |
读取 CPSR 到通用寄存器 Rd(必须先读再改) |
|
MSR |
MSR CPSR_<域>, Operand |
将 Operand 写入 CPSR 的指定域 |
8.2 MSR 的域(_fields)
|
域 |
代表 |
说明 |
|
CPSR_C |
控制域(bit0~7) |
包含:中断屏蔽位I/F,Thumb状态T,工作模式M[4:0]。切换模式用这个域 |
|
CPSR_F |
标志域(bit24~31) |
包含:条件码N/Z/C/V |
|
CPSR_X |
扩展域(bit8~15) |
很少直接使用 |
|
CPSR_S |
状态域(bit16~23) |
通常不直接操作 |
8.3 切换到 User 模式(代码解析)
MRS R0, CPSR ; 第一步:将当前CPSR读入R0(不能直接写CPSR)
BIC R0, R0, #0x1F ; 第二步:清除低5位(当前模式位清零)
ORR R0, R0, #0x10 ; 第三步:写入User模式编码 10000 = 0x10
MSR CPSR_C, R0 ; 第四步:将R0写回CPSR控制域(生效!)
|
⚠️ 重要:各模式 SP 是独立的! |
每种工作模式都有自己独立的 SP 寄存器(Banked Register) 切换模式后,SP 的值变为该模式自己的值(未初始化时为未知值) 所以切换到新模式后,必须立即重新设置该模式的 SP! start.s 中:SVC模式 SP = 0x40001000,切换到User后 SP = 0x40000C00 |
九、SWI 软中断
9.1 SWI 指令原理
SWI(Software Interrupt,软件中断)是一条主动触发中断的指令,用于从用户模式进入管理模式(系统调用)。
|
要素 |
说明 |
|
语法 |
SWI #立即数 (立即数范围:0 ~ 0x00FFFFFF,24位) |
|
触发效果 |
CPU 自动切换到 SVC 模式,PC 跳转到 0x08(异常向量表的SWI入口) |
|
立即数的作用 |
编码在 SWI 指令机器码的低24位,用于区分不同的系统调用功能 |
|
Linux 系统调用 |
Linux 用 SWI #0 触发所有系统调用,用 R7 存放系统调用号 |
9.2 如何从 SWI 指令中提取号码
进入 deal_swi 时,LR 指向 SWI 指令的下一条指令,所以 SWI 指令地址 = LR - 4。
deal_swi
STMFD SP!, {R4-R12, LR} ; 保护现场
SUB R0, LR, #4 ; R0 = LR - 4 = SWI指令的地址
LDR R1, [R0] ; R1 = 读取该地址的内容(SWI指令的机器码)
BIC R0, R1, #(0xFF<<24) ; 清除高8位(操作码 0xEF),剩下低24位就是SWI号
; 此时 R0 = SWI号(如 SWI #7 → R0 = 7)
BL c_deal_swi ; 将SWI号作为参数传给C函数处理
LDMFD SP!, {R4-R12, PC}^ ; 恢复现场,^ 表示同时将SPSR恢复到CPSR
9.3 SWI 机器码格式
|
位段 |
bit31~28 |
bit27~24 |
bit23~0 |
|
内容 |
条件码(默认1110=AL) |
0xF(SWI操作码标识) |
SWI立即数(你写的数字) |
|
示例 SWI #7 |
1110 |
1111 |
000000000000000000000111 |
|
💡 异常向量表在代码开头的原因 |
ARM 上电复位后 PC = 0x00000000,执行的第一条指令就是地址0处的内容 如果这里是普通代码,遇到SWI/中断时 CPU 会跳到 0x08,但那里可能是普通指令而不是处理函数 所以代码开头8条必须是异常向量表(B/NOP),真正的初始化代码(start)在向量表之后 |
十、C语言底层原理(汇编视角)
10.1 变量初始化
- 全局变量:存储在 BSS 段(未初始化)或 DATA 段(已初始化),默认为0,无需手动初始化
- 局部变量:存储在栈上,内容为随机值(上次使用的残留数据),必须手动初始化!
10.2 指针的本质
指针是变量,存储的内容是内存地址(一个整数)。指针本身是什么类型,只有在定义声明时才确定。
- 同一个地址(如 0x20000000)可以解释为 int(4字节读取)、char(1字节读取)、二级指针* 或普通整数
- 内存寻址:地址以字节为单位编号,地址0x00=第0字节,0x01=第1字节,依次递增
10.3 if / switch 的底层
- if 条件判断:底层用 CMP 指令比较,结果更新 CPSR 标志位,再用条件跳转指令(BEQ/BNE/BGT等)实现分支
- switch:多个 case 对应多个条件判断,等价于嵌套的 if-else,底层同样是 CMP + 条件跳转
- while 循环:底层是「CMP 判断条件 → BEQ 退出 → 执行循环体 → B 跳回判断」的循环结构
10.4 为什么要用汇编直接操作 SP/LR/PC
C 语言无法直接操作 SP(栈指针)、LR(返回地址)、PC(程序计数器)这些特殊寄存器,因为:
- C 编译器不提供访问这些寄存器的语法
- 初始化 SP 必须在任何 C 代码执行之前完成(SP 未设置时压栈会破坏内存)
- 异常返回需要特殊的 PC 回写语法(LDMFD ... PC}^),只有汇编能表达
- 这就是嵌入式系统的 start.s 必须用汇编写的原因

1514

被折叠的 条评论
为什么被折叠?



