一、ARM 基础知识
说明:面试会涉及到前5点知识,必须牢记;
(1)ARM工作模式
(2)每种模式的寄存器资源
(3)ARM异常处理
(4)ARM指令部分
(5)汇编与C语言混合编程
(6)硬件外设操作(LED/KEY/PWM/INTERRUPT/ADC/CLOCK/UART/WDT/IIC/SPI)
二、ARM工作模式
U(usr用户模式) S(sys系统模式) A(abt终止模式) U(udf 未定义模式) F(fiq 快中断模式)
I(IRQ 慢中断模式) S(svc管理模式) M(mon安全监控模式[cotex-A])
问题1: ARM核为什么需要设计这么多模式?
为了更好的处理各种异常(例如:中断 ...)
问题2: 什么特权模式,什么是非特权模式?
特权模式下,权限最大化,特权模式可以自己手动切换到其他模式
非特权模式,权限受限,不能自己手动切换到其他模式
(1)非特权模式
usr(用户模式)
(2)特权模式
除用户模式外,其他都是
(3)异常模式
IRQ,FIQ,ABT,SVC,UDF
三、ARM寄存器资源
R0-R15,CPSR,SPSR
[1] r0-r12 普通寄存器使用
[2] r13 (sp) 栈指针寄存器
[3] rl4 (lr) 在跳转之前保存PC值,便于返回(异常处理,子函数调用)
[4] r15 (pc) 告诉ARM核,需要取的指令所在的地址
[5] CPSR 反映ARM核状态
-------------------------------------------------------------------------
31 30 29 28 7 6 5 4:0
-------------------------------------------------------------------------
N Z C V I F T MODE
-------------------------------------------------------------------------
<1>MODE 当前所在的模式
<2>T 1:thumb状态(指令16bit宽度) 0:ARM状态(指令32bit宽度)
<3>F 1 : 禁用FIQ异常 0 : 允许FIQ异常
<4>I 1 : 禁用IRQ异常 0 : 允许IRQ异常
<5>V 有符号溢出 [两个整数相加结果为负数]
<6>C 加法产生进位则置1(计算结果超过32bit),否则置0 / 减法没有产生借位, 则置1,否则置0
<7>Z 计算结果为0
<8>N 计算结果为负数
问题:开发板复位,ARM核的状态?
SVC模式,ARM态,IRQ/FIQ异常禁用
(1)所有模式共用的寄存器
10个
r0-r7,pc,cpsr
(2)r8-r12寄存器
FIQ模式的r8-r12是私有的,其他模式共用
10个
(3)异常模式私有
sp,lr,spsr
3*5 = 15个
(4)用户和系统模式共用同一套寄存器
2个 // 是:sp(r13)、lr(r14)
说明:用户模式(usr)和系统模式(sys)无spsr寄存器,因为只有异常模式才考虑用spsr备份CPSR
(5)cotex-A系列 安全监控模式私有寄存器:sp,lr,spsr
总结:
如果不考虑cotex-A系列,共有37个寄存器;
算上cotex-A系列,多了3个安全监控模式私有寄存器:sp,lr,spsr,共有40个寄存器;
四、ARM异常处理
说明:5种异常模式、7种异常类型,这7种异常类型对应的是这5种异常模式
(1)ARM的异常类型
复位异常 : 开发板复位 ->
SVC模式
未定义指令异常 : 执行了未定义的指令 ->
UDF模式
软中断异常 : 执行了swi 指令 (系统调用使用:每个系统调用接口都对应一个软中断号) ->
SVC模式
预取指令终止异常 : 没有取到指令(从虚拟地址取指令,而虚拟地址没有进行映射到指定的物理地址)->
ABT模式
数据终止异常 : 没有取到数据 ->
ABT模式
慢中断异常 : 产生了中断 ->
IRQ模式
快中断异常 : 产生了中断 ->
FIQ模式
(2)异常产生,需要做的事情
<1>ARM核硬件自动做的事情
[1]将CPSR拷贝到异常模式的SPSR
[2]设置CPSR相应位
.进入ARM状态
.进入相应的异常模式
.禁用中断[复位异常的异常]
[3]将pc的值保存在异常模式的lr
[4]跳转到异常向量表
问题: 什么是异常向量表?有什么作用 ?
异常向量表: 存放的是一些跳转指令;
当异常产生的时候,ARM核会将PC指向异常向量表的相应位置,从异常向量表取出指令执行,
这些指令执行的结果是跳转到异常处理函数
问题: 异常向量表存放的位置 ?
在cotex-A8之前的ARM核,异常向量表可以存放在0x0000,0000或0xffff,0000
在cotex-A8系列中,异常向量表可以存放在任何位置,只需要将异常向量表的基地址写到
协处理器cp15的c12寄存器就可以了。
问题:Linux 操作系统中,异常向量表的位置?
用户态[0~3G ] 0x0000,0000 ~ 0xc000,0000
------------------------------
内核态[3G~4G] 0xc000,0000 ~ 0xffff,ffff
<2>程序需要做的事情:异常函数编写
[1]保存共用的寄存器(把共用寄存器的值压入栈)
[2]异常处理
[3]异常返回(恢复CPSR,PC,共用的寄存器)
五、ARM常用指令
1.ARM指令格式
opcode{cond}{s} Rd,Rn,Rm
(1)opcode 指令助记符号 ,例如:ADD, MOV ...
(2)cond 大于(gt),大于等于(ge),小于(lt),小于等于(le),相等(eq),不相等(ne)
(3)s 指令执行的结果影响CPSR的N,Z,C,V
(4)Rd 目标寄存器(r0-r15)
(5)Rn 第一个操作数,必须是寄存器(r0-r15)
(6)Rm 第二个操作数,有多种形式
[1]立即数,表示: #数字
[2]寄存器
[3]寄存器移位 lsl(逻辑左移) ,lsr(逻辑右移), asr(算术右移)
问题:合法立即数?
如果可以拿一个8bit数字[0~255]循环右移偶数位得到当前的数字,则当前数字为合法立即数
问题:寄存器移位?
r0,lsl #4 => r0 << 4
r0,lsr #2 => r0 >> 2
2.数据传送指令
指令格式:MOV/MVN Rd,Rm
mov r0,#5 => r0 = 5
mvn r0,#5 => r0 = ~5
mov r0,r0,lsl #4 => r0 = r0 << 4
mov r0,#0x12345678
错误,因为0x12345678不是合法立即数
问题:如何将任意数字装载到寄存器?
ldr Rd,=数字
ldr r0,=0x12345678 [正确]
3.数据运算指令
ADD/SUB/MUL Rd,Rn,Rm
ADD r0,r1,r2 => r0 = r1 + r2
ADD r0,r0,r1,lsr #4 => r0 = r0 + (r1 >> 4)
SUB r0,#5,r1 [错误:第一个操作数必须是寄存器]
ADD r0,r1,#5,lsl #4[错误:只能是寄存器移位,不能是立即数]
MUL r0,r1,r2 => r0 = r1 * r2
MUL r0,r1,#4 [错误:对于乘法指令,所有的操作数必须是寄存器]
MUL r1,r1,r0 [错误:对于乘法指令, 第一个操作数寄存器不能与目标寄存器相同]
练习:0x1235 + 3 * 4 - 8,最终结果存放在r0中
mov r1,#3
mov r2,#4
mul r0,r1,r2
sub r0,r0,#8
ldr r1,=0x1235
add r0,r0,r1
4.位运算指令
AND/ORR/EOR/BIC Rd,Rn,Rm
AND r0,r0,r1 => r0 = r0 & r1
ORR r0,r0,r1,lsl #4 => r0 = r0 | (r1 << 4)
BIC r0,r0,r1 => r0 =r0 & ~r1
BIC r0,r0,#3 => r0 = r0 & ~3 清除了r0的低两位
问题:如何清除r0的[5-8]位(从第0bit开始),其他位不变?
mov r1,#0xf
#r0 =r0 & ~(r1 << 5)
bic r0,r0,r1,lsl #5
练习:
(1)0x12345678,将它的高16bit和低16bit相加结果存放在r0中
(2)将r0的14-12位变成101,其他位不变
x= 0x12345678;
r0 = (x >> 16) + (x & 0xffff)
r0 = r0 & ( ~(7 << 12)) | (0x5 << 12)
-------------------------------------------------------------------------
ldr r0,=0x12345678
ldr r1,=0xffff
and r2,r0,r1 //获得低16bit
//r1 = r0 >> 16
mov r1,r0,lsr #16//获得高16bit
add r0,r1,r2 //高和低相加
mov r3,#7
//r0 =r0 & ~(r3 << 12)
bic r0,r0,r3,lsl,#12 //将r0的[14:12]清除
mov r4,#5
orr r0,r0,r4,lsl #12 //将r0的[14:12]设置为101
5.比较指令
指令格式:CMP Rd,Rm
注意:CMP指令比较的结果会自动影响CPSR的NZCV位 (也即不需要添加S来影响标志位)
cmp r0,r1
cmp r0,#4
cmp r0,r1,lsl #4
练习: a:r0
if(a == 4 || a == 10) [C语言的短路特性]
a ++;
cmp r0,#4
cmpne r0,#10
addeq r0,r0,#1 [条件:最近影响CPSR的N,Z,C,V位]
if(a > b)
a ++;
else
b ++;
cmp r0,r1
addgt r0,r0,#1
addle r1,r1,#1
6. 跳转指令
指令格式:B/BL 标签
注意 :
[1].跳转范围 +/- 32M
[2].BL在跳转到指定标签之前,会将pc的值保存到lr寄存器
问题:如何实现长跳转(>32M)?
直接给pc赋值[将地址直接赋值给pc]
练习:求1 + 2 + ... + 100 累加和?
int sum = 0,i;
for(i = 1;i <= 100;i ++)
{
sum = sum + i;
}
---------------------------------------------------------------------
mov r0,#0
mov r1,#1
loop:
cmp r1,#100
addle r0,r0,r1
addle r1,r1,#1
ble loop
或
loop:
add r0,r0,r1
add r1,r1,#1
cmp r1,#100
ble loop
7.内存访问指令
指令格式:ldr/str Rd , [Rn]
ldr r0,[r1] =>将r1保存地址中的4byte读到r0中 => r0 = *r1
str r0,[r1] =>将r0的值写到r1保存的地址 => *r1 = r0
data:r0 , p:r1
data = *p; =>ldr r0,[r1] =>读内存
*p = data; =>str r0,[r1] =>写内存
----------------------------------------------------------------------------------------------
常用的写法:
ldr/str r0,[r1] =>r0 = *r1 / *r1 = r0
ldr/str r0,[r1],#4 =>r0 = *r1 && r1 = r1 + 4 / *r1 = r0 && r1 = r1 + 4
ldr/str r0,[r1,#4] =>r0 = *(r1 + 4) / *(r1 + 4) = r0
ldr/str r0,[r1,#4]! =>r0 = *(r1 + 4) && r1 = r1 + 4 / *(r1 + 4) = r0 && r1 = r1 + 4
--------------------------------------------------------------------------------------------
data:r0 p:r1
data = *p ++; =>ldr r0,[r1],#4
data = *(p + 4); =>ldr r0,[r1,#4]
*p ++ = data; =>str r0,[r1],#4
思考:将0x20000开始的地址,存放0x00,0x10,0x20,0x30,..,0x90
int i = 0;
int *p = (int *)0x20000;
for(i = 0;i < 10;i ++)
{
*p++ = i << 4;
}
------------------------------------------------------------------------------------
mov r1,#0x20000
mov r0,#0
loop:
mov r2,r0,lsl #4
str r2,[r1],#4
add r0,r0,#1
cmp r0,#9
ble loop
练习:
(1)向0x20000地址写入0x00,0x11,0x22,....,0x99
(2)将0x20000开始地址的10个数据拷贝到0x40000
mov r1,#0x20000
mov r0,#0
mov r2,#0
loop1:
str r0,[r1],#4
add r2,r2,#1
orr r0,r2,r2,lsl #4 //r0 = r2 | (r2 << 4)
cmp r0,#0x99
ble loop1
------------------------------------------------------------------------------
mov r1,#0x20000
mov r2,#0x40000
mov r3,#10
loop2:
ldr r0,[r1],#4
str r0,[r2],#4
sub r3,r3,#1
cmp r3,#0
bgt loop2
或:
loop2:
ldr r0,[r1],#4
str r0,[r2],#4
subs r3,r3,#1
bgt loop2
8. 多寄存器装载指令
LDM/STM{cond}<mode> Rn{!},{reglist}^
LD:loadr ST:store M:many
(1)cond 指令执行的条件(le,get,....),可选
(2)mode IA,IB,DA,DB
I :基地址寄存器向地址大的方向移动
D :基地址寄存器向地址小的方向移动
A :先操作数据,后操作地址
B :先操作地址,后操作数据
IA:increase after
IB:increase before
DA:decrease after
DB:decrease before
(3)Rn 基地址寄存器:它的初始值为内存地址
(4)! 最后更新基地址寄存器值
(5)reglist {r0,r5,r8-r12}
(6)^ 寄存器列表中没有pc寄存器的时候,此时操作的寄存器用户模式的寄存器
对于LDM指令,寄存器列表有pc的时候,除了正常寄存器传送外,还将异常模式SPSR拷贝到CPSR,
用户异常返回
例如:r10的值为0x20000 ,r0:100,r1:200,r2:300,r5:400
stmia r10!,{r0-r2,r5}
| | 大
| |r10
| 400 |
| 300 |
| 200 |
| 100 |0x20000 小
| |
思考:如何读取上面内存的值到r4-r6,r8?
ldmdb r10!,{r4-r6,r8}
r8:400 r6:300 r5:200 r4:100
注意:
[1]寄存器列表中的寄存器编号,一定是从小到大
[2]大地址对应大寄存器编号,小地址对应小寄存器编号
9.堆栈操作指令
ldm<mode>/stm<mode> sp{!},{reglist}
mode:FD,FA,ED,EA
F:full 满 [压栈结束后,SP指向有效数据的地址]
E:empty 空 [压栈结束后,sp指向无效数据的地址]
A:add 递增 [sp向大地址移动]
D:del 递减 [sp向校地址移动]
ATPCS标准规定堆栈操作的方式为:满递减栈FD
stmfd sp!,{r0-r12} //进栈
ldmfd sp!,{r0-r12} //出栈
注意:对于堆栈操作,怎么进栈怎么出栈
六、常用的ARM伪指令
ARM伪指令:编译器设计,目的是为了操作灵活,编译器最终会将其翻译成ARM核所能理解的指令
_start:
ldr r0,=0x12345678 //将0x12345678装载到r0
ldr r1,=lable //将lable标签表示的地址装载到r1
ldr r2,lable //将lable标签表示的地址内容装载到r2
adr r3,lable //将pc +/- label所在的偏移量
lable:
.word 0x11223344
Disassembly of section .text:
00020000 <_start>:
20000: e59f000c ldr r0, [pc, #12] ; 20014 <lable+0x4>
20004: e59f100c ldr r1, [pc, #12] ; 20018 <lable+0x8>
20008: e59f2000 ldr r2, [pc, #0 ] ; 20010 <lable>
2000c: e24f3004 sub r3, pc, #4
00020010 <lable>:
20010: 11223344 teqne r2, r4, asr #6
20014: 12345678 eorsne r5, r4, #125829120 ; 0x7800000
20018: 00020010 andeq r0, r2, r0, lsl r0
思考:如何知道当前代码是在IRAM运行还是在DDR2运行的基地址?
adr r3,_start
如果是在IRAM运行:r3 -> 0x20000
如果是在DDR2运行:r3 -> 0x2000,0000
七 、汇编与C语言混合编程
(1)ATPCS标准规定
[1]参数传递
参数小于等于4个时候,通过r0~r3来传递
参数大于4个时候,前面4个参数通过r0~r3传递,后面参数通过压入栈中传递
[2]函数返回值带回
函数返回值小于32bit的时候,通过r0带回 ,如果大于32bit,则通过r0,r1带回,依次类推
注意:汇编在调用C语言代码,必须先设置SP