参考:<ARM编程与架构> 韦东山
ARM内部寄存器及汇编指令概述
寄存器:
我们说”寄存器”时,需要分辨是哪种:
1.CPU内部的寄存器
2.CPU之外设备自己的寄存器
这是不一样的:
CPU通过各类汇编指令访问内部寄存器
CPU要访问设备的寄存器时,就像访问内存一样,要先知道地址
举例:外部寄存器的地址是0x1000,要读它,使用如下汇编指令:
mov r0,#0x1000 //把外部存器地址值赋给CPU内部寄存器r0
ldr r1,[r0] // 使用r0所表示的地址来读数据,存入CPU内部寄存器r1
一、ARM内部寄存器
无论是cortex-M3/M4,还是cortex-A7,CPU内部都有R0、R1、……R15寄存器;它们可以用来“暂存”数据。
其中R0~R12为通用目的寄存器;
对于R13、R14、R15,还另有用途:
R13:别名SP(Stack Pointer),栈指针
R14:别名LR(Link Register),用来保存返回地址
R15:别名PC(Program Counter),程序计数器,表示当前指令地址,写入新值即可跳转
1.1 M3/M4/A7 CPU内部寄存器
cortex-M3/M4
1)对于cortex-M3/M4来说,比较两个数时,结果保存在哪?
结果就保存在xPSR(Program Status Register)寄存器中
2)对于cortex-M3/M4来说,xPSR实际上对应3个寄存器:
① APSR:Application PSR,应用PSR
② IPSR:Interrupt PSR,中断PSR
③ EPSR:Exectution PSR,执行PSR
这3个寄存器的含义如下图所示
3)这3个寄存器,可以单独访问:
MRS R0, APSR ;读APSR
MRS R0, IPSR ;读IPSR
MSR APSR, R0 ;写APSR
这3个寄存器,也可以一次性访问:
MRS R0, PSR ; 读组合程序状态
MSR PSR, R0 ; 写组合程序状态
所谓组合程序状态,如下图所示:
cortex-A7
A7架构的寄存器介绍及相应的工作模式中的专用寄存器
1)对于A7来说:比较两个数时,结果保存在哪?
结果保存在CPSR(Current Program Status Register)寄存器中
2)组合程序状态,与M3/M4的类似,但是异常编号不同
二、ARM汇编概述
一开始,ARM公司发布两类指令集:
① ARM指令集,这是32位的,每条指令占据32位,高效,但是太占空间
② Thumb指令集,这是16位的,每条指令占据16位,节省空间
要节省空间时用Thumb指令,要效率时用ARM指令。
一个CPU既可以运行Thumb指令,也能运行ARM指令。
怎么区分当前指令是Thumb还是ARM指令呢?
程序状态寄存器中有一位,名为“T”,它等于1时表示当前运行的是Thumb指令。
假设函数A是使用Thumb指令写的,函数B是使用ARM指令写的,怎么调用A/B?
我们可以往PC寄存器里写入函数A或B的地址,就可以调用A或B,
但是怎么让CPU在执行A函数是进入Thumb状态,在执行B函数时进入ARM状态?
做个手脚:
调用函数A时,让PC寄存器的BIT0等于1,即:PC=函数A地址+(1<<0);
调用函数B时,让PC寄存器的BIT0等于0:,即:PC=函数B地址
麻烦吧?麻烦!
引入Thumb2指令集,它支持16位指令、32位指令混合编程。
有那么多指令集:ARM、Thumb、Thumb2,
不好记啊!
不用区分它们,不用担心,
ARM公司推出了: Unified Assembly Language
UAL,统一汇编语言,你不需要去区分这些指令集
2.1 日常工作中常用的几条汇编指令
MOV
LDR/STR
LDM/STM
AND/OR
ADD/SUB
B/BL
DCD
ADR/LDR
CMP
注:写不出很复杂的汇编程序?没必要写很复杂的,设置栈后就用C语言来写函数
2.2 汇编指令格式
以“数据处理”指令为例,UAL汇编格式为:
Operation表示各类汇编指令,比如ADD、MOV;
cond表示conditon,即该指令执行的条件;
S表示该指令执行后,会去修改程序状态寄存器;
Rd为目的寄存器,用来存储运算的结果;
Rn、Operand2是两个源操作数
参考:参考《DEN0013D_cortex_a_series_PG.pdf》P70、《ARM Cortex-M3与Cortex-M4权威指南.pdf》第5章
2.2.1 数据处理指令
Operation表示各类汇编指令,比如ADD、MOV;如下图:
cond有多种取值,如下:
// 几条数据处理指令
1.加法指令ADD:
ADD R1, R2, R3 ; R1 = R2 + R3
ADD R1, R2, #0x12 ; R1 = R2 + 0x12
2. 减法指令SUB:
SUB R1, R2, R3 ; R1 = R2 - R3
SUB R1, R2, #0x12 ; R1 = R2 - 0x12
3. 位操作:
; VisUAL里不支持(1<<4)这样的写法,写成:0x10
AND R1, R2, #(1<<4) ; 位与,R1 = R2 & (1<<4)
AND R1, R2, R3 ; 位与,R1 = R2 & R3
BIC R1, R2, #(1<<4) ; 清除某位,R1 = R2 & ~(1<<4)
BIC R1, R2, R3 ; 清除某位,R1 = R2 & ~R3
ORR R1, R2, R3 ; 或操作
4. 比较:
CMP R0, R1 ; 比较R0-R1的结果
CMP R0, #0x12 ; 比较R0-0x12的结果
TST R0, R1 ; 测试 R0 & R1的结果
TST R0, #(1<<4) ; 测试 R0 & (1<<4)的结果
2.2.2 内存访问指令
读内存指令LDR/LDM:参考《DEN0013D_cortex_a_series_PG.pdf》P340、P341
写内存指令STR/STM:参考《DEN0013D_cortex_a_series_PG.pdf》P377、P378
LDR/STR:
LDR:Load Register;LDM:Load Multiple Register;
STR:Store Register;STM:Store Multiple Register。
// 示例
MOV R0, #0x20000
MOV R1, #0x10
MOV R2, #0x12
STR R2, [R0] ; R2的值存到R0所示地址
STR R2, [R0, #4] ; R2的值存到R0+4所示地址
STR R2, [R0, #8]! ; R2的值存到R0+8所示地址, R0=R0+8
STR R2, [R0, R1] ; R2的值存到R0+R1所示地址
STR R2, [R0, R1, LSL #4] ; R2的值存到R0+(R1<<4)所示地址
STR R2, [R0], #0X20 ; R2的值存到R0所示地址, R0=R0+0x20
MOV R2, #0x34
STR R2, [R0] ; R2的值存到R0所示地址
LDR R3, [R0], +R1, LSL #1 ; R3的值等于R0+(R1<<1)所示地址上的值
LDM/STM:
//LDM/STM的地址模式:
addr_mode:
IA - Increment After, 每次传输后才增加Rn的值(默认,可省)
IB - Increment Before, 每次传输前就增加Rn的值(ARM指令才能用)
DA – Decrement After, 每次传输后才减小Rn的值(ARM指令才能用)
DB – Decrement Before, 每次传输前就减小Rn的值
! : 表示修改后的Rn值会写入Rn寄存器,
如果没有"!", 指令执行完后Rn恢复/保持原值
^ : 会影响CPSR, 在讲异常时再细讲
// 示例
MOV R1, #1
MOV R2, #2
MOV R3, #3
MOV R0, #0x20000
STMIA R0, {R1-R3} ; R1,R2,R3分别存入R0,R0+4,R0+8地址处
ADD R0, R0, #0x10
STMIA R0!, {R1-R3} ; R1,R2,R3分别存入R0,R0+4,R0+8地址处, R0=R0+3*4
1.低标号的寄存器对应低地址;高标号的寄存器对应高地址
2.代码详解:
从内存中把R1-R3读出来,并且读到R1-R3的位置,这就是栈的作用;
栈就是用来保存现场的,所谓现场就是保存那些寄存器
调用某个函数之前,先把当前寄存器的值保存起来,保存
在栈中(栈就是内存),调用完之后,再把对应寄存器中的值读出来
2.2.3 分支/跳转指令
参考《DEN0013D_cortex_a_series_PG.pdf》P327、P328、P329
核心指令是B、BL:
C程序中,函数A调用函数B的实质是什么?
void A()
{
int a = 10;
B(a);
printf(“ok”);
}
实质是:跳转去执行函数B的代码,函数B执行完后,还要回到函数A继续执行后面的代码。
对应的汇编指令就是跳转指令
B:Branch,跳转
BL:Branch with Link,跳转前先把返回地址保持在LR寄存器中
BX:Branch and eXchange,根据跳转地址的BIT0切换为ARM或Thumb状态(0:ARM状态,1:Thumb状态)
BLX:Branch with Link and eXchange
根据跳转地址的BIT0切换为ARM或Thumb状态(0:ARM状态,1:Thumb状态)
// 几条跳转指令实现
1.B指令示例:
B Delay
Delay
MOV R0, #1000
Loop
SUBS R0, R0, #1
BNE Loop
MOV R1, #1
2. BL指令示例:
BL Delay ; 跳转前把返回地址保持在LR寄存器里
MOV R1, #1
Delay
MOV R0, #1000
Loop
SUBS R0, R0, #1
BNE Loop
MOV PC, LR ; 把LR赋给PC,返回
3.给PC直接赋值
ADR LR, Ret ; 伪指令,读取Ret标号的地址赋给LR,这是返回地址
ADR PC, Delay ; 伪指令,读取Delay标号的地址赋给PC,直接跳转
Ret
MOV R1, #1
Delay
MOV R0, #1000
Loop
SUBS R0, R0, #1
BNE Loop
MOV PC, LR ; 把LR赋给PC,返回
2.2.4 立即数
这样一条指令:
MOV R0, #VAL
意图是把VAL这个值存入R0寄存器。
问:VAL可以是任意值吗?
答:不可以,必须是立即数。符合某些规则的数
问:为什么?
答:假设VAL可以是任意数,”MOV R0, #VAL”本身是16位或32位,哪来的空间保存任意数值的VAL?所以,VAL必须符合某些规定。
2.2.4 LDR伪指令
去判断一个VAL是否立即数,麻烦!
并且我就是想把任意数值赋给R0,怎么办?
可以使用伪指令:
LDR R0, =VAL
“伪指令”,就是假的、不存在的指令。
注意LDR作为“伪指令”时,指令中有一个“=”,否则它就是真实的LDR(load regisgter)指令了。
编译器会把“伪指令”替换成真实的指令,比如:
LDR R0, =0x12
0x12是立即数,那么替换为:MOV R0, #0x12
LDR R0, =0x12345678
0x12345678不是立即数,那么替换为:
LDR R0, [PC, #offset] // 2. 使用Load Register读内存指令读出值,offset是链接程序时确定的
……
Label DCD 0x12345678 // 1. 编译器在程序某个地方保存有这个值
2.2.4 ADR伪指令
ADR的意思是:address,用来读某个标号的地址
示例:
ADR R0, Loop
Loop
ADD R0, R0, #1
它是“伪指令”,会被转换成某条真实的指令,比如:
ADD R0, PC, #val ; val在链接时确定
Loop
ADD R0, R0, #1
2.2.5 ARM编译器与GCC编译器语法差异
2.3 栈的四种方式
根据栈指针指向,可分为满(Full)/空(Empty):
满SP指向最后一个入栈的数据,需要先修改SP再入栈
空SP指向下一个空位置,先入栈再修改SP
根据压栈时SP的增长方向,可分为增/减:
增(Ascending):SP变大
减(Descending):SP变小
组合后,就有4种方式:
满增、满减,
空增,空减。
常用的“满减”:
入栈时用STMDB,也可以用STMFD,作用一样;
出栈时用LDMIA,也可以用LDMFD,作用一样。
最常用的就是满减这种类型的栈
// 示例
STMFD sp!, {r0-r5} ; Push onto a Full Descending Stack
LDMFD sp!, {r0-r5} ; Pop from a Full Descending Stack
压栈:先减SP,再存数据
出栈:先读地址,再加