读书笔记|ARM汇编语言
最近在看《嵌入式 C 语言自我修养-从芯片、编译器到操作系统》,这本书的作者写东西很有意思,用诙谐的语言讲清楚了很多嵌入式领域的基础知识。
今天主要看了看第三章的知识,讲了 ARM 汇编语言的相关知识,这里做一点简单的记录。
ARM 汇编部分知识的难度属实有点超过了我的能力范围(可能是因为我对汇编语言的了解还停留在深圳 IO 那个游戏里)。但我还是尽量把我能读懂的部分在这里加以记载,便于后面复习。
ARM 体系结构
ARM 基于精简指令集(RISC),采用Load/Store
架构(CPU 需要从内存上 Load 数据到寄存器才能操作,处理结果也需要 Store 到内存中)。
ARM 处理器有多种工作模式,如下:
处理器模式 | 模式介绍 |
---|---|
User mode | 应用程序正常工作的模式 |
FIQ mode | 快速中断模式,中断优先级高于 IRQ 模式 |
IRQ mode | 中断模式 |
Supervisor mode | 管理模式,保护模式,软中断或复位会进入该模式 |
Abort mode | 数据存取异常、指令读取失败时会进入该模式 |
Undefined mode | CPU 遇到无法识别、未定义的指令时会进入该模式 |
System mode | 与用户模式相似,但可以运行特权 OS 任务 |
Monitor mode | 仅限于安全扩展 |
ARM 处理器中有一系列寄存器,包括各种通用寄存器、状态寄存器、控制寄存器等。这些寄存器可以分为通用寄存器和专用寄存器两种。
寄存器 R0~R12 属于通用寄存器(除 FIQ 模式),各寄存器功能如下表所示:
寄存器名 | 功能 |
---|---|
R0~R3 | 传递函数参数 |
R4~R11 | 保存程序运算的中间结果或函数的局部变量 |
R12 | 函数调用过程中的临时寄存器 |
其它寄存器因工作模式的不同而不同,不会为各个模式所共享,各寄存器功能如下表所示:
寄存器名 | 功能 |
---|---|
R13 | 堆栈指针寄存器(SP)用以维护、管理函数调用过程中栈帧的变化 |
R14 | 链接寄存器(LR)用来保存上一级函数调用者的返回地址 |
R15 | 程序计数器(PC)CPU 从内存取指令时,默认从 PC 保存的地址中取得 |
CPSR | 当前处理器状态寄存器,表征当前处理器的运行状态 |
SPSR | 程序状态保存寄存器,保存当前工作模式下处理器的现场 |
对于CPSR
寄存器,其有以下状态位、标志位和部分控制位,如下所示:
各状态位、标志位和部分控制位的详细说明如下:
标识 | 内容 | 可选项 |
---|---|---|
N | 表示运算结果符号 | 1 |
为负数,0
为非负数 |
| Z | 表示运算结果是否为零 | 1
为零,0
为非零 |
| C | 表示是否进位 | Carry/Borrow/Extend |
| V | 表示符号是否溢出 | 1
代表符号溢出 |
| I | 是否禁止 IRQ 中断 | 1
代表禁止 |
| F | 是否禁止 FIQ 中断 | 1
代表禁止 |
| T | 控制指令格式 | 0
为 ARM 指令,1
为 Thumb 指令 |
| M4-M0 | 处理器模式 | 通过 5 个位来调控工作模式 |
对于SPSR
,即将CPSR
寄存器的值保存到当前工作模式下的SPSR
寄存器。
ARM 汇编指令
一个完整的 ARM 指令通常由操作码+操作数组成,如下:
<opcode> {<cond> {s} <Rd>,<Rn> {,<operand2>}}
- 其中,使用
<>
括起来的是必填项,使用{}
括起来的是可选项 opcode
指汇编指令,如MOV
、ADD
等cond
为执行条件,执行条件具体指什么,我还存有疑问S
决定了是否影响CPSR
寄存器的标志位(N\Z\C\V
)Rd
即目标寄存器,如ADD
的执行结果Rn
为第一个操作数寄存器,如ADD
的第一个加数operand2
为第二个可选操作数寄存器,如ADD
的第二个加数
存储访问指令
ARM 指令集基于 RISC 指令集,采用典型的 L/S(加载/存储)体系结构,所以 ARM 处理器对程序指令、数据、I/O 空间中外设寄存器的访问都要通过 Load/Store 指令完成,指令如下:
;注:[R0]表示R0中值对应内存的地址,所以是把R0中的数当作一个地址
LDR R1,[R0] ;将R0中的值作为地址,将该地址上的数据存到R1
STR R1,[R0] ;将R0中的值作为地址,将R1中的值存储到该地址
LDRB/STRB ;每次读写1字节,LDR/STR默认每次读取4字节
LDM/STM ;批量加载/存储指令,在一组寄存器和一片内存间传输数据
SWP R1,R1,[R0] ;将R1与R0中地址指向的内存单元中的数据进行交换
SWP R1,R2,[R0] ;将[R0]存储到R1,将R2写入[R0]这个内存存储单元
这里需要注意的是,MOV
指令仅可做寄存器到寄存器的移动,LDR
指令则可以做内存到寄存器的移动。
此外,对于批量加载/存储指令,我们也可以对寄存器进出内存栈进行操作,指令如下:
LDMFD SP!,{R0-R2,R14} ;将内存栈中的数据依次弹出到R14,R2,R1,R0
STMFD SP!,{R0-R2,R14} ;将R0,R1,R2,R14依次压入内存栈
PUSH {R0-R2,R14} ;将R0,R1,R2,R14依次压入栈
POP {R0-R2,R14} ;将栈中数据依次弹出到R0,R1,R2,R14
其中,LDM
后跟的FD
表示堆栈格式的不同,如下所示:
堆栈格式 | 备注 |
---|---|
FA | 满递增堆栈 |
FD | 满递减堆栈 |
EA | 空递增堆栈 |
ED | 空递减堆栈 |
满栈:指堆栈指针 SP 总是指向栈顶元素;空栈:指堆栈指针 SP 指向栈顶元素的下一个空闲的存储单元。
递增:栈指针 SP 从低位址到高位址移动;递减:栈指针 SP 从高位址到低位址移动。
一般来说,ARM 处理器使用的是满递减堆栈。此外注意栈的特点是 FILO(先入后出,理解为 SP 指针不动,动的是寄存器的位置)。
数据传送指令
MOV
主要负责在寄存器之间传送数据,指令如下:
MOV {cond} {S} Rd, operand2
MVN {cond} {S} Rd, operand2
其中,MVN
用来将操作数operand2
按位取反后传送到目标寄存器 Rd,operand2
可以是寄存器也可以是立即数。
举个例子,如下所示:
MOV R1, #1 ;将立即数1传送到寄存器R1
MOV R1, R0 ;将R0寄存器中的值传送到R1寄存器中
MOV PC, LR ;子程序返回
MVN R0, #0xFF ;将立即数0xFF取反后赋值给R0
MVN R0, R1 ;将R1寄存器的值取反后赋值给R0
算术逻辑运算指令
算术逻辑运算指令有如下几种表示:
ADD {cond} {S} Rd, Rn, operand2 ;加法
ADC {cond} {S} Rd, Rn, operand2 ;带进位加法
SUB {cond} {S} Rd, Rn, operand2 ;减法
SBC {cond} {S} Rd, Rn, operand2 ;带进位减法
AND {cond} {S} Rd, Rn, operand2 ;逻辑与
ORR {cond} {S} Rd, Rn, operand2 ;逻辑或
EOR {cond} {S} Rd, Rn, operand2 ;异或
BIC {cond} {S} Rd, Rn, operand2 ;位清除
示例如下:(主要关注ADC
和SBC
)
ADC R1, R1, #1 ;R1 = R1 + 1 + C (其中C为CPSR寄存器中进位)
SBC R1, R1, R2 ;R1 = R1 - R2 - C (其中C为CPSR寄存器中进位)
AND R0, R0, #3 ;保留R0的bit0和1,其它位清除
ORR R0, R0, #3 ;置位R0的bit0和bit1
EOR R0, R0, #3 ;反转R0的bit0和bit1
BIC R0, R0, #3 ;清除R0的bit0和bit1
对于operand2
,可以通过它来实现移位,如下所示:
ADD R3, R2, R1, LSL #3 ;R3 = R2 + R1 << 3
ADD R3, R2, R1, LSL R0 ;R3 = R2 + R1 << R0
ADD IP, IP, #16, 20 ;IP = IP + 立即数16循环右移20位
比较指令
比较指令用于比较两个数的大小/是否相等,会影响 CPSR 寄存器的 N、Z、C、V 标志位,指令格式如下:
CMP {cond} Rn, operand2 ;比较两个数大小
CMN {cond} Rn, operand2 ;取负比较
常见的指令如以下示例:
CMP R1, #10 ;R1-10, 比较结果会影响N/Z/C/V位
CMP R1, R2 ;R1-R2, 比较结果会影响N/Z/C/V位
CMN R0, #1 ;R0-(-1), 比较结果会影响N/Z/C/V位
比较指令结果Z=1
时,运算结果为 0,两数相等;N=1
表示运算结果为负,N=0
表示运算结果非负。
条件执行指令
跳转指令+条件码可以构成组合指令,从而实现在某种条件下的跳转,如BEQ
、BNE
指令。
条件码 | CPSR 标志位 | 说明 |
---|---|---|
EQ | Z=1 | 相等 |
NE | Z=0 | 不等 |
CS/HS | C=1 | 无符号数大于或等于 |
CC/LO | C=0 | 无符号数小于 |
AL | 忽略 | 无条件执行 |
NV | 忽略 | 从不执行 |
以一个例子说明条件执行指令的使用:
……
START
LDR R0 ,= SRC ;源地址
LDR R1 ,= DST ;目的地址
MOV R2 ,#10 ;循环次数
LOOP
LDR R3, [R0], #4 ;从源地址取数据
STR R3, [R1], #4 ;复制到目的地址
SUBS R2, R2, #1 ;循环次数减一
BNE LOOP ;只要R2不等于0,继续循环
……
END
跳转指令
跳转指令包括:B\BL\BX\BLX 等,其通用指令如下:
;X指的是跳转指令
X {cond} label ;跳转到标号label处执行
X {cond} Rm ;寄存器Rm中保存的是跳转地址
跳转指令的说明如下:
指令名称 | 说明 |
---|---|
B label | 跳转到 label 处 |
BL label | 跳转之前,先将返回地址保存在 LR 寄存器 |
BX Rm | 带状态切换的跳转 |
ARM 寻址方式
寻址,即寻找地址,代表了数据传输中数据在源地址和目的地址之间的转移。数据传输往往发生在:内存-寄存器、内存-内存、寄存器-寄存器之间。
常见的寻址方式如下表所示:
寻址方式 | 寻址示例代码 | 说明 |
---|---|---|
寄存器寻址 | MOV R1, R2 |
将寄存器 R2 的值传送到 R1 |
立即数寻址 | ADD R1, R1, #1 |
将寄存器 R1 的值加 1,并将结果存入 R1 |
寄存器偏移寻址 | MOV R2, R1, LSL, #3 |
R2 = R1<<3 |
寄存器间接寻址 | LDR R1, [R2] |
C 语言指针操作在汇编层次的实现方式 |
基址寻址 | LDR R1,[FP, #2] |
FP+2 的值作为新地址,将该内存地址的数据保存到 R1 |
多寄存器寻址 | LDMIA SP!, {R0-R2, R14} |
将内存栈中的数据依次弹出到 R14\R2\R1\R0 |
相对寻址 | B LOOP |
以 PC 指针为基址做基址寻址 |
ARM 伪指令
ARM 伪指令指的是在 ARM 指令集外定义的为了编程方便,各家编译器厂商自定义的一些辅助指令,常见的 ARM 伪指令如下表所示:
伪指令 | 伪指令示例 | 说明 |
---|---|---|
ADR |
ADR R0, LOOP |
将标号为 LOOP 的地址保存到 R0 寄存器中 |
ADRL |
ADRL RO, LOOP |
中等范围的地址读取 |
LDR |
LDR R0, =0x30008000 |
将内存地址 0x30008000 赋值给 R0 |
NOP |
NOP |
(MOV R0, R0
) | 空操作:用于延时或插入流水线中暂停指令的运行 |
其中,LDR
伪指令是为了解决MOV
指令不能传递 32 位立即数而存在的。其与LDR
加载指令的区别在于:会在操作数前加一个=
号。
ARM 汇编程序设计
ARM 汇编程序以段为单位进行组织,一个汇编文件中,可以有不同的 section,分为代码段、数据段等。使用 AREA 伪操作标识符来标识一个段的起始、段名和段的读写属性。如下所示:
AREA COPY,CODE,READONLY ;当前段段名为COPY,为代码段,属性只读
ENTRY ;汇编程序运行入口
START
LDR R0 ,=SRC
LDR R1 ,=DST
MOV R2 ,#10
LOOP
LDR R3, [R0], #4 ;从源地址取数据
STR R3, [R1], #4 ;复制到目的地址
SUBS R2, R2, #1 ;循环次数减一
BNE LOOP ;只要R2不等于0,继续循环
AREA COPYDATA,DATA,READWRITE ;当前段名为COPYDATA,为数据段,属性可读可写
SRC DCD 1,2,3,4,5,6,7,8,9,0
DST DCD 0,0,0,0,0,0,0,0,0,0
END ;汇编程序运行出口
其中,LOOP
称为标号,指用符号来表示一个地址,其与BNE LOOP
构成了一个循环结构。
伪操作
在 C 语言中,为了编程方便,编译器会定义一系列预处理命令并用#
标识(如#include
、#define
等),在编译器预处理环节会将这些预处理命令转化为标准 C 代码,然后再进行编译。
同样的,在 ARM 汇编语言中,也有一些伪操作代码,如下表所示:
伪操作 | 伪操作示例 | 说明 |
---|---|---|
GBLA |
GBLA a |
定义一个全局算术变量 a,并初始化为 0 |
SETA |
a SETA 10 |
给算术变量 a 赋值为 10 |
GBLL |
GBLL b |
定义一个全局逻辑变量 b,并初始化为{false} |
SETL |
b SETL 20 |
给逻辑变量 b 赋值为 20 |
GBLS |
GBLS STR |
定义一个全局字符串变量 STR,并初始化为 0 |
SETS |
STR SETS "bolun.xyz" |
给字符串变量 STR 赋值为“bolun.xyz” |
LCLA |
LCLA a |
定义一个局部算术变量 a,并初始化为 0 |
LCLL |
LCLL b |
定义一个局部逻辑变量 b,并初始化为{false} |
LCLS |
LCLS name |
定义一个局部字符串变量 name,并初始化为 0 |
此外,关于数据定义也有一些伪操作代码,如下表所示:
伪操作 | 伪操作示例 | 说明 |
---|---|---|
DCB |
DATA1 DCB 10,20,30,40 |
|
STR DCB "bolun.xyz" |
分配一片连续的字节存储单元并初始化 | |
DCD |
DATA2 DCD 10,20,30,40 |
分配一片连续的字存储单元并初始化 |
SPACE |
BUF SPACE 100 |
给 BUF 分配 100 字节的存储单元并初始化为 0 |