V1.0:初始版本、读完《ARM体系结构与编程》后的一个小总结
时间:2021-10-19
基本知识
arm版本变种
- T:thumb指令集
- M:长乘法指令集(V5版本后、均带有)
- E:增强型DSP指令集
- J:java加速器
如armv5TEJ
了解下即可;
处理器模式
arm有7种模式
- 用户模式
- 系统模式
- 快速中断模式
- 外部中断模式
- 特权模式
- 数据访问中止模式
- 未定义指令模式
Linux主要用了2中模式;用户模式和系统模式
寄存器组
-
用户模式:R0-R15、CPSR 17个
-
系统模式:没有自己的寄存器,和用户的共用
-
快速中断模式:有自己的R8_fiq-R14_fiq、SPSR_fiq;8个
-
外部中断模式:有自己的R13_irq(SP)、R14_irq(LR)、SPSR_irq;3个
-
特权模式:有自己的R13_svc(SP)、R14_svc(LR)、SPSR_svc;3个
-
数据访问中止模式:有自己的R13_abt(SP)、R14_abt(LR)、SPSR_abt;3个
-
未定义指令模式:有自己的R13_und(SP)、R14_und(LR)、SPSR_und;3个
用户模式和系统模式共用R-0-R15、SPSR;17个
除了快速中断模式,其余4种模式均只有自己的R13、R14、SPSR;4*3=12个
快速中断模式还有自己的R8-R12;5+3 = 8个
一个17 + 12 + 8 + = 37个寄存器。
状态寄存器CPSR
控制处理器的状态,如模式、条件等
条件标志位(高4位)
- N:=1,表示负数
- Z:=1,表示结果=0
- C:加法:=1,发送进位;减法:=0,发送借位
- V:=1,发送符号位溢出
基本上ARM的每条指令都支持条件执行、可以后面添加一些条件标志,如 BEQ,相等时才转跳;BLT,小于的时候才转跳
4位、有16种组合,其相应的助记符如下、其中b1111保留:
条件吗 | 助记符 | 标志 | 含义 |
---|---|---|---|
b0000 | EQ | Z = 1 | == |
b0001 | NE | Z = 0 | != |
b0010 | CS | C = 1 | ≥(无符号) |
b0011 | CC | C = 0 | <(无符号) |
b0100 | MI | N = 1 | < 0 |
b0101 | PL | N = 0 | ≥ 0 |
b0110 | VS | V = 1 | 溢出 |
b0111 | VC | V = 0 | 未溢出 |
b1000 | HI | C=1、Z=0 | >(无符号) |
b1001 | LS | C=0、Z=1 | ≤(无符号) |
b1010 | GE | N = V | ≥(有符号) |
b1011 | LT | N != V | <(有符号) |
b1100 | GT | Z=0、N=V | >(有符号) |
b1101 | LE | Z=1、N!=V | ≤(有符号) |
b1110 | AL | 无条件 |
总结:
无符号比较
助记符 | 含义 |
---|---|
CS | > |
CC | < |
HI | ≥ |
LS | ≤ |
有符号比较
助记符 | 含义 |
---|---|
GT | > |
LT | < |
GE | ≥ |
LE | ≤ |
判断
助记符 | 含义 |
---|---|
EQ | == |
NE | != |
MI | <0 |
PL | ≥0 |
VS | 溢出 |
VC | 未溢出 |
AL | 无条件转跳 |
控制位(低8位)
I[7](IRQ失能位)
IRQ禁止位;=1,禁止IRQ中断
F[6](FIQ使能位)
FIQ进制位;=1,禁止FIQ中断
T[5](指令执行状态)
控制指令执行状态;T = 0,在执行ARM指令
T = 1:
版本>= V4:执行Thumb指令
版本>= V5:强制下一条指令产生未定义指令中断
M[4:0](模式控制)
M[4:0] | 模式 |
---|---|
b10000 | User |
b10001 | FIQ |
b10010 | IRQ |
b10011 | Supervisor |
b10111 | Abort |
b11011 | Undefined |
b11111 | System |
指令集
ARM指令构成
ARM指令都是32位;主要由一下几个部分构成:
- opcode:指令操作符编码
- cond:条件码(前面提到的NE、EQ…)
- S:是否影响CPSR的值(可以才指令后加S觉得,如ADDS)
- Rd:目标寄存器
- Rn:包含第一个操作数的寄存器编码(有的指令有多个源寄存器)
- shifter_operand:第二个操作数
一般结构:<opcode>{<cond>}{S} <Rd>,<Rn>,<shifter_operand>
其中 花括号里面的是可选,其余是必选。
寄存器移位方式
- ASR:算数右移
- LSL:逻辑左移
- LSR:逻辑右移
- ROR:循环右移
- RRX:拓展循环右移
数据处理指令操作数寻址方式
有11种寻址方式:
寻址方式 | 示例 | 含义 |
---|---|---|
#<imm> | MOV R0,#1 | R0=1 |
<Rm> | MOV R0,R1 | R0=R1 |
<Rm>, LSL #<shift_imm> | MOV R0,R1,LSL #3 | R0=R1*(2^3) |
<Rm>, LSL <Rs> | MOV R0,R1,LSL R2 | R0=R1*(2^R2) |
<Rm>, LSR #<shift_imm> | MOV R0,R1,LSR #3 | R0=R1/(2^3) |
<Rm>, LSR <Rs> | MOV R0,R1,LSR R2 | R0=R1/(2^R2) |
<Rm>, ASR #<shift_imm> | MOV R0,R1,ASR #3 | 算数右移3位 |
<Rm>, ASR <Rs> | MOV R0,R1,ASR R2 | 算数右移R2位 |
<Rm>, ROR #<shift_imm> | MOV R0,R1,ROR #3 | 循环右移3位 |
<Rm>, ROR <Rs> | MOV R0,R1,ROR R2 | 循环右移R2位 |
<Rm>, RRX | MOV R0,RRX |
操作内存地址的寻址方式
- [Rn, #±offest_12]{!}
- 如:LDR R0,[R1,#4];读取R1+4内存单元的字到R0
- 如:LDR R0,[R1,#-4]!;读取R1±4内存单元的字到R0,同时R1 = R1 - 4
- [Rn, ±Rm]{!}
- 如:LDR R0,[R1,R2];读取R1+R2内存单元的字到R0
- 如:LDR R0,[R1,R2]!;读取R1+R2内存单元的字到R0,同时R1 = R1 + R2
- [Rn, ±Rm,shift #shift_imm]{!}
- LDR R0, [R1,R2,LSL #2];R0 = (R1 + R2*2^2)内存单元的值
- 后跟!执行完后更新Rn的值
- shift 可从上面@寄存器移位方式 进行选取
- [Rn], #±offest
- 事后访问,先访问[Rn],然后更新Rn
- LDR R0,[R1],#4;事后访问,R0=R1,然后R1 = R1 + 4
- [Rn], ±Rm
- 同上
- [Rn], ±Rm, shift, #shitf_imm
- 同上
杂类修饰符
一般都可以跟在指令最后面:
- S:决定指令是否影响CPSR标志位
杂类读写(LDR|STR)
- SB:带符号字节操作;如LDRSB,将高24位设置成符号位
- SH:带符号半字操作,如LDRSH,将高16位设置成符号位;
- H:半字操作,如STRH R0,[R1], #8,将R0的低16存待R1内存单元,然后R1 += 8
- D:双字操作
批量读写(LDM|STM)
- IA:事后递增,LDMIA SP!,{R0-R1};将SP、SP+4,2个字读到R0和R1中,然后更新SP(如果SP后没有!,则不会更新SP)
- IB:事前递增,LDMIB SP,{R0-R1};将SP+4、SP+8,2个字读到R0和R1中,不更新SP(因为SP后没有!)
- DA:事后递减,LDMDA SP!,{R0-R1};将SP、SP-4,2个字读到R0和R1中,然后更新SP(如果SP后没有!,则不会更新SP)
- DB:事前递减,LDMDB SP,{R0-R1};将SP-4、SP-8,2个字读到R0和R1中,不更新SP(因为SP后没有!)
栈读写(LDM|STM)
- FD:满减栈,先赋值,在SP–(这是ARM默认的栈方式);LDMFD
- ED:空减栈,先SP–,在赋值;最后栈空间有一个单元没用上;STMED
- FA:满增栈,先赋值,再SP++
- EA:空增栈,先SP++,再赋值
基本上ARM的栈都是满减(FD)
note
- LDR R1 [R2] :方向 R1<- [R2]
- STR R1 [R2]:方向 R1 -> [R2]
- LDM:LDMIA SP! {R2} 方向:SP -> R2…; (和LDR方向相反)
- STM:STMFD SP! {R0} 方向:R0… -> SP; (和STR 方向相反)
一般一个函数:
stmfd sp! {r1,r2-rx...} ;保存要用到的寄存器旧值
;或
push {r1,r....}!
;然后是中间的具体指令
ldmia sp! {r1,r2-rx...} ;恢复寄存器原来的值
详细指令集
- S:决定指令是否影响CPSR标志位,跟在指令最后,如MOVS,ADDS,ADCS…
下面仅仅简练列举指令大概意思(具体太多了记不住)、详细请百度。
转跳指令
可以向前、向后跳32M的程序空间:
- B:仅转跳
- BX:转跳,并根据目标地址的bit[0]确定是ARM还是Thumb指令
- BL:转跳,并将返回地址复制到LR
- BLX:实现BL+BX的功能
数据传送
- MOV:MOV{cond}{S} Rd,shift_oprend…
- MVN:将操作数的反码传输到目标寄存器
逻辑运算
- ADD:ADD Rd,Rn,operand;Rd = Rn + oprand
- ADC:带进位加法
- SUB:减法
- SBC:带借位减法
- RSB:逆向检测;RSB Rx,Rx,#0(Rx = 0 - Rx)
- RSC:带借位逆向减法
- AND:与
- BIC:位清除;bit Rx ,Rx, 0x03;将低2位清零
- EOR:异或
- ORR:或
比较(影响CPSR标志位)
- CMP:比较(目标寄存器减源寄存器)status = op1 - op2
- CMN:比较取负数:status = op1 - (-op2)
- TST:测试位:status = op1 and op2
- TEQ:测试等价位:status = op1 eor op2
状态访问
- MRS:读状态寄存器;MRS R0 ,SPSR
- MSR:写状态寄存器;MSR R0,CPSR
内存访问
- LDR{B|BT|H|SB|SH|T}:读取指令
- STR{B|BT|H|T}:读取指令
- LDM:批量读取指令:LDM{cond}<addr_mode> <Rn>{!}, <regisgers>
- 花括号里面位可选项
- 条件码参考前面条件标志位章节
- 地址模式参考前面批量读写、栈读写章节
- {!},表示写入后更新Rn的值
- STM:批量写入;各位用法参考上面LDM
中断指令
- SWI:软件中断指令;SWI 立即数(24位)
- BKPT:断点中断指令;用于产生软件断点中断
协处理器指令
ARM支持16个协处理器、然后每个处理器又有自己的寄存器。
主要有5条指令:
- CDP:协处理器数据操作;通知协处理器完成特定操作,失败会产生指令未定义异常
- CDP 协处理器编号,操作码1,目标寄存器(协处理器),源寄存器(协处理器),源操作数(协处理器),操作码2
- 如:CDP p5,2,c12,c10,c3,4:通知p5协处理器进行初始化;操作码1=2,目标寄存器=c12,源操作数是c10和c3,操作码2=4
- LDC:协处理器数据读取;协处理器从内存读取数据
- LCD p6,c4,[R2,#4]:读取内存单元(R2+4)的字数据到协处理器p4的c4寄存器
- STC:数据写入;协处理器写数据到内存
- STC p8, CR8,[R2,#4];将协处理器p8的CR8寄存器的字数据写到内存单元(R2+4)
- MCR:ARM寄存器 -> 协处理器寄存器
- 格式:MCR 编号,op1,源操作数(主寄存器),目标寄存器1,目标寄存器2,op2
- MCR p14,3,R7,c7,c11,6:操作码1和2为3,6;源操作数放在R7,目标寄存器c7,c11
- MRC:ARM寄存器 <- 协处理器寄存器;格式同MCR
杂类指令
- SWP:交换指令;SWP Rd,Rm,[Rn];将Rn内存内容->Rd,Rm->[Rn],当Rn=Rm则相当于交换内容
- SWP R1,R1,[R2]:R1寄存器内容和内存单元(R2)交换
- SWPB:同SWP,只是交换字节
其中
- T表示用户模式,B表示读字节,S表示有符号(默认无符号)。
- 读无符号数是时候、没用到的高位会清零,如LDRB,寄存器高24位会清零
- 读有符号数的时候、会进行符号位拓展(没用到的高位=符号位)
存储系统
主要是ARM中的协处理器CP15。因为它管理CPU的存储系统(MMU和TLB)
CP15
访问CP15只有2种指令:MCR和MRC。且操作码1都永远为0;也就是开头永远只有以下2种:
- MCR p15 , 0 , Rx , cx , cx , xx
- MRC p15 , 0 , Rx , cx , cx , xx
op2用于区分同一个编号的不同物理寄存器,否则为0,所以一般情况下,op2也为0。
C0寄存器(RO)
只读、存储ID编码、cache信息(cache大小、类型)
C1寄存器
控制寄存器:控制MMU和存储系统的工作模式等
其他寄存器
略(太多了、不记得),用到的时候在搜索把。
MMU
实现地址转换(虚拟地址->物理地址)、还有一些权限控制
和MMU相关的CP15的寄存器:
- C1某些位:配置MMU
- C2:页表基址
- C3:域的一些属性,MMU支持16个域
- C5:访问失效的状态
- C6:失效的地址
- C8:TLB相关
- C10:TLB相关
快大小
ARM支持以下4种大小(个人觉得应该是ARM32,64位就不一定了)
- 超级段(Supersection):16M
- 段(section):1M
- 大页(Large Page):64K
- 小页(Small Page):4K
页表
在ARM中使用2级页表机制。
通常如果单位是段的话、一级页表就可以了。
以页位单位的话需要二级页表。
一级页表
一级页表使用了 短描述符页表(Short-descriptor translation table);页表项有以下特征:
- PTE为32bit
- 有2级以上页表
- 支持32bit物理地址
- 支持4种内存(16M、1M、64K、4K)
对于一级页表项:低2位标识自己的状态。
- b00:无效页表
- b01:指向二级页表
- b10:已映射,bit[18] = 0为段、映射大小是1M(高12位为物理基址)
- b10:已映射,bit[18] = 1为超级段、映射大小是16M(高8位物理基址)
二级页表
对于二级页表项:低2位标识自己的状态。
- b00:无效页表
- b01:已映射,为大页(64K)
- b1X:已映射,为小页(4K)
访问无效页表会触发指令取指异常或数据取指异常
地址转换过程:
虚拟地址->一级页表项->二级页表项->物理地址
- 一级页表页表项 = 页表基址 |(虚拟地址高12位 >> 20)*4
- 二级页表页表项 = 一级页表页表项的二级页表基址 | 虚拟地址[19:12]中间8位
- 物理地址 = 二级页表描述符的物理地址基址 | 虚拟地址[11:0]
内存属性
每个内存区域都有自己的权限;内存的权限主要是由页表项(PTE)中的AP、APX、Domain 3个字段来共同控制。
AP、APX:组成成不同的访问权限,主要是对不同模式下内存的权限进行控制;如设置某块内存只能特权模式读写、用户模式只能读等
Domain:ARMMMU可以将内存分成16个域、不同的域由自己的访问权限、由以下3种:
- 不可访问:任何访问都会引发异常
- 管理者:谁都可以访问、不会引发异常
- 用户:使用AP和APX来进行权限控制
内存类型
ARM里有3种:
- Normal:还分为可共享和非可共享 、可以被缓存
- Device:不可被缓存
- Strongly-ordered:不可被缓存
由页表项的 TEX、C、B字段进行设置。
ASID
主要用于减少进程切换的开销。
每次切换进程都需要吧TLB进行清空、切换效率较低。
ASID的范围是0-255;
作用:
-
如果 当前进程的ASID,那么 MMU 在查找 TLB 时, 只会查找 TLB 中具有 相同ASID值 的 TLB行
-
进程切换的时候、设置了 ASID 的 TLB行 不会被清理掉,当下次切换回来的时候还在、不需要清除所有数据、大大减少切换开销
使能
控制位:CP15的C1的bit[0]
;enabled mmu
MRC p15,0,r0,c1,0,0
ORR r0,#1
MCR p15,0,r0,c1,0,0
TLB
快速版的PTE、可以算是页表的cache
无效(invalidata)
当页表的项目该表的时候、需要告诉TLB某个表项或者全部标识成无效(即需要重新到页表中读取映射关系)
C8协处理器就是用来清除TLB相干操作的
全部无效
;Rd应该为0
MCR p15,0,Rd,c8,c7,0 ;使cache全部无效
MCR p15,0,Rd,c8,c5,0 ;整个 指令cache 无效
MCR p15,0,Rd,c8,c6,0 ;整个 数据cache 无效
单个无效
;Rd存放对应虚拟地址
MCR p15,0,Rd,c8,c7,1 ;使统一cache某个虚拟地址 无效
MCR p15,0,Rd,c8,c5,1 ;指令cache某个虚拟地址 无效
MCR p15,0,Rd,c8,c6,1 ;数据cache某个虚拟地址 无效
锁定
就是将地址映射关系存或者说缓存到快表上、这中操作成为块表
C10寄存器用来控制TLB的锁定
具体操作还没看懂、这里先了解原理
cache
CPU速度很快、内存速度很慢(相对于CPU)。所以出现了Cache、它的速度略慢于CPU、快于内存,根据局部性原理,所以加入cache能大大提升存储系统的性能。
但事无完美,Cache有以下问题:
- 容量较小,速度快意味着昂贵;怎么样将其映射到比他大得多的存储系统上呢?
- 一致性问题,数据被缓存了,但是某些情况下改变了、缓存没即使更新,导致CPU仍使用旧的数据。
最小单位:快(cache line),类似于Linux内存管理里面的页。
所以对于Cache、一个虚拟地址由B和W2部分组成:B=块号、W=块内地址
ARM的协处理器CP15的
C1寄存器的某些位控制着Cache的使能功能;
C7寄存器控制着Cache的清空、无效操作;
C9寄存器控制着Cache的锁定操作;
地址映射和变换
3种映射方式:
- 全相联映射和变换方式:任意一个地址都能映射到Cache任意位置(跟去教师上课一样、位置随便坐,先到先得)
- 直接映射和变换方式:主存的某一块地址只能映射到Cache的一个特定块中;
- 如 b = B mod Cb;b为Cache的块号、B为主存的快号,Cb为Cache的容量
- 组相联映射和变换方式:前两种方法的折中、调节一个中间对象组(Set)
- 对主存和Cache分组,组大小相同
- 主存的组->Cache的组:直接映射
- 组与组建立映射后、组内全相联映射
一个组的块数成为组容量,当
组容量 = 1 = 直接映射(因为一个组只有一个块、组退化成了块、组间直接映射)
组容量 = Cache块数 = 全相联映射(此时只有一个组、因为所有的块都在这个组、组内全相联映射)
一块内存只能访问Cache的一个特定组、但是组内的块能随便映射。
如:主存 1M;Cache 64K
假设分组大小为 1K
主存有:1M / 1K = 1024 组;Cache有:64K / 1K = 64组
组间直接映射:
主存组号 = 地址 / 1K
组号 = 主存组号 mod 64;
也就是 0K、(0+64)K、(0+2*64)K…等只能映射到第0个组
映射的时候、先找到能映射到的组、在进行具体映射;
组号 = (地址 / 1K) % 组数
在这个组里找一个空闲的进行映射
分类
当CPU更新了Cache的内容,要写回到主存,通常有2种方法:
- 写通Cache:更新Cache的时候、同时更新主存
- 写回Cache:只更新Cache、更Cache被替换的时候、才更新主存
从读的角度
当Cache未命中的时候、需要从主存读数据,此时有2种策略:
- 读分配Cache:写数据未命中的时候,将数据写入主存,读取的时候才缓存
- 写分配Cache:写数据未命中的时候,将数据读到Cache,进行修改
ATPCS
一个约定;规定了子程序间调用的基本规则。
寄存器使用规则
- R0-R3:用来传递参数、被调用者无需保护这些寄存器
- R4-R7:保留寄存器、子程序要保证调用前后寄存器的内容不变(使用前入栈存起来、用完后出栈恢复)
- R12:用作子程序间备份寄存器
- R13、14、15:分别用作SP、LR、PC
参数传递规则
入口参数:
- 参数<=4个:用R0-R3来传递
- 参数>4个:多出来的用栈传递、并从右到左入栈
返回参数:
- 32位:用R0
- 64位:用R0-R1
- 浮点:用f0、d0或s0
- 更多的位:用内存
异常中断
一般情况的,PC = 当前执行指令 + 2条;ARM是执行完指令后(此时PC已更新:PC = 当前执行指令 + 3条)才查询中断状态
所以:发生IRQ、FIR和数据访问中止异常的时候(PC = 当前执行指令 + 3条)、硬件会将(PC-4)放到对面的LR_mode寄存器、如果要返回还需要再减4;
SUBS PC,LR,#4
但是,但是,但是:
SWI、未定义指令异常、指令预取中止异常:PC = 当前执行指令 + 3条;因为中断是由指令产生的、此时PC还没更新。
ARM映像文件(ELF)
组成
- 域:一个映像文件由多(1-3)个域组成;
- 输出端:>=1个 输出段 组成 域;输出端包含一系列属性相同的输入端
- 输入段:>=1个 输入段 组成 输出端;输入段包含目标文件的代码和数据
连接器需要直到以下信息才能生成对应映像文件
- 分组信息:觉得如何将输入段组成输出段和域
- 位置信息:各个域要加载到哪
入口
有2种入口地址:
- 初始入口地址:一个映像文件只有一个(条件:必须位于运行时域、加载地址=运行时地址)
- 入口地址:由汇编中的ENTRY来定义;标识该段代码通过异常中断进入;可以有多个
对于加载地址=运行时地址的域、又称为固定域(root region)
连接控制文件
使用scatter对映像文件的地址映射进行控制;个人接的和gcc的lds文件功能差不多。
输入域的一些属性:
- RO-CODE、CODE
- RO-DATA、CONST
- RO、TEXT(CODE+CONST)
- RW-DATA
- RW、DATA(RW-DATA+RW-CODE)
- ZI、BSS
一个加载时域、3个连续运行时域:
LR_1 0x08000 ;定义加载时域、名称=LR_1 起始地址=0x08000
{
ER_RO +0 ;定义第一个运行域 起始地址=0x08000
{
*(+R0) ;该运行时域包含所有RO属性的输入段
}
ER_RW +0 ;定义第一个运行域 起始地址=上一个域结束地址的下一个地址
{
*(+RW)
}
ER_ZI +0
{
*(+ZI)
}
}
一个加载时域、3个不连续运行时域
LR_1 0x08000 ;定义加载时域、名称=LR_1 起始地址=0x08000
{
ER_RO +0 ;定义第一个运行域 起始地址=0x08000
{
*(+R0) ;该运行时域包含所有RO属性的输入段
}
ER_RW 0x40000 ;定义运行域 起始地址=0x40000
{
*(+RW)
}
ER_ZI +0
{
*(+ZI)
}
}
两个加载时域、3个不连续运行时域
LR_1 0x08000 ;定义加载时域、名称=LR_1 起始地址=0x08000
{
ER_RO +0 ;定义第一个运行域 起始地址=0x08000
{
*(+R0) ;该运行时域包含所有RO属性的输入段
}
}
LR_1 0x04000 ;定义加载时域、名称=LR_1 起始地址=0x08000
{
ER_RW +0 ;定义运行域 起始地址=0x40000
{
*(+RW)
}
ER_ZI +0
{
*(+ZI)
}
}