
一、基础概念
1.1 机器码
机器码就是机器或计算机能够识别的0和1的二进制格式
1.2 汇编指令
一条具备特殊功能的指令
1.3 汇编指令集
1)定义
多条不同功能的汇编指令的集合
2)分类
- x86-64 汇编指令集
- ARM 汇编指令集
- ZEN 汇编指令集
- PowerPC 汇编指令集
- RISC-V 汇编指令集
- …………
1.4 架构
1)定义
架构是汇编指令集的别名,不同的汇编指令集可以设计出不同的架构
2)主流架构
1.5 内核 - CPU 的核心
不同的架构可以设计出不同的内核
以下架构都是ARM公司基于arm汇编指令集设计出的不同的架构
arm-v1~v6架构:目前已经淘汰
arm-v7架构:支持32位汇编指令集(ARM课程所使用的STM32MP157AAA这款芯片就是通过arm-v7架构设计而来的)
arm-v8架构:支持64位汇编指令集,向下兼容32位汇编指令集
arm-v9架构:最新的架构,主要应用在性能要求比较高的设备上
基于arm-v7架构设计出了Cortex-M4内核
基于arm-v8架构设计出了Cortex-A9内核
基于arm-v9架构设计出了Cortex-X5内核
ARM公司设计的内核分类:
Cortex-M系列内核(M3、M4、M33......) ------ 应用在功能相对简单(低功耗产品)
Cortex-A系列内核(A7、A8、A9......) ------ 之前应用在手机、电脑上
Cortex-R系列内核 ------ 应用在对实时性要求比价高的产品上
Cortex-X系列内核 ------ 最新的、性能最高的系列
ARM课程使用的芯片:STM32MP157AAA芯片(通过arm-v7架构设计) = Cortex-A7核 + Cortex-M4核
单片机课程使用的芯片:STM32U575RIT6芯片 = Cortex-M33内核
1.6 处理器
1)分类
- CPU:中央处理器
- MCU:微控制单元(微控制器)
- MPU:微处理单元(微处理器)
- SOC:SOC 的全称是 System on a Chip,中文通常译为 “系统级芯片”。
2)☆MCU 和 MPU 的区别
- MCU:传统意义上的单片机,不支持使用操作系统(目前可以使用小行实时操作系统),性能和功耗低。
- MPU:高性能的处理器,支持使用操作系统(linux、Android……)
- 对于ARM公司
- MCU为Cortex-M系列内核
- MPU为Cortex-A / X系列内核
3)☆问:使用过哪些MCU?
使用过 STM32F103、STM32F407、STM32MP157A、STM32U575……
4)结构图
5)★STM32MP157AAA 型号的命名规则和含义
ST:公司名字
M:MCU(微控制器)
32:支持32位的汇编指令集
MP1xx:芯片型号(MP1系列:MP157、MP158....)
AAA:芯片性能
二、ARM公司发展史
ARM前身为艾康电脑(Acorn),于1978年,英国剑桥成立,大学的孵化物。
- 1980年代晚期,苹果开始与艾康合作,开发新版ARM核心。
- 1985年,艾康开发出全球第一款商用RISC(精简指令集)处理器,即ARM1,针对于PC市场,还没有嵌入式呢!!!
- 1990年,艾康财务危机,受苹果和VLSI(最早做超大规模集成电路的公司)的投资,成立独立子公司:Advanced RISC Machines(ARM), ARM公司正式成立面世。
- 2004年,发布ARMv7架构的Cortex系列处理器,同时推出Cortex-M3。
- 2005年,发布Cortex-A8处理器。
- 2007年,发布Cortex-M1和Cortex-A9
- 2009年,实现Cortex-A9、发布Cortex-M0
- 2010年,推出Cortex-M4(F)、成立Linaro (ARM公司牵头成立的公共组织,专门做ARM处理器在Linux平台上的一些软件的开发和移植), 推出Cortex-A15 MPcore高性能处理器(性能比较高了,但是发热量很大哦)。
- 2011年,推出32位 Cortex-A7 处理器,ARMv8发布
- 2012年,开始推出64位处理器。推出 Cortex-M0+、ARM 首款64位处理器架构 Cortex-A53、Cortex-A57 架构。全球第一款64位ARM手机iPhone5s。
- 2013年,推出32位 Cortex-A12 处理器架构
- 2014年,推出 Cortex-M7(F) 微控制器架构;32位 Cortex-A17处理器架构。
- 2015年,推出64位 Cortex-A35、Cortex-A72 处理器架构。
- 2016年,推出 Cortex-M23 、Cortex-M33(F) 微控制器架构;32位 Cortex-A32 处理器架构;64位 Cortex-A73 处理器架构。
- 2017年,推出64位 Cortex-A55 、Cortex-A75 处理器架构。
- 2018年,推出微控制器 Cortex-M35P;64位 Cortex-A76 处理器架构。
- 2016年,日本软银收购ARM公司
- 2020年,NVIDIA想收购(没成功)
- 现在的ARM公司不再做芯片,只做内核的技术支持和授权
三、复杂指令集与精简指令集
3.1 复杂指令集
1)概念
复杂指令集中的汇编指令的指令宽度和指令周期是不固定的
- 指令宽度:每条汇编指令占用内存空间的大小
- 指令周期:每条汇编指令被执行需要的时间
X86汇编指令集就是复杂指令集
反汇编终端指令:objdump
- 作用:将二进制编码格式的文件转换为汇编指令格式的文件
3.2 精简指令集
1)概念
精简指令集的汇编指令的指令周期和指令宽度是固定的
- ARM汇编指令集、Mips汇编指令集、RISC-V汇编指令集、PowerPC汇编指令集……
ARM汇编指令存在很多子集,其中有一个Thumb汇编指令集
- 默认状态时,自动选择使用Thumb汇编指令
- Thumb汇编指令集大小为 16 位,即指令宽度都是 16 位(2 字节)
- ARM汇编指令集大小为 32 位,即指令宽度都是 32 位(4 字节)
Cortex-M内核大多使用 Thumb 汇编指令集
Cortex-A内核大多使用 ARM 汇编指令集
3.3 修改 Thumb 指令集为 ARM 指令集
使用
arm-linux-gnueabihf-gcc
交叉编译器时, 可以添加-mthumb
或-marm
选项来指定使用的指令集。-marm
选项明确指示编译器生成 ARM 指令集的代码。
修改完成后
四、内核中的寄存器组织
4.1 六大存储类型
1)定义变量的流程
存储类型 + 数据类型 + 变量名;
2)分类
auto、static、const、extern、volatile、register
3)register:寄存器存储类型
- 使用register修饰变量时,变量会直接存储到核内寄存器中
- 无法通过内存地址进行访问,只能通过寄存器编号进行访问
- 访问速度快,但寄存器个数有限
4)volatile
作用:防止编译器对代码进行优化,直接从内存中取最新的值
应用场景:
- 多线程使用同一资源时
- 单片机的开发
volatile 修饰的变量不会利用高速缓存(DCache)来暂存数据,而是直接与内存进行数据交互。为什么要添加 volatile 呢?因为如果不添加,会存在隐患:
变量 a 可能会被数据高速缓存区(DCache)暂存,当线程 1、2、3 访问 a 时,可能读取到缓存区的旧值而非最新值。比如线程 1、2 都修改了 a,但由于缓存的存在,线程 3 在执行 c = a + 1 时,可能获取的是旧值,导致结果混乱。
而添加 volatile 后,会直接跳过缓存,直接操作内存,强制每次读写操作都直接与内存交互,这样就能读取到最新的数据。无论哪个线程修改了 a 的值,其他线程都能立刻看到内存里的最新值,避免因缓存“留存旧数据”而导致计算错误(例如线程 3 能获取到线程 1、2 改过的 a,而不是缓存里的旧值)。
另外,编译 main 函数所在的程序,生成二进制文件后,将二进制文件下载到 STM32MP157AAA 芯片中。在该芯片(基于 Cortex - A7 内核)里,ICache 是指令高速缓存区,DCache 是数据高速缓存区。被 volatile 修饰的变量不再通过高速缓存暂存数据,而是直接和内存进行数据交互,像在 main 函数中,volatile int a = 10;,线程 1 执行 a = 1;、线程 2 执行 a = 2;、线程 3 执行 c = a + 1; 时,都能基于内存中的最新 a 值进行操作。
4.2 ARM 处理器(内核)的工作模式
① Cortex-M 核的工作模式
- 线程模式(执行用户代码,类似main函数)
- 异常模式
② Cortex-A 核的工作模式
非特权模式:
- User模式(用户模式,执行用户代码)
特权模式:
- 非异常模式:Sys模式(系统模式 / 管理员模式)
- 异常模式:
- IRQ模式:普通中断异常模式
- FIQ模式:快速中断异常模式
- SVC模式:超级管理异常模式
- ABT模式:访问中止异常模式
- UDF / UNF模式:未定义异常模式
以上是使用 ARM-v7 架构的工作模式
ARM-v8 架构之后,有两种新的工作模式
- MON模式:安全监管模式
- HYP模式:虚拟化技术模式
4.3 寄存器组织

