谨以此篇作为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时,如有新的发现再行更新。