本系列学习笔记基于 32 位 ARM,暂不提 ARM64 和 16位的 Thumb 模式
笔记博主一年前修的西电课程《嵌入式系统》课堂笔记整理,博主非嵌入式方向,也不打算做嵌入式开发,仅仅就只是单纯学着玩的,这笔记主要是给自己看的,也就图一乐
ARM 运行模式
ARM 一共有三大类、共七种运行模式,可以根据需要在运行模式之间进行切换
- USR:用户模式,运行用户代码。除此之外的模式均为特权模式
- SYS:系统模式,运行操作系统代码
- 异常模式:因各种原因,让程序不能再继续执行时进入的模式
- FIQ:快中断,用于高速数据传输
- IRQ:外部中断,用于通常的中断处理
- SVC:管理模式,操作系统用的保护模式(高权限),复位和软中断进入
- ABT:Abort,数据访问中止模式,数据或指令预取终止时进入,可用于虚拟内存、存储保护
- und:指令未定义中止。该异常可用于处理未知指令的情况,遇到不认识的指令时,为避免程序跑飞,会跳转到异常处理程序中。也可以用于自定义指令、支持协处理器等
CPU 的【取指-译码-执行】循环
只要是 CPU 在正常运行,就会永远执行 【取指-译码-执行】 的流程。该流程可以分为三种模式:
- 顺序执行:取指-译码-执行->取下条指令-译码-执行……
- 跳转:
- 子函数调用:跳转到另一个位置执行指令,执行后会返回跳走的位置
- 分支:跳转后不再返回
- 中断:执行完当前正在执行的指令后,跳转到中断服务程序。完成中断服务程序后再返回原位的下一条指令继续执行。(中断服务程序里面可以做不少事情,比如虚拟内存的换页——因此程序员是感觉不到换页的)
ARM 寄存器
寄存器是 CPU 内部与 ALU 通过内部总线直接连起来的空间。
ARM 总共有 37 个寄存器,比 x86 丰富多了。。
-
31 * 通用寄存器
- R0~R14 以及 PC 是所有模式都可见的(ARM 可以对 PC 进行运算)
- R0~R7 所有模式共用,可以用来在切换状态时传参
- R13 是堆栈指针(SP),指向当前栈顶。每个异常都有自己的 SP,这意味着每个异常都可以有自己的栈
- R14 是链接寄存器(LR),其可在异常发生时自动保存返回地址
- R15 虽然也是通用存储器,但一般用作 PC 。想跳到哪里可以直接向 R15 写值,非常方便。
- 剩下的是特供某些模式的,比如 USR、SYS 下可用 R13、R14,就和 SVC 下的 R13_SVC、R14_SVC 相互独立(模式独立的寄存器非常好用,比如对于 FIQ,其有专属的 R8_FIQ~R12_FIQ,因此跳走时可以用这几个寄存器来代替压栈,实现现场的保护)
- R0~R14 以及 PC 是所有模式都可见的(ARM 可以对 PC 进行运算)
-
6 * 状态寄存器
- 1 * CPSR (Current Program State Register):和 PSW 是一回事
- 5 * SPSR (Saved Program Status Register):每个异常都有自己的 SPSR,用于在进异常时保存 CPSR 值,以便异常处理结束后返回原工作状态
- 1 * CPSR (Current Program State Register):和 PSW 是一回事
ARM 指令系统
ARM 是 Load/Store 结构的处理器,能且仅能在寄存器中进行操作,而不能在内存中做运算。使用时要从存储器读值送寄存器,算完再放回去
指令可以分为这么几类:数据处理、Load/Store(寄存器、内存数据传输)、跳转、CPSR处理、异常产生、协处理器
寻址方式
-
立即寻址(立即数寻址):操作数直接放在指令流中
-
寄存器寻址:去寄存器取值
-
寄存器位移寻址:
MOV R0, R2, LSL #3R2 内的值逻辑左移 3 位送 R0(立即数以#开头) -
基址寻址:值放在内存中,用寄存器值作为存储器的地址,如:
LDR R0, [R1] @ R1 地址的值读到 R0 STR R0, [R1] @ R0 寄存器的值写入地址 R1 -
变址寻址:可用 基址+偏移量 作为地址值去读内存
LDR R0, [R1, #4] @ 偏移量为立即数 LDR R0, [R1, R2] @ 偏移量为寄存器值 LDR R0, [R1, R2, LSL#2] @ 偏移量为 R2 值左移两位 -
相对寻址:使用 PC 作为变址寻址的基址寄存器
-
块拷贝寻址
-
堆栈寻址
ARM 汇编
比较杂,由于我不想把笔记写成字典,因此此处就只记录一些代码示例,用例子代替那些到处都能查到的手册一般的解释
@ 通用格式:<opcode>{<cond>}{S} <Rd>, <Rn>, {, {oprand2}}
@ opcode 是指令,cond 是指令条件比如什么 EQ、NE
@ Rd是目标寄存器,Rn是第一操作数所在的寄存器
@ oprand2是第二操作数,可以是寄存器也可以是立即数
ADD R2, R1, R0 @ 加法,三个操作数, R1+R0 结果值送 R2
ADDS R1, R1, #1 @ 加法,立即数1作为第二个操作数:R1+1结果送R1。
@ S 代表运算影响 CPSR(嗯,ARM 的 PSW 是可以这么手动控制的)
BEQ DATAEVEN @ B 是跳转,EQ 是条件 Equal。DATAEVEN 是目的位置
SUBNES R1, R1, #0x0D @ SUB:减法; NE:NotEq; S:结果影响 CPSR。R1-0x0D 结果送 R1
MOV R0, R2, LSL #3 @ R2左移3位,结果送R0。其实就是 R2*8
ADD R3, R2, LSR R4 @ R2右移R4中值那么多位,结果送 R3
其他还有什么 ADC带进位加、SBC带借位减、AND按位与、ORR按位或、EOR异或、TST按位与、TEQ按位或
LDR、STR 单寄存器整个字(word,32位)存取,STRH 半字(half-word,16位),STRB 字节(8位)
LDM、STM 多寄存器存取,LDMFD R0!, {R1-R4} 从[R0] 开始的四个连续内存单元写 R1-R4,这里可以通过在R0后面写!说明需要回写,通过在{R1-R4}后面加^说明要把 SPSR 复制到 CPSR
SWP 交换俩寄存器的数据,这个可以用于进程间的同步和互斥
MRS、MSR:实现 CPSR、SPSR 的值和通用寄存器 Rn 互写。比如:
MRS R0, CPSR把 CPSR 送 R0MSR R0, SPSR把 R0 送 SPSR
跳转
跳转有两种方式,一是直接往 PC (即,R15) 中写目标地址值,该方式可以跳前后 4GB 的空间;
二是用如下跳转指令(范围是当前指令前后 32MB 的空间)
- B 直接跳
- BL 带返回的跳,跳走前会将当前 PC 值送 R14,后面再回来
- BX 带状态切换,可以实现 ARM、Thumb 的模式切换
- BLX L且X,含义与前面俩相同
伪操作和伪指令
其实伪指令是包含在伪操作中的。这俩玩意都是给汇编器看的。这伪的玩意虽然和正儿八经的汇编一起写到汇编文件中,但汇编器会在汇编时对这些伪的玩意进行处理,给整成真的机器指令或者指令序列
伪操作可以用来定义符号、定义数据、进行控制、进行信息报告(用于在汇编时报错等)、做做段定义、指定代码类型、定义程序入口点之类的
这里一个个写的话也会写成字典,所以还是写几个大例子吧
求最大公约数:
AREA gcd, CODE, READONLY @ 定义代码段
ENTRY @ 标识程序入口点,一个源文件至多有一个
@ 若有多个 ENTRY,需要由链接器指定入口
start CMP R0, R1 @ 标号 start 及指令序列,这里做个比较
BEQ stop @ 相等则跳到 stop 标号处,实现分支跳转
BLT less @ 小于则跳到 less 标号处
SUB R0,R0,R1
B start @ 跳回来,其实是循环
less SUB R1, R1, R0
B start @ 跳回 start 标号开始新的循环
stop NOP @ 空操作,不影响 CPSR
MOV PC, LR
END @ 指定段结尾
AREA gcd, CODE, READONLY 定义了这个小程序的代码段。ARM 汇编以段为单位组织源文件,一个段就是一个相对独立、有特定名称的指令 / 数据序列。多个段经过编译、链接后会成为一个可执行映像文件。这个文件中包含至少一个代码段,可以有零或多个【含或不含初始化数据的】数据段
ARM 异常中断处理
当 CPU 正常运行时,每执行完一条指令,PC 值都会增加 4 (即往后移动 32 位,指向下一条指令)
异常中断时,则会:执行完当前指令后,保护现场并跳转到异常中断处理程序处(CPU 通过将 PC 值改为异常向量表中对应异常处理程序的地址,实现跳转)开始执行。处理程序执行完毕,再回到中断前的位置,恢复现场并继续执行
异常类型及来源:
- 复位(Reset):复位引脚有效
- 未定义指令(Undefined Instruction):读到了无法解码的指令
- 软件中断(SWI,Software Interrupt):指令引起的异常
- 指令预取中止(Prefetch)、数据访问终止(DataAbort):取数或取指时,没有取到期望的值
- 外部中断(IRQ)、快速中断(FIQ):来自引脚有效
向量表是异常的入口地址,发生对应异常时CPU会跳到对应的向量地址,然后再跳转到向量地址中的异常处理地址,实现异常处理。
异常向量表一般都存在地址的最低端,分布如下表
| 地址 | 异常 |
|---|---|
| 0x00 | reset |
| 0x04 | undefined |
| 0x08 | SWI |
| 0x0c | Prefetch |
| 0x10 | DataAbort |
| 0x14 | NOP |
| 0x18 | IRQ |
| 0x1c | FIQ |
其中,0x00 也是系统刚上电时 CPU 跑到的第一个地址
响应和返回步骤
响应
- CPSR 存入对应 SPSR。这个 SPSR 指的是要跳到的中断模式的 SPSR,比如要从 USR 跳到 FIQ,那就把用户模式的 CPSR 送 FIQ 的SPSR
- 重设 CPSR 各域值,实现把 CPU 切换到对应的模式,比如 FIQ 模式位就是 10001,然后就可以用该模式下的独立寄存器了
- 把返回地址存入对应模式的 LR
- 设置 PC 为中断向量地址,实现强制跳转
返回
-
从 SPSR 恢复 CPSR
-
LR 值减去偏移量后送 PC(减偏移量是因为异常来的时候,硬件可能来不及调整地址。下面是一些异常和对应返回的位置)
指令预取中止:LR-4
数据中止:LR-8
IRQ:LR-4
FIQ:LR-4
未定义、SWI直接返回 LR 即可。(复位就不用返回了)
-
视实际情况恢复各寄存器值
跳转(设置向量表)
一般可以用 B 来跳,如果想要远跳则需要写 PC
AREA boot, CODE, READONLY
ENTRY
LDR PC, Reset_Add @ 中断向量表
LDR PC, Undefined_Add
@...
LDR PC, FIQ_Add
Reset_Add DCD Start_Boot @ 中断处理程序入口,放在内存中
@ DCD:分配一段连续的空间并初始化
@ 意思是:标号 Reset_Add 的值为 Start_Boot 起的一段
Undefined_Add DCD Undefined_Handler
@...
FIQ_Add DCD FIQ_Handler
Start_Boot @...进行实际处理的代码,即中断处理程序实现
Undefined_Handler @...进行实际处理的代码
FIQ_Handler @...进行实际处理的代码
实际中断处理程序例子:IRQ_Handler
这个可以当模板用,其他的中断处理也就是改改调用的函数(吧?)
EXPORT IRQ_Handler @ EXPORT,声明全局标号,全程序可见,可在其他文件中引用
AREA IRQ_Handler, CODE, READONLY
SUB LR, LR, #0x4 @ 设置返回地址为 LR-4
STMFD SP!, {R0-R12, LR} @ 寄存器压栈,保护现场。进中断后这些寄存器随便用
MRS R4, SPSR @ SPSR 存 R4
STMFD SP!, {R4} @ 压栈,R4 存入 SP 指向的内存位置(即栈顶)
BL IRQ_Function @ 跳转到真正的中断处理子函数
LDMFD SP!, {R4} @ 恢复
MSR SPSR_cxsf, R4 @ cxsf是指四个8位的域,此处是整个32位寄存器
LDMFD SP!, {R0-R12, PC}^ @ 恢复现场并返回
END