根据上图可知:
- 每个小方块都是一个寄存器,每个寄存器的大小都是4个字节
- 黑色字体的寄存器是实际存在的寄存器,灰色字体的寄存器是实际不存在的寄存器
- 白色底的寄存器是公用的寄存器,蓝色底的寄存器是私有的寄存器(只有在对应模式下可以使用的私有寄存器)
- 实际可以操作的寄存器一共有43个(大小:43*4个字节)
- 上述寄存器均为核内寄存器(无法通过内存地址进行访问,只能通过寄存器编号进行访问,如R0、R1、R2....) 核外寄存器可以通过内存地址进行访问
五、特殊功能寄存器
5.1 R13 寄存器(SP寄存器)
the stack pointer
|
栈指针寄存器
|
用于保存栈区的一篇内存空间地址
跳转执行函数 func 前将 r1 和 r2 寄存器中的值保存压入到 SP 寄存器所指向的一片内存栈地址空间中,函数执行完毕后,将保存的值恢复到 r1 和 r2 中,从而实现 C 语言中对于局部变量的操作。
5.2 R14 寄存器(LR 寄存器)
the linking register
|
链接寄存器
|
用于保存函数的返回地址
在调用 func 函数的过程中,会将返回地址(即r0 = r1 + r2)保存到 LR 寄存器中
5.3 R15 寄存器(PC寄存器)
the program conter register
|
程序计数寄存器
|
用于保存下一条需要执行的汇编指令的地址
在 ARM 指令集(32 位指令)下,PC 寄存器在执行一条指令后会自动 + 4 以指向下一条指令;若为 Thumb 指令集(16 位指令),则通常自动 + 2。函数返回时,LR 寄存器中预先存储了返回地址,通过
BX LR
或MOV PC, LR
等指令将 LR 的值传递给 PC,使程序跳转回返回地址继续执行。
SP、LR、PC 寄存器总结
- SP寄存器:用于保存栈空间的一片地址,用于实现C语言中的局部变量
- LR寄存器:用于保存函数的返回地址,用于实现C语言中的函数返回
- PC寄存器:用于保存下一条需要执行的汇编指令的地址,用于实现C语言语句的一条一条执行
- 综合使用实现函数之间的跳转
5.4 CPSR寄存器
the current program status register
|
当前程序状态寄存器
|
用于保存当前程序的状态
★CPSR寄存器(4个字节32位)的高四位与低八位
高 4 位
N - 31位 - 负数标志位 - 比较两个数的大小
- 取决于最近一次影响标志位的运算(ADD、SUB、CMP等)结果的最高位:
- 最高位结果为1(即负数,计算机中补码表示),N = 1
- 最高位结果为0(即正数或 0 ),N = 0
Z - 30位 - 零标志位 - 判断两数相等
- 当指令运算结果为 0 时,Z = 1;否则,Z = 0
C - 29位 - 进位 / 借位标志位 - 比较无符号数的大小
- 无符号数加法中,结果超出位数范围产生进位,C = 1
- 无符号数减法中,被减数小于减数产生借位,C = 0
V - 28位 - 溢出标志位 / 符号标志位
- 有符号数运算时,结果超出了有符号数的表示范围,对符号位产生了变化,V = 1;否则 V = 0
低 8 位 - 控制位
I - 7位 - IRQ模式使能位 / IRQ模式屏蔽位 / 中断禁止位
- I = 1 -> 禁止IRQ普通中断模式
- I = 0 -> 允许IRQ普通中断模式
F - 6位 - FIQ模式使能位 / FIQ模式屏蔽位 / 快速中断禁止位
- F = 1 -> 禁止FIQ快速中断模式
- F = 0 -> 允许FIQ快速中断模式
T - 5位 - 状态位 / ARM状态屏蔽位 / Thumb 状态位
- T = 1 -> 处理器屏蔽 ARM 状态,运行 Thumb 指令集(16 位指令)
- T = 0 -> 不屏蔽 ARM 状态,运行 ARM 指令集(32位指令)
M - [4 : 0] 位 - 模式位 / 工作模式位
- 10000 -> User 模式
- 10001 -> FIQ 模式
- 10010 -> IRQ 模式
- 10011 -> Supervisor(SVC)模式
- 10110 -> Monitor(MON)模式
- 10111 -> Abort(ABT)模式
- 11010 -> Hyp(HYP)模式
- 11011 -> Undef(UND)模式
- 11111 -> System(SYS)模式
5.5 SPSR寄存器
the save program status register
|
备份程序状态寄存器
|
用于备份程序的状态
在USER模式(非特权模式)切换到执行IRQ(异常模式)代码时,将CPSR寄存器当前的值存放至SPSR寄存器中,异常模式代码执行结束后,需要切换回原用户代码继续执行,将SPSR寄存器中的值取回还原至CPSR中,从而实现不同模式的切换
六、汇编指令
6.1 汇编指令的分类
1)基础汇编指令
① 数据操作指令
- 数据搬移指令(赋值指令 = )
- 移位运算指令(<<、>>)
- 位运算指令(&、|、~、^)
- 算数运算指令(+、-、*、/)
- 比较指令(<、>、==、!=)
② 跳转指令
2)进阶汇编指令
③ 单寄存器内存读写指令
④ 栈指针寄存器内存读写指令(包含多寄存器内存读写指令)
⑤ CPSR 特殊功能寄存器内存读写指令
⑥swi软中断指令
6.2 汇编指令基本格式
{opcode}{cond}{s} Rd, Rn, oprand_shifter2
{opcode} ---> 指令码,汇编指令的名字
{cond} ---> 条件码,用于指定指令在满足特定条件时才会执行,省略则指令无条件执行
{s} ---> 当指令带有该后缀时,会影响程序状态寄存器(CPSR)中的 N Z C V 标志位,不带该后缀则不会影响
Rd ---> 目标寄存器,用于存放指令执行后的结果
Rn ---> 第一操作寄存器,源寄存器之一,相当于左操作数,通常是操作数所在的寄存器
oprand_shifter2 ---> 可选第二操作数,可以是 立即数、普通寄存器或经过移位的寄存器(如ADD R0, R1, R2, LSL #2)
注意:
1、{opcode}{cond}{s} 指令码、条件码、状态位连在一起写的,不允许出现空格
2、Rd, Rn, oprand_shifter2 目标寄存器、第一操作寄存器、第二操作数连在一起写,但是需要使用 , 隔开
3、{opcode}{cond}{s} 和 Rd, Rn, oprand_shifter2 这两个部分中间需要由空格隔开
4、汇编指令没有以 分号 ; 结尾,所以一条汇编指令写一行
5、汇编指令没有大小写区别
mov r0, #0xff ==== MOV R0, #0XFF ==== MoV r0, #0xFF
这三句话是一样的
6、在当前汇编文件中,使用的编译器支持的注释方式:
6.1 单行注释 @ (不同的编译器支持的注释方式不同,可能有; , #......)
6.2 多行注释
/**/
.if 0/1 .else .endif
6.3 数据搬移指令(赋值指令)
1)指令码
mov 直接赋值指令
mvn 按位取反后赋值
2)指令格式
mov / mvn Rd, oprand_shifter2
无第一操作寄存器,只有目标寄存器和第二操作数
3)测试代码
.text
.global _start
_start:
mov r0, #0xff @ 解释:将0xff赋值到R0寄存器中
@ 如果使用赋值指令时,第二操作数是一个立即数
@ 需要在他的前面加上#
mvn r1, #0xff @ 解释:将0xff全部按位取反后,赋值给r1寄存器
@ r1 = ~(0xff) = 0xffffff00
mov r2, #(-0xff) @ -0xff(存储的补码,正数源码和补码是一致的,负数需要计算)
@ 源码:0x800000ff
@ 反码:0xffffff00
@ 补码:0xffffff01
stop:
b stop
.end
4)立即数
一条汇编代码占 4 个字节空间
mov r0 {cond} {s} Rn 0~7 位是mov可直接操作的数值范围
0~255
0x00~0xff
如何判断一个数是否是立即数
- 需要判断的数为 A
- 可以在 0x00 ~ 0xFF 之间找到一个数 B
- 数 B 可以通过 循环右移偶数位 是的其 与数 A 相等
则数 A 为立即数
例:
判断0XFF是否是立即数
1、数A = 0XFF = 0B 0000 0000 0000 0000 0000 0000 1111 1111
2、数B = 0XFF = 0B 0000 0000 0000 0000 0000 0000 1111 1111
3、循环右移偶数位:32位
判断0XFFF是否是立即数
1、数A = 0XFFF = 0B 0000 0000 0000 0000 0000 11111 1111 1111
2、数B = 0XFF = 0B 0000 0000 0000 0000 0000 0000 1111 1111 (找不到数B)
3、0XFFF不是一个立即数
判断0X1F000000是否是立即数
1、数A = 0X1F000000 = 0B 0001 1111 0000 0000 0000 0000 0000 0000
2、数B = 0X1F = 0B 0000 0000 0000 0000 0000 0000 0001 1111 (循环右移8位)
3、数B = 0XF8 = 0B 0000 0000 0000 0000 0000 0000 1111 1000 (循环右移11位)
4、数B = 0X7C = 0B 0000 0000 0000 0000 0000 0000 0111 1100 (循环右移10位)
可能存在多个数B,但是只要有一个数B符合要求,当前的数A就是一个立即数
5)有效数
一个数全部按位取反后,可以得到一个立即数,则这个数就是一个 有效数
6)ldr 伪指令
将一个较大范围的 32 位立即数加载到寄存器中
可以实现 0x00000000 ~ 0xFFFFFFFF 之间任意数值的赋值操作
6.4 移位操作运算指令
1)指令码
lsl(logical Shift Left) ---> 逻辑左移 / 无符号数左移 ---> 高位移出,低位补 0
lsr(Logical Shift Right) ---> 逻辑右移 / 无符号数右移 ---> 低位移出,高位补 0
ror(Rotate Right) ---> 循环右移 ---> 低位移出,补到高位
asr(Arithmetic Shift Right) ---> 算数右移 / 有符号数右移 ---> 低位移出,高位补符号位
2)指令格式
lsl/lsr/ror/asr Rd, Rn, oprand_shifter2
3)测试代码
/***********************2、移位运算指令**************************/
mov r0, #0xff
lsl r1, r0, #8 @ 将r0寄存器中的值逻辑左移8位后赋值给r1寄存器
@ r1 = r0 << 8 = 0xff00
lsr r2, r1, #12 @ 将r1寄存器中的值逻辑右移12位后赋值给r2寄存器
@ r2 = r1 >> 12 = 0xf
ror r3, r2, #4 @ 将r2寄存器中的值循环右移4位后赋值给r3寄存器
@ r3 = 0xf0000000
asr r4, r3, #4 @ 将r3寄存器中的值算术右移4位后赋值给r4寄存器
@ r4 = 0xff
@ 高位补符号位指的是所有高位都需要补符号位,而不是只有最高位补符号位
/***********************2、第二操作数的所有情况**************************/
@ 第二操作数可以是一个立即数
mov r0, #0xff @ 解释:将0xff赋值给r0寄存器中
@ 第二操作数可以是一个普通寄存器
mov r1, r0 @ 解释:将r0寄存器中的值赋值给r1寄存器中
@ 第二操作数可以是一个经过移位的寄存器
mov r2, r1, lsl #4 @ 解释:将r1寄存器中的值逻辑左移4位后赋值给r2寄存器
@ 上述汇编指令的作用和这条lsl r2, r1, #4汇编指令的作用是一致的
6.5 位运算操作指令
1)指令码
and ---> 按位与(&)
orr ---> 按位或(|)
eor ---> 按位异或(^)
mvn ---> 按位取反(~)
口诀:
与 0 清 0,与 1 不变
或 1 置 1,或 0 不变
异或 1 取反,异或 0 不变
2)指令格式
and/orr/eor/mvn Rd, Rn, oprand_shifter2
3)测试代码
/***********************4、位运算操作指令**************************/
@ 32位数:
@ 31 0
@ **** **** **** **** **** **** **** ****
mov r0, #0xff
@ 目的:将r0寄存器中的第[3]位清0,其他位不变,最后赋值给r0寄存器
@ c语言写法:r0 = r0 & (~(0x1 << 3))
and r0, r0, #(~(0x1 << 3)) @ r0 = 0xf7
@ 目的:将r0寄存器中的第[3]位置1,其他位不变,最后赋值给r0寄存器
orr r0, r0, #(0x1 << 3)
练习
@假设你不知道r0寄存器中的值
ldr r0, =0x12345678
@ 31 0
@ **** **** **** **** **** **** **** ****
@ 1> 将r0寄存器的第[3]位清0,保持其他位不变
and r0, r0, #(~(0x1 << 3))
@ 2> 将r0寄存器的第[29]位置1,保持其他位不变
orr r0, r0, #(0x1 << 29)
@ 3> 将r0寄存器的第[7:4]位清0,保持其他位不变
and r0, r0, #(~(0xf << 4))
@ and r0, r0, #(~(0b1111 << 4))
@ 4> 将r0寄存器的第[15:8]位置1,保持其他位不变
orr r0, r0, #(0xff << 8)
@ orr r0, r0, #(0x0000ff00)
@ 5> 将r0寄存器的第[3:0]位按位取反,保持其他位不变
eor r0, r0, #(0xf << 0)
@ 6> 将r0寄存器的第[11:4]位修改为10101011,保持其他位不变
@ 推荐使用先清0再置1
and r0, r0, #(~(0xff << 4))
orr r0, r0, #(0b10101011 << 4) @ orr r0, r0, #(0xab << 4)
@ 也可以先置1再清0
orr r0, r0, #(0xab << 4)
and r0, r0, #(~(0x54 << 4))
我的答案
.text
.global _start
_start:
ldr r0,=0x12345678
@1>将r0寄存器的第[3]位清0,保持其他位不变
and r0, r0, #(~(0x1<<3))
@2>将r0寄存器的第[29]位置1,保持其他位不变
orr r0, r0, #(0x1<<29)
@3>将r0寄存器的第[7:4]位清0,保持其他位不变
and r0, r0, #(~(0xf<<4))
@4>将r0寄存器的第[15:8]位置1,保持其他位不变
orr r0, r0, #(0xf<<8)
@5>将r0寄存器的第[3:0]位按位取反,保持其他位不变
eor r0, r0, #(0xf)
@6>将r0寄存器的第[11:4]位修改为10101011,保持其他位不变
and r0, r0, #(~(0xff<<4))
orr r0, r0, #(0xAB<<4)
stop:
b stop
.end
6.6 算数运算指令
add ---> 基础加法指令,不影响 CPSR 寄存器的 C 位;
adc ---> 带进位加法指令,执行指令时要加上 CPSR 中的 C 位,进位时 C = 1;
sub ---> 基础减法指令,不影响 CPSR 寄存器的 C 位;
sbc ---> 带借位减法指令,执行指令时要多减 (1-C) ,借位时 C = 0;
mul ---> 乘法指令;
div ---> 除法指令,需要在 ARM-v8 及以上架构使用;
1)指令格式
add/adc/sub/abc/mul/div Rd, Rn, oprand_shifter2
2)测试代码
/***********************5、算数运算操作指令**************************/
mov r1, #0x1
mov r2, #0x3
@ add r3, r1, r2 @ r3 = 0x1 + 0x3 = 0x4
@ sub r4, r2, r1 @ r4 = 0x3 - 0x1 = 0x2
@ 模拟两个64位数相减
@ r1寄存器存放第一个64位数的高32位,r2寄存器存放第一个64位数的低32位
@ r3寄存器存放第二个64位数的高32位,r4寄存器存放第二个64位数的低32位
@ r5寄存器中存放相减后的高32位数,r6寄存器存放相减后的低32位数
mov r1, #0x5
mov r2, #0x2
mov r3, #0x1
mov r4, #0x8
@ 先低位相减
subs r6, r2, r4 @ 此时需要使用到s状态位
@ 由于当前指令的执行结果产生了借位
@ 并且高32位运算时,需要使用到产生借位后的C位
@ r6 = r2 - r4 = 0x2 - 0x8 = 0x2 - 0x2 - 0x6
@ = 0x0 - 0x6 = 0x0 - 0x1 - 0x5
@ = 0xffffffff - 0x5 = 0xfffffffa
sbc r5, r1, r3 @ 此时需要使用到sbc
@ 由于此处高32位相减时,需要多减去一个借位
@ r5 = r1 - r3 - c位 = 0x5 - 0x1 - 0x1 = 0x3
@ 注意:不管c位存放的是0/1
@ 只要c位产生了借位,就需要多减一个0x1
@ 只要c位产生了进位,就需要多加一个0x1
@ 模拟两个64位数相加
@ r1寄存器存放第一个64位数的高32位,r2寄存器存放第一个64位数的低32位
@ r3寄存器存放第二个64位数的高32位,r4寄存器存放第二个64位数的低32位
@ r5寄存器中存放相加后的高32位数,r6寄存器存放相加后的低32位数
mov r1, #0x1
mov r2, #0xfffffffe
mov r3, #0x2
mov r4, #0x4
adds r6, r2, r4 @ r6 = 0xfffffffe + 0x4
@ = 0xfffffffe + 0x1 + 0x3
@ = 0xffffffff + 0x3
@ = 0xffffffff + 0x1 + 0x2
@ = 0x0 + 0x2 = 0x2
adc r5, r1, r3 @ r5 = r1 + r3 + c位 = 0x1 + 0x2 + 0x1 = 0x4
6.7 比较指令
1)指令码
cmp -> 比较指令码
2)指令格式
cmp Rn, oprand_shifter2
3)条件码
注意:
- 比较指令就相当于c语言语句中的if语句
- 比较指令不需要使用目标寄存器,比较完的值不需要存储
- 比较指令的本质是做减法,也就是Rn - oprand_shifer2
- 比较指令的执行结果会直接影响CPSR寄存器的NZCV位,使用比较指令时不需要+s
- 比较指令通常会搭配条件码({cond})进行使用(并不是在cmp这条汇编指令后+cond)
4)测试代码
if(r1 >= r2)
r3 = r1 + r2;
else
r3 = r1 - r2;
将上述简单c语言语句转化为汇编指令
/***********************6、比较指令**************************/
mov r1, #0x1
mov r2, #0x2
cmp r1, r2
addcs r3, r1, r2 @ 解释:当r1 >= r2时,r3 = r1 + r2
subcc r3, r1, r2 @ 解释:当r1 < r2时,r3 = r1 - r2
6.8 跳转指令
1)指令码
b -> 有去无回的跳转
bl -> 有去有回的跳转
指令中的 l 为 -> LR寄存器
2)指令格式
b/bl 标签(类似于函数名)
3)测试代码
main(void)
{
int r1=0x1, r2=0x2;
func();
r3=r1+r2;
}
func(void)
{
int r1=0x5, r2=0x5;
r3=r1+r2;
}
/***********************7、跳转指令**************************/
mov r1, #0x1
mov r2, #0x2
bl func @ bl这条汇编指令只是将函数的返回地址保存到LR寄存器中
@ 如果想要实现函数的返回,还需要手动将函数返回地址中的汇编指令作为下一条汇编指令去执行
add r3, r1, r2
func:
mov r1, #0x5
mov r2, #0x5
sub r3, r1, r2
@ 手动添加一条汇编指令
@ 将函数返回地址中的汇编指令作为下一条汇编指令去执行
@ LR PC
mov pc, lr
练习
使用汇编代码实现如图所示的逻辑关系
mov r0, #0x1
mov r1, #0x2
func1:
@ 比较寄存器r0和r1的值(计算r0 - r1,仅影响标志位)
cmp r0, r1
@ 如果r0等于r1(BEQ = Branch if Equal),跳转到stop标签
beq stop
@ 如果r0不等于r1(BNE = Branch if Not Equal),跳转到func2标签
bne func2
func2:
cmp r0, r1
@ 如果r0大于r1(SUBHI = Subtract if Higher,无符号比较)
@ 执行r0 = r0 - r1
subhi r0, r0, r1
@ 如果r0小于或等于r1(SUBLS = Subtract if Lower or Same,无符号比较)
@ 执行r1 = r1 - r0
subls r1, r1, r0
@ 无条件跳转到func1标签,继续循环比较
b func1
@ 程序结束标签(循环终止点)
stop:
6.9 单寄存器内存读写指令
在C语言中有如下代码,指针p
存储变量a
的地址,通过*p
可以读写a
所在的内存数据。
int a=10;
int *p = &a;
*p = 20;
1)指令码
ldr ---> 从内存空间中读取 4 个字节的数据 32位系统 ------- 1字 = 4字节
ldrh ---> 从内存空间中读取 2 个字节的数据 ---> h ->half 半个字
ldrb ---> 从内存空间中读取 1 个字节的数据 ---> b -> binary 一个字节
str ---> 向内存空间中写入 4 个字节的数据
strh ---> 向内存空间中写入 2 个字节的数据
strb ---> 向内存空间中写入 1 个字节的数据
- ld 是 load ,加载的意思,从内存地址空间去读取数据
- st 是 store ,存储的意思,向内存地址空间中写入数据
- r 是 register ,1 个 register 4 个字节
2)指令格式
ldr/ldrh/ldrb Rd, [Rn]
将 [Rn] 寄存器中的值看作是一片内存空间的地址
从 [Rn] 这个内存地址空间中读取 4 / 2 / 1 个字节数据到目标寄存器 Rd 中
str/strh/strb Rd, [Rn]
将 [Rn] 寄存器中的值看作是一片内存空间的地址
将 Rd 寄存器中的 4 / 2 /1 个字节的数据写入到 [Rn] 这片内存地址空间中
注意:
在汇编指令中,寄存器用 [ ] 修饰时,该寄存器中的值会被当作一片内存空间的地址
3)测试代码
/***********************8、单寄存器内存读写指令**************************/
ldr r0, =0x40000820
ldr r1, =0x12345678
str r1, [r0] @ 将r0寄存器中的值看作是一片内存空间地址
@ 将r1寄存器中4个字节的数据写入到[r0]这片内存空间地址中
ldr r2, [r0] @ 将r0寄存器中的值看作是一片内存空间地址
@ 从[r0]这片内存空间地址中读取4个字节的数据到r2寄存器中
4)地址偏移 - 三种偏移内存地址空间的写法
1. ldr/ldrh/ldrb Rd, [Rn], #offset
- 数据读取完后,Rn 寄存器中的地址会更新 -> Rn = Rn + #offset
- 将Rn寄存器中的值看作是一片内存空间地址 从 [Rn] 这片内存空间地址中读取4 /2 /1 个字节的数据到 Rd 寄存器中
2. ldr/ldrh/ldrb Rd, [Rn, #offset]
- 数据读取完后,Rn 寄存器中的地址不会发生变化
将 Rn + #offset 的值看作是一片内存空间地址 从 Rn + #offset 这片内存空间地址中读取4 /2 /1 个字节的数据到 Rd 寄存器中
3. ldr/ldrh/ldrb Rd, [Rn, #offset]!
- 由于存在 ! ,在数据读取完成后,Rn 寄存器中的地址会更新 -> Rn = Rn + #offset
- 将 Rn + #offset 的值看作是一片内存空间地址 从 Rn + #offset 这片内存空间地址中读取4 /2 /1 个字节的数据到 Rd 寄存器中
注意:
- #offset 是一个值,为地址偏移量(#1、#4、#8)
- str / strh / strb 地址偏移效果一样,从读取变为写入
- 使用 str / ldr 地址偏移时,#offset 地址偏移量必须是 4 的倍数
- 使用 strh / ldrh 地址偏移时,#offset 地址偏移量必须是 2 的倍数
- 使用 strb / ldrb 地址偏移时,#offset 地址偏移量必须是 1 的倍数
5)地址偏移量测试代码
/**********************9、单寄存器内存读写指令 - 地址偏移**************************/
ldr r0, =0x40000820
ldr r1, =0x12345678
str r1, [r0] @ 执行完上述操作,0x40000820地址中存在0x12345678这个值
ldr r2, [r0], #4
/* 解释:
将r0寄存器中的值(0x40000820)看作是一片内存空间地址
从0x40000820这片内存空间地址读取4个字节的数据(0x12345678)到r2寄存器中
r2 = 0x12345678
读取完数据后,r0寄存器中的地址发生变化 r0 = r0 + 4 = 0x40000820 + 4 = 0x40000824
*/
ldr r0, =0x4000081C
ldr r3, [r0, #4]
/* 解释:
将r0+#4看作是一片内存空间地址(0x4000081C+4=0x40000820)
从0x40000820这片内存空间地址中读取4个字节的数据到r3寄存器中
r3 = 0x12345678
r0 = 0x4000081C
*/
ldr r4, [r0, #4]!
/* 解释:
将r0+#4看作是一片内存空间地址(0x4000081C+4=0x40000820)
从0x40000820这片内存空间地址中读取4个字节的数据到r3寄存器中
r4 = 0x12345678
r0 = r0 + #4 = 0x4000081C + 4 = 0x40000820
*/
6)练习
0x40000820 这片内存空间中存储了 0x12345678 这个数据,通过ldrb和地址偏移的三种方法,将0x40000820这片内存空间地址中的值,读取到 r1(0x12), r2(0x34), r3(0x56), r4(0x78) 这四个寄存器中,将 r1, r2, r3, r4 这四个寄存器中的值拼接到 r5 寄存器中(r5 = 0x12345678)
.text
.global _start
_start:
/*
0x40000820这片内存空间中存储了0x12345678这个数据
通过ldrb和地址偏移的三种方法,将0x40000820这片内存空间地址中的值
读取到r1(0x12), r2(0x34), r3(0x56), r4(0x78)这四个寄存器中
将r1, r2, r3, r4 这四个寄存器中的值拼接到r5寄存器中(r5 = 0x12345678)
*/
/*
ldr r0, =0x40000820
ldr r6, =0x12345678
str r6, [r0]
@ 1、从内存空间读取数据
@ 1.1 ldrb Rd, [Rn], #offset
ldrb r4, [r0], #1 @ r0 = 0x40000820
ldrb r3, [r0], #1 @ r0 = 0x40000821
ldrb r2, [r0], #1 @ r0 = 0x40000822
ldrb r1, [r0], #1 @ r0 = 0x40000823
@ 1.2 ldrb Rd, [Rn, #offset]
ldrb r4, [r0, #0]
ldrb r3, [r0, #1]
ldrb r2, [r0, #2]
ldrb r1, [r0, #3]
@ 1.3 ldrb Rd, [Rn, #offset]!
ldrb r4, [r0, #0]!
ldrb r3, [r0, #1]!
ldrb r2, [r0, #1]!
ldrb r1, [r0, #1]! @ ldr 从内存加载到寄存器 r1-r4 中
@ 2、将数据拼接到r5寄存器中
orr r5, r5, r4
orr r5, r5, r3, lsl #8
orr r5, r5, r2, lsl #16
orr r5, r5, r1, lsl #24
*/
stop:
b stop
.end
6.10 栈指针寄存器内存读写指令
1)栈的分类
栈的类型 | 出栈入栈 | 指向特点 |
---|---|---|
满减栈 (Full Descending Stack) |
压栈:栈指针指向向低地址方向移动 出栈:栈指针指向向高地址方向移动 |
当前栈指针指向的地址空间内存在有效数据 压栈时需先移动栈指针指向,再压入新的数据,防止覆盖 |
满增栈 (Full Ascending Stack) |
压栈:栈指针指向向高地址方向移动 出栈:栈指针指向向低地址方向移动 |
当前栈指针指向的地址空间内存在有效数据 压栈时需先移动栈指针指向,再压入新的数据,防止覆盖 |
空减栈 (Empty Descending Stack) |
压栈:栈指针指向向低地址方向移动 出栈:栈指针指向向高地址方向移动 |
当前栈指针指向的地址空间内不存在有效数据 当使用空栈进行压栈操作时,可以直接压入新的有效数据 |
空增栈 (Empty Ascending Stack) |
压栈:栈指针指向向高地址方向移动 出栈:栈指针指向向低地址方向移动 |
当前栈指针指向的地址空间内不存在有效数据 当使用空栈进行压栈操作时,可以直接压入新的有效数据 |
2)指令码
满减栈(Full Descending Stack) ---> ldmfd / stmfd
满增栈(Full Ascending Stack) ---> ldmfa / stmfa
空减栈(Empty Descending Stack) ---> ldmed / stmed
空增栈(Empty Ascending Stack) ---> ldmea / stmea
m:mutiple,多个、多种的意思
3)指令格式
以满减栈为例:压栈操作时,栈指针的指向先向低地址方向移动,再压入新的数据
ldmfd sp!, {寄存器列表}
! 的作用:更新栈指针的指向
从栈指针指向的一片内存地址空间中读取数据到寄存器列表中
stmfd sp!, {寄存器列表}
! 的作用:更新栈指针的指向
将寄存器列表中的值写入到栈指针指向的一片内存空间地址中
当进行压栈操作时,sp的指向先向低地址方向移动,再压入新的数据
4)寄存器列表的编写规则
- 寄存器列表中用于存放多个寄存器的编号
- 当寄存器列表中寄存器编号连续时,此时使用-进行分隔,如{r0-r5}
- 当寄存器列表中寄存器编号不连续时,此时使用 , 进行分隔,如{r1, r3, r5, r7}
- 寄存器列表中可以存在部分连续,部分不连续的寄存器编号,如{r1-r4, r6, r8-r9}
- 寄存器列表中的寄存器编号一般采用从小到大的方式编写,也可以从大到小写,但是如果从大到小写,只能使用 , 分隔,如{r4, r3, r2, r1}
5)测试代码
① 将 r1 - r4 寄存器中的值使用 stmfd 指令写入栈指针内存空间中,再使用 ldmfd 命令读取数据(出栈)到 r5 - r8 寄存器中:
/***********************9、栈指针寄存器内存读写指令**************************/
ldr sp, =0x40000820
ldr r1, =0x11111111
ldr r2, =0x22222222
ldr r3, =0x33333333
ldr r4, =0x44444444
stmfd sp!, {r1-r4}
@ 解释:
@ 将r1-r4寄存器中的值写入到sp栈指针指向的内存空间地址中
@ 由于使用的是满减栈,栈指针会先移动指向(向低地址方向移动),再压入数据
@ sp = 0x40000820 - 0x10(16个字节) = 0x40000810
ldmfd sp!, {r5-r8}
@ 解释:
@ 将sp栈指针指向的内存空间地址中的数据读取到r5-r8寄存器中
@ 此时在做出栈操作,sp的指向会向高地址方向移动
@ sp = 0x400000810 + 0x10(16个字节) = 0x40000820
② 栈指针寄存器本质是实现 C 语言中的局部变量的功能,现将 r1 和 r2 的值入栈到 sp 指向的内存空间中存储,函数返回时,将 sp 指向的内存空间出栈,恢复原来的变量数值
汇编实现如下 C 语言代码(一层函数),实现局部变量功能
int main(void)
{
int r1=0x1, r2=0x2;
func();
add r3, r1, r2
}
void func(void)
{
int r1=0x5, r2=0x5;
sub r3, r1, r2
}
@ 使用栈指针寄存器本质是为了实现c语言中的局部变量的功能
@ 为了实现局部变量的功能,需要将r1和r2寄存器中的值先压入到栈空间地址中
ldr sp, =0x40000820
mov r1, #0x1
mov r2, #0x2
@ 此处可以进行压栈操作
@ 压栈操作取决于是否二次使用这个寄存器
@ 只要在还没有二次使用这个寄存器的位置进行压栈即可
bl func
add r3, r1, r2
b stop
func:
@ 此处也可以进行压栈操作
stmfd sp!, {r1-r2}
@ 相当于函数体
mov r1, #0x5
mov r2, #0x5
sub r3, r1, r2
@ 相当于函数体
@ 进行出栈操作
ldmfd sp!, {r1-r2}
mov pc, lr @ 手动添加函数返回
汇编实现如下 C 语言代码(多层函数),实现局部变量功能
int main(void)
{
int r1=0x1, r2=0x2;
func1();
add r3, r1, r2
}
void func1(void)
{
int r1=0x5, r2=0x5;
func2();
sub r3, r1, r2
}
void func2(void)
{
int r1=0x9, r2=0x1;
mul r3, r1, r2
}
ldr sp, =0x40000820
mov r1, #0x1
mov r2, #0x2
bl func1
add r3, r1, r2 @ 期望值:r3=0x3
func1:
stmfd sp!, {r1-r2} @ 将r1=0x1,r2=0x2进行压栈
mov r1, #0x5
mov r1, #0x5
bl func2
sub r3, r1, r2 @ 期望值:r3=0x0
ldmfd sp!, {r1-r2} @ 将r1=0x1,r2=0x2进行出栈
mov pc, lr @ 实现func1返回到main函数
func2:
stmfd sp!, {r1-r2} @ 将r1=0x5,r2=0x5进行压栈
mov r1, #0x9
mov r2, #0x1
mul r3, r1, r2 @ 期望值:r3=0x9
ldmfd sp!, {r1-r2} @ 将r1=0x5, r2=0x5进行出栈
mov pc, lr @ 实现func2返回到func1
多层嵌套调用函数时,使用 mov pc, lr 会出现问题:
LR 寄存器第一次保存的地址会被第二次保存发热函数返回地址覆盖,需要将LR寄存器中保存的函数返回地址进行备份(将LR寄存器中的值压入到栈空间中存储)
以下代码没有问题
使用 stmfd sp!, {r1-r2, lr} 入栈保存第一次函数调用 LR 寄存器中的值,在调用 stmfd sp!, {r1-r2, lr} 保存第二次函数调用 LR 寄存器的值。
函数结束返回时,使用 ldmfd sp!, {r1-r2, pc} 出栈恢复 LR 寄存器的值,栈是先进后出,后进先出,再次返回时,可恢复第一次 LR 寄存器的值,从而实现多层函数嵌套调用的逐层返回。
@ 没有问题的代码
ldr sp, =0x40000820
mov r1, #0x1
mov r2, #0x2
bl func1
add r3, r1, r2 @ 期望值:r3=0x3
func1:
stmfd sp!, {r1-r2, lr} @ 将r1=0x1,r2=0x2进行压栈
mov r1, #0x5
mov r2, #0x5
bl func2
sub r3, r1, r2 @ 期望值:r3=0x0
ldmfd sp!, {r1-r2, pc} @ 将r1=0x1,r2=0x2进行出栈
@ 将lr寄存器中的值出栈给pc寄存器,就相当于mov pc, lr
@ 将func1的返回地址出栈给PC
func2:
stmfd sp!, {r1-r2, lr} @ 将r1=0x5,r2=0x5进行压栈
mov r1, #0x9
mov r2, #0x1
mul r3, r1, r2 @ 期望值:r3=0x9
ldmfd sp!, {r1-r2, pc} @ 将r1=0x5, r2=0x5进行出栈
@ 将lr寄存器中的值出栈给pc寄存器,就相当于mov pc, lr
@ 将func2的返回地址出栈给pc
6.11 CPSR 特殊功能寄存器读写指令
1)指令码
msr 向 CPSR 寄存器中写入数据 Move to Special register from Register
mrs 从 CPSR 寄存器中读取数据 Move to Register from Special register
2)指令格式
只有mrs和msr这两条汇编指令可以操作CPSR寄存器,其他的汇编指令无法直接操作CPSR寄存器!!!
msr CPSR, oprand_shifter2
作用:将 oprand_shifter2 第二操作数写入到 CPSR 寄存器中
mrs Rd, CPSR
作用:将 CPSR 寄存器中的数据读取到 Rd 寄存器中
3)测试代码
/***********************10、CPSR特殊功能寄存器指令**************************/
@ 已知CPSR寄存器中的值为0xD3(正常情况下,我们并不知道CPSR寄存器中具体是什么值)
@ 目的:我们需要将当前工作模式从SVC模式切换到User模式
@ 我们只可以改变CPSR寄存器中的M[4:0]位,其他的位是不可以改变的
@ 此时,需要用于位运算相关的汇编指令
/*
问题:普通的汇编指令无法直接操作CPSR寄存器,需要通过MSR和MRS这条湖边指令来操作CPSR寄存器
and CPSR, CPSR, #(~(0x1f << 0))
orr CPSR, CPSR, #(0x1 << 4)
*/
/*
1、可以先使用mrs将CPSR寄存器中的值读取到普通寄存器中
2、再通过and和orr修改普通寄存器中的值
3、将修改完的值通过msr写入到CPSR寄存器中
SVC模式(10011) -----> User模式(10000)
*/
/*
mrs r0, cpsr
and r0, r0, #(~(0x1f << 0))
orr r0, r0, #(0x1 << 4)
msr cpsr, r0
*/
@ 不推荐这样写,由于你并不清楚CPSR寄存器中具体是什么值
@ msr cpsr, #0xd0
注意:
由于在修改工作模式前,并不知道当前 CPSR 中的值,所以最好先使用 mrs 获取CPSR 的值,再对应只修改工作模式(低 5 位),再使用 msr 将修改过后的值赋值回去。
6.12 软中断指令
1)什么是软中断
软中断就是一种由软件主动触发的中断机制,用于从用户模式(或非特权模式)切换到特权模式(通常是管理模式 SVC),以请求操作系统提供特定服务(如系统调用)。
2)指令格式
swi 软中断号
软中断号就是一个数字,通常作为指令的 8 位立即数存在,因此范围为 0 ~ 255
3)软中断的作用
执行软中断指令(SWI/SVC)时,处理器会无条件切换到管理模式(SVC 模式)
4)测试代码
mrs r0, cpsr
and r0, r0, #(~(0x1f << 0))
orr r0, r0, #(0x1 << 4)
msr cpsr, r0
@ 不推荐这样写,由于你并不清楚CPSR寄存器中具体是什么值
@ msr cpsr, #0xd0
@ 软中断指令
swi 10
@ 当执行完这条汇编指令,工作模式默认被切换到SVC模式下
mov r1, #0x1