Linux ARM 中断向量重定位分析

谨以此篇作为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的范围。
Linux ARM 中断向量重定位分析_第1张图片

下轴是向量表原始的存储位置,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时,如有新的发现再行更新。

你可能感兴趣的:(Linux,嵌入式)