6 中断处理程序的制作(harib03e)重印时的补充说明:本文中只讲到了IRQ1和IRQ12的中断处理程序。事实上附属光盘中还有IRQ7的中断处理程序。要它干什么呢?因为对于一部分机种而言,随着PIC的初始化,会产生一次IRQ7中断,如果不对该中断处理程序执行STI(设置中断标志位,见第4章),操作系统的启动会失败。关于inthandler27的处理内容,大家读一读7.1节会更容易理解。

今天的内容所剩不多了,大家再加一把劲。鼠标是IRQ12,键盘是IRQ1,所以我们编写了用于INT 0x2c和INT 0x21的中断处理程序(handler),即中断发生时所要调用的程序。

int.c的节选

void inthandler21(int *esp)
/* 来自PS/2键盘的中断 */
{
    struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO;
    boxfill8(binfo->vram, binfo->scrnx, COL8_000000, 0, 0, 32 * 8-1, 15);
    putfonts8_asc(binfo->vram, binfo->scrnx, 0, 0, COL8_FFFFFF, "INT 21 (IRQ-1) : PS/2 keyboard");
    for (; ; ) {
        io_hlt();
    }
}

正如大家所见,这个函数只是显示一条信息,然后保持在待机状态。鼠标的程序也几乎完全一样,只是显示的信息不同而已。“只写鼠标程序不就行了吗,怎么键盘也写了呢?”,因为键盘与鼠标的处理方法很相像,所以顺便写了一下。inthandler21接收了esp指针的值,但函数中并没有用。在这里暂时不用esp,不必在意。

■■■■■

如果这样就能运行,那就太好了,可惜还不行。中断处理完成之后,不能执行“return; ”(=RET指令),而是必须执行IRETD指令,真不好办。而且,这个指令还不能用C语言写对于我们今天这个程序来说,在中断处理程序中无限循环,IRETD指令得不到执行,所以怎么都行。之所以说“不能用C语言来写”,是为了今后。。所以,还得借助汇编语言的力量修改naskfunc.nas。

本次的naskfunc.nas节选

        EXTERN _inthandler21, _inthandler2c

_asm_inthandler21:
        PUSH     ES
        PUSH     DS
        PUSHAD
        MOV      EAX, ESP
        PUSH     EAX
        MOV      AX, SS
        MOV      DS, AX
        MOV      ES, AX
        CALL     _inthandler21
        POP      EAX
        POPAD
        POP      DS
        POP      ES
        IRETD

我们只解释键盘程序,因为鼠标程序和它是一样的。最后的IRETD刚才已经讲过了。最开头的EXTERN指令,在调用(CALL)的地方再进行说明。这样一来,问题就只剩下PUSH和POP了。

■■■■■

继续往下说明之前,我们要先好好解释一下栈(stack)的概念。

写程序的时候,经常会有这种需求——虽然不用永久记忆,但需要暂时记住某些东西以备后用。这种目的的记忆被称为缓冲区(buffer)。突然一下子接收到大量信息时,先把它们都保存在缓冲区里,然后再慢慢处理,缓冲区一词正是来源于这层意思。根据整理记忆内容的方式,缓冲区分为很多种类。

最简单明了的方式,就是将信息从上面逐渐加入进来,需要时再从下面一个个取出。

缓冲的种类(1)

最先加入的信息也最先取出,所以这种缓冲区是“先进先出”(first in, first out),简称FIFO。这应该是最普通的方式了。有的书中也会称之为“后进后出”(last in, last out),即LILO。叫法虽然不同,但实质上是同样的东西。

下面要介绍的一种方式,有点类似于往桌上放书,也就是信息逐渐从上面加入进来,而取出时也从最上面开始。

缓冲的种类(2)

最先加入的信息最后取出,所以这种缓冲区是“先进后出”(first in, last out),简称FILO。有的书上也称之为“后进先出”(last in, first out),即LIFO。

■■■■■

这里要说明的栈,正是FILO型的缓冲区。PUSH将数据压入栈顶,POP将数据从栈顶取出。PUSH EAX这个指令,相当于:

ADD ESP, -4
MOV [SS:ESP], EAX

也就是说,ESP的值减去4,以所得结果作为地址值,将寄存器中的值保存到该地址所对应内存里。反过来,POP EAX指令相当于:

MOV EAX, [SS:ESP]
ADD ESP,4

CPU并不懂栈的机制,它只是执行了实现栈功能的指令而已。所以,即使是PUSH太多,或者POP太多这种没有意义的操作,基本上CPU也都会遵照执行。

