arm的模式 arm的中断

cpu的工作就是执行给定的机器指令,也就是它从内存fetch指令后经过decode得到机器码后便开始执行,从这个角度上讲cpu是没有所谓用户或者系统等区别的,一视同仁,但显然这样的硬件设计并不灵活,这意味着“无论是谁”都有资格来操纵硬件,这并不科学也并不安全,这显然破坏了硬件与软件的整体性稳定性和安全性,因此,站在CPU的角度上讲有必要对执行设限,不能让任何来源的代码都能控制操纵硬件或者发出指令,这从cpu硬件上就有了权限之分。

cpu从硬件上设定了几个级别,不同的级别拥有不同的资源访问操作能力,arm架构的cpu一共划分了3个权限级别:

arm架构 需要cpu吗 arm架构需要授权吗_中断向量表

图1.arm处理器的9种模式

arm架构一共有PL0PL1PL2三个权限级别,但在cpu的实际设计中并不一定会有PL2级别的权限,因此通常我们可以认为有PL0PL1两级权限,其中PL0被称为User非特权级别PL1(以及PL2)被称为特权级别,用户代码工作在User模式,操作系统代码工作在Supervisor模式,用户层要切换到内核(操作系统)层需要调用指令scv进入。

普通的应用程序就是运行在user模式(PL0)下,没有对硬件的直接访问权,所有的硬件操作都需要通过系统调用向内核进行申请,内核运行在特权级模式PL1)下,对系统调用、中断、异常等系统事件进行响应、处理并返回,以这种隔离的方式保证了内核的安全。在 linux 内核的实现中,arm处理器尽管实现了 IRQAbort 等模式,当中断和异常发生时,硬件上会直接跳转到对应的模式下,但是其真正的处理却统一在 svc 模式下,比如发生中断时,只会在 IRQ 模式下作短暂停留,随后就跳转到 svc 模式下,由内核进行统一处理。

事实上,大多数的特权级模式都是因为系统触发异常而自动进入的,比如 IRQFIQ 是因为产生了硬件中断,处理器强制性地修改 CPSR 模式位并跳转到相应的模式下执行程序,而 UndefinedAbort 是因为产生系统错误,同样也是系统强制地修改 CPSR,对于 svc ,在处理中断时,内核大多数情况下都运行在这个模式,在用户程序需要使用系统调用时,使用 svc 指令进入到 supervisor(管理者) 模式,进入内核。处理器当前的模式是由状态寄存器 CPSRbits[4:0](这5bits被称为mode位) 来控制的,对于 user 模式而言,并没有权限操作 CPSR mode 位,只能通过 svc 汇编指令进入到 svc(Supervisor) 模式,对于其它 PL1 及以上的特权级而言,可以通过给相应的模式位赋值来切换到目标模式,比较常见的是 svc 切换到 system 模式,而对于其它自动进入的模式,并没有太多手动切换的需求。

armv7架构的9种工作模式:

  • User:用户进程运行在 User 模式下,拥有受限的系统访问权限
  • FIQ : 快中断异常处理模式,相对于中断而言,快中断拥有更高的响应等级、更低延迟。
  • IRQ : 中断异常处理模式
  • Supervisor : 内核通常运行在该模式下,在系统复位的时候或者应用程序调用 svc 指令的时候将会进入到当前模式下,系统调用就是通过 svc 指令完成
  • Abort : 内存访问异常处理模式,常见的MMU fault就会跳转到该模式下进行相应的处理
  • Undefined:当执行未定义的指令时,触发硬件异常,硬件上自动跳转到该模式下
  • System : 系统模式,这个模式下将与用户模式共享寄存器视图
  • Monitor: 针对安全扩展,在该模式下执行 securenon-secure 处理器状态的切换
  • Hypervisor : 针对虚拟化扩展

不同的模式下有不同的资源处理权限以及不同的异常处理方式,实现这种隔离机制离不开寄存器,arm架构是r0~r15一共16个通用寄存器,其实,这只是一个寄存器接口,实际arm架构存在不同的Bank寄存器 ,所谓bank寄存器,即相同的寄存器名对应不同的寄存器实体。这其实就表示工作在不同模式下的CPU虽然都有一个pc寄存器,但实质却是不同的寄存器实体。

arm架构 需要cpu吗 arm架构需要授权吗_arm架构 需要cpu吗_02

图2.不同模式下的寄存器

观察上图(图2)会发现:

  • arm架构中splr寄存器其实并不是只有2个,不同的模式下分别拥有不同的bank寄存器FIQIRQABTSVCUNDMON这些模式都有各自不同的splr寄存器,实质也就是意味这这些不同的工作模式都分别拥有各自的栈空间,这能避免不同模式的切换而导致的sp、lr数据的反复保存于取出,这里既有效率的原因又有安全的考量
  • FIQ模式下的bank寄存器数量是最多的,所谓FIQ就是fast interrupt request即快速中断请求,既然是快速就要求效率,更多的实体寄存器能够更快的参数保存(参数直接保存在寄存器而非内存栈上),当然,为了实现这个fast,还有其他的手段来保证更高的效率,如:
  • 更高的优先级,FIQIRQ同时到达时,FIQ 将会先响应并处理,在操作系统的软件实现中,应该支持 FIQ 可以抢占 IRQ 运行,同时 FIQ 的处理不应该被任何其它异常所抢占。
  • FIQ中断向量表的位置在最后,对于 IRQ 的中断向量,只有四个字节,所以 IRQ 的向量必然是一条跳转指令,而 FIQ 在中断向量表的最后,FIQ 的中断处理代码可以直接放在中断向量表之后,避免跳转指令(当然这也由实现来决定)

