ARM中断
本文会介绍linux 中断系统 中与ARM架构相关的部分,对于通用的中断处理(平台无关的),有另一篇博客介绍。
首先,容我在开头就指出 一个重要的结论:linux只会让代码运行在两个空间,user space 和 kernel space,这是与平台无关的。对于ARM架构来说,user space对应user mode,kernel space 则对应SVC mode。对于ARM 中的剩余mode,linux 会将其业务都放到SVC mode 中处理。例如,当IRQ产生时,cpu会短暂的进入IRQ mode,保存寄存器后,立刻切换到SVC mode,在SVC mode 中执行中断处理函数。
总的来说,linux 希望在user mode处理应用逻辑,在svc mode 处理其他的代码逻辑。有了这个认识,就让我们开始学习Linux 是如何从头处理arm 中断的。
一、ARM中断
本文只追踪ARM 的IRQ中断,如下所示,ARM core 有R0-R15 16个通用寄存器和一个CPSR 寄存器,ARM寄存器的定义可以参考另一篇博客。这里只需要知道重点是:
红色框的部分是banked 寄存器,当cpu 处于user mode时,使用的是R13、R14,当切换为IRQ mode 时,R13寄存器实际上对应的是SP_svc,R14寄存器实际上对应的是LR_svc。在不同mode下,虽然寄存器在代码的命名上相同,但实际的物理电路却是不一样的。
当ARM core 接受到一个IRQ中断时,硬件会自动完成以下步骤:
- 将CPSR寄存器的值复制到SPSR_irq寄存器中,保存被中断时的cpu状态
- 将返回地址保存到LR_irq寄存器中,以便将来能继续执行
- 设置CPSR,切换到IRQ mode,并且关闭中断
- 将PC 设置为中断向量表中IRQ 处理函数的地址
上面4步由硬件完成后,cpu 就会去执行中断向量表IRQ处理函数,这个函数在linux 初始化时就设置好了。
二、vector_stub
2.1、中断向量表
我们开始进入linux 的代码世界,幸运的是,我们所需的代码都在 arch/arm/kernel/entry-armv.S 中,其中中断向量表的定义为:
.section .vectors, "ax", %progbits
.L__vectors_start:
W(b) vector_rst
W(b) vector_und
W(ldr) pc, .L__vectors_start + 0x1000
W(b) vector_pabt
W(b) vector_dabt
W(b) vector_addrexcptn
W(b) vector_irq
W(b) vector_fiq
可以看到每个向量对应一种异常模式,可是我搜索了vector_irq,却找不到其定义,这是为何?
我们在文章的开头中提到,linux 希望将所有异常的实际处理逻辑都放到svc mode 中去处理,故在此,linux 定义了一个宏vector_stub
.macro vector_stub, name, mode, correction=0
.align 5
vector_\name:
......
ENDPROC(vector_\name)
并且在代码中有 vector_stub irq, IRQ_MODE, 4
,将其宏展开之后就是:
vector_stub irq, IRQ_MODE, 4
.long __irq_usr @ 0 (USR_26 / USR_32)
.long __irq_invalid @ 1 (FIQ_26 / FIQ_32)
.long __irq_invalid @ 2 (IRQ_26 / IRQ_32)
.long __irq_svc @ 3 (SVC_26 / SVC_32)
@宏展开后:
vector_irq:
......
ENDPROC(vector_irq)
.long __irq_usr @ 0 (USR_26 / USR_32)
.long __irq_invalid @ 1 (FIQ_26 / FIQ_32)
.long __irq_invalid @ 2 (IRQ_26 / IRQ_32)
.long __irq_svc @ 3 (SVC_26 / SVC_32)
原来vector_irq 就是在这里定义的。
vector_stub 宏的作用就是:
- 把被打断执行的程序的寄存器保存到异常模式的栈中
- 将cpu mode切换到svc mode
- 根据被打断时的cpu mode,跳转到指定的函数处理
结合文章开头说的,linux 希望将所有异常的实际处理逻辑都放到svc mode 中去处理,vector_stub概括了所有异常所需的操作,保存寄存器,切到svc mode,执行对应的代码。具体的实现如下:
#name 是名称,mode 是异常模式,correction是纠正lr为下一条指令
.macro vector_stub, name, mode, correction=0
vector_\name:
# 对lr 进行纠正,将lr设置成 "被打断指令"的下一条指令,具体见附录说明1
.if \correction
sub lr, lr, #\correction
.endif
# 将r0,lr 保存到栈中
stmia sp, {r0, lr}
# 把spsr复制到lr中(spsr只能使用mrs指令访问)
mrs lr, spsr
# 把lr保存到sp加上偏移8的地址,保存的是spsr的值
str lr, [sp, #8]
# 把cpsr复制到r0
mrs r0, cpsr
# 将r0 的mode bit 修改为SVC_MODE
eor r0, r0, #(\mode ^ SVC_MODE | PSR_ISETSTATE)
# 将r0 复制到spsr_cxsf
msr spsr_cxsf, r0
# lr = lr & 0x0f:lr 保存的是spsr,即取低四bit,也就是lr 保存的是cpu mode
and lr, lr, #0x0f
# 将sp 复制到r0
mov r0, sp
# lr = *(pc + (lr << 2)),根据cpu mode更新lr 地址
ldr lr, [pc, lr, lsl #2]
# 跳转到lr地址执行,并将spsr_cxsf 的值复制到cpsr,即切换到SVC Mode
movs pc, lr
ENDPROC(vector_\name)
2.1、vector_irq
这么看还是比较难理解,我们以vector_irq 展开后为例子进行分析,假设当前cpu 正在svc mode执行(kernel space),此时中断产生:
# 第一节中所述,当进入vector_irq时,lr保存的是返回地址,spsr保存的是被中断时的cpu cpsr,sp切换到SP_irq
vector_irq:
# lr - 4 就是IRQ返回时要继续执行的指令
sub lr, lr, #4
# 将r0,lr按地址递增保存到sp 为基地址的栈中
stmia sp, {r0, lr}
# 把spsr复制到lr中(spsr只能使用mrs指令访问)
mrs lr, spsr
# 把lr保存到sp加上偏移8的地址上,保存的是spsr的值,也就是被中断时的cpu cpsr
str lr, [sp, #8]
# 把cpsr复制到r0
mrs r0, cpsr
# 将r0 的mode bit 修改为SVC_MODE
eor r0, r0, #(IRQ_MODE ^ SVC_MODE | PSR_ISETSTATE)
# 将r0 复制到spsr_cxsf
msr spsr_cxsf, r0
# lr = lr & 0x0f:lr 保存的是spsr,即取低四bit,也就是lr保存的是cpu mode
and lr, lr, #0x0f
# 将sp 复制到r0
mov r0, sp
# lr = *(pc + (lr << 2)),pc 指针加上 lr << 2,也就是__irq_svc的地址,lr 就是__irq_invalid的值
ldr lr, [pc, lr, lsl #2]
# 跳转到__irq_svc执行,并将spsr_cxsf 的值复制到cpsr,即切换到SVC Mode
movs pc, lr
ENDPROC(vector_irq)
.long __irq_usr @ 0 (USR_26 / USR_32)
.long __irq_invalid @ 1 (FIQ_26 / FIQ_32)
.long __irq_invalid @ 2 (IRQ_26 / IRQ_32)
.long __irq_svc @ 3 (SVC_26 / SVC_32)
执行完成后,SP_irq栈空间如图所示,并且cpu切换到svc mode,去执行__irq_svc函数,r0作为参数保存了SP_irq的值。
到此,我们可以了解vector_stub 宏所实现的目的:保存被中断的r0,lr,cpsr到异常栈中,切到svc 模式去处理异常。
三、__irq_svc
好了,vector_stub的事告一段落,接下来就要处理与中断处理有关的逻辑了。linux 将IRQ中断处理分为两种情况:
- 当中断产生时,cpu 正在执行用户程序
- 当中断产生时,cpu 正在执行内核代码
无论哪种情况下,都需要保存被打断时刻的寄存器信息,但这两种情况所需要的处理有些不同,分别由 __irq_svc 和 __irq_user 来进行处理.
本节会优先分析__irq_svc,简单来说,这个函数先保存上下文,然后执行中断处理函数,在退出异常之前,检查是否需要进行抢占调度。具体代码注释如下:
__irq_svc:
svc_entry @ 保存寄存器到svc 的栈
irq_handler @ 执行中断处理函数
ldr r8, [tsk, #TI_PREEMPT] @ 获取 preempt count
ldr r0, [tsk, #TI_FLAGS] @ 获取 flags
teq r8, #0 @ 将r8 与 0 进行异或操作,若结果为0则Z标志为1,否则Z标志为0
movne r0, #0 @ 若Z为0,则将r0 强制设置为0,不允许抢占调度
tst r0, #_TIF_NEED_RESCHED @ 将r0 与 _TIF_NEED_RESCHED进行与操作,若结果为0则Z标志为1,否则为0
blne svc_preempt @ 若Z为0,则跳转到svc_preempt 执行
svc_exit r5, irq = 1 @ 从异常退出,返回到被打断的执行流
ENDPROC(__irq_svc)
3.1、svc_entry
在中断产生时,我们希望保存被打断执行的程序的上下文,也就是所有的寄存器信息,对于arm 来说,就是r0->r15,以及CPSR,这样中断退出后,被打断的程序就能继续执行。
在 vector_stub 中,我们已经保存了R0,程序返回地址LR,程序状态SPSR。那么在svc_entry中,将继续保存剩下的所有寄存器!
svc_entry 将要保存的r0 -> r15 ,cpsr按照下图的排列顺序,保存到当前的内核栈中,kernel 的struct pt_regs 结构体表示内核栈中的寄存器信息:
// 一共有18个reg 的值,其排列如下:低地址为r0
struct pt_regs {
unsigned long uregs[18];
};
接下来分析源码,以及最终栈内寄存器的局势图
.macro svc_entry
sub sp, sp, #(SVC_REGS_SIZE - 4) @ 将sp减去17个reg 大小,预留出17个reg 的位置
stmia sp, {r1 - r12} @ 将r1->r12 的13个值依次填入预留的位置
# 接下来是计算一些地址
ldmia r0, {r3 - r5} @ 取出IRQ Stack 中r0,lr,spsr 的值到r3、r4、r5,这些值也要保存到栈中
add r7, sp, #S_SP - 4 @ R7 = SP + 13个reg的大小,R7的值就是栈内r13的地址
mov r6, #-1 @ r6 赋值为-1
add r2, sp, #(SVC_REGS_SIZE - 4) @ R2=SP+17个reg大小,R2的值就是一开始SP的值
str r3, [sp, #-4]! @ 将r3(也就是被打断的r0)保存到sp - 4的地址
mov r3, lr @ 将lr 保存到r3
@ 经过上面的操作,目前的寄存器信息如下:
@ r2 - sp_svc 进入svc_entry时,SP_SVC的值
@ r3 - lr_svc 进入svc_entry时,LR_SVC的值
@ r4 - 退出中断处理后要恢复执行的地址
@ r5 - 退出中断处理后要恢复执行的cpu 状态
@ r6 - -1 (see pt_regs definition in ptrace.h)
@ 将上述寄存器都保存到栈内
stmia r7, {r2 - r6}
@ 似乎是访问权限相关
get_thread_info tsk @ 获取当前task的thread_info,保存到tsk,(tsk就是r9)
uaccess_entry tsk, r0, r1, r2, 1 @ 保存用户空间访问的相关信息到内核栈
.endm
最终栈内的信息可以概况成下图:
3.2、svc_exit
与svc_entry 成对的是svc_exit,顾名思义,他的作用应该是恢复保存在栈中的寄存器,最终实现恢复被打断的执行流:
#在irq_entry中是:svc_exit r5, irq = 1,r5保存的是要恢复的CPSR,irq=1表示中断关闭
.macro svc_exit, rpsr, irq = 0
@ 确保中断已经关闭
.if \irq != 0
.else
disable_irq_notrace
.endif
@ 从栈恢复用户空间访问信息
uaccess_exit tsk, r0, r1
@ 将cpsr保存到spsr_cxsf
msr spsr_cxsf, \rpsr
@ 将栈中的值恢复到r0 -> pc寄存器,^ 符号会将cpsr更新为spsr_cxsf,执行完这个指令后,cpu就会恢复之前的status,并恢复之前的执行流
ldmia sp, {r0 - pc}^ @ load r0 - pc, cpsr
.endm
3.3、irq_handler && svc_preempt
在svc_entry 和 svc_exit 之间,由 irq_handler 负责完成中断具体逻辑的处理,从此会进入linux irq 管理的世界,与平台无关的。
若中断执行完成后,此时符合内核抢占,则会调用svc_prermpt 进行内核抢占调度,这就是下一话的内容了。
四、__irq_usr
当中断产生时,cpu正在执行用户程序,处于user mode时,vector_irq会进入到irq_usr 进行中断处理。这里的逻辑和 __irq_svc 很类似,都是保存寄存器后再进行中断处理,然后返回到被中断的地方继续执行:
__irq_usr:
usr_entry @ 保存寄存器
irq_handler @ 中断处理
get_thread_info tsk @ 获取thread_info 到r9
mov why, #0 @ why 是r8
b ret_to_user_from_irq @ 返回到用户
ENDPROC(__irq_usr)
4.1、usr_entry
usr_entry 和 svc_entry 的目的是一样的,就是将寄存器保存到当前的栈中,寄存器在栈中的排列也和struct pt_regs 一样。
.macro usr_entry, trace=1, uaccess=1
sub sp, sp, #PT_REGS_SIZE @ 在栈中预留18个reg 的位置
stmib sp, {r1 - r12} @ 保存r1 - r12 到栈内
ldmia r0, {r3 - r5} @ 从中断栈中取出寄存器
add r0, sp, #S_PC @ r0 指向栈中PC的位置
mov r6, #-1 @ r6 = -1
str r3, [sp] @ 保存之前的r0
@ 保存剩余的其他寄存器
@ r4 - lr_<exception>, already fixed up for correct return/restart
@ r5 - spsr_<exception>
@ r6 - orig_r0 (see pt_regs definition in ptrace.h)
@
@ Also, separately save sp_usr and lr_usr
@
stmia r0, {r4 - r6}
stmdb r0, {sp, lr}^
.endm
4.2、ret_to_user_from_irq
在返回用户程序之前,也会做一些检查,查看是否需要调度、处理信号等pending job,最后通过restore_user_regs 返回到用户程序
ENTRY(ret_to_user_from_irq)
@ 对用户空间访问的一些检查
ldr r2, [tsk, #TI_ADDR_LIMIT]
cmp r2, #TASK_SIZE
blne addr_limit_check_failed
@ 检查是否需要调度、是否有信号处理、返回用户空间之前是否需要调用callback函数,若需要则进入slow_work_pending处理
ldr r1, [tsk, #TI_FLAGS]
tst r1, #_TIF_WORK_MASK
bne slow_work_pending
no_work_pending:
arch_ret_to_user r1, lr
ct_user_enter save = 0
restore_user_regs fast = 0, offset = 0
ENDPROC(ret_to_user_from_irq)
restore_user_regs 与svc_exit 的逻辑是像似的,即从栈中恢复寄存器,并跳转到被打断的地方继续执行。
.macro restore_user_regs, fast = 0, offset = 0
uaccess_enable r1, isb=0
mov r2, sp
ldr r1, [r2, #\offset + S_PSR] @ get calling cpsr r1保存了pt_regs中的spsr,也就是发生中断时的CPSR
ldr lr, [r2, #\offset + S_PC]! @ get pc lr保存了PC值,同时sp移动到了pt_regs中PC的位置
tst r1, #PSR_I_BIT | 0x0f
bne 1f
msr spsr_cxsf, r1 @ save in spsr_svc 赋值给spsr,进行返回用户空间的准备
ldmdb r2, {r0 - lr}^ @ get calling r0 - lr
add sp, sp, #\offset + PT_REGS_SIZE @ 将sp_svc恢复到进入中断时的位置
movs pc, lr @ 恢复用户程序执行,并更新cpsr
.endm
参考书籍
- 《arm v7 user program guide》关于寄存器、异常的章节
- Linux kernel的中断子系统之(六):ARM中断处理过程 (wowotech.net)
附录
1、从异常返回时,返回地址的修正;