所以,如果写了以下程序,

PUSH EAX
PUSH ECX
PUSH EDX
各种处理
POP  EDX
POP  ECX
POP  EAX

在“各种处理”那里,即使把EAX, ECX, EDX改了,最后也还会恢复回原来的值……其实ES、DS这些寄存器,也就是靠PUSH和POP等操作而变回原来的值的。

■■■■■

还有一个不怎么常见的指令PUSHAD,它相当于:

PUSH EAX
PUSH ECX
PUSH EDX
PUSH EBX
PUSH ESP
PUSH EBP
PUSH ESI
PUSH EDI

反过来,POPAD指令相当于按以上相反的顺序,把它们全都POP出来。

■■■■■

结果,这个函数只是将寄存器的值保存到栈里,然后将DS和ES调整到与SS相等,再调用_inthandler21,返回以后,将所有寄存器的值再返回到原来的值,然后执行IRETD。内容就这些。如此小心翼翼地保存寄存器的值,其原因在于,中断处理发生在函数处理的途中,通过IRETD从中断处理返回以后,如果寄存器的值乱了,函数就无法正常处理下去了,所以一定要想尽办法让寄存器的值返回到中断处理前的状态。

关于在DS和ES中放入SS值的部分,因为C语言自以为是地认为“DS也好,ES也好,SS也好,它们都是指同一个段”,所以如果不按照它的想法设定的话,函数inthandler21就不能顺利执行。所以,虽然麻烦了一点,但还是要这样做。

这么说来,CALL也是一个新出现的指令,它是调用函数的指令。这次要调用一个没有定义在naskfunc.nas中的函数,所以我们最初用一个EXTERN指令来通知nask:“马上要使用这个名字的标号了,它在别的源文件里,可不要搞错了”。

■■■■■

好了,这样_asm_inthandler21的讲解就没有问题了吧。下面要说明的,就是要将这个函数注册到IDT中去这一点。我们在dsctbl.c的init_gdtidt里加入以下语句。

/* IDT的设定 */
set_gatedesc(idt + 0x21, (int) asm_inthandler21, 2 * 8, AR_INTGATE32);
set_gatedesc(idt + 0x2c, (int) asm_inthandler2c, 2 * 8, AR_INTGATE32);

asm_inthandler21注册在idt的第0x21号。这样,如果发生中断了,CPU就会自动调用asm_inthandler21。这里的2 * 8表示的是asm_inthandler21属于哪一个段,即段号是2,乘以8是因为低3位有着别的意思,这里低3位必须是0。

所以,“2 * 8”也可以写成 “2<<3”,当然,写成16也可以。

不过,号码为2的段,究竟是什么样的段呢?

set_segmdesc(gdt + 2, LIMIT_BOTPAK, ADR_BOTPAK, AR_CODE32_ER);

程序中有以上语句,说明这个段正好涵盖了整个bootpack.hrb。

最后的AR_INTGATE32将IDT的属性,设定为0x008e。它表示这是用于中断处理的有效设定。

■■■■■

还有就是对bootpack.c的HariMain的补充。“io_sti(); ”仅仅是执行STI指令,它是CLI的逆指令。就是说,执行STI指令后,IF(interrupt flag,中断许可标志位)变为1, CPU接受来自外部设备的中断(参考4.6节)。CPU的中断信号只有一根,所以IF也只有一个,不像PIC那样有8位。

在HariMain的最后,修改了PIC的IMR,以便接受来自键盘和鼠标的中断。这样程序就完成了。只要按下键盘上某个键,或动一动鼠标,中断信号就会传到CPU,然后CPU执行中断处理程序,输出信息。

■■■■■

那好,我们运行一下试试看。“make run”……然后按下键盘上的“A”……哦!显示了一行信息。

让我们先退出程序,再运行一次“make run”吧。这次我们随便转转鼠标。但怎么让鼠标转起来呢?首先我们在QEMU画面的某个地方单击一下,这样就把鼠标与QEMU绑定在一起了,鼠标事件都会由QEMU接受并处理。然后我们上下左右移动鼠标,就会产生中断。哎?怎么没反应呢?

在这个状态下,我们不能对Windows进行操作,所以只好按下Ctr键再按Alt键,先把鼠标从QEMU中解放出来。然后点击“×”,关闭QEMU窗口。

虽然今天的结果还不能让人满意,但天色已经很晚了,就先到此为止吧。原因嘛,让我们来思考一夜。但不论怎么说,键盘的中断设定已经成功了,至于鼠标的问题,肯定也能很快找到原因的。我们明天再继续吧。