3.3.1 函数库intArchLib
函数库主要为上层应用程序提供了一组与外部硬件无关的中断控制函数。其中比较常用的就是函数intConnect、intEnable、intDisable。
1. STATUS intConnect
(
VOIDFUNCPTR *vector,
VOIDFUNCPTR routine,
int parameter
)
该函数的作用是将一个C函数与一个硬件中断连接起来,当发生了硬件中断时,自动执行该C函数。
它主要完成了三件事情,一是调用函数(* intEoiGet)(......)获取该中断的ISR处理前后需要执行的函数routineBoi、routineEoi及其参数;
二是调用函数intHandlerCreateI86填充数组intConnectCode,主要填充routineBoi、routine、routineEoi,组成完整的中断执行代码;
三是调用函数intVecSet将数组intConnectCode存放到参数vector指定的位置。
注意:代码中调用intHandlerCreate的情况属较老的vxWorks版本,对于新版本已经被intHandlerCreateI86函数所代替,因此不再过多分析。
注意,routineEoi函数的参数没有填充。此外还要注意routineBoi函数及参数的位置,用NOP填充了,因此就少了一个pushl指令,后面的addl $12, %esp指令也要做相应修正。
如果有必要(参数routineBoi有效)则填充函数_routineBoi函数及其参数;否则用NOP填充。
如有必要(参数routineEoi有效)中则填充_routineEoi函数及其参数;否则用NOP只填充_routineEoi函数而不填充其参数。
参考这段代码的注释:
LOCAL UCHAR intConnectCode [] = /* intConnect stub */
{
/*
* 00 e8 kk kk kk kk call _intEnt * tell kernel
* 05 50 pushl %eax * save regs
* 06 52 pushl %edx
* 07 51 pushl %ecx
* 08 68 pp pp pp pp pushl $_parameterBoi * push BOI param
* 13 e8 rr rr rr rr call _routineBoi * call BOI routine
* 18 68 pp pp pp pp pushl $_parameter * push param
* 23 e8 rr rr rr rr call _routine * call C routine
* 28 68 pp pp pp pp pushl $_parameterEoi * push EOI param
* 33 e8 rr rr rr rr call _routineEoi * call EOI routine
* 38 83 c4 0c addl $12, %esp * pop param
* 41 59 popl %ecx * restore regs
* 42 5a popl %edx
* 43 58 popl %eax
* 44 e9 kk kk kk kk jmp _intExit * exit via kernel
*/
intConnectCode十分难以理解,主要在于它本身是一段机器代码,为了程序的高效运行,软件直接对机器代码进行修改并将其保存到相应的中断处理地址。这段程序一共需要执行5个函数调用,这个5个函数分别是:
l call _intEnt。调用intEnt()进入中断,无调用参数,必须调用执行。
l routineBoi(......)。有参数,参数为一个4字节的整数,可以调用也可以不调用。
l C函数,有参数,参数为一个4字节的整数,核心的中断处理程序,必须调用。
l routineEoi(......),有参数,参数为一个4字节的整数,可以调用也可以不调用。
l jmp _intExit。跳转进入函数intExit函数以退出中断,无参数,必须执行,其实它和call _intEnt的执行模式是类似的,都是一个相对跳转指令。
指令call _intEnt如果翻译成机器语言的话,call指向的是一个相对的地址,具体来说是相对于call指令的下一条指定pushl $_parameter的开始地址,因此有*(int *)&pCode[ICC_INT_ENT] = (int)intEnt -(int)&pCode[ICC_INT_ENT + 4]。
对于call函数的运行,首先进行跳转,跳转到相应的子程序,相应的子程序在运行前要根据相反的顺序读出堆栈里面保存的参数,因此可以看出对有参数函数的call调用,必须在调用前将其参数压入堆栈。不过这里需要注意的是,子程序从堆栈里面读取参数并不是采用popl的办法,而是利用堆栈指针进行访问,也就是说当子程序可以直接读取堆栈esp指针指向的内容,这样并没有改变堆栈中保存数据的数量,因此可以看出在将寄存器ecx压入堆栈之后,先后将routinBoi参数、C函数参数以及routineEoi参数压入堆栈,待这些函数退出之后原来保存ecx的之上又有了三个参数了,因此需要执行$12, %esp以修改堆栈指针,然后再调用popl %ecx指令恢复的才是原来保存在ecx里面的内容。因此如果三个函数routinBoi、C函数以及routineEoi如果有一个没有执行,也就不会把相应的参数压入堆栈,那么在后面的popl执行之前调整esp的数值的时候也就需要考虑相应的修改。在本函数中,routineBoi和routineEoi的判断不太一样,如果routineBoi不存在就将压入routineBoi参数及调用routineBoi函数的操作用nop指令代替,因此后续的esp就需要做出相应修正;而对于routineEoi函数,如果不存在就将call _routineEoi指令修改为nop,原来压入routineEoi参数的指令并没有改变,也就是不管routineEoi参数是否有效都要压入堆栈,因此后面的esp无需对此进行调整了。
函数库intArchLib中有个全局变量intLockMask。汇编代码库intALib.s中的intLock()及intUnlock()函数使用该变量通过操作CPU的EFLAGS 寄存器进行中断屏蔽或者解除中断屏蔽。调用intLock()时将屏蔽中断,同时将调用前的EFLAGS的屏蔽标识位保存到变量intLockMask中;调用intUnlock()解除中断屏蔽时,函数根据intLockMask的状态决定是否需要解除,如果确实需要解除,则才进行中断屏蔽解除操作。
注意,intLock()函数可以从中断级或任务级程序中调用。因此仅仅修改EFLAGS 寄存器的屏蔽位并不能立即生效,直至调用函数intUnlock()。
在Pentium的保护模式下,IDT寄存器共有48bit,其中高32bit用于保存中断向量表的起始地址,低16位描述的是中断描述符表的最大容量,这里设置为0x7ff,即限制为32KB。
intVBRSet函数baseAddr数值写入到写入到idt寄存器中。参见汇编程序文件src\arch\i86\intALib.s。
图3.3 中断向量的地址偏移
上图描述是一个主要的原理性的示意图,因为中断是一个原始的CPU指令,它和CPU的结构紧密相关,图3.4说明了pentium处理器实模式下中断向量的存放格式。
图3.4 Pentium处理器实模式下对应的中断向量保存格式
图3.5 Pentium处理器在保护模式下对应的中断向量格式