6 中断处理程序的制作(harib03e)

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

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

int.c的节选

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

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

■■■■■

如果这样就能运行,那就太好了,可惜还不行。中断处理完成之后,不能执行“return;”(=RET指令), 而是必须执行IRETD指令,真不好办。而且,这个指令还不能用C语言写2。所以,还得借助汇编语言的力量修改naskfunc.nas。

2 对于我们今天这个程序来说,在中断处理程序中无限循环,IRETD指令得不到执行,所以怎么都行。之所以说“不能用C语言来写”,是为了今后。

本次的naskfunc.nas节选

  1. EXTERN _inthandler21, _inthandler2c
  2. _asm_inthandler21:
  3. PUSH ES
  4. PUSH DS
  5. PUSHAD
  6. MOV EAX,ESP
  7. PUSH EAX
  8. MOV AX,SS
  9. MOV DS,AX
  10. MOV ES,AX
  11. CALL _inthandler21
  12. POP EAX
  13. POPAD
  14. POP DS
  15. POP ES
  16. IRETD

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

■■■■■

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

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

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

6 中断处理程序的制作(harib03e) - 图1

缓冲的种类(1)

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

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

6 中断处理程序的制作(harib03e) - 图2

缓冲的种类(2)

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

■■■■■

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

  1. ADD ESP,-4
  2. MOV [SS:ESP],EAX

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

  1. MOV EAX,[SS:ESP]
  2. ADD ESP,4

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

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

  1. PUSH EAX
  2. PUSH ECX
  3. PUSH EDX
  4. 各种处理
  5. POP EDX
  6. POP ECX
  7. POP EAX

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

■■■■■

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

  1. PUSH EAX
  2. PUSH ECX
  3. PUSH EDX
  4. PUSH EBX
  5. PUSH ESP
  6. PUSH EBP
  7. PUSH ESI
  8. PUSH EDI

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

■■■■■

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

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

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

■■■■■

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

  1. /* IDT的设定 */
  2. set_gatedesc(idt + 0x21, (int) asm_inthandler21, 2 * 8, AR_INTGATE32);
  3. 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的段,究竟是什么样的段呢?

  1. 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”……哦!显示了一行信息。

6 中断处理程序的制作(harib03e) - 图3

按下字母A之后

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

6 中断处理程序的制作(harib03e) - 图4

哎?明明动了鼠标嘛?!

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

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