ARM Linux中断机制分析
——以用户模式产生irq中断为例
以下代码基于内核linux2.6.38.3(trimslice官网下载)
本文主要分析ARM发生中断时的处理流程,以在usr态发生IRQ为例,即usr—>irq为例讨论。
1.内核异常向量表的初始化
1.1初始化大致流程
ARM linux内核启动时,首先运行的是linux/arch/arm/kernel/head.S,进行一些初始化工作,然后调用main.c->start_kernel()函数,进而调用trap_init()(或者调用early_trap_init()函数进行初始化)、init_IRQ()函数进行中断初始化、建立异常向量表.
1.2异常向量表的建立
异常向量表的建立过程就是拷贝过程,为了将内核代码写成位置无关的,有很多地方需要注意。
1.2.1异常向量表基地址确定
在ARM V4及V4T以后的大部分处理器中,中断向量表的位置可以有两个位置:一个是0x00000000,另一个是0xffff0000。可以通过CP15协处理器c1寄存器中V位(bit[13])控制。V和中断向量表的对应关系如下:
V=0 ~ 0x00000000~0x0000001C
V=1 ~ 0xffff0000~0xffff001C
注:CP15控制寄存器说明详见ARM ARMB4-1690.
在异常向量表初始化前运行的文件linux/arch/arm/kernel/head.S中设置了CP15寄存器(在 ~/arch/arm/mm/proc-v7.S文件中的__v7_setup函数中设置),这里通过设置CP15的c1寄存器已经确定了异常向量表的基地址(0xffff0000)。
1.2.2 异常向量表拷贝过程
内核代码编译生成后,需要将异常向量表拷贝到指定位置(0x00000000 or 0xffff0000),这就需要将内核中的异常向量表设计成与位置无关的。
本文所使用内核版本使用了early_trap_init()代替trap_init()来初始化异常。
early_trap_init()在linux/arch/arm/kernel/traps.c中,代码如下:
1、CONFIG_VECTORS_BASE在处理器型号确定后就已经确定,其值在内核配置完成后自动生成,保存在.config文件中。本文使用内核版本在maketrimslice_deconfig后自动生成的.config中定义:CONFIG_VECTORS_BASE=0xffff0000,也就是说,异常向量表的基地址0xffff0000。
~/arch/arm/kernel/traps.c line783
void __init early_trap_init(void) { #if defined(CONFIG_CPU_USE_DOMAINS) unsigned long vectors = CONFIG_VECTORS_BASE; //vectors是中断向量基地址 #else unsigned long vectors = (unsigned long)vectors_page; #endif /*以下这些都在arch/arm/kernel/entry-armv.S下定义*/ extern char __stubs_start[], __stubs_end[]; extern char __vectors_start[], __vectors_end[]; extern char __kuser_helper_start[], __kuser_helper_end[]; int kuser_sz = __kuser_helper_end - __kuser_helper_start;
/* * 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. */ /*__vectors_end 至 __vectors_start之间为异常向量表。__stubs_end至
memcpy((void *)vectors, __vectors_start, __vectors_end - __vectors_start); memcpy((void *)vectors + 0x200, __stubs_start, __stubs_end - __stubs_start); memcpy((void *)vectors + 0x1000 - kuser_sz, __kuser_helper_start, kuser_sz); /* * Do processor specific fixups for the kuser helpers */ kuser_get_tls_init(vectors); /* * Copy signal return handlers into the vector page, and * set sigreturn to be a pointer to these. */ memcpy((void *)(vectors + KERN_SIGRETURN_CODE - CONFIG_VECTORS_BASE), sigreturn_codes, sizeof(sigreturn_codes)); memcpy((void *)(vectors + KERN_RESTART_CODE - CONFIG_VECTORS_BASE), syscall_restart_code, sizeof(syscall_restart_code)); flush_icache_range(vectors, vectors + PAGE_SIZE); modify_domain(DOMAIN_USER, DOMAIN_CLIENT); } |
以下是__vectors_start, __vectors_end,__stubs_end__stubs_start的定义。
arch/arm/kernel/entry-armv.S .globl __vectors_start __vectors_start: 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 W(b) vector_fiq + stubs_offset .globl __vectors_end
.globl __stubs_start __stubs_start: /* * Interrupt dispatcher */ vector stub irq,IRQ_MODE,4 //vector_stub是一个宏,展开后是一块代码,后面紧跟着跳转表 .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 …… …… …… …… .globl __stubs_end __stubs_end:
.equ stubs_offset, __vectors_start + 0x200 - __stubs_start
vector_stub irq, IRQ_MODE, 4 展开后如下: // -------------------------------- begin 展开 .align 5 //将异常入口强制进行2^5字节对齐,即一个cache line大小对齐,出于性能考虑 vector_irq: sub lr, lr, 4 //需要调整pc返回值,对于irq异常,将lr减去4,对于其他异常需要作出不同调整
@ Save r0, lr_<exception> (parent PC) and spsr_<exception> @ (parent CPSR) @ stmia sp, {r0, lr} @ save r0, lr mrs lr, spsr str lr, [sp, #8] @ save spsr
@ Prepare for SVC32 mode. IRQs remain disabled. @ mrs r0, cpsr eor r0, r0, IRQ_MODE ^ SVC_MODE) msr spsr_cxsf, r0
@ the branch table must immediately follow this code @ and lr, lr, #0x0f mov r0, sp ldr lr, [pc, lr, lsl #2] movs pc, lr @ branch to handler in SVC mode // -------------------------------- end 展开 |
异常向量表的拷贝过程用图表示比较清晰,如下图所示:
图一 向量表搬移及offset偏移量计算示图
图一说明:上面两条有方向的横线,横线方向代表地址生长方向,下面那个是Code/Load视图,是搬移前的代码在生成的二进制内核中的组织情况,上面的Exec view是代码在内存中开始执行后的分配情况。
2.linux对ARM异常、中断的处理流程
2.1当IRQ发生时,硬件完成的操作
R14_irq= address of next instruction to be executed + 4/*将寄存器lr_mode设置成返回地址*/
SPSR_irq = CPSR /*保存处理器当前状态、中断屏蔽位以及各条件标志位*/
CPSR[4:0] = 0b10010 /*设置当前程序状态寄存器CPSR中相应的位进入IRQ模式*/
CPSR[5] = 0 /*在ARM状态执行*/
/*CPSR[6] 不变*/
CPSR[7] = 1 /*禁止正常中断*/
If high vectors configured then
PC=0xFFFF0018 /*将程序计数器(PC)值设置成该异常中断的中断向量地址,从
*而跳转到相应的异常中断处理程序处执行,对于ARMv7向量表普遍是0xFFFF0018
*/
else
PC=0x00000018
2.2 指令流跳转过程
以上CPU操作完成后,PC跳转到0xFFFF0018,该地址就是指令W(b) vector_irq + stubs_offset所在地址。然后跳转到vector_stub irq,IRQ_MODE, 4,去执行相应的异常、中断处理函数。
接下来具体看代码:
.globl __vectors_start __vectors_start: 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 //中断发生后的跳转地址0xFFFF0018 W(b) vector_fiq + stubs_offset .globl __vectors_end __vectors_end: |
stubs_offset只是个偏移量,用来修正跳转地址的,主要的操作是vector_irq执行。vector_irq是由宏vector_stub irq,IRQ_MODE,4(IRQ_MODE在include\asm\ptrace.h中定义:0x12)生成。以下是vector_irq生成后的代码(汇编代码中,@开始的语句、//、/**/都代表注释):
.align 5 vector_irq: sub lr, lr, 4 //因为异常发生时,cpu将pc地址+4赋值给lr,这里做修正。 @ Save r0, lr_<exception> (parent PC) and spsr_<exception> @ (parent CPSR) @ stmia sp, {r0, lr} mrs lr, spsr str lr, [sp, #8]
@ Prepare for SVC32 mode. IRQs remain disabled. @ mrs r0, cpsr eor r0, r0,#( IRQ_MODE ^ SVC_MODE| PSR_ISETSTATE) // PSR_ISETSTATE:选择ARM/Thumb指令集 msr spsr_cxsf, r0 |
异或运算是可以交换位置的,也即A^B^C等价于A^C^B。所以这里的r0^( IRQ_MODE ^ SVC_MODE| PSR_ISETSTATE)等价于r0^ IRQ_MODE ^SVC_MODE,由于r0的低5位模式位与IRQ_MODE相同,所以r0^ IRQ_MODE的运算结果的低5位全被清零,然后再^SVC_MODE,也即低5位被设置为SVC_MODE,其它位保持不变。
@ the branch table must immediately follow this code and lr, lr, #0x0f mov r0, sp ldr lr, [pc, lr, lsl #2] movs pc, lr |
sp是SVC32模式下的堆栈指针,这里将它移到r0中,就可以作为C函数的第一个参数,即C函数中的pt_regs参数。
pc是当前地址+8,也就是本段代码后面紧跟的跳转表的基地址,lr用于在跳转表中索引,lr左移两位等同于*4,因为每个条目是4字节。从usr模式进入irq模式,则lr=pc+4*0,若从svc模式进入irq,则lr=pc+4*3,即__irq_svc的地址,其他地址进入__irq_invalid出错处理,因为不能从其他模式进入irq异常。
假设这里是从usr进入irq,则执行跳转表中的第一条指令。跳转的基准地址为当前pc,因为ARMv4是三级流水线结构的,它总是指向当前指令的下两条指令的地址,尽管以后版本的指令流水线扩展为5级和8级,但是这一特性一直被兼容处理,也即pc(excute)=pc(fetch) + 8,其中:pc(fetch)是当前正在执行的指令,就是之前取该指令时候的PC的值;pc(execute):当前指令执行的,计算中如果用到pc,是指此时pc的值。
当mov指令的目标寄存器是PC,且指令以S结束,则它会把spsr的值恢复给cpsr,上面说到当前的spsr中保存的是r0的值,即svc模式。所以本条指令是跳转到__irq_usr的同时将处理器模式转为svc模式。异常处理一定要进入svc模式的原因是:异常处理一定要进入PL1特权级;另一个原因是使能嵌套中断。具体原因在问题4中解释。关于__irq_svc和__irq_invalid暂时不讨论。
/* * Interrupt dispatcher以下跳转表必须紧跟在vector_irq之后 */ vector_stub irq, IRQ_MODE, 4 //生成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) /* 从SVC进入中断的处理函数 */ .long __irq_svc @ 3 (SVC_26 / SVC_32) .long __irq_invalid @ 4 .long __irq_invalid @ 5 .long __irq_invalid @ 6 |
图2IRQ中断处理跳转示意图
注意,以下操作都是在svc模式中,因为要借用SVC模式进行ISP处理,所以需要保存所有SVC模式下的寄存器到SVC堆栈中,最后才去调用中断服务例程(ISP)irq_handler。
2.2.1 __irq_usr
.align 5 __irq_usr: usr_entry //用于用户模式下发生中断时初始化中断处理堆栈,同时保存所有SVC态寄存器到堆栈。 kuser_cmpxchg_check //对低版本的ARM核来说,用户态无法实现原子比较交换。如果用户态在处理原 //子比较交换的过程中发生中断,需要特殊处理,略过 get_thread_info tsk //根据当前sp指针,将该指针最右边13位清0,获得当前任务的thread_info #ifdef CONFIG_PREEMPT //如果可以抢占,递增任务的抢占计数 ldr r8, [tsk, #TI_PREEMPT] //T被定义为offsetof(struct thread_info, preempt_count),显然通过tsk就 可以很容易得到进程preempt_count成员的地址了 add r7, r8, #1 @ increment it str r7, [tsk, #TI_PREEMPT] #endif irq_handler //中断服务例程,后面分析 #ifdef CONFIG_PREEMPT ldr r0, [tsk, #TI_PREEMPT] // 获得当前的抢占计数 str r8, [tsk, #TI_PREEMPT] //并将r8中的值保存回去。相当于将前一步递增的抢占计数减回去了 teq r0, r7 //程序没有配对操作抢占计数导致系统错误。 ARM( strne r0, [r0, -r0] ) //如果抢占计数被破坏,则强制写入0. THUMB( movne r0, #0 ) THUMB( strne r0, [r0] ) #endif mov why, #0 b ret_to_user //返回到用户态 UNWIND(.fnend ) ENDPROC(__irq_usr) |
接下来分别看各个函数的功能
arch/arm/include/asm/ptrace.h struct pt_regs { unsigned long uregs[18]; }; #endif /* __KERNEL__ */ #define ARM_cpsr uregs[16] #define ARM_pc uregs[15] #define ARM_lr uregs[14] #define ARM_sp uregs[13] #define ARM_ip uregs[12] #define ARM_fp uregs[11] #define ARM_r10 uregs[10] #define ARM_r9 uregs[9] …… #define ARM_ORIG_r0 uregs[17] |
pt_regs结构体定义,后面要用到。
2.2.2 usr_entry
@ We are now ready to fill in the remaining blanks on the stack: @ @ 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} ARM_ORIG_r0,显然ARM_ORIG_r0保存了-1(r6)这个常量 ARM( stmdb r0, {sp, lr}^ ) 到ARM_sp和ARM_lr中。 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 |
以上的指令的作用可以总结如下,其中将普通寄存器r1到r12保存到ARM_r1- ARM_r12,这相当于把发生中断时的寄存器r1-r12进行了保存。接下来保存中断发生时的r0,lr_irq和spsr_irq保存到r1-r3,r4赋值为-1,它们接下来将被使用。接下来保存r0到ARM_r0,lr_irq,spsr_irq和-1到ARM_pc ARM_cpsr ARM_ORIG_R0,注意到stmdb指令中的"^",它保存sp_usr和lr_usr分别到ARM_sp和ARM_lr,显然这里将所有中断发生时的寄存器都进行了保存。
图3 保存中断堆栈
2.2.3 get_thread_info
get_thread_info宏用来根据当前的sp值,通过lsr和lsl分别右移左移13位,相当于对sp向下圆整到8K对齐。这里也就是thread_info所在的地址。
arch/arm/kernel/entry-header.S .macro get_thread_info, rd mov \rd, sp, lsr #13 mov \rd, \rd, lsl #13 .endm |
linux/arch/arm/kernel/entry-armv.S /* * Interrupt handling. Preserves r7, r8, r9 */ .macro irq_handler #ifdef CONFIG_MULTI_IRQ_HANDLER ldr r5, =handle_arch_irq mov r0, sp ldr r5, [r5] adr lr, BSYM(9997f) teq r5, #0 movne pc, r5 #endif arch_irq_handler_default 9997: .endm |
2.2.4 irq_handler
linux/arch/arm/kernel/entry-armv.S
/*
* Interrupt handling. Preserves r7, r8, r9
*/
.macro irq_handler
#ifdefCONFIG_MULTI_IRQ_HANDLER
ldr r5,=handle_arch_irq
mov r0,sp
ldr r5,[r5]
adr lr,BSYM(9997f)
teq r5,#0
movne pc,r5
#endif
arch_irq_handler_default
9997:
.endm
2.2.5 arch_irq_handler_default
irq_handler是真正的IRQ中断处理入口,在中断处理中需要预留r7,r8和r9寄存器。它们被用来处理内核抢占。在没有配置MULTI_IRQ_HANDLER 的情况下,irq_handler的逻辑很简单,就是简单的调用arch_irq_handler_default。
如果配置了该选项,平台代码可以修改全局变量:handle_arch_irq,这里只讨论默认实现.
arch/arm/include/asm/entry_macro_multi.S /* * Interrupt handling. Preserves r7, r8, r9 */ .macro arch_irq_handler_default //get_irqnr_preamble用来获取中断状态寄存器基地址 get_irqnr_preamble r5, lr //将中断控制器的状态寄存器的地址存储到r5 1: get_irqnr_and_base r0, r6, r5, lr //判断中断号,通过r0返回 movne r1, sp //如果还存在中断,就将sp作为第二个参数,调用asm_do_IRQ。sp目前指向pt_regs @ @ routine called with r0 = irq number, r1 = struct pt_regs * @ adrne lr, BSYM(1b) //这里将lr设置为get_irqnr_and_base的第二条指令,因为第二次循环时,不必执行其第一条指令(加载寄存器基址) bne asm_do_IRQ //将中断号、pt_regs(中断前的寄存器现场)传递给asm_do_IRQ。asm_do_IRQ返回时, //会返回到get_irqnr_and_base处,直到所有中断都已经处理完毕才退出循环。 #ifdef CONFIG_SMP //针对SMP系统的处理 /* * this macro assumes that irqstat (r6) and base (r5) are * preserved from get_irqnr_and_base above */ ALT_SMP(test_for_ipi r0, r6, r5, lr) // ALT_UP_B(9997f) movne r1, sp adrne lr, BSYM(1b) //同理,这里也是将返回地址设置为ALT_SMP的第二条指令,构造成一个循环 bne do_IPI //只要存在IPI就调用do_IPI,并循环直到处理完所有IPI
#ifdef CONFIG_LOCAL_TIMERS // 同理,这里循环处理多核系统中的本地时钟中断。 test_for_ltirq r0, r6, r5, lr movne r0, sp adrne lr, BSYM(1b) bne do_local_timer #endif #endif 9997: .endm |
2.2.6 get_irqnr_preamble
get_irqnr_preamble用于获得中断状态寄存器基地址,特定于CPU,这里CPU用的是tegra,其定义如下
/* arch/arm/mach-tegra/include/mach/entry-macro.S /* Uses the GIC interrupt controller built into the cpu */ #define ICTRL_BASE (IO_CPU_VIRT + 0x40100) // #define IO_CPU_VIRT 0xFE000000 .macro get_irqnr_preamble, base, tmp movw \base, #(ICTRL_BASE & 0x0000ffff) movt \base, #((ICTRL_BASE & 0xffff0000) >> 16) .endm |
2.2.7 get_irqnr_and_base
get_irqnr_and_base用来获取中断号。
/* arch/arm/mach-tegra/include/mach/entry-macro.S .macro get_irqnr_and_base, irqnr, irqstat, base, tmp ldr \irqnr, [\base, #0x20] @ EVT_IRQ_STS cmp \irqnr, #0x80 .endm |
get_irqnr_preamble和get_irqnr_and_base两个宏由machine级的代码定义,目的就是从中断控制器中获得IRQ编号,紧接着就调用asm_do_IRQ,从这个函数开始,中断程序进入C代码中,传入的参数是IRQ编号和寄存器结构指针,
2.2.8 asm_do_IRQ
图4 asm_do_IRQ流程图
asm_do_IRQ是ARM处理硬件中断的核心函数,第一个参数指定了硬中断的中断号,第二个参数是寄存器备份组成的一个结构体,保存了中断发生时的模式对应的寄存器的值,在中断返回时使用。
linux/arch/arm/kernel/irq.c asmlinkage void __exception_irq_entry asm_do_IRQ(unsigned int irq, struct pt_regs *regs) { struct pt_regs *old_regs = set_irq_regs(regs); irq_enter(); /* * Some hardware gives randomly wrong interrupts. Rather * than crashing, do something sensible. */ if (unlikely(irq >= nr_irqs)) { if (printk_ratelimit()) printk(KERN_WARNING "Bad IRQ%u\n", irq); ack_bad_irq(irq); } else { generic_handle_irq(irq); } /* AT91 specific workaround */ irq_finish(irq); irq_exit(); set_irq_regs(old_regs); } |
2.2.9 irq_enter
irq_enter是更新一些系统的统计信息,同时在__irq_enter宏中禁止了进程的抢占。虽然在产生IRQ时,ARM会自动把CPSR中的I位置位,禁止新的IRQ请求,直到中断控制转到相应的流控层后才通过local_irq_enable()打开。那为何还要禁止抢占?这是因为要考虑中断嵌套的问题,一旦流控层或驱动程序主动通过local_irq_enable打开了IRQ,而此时该中断还没处理完成,新的irq请求到达,这时代码会再次进入irq_enter,在本次嵌套中断返回时,内核不希望进行抢占调度,而是要等到最外层的中断处理完成后才做出调度动作,所以才有了禁止抢占这一处理。
linux/kernel/softirq.c void irq_enter(void) { int cpu = smp_processor_id(); rcu_irq_enter(); if (idle_cpu(cpu) && !in_interrupt()) { /* Prevent raise_softirq from needlessly waking up ksoftirqd * here, as softirq will be serviced on return from interrupt.*/ local_bh_disable(); tick_check_idle(cpu); _local_bh_enable(); } __irq_enter(); } #define __irq_enter() \ do { \ account_system_vtime(current); \ add_preempt_count(HARDIRQ_OFFSET); \ trace_hardirq_enter(); \ } while (0) |
2.2.10 generic_handle_irq
~/include /linux/Irqdesc.h /* * Architectures call this to let the generic IRQ layer * handle an interrupt. If the descriptor is attached to an * irqchip-style controller then we call the ->handle_irq() handler, * and it calls __do_IRQ() if it's attached to an irqtype-style controller. */ static inline void generic_handle_irq_desc(unsigned int irq, { desc->handle_irq(irq, desc); //调用该irq注册的函数处理,该函数在注册中断时填写irq_desc结构体时指定。 } //沿跳变处理和电平处理。
static inline void generic_handle_irq(unsigned int irq) //该函数是与体系结构无关的通用逻辑层API { generic_handle_irq_desc(irq, irq_to_desc(irq)); } |
2.2.11 ret_to_user
以上内容处理结束后,退回用户层。
arch/arm/kernel/entry-common.S /* * "slow" syscall return path. "why" tells us if this was a real syscall. */ ENTRY(ret_to_user) ret_slow_syscall: disable_irq @ disable interrupts ldr r1, [tsk, #TI_FLAGS] //从任务的TI_FLAGS标志判断是否需要处理抢占或者信号。 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 //在返回用户态前,处理各个体系结构的钩子
restore_user_regs fast = 0, offset = 0 //恢复寄存器现场,并切回用户态。这里不再具体分析恢复方式。 ENDPROC(ret_to_user) |
3.问题及解答
问题1:vector_irq已经是异常、中断处理的入口函数了,为什么还要加stubs_offset?( b vector_irq + stubs_offset)
答:(1)内核刚启动时(head.S文件)通过设置CP15的c1寄存器已经确定了异常向量表的起始地址(例如0xffff0000),因此需要把已经写好的内核代码中的异常向量表考到0xffff0000处,只有这样在发生异常时内核才能正确的处理异常。
(2)从上面代码看出向量表和stubs(中断处理函数)都发生了搬移,如果还用b vector_irq,那么实际执行的时候就无法跳转到搬移后的vector_irq处,因为指令码里写的是原来的偏移量,所以需要把指令码中的偏移量写成搬移后的。至于为什么搬移后的地址是vector_irq+stubs_offset,如图一所示。下图是搬移示意图更加清晰说明了搬移过程。。
问题2:为什么在异常向量表中,用b指令跳转而不是用ldr绝对跳转?
答:因为使用b指令跳转比绝对跳转(ldr pc,XXXX)效率高,正因为效率高,所以把__stubs_start~__stubs_end之间的代码考到了0xffff0200起始处。
注意:
因为b跳转指令只能在+/-32MB之内跳转,所以必须拷贝到0xffff0000附近。
b指令是相对于当前PC的跳转,当汇编器看到 B 指令后会把要跳转的标签转化为相对于当前PC的偏移量写入指令码。
经过Uboot的启动,内核跳入linux/arch/arm/kernel/head.S开始执行。
问题1:为什么首先进入head.S开始执行?
问题3:为什么首先进入head.S开始执行?
答:内核源代码顶层目录下的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中。
问题4: 中断为什么必须进入svc模式?
一个最重要原因是:
如果一个中断模式(例如从usr进入irq模式,在irq模式中)中重新允许了中断,并且在这个中断例程中使用了BL指令调用子程序,BL指令会自动将子程序返回地址保存到当前模式的sp(即r14_irq)中,这个地址随后会被在当前模式下产生的中断所破坏,因为产生中断时CPU会将当前模式的PC保存到r14_irq,这样就把刚刚保存的子程序返回地址冲掉。为了避免这种情况,中断例程应该切换到SVC或者系统模式,这样的话,BL指令可以使用r14_svc来保存子程序的返回地址。
问题5:为什么跳转表中有的用了b指令跳转,而有的用了ldr px,xxxx?
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,则