最近在看《嵌入式 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指汇编指令,如MOVADD
  • 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        ;位清除

示例如下:(主要关注ADCSBC

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表示运算结果非负。

条件执行指令

跳转指令+条件码可以构成组合指令,从而实现在某种条件下的跳转,如BEQBNE指令。

条件码 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