当有异常产生时,处理器总会跳转到对应的向量处取指令执行。异常向量表是各个异常处理机制的入口,通过异常向量表我们可以管窥系统整个的异常处理的框架。它就仿佛是我们研究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:
movr0,
#0
mcrp15,
0, r0, c7, c7@ invalidate I,D caches
on v4
mcrp15,
0, r0, c7, c10, 4@ drain write
buffer on v4
#ifdef
CONFIG_MMU
mcrp15,
0, r0, c8, c7@ invalidate I,D TLBs on
v4
#endif
adrr5,
arm920_crval
ldmiar5,
{r5, r6}
mrcp15,
0, r0, c1, c0@ get control register v4
bicr0,
r0, r5
orrr0,
r0, r6
movpc,
lr
这一段首先使i,d cache中内容无效,清除write buffer,使TLB内容无效。然后便是几行为打开MMU做准备的代码:加载符号arm920_crval的地址,从该地址处取两个变量到r5和r6寄存器中,我们来看下在arm920_crval处的内容:
arm920_crval:
crvalclear=0x00003f3f,
mmuset=0x00003135, ucset=0x00001130
crval为一个宏,在arch/arm/mm/proc-macros.S中定义:
.macrocrval, 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)
{
unsigned
long vectors = CONFIG_VECTORS_BASE;
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);
/*
* Copy signal return handlers into the vector
page, 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_stubirq, 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_stubdabt, 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_stubpabt, 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_stubund, 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
.align5
/*===================================================================
* 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
subspc, 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:
bvector_addrexcptn
/*
* We group all the following data together to
optimise
* for CPUs with separate I & D caches.
*/
.align5
.LCvswi:
.wordvector_swi
.globl__stubs_end
__stubs_end:
上面的代码可以分段来解读,每一段都是以用vector_stub符号开头的行开始的,然后紧接着的就是定义的一组变量。vector_stub实际上一个宏,展开后是一块代码,其定义为:
.macrovector_stub, name, mode, correction=0
.align5
vector_\name:
.if
\correction
sublr, lr, #\correction
.endif
@
Save r0, lr_(parent PC) and spsr_
@ (parent CPSR)
stmiasp, {r0, lr}@
save r0, lr
mrslr, spsr
strlr, [sp, #8]@
save spsr
@
Prepare for SVC32 mode.IRQs remain disabled.
mrsr0, cpsr
eorr0, r0, #(\mode ^ SVC_MODE | PSR_ISETSTATE)
msrspsr_cxsf, r0
@
the branch table must immediately follow this code
andlr, lr, #0x0f
THUMB(adrr0, 1f)
THUMB(ldrlr, [r0, lr, lsl #2])
movr0, sp
ARM(ldrlr, [pc, lr, lsl #2])
movspc, lr@
branch to handler in SVC mode
ENDPROC(vector_\name)
.align2
@
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为例吧,我们把整个宏及后面的那些变量定义都展开:
.macrovector_stub,
name, mode, correction=0
.align5
vector_irq:
.if
4
sublr, lr, #4
.endif
@
@
Save r0, lr_(parent PC) and spsr_
@
(parent CPSR)
@
stmiasp, {r0, lr}@
save r0, lr
mrslr, spsr
strlr, [sp, #8]@
save spsr
@
@
Prepare for SVC32 mode.IRQs remain
disabled.
@
mrsr0, cpsr
eorr0, r0, #(IRQ_MODE ^ SVC_MODE |
PSR_ISETSTATE)
msrspsr_cxsf, r0
@
@
the branch table must immediately follow this code
@
//
lr中当前存储了进入异常处理程序之前的状态寄存器的值,宏定义的前面部
//分有从spsr取值到lr的代码,对后几位做与,即是获取在中断前处理器所
//处的状态,这个值在后面会被用作跳转表的索引。
andlr, lr, #0x0f
//用做他用,sp值当第一个参数传给后面函数
movr0, sp
//
pc是当前执行指令地址加8,即跳转表的基地址,lr是索引,很好的技巧,
//取pc获取当前指令地址什么时候都没错
ARM(ldrlr, [pc, lr, lsl #2])
movspc, 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就是与位置无关选项,相信编译过动态库的人都熟悉,看看它是怎么做的。你会发现异曲同工。
然后我们再来看异常向量表:
.equstubs_offset, __vectors_start + 0x200 - __stubs_start
.globl__vectors_start
__vectors_start:
ARM(swiSYS_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(swiSYS_ERROR0)
如果没有进入操作系统,异常向量表中的第一项是什么呢?不正是打开机器,执行的第一条指令吗。或者也可以称之为复位异常。在Linux下的复位异常是执行一条软件中断指令。
接着看,其他的基本上都是跳转指令。我们发现了除了第三行代码用了绝对地址进行了跳转,其它都是用的b跳转。举个例子,b vector_dabt + stubs_offset,(vector_dabt在__stubs_start和__stubs_end之间),如果用b
vector_dabt,这肯定是有问题的,因为copy之后exec
view的组织(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,是我们所需要的。
向量表定义的前面我们看到:
.equstubs_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没有被搬移,所以可以直接用。
附上牛人做的一张代码搬移时的映射映射图
参考资料:
ARM
Linux中断向量表搬移设计过程,
。