Linux ARM 中断向量重定位分析

2019-07-12 23:23发布

谨以此篇作为2019年的开篇,开启新的征程。虽然文章的内容写于3年之前,但是既然开始了博客之路,就把之前写的一些笔记陆续搬到博客上,也不枉费了昨日的辛苦。
作为Linux的开篇,延续我一贯的风格,从启动以及中断向量开始。此篇文章的内容是以Linux版本是2.6.39,硬件平台是ATMEL9G25。
大神路过,看一眼,请指正其中错误之处。好了,开始正题。
在arch/arm/kernel/entry-armv.S 中_vectors_start和__vectors_end之间保存了异常向量表。 .globl __stubs_start __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 /* * Data abort dispatcher * Enter in ABT mode, spsr = USR CPSR, lr = USR PC */ vector_stub dabt, ABT_MODE, 8 .long __dabt_usr @ 0 (USR_26 / USR_32) .long __dabt_invalid @ 1 (FIQ_26 / FIQ_32) .long __dabt_invalid @ 2 (IRQ_26 / IRQ_32) .long __dabt_svc @ 3 (SVC_26 / SVC_32) .long __dabt_invalid @ 4 .long __dabt_invalid @ 5 .long __dabt_invalid @ 6 .long __dabt_invalid @ 7 .long __dabt_invalid @ 8 .long __dabt_invalid @ 9 .long __dabt_invalid @ a .long __dabt_invalid @ b .long __dabt_invalid @ c .long __dabt_invalid @ d .long __dabt_invalid @ e .long __dabt_invalid @ f /* * Prefetch abort dispatcher * Enter in ABT mode, spsr = USR CPSR, lr = USR PC */ vector_stub pabt, ABT_MODE, 4 .long __pabt_usr @ 0 (USR_26 / USR_32) .long __pabt_invalid @ 1 (FIQ_26 / FIQ_32) .long __pabt_invalid @ 2 (IRQ_26 / IRQ_32) .long __pabt_svc @ 3 (SVC_26 / SVC_32) .long __pabt_invalid @ 4 .long __pabt_invalid @ 5 .long __pabt_invalid @ 6 .long __pabt_invalid @ 7 .long __pabt_invalid @ 8 .long __pabt_invalid @ 9 .long __pabt_invalid @ a .long __pabt_invalid @ b .long __pabt_invalid @ c .long __pabt_invalid @ d .long __pabt_invalid @ e .long __pabt_invalid @ f /* * Undef instr entry dispatcher * Enter in UND mode, spsr = SVC/USR CPSR, lr = SVC/USR PC */ vector_stub und, UND_MODE .long __und_usr @ 0 (USR_26 / USR_32) .long __und_invalid @ 1 (FIQ_26 / FIQ_32) .long __und_invalid @ 2 (IRQ_26 / IRQ_32) .long __und_svc @ 3 (SVC_26 / SVC_32) .long __und_invalid @ 4 .long __und_invalid @ 5 .long __und_invalid @ 6 .long __und_invalid @ 7 .long __und_invalid @ 8 .long __und_invalid @ 9 .long __und_invalid @ a .long __und_invalid @ b .long __und_invalid @ c .long __und_invalid @ d .long __und_invalid @ e .long __und_invalid @ f .align 5 /*============================================================================= * Undefined FIQs *----------------------------------------------------------------------------- * Enter in FIQ mode, spsr = ANY CPSR, lr = ANY PC * MUST PRESERVE SVC SPSR, but need to switch to SVC mode to show our msg. * Basically to switch modes, we *HAVE* to clobber one register... brain * damage alert! I don't think that we can execute any code in here in any * other mode than FIQ... Ok you can switch to another mode, but you can't * get out of that mode without clobbering one register. */ vector_fiq: disable_fiq subs pc, lr, #4 /*============================================================================= * Address exception handler *----------------------------------------------------------------------------- * These aren't too critical. * (they're not supposed to happen, and won't happen in 32-bit data mode). */ vector_addrexcptn: b vector_addrexcptn /* * We group all the following data together to optimise * for CPUs with separate I & D caches. */ .align 5 .LCvswi: .word vector_swi .globl __stubs_end __stubs_end: .equ stubs_offset, __vectors_start + 0x200 - __stubs_start .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 __vectors_end: 上述中断向量并不像普通的启动代码,将中断向量定位于链接地址的起始位置,而是将head.s等启动代码放置到链接地址的起始位置。
ARM架构的CPU的异常向量基址可以是0x00000000,也可以是 0xffff0000,它由CONFIG_VECTORS_BASE决定,CONFIG_VECTORS_BASE在arch/arm/Kconfig中配置: 182 config VECTORS_BASE 183 hex 184 default 0xffff0000 if MMU || CPU_HIGH_VECTOR 185 default DRAM_BASE if REMAP_VECTORS_TO_RAM 186 default 0x00000000 187 help Linux内核开启了MMU功能,所以内核使用后者。early_trap_init函数将异常向量复制到0xffff0000处。vectors 在earl_tarp_init函数第一行定义等于CONFIG_VECTORS_BASE,也就是0xffff0000。__vectors_start、__stubs_start和kuser_sz为异常向量表、子向量等所处的位置,分别拷贝到0xffff0000、 0xffff0200以及0xffff1000处。
此函数位于:arch/arm/kernel/traps.c文件,代码如下: void __init early_trap_init(void) { #if defined(CONFIG_CPU_USE_DOMAINS) unsigned long vectors = CONFIG_VECTORS_BASE; #else unsigned long vectors = (unsigned long)vectors_page; #endif 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. */ 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之间的代码就是异常向量,在arch/arm/kernel/entry-armv.S中定义,第一行代码将它们复制到地址 0xffff0000处。异常向量的代码很简单,它们只是一些跳转指令。发生异常时,CPU自动执行这些指令,跳转去执行更复杂的代码,比如保存被中断程序的执行环境,调用异常处理函数,恢复被中断程序的执行环境并重新运行。这些“更复杂的代码”在地址__stubs_start ~__stubs_end之间,它们也在arch/arm/kernel/entry-armv.S中定义。第二行代码将它们复制到地址 0xffff0000+0x200处。迁移代码如下: 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); 将搬移前的代码组织称为Code/Load 视图,因为这是代码中的(或image中的)组织情况,把搬移后的代码组织称为Exec视图,反映的是代码执行时在内存中的情况。当代码发生迁移后,跳转地址会发生变化,因此需要将代码设计成“位置无关代码”,即代码设计成能随便拷贝,任意地址均可正常运行。
上述中断向量代码即为位置无关的代码。因为要拷贝到别的地方,而且里面都是跳转指令;除了第三个行代码用了绝对地址进行了跳转,其它都是用的b指令跳转。例如:b vector_irq + stubs_offset,(vector_irq在__stubs_start和__stubs_end之间),如果使用b vector_irq, 这肯定是有问题的,因为copy之后exec view的组织(map)是不一样的,所以b指令后这个偏移量就不对了,需要对这个偏移进行一次调整,Stubs_offset就是这个调整值。
尽管ldr pc, .LCvswi + stubs_offset这条指令用的是绝对地址跳转,用跳转表的方法,但找地址的过程也用到位置无关代码。
.align 5
1217 .LCvswi:
1218 .word vector_swi
.LCvswi这个位置存储的是一个地址,就是要跳到这个地方。.align 5的意思是32字节对齐,这个是保证cache line对齐。在exec view中找这个地址然后加上新的偏移量,原理与其他它跳转指令相同。Stubs_offset具体计算过程如上图所示,具体描述如下:
b跳转是一个相对跳转,依赖于当前的PC值和label相对于当前PC值的偏移量,这个偏移量在编译链接的时候就已经确定了,会存在b跳转指令机器码的bit[23:0],是24bit有符号数;因为ARM指令是word对齐的,最低2bit永远为0;所以左移两位后表示有效偏移的而是26bit的有符号数,也就是可以向前和向后都可以跳转32MB的范围。
在这里插入图片描述 下轴是向量表原始的存储位置,b vector_irq机器码中存储的偏移量(链接时就确定了)就是位于这条指令的PC值相对于vector_irq标号的偏移量,所以一跳就跳到了。但是如果要跳到copy后的vector_irq标号处,还是用那个偏移量,鬼知道跳到哪里去了;但是我们也不用找偏移量,只有找到要跳的label地址,编译的时候会自动算偏移量的。对照上轴copy后的情况,当发生数据预取异常时,会跳到异常向量表(E(__vectors_start)开始的)的异常向量上,就是T1的位置,T1里存了一条指令W(b) vector_irq + stubs_offset,这条指令执行完就跳转到数据预取异常的入口E(vector_irq)处了,也就是要跳到L2结束的地址。
这个绝对地址为:
L1 + L2 = 0x200 + “vector_irq在stubs中的偏移量”- “vector_irq_jump中断入口在向量表中的偏移量”;
L1 + L2 = 0x200+ (vector_irq-stubs_start)-(irq_jump-vectors_start )
L1 + L2 = (vector_irq-irq_jump) + vectors_start + 200 - stubs_start,
对于最后一个括号内的值实际上就是中断向量表中写的vector_irq - irq_jump由汇编器完成,而后面的 vectors_start+200-stubs_start即是stubs_offset,即汇编代码中定义的stubs_offset .equ stubs_offset, __vectors_start + 0x200 - __stubs_start 此篇文章写于3年之前,不知最新的内核如何变化,等再研究imx6的kernel时,如有新的发现再行更新。