当有异常产生时,处理器总会跳转到对应的向量处取指令执行。异常向量表是各个异常处理机制的入口,通过异常向量表我们可以管窥系统整个的异常处理的框架。它就仿佛是我们研究Linux系统复杂的异常处理机制的地图一般。所以,我们自然要先对异常向量表有一定的了解。
在ARM V4及V4T以后的大部分处理器中,中断向量表的基地址可以有两个位置:一个是0,另一个是0xffff0000。可以通过CP15协处理器c1寄存器中V 位(bit[13])控制。V和中断向量表的对应关系如下:
V=0 ~ 0x00000000~0x0000001C
V=1 ~ 0xffff0000~0xffff001C
回想,ARM Linux启动汇编部分代码,创建页表之后,打开MMU之前执行的那段代码,arch/arm/mm/proc-arm920.S文件中的例程__arm920_setup:
__arm920_setup:
mov r0,#0
mcr p15,0, r0, c7, c7 @ invalidate I,D cacheson v4
mcr p15,0, r0, c7, c10, 4 @ drain writebuffer on v4
#ifdefCONFIG_MMU
mcr p15,0, r0, c8, c7 @ invalidate I,D TLBs onv4
#endif
adr r5,arm920_crval
ldmia r5,{r5, r6}
mrc p15,0, r0, c1, c0 @ get control register v4
bic r0,r0, r5
orr r0,r0, r6
mov pc,lr
这一段首先使i,d cache中内容无效,清除write buffer,使TLB内容无效。然后便是几行为打开MMU做准备的代码:加载符号arm920_crval的地址,从该地址处取两个变量到r5和r6寄存器中,我们来看下在arm920_crval处的内容:
arm920_crval:
crval clear=0x00003f3f,mmuset=0x00003135, ucset=0x00001130
crval为一个宏,在arch/arm/mm/proc-macros.S中定义:
.macro crval, clear, mmuset, ucset
#ifdef CONFIG_MMU
.word \clear
.word \mmuset
#else
.word \clear
.word \ucset
#endif
.endm
crval宏定义两个变量。之后,从协处理器读取c1寄存器寄存器的内容到r0中,然后根据加载的变量对某些位进行清除和设置,然后便得到了我们想要往协处理器C1寄存器中写入的数据的初始形态。
根据clear=0x00003f3f, mmuset=0x00003135,因此我们可以判断bit13=1 中断向量表基址为0xFFFF0000。R0的值将被付给CP15的C1。
Linux内核中搬移代码的这个方法很具有通用性,我们把它叫做代码大挪移。您说搬代码谁不会阿,不就是拷贝吗,的确如此,但是拷贝也有技巧。拷贝很简单啦,也就是调用memcpy,传递一个源地址、一个目的地址外加一个内存区长度而已啊,这不用提,我们在这里想说的是,怎么样把代码(注意是代码,是机器指令,而不是简单的数据,跳转指令什么的,指令中会有相对地址什么的)设计成能随便拷贝的,换句专业点的术语,叫位置无关代码,拷到哪都正常起作用,能正确执行。
我们先看实际的代码搬运动作。在linux中,向量表建立的函数为:
init/main.c中的start_kernel()函数->arch/arm/kernel/setup.c中的setup_arch()函数->arch/arm/kernel/traps.c中的early_trap_init()函数:
void __init early_trap_init(void)
{
unsignedlong vectors = CONFIG_VECTORS_BASE;
externchar __stubs_start[], __stubs_end[];
externchar __vectors_start[], __vectors_end[];
externchar __kuser_helper_start[], __kuser_helper_end[];
intkuser_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);
/*
* Copy signal return handlers into the vectorpage, and
* set sigreturn to be a pointer to these.
*/
memcpy((void*)KERN_SIGRETURN_CODE, sigreturn_codes,
sizeof(sigreturn_codes));
memcpy((void*)KERN_RESTART_CODE, syscall_restart_code,
sizeof(syscall_restart_code));
flush_icache_range(vectors,vectors + PAGE_SIZE);
modify_domain(DOMAIN_USER,DOMAIN_CLIENT);
}
实际copy动作一目了然,就是两个memcpy(第三个实际上是拷贝一些别的东西,原理是一样的,这里不提了). copy的目的地是vectors,这个值是CONFIG_VECTORS_BASE,在2.6.32.7内核中CONFIG_VECTORS_BASE是在各个平台的配置文件中设定的,如:
arch/arm/configs/S3C2410_defconfig中
CONFIG_VECTORS_BASE=0xffff0000
把什么东西往那copy呢?第一部分是从__vectors_start到__vectors_end之间的代码,也就是异常向量表。第二部分是从__stubs_start到__stubs_end之间的代码,而第二部分是copy到vectors + 0x200起始的位置。也就是说,两部分之间的距离是0x200,即512个字节。看到这里,确实,也代码拷贝也确实没什么新鲜的,也还确实就仅仅是memcpy而已。只是,内有乾坤啊,拷贝的内容本身是耐人寻味的。
我们来看__vectors_start、__vectors_end之间,以及__stubs_start和__stubs_end之间到底是什么东西,只要知道它们在哪里定义的,就知道怎么回事了。
他们都位于arch/arm/kernel/entry-armv.S,这个文件是arm中各个模式的入口代码,熟悉arm的朋友们知道arm有几种模式,不知道的自己查查,不说了。我们取一个片断,和我们的阐述相关的部分。有兴趣的朋友可以查看源代码,研究全部,里面还是比较有内涵的。首先来看__stubs_start和__stubs_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 = USRPC
*/
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 = USRPC
*/
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 = ANYPC
* MUST PRESERVE SVC SPSR, but need to switchto SVC mode to show our msg.
* Basically to switch modes, we *HAVE* toclobber one register... brain
* damage alert! I don't think that we can execute any code inhere
* in any other mode than FIQ... Ok you can switch to another mode,
* but you can't get out of that mode without clobbering oneregister.
*/
vector_fiq:
disable_fiq
subs pc, lr, #4
/*===================================================================
* Address exception handler
*-------------------------------------------------------------------
* These aren't too critical.
* (they're not supposed to happen, and won'thappen in 32-bit data mode).
*/
vector_addrexcptn:
b vector_addrexcptn
/*
* We group all the following data together tooptimise
* for CPUs with separate I & D caches.
*/
.align 5
.LCvswi:
.word vector_swi
.globl __stubs_end
__stubs_end:
上面的代码可以分段来解读,每一段都是以用vector_stub符号开头的行开始的,然后紧接着的就是定义的一组变量。vector_stub实际上一个宏,展开后是一块代码,其定义为:
.macro vector_stub, name, mode, correction=0
.align 5
vector_\name:
.if\correction
sub lr, lr, #\correction
.endif
@Save r0, lr_ (parent PC) and spsr_
@ (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, #(\mode ^ SVC_MODE | PSR_ISETSTATE)
msr spsr_cxsf, r0
@the branch table must immediately follow this code
and lr, lr, #0x0f
THUMB( adr r0, 1f )
THUMB( ldr lr, [r0, lr, lsl #2] )
mov r0, sp
ARM( ldr lr, [pc, lr, lsl #2] )
movs pc, lr @branch to handler in SVC mode
ENDPROC(vector_\name)
.align 2
@handler addresses follow this label
1:
.endm
为了使我们对上面的代码有更好的了解,我们再来把代码的结构简化成这样:
.globl __stubs_start
__stubs_start:
.align 5
vector_irq:
[code part] // 展开代码
[jump table part] // 地址跳转表
……
.align 5
vector_dabt:
[code part]
[jump table part]
……
.align 5
vector_ pabt:
[code part]
[jump table part]
……
.align 5
vector_und:
[code part]
[jump table part]
……
.align 5
vector_fiq:
……
.globl __stubs_end
__stubs_end:
看到这里,想必我们都应该明白__stubs_start和__stubs_end之间,实际上就是异常处理程序的入口。我们来研究一下展开代码部分的特征,这部分代码是与位置无关的代码,我们稍微研究一下,它为什么会这么写。以irq为例吧,我们把整个宏及后面的那些变量定义都展开:
.macro vector_stub,name, mode, correction=0
.align 5
vector_irq:
.if4
sub lr, lr, #4
.endif
@
@Save r0, lr_ (parent PC) and spsr_
@(parent CPSR)
@
stmia sp, {r0, lr} @save r0, lr
mrs lr, spsr
str lr, [sp, #8] @save spsr
@
@Prepare for SVC32 mode. IRQs remaindisabled.
@
mrs r0, cpsr
eor r0, r0, #(IRQ_MODE ^ SVC_MODE |PSR_ISETSTATE)
msr spsr_cxsf, r0
@
@the branch table must immediately follow this code
@
//lr中当前存储了进入异常处理程序之前的状态寄存器的值,宏定义的前面部
// 分有从spsr取值到lr的代码,对后几位做与,即是获取在中断前处理器所
// 处的状态,这个值在后面会被用作跳转表的索引。
and lr, lr, #0x0f
// 用做他用,sp值当第一个参数传给后面函数
mov r0, sp
//pc是当前执行指令地址加8,即跳转表的基地址,lr是索引,很好的技巧,
// 取pc获取当前指令地址什么时候都没错
ARM( ldr lr, [pc, lr, lsl #2] )
movs pc, lr @branch to handler in SVC mode
ENDPROC(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)
.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
这部分代码大致都是一样的结构,前面是一些代码,后面跟着一个跳转表。前面的代码首先保存一些寄存器,紧接着便是设置CPSR,使得处理器处于SVC模式。在ARM中,Linux只使用两种模式,SVC和USR。跳转表里面定义了一些地址。真正的跳转在最后一句完成,大家都看得很清楚。跳到哪里去了?如果中断以前是svc模式,就会跳到__irq_svc。即是说,Linux下的异常处理是根据进入异常状态之前处理器所处的状态来选择不同的异常处理程序的。我们发现这里不会直接用 b(bl,bx) 等跳转语句:
一是b类跳转指令是将偏移编码进指令里的,而这个偏移是有限制的,不能太大。
二是b跳转后面的偏移你不知道在代码拷贝后还是不是那个样子,跳转的目的地址通常都是以PC为基准进行计算的。因为我们要搬移代码,所以如果你不能确定搬移后的偏移不变,那你就用绝对地址,而上面的代码前三句就是算出绝对地址来,然后用绝对地址赋值给pc直接完成跳转。
这些都是一些技巧,总之你要注意的是写位置无关的代码时涉及到跳转部分,用b跳转还是直接赋成绝对地址(通过跳转表实现),如果你不能保证搬移后的偏移一致,写这部分就要注意了,要用一些技巧的。
大家可以去用gcc的-fPIC和-S选项汇编一个小的函数看看,fPIC就是与位置无关选项,相信编译过动态库的人都熟悉,看看它是怎么做的。你会发现异曲同工。
然后我们再来看异常向量表:
.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:
之前我们说过,这叫做位置无关的代码,因为要拷贝到别的地方。
注意表里的第一项:
ARM( swi SYS_ERROR0 )
如果没有进入操作系统,异常向量表中的第一项是什么呢?不正是打开机器,执行的第一条指令吗。或者也可以称之为复位异常。在Linux下的复位异常是执行一条软件中断指令。
接着看,其他的基本上都是跳转指令。我们发现了除了第三行代码用了绝对地址进行了跳转,其它都是用的b跳转。举个例子,b vector_dabt + stubs_offset,(vector_dabt在__stubs_start和__stubs_end之间),如果用bvector_dabt, 这肯定是有问题的,因为copy之后execview的组织(map)是不一样的,所以b指令中编码的偏移就不对了。这里面,我们就要对这个偏移进行一次调整。stubs_offset就是这个调整值,是可以计算出来的。
我们先来考虑,我们希望这些指令中编码的偏移地址是多少呢?以vector_irq为例,假设指令中偏移地址是根据正在执行的指令地址来计算的。实际的异常向量表的基地址在CONFIG_VECTORS_BASE = 0xffff0000处,所以执行中断异常的时候,其指令地址应该是CONFIG_VECTORS_BASE + (向量表中表项地址 - __vectors_start),因为__stubs_start与__stubs_end之间的内容会被复制到CONFIG_VECTORS_BASE + 0x200位置处,所以实际的vector_irq符号的地址应该是CONFIG_VECTORS_BASE + 0x200 + vector_irq(编译地址) - __stubs_start(编译地址),所以我们希望指令中编码的偏移地址是:
(CONFIG_VECTORS_BASE + 0x200 + (vector_irq(编译地址) - __stubs_start(编译地址))) - (CONFIG_VECTORS_BASE + (向量表中表项地址 - __vectors_start))
= (0x200 + (vector_irq(编译地址) - __stubs_start(编译地址))) - (向量表中表项地址 - __vectors_start(编译地址))
= __vectors_start+ 0x200 - __stubs_start + vector_irq - 向量表中表项地址。
然后来检验一下,这个值是不是对的:
搬移之后的向量表中表项地址 + __vectors_start + 0x200 - __stubs_start + vector_irq - 向量表中表项地址
= 搬移之后的向量表中表项地址 – 向量相对于表基址的偏移+ 0x200 - __stubs_start + vector_irq
= CONFIG_VECTORS_BASE+ 0x200 - __stubs_start + vector_irq
= CONFIG_VECTORS_BASE+ 0x200 + vector_irq - __stubs_start
OK,是我们所需要的。
向量表定义的前面我们看到:
.equ stubs_offset,__vectors_start + 0x200 - __stubs_start
上面的一段正是这个定义的来源啊。
其实尽管ldr pc, .LCvswi + stubs_offset这条指令用的是直接往PC中加载指令地址的方法来完成跳转,用得跳转表的方法,但找地址的过程也用到了这个技术。我们看到:
.align 5
.LCvswi:
.word vector_swi
.LCvswi这个位置存储的是一个地址,就是要跳到这个地方。.align 5的意思是32字节对齐,这个是保证cache line对齐的,不提了。在exec view中找这个地址,就得加上个offset。原理是一样的,因为.LCvswi在__stubs_start和__stubs_end之间,这个区域被搬移了,不能直接用这个符号地址了,vector_swi没有被搬移,所以可以直接用。
参考资料:
ARMLinux中断向量表搬移设计过程,
http://www.unixresources.net/linux/clf/linuxK/archive/00/00/71/27/712710.html。