设置完GDT以后,接下来就可以做一个简单的时钟中断实验。为了使能时钟中断,需要将时钟中断的屏蔽位打开,打开的方式很简单,代码如下:
1 movb $0xfe,%al
2 outb%al,$0x21
3
4 movb $0xff,%al
5 outb%al,$0xA1
只需要修改setup.S中设置OCW1的语句,即向0x21端口写入0xfe(11111110b),这样就把主8259A的IRQ0打开了,IRQ0对应的外部中断正好是时钟中断,同时向0xA1端口继续写入0xff(11111111b),表示屏蔽来自8259A的全部中断。
中断打开之后,需要为其设置中断服务程序,并同时修改IDT。时钟中断服务程序如下:
1 .code32
2 .text
3 .globl startup_32
4 startup_32:
5 ...
6
7 call setup_idt
8 call set_clock_idt
9 int $0x80
10 sti
11
12 loop:
13 jmploop
14
15 setup_idt:
16...
17
18 ignore_int:
19...
20
21 set_clock_idt:
22 leal clock_handle,%edx
23 movl $0x00080000, %eax
24 movw %dx,%ax
25 movw $0x8e00,%dx
26 leal idt,%edi
27 addl $0x100,%edi
28 movl %eax,(%edi)
29 movl%edx,4(%edi)
30 ret
31 clock_handle:
32 movl $0x96,%edi
33 incb%gs:(%edi)
34 /* 向0x20端口发送EOI */
35 movb $0x20,%al
36 outb%al,$0x20
37 iret
38
39 .align 4
40 .word 0
41 idt_descr:
42 .word 256 *8-1
43 .long idt
44
45 idt:
46 .fill 256,8,0
从31行开始是中断服务程序,主要用于将%gs:(%edi)内置位置的一个字节值加1,而%gs中的选择子对应GDT的第三项,它的段基地址为显存的基地址,即0xb8000。在第9行的软中断触发之后,会在屏幕的右上角打印一个红色字母I,而clock_handle的功能是将这个字节加1。也就是说,时钟中断到来之后红色字母将变成J,下一次时钟中断到来之后将变成K,依此类推。clock_handle处理程序向20h端口写入了0x20,也就是OCW2的EOI标识,通知8259A将当前处理程序结束,并且通过执行iret指令返回,这样8259A就可以继续接收下一个中断。
第21~30行对IDT进行了设置,并且设置了其中一个表项(第29行),它相对idt的偏移为0×100(256),因为一个IDT占用8个字节,所以这里设置的中断向量号为256/8=32=0×20,正是主8259A的IRQ0对应的时钟中断向量号。还需要注意的是第22行、23行对中断描述符中服务程序入口的设置,对照图2-10可以推断,set_clock_idt程序的作用是将第32号中断的中断处理程序设置为clock_handle。它所对应的中断描述符的8字节的最终内容为:
❑第0~15位保存了中断处理程序clock_handle的低16位。
❑第16~31位保存了段选择子0x0008表示的是代码段。
❑第32~47位保存了0x8E00,设置了IDT的类型、特权级等信息。
❑第48~63位保存了中断处理程序clock_handle的高16位。如此一来,在初始化所有IDT之后,调用set_clock_idt,就成功设置了时钟中断的处理程序,虽然已经打开了时钟中断的屏蔽位,但是还需要一个条件,时钟中断才能被8259A成功接收,那就是中断标识位IF(Interrupt Flag,中断标志)。
通过第10行的sti指令,可以将IF打开,这次时钟中断就可以被CPU响应并处理。如果一切都顺利的话,重新编译内核会发现原本红色字符I出现的位置会不停地加1,呈现出字符不断跳动的状态。