FIQ虽然强,不过在 linux 中,并不使用 FIQ,所有的中断都只会被路由到IRQ 引脚上。

此外,我们还会发现在这些模式中,还有一个bank寄存器,那就是SPSR_,这类寄存器也是状态寄存器,全称saved program status register,专门用来保存模式切换之前那个模式的状态寄存器数据(除了sysuser模式外,其他每个模式都有一个spsr寄存器)。切换模式往往都伴随着cpsr或apsr寄存器保存数据到spsr寄存器,这一步是硬件自动完成的。

模式的切换伴随着跳转

arm架构cpu模式由状态寄存器(cpsr)最后的5bits控制,改变状态寄存器的最后5bis便能切换工作模式,当然,User模式下的指令是无权修改这些位置的值的,一般而言,模式的切换都是有目的的,要么是用户代码进行系统调用,从user模式切换到svc模式,从而执行更底层的代码,要么是异常中断伴随着异常处理,无论哪种情况都需要进行代码的跳转,只有跳转才能完成目标工作。

在处理器架构层面,软件中断、IRQFIQ被统称为异常,异常还包括 Abort 和`` undefined 指令等,在工作中最常接触的就是软件中断和 IRQ,在 linux 中,通常我们所说的软件中断就是通过 svc 指令发起,**软件中断即 svc **,是用户空间进入内核空间的**唯一通道**,也是 linux `实现系统调用的关键所在。

IRQ则是指硬件中断,由 CPU 上引出一条 IRQ 线,这条 IRQ 线通常连接到 GIC(中断控制器) 上,GIC 向下再连接各外设,当外设产生中断信号时,经由 GIC 传递到 CPUIRQ 引脚上,在 CPU 执行指令的间隙会查看是否有中断产生,如果有,则跳转到中断向量表的位置执行相应的异常处理程序。处理器的 IRQ 可以通过 CPSR 状态寄存器的中断屏蔽位屏蔽掉 IRQ。(如常见的鼠标键盘就是通过硬件中断与系统交互)。

FIQ(快速中断) ,相对于 IRQ 而言,FIQ 拥有更高的优先级,它可以抢占 IRQ 的执行,同时 FIQ 本身的执行速度比IRQ要快一些,这类中断通常用在对响应时间有极高要求的系统中(实时系统),比如 armv7-R 系列的处理器中会使用到。FIQ 的快速执行一方面体现在它有更高的优先级,另一方面,它拥有单独的寄存器,省去了参数的压栈时间,且处于中断向量表的最后一项,其执行代码不需要经过跳转。(linux系统并不支持快速中断,因为快速中断使得系统设计的复杂性增加,其提供的效率也伴随着硬件的发展而显得辉煌渐逝(老版本的linux还是支持fiq的))。

工作模式切换带来的跳转的设计机制是中断向量表

向量表中保存了一系列的跳转指令,当系统发生异常时,由处理器负责将程序执行流转到向量表中的跳转指令,最常见的就是中断向量,应用工程师只需要使用固定的函数名编写中断处理程序,在中断发生时该中断处理程序就会被自动调用,这背后的实现就是中断向量表的功劳。

offset

address

except mode

0x0

0xffff0000/0x00000000

reset

0x4

0xffff0004/0x00000004

undefine instruction

0x8

0xffff0008/0x00000008

svc call

0xc

0xffff000c/0x0000000c

prefetch abort

0x10

0xffff0010/0x00000010

data abort

0x14

0xffff0014/0x00000014

not use

0x18

0xffff0018/0x00000018

IRQ interrupt

0x1c

0xffff001c/0x0000001c

FIQ interrupt

中断向量表

中断向量表既可以是在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 中的值 copyCPSR

对于 SPSRcopy 并没有定义单独的指令,而是在操作 PC 指针时,在指令后添加一个'S'后缀即执行 SPSR 的拷贝,在阅读源代码时这种操作很容易被人忽略,因为普通的指令也可以带 S 后缀,表示当前操作是否更新 CPSR,另一种更新 SPSR 的方式是在 LDM 指令后添加 ^后缀,也是一样的效果,但是在 STM 指令后添加 ^ 并不是同样的意思,这个需要注意

  • 将保存在栈上的中断前模式的所有寄存器值恢复到寄存器中,返回程序断点(软件)
  • 设置 PC 寄存器到异常发生前的返回地址,这时候返回地址并不一定还保存在lr中,很可能 lr 已经被程序其它部分挪用,这取决于实现

在切换模式时,需要将之前模式的相关上下文保存到当前模式的栈中,那么就可能需要访问之前模式的sp寄存器的值,而splr通常都是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,接着保存 CPSRSPSR_IRQ,需要注意的是,这里还有一个操作:设置 CPSR I bit 为 1 屏蔽中断,直到软件上重新设置,才会接收新的中断,而软件上何时将中断打开决定了中断执行的策略,如果软件中的实现是在执行用户定义的中断服务函数之前打开中断,那么中断嵌套就可能发生,如果在执行完用户定义的中断服务函数之后打开,中断就不允许嵌套.
  • 指令跳转到 IRQ 对应的中断向量处,对应的中断向量处的执行代码有两种情况:针对svc模式下发生中断的处理和针对user模式下发生中断的处理
  • 中断服务子程序中保存中断发生前模式的所有寄存器值到栈上,由软件实现
  • 处理用户定义的中断处理程序,由软件实现
  • 中断返回时,将 SPSR 拷贝到 CPSR,并将所有保存在栈上的寄存器值恢复到寄存器中,设置 PC 返回到程序中

实际上 linux 的中断处理并不仅仅涉及到 IRQ 模式,在中断发生时仅仅是短暂地位于irq模式下,真正的中断处理流程是在 svc 模式下执行的。