通过前面中断的实验我们知道LABEL_IDT是放到内存中的一张表,里面是一个一个的门描述符,对应相应的中断向量。我们的工作就是在描述符中填充相应的内容。为了能在c中设置中断门,我们在head.S中添加.globl LABEL_IDT,这样在c中我们就可以使用了,我们在main.c中是这样声明的:
typedef struct desc_struct {
unsigned long a,b;
} desc_table[256];
extern desc_table LABEL_IDT;
然后定义了一个set_intr_gate,参数n代表中断向量号,参数addr代表中断服务函数的地址。
#define set_intr_gate(n,addr) \
_set_gate(&LABEL_IDT[n],14,0,addr)
下面看一下_set_gate的实现:
#define _set_gate(gate_addr,type,dpl,addr) \ __asm__ ("movw %%dx,%%ax\n\t" \ "movw %0,%%dx\n\t" \ "movl %%eax,%1\n\t" \ "movl %%edx,%2" \ : \ : "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \ "o" (*((char *) (gate_addr))), \ "o" (*(4+(char *) (gate_addr))), \ "d" ((char *) (addr)),"a" (0x00080000))
c嵌入汇编的格式为:
asm("汇编语句"
:输出寄存器
:输入寄存器
:会被修改的寄存器);
把输出寄存器和输入寄存器统一编号,分别对应%0,%1,%2 ..... 顺序为:先输出再输入,从左到右(从上到下,从左到右)。
"i","o","d",这些是寄存器加载代码,"i"表示立即数,"o"表示使用内存地址并可以加偏移值,"d"使用寄存器edx,"a"使用寄存器eax。
重点看输入寄存器:
: "i" ((short)(0x8000+(dpl<<13)+(type<<8))), \
%0:输入项是立即数,0x8000对应P=1,(dpl<<13)对应DPL=0,(type<<8)对应TYPE=0xe,386中断门
"o"(*((char *) (gate_addr))), \
%1:gate_addr低4字节,对应LABEL_IDT[n] 低4字节
"o"(*(4+(char *) (gate_addr))), \
%2:gate_addr高4字节,对应LABEL_IDT[n] 高4字节
"d"((char *) (addr)),
把偏移地址放到edx,对应函数divide_error的地址。
"a"(0x00080000))
把0x00080000放到eax
总结:
eax=0x00080000,edx=÷_error,%0=访问,%2= LABEL_IDT[n]低4字节,%2= LABEL_IDT[n] 高4字节
上面我们得到了所有的值,那么汇编语句无非就是把它们放到合适的位置,那具体应该放那呢?下面的两张图描述了一个中断门中的内容,也就是LABEL_IDT[n]中应该填的内容。
P:存在位,占1位。P=1表示段在内存中存在;P=0表示段在内存中不存在
DPL:特权级,占2位。0~3
S: 占1位。S=0,系统段或门描述符;S=1,数据段或代码段描述符。
TYPE:描述符类型,占4位。0xe表示386中断门,0xf表示386陷阱门。
通过上面的两张图我们知道,无非也就是在 LABEL_IDT[n]填对应的数据,不过在这里用%1和%2表示LABEL_IDT[n]而已。下面解释一下汇编语句:
"movw%%dx,%%ax\n\t" \
edx的低16位放到eax的低16位,eax=0x00080000|divide_error地址的低16位
"movw %0,%%dx\n\t" \
%0的内容放到dx寄存器,edx高16为divide_error地址的高16位,edx低16为访问权字段的内容
"movl %%eax,%1\n\t" \
%1=eax,即0x00080000|divide_error地址的低16位,对应selector为0x0008
"movl %%edx,%2" \
%1=edx,即divide_error地址的高16位|访问权字段的内容
为了加深印象我们用c再重写一下_set_gate函数:
typedef unsigned int u32; typedef unsigned short u16; typedef unsigned char u8; typedef struct s_gate { u16 offset_low; u16 selector; u8 dcount; u8 attr; u16 offset_high; }GATE; void _set_gate(GATE *gate_addr,u8 type,u8 dpl,void (*addr)()) { gate_addr->offset_low = ((u32)addr) & 0xFFFF; gate_addr->selector = 0x08; gate_addr->dcount = 0; gate_addr->attr = 0x80 | type | (dpl << 5);//0x80对应图中的存在位P gate_addr->offset_high = ((u32)addr >> 16) & 0xFFFF; }
我们写一个函数,让一个变量每次加1,然后打印出来。
int i =0;
void do_divide_error(long esp, long error_code)
{
i++;
disp_int(i);
outb(0x20,0x20);
}
然后在main函数中添加如下语句,32是时钟中断,do_divide_error是中断服务函数。
set_intr_gate(32,&do_divide_error);
sti();
不过你可能很失望,因为打印了一个00000001h就停止了。
下面讲一下中断门和陷阱门的区别
通过中断门的转移和通过陷阱门的转移之间的差别只是对IF标志的处理。对于中断门,在转移过程中把IF置为0,使得在处理程序执行期间屏蔽掉INTR中断(当然,在中断处理程序中可以人为设置IF标志打开中断,以使得在处理程序执行期间允许响应可屏蔽中断);对于陷阱门,在转移过程中保持IF位不变,即如果IF位原来是1,那么通过陷阱门转移到处理程序之后仍允许INTR中断。因此,中断门最适宜于处理中断,而陷阱门适宜于处理异常。
因为上面我们用的是中断门,在转移过程中把IF置为0,屏蔽了INTR中断,所以我们看到只打印了一个00000001h就停止了。我们只要在此处用陷阱门就会不停的打印了,其实中断门和陷阱门真的很像,稍加修改即可。
#define set_trap_gate(n,addr) \
_set_gate(&idt[n],15,0,addr)
使用:
set_trap_gate(32,&do_divide_error);
sti();
在之前实验的时候把中断门和陷阱门搞反了,还怀疑是c代码返回时用了iret,objdump后发现根本不是那么回事,所以使用的时候一定要小心了。
为了让中断门正常工作我们添加一个asm.S,我们先保存所有寄存器的值,然后调用中断处理函数,最后将恢复寄存器,执行iret返回,代码如下:
.globl divide_error divide_error: pushl $do_divide_error no_error_code: xchgl %eax,(%esp)//eax寄存器与esp指向的地址的内容互换(如eax=do_divide_error的地址),栈顶存放eax的内容 pushl %ebx//相当于eax,ebx,ecx依次入栈,所以有最后的popl %eax,然后将错误处理函数(如:do_divide_error)的地址放到eax寄存器 pushl %ecx pushl %edx pushl %edi pushl %esi pushl %ebp push %ds push %es push %fs pushl $0//将0作为错误码入栈,作为函数参数,对应error_code lea 44(%esp),%edx//我们push了11个寄存器,每个占4个字节,共44个字节,所以开始时esp的值为esp+44 pushl %edx//edx入栈,其实就是原来的esp入栈,作为函数参数,对应esp movl $0x20,%edx//初始化段寄存器ds、es和fs,加载内核数据段选择符 mov %dx,%ds mov %dx,%es mov %dx,%fs call *%eax//调用c参数,如do_divide_error addl $8,%esp pop %fs pop %es pop %ds popl %ebp popl %esi popl %edi popl %edx popl %ecx popl %ebx popl %eax iret
同时中断门修改如下: