ARM Linux中断机制分析
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至 __stubs_start之间是异常处理的位置。这些变量定义都在arch/arm/kernel/entry-armv.S中*/
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 //异常向量表开始0xFFFF0000 __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} //保存r0, lr,到irq堆栈中(每个异常都有属于自己的堆栈) mrs lr, spsr //lr保存spsr_irq的值,即usr状态的cpsr的值(见2.1) str lr, [sp, #8] // 保存 spsr到[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 //这里的cxsf表示从低到高分别占用的4个8bit的数据域 |
异或运算是可以交换位置的,也即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 //提取发生异常前的处理器模式,这里也就是usr模式 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 //r0,r7是调用irq_handler前后的抢占计数,这里进行比较,是防止驱动的ISR //程序没有配对操作抢占计数导致系统错误。 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结构体定义,后面要用到。
@ 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} //stmia将svc模式下的寄存器r4-r6保存到ARM_pc,ARM_cpsr和 ARM_ORIG_r0,显然ARM_ORIG_r0保存了-1(r6)这个常量 ARM( stmdb r0, {sp, lr}^ ) //stmdb指令的^标志表示存储发生中断的模式下的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 //alignment_trap在配置CONFIG_ALIGNMENT_TRAP时有效,如果开启了该选 //项,中断处理中将支持对齐跟踪 @ Clear FP to mark the first stack frame zero_fp //zero_fp用来设置fp栈帧寄存器为0 #ifdef CONFIG_IRQSOFF_TRACER bl trace_hardirqs_off #endif .endm @usr_entry宏定义结束 |
以上的指令的作用可以总结如下,其中将普通寄存器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 |
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) // 这里是从寄存器中读取ipi标志 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, struct irq_desc *desc) { desc->handle_irq(irq, desc); //调用该irq注册的函数处理,该函数在注册中断时填写irq_desc结构体时指定。 } // handle_irq是个函数指针,它用来实现中断处理器的电流处理。电流处理分为边 //沿跳变处理和电平处理。
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指令会自动将子程序返回地址保存到当前模式的lr(即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,则定义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()函数实现向量表的设定操作。
参考文献
1. ARM Linux中断向量表搬移设计过程http://blog.chinaunix.net/uid-361890-id-175347.html
2. 《LINUX3.0内核源代码分析》第二章:中断和异常 http://blog.chinaunix.net/uid-25845340-id-2982887.html
3. Kernel Memory Layout on ARM Linux http://www.arm.linux.org.uk/developer/memory.txt
4.http://emblinux.sinaapp.com/ar01s16.html#id3603818
5. Linux中断(interrupt)子系统之二:arch相关的硬件封装层http://blog.csdn.net/droidphone/article/details/7467436
附录1
Kernel Memory Layout on ARM Linux
Start End Use
--------------------------------------------------------------------------
ffff8000 ffffffff copy_user_page / clear_user_page use.
ForSA11xx and Xscale, this is used to
setupa minicache mapping.
ffff1000 ffff7fff Reserved.
Platformsmust not use this address range.
ffff0000 ffff0fff CPUvector page.
The CPU vectors are mapped here ifthe
CPU supports vector relocation(control
register V bit.)
ffc00000 fffeffff DMA memory mapping region. Memory returned
bythe dma_alloc_xxx functions will be
dynamicallymapped here.
ff000000 ffbfffff Reserved for future expansion of DMA
mappingregion.
VMALLOC_END feffffff Free for platform use, recommended.
VMALLOC_ENDmust be aligned to a 2MB
boundary.
VMALLOC_START VMALLOC_END-1 vmalloc() /ioremap() space.
Memoryreturned by vmalloc/ioremap will
bedynamically placed in this region.
VMALLOC_STARTmay be based upon the value
ofthe high_memory variable.
PAGE_OFFSET high_memory-1 Kernel direct-mapped RAM region.
Thismaps the platforms RAM, and typically
mapsall platform RAM in a 1:1 relationship.
TASK_SIZE PAGE_OFFSET-1 Kernel module space
Kernelmodules inserted via insmod are
placedhere using dynamic mappings.
00001000 TASK_SIZE-1 User space mappings
Per-threadmappings are placed here via
themmap() system call.
00000000 00000fff CPU vector page / null pointer trap
CPUswhich do not support vector remapping
placetheir vector page here. NULL pointer
dereferencesby both the kernel and user
spaceare also caught via this mapping.
Please note that mappings which collidewith the above areas may result
in a non-bootable kernel, or may cause thekernel to (eventually) panic
at run time.
Since future CPUs may impact the kernelmapping layout, user programs
must not access any memory which is notmapped inside their 0x0001000
to TASK_SIZE address range. If they wish to access these areas, they
must set up their own mappings using open()and mmap().
转载请注明出处:http://blog.csdn.net/qianlong4526888/article/details/8598524
更多推荐
所有评论(0)