第三章 ARM指令系统
文章目录
- 第三章 ARM指令系统
- 一、ARM指令系统简介
- (一)体系架构与指令系统
- (二)指令格式
- 1.四种格式
- 2.ARM指令系统:
- 二、ARM指令的寻址方式
- 1. 立即寻址
- 2. 寄存器直接寻址
- 3. 寄存器移位寻址
- 4. 寄存器间接寻址
- 5. 基址变址寻址
- 6. 多寄存器直接寻址
- 7. 堆栈寻址
- 三、ARM核心指令
- 1.数据传送指令
- 2.存储器访问指令
- 3.算术运算指令
- 4.逻辑运算指令
- 5.移位和循环指令
- 6.符号扩展指令
- 7.字节调序指令
- 8.位域处理指令
- 9.比较和测试指令
- 10.子程序调用与无条件转移指令
- 11.饱和运算指令
- 12.其他指令
- 13.伪指令
一、ARM指令系统简介
可以让计算机执行某种操作的命令,称为机器指令。计算机的指令系统是只改计算机的CPU所能识别和执行的全部指令的集合。
(一)体系架构与指令系统
读懂上图:
- ARM7TDMI(即第二列的CPU),是基于ARMV4T1架构的,其中V4表示ARM指令集版本4(32位),T1表示Thumb指令集版本1(16位),简称Thumb-1。
- ARM指令集与Thumb指令集:
- 所有的Cortex-A系列, Cortex-R 系列和 ARM11 系列支持ARM指令集和Thumb指令集
- V5版本以前的处理器支持指令长度为32位的ARM指令集和16位的Thumb-1指令集,例如ARM7TDMI
- V5版本以后的处理器内核开始支持Thumb-2指令集
- ARM程序段只使用ARM指令,Thumb程序段只使用Thumb指令
- 状态可以相互切换,良好的执行效率,支持ARM处理器的所有功能
- Thumb-1指令集是ARM指令集的子集,有良好的代码密度
- Thumb-2指令集是一种兼容16位和32位指令的改进Thumb指令集,无须在Thumb状态(16位)和ARM状态(32位)间切换
- Cortex-M的处理器内核(ARM V6M、V7M等架构),不再支持ARM指令集,仅支持Thumb-1和Thumb-2指令集。(注意,Cortex-A, Cortex-R ,Cortex-M是Cretex的三种系列,其中A表示“应用”,R表示“实时”,M表示“微型控制器”,所以M比A、R的要求更低)
- Cortex-M0、M1、M1+到Cortex-M3、M4的指令集设计是向上兼容的,即CortexM0处理器编译的代码可以M3、M4处理器上运行
- Cortex-M0的多数指令是16位的,而Cortex-M4支持更多的32位指令
- Cortex-M处理器不支持ARM指令,它们不向后兼容ARM7TDMI处理器
- 不同指令集的语法各不相同
- ARM开发工具中新支持的UAL(Unified Assembly language)统一了ARM指令集和Thumb指令集的语法,16位汇编和32位汇编指令可以无缝出现在代码里,ARM和Thumb的指令本质上一样,只是编码成不一样的格式,注意,在汇编工具中一般要选择汇编器的语法。
- Thumb-2指令时不需要分析这条指令是32位还是16位指令,编译器会按照最简原则自动完成汇编(即若位数足够,默认为16位)
- UAL(Unified Assembly language)中可以利用后缀“.N”和“.W”来指定指令长度
ADD.W R1,R2 ; 原来是16位代码,指定汇编为32位代码(W-变32位)
ADD.N R1,R1,R2 ; 汇编为16位代码(N-变16位)
- “.N”后缀不能把原是32位的代码变成16位的代码
- 有超过8位范围的立即数指令一般自动汇编成32位代码
(二)指令格式
1.四种格式
指令的格式一般由操作码和操作数两部分组成,常见有四种形式:
分类 | 格式 | 意义 | 举例 |
三地址指令 | OP Ad1 Ad2 Ad3 ; | 表示 (Ad2)OP(Ad3)–(Ad1),即Ad2与Ad3做操作,然后放进Ad1; | 如ADD R1 R2 R3; 表示R1=R2+R3; |
二地址指令 | OP Ad1 Ad2 ; | 表示 (Ad1)OP(Ad2)–(Ad1),即Ad1与Ad2做操作,然后放进Ad1; | 如ADD R1 R2; 表示R1=R1+R2或R1+=R2; |
一地址指令 | OP Ad1 ; | 表示 (Ad1)OP–(Ad1),即Ad1做操作,然后放进Ad1; | 如B R1;表示跳转到R1; |
零地址指令 | OP; | 根据OP决定 | 如NOP;这条指令什么也不做,只是消耗一条指令的时间,用于产生指令对齐或延时。 |
此时,指令长度=操作码长度+操作数地址码长度。指令长度是不固定的,指令长度会影响CPU速度。
2.ARM指令系统:
ARM指令系统是RISC指令系统,指令长度固定,基本采用下列指令基本格式:
<opcode>{<cond>}{S} <Rd>,<Rn> {,<shifter_operand>}
其中:
opcode:指令操作码 (不可省略)
cond:执行条件 (可省略)
S:该指令的执行是否影响APSR(标志)寄存器的值 (不写S则表示不影响,写S则表示影响)
Rd:目的操作数(寄存器)(不可省略)
Rn:第一源操作数(寄存器) 【一般都是二地址指令,但是也有一地址和零地址】
shifter_operand :第二源操作数 (可以为三地址指令形式)
几个重点:
- 1. 标志寄存器:
解释:上图中,条件码的助记符即指令中的,标志位状态是根据其后面的含义确定的,NZCV各有标志,只是一个标记。比如通过下面这个例子进行解释:
//前提条件:R1,R2相等
SUBS R1,R2;//SUB为减法指令,S表示会影响标志寄存器,由于R1=R2,故更新Z=1
ADDEQ R2, R3,R4; //ADD为加法指令,EQ为“相等”的助记符,这条指令表示“若EQ对应的Z=1成立,则执行ADD指令”
BGE L1; //B跳转指令 + GE后缀; 若之前的比较结果是有符号数大于或等于(即N==V),则跳转至L1
具体“如何影响标志寄存器”:
助记符:
- 2. 第二源操作数<shifter_operand>
共有三类共11种选择形式,分别为立即数方式、寄存器方式和寄存器移位方式。
(1)立即数方式(#immed)
MOVS R0,#0X104 ; //把0X104这个32位的常数赋给R0,这里的“#0x104”就是一个立即数
立即数是一个无符号的32位数值常量#immed,但并不是任意的32位数都是合法的立即数。从下图可以看到,第二操作数立即数的存储的位数只有12位。要用这12位来表示一个32位的数,就要采用下面的规则,但是这种方法也不能完全表示所有的立即数,所以存在“不合法”的立即数。
规则:把12位分为两部分,一段为8位立即数(immed_8),另一段4位为循环移位的位数(rotate_imm),一个8位的无符号数值常量,高位用0补全到32位后,循环右移(2×rotate_imm)次后的一个32位值,就是立即数的值,立即数如果能通过上述计算来得到,就是合法的立即数。反之,就不合法。即:32位立即数 = immed_8循环右移(2×rotate_imm)位
如果需要对寄存器传送一个不合法的立即数,可以采用伪指令的方法把该立即数定义在存储区,然后通过LOAD操作指令传送到寄存器。
LDR R0,=data1 ;R0保存data1的首地址
LDR R1, [R0] ;寄存器间接寻址方式,R1=0x11223344
LDR R2, [R0,#4] ;寄存器间接寻址方式,R2=0xFFDDCCBB
……
data1 DCD 0x11223344,0xFFDDCCBB ;定义数据的伪指令
不同架构的ARM中立即数的具体规定会有所不同,在Thumb-2集下的immed_12的格式:
包括两部分:0-7的8位abcdefgh,12-14的imm3,26的i。循环码由i:imm3:a组成,共5位。
- 存储位置:
- 编码表:
例:#0x104如何进行存储?
0x104,即二进制的:0000 0000 0000 0000 0000 0001 0000 0100(32位)
观察到,只有这部分不为0:0000 0000 0000 0000 0000 000 1 0000 010 0(32位)
故改写为:1000 0010 + 循环左移一位(即循环右移31位)= 0x104
所以:8位immed_8为1000 0010,即0x82;五位循环码i:imm:a=11111
(2)寄存器方式(Rm)
直接用ARM寄存器
ADD R2,R3,R4 ; //R2 ← R3 + R4
SUB R1,R3,R2 ; //R1 ← R3 – R2
(3)寄存器移位方式(Shift_operand)
第一源操作数是寄存器Rm时,第二源操作数可以指定第一源操作数的移位方式和移位的位数,移位的位数可以是立即数#shift_imm或者寄存器Rs的数值。将第一源操作数寄存器中的内容按指定位数移位后,才是真正的操作数。
例:
MOV R0, R2, LSL #3 ; //第二源操作数“LSL #3”指示R2逻辑左移3位(相当于乘8)后,赋值给R0寄存器
//这里移位位数是立即数#3,但是注意,这里的立即数#shift_imm与上面的#immed不同,#shift_imm只有5位,取值范围为0~31
MOV RS,#0x03 ;
MOV R0,RS ; //这里就是用RS的值设置移位位数
格式 | 记忆 | 理解含义 | 算术含义 |
<Rm> , ASR # <shift_imm>/ <Rs> | Arithmatic Shift Right | 算术右移——高位补上“符号位”。 | 算术右移一位相当于有符号数除以2 |
<Rm> , LSL # <shift_imm>/ <Rs> | Logic shift Left | 逻辑左移——最低位用0填充 | 逻辑左移一位相当于无符号数乘以2 |
<Rm> , LSR # <shift_imm>/ <Rs> | Logic shift Right | 逻辑右移——最高位用0填充 | 逻辑右移一位相当于无符号数除以2 |
<Rm> , ROR# <shift_imm>/ <Rs> | Routate Right | 寄存器Rm中的值向右循环移动,左端用右端移出的位填充 | |
<Rm> , RRX | 带扩展循环右移一次 | 右移一次,CPSR中的进位标志位C移入最高位,最低位移入C |
二、ARM指令的寻址方式
- 指令可以直接指明操作数本身,也可以只给出操作数的地址信息(即从何处可以取得该操作数)。寻址方式是指寻找指令操作数(地址)的方式。
- ARM指令系统支持的常见寻址方式有7种:立即寻址,寄存器直接寻址,寄存器移位寻址,寄存器间接寻址,基址变址寻址,多寄存器直接寻址以及堆栈寻址
结构 | 寻址方式 | 原因 |
ARM | 立即寻址、寄存器直接寻址、寄存器移位寻址 | ARM采用RISC架构,ALU与外部存储器之间不能直接传递数据,ARM指令系统只有寄存器操作数可以进行算术/逻辑运算,基本上只使用立即寻址、寄存器直接寻址和寄存器移位寻址这3种方式 |
存储器 | 寄存器间接寻址,基址寻址,多寄存器直接寻址方式 | 存储器操作数只有STORE/LOAD两种操作。必须将存储器操作数取到寄存器中来进行计算,计算以后再存回存储器。所以寄存器间接寻址,基址寻址,多寄存器直接寻址方式多用于访问存储器操作数的STR和LDR指令。 |
堆栈 | 堆栈寻址方式 | 相对寻址方式用于转移指令的执行,执行堆栈操作时则会使用堆栈寻址方式 |
1. 立即寻址
- 操作数直接放在指令中,与操作码一起放在代码段区域中,立即数要以“#”为前缀。
- 十六进制要求在“#”后加上“0x”或“&”,二进制要求在“#”后加上“0b”, 十进制要求在“#”后加上“0d”或者缺省。
- Thumb-2指令集的带有立即数的MOV指令,根据不同情况编译为16位或者32位指令。(例如MOV指令时被汇编成采用immed_12格式立即数的32位指令,MOVW指令时被汇编成采用immed_16格式立即数的32位指令,MOVS指令在带有8位范围内的立即数的时候会编译为16位指令)
MOV R0,#0 ; R0 ← 0,32位指令,imm12格式立即数0传送到寄存器R0中
MOVS R0,#0x08 ; R0 ← 0x08,16位指令,imm8格式立即数0x08传送到寄存器R0中
MOVS R0,#0x104 ; R0 ← 0x104,32位指令, imm12格式立即数0x104传送寄存器R0
//这里立即数0x104超过了8位的范围,所以尽管为MOVS指令,也要汇编成immed12格式汇编成32位指令
ADD R1,R0,#1 ; R1 ← R0 + 1,32位指令,寄存器R0的内容加1,结果传送到R1中
2. 寄存器直接寻址
- 操作数的值位于CPU的内部寄存器中,是各类处理器中通常采用的一种执行效率较高的寻址方式
MOV R1,R2 ; R1 ← R2,寄存器R2的内容传送到寄存器R1 //过程见下图
ADD R0,R1,R2 ; R0 ← R1 + R2,寄存器R1和R2的内容相加,结果传送到R0
3. 寄存器移位寻址
- 不同于Intel X86 CPU的寻址方式,寻址的操作数由寄存器中的数值进行相应移位得到,移位的方式以助记符形式给出(例如ASR、LSL等,与上面的表格相同)
AND R0,R1,R2,LSL #2 ; R0 ← R1 & (R2 逻辑左移2位) //过程见下图
MOV R0,R1,ASR R2 ; R0 ← (R1算术右移R2规定的位数)
4. 寄存器间接寻址
- 寄存器中存放的内容为操作数的内存地址,寄存器是内存操作数的地址指针,在指令中须用中括号“[ ]”括起来。最大特点是,执行了赋值操作后,地址指向的寄存器R1数据改变,但是存这个地址的寄存器R0值不变
STR R0,[R1]; R0中的值传送到以R1的值作为地址的存储器中;指令执行完成后R1的值不变
LDR R0,[R1]; R1中的值作为地址,将内存中该地址单元的数据传送到R0; 指令执行完成后R1的值不变
5. 基址变址寻址
- 某个寄存器(一般作为基址寄存器)提供一个基准地址,该基地址将与指令中给出的称为“地址偏移量”(变址,索引)的数据相加,形成操作数的有效地址。
- 常用于访问基地址附近的地址单元,如查表、数组操作、功能部件寄存器访问等
- 寄存器间接寻址方式也可以看作是偏移量为0的基址变址寻址
- 感叹号“!”表示指令执行后是否更新存放地址的寄存器(写回) ,但是只用于偏移量为立即数的变址寻址,不能用于偏移量为寄存器的变址寻址
//前索引(前序),即先变址再得到操作数,进行赋值( Pre-indexed Addressing))
(1)LDR R0,[R1,#4] ; R0←[R1+4],立即数前索引变址寻址,不带更新
//最常见的形式,R1就是基址寄存器,先+4,再取新地址指向的寄存器的值,赋给R0
(2)LDR R0,[R1,#4]! ; R0←[R1+4],R1←R1+4,立即数前索引变址寻址,带更新
//这里有感叹号,表示更新了基址寄存器,下一次的基址寄存器R1就是R1+4
(3)LDR R0,[R1,R2] ; R0←[R1+R2] ,寄存器前索引变址寻址,不带更新
//这里其实可以看做寄存器间接寻址,先把R1,R2存的数据相加得到地址,再找地址指向的寄存器的数据,赋给R0
//后索引(后序),先得到操作数,再变址(Post-indexed Addressing)
(4)LDR R0,[R1],#4 ; R0←[R1],R1←R1+4,立即数后索引变址寻址,带更新
(5)LDR R0,[R1,R2,LSL #2] ; R0←[R1+(R2<<2)],寄存器前索引变址寻址,不带更新
- 跳转指令也采用基址变址寻址方式,程序计数器(PC)的值为基地址,指令中的地址标号作为偏移量(变址),两者相加。又称为相对寻址。
BL SUB1 ; 调用SUB1子程序,相对寻址,返回地址保存在LR寄存器中
BEQ L2 ; 条件跳转到L2标号处,相对寻址
L2 ……
LDR R0,L2 ; 将标号L2所在地址的字数据传送至R0,相对寻址
SUB1 ……
MOV PC,LR ; 返回主程序,LR是链接寄存器R14,PC是程序计数器R15
6. 多寄存器直接寻址
- LDM(加载多个寄存器)和STM(存储多个寄存器)指令可以读写存储器中的多个连续数据传输到处理器的多个寄存器
- 这类指令只支持32位数据块的存取
- 一条指令传送最多16个通用寄存器的值,连续的寄存器之间用“-”(减号)连接,不连续则用“,”(逗号)分隔
- 用后缀表示指针变化:
- IA(increase after):每次读写后地址+4
- IB(increase before):每次读写前地址+4
- DA(decrease after):每次读写后地址-4
- DB(decrease before):每次读写前地址-4
LDMIA R0!,{R1,R3-R5}; R1←[R0],R3←[R0+4],R4←[R0+8],R5←[R0+12],R0←R0+16
// R1,R3-R5中,逗号表示不连续,即没有R2,-表示连续,即有R4
// 同样的,感叹号表示要更新
// IA后缀表示每次读写后地址要加四。正因此,注意到最后更新操作R0的地址上移了16位!
// 注意,从这里我们可以看到LDM和LDR的区别,LDR表示加载,即把寄存器里的值加载出来,但是LDM
// 的方向相反,是把现在地址指向的值存到多个寄存器里。
STMDB R0!,{R1,R3-R5}; [R0-4]←R1,[R0-8]←R3,[R0-12]←R4,[R0-16]←R5,R0-16←R0
7. 堆栈寻址
- 关于堆栈:
- 满堆栈和空堆栈?
满堆栈:栈顶指针总是指向最后一个元素的地址;
空堆栈:栈顶指针总是指向最后一个元素的下一个空地址; - 递增和递减?
递增:堆栈由低地址向高地址生长,第一个元素在低地址,每次新增元素指针位置增加。
递减:堆栈由高地址向低地址生长,第一个元素在高地址,每次新增元素指针位置减小。
- ARM处理器可以支持4种类型的堆栈工作方式,只是根据ARM架构的不同,支持也会有差异。例如Cortex-A类型的处理器支持全部4种类型的堆栈模型。Intel X86 处理器指令系统的堆栈指令支持满递减堆栈的工作方式。Cortex-M类型的处理器只使用“满递减”的堆栈模型。
- 可以在指令后面选择以下模式后缀来指定工作方式:
- FD:满递减堆栈
- ED:空递减堆栈
- FA:满递增堆栈
- EA:空递增堆栈
- 两种指令:PUSH和POP
POP {R0,R4-R5,PC} ; //出栈操作;R0←[SP],R4←[SP+4],R5←[SP+8],PC←[SP+12],SP←SP+16
//再次看下图,可以看到这是一个满递减堆栈,指针指向的是栈顶的元素,而每POP一个元素SP地址增加
//等效于:
LDMIA SP!,{R0,R4-R5,PC} ; //IA,“读写后增加4”,这适配与满递减堆栈的特性,这时就不能用IB
PUSH {R0,R4,R5} ; //入栈操作;[SP-4]←R5,[SP-8]←R4,[SP-12]←R0, SP←SP-12
//再次看下图,可以看到这是一个满递减堆栈,指针指向的是栈顶的元素,而每PUSH一个元素SP地址减小
//等效于:
STMDB SP!,{R0,R4,R5} ; //DB,“读写前减小4”,这适配与满递减堆栈的特性,这时就不能用DA
注意:
- 在多寄存器直接寻址和堆栈寻址中的寄存器列表书写顺序并不重要,多个寄存器和内存单元的对应关系满足这样的顺序对应规则,即编号低的寄存器对应于内存中的低地址单元,编号高的寄存器对应于内存中的高地址单元。
POP {R3,R2} ; POP {R2,R3} ; //两者是等效的
- POP指令和LDMIA指令中的寄存器列表中存在PC寄存器的时候,不能同时存在LR寄存器。
POP和LDMIA即把堆栈里的放进寄存器,把堆栈里的放进PC时,LR会工作,所以不能同时存在。 - PUSH指令和STMDB指令的寄存器列表中则不能有PC和SP寄存器。
PUSH和STMDB即把寄存器里的放进堆栈,不能把SP,PC指向的下一条指令放进堆栈。
PUSH、POP与STM、LDR的区别:
- STM、LDR指令能对任意地址空间进行操作,而PUSH、POP只能对堆栈空间进行操作;
- STM、LDM指令的生长方式可以向上可以向下,而PUSH、POP只能跟着堆栈规定走;
- STM、LDR指令可以用!控制是否更新,而PUSH、POP会自动修改SP的值。
三、ARM核心指令
本节主要介绍Cortex-M4微处理器的Thumb-2指令集。
除了指令外,还有少量的伪指令(pseudo-instruction),可以像使用指令一样使用他们,在汇编是汇编器将这些伪指令解释为指令组合。
1.数据传送指令
基本:
MOV R0,R1 ;两个寄存器间数据传送
MOV R1,#immed_8/12 ;将立即数加载到寄存器中
MRS R0,PRIMASK ;加载特殊功能寄存器(SReg)的值到R0
MSR PRIMASK,R0 ;加载R0到特殊功能寄存器的值
扩展:
MVN R0,R1 ;把R1的值取反后传送给R0
MOVT R1,#immed_16 ;将16位立即数加载到寄存器的高半字[31:16]中,低16位不影响
MOVW R1,#immed_16 ;将16位立即数加载到寄存器的低半字[15:0]中,高16位清0
//根据这两条指令,可以吧32位立即数传送到寄存器,但是要先MOVW再MOVT
2.存储器访问指令
Cortex-M4微处理器对存储器的访问只能通过下面几种指令:
- 单寄存器加载和存储(LOAD、STORE)
- 多寄存器加载和存储(LDM、STM)
PUSH、POP指令也可以实现多寄存器加载功能(第二节已经解释过了)
(1)单寄存器:
LDR R0,[R1,#offset] ;从R1+offset处读取一个字(32位)到R0
STR R0,[R1,#offset] ;从R0读取一个字(32位)到R1+offset处
后缀扩展:
- B(Byte):一个字节(8位)
- H(Half Word):半个字(16位)
- D(Double Word):两个字(32+32)
//以LDR为例,STR同理
LDRB R0,[R1,#offset] ;从R1+offset处读取一个字节(8位)到R0
LDRH R0,[R1,#offset] ;从R1+offset处读取半个字(16位)到R0
LDRD R0,R1,[R2,#offset] ;从R2+offset处读取一个双字(64位)到R0(低32位)和R1(高32位)
自动索引:
其实与上面LDM、STM的形式类似,只不过是单个寄存器的。
//前索引
LDR R0,[R1,#20]! ;从R1+20处读取一个字到R0,然后更新R1=R1+20
//后索引
LDR R0,[R1],#20 ;先更新R1=R1+20,之后再从R1处读取一个字到R0
(2)多寄存器
LDMIA R0!,{寄存器列表} ;从R0处读取一个字(32位)到寄存器列表,每读一次之后更新R0自增一个字(32位,也就是4字节,也就是地址+4)
STMIA R0!,{寄存器列表} ;从寄存器列表读取一个字(32位)到R0,每读一次之后更新R0自增一个字(32位)
后缀扩展:
后缀可以修改:
- IA(increase after):每次读写后地址+4
- IB(increase before):每次读写前地址+4
- DA(decrease after):每次读写后地址-4
- DB(decrease before):每次读写前地址-4
3.算术运算指令
//加法
ADD R0,R1,R2 ;R0=R1+R2
ADD R0,R1 ;R0=R0+R1
ADD R0,#imm ;R0=R0+#imm //这三种是基本形式,之后的省略就是省略这种基本模式
//扩展
ADC R0,R1,R2 ;R0=R1+R2+C,表示带进位的加法
ADC ...
//减法
SUB R0,R1,R2 ;R0=R1-R2
SUB ...
//扩展
SBC R0,R1,R2 ;R0=R1-R2-C,表示带借位的减法
SBC ...
RSB R0,R1,R2 ;R0=R2-R1,反向减法
RSB R0,R1,#imm_12
//乘法
MUL R0,R1,R2 ;R0=R1*R2
MUL R0,R1 ;R0=R0*R1
//乘加与乘减
MLA R0,R1,R2,R3 ;R0=R1*R2+R3
MLS R0,R1,R2,R3 ;R0=R3-R1*R2
//扩展。后缀L:Long,表示扩展为64的
MUL R0,R1,R2 ;R0=R1*R2(常规乘)
MLA R0,R1,R2,R3 ;R0=R1*R2+R3(常规乘加)
----
SMULL R0,R1,R2,R3 ;[R1:R0]=R2*R3 //前缀S表示带符号,若换为U表示不带符号
SMLAL R0,R1,R2,R3 ;[R1:R0]=[R1:R0]+R2*R3
//除法,除法的余数会被丢弃
UDIV R0,R1,R2 ;R0=R1/R2,无符号除法
SDIV R0,R1,R2 ;R0=R1/R2,带符号除法
4.逻辑运算指令
//按位与
AND R0,R1,R2 ;R0=R1&R2
AND R0,R1 ;R0=R0&R1
AND R0,#imm_12 ;R0=R0&#imm_12 //这三种是基本形式,之后的省略就是省略这种基本模式
//按位或
ORR R0,R1,R2 ;R0=R1|R2
ORR ...
//按位与非(效果就是,如果R2第1、3位为1,与非之后看上去就是把R1的1、3位清零了)
BIC R0,R1,R2 ;R0=R1&~R2
BIC ...
//按位或非
ORN R0,R1,R2 ;R0=R1|~R2
ORN ...
//按位异或
EOR R0,R1,R2 ;R0=R1^R2
EOR ...
5.移位和循环指令
//逻辑左移,低位填0
LSL R0,R1,R2 ;R0=R1<<R2
LSL R0,R1 ;R0=R0<<R1
LSL R0,#imm_5 ;R0=R0&<<imm_5 //这三种是基本形式,之后的省略就是省略这种基本模式
//逻辑右移,高位填0
LSR R0,R1,R2 ;R0=R1>>R2
LSR...
//算术右移,高位填符号位
ASR R0,R1,R2 ;R0=R1·>>R2
ASR...
//循环右移,高位用低位填充
ROR R0,R1,R2 ;R0=R1>>R2
ROR...
//带进位的循环右移一位
RRX R0,R1 ;R0=(R1>>1)+(C<<31) //就是R1右移一位,用C来填最高位
RRXS R0,R1 ;R0=(R1>>1)+(C<<31),C=R1&1 //同时更新C为刚刚出来的最低位
6.符号扩展指令
SXTB R0,R1 ;把带符号的字节整数(8位)扩展到32位
SXTH R0,R1 ;把带符号的半字整数(16位)扩展到32位
带符号扩展:
二进制补码表示法中,最高位为符号位。1说明是复数,那么高位全填1;0说明是正数,高位全填0。
7.字节调序指令
REV R0,R1 ;在字中按字节为单位调节R1,赋给R0
REV16 R0,R1 ;在高、低半字中按字节为单位分别调节
REVSH R0,R1 ;只在低半字中按字节为单位调序,然后把带符号扩展覆盖高半字
8.位域处理指令
BFC R0,#<LSB>,#<WIDTH> ;从R0的第LSB位开始,一直到LSB+WIDTH位,清零。
BFI R0,R1,#<LSB>,#<WIDTH> ;从R0的第LSB位开始,一直到LSB+WIDTH位,用R1的0-WIDTH位代替,相当于插入。
SBFX R0,R1,#<LSB>,#<WIDTH> ;从R1的第LSB位开始,一直到LSB+WIDTH位,取下来放到R1的0-WIDTH位,然后带符号扩展高位,相当于拷贝。
UBFX R0,R1,#<LSB>,#<WIDTH> ;无符号扩展高位
CLZ Rd, Rn ;计算前导0 的数目
RBIT Rd, Rn ;按位旋转180 度 //就是先按位展开(二进制),然后取中心,做轴对称
例:
(1)BFC(位域清零)指令
LDR R0, =0x1234FFFF
BFC R0, #4, #10
执行完后,R0= 0x1234C00F
(2)BFI(位域插入指令)
LDR R0, =0x12345678
LDR R1, =0xAABBCCDD
BFI.W R1, R0, #8, #16
则执行后,R1= 0xAA5678DD
(3)RBIT指令
记R1=0xB4E10C23
(二进制数值为 1011,0100,1110,0001,0000,1100,0010,0011),指令
RBIT.W R0, R1
执行后,则R0=0xC430872D (二进制数值为1100,0100,0011,0000,1000,0111,0010,1101)。
(4)UBFX/SBFX
LDR R0, =0x5678ABCD
UBFX.W R1, R0, #12,#16 ;R1=0x0000678A。
类似地,SBFX 也抽取任意的位域,但是以带符号的方式进行扩展。
LDR R0, =0x5678ABCD
SBFX.W R1, R0, #8,#4 ;
执行后,R1=0xFFFFFFFB
9.比较和测试指令
CMP <Rn>,<Rm> ;比较:计算Rn-Rm,APSR更新但计算结果不保存
CMP <Rn>,#<immed>
CMN <Rn>,<Rm> ;负比较:计算Rn-(-Rm),APSR更新但计算结果不保存
CMN <Rn>,#<immed>
TST <Rn>,<Rm> ;测试,即按位与:计算Rn和Rm相与后的结果,APSR中的N位和Z位更新,但不保存与运算的结果。若使用了桶形移位则更新C位
TST <Rn>,#<immed> ;测试,即按位与:计算Rn和立即数相与后的结果,APSR中的N位和Z位更新,但不保存与运算的结果
TEQ <Rn>,<Rm> ;测试,即按位异或:计算Rn和Rm异或后的结果,APSR中的N位和Z位更新,但不保存运算的结果。若使用了桶形移位则更新C位
TEQ <Rn>,#<immed> ;测试,即按位异或:计算Rn和立即数异或后的结果,APSR中的N位和Z位更新,但不保存运算的结果
10.子程序调用与无条件转移指令
- B,BL,BLX,BX 跳转 实现程序转移
- MOV 、LDR、POP、LDM 直接修改 PC 值,实现程序转移
主要指令格式有:
B Label ;转移到Label 处对应的地址(Lable:地址或标号)
BX reg ;转移到由寄存器reg 给出的地址
BL Label ;转移到Label 处对应的地址,并且把转移前的下条指令地址保存到LR
BLX reg ;转移到由寄存器reg 给出的地址,并且把转移前的下条指令地址保存到LR
MOV PC, R0 ;转移地址由R0 给出
LDR PC, [R0] ;转移地址存储在R0 所指向的存储器中
POP {…, PC} ;把返回地址以弹出堆栈的风格送给PC,从而实现转移
LDMIA SP!, {…, PC} ; POP 的另一种等效写法
11.饱和运算指令
Cortex-M4 中的饱和运算指令分为两种:一种是带符号饱和运算;另一种是无符号饱和运算。 饱和运算多用于信号处理。当信号被放大后,有可能使它的幅值超出允许输出的范围。如果简单地清除数据最高位MSB,则常常会严重破坏信号的波形,而饱和运算则只是使信号产生削顶失真。
SSAT Rd, #imm5, Rn, {,shift} ;以带符号数的边界进行饱和运算(交流)
USAT Rd, #imm5, Rn, {,shift} ;以无符号数的边界进行饱和运算(带纹波的直流)
12.其他指令
(1)NOP指令
Cortex-M4支持NOP指令,用于产生指令对齐或延时。NOP指令什么也不做,仅消耗一条指令的时间。指令格式如下:
NOP ;什么也不做
_NOP() ;也可以用C语言调用:什么也不做
(2)BKPT指令
BKRT指令产生软件断点中断,主要用于程序的调试。其中immed_8为数字表达式,取值为0~255范围内的整数。
BKPT <immed_8> ;
_BKPT(immed_8); 也可以用C语言调用:断点
13.伪指令
(1)ADR伪指令
ADR是小范围的地址读取伪指令,将基于PC相对偏移的地址值或基于寄存器相对偏移的地址值读取到目标寄存器中。在汇编器汇编源程序时,ADR伪指令被汇编器替换成一条合适的指令。通常,编译器用一条ADD指令或SUB指令来实现该ADR伪指令的功能,若不能用一条指令实现,则产生错误,编译失败。
指令格式:
ADR{cond} register, expr
其中,register是目标寄存器;expr是相对偏移地址表达式,通常是一个地址标号。expr的取值范围:
1)当地址值是字节对齐时,其取指范围为: -255B~255B
2)当地址值是字对齐时,其取指范围为: -1020B~1020B
示例:
Next MOV R1, #0XF0 ; Next是标号
ADR R2, Next ; 读取Next的地址到R2
(2)ADRL伪指令
ADRL是中等范围的地址读取伪指令,与ADR功能相似,也是将基于PC相对偏移的地址值或基于寄存器相对偏移的地址值读取到目标寄存器中,但是可以读取更大范围的地址。在汇编器汇编源程序时,ADRL伪指令被汇编器替换成两条合适的指令。若不能用两条指令实现,则产生错误,编译失败。
ADRL伪指令格式与ADR相同。
expr的取值范围:
1)当地址值是字节对齐时,其取指范围为: -64K~64K
2)当地址值是字对齐时,其取指范围为: -256K~256K
(3)LDR伪指令
LDR是大范围的地址读取伪指令,用于加载32位的立即数或一个地址值到指定寄存器。
LDR伪指令格式:
LDR register, =const-expr
const-expr是常量表达式,可以包含标号。
示例:
LDR R0, =Next ; Next是标号
LDR R1, =(Next+10) ; Next是标号
LDR R2, =0x0F ; 表达式是一个常量
LDR R3, =0x5678ABCD ; 表达式是一个常量
在汇编器汇编源程序时,LDR伪指令被汇编器替换成一条合适的指令。若加载的常量未超出MOV或MVN所能加载的数值范围,则使用MOV或MVN指令代替该LDR伪指令,否则汇编器将常量放入文字池(Literal pools),并使用一条程序相对偏移的LDR指令从文字池读出常量。
- 文字池:文字池其实就是一个存储常量数据的存储空间,汇编器会使用文字池来在代码段中存储常量数据,汇编器通常会在每个段的末尾放置文字池。
- 在前面的示例中, LDR R2, =0x0F 被替换为一条 MOV R2, 0x0F 指令。而 LDR R3, =0x5678ABCD 中的常量0x5678ABCD被汇编器放入文字池(位于代码段的末尾),然后汇编器使用 LDR R3, [PC, #offset] 指令从文字池读出常量0x5678ABCD到R3。其中,#offset是常量的存储地址与PC值之间的相对偏移量。