arm的模式
arm的中断
cpu
的工作就是执行给定的机器指令,也就是它从内存fetch
指令后经过decode
得到机器码后便开始执行,从这个角度上讲cpu
是没有所谓用户
或者系统
等区别的,一视同仁,但显然这样的硬件设计并不灵活,这意味着“无论是谁”都有资格来操纵硬件,这并不科学也并不安全,这显然破坏了硬件与软件的整体性稳定性和安全性,因此,站在CPU
的角度上讲有必要对执行设限,不能让任何来源的代码都能控制操纵硬件或者发出指令,这从cpu
硬件上就有了权限
之分。
cpu
从硬件上设定了几个级别,不同的级别拥有不同的资源访问操作能力,arm
架构的cpu
一共划分了3
个权限级别:
图1.arm处理器的9种模式
arm架构
一共有PL0
、PL1
、PL2
三个权限级别,但在cpu
的实际设计中并不一定会有PL2
级别的权限,因此通常我们可以认为有PL0
和PL1
两级权限,其中PL0
被称为User
非特权级别,PL1
(以及PL2
)被称为特权级别,用户代码工作在User
模式,操作系统代码工作在Supervisor
模式,用户层要切换到内核(操作系统)层需要调用指令scv
进入。
普通的应用程序就是运行在user
模式(PL0
)下,没有对硬件的直接访问权,所有的硬件操作都需要通过系统调用向内核进行申请,内核运行在特权级模式PL1
)下,对系统调用、中断、异常等系统事件进行响应、处理并返回,以这种隔离的方式保证了内核的安全。在 linux
内核的实现中,arm
处理器尽管实现了 IRQ
、Abort
等模式,当中断和异常发生时,硬件上会直接跳转到对应的模式下,但是其真正的处理却统一在 svc
模式下,比如发生中断时,只会在 IRQ
模式下作短暂停留,随后就跳转到 svc
模式下,由内核进行统一处理。
事实上,大多数的特权级模式都是因为系统触发异常而自动进入的,比如 IRQ
、FIQ
是因为产生了硬件中断,处理器强制性地修改 CPSR
模式位并跳转到相应的模式下执行程序,而 Undefined
、Abort
是因为产生系统错误,同样也是系统强制地修改 CPSR
,对于 svc
,在处理中断时,内核大多数情况下都运行在这个模式,在用户程序需要使用系统调用时,使用 svc
指令进入到 supervisor(管理者)
模式,进入内核。处理器当前的模式是由状态寄存器 CPSR
的 bits[4:0](这5bits被称为mode位)
来控制的,对于 use
r 模式而言,并没有权限操作 CPSR mode
位,只能通过 svc
汇编指令进入到 svc(Supervisor)
模式,对于其它 PL1
及以上的特权级而言,可以通过给相应的模式位赋值来切换到目标模式,比较常见的是 svc
切换到 system
模式,而对于其它自动进入的模式,并没有太多手动切换的需求。
armv7
架构的9种工作模式:
-
User
:用户进程运行在User
模式下,拥有受限的系统访问权限 -
FIQ
: 快中断异常处理模式,相对于中断而言,快中断拥有更高的响应等级、更低延迟。 -
IRQ
: 中断异常处理模式 -
Supervisor
: 内核通常运行在该模式下,在系统复位的时候或者应用程序调用svc
指令的时候将会进入到当前模式下,系统调用就是通过svc
指令完成 -
Abort
: 内存访问异常处理模式,常见的MMU fault
就会跳转到该模式下进行相应的处理 -
Undefined
:当执行未定义的指令时,触发硬件异常,硬件上自动跳转到该模式下 -
System
: 系统模式,这个模式下将与用户模式共享寄存器视图 -
Monitor
: 针对安全扩展,在该模式下执行secure
和non-secure
处理器状态的切换 -
Hypervisor
: 针对虚拟化扩展
不同的模式下有不同的资源处理权限以及不同的异常处理方式,实现这种隔离机制离不开寄存器,arm
架构是r0~r15
一共16个通用寄存器,其实,这只是一个寄存器接口,实际arm
架构存在不同的Bank寄存器 ,所谓bank
寄存器,即相同的寄存器名对应不同的寄存器实体。这其实就表示工作在不同模式下的CPU
虽然都有一个pc
寄存器,但实质却是不同的寄存器实体。
图2.不同模式下的寄存器
观察上图(图2)会发现:
arm
架构中sp
和lr
寄存器其实并不是只有2个,不同的模式下分别拥有不同的bank寄存器
,FIQ
、IRQ
、ABT
、SVC
、UND
、MON
这些模式都有各自不同的sp
和lr
寄存器,实质也就是意味这这些不同的工作模式都分别拥有各自的栈空间
,这能避免不同模式的切换而导致的sp、lr
数据的反复保存于取出,这里既有效率的原因又有安全的考量FIQ
模式下的bank寄存器
数量是最多的,所谓FIQ
就是fast interrupt request
即快速中断请求,既然是快速就要求效率,更多的实体寄存器能够更快的参数保存(参数直接保存在寄存器而非内存栈上),当然,为了实现这个fast
,还有其他的手段来保证更高的效率,如:
- 更高的优先级,
FIQ
和IRQ
同时到达时,FIQ
将会先响应并处理,在操作系统的软件实现中,应该支持FIQ
可以抢占IRQ
运行,同时FIQ
的处理不应该被任何其它异常所抢占。 -
FIQ
中断向量表的位置在最后,对于IRQ
的中断向量,只有四个字节,所以IRQ
的向量必然是一条跳转指令,而FIQ
在中断向量表的最后,FIQ
的中断处理代码可以直接放在中断向量表之后,避免跳转指令(当然这也由实现来决定)
FIQ
虽然强,不过在 linux
中,并不使用 FIQ
,所有的中断都只会被路由到IRQ
引脚上。
此外,我们还会发现在这些模式中,还有一个bank寄存器
,那就是SPSR_
,这类寄存器也是状态寄存器,全称saved program status register
,专门用来保存模式切换之前那个模式的状态寄存器数据(除了sys
和user
模式外,其他每个模式都有一个spsr
寄存器)。切换模式往往都伴随着cpsr或apsr
寄存器保存数据到spsr
寄存器,这一步是硬件自动完成的。
模式的切换伴随着跳转
arm
架构cpu
模式由状态寄存器(cpsr
)最后的5bits
控制,改变状态寄存器的最后5bis
便能切换工作模式,当然,User
模式下的指令是无权修改这些位置的值的,一般而言,模式的切换都是有目的的,要么是用户代码进行系统调用,从user
模式切换到svc
模式,从而执行更底层的代码,要么是异常中断伴随着异常处理,无论哪种情况都需要进行代码的跳转,只有跳转才能完成目标工作。
在处理器架构层面,软件中断、IRQ
和FIQ
被统称为异常,异常还包括 Abort
和`` undefined 指令等,在工作中最常接触的就是软件中断和
IRQ,在
linux 中,通常我们所说的软件中断就是通过
svc 指令发起,**软件中断即
svc **,是用户空间进入内核空间的**唯一通道**,也是
linux `实现系统调用的关键所在。
而IRQ
则是指硬件中断,由 CPU
上引出一条 IRQ
线,这条 IRQ
线通常连接到 GIC(中断控制器)
上,GIC
向下再连接各外设,当外设产生中断信号时,经由 GIC
传递到 CPU
的 IRQ
引脚上,在 CPU
执行指令的间隙会查看是否有中断产生,如果有,则跳转到中断向量表的位置执行相应的异常处理程序。处理器的 IRQ
可以通过 CPSR
状态寄存器的中断屏蔽位屏蔽掉 IRQ
。(如常见的鼠标键盘就是通过硬件中断与系统交互)。
FIQ(快速中断)
,相对于 IRQ
而言,FIQ
拥有更高的优先级,它可以抢占 IRQ
的执行,同时 FIQ
本身的执行速度比IRQ
要快一些,这类中断通常用在对响应时间有极高要求的系统中(实时系统),比如 armv7-R
系列的处理器中会使用到。FIQ
的快速执行一方面体现在它有更高的优先级,另一方面,它拥有单独的寄存器,省去了参数的压栈时间,且处于中断向量表的最后一项,其执行代码不需要经过跳转。(linux
系统并不支持快速中断,因为快速中断使得系统设计的复杂性增加,其提供的效率也伴随着硬件的发展而显得辉煌渐逝(老版本的linux
还是支持fiq
的))。
工作模式切换带来的跳转的设计机制是中断向量表
向量表中保存了一系列的跳转指令,当系统发生异常时,由处理器负责将程序执行流转到向量表中的跳转指令,最常见的就是中断向量,应用工程师只需要使用固定的函数名编写中断处理程序,在中断发生时该中断处理程序就会被自动调用,这背后的实现就是中断向量表的功劳。
offset | address | except mode |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
中断向量表
中断向量表既可以是在0x0
地址处也可以是在0xffff0000
地址处,不过0x0
地址位于用户空间,且也是空指针所在处,放在这里处理起来略微麻烦,因此很多时候linux
将中断向量表放在地址0xffff0000
处,在向量表中的最后一个元素是FIQ
的中断处理代码,而表中的其他地址处都是放的跳转指令,毕竟每项4字节的空间不可能放真正的中断处理代码,而FIQ
位于最后一项,这样就没有4个字节的限制,可以直接将处理指令就放到这个地址处(而不用担心覆盖后面的项目),这样设计也是为了提高FIQ
的中断处理效率。
当对应的exception event
发生时,系统会自动地修改 CPSR
状态寄存器,并跳转到上表中的地址执行指令,而软件上要做的,就是在该地址上放置对应的代码。
_irq: .word irq #arm架构中word占4字节 hword占2字节
irq:
get_irq_stack
irq_save_user_regs //保存断点
bl do_irq
irq_restore_user_regs //恢复断点
在汇编中标号表示标号之后的第一条指令的地址,比如,当发生 irq 中断时,处理器会强制跳转执行 ldr pc, _irq 这条指令
也就是说,中断向量表定义了在异常发生的那一刻,程序将要跳转执行的地址,这是由硬件自动进行设置的,异常发生时arm
处理器自动执行了这些操作:
- 将异常发生前所属模式的
CPSR(user下为APSR)
拷贝到异常发生后将要进入模式的SPSR_\<mode>
中,除了System
模式,其它所有PL1
特权级模式都有SPSR bank
寄存器,这个操作并不难理解,就是保存现场,方便在异常处理完成之后还原之前的CPSR
. - 将返回地址保存到
LR
寄存器中,返回地址自然是当前指令的下一条指令的地址,但是因为指令流水线的存在,PC
寄存器中保存的是当前指令地址 +8 处的指令(或者+4处),所以需要针对PC
做一个偏移,这个偏移并不是固定的,而是根据不同模式有不同的值 - 修改 CPSR 中的某些
bit
:
- 修改
CPSR
的mode
部分,修改为异常处理模式下对应的模式 - 设置
CP15
的TE bit
(协处理器也需要参与) -
J
(指令集模式)bit
被清除,同时E(大小端) bit
设置为EE(exception 大小端) bit
. 主要是根据预先的设置配置异常处理的指令集模式和大小端 - 设置
PC
指针到对应的异常模式向量处,执行软件定义的异常处理程序
以上这些就是异常发生时,硬件系统所做的处理,此外,还需要软件参与才能完成完整的中断处理流程,通常情况下在进入异常后软件需要保存断点信息,将异常发生前模式的所有寄存器保存在栈上,在异常返回时才能进行恢复.
进入异常流程处理完毕后,需要退出异常流程返回到之前的处理流程中去,那么退出异常流程的步骤是:
- 将
SPSR
中的值copy
到CPSR
中
对于
SPSR
的copy
并没有定义单独的指令,而是在操作PC
指针时,在指令后添加一个'S'
后缀即执行SPSR
的拷贝,在阅读源代码时这种操作很容易被人忽略,因为普通的指令也可以带S
后缀,表示当前操作是否更新CPSR
,另一种更新SPSR
的方式是在LDM
指令后添加^
后缀,也是一样的效果,但是在STM
指令后添加^
并不是同样的意思,这个需要注意
- 将保存在栈上的中断前模式的所有寄存器值恢复到寄存器中,返回程序断点(软件)
- 设置
PC
寄存器到异常发生前的返回地址,这时候返回地址并不一定还保存在lr
中,很可能lr
已经被程序其它部分挪用,这取决于实现
在切换模式时,需要将之前模式的相关上下文保存到当前模式的栈中,那么就可能需要访问之前模式的sp
寄存器的值,而sp
和lr
通常都是bank
类型,这就带来了一个问题,irq
模式下的sp
实际是sp_irq
,而user
模式下的sp
实际是sp_user
,那怎么将sp_user
的值保存到irq
模式的栈上呢?
arm
实现的方式就是直接启用特殊指令,可以实现跨模式的bank
寄存器操作。
stmdb r8, {sp, lr}^
stm* 指令后添加 ^,表示操作的不是当前模式下的 sp,lr,而是 USER 模式下的寄存器,这是取自内核中的一条指令.
中断属于异常的一种,在驱动和硬件等场合中有重要的应用,那么,当发生中断时,arm
处理器自动执行哪些操作呢:
- 当处理器接收到一个
IRQ
信号时,处理器可能位于user
和svc
两种模式下,和异常处理一样,先会将返回值保存到LR
,接着保存CPSR
到SPSR_IRQ
,需要注意的是,这里还有一个操作:设置CPSR
的I bit 为 1
屏蔽中断,直到软件上重新设置,才会接收新的中断,而软件上何时将中断打开决定了中断执行的策略,如果软件中的实现是在执行用户定义的中断服务函数之前打开中断,那么中断嵌套就可能发生,如果在执行完用户定义的中断服务函数之后打开,中断就不允许嵌套. - 指令跳转到
IRQ
对应的中断向量处,对应的中断向量处的执行代码有两种情况:针对svc
模式下发生中断的处理和针对user
模式下发生中断的处理 - 中断服务子程序中保存中断发生前模式的所有寄存器值到栈上,由软件实现
- 处理用户定义的中断处理程序,由软件实现
- 中断返回时,将
SPSR
拷贝到CPSR
,并将所有保存在栈上的寄存器值恢复到寄存器中,设置PC
返回到程序中
实际上 linux
的中断处理并不仅仅涉及到 IRQ
模式,在中断发生时仅仅是短暂地位于irq
模式下,真正的中断处理流程是在 svc 模式下执行的。