异常向量表的映射和调用
在开始学习Interrupt子系统之前,需要了解的一些知识
1.异常向量表
向量表/页的工作方式是 CPU 将程序计数器和处理器状态存储在内部寄存器中,并将程序计数器放在相应的向量地址。向量表可以放在内存中的两个位置:地址 0x00000000 或地址 0xFFFF0000。该位置通过 CP15 控制寄存器 1 中的单个位来选择。Linux 支持将向量放在任一位置,但优先选择 0xFFFF0000。如果 MMU 关闭并且您与从地址 0x00000000 开始的物理内存有 1:1 映射,则使用地址 0x00000000 通常最有帮助。如果 MMU 处于打开状态(对我们来说是打开的),则使用的地址是虚拟地址,即使向量表也要经过 MMU 转换,并且通常使用内存中高位(地址 0xFFFF0000)的向量。
先看一下异常向量的构造
arch/arm/kernrl/entry-armv.S
.section .vectors.bhb.bpiall, "ax", %progbits
RELOC_TEXT_NONE
W(b) vector_rst
W(b) vector_bhb_bpiall_und
ARM( .reloc ., R_ARM_LDR_PC_G0, .L__vector_bhb_bpiall_swi )
THUMB( .reloc ., R_ARM_THM_PC12, .L__vector_bhb_bpiall_swi )
W(ldr) pc, .
W(b) vector_bhb_bpiall_pabt
W(b) vector_bhb_bpiall_dabt
W(b) vector_addrexcptn
W(b) vector_bhb_bpiall_irq
W(b) vector_bhb_bpiall_fiq
在运行的时并不能保证这段代码的地址就是0xFFFF0000,所以就需要把这段代码所在的虚拟地址设置为0xFFFF0000,然后中断发生时,执行跳转指令 W(b) vector_bhb_bpiall_irq。
并且所处的段是 .vectors,下面还会提及。
arch/arm/kernrl/head.S 这是arm架构运行的第一个文件
/*
* The following calls CPU specific code in a position independent
* manner. See arch/arm/mm/proc-*.S for details. r10 = base of
* xxx_proc_info structure selected by __lookup_processor_type
* above.
*
* The processor init function will be called with:
* r1 - machine type
* r2 - boot data (atags/dt) pointer
* r4 - translation table base (low word)
* r5 - translation table base (high word, if LPAE)
* r8 - translation table base 1 (pfn if LPAE)
* r9 - cpuid
* r13 - virtual address for __enable_mmu -> __turn_mmu_on
*
* On return, the CPU will be ready for the MMU to be turned on,
* r0 will hold the CPU control register value, r1, r2, r4, and
* r9 will be preserved. r5 will also be preserved if LPAE.
*/
ldr r13, =__mmap_switched @ address to jump to after
@ mmu has been enabled
badr lr, 1f @ return (PIC) address
#ifdef CONFIG_ARM_LPAE
mov r5, #0 @ high TTBR0
mov r8, r4, lsr #12 @ TTBR1 is swapper_pg_dir pfn
#else
mov r8, r4 @ set TTBR1 to swapper_pg_dir
#endif
ldr r12, [r10, #PROCINFO_INITFUNC]
add r12, r12, r10
ret r12
1: b __enable_mmu
ENDPROC(stext)
.ltorg
首先将 __mmap_switched 的地址放到 r13寄存器
然后跳转__enable_mmu去开启MMU
在启动MMU后回跳转执行到 __mmap_switched
__INIT
__mmap_switched:
...
#ifdef CONFIG_KASAN
bl kasan_early_init
#endif
mov lr, #0
b start_kernel
ENDPROC(__mmap_switched)
...
通过汇编跳转到了 start_kernel() init\main.c
start_kernel()
setup_arch()
paging_init()
devicemaps_init()
我们进入这个函数
static void __init devicemaps_init(const struct machine_desc *mdesc)
{
/*
* Allocate the vector page early.
*/
vectors = early_alloc(PAGE_SIZE * 2);
early_trap_init(vectors);
...
/*
* Create a mapping for the machine vectors at the high-vectors
* location (0xffff0000). If we aren't using high-vectors, also
* create a mapping at the low-vectors virtual address.
*/
map.pfn = __phys_to_pfn(virt_to_phys(vectors));
map.virtual = 0xffff0000;
map.length = PAGE_SIZE;
#ifdef CONFIG_KUSER_HELPERS
map.type = MT_HIGH_VECTORS;
#else
map.type = MT_LOW_VECTORS;
#endif
...
}
- early_alloc(PAGE_SIZE * 2); 申请8k的内存
- early_trap_init(vectors); 把异常向量表复制到这8k的位置
其余部分就是映射,将0xffff0000 ------> vectors
2.向量表的映射
进入early_trap_init()
void __init early_trap_init(void *vectors_base)
{
extern char __stubs_start[], __stubs_end[];
extern char __vectors_start[], __vectors_end[];
unsigned i;
vectors_page = vectors_base;
/*
* Poison the vectors page with an undefined instruction. This
* instruction is chosen to be undefined for both ARM and Thumb
* ISAs. The Thumb version is an undefined instruction with a
* branch back to the undefined instruction.
*/
for (i = 0; i < PAGE_SIZE / sizeof(u32); i++)
((u32 *)vectors_base)[i] = 0xe7fddef1;
/*
* Copy the vectors, stubs and kuser helpers (in entry-armv.S)
* into the vector page, mapped at 0xffff0000, and ensure these
* are visible to the instruction stream.
*/
copy_from_lma(vectors_base, __vectors_start, __vectors_end);
copy_from_lma(vectors_base + 0x1000, __stubs_start, __stubs_end);
kuser_init(vectors_base);
flush_vectors(vectors_base, 0, PAGE_SIZE * 2);
}
可以看到将 __vectors_start copy到了这块物理地址上
那么 __vectors_start 是什么呢?这就要去内核查看链接脚本了
arch/arm/kernel/vmlinux.lds
__vectors_start = LOADADDR(.vectors); __vectors_end = LOADADDR(.vectors) + SIZEOF(.vectors);
__vectors_start = LOADADDR(.vectors) 会获取这个段的地址,那么向量表刚好就在.vectors。到此就完成了向量表的复制。
3.调用过程
回到向量表,当发生异常时,会来调用 vector_bhb_bpiall_irq
/*
* Interrupt dispatcher
*/
vector_stub irq, IRQ_MODE, 4
...
而 vector_stub 是一个宏,展开为:
.macro vector_stub, name, mode, correction=0
.align 5
...
vector_\name:
.if \correction
sub lr, lr, #\correction
.endif
@ Save r0, lr_<exception> (parent PC)
stmia sp, {r0, lr} @ save r0, lr
@ Save spsr_<exception> (parent CPSR)
.Lvec_\name:
mrs lr, spsr @从SPSR寄存器(当前程序状态寄存器)中获取打断的程序位于什么模式下
str lr, [sp, #8] @ save spsr
@
@ Prepare for SVC32 mode. IRQs remain disabled.
@
mrs r0, cpsr
eor r0, r0, #(\mode ^ SVC_MODE | PSR_ISETSTATE)
msr spsr_cxsf, r0
@
@ the branch table must immediately follow this code
@
and lr, lr, #0x0f @提取模式
THUMB( adr r0, 1f )
THUMB( ldr lr, [r0, lr, lsl #2] )
mov r0, sp
ARM( ldr lr, [pc, lr, lsl #2] )
movs pc, lr @执行
ENDPROC(vector_\name)
根据提取出来的模式来执行以下的函数
/*
* Interrupt dispatcher
*/
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)
.long __irq_invalid @ 4
.long __irq_invalid @ 5
.long __irq_invalid @ 6
.long __irq_invalid @ 7
.long __irq_invalid @ 8
.long __irq_invalid @ 9
.long __irq_invalid @ a
.long __irq_invalid @ b
.long __irq_invalid @ c
.long __irq_invalid @ d
.long __irq_invalid @ e
.long __irq_invalid @ f
在用户态发生执行 __irq_usr
在内核态发生执行 __irq_svc,(在linux中,禁止在其他模式下产生中断)
最后来看一下这两个函数
arch/arm/kernrl/entry-armv.S
__irq_usr:
__irq_usr:
usr_entry @保存现场
kuser_cmpxchg_check
irq_handler from_user=1 @处理
get_thread_info tsk
mov why, #0
b ret_to_user_from_irq @恢复现场
UNWIND(.fnend )
ENDPROC(__irq_usr)
__irq_svc:
__irq_svc:
svc_entry @保存现场
irq_handler from_user=0 @处理
#ifdef CONFIG_PREEMPTION
ldr r8, [tsk, #TI_PREEMPT] @ get preempt count
ldr r0, [tsk, #TI_FLAGS] @ get flags
teq r8, #0 @ if preempt count != 0
movne r0, #0 @ force flags to 0
tst r0, #_TIF_NEED_RESCHED
blne svc_preempt
#endif
svc_exit r5, irq = 1 @恢复现场
UNWIND(.fnend )
ENDPROC(__irq_svc)
处理的流程都是一样的,而这里有个处理函数 irq_handler,它是由 GIC Driver 提供的。
.macro irq_handler, from_user:req
mov r1, sp
ldr_this_cpu r2, irq_stack_ptr, r2, r3
.if \from_user == 0
@
@ If we took the interrupt while running in the kernel, we may already
@ be using the IRQ stack, so revert to the original value in that case.
@
subs r3, r2, r1 @ SP above bottom of IRQ stack?
rsbscs r3, r3, #THREAD_SIZE @ ... and below the top?
#ifdef CONFIG_VMAP_STACK
ldr_va r3, high_memory, cc @ End of the linear region
cmpcc r3, r1 @ Stack pointer was below it?
#endif
bcc 0f @ If not, switch to the IRQ stack
mov r0, r1
bl generic_handle_arch_irq <----------------调用的函数
b 1f
generic_handle_arch_irq 以后再做分析。