R14_irq = address of next instruction to be executed + 4/*将寄存器lr_mode设置成返回地址,即为当前pc的值,因为pc是当前执行指令的下两条指令*/
SPSR_irq = CPSR /*保存处理器当前状态、中断屏蔽位以及各条件标志位*/
CPSR[4:0] = 0b10010 /*设置当前程序状态寄存器CPSR中相应的位进入IRQ模式,注意cpsr是所有模式共享的*/
CPSR[5] = 0 /*在ARM状态执行*/
/*CPSR[6] 不变*/
CPSR[7] = 1 /*禁止正常中断*/
If high vectors configured then
PC=0xFFFF0018 /*将程序计数器(PC)值设置成该异常中断的中断向量地址,从而跳转到相应的异常中断处理程序处执行,对于ARMv7向量表普遍中断是0xFFFF0018*/
else
PC=0x00000018 /*对于低向量*/
假设在用户空间时,产生了外部硬件中断,则这个时候的指令跳转流程如下:__vectors_start:---------------〉在中断向量表被拷贝后,该地址就是0xffff0000.
ARM( swi SYS_ERROR0 )
THUMB( svc #0 )
THUMB( nop )
W(b) vector_und + stubs_offset
W(ldr) pc, .LCvswi + stubs_offset
W(b) vector_pabt + stubs_offset
W(b) vector_dabt + stubs_offset
W(b) vector_addrexcptn + stubs_offset
W(b)vector_irq + stubs_offset----------〉当外部中断产生时,pc直接指向这个地址。
W(b) vector_fiq + stubs_offset
.globl __vectors_end
__stubs_start:
/*
* 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
vector_stub
irq, IRQ_MODE, 4语句展开如下:
/*
* Vector stubs.
*
* This code is copied to 0xffff0200 so we can use branches in the
* vectors, rather than ldr's. Note that this code must not
* exceed 0x300 bytes.
*
* Common stub entry macro:
* Enter in IRQ mode, spsr = SVC/USR CPSR, lr = SVC/USR PC
*
* SP points to a minimal amount of processor-private memory, the address
* of which is copied into r0 for the mode specific abort handler.
*/
.macro vector_stub, name, mode, correction=0
.align 5
vector_\name:
.if \correction
sub lr, lr, #\correction //因为硬件处理器是将当前指令的下两条指令的地址存储在lr寄存器中,所以这里需要减4,让他指向被中断指令的下一条,这样当中断被恢复时,可以继续被中断的指令继续执行。
.endif //需要注意的是,这个时候的lr寄存器,已经是irq模式下的私有寄存器了,在中断产生时,硬件处理器已经自动为他赋了值。
@
@ Save r0, lr_ (parent PC) and spsr_
@ (parent CPSR)
@
stmia sp, {r0, lr} @ save r0, lr//保存r0和lr寄存器,即被中断的下一条指令
mrs lr, 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 //让r0寄存器指向中断模式下堆栈的基地址
ARM( ldr lr, [pc, lr, lsl #2] )
movs pc, lr @ branch to handler in SVC mode,同时将中断模式下的spsr_irq(irq私有的)赋值给cpsr(该寄存器所有模式共享)
ENDPROC(vector_\name)
__irq_usr:
usr_entry //进行中断前的硬件上下文的保存
kuser_cmpxchg_check
irq_handler
get_thread_info tsk//获取被中断的用户进程或内核线程所对应的内核栈所对应的thread info结构。
mov why, #0
b ret_to_user_from_irq//恢复被中断时的上下文,然后继续被中断的进程或线程的执行
UNWIND(.fnend )
ENDPROC(__irq_usr)
.macro usr_entry
UNWIND(.fnstart )
UNWIND(.cantunwind ) @ don't unwind the user space
sub sp, sp, #S_FRAME_SIZE // #S_FRAME_SIZE的值为72
ARM( stmib sp, {r1 - r12} ) //尽管当前是处于管理模式,但由于svc和usr的r0-r12是公共的,所以相当于保存用户模式的r1-r12寄存器
THUMB( stmia sp, {r0 - r12} )
ldmia r0, {r3 - r5} //将之前保存在中断模式堆栈中的r0_usr,lr,spsr分别存储到r3-r5中
add r0, sp, #S_PC @ here for interlock avoidance #S_PC=60
mov r6, #-1 @ "" "" "" ""
str r3, [sp] @ save the "real" r0 copied
@ from the exception stack
@
@ We are now ready to fill in the remaining blanks on the stack:
@
@ r4 - lr_, already fixed up for correct return/restart
@ r5 - spsr_
@ r6 - orig_r0 (see pt_regs definition in ptrace.h)
@
@ Also, separately save sp_usr and lr_usr
@
stmia r0, {r4 - r6}
ARM( stmdb r0, {sp, lr}^ )//保存用户模式下的sp_usr,lr_usr
THUMB( store_user_sp_lr r0, r1, S_SP - S_PC )
@
@ Enable the alignment trap while in kernel mode
@
alignment_trap r0
@
@ Clear FP to mark the first stack frame
@
zero_fp
#ifdef CONFIG_IRQSOFF_TRACER
bl trace_hardirqs_off
#endif
.endm
cpsr(r16) |
pc(r15) |
lr(r14) |
sp(r13) |
r12(ip) |
r11(fp) |
r10 |
r9 |
r8 |
r7 |
r6 |
r5 |
r4 |
r3 |
r2 |
r1 |
r0 |
上图中的S_FRAME_SIZE, S_PC在arch/arm/kernel/Asm-offsets.c:中定义
DEFINE(S_FRAME_SIZE, sizeof(struct pt_regs));
DEFINE(S_PC, offsetof(struct pt_regs, ARM_pc));
include/asm-arm/Ptrace.h:
struct pt_regs {
long uregs[18];
};
#define ARM_pc uregs[15]
呵呵,pt_regs中对应的就是上面栈上的18个寄存器,ARM_pc是pc寄存器存放在这个数组中的偏移。
接着看get_thread_info, 它也是个宏,用来获取当前线程的地址。他的结构体定义如下:
include/linux/Sched.h:
union thread_union {
struct thread_info thread_info; /*线程属性*/
unsigned long stack[THREAD_SIZE/sizeof(long)]; /*栈*/
};
由它定义的线程是8K字节对齐的, 并且在这8K的最低地址处存放的就是thread_info对象,即该栈拥有者线程的对象,而get_thread_info就是通过把sp低13位清0(8K边界)来获取当前thread_info对象的地址。
arch/arm/kernel/entry-armv.S:
.macro get_thread_info, rd
mov /rd, sp, lsr #13
mov /rd, /rd, lsl #13
.endm
调用该宏后寄存器tsk里存放的就是当前线程的地址了, tsk是哪个寄存器呢,呵呵我们在看:
arch/arm/kernel/entry-header.S:
tsk .req r9 @ current thread_info
呵呵,tsk只是r9的别名而已, 因此这时r9里保存的就是当前线程的地址。
ENTRY(ret_to_user_from_irq)
ldr r1, [tsk, #TI_FLAGS] //tsk如上所述,是r9寄存器的别名,并且是指向thread_info结构体的
tst r1, #_TIF_WORK_MASK //检测是否有待处理的任务
bne work_pending
no_work_pending:
#if defined(CONFIG_IRQSOFF_TRACER)
asm_trace_hardirqs_on
#endif
/* perform architecture specific actions before user return */
arch_ret_to_user r1, lr //针对arm,是dummy的
restore_user_regs fast = 0, offset = 0//恢复之前用户模式时被中断时所保存的寄存器上下文
ENDPROC(ret_to_user_from_irq)
.macro restore_user_regs, fast = 0, offset = 0
ldr r1, [sp, #\offset + S_PSR] @ get calling cpsr 即为被中断时,处理器的cpsr值
ldr lr, [sp, #\offset + S_PC]! @ get pc 即为被中断指令的,下一条指令
msr spsr_cxsf, r1 @ save in spsr_svc //将r1赋值给管理模式下的spsr_svc,这样在movs时,会自动将该值赋值为cpsr
#if defined(CONFIG_CPU_V6)
strex r1, r2, [sp] @ clear the exclusive monitor
#elif defined(CONFIG_CPU_32v6K)
clrex @ clear the exclusive monitor
#endif
.if \fast
ldmdb sp, {r1 - lr}^ @ get calling r1 - lr
.else
ldmdb sp, {r0 - lr}^ @ get calling r0 - lr,将保存在内核栈中的r0到r14恢复到用户模式中的寄存器
.endif
mov r0, r0 @ ARMv5T and earlier require a nop
@ after ldm {}^
add sp, sp, #S_FRAME_SIZE - S_PC //恢复内核栈到中断产生之前的位置。
movs pc, lr @ return & move spsr_svc into cpsr
.endm
答:(1)内核刚启动时(head.S文件)通过设置CP15的c1寄存器已经确定了异常向量表的起始地址(例如0xffff0000),因此需要把已经写好的内核代码中的异常向量表考到0xffff0000处,只有这样在发生异常时内核才能正确的处理异常。
(2)从上面代码看出向量表和stubs(中断处理函数)都发生了搬移,如果还用b vector_irq,那么实际执行的时候就无法跳转到搬移后的vector_irq处,因为指令码里写的是原来的偏移量,所以需要把指令码中的偏移量写成搬移后的。至于为什么搬移后的地址是vector_irq+stubs_offset,请参考我的上篇blog:linux中断系统那些事之----中断初始化过程
答:因为使用b指令跳转比绝对跳转(ldr pc,XXXX)效率高,正因为效率高,所以把__stubs_start~__stubs_end之间的代码考到了0xffff0200起始处。
注意:
因为b跳转指令只能在+/-32MB之内跳转,所以必须拷贝到0xffff0000附近。
b指令是相对于当前PC的跳转,当汇编器看到 B 指令后会把要跳转的标签转化为相对于当前PC的偏移量写入指令码。
答:内核源代码顶层目录下的Makefile制定了vmlinux生成规则:
# vmlinux image - includingupdated kernel symbols
vmlinux: $(vmlinux-lds)$(vmlinux-init) $(vmlinux-main) vmlinux.o $(kallsyms.o)FORCE
其中$(vmlinux-lds)是编译连接脚本,对于ARM平台,就是arch/arm/kernel/vmlinux-lds文件。vmlinux-init也在顶层Makefile中定义:
vmlinux-init := $(head-y)$(init-y)
head-y 在arch/arm/Makefile中定义:
head-y:=arch/arm/kernel/head$(MMUEX T).o arch/arm/kernel/init_task.o
…
ifeq ($(CONFIG_MMU),)
MMUEXT := -nommu
endif
对于有MMU的处理器,MMUEXT为空白字符串,所以arch/arm/kernel/head.O 是第一个连接的文件,而这个文件是由arch/arm/kernel/head.S编译产生成的。
综合以上分析,可以得出结论,非压缩ARM Linux内核的入口点在arch/arm/kernel/head.s中。
一个最重要原因是:
如果一个中断模式(例如从usr进入irq模式,在irq模式中)中重新允许了中断,并且在这个中断例程中使用了BL指令调用子程序,BL指令会自动将子程序返回地址保存到当前模式的sp(即r14_irq)中,这个地址随后会被在当前模式下产生的中断所破坏,因为产生中断时CPU会将当前模式的PC保存到r14_irq,这样就把刚刚保存的子程序返回地址冲掉。为了避免这种情况,中断例程应该切换到SVC或者系统模式,这样的话,BL指令可以使用r14_svc来保存子程序的返回地址。
W(b) vector_und+ stubs_offset
W(ldr) pc, .LCvswi + stubs_offset
W(b) vector_pabt+ stubs_offset
W(b) vector_dabt+ stubs_offset
W(b) vector_addrexcptn+ stubs_offset
W(b) vector_irq+ stubs_offset
W(b) vector_fiq+ stubs_offset
.LCvswi:
.word vector_swi
由于系统调用异常的代码编译在其他文件中,其入口地址与异常向量相隔较远,使用b指令无法跳转过去(b指令只能相对当前pc跳转32M范围)。因此将其地址存放到LCvswi中,并从内存地址中加载其入口地址,原理与其他调用是一样的。这也就是为什么系统调用的速度稍微慢一点的原因。
问题6:为什么ARM能处理中断?
因为ARM架构的CPU有一个机制,只要中断产生了,CPU就会根据中断类型自动跳转到某个特定的地址(即中断向量表中的某个地址)。如下表所示,既是中断向量表。
ARM中断向量表及地址
问题7:什么是High vector?
A:在Linux3.1.0,arch/arm/include/asm/system.hline121 有定义如下:
#if __LINUX_ARM_ARCH__ >=4
#define vectors_high() (cr_alignment & CR_V)
#else
#define vectors_high() (0)
#endif
意思就是,如果使用的ARM架构大于等于4,则定义vectors_high()=cr_alignment&CR_V,该值就等于0xffff0000
在Linux3.1.0,arch/arm/include/asm/system.hline33有定义如下:
#define CR_V (1 << 13) /* Vectors relocated to 0xffff0000 */
arm下规定,在0x00000000或0xffff0000的地址处必须存放一张跳转表。
问题8:中断向量表是如何存放到0x00000000或0xffff0000地址的?
A:Uboot执行结束后会把Linux内核拷贝到内存中开始执行,linux内核执行的第一条指令是linux/arch/arm/kernel/head.S,此文件中执行一些参数设置等操作后跳入linux/init/main.c文件的start_kernel函数,此函数调用一系列初始化函数,其中trip_init()函数实现向量表的设定操作。