本篇的内容重点是中断,首先对GDT做一些补充说明,接着围绕着中断介绍PIC及其初始化,讲解中断的处理逻辑。
1. 关于GDT的补充说明
本篇的内容将要详细介绍IDT的设置,在此之前再来补充说明以下GDT。
上一篇中的load_gdtr函数代码如下:
_load_gdtr: ; void load_gdtr(int limit, int addr);
MOV AX,[ESP+4] ; limit
MOV [ESP+6],AX
LGDT [ESP+6]
RET
GDTR是CPU中一个48位的寄存器,这里是将段上限和地址赋值给这个寄存器。而这个寄存器的赋值只能通过一个内存地址读取6个字节,相应的指令就是LGDT。这个寄存器的低16位表示段上限,等于GDT的有效字节数-1;而高32位则是GDT的起始地址。
调用这个函数时,在[ESP+4]中存放的是段上限0x0000ffff,而[ESP+8]中存放的是地址0x00270000。而由于低字节存放在内存地址较小的字节中,将[ESP+4]中存放的内容与[ESP+8]中存放的内容列出,则是[FF FF 00 00 00 00 27 00],由于要读取6个字节存放到GDTR寄存其中,通过MOV[ESP+6], AX后,[ESP+4]和[ESP+8]中的内容变为[FF FF FF FF 00 00 27 00],此时从[ESP+6]开始读取6个字节,得到的就是我们需要的[FF FF 00 00 27 00]。
再来补充说明一下上一篇略过的段属性。
前面说过,表示段的信息需要用8个字节共64位,其中起始地址就占了4个字节32位,也就是基址base。为了兼容较老的CPU,这4个字节又分成了3段:low(2字节),mid(1字节),high(1字节)。
然后是段上限limit。由于基址已经占用了4个字节,剩余的4个字节除了段上限还有段属性,因此段上限不能使用全部的32位,只用了20位。如果用20位来表示段上限,起始只能覆盖1M的地址范围。为了解决这个问题,段属性中增加了一个标志位Gbit。这个标志位置1的时候,limit的单位就不是byte字节而是page页。一个页的大小是4KB。这样段上限所能覆盖的内存地址就变成了1M x 4KB = 4GB。
最后的12位就是留给段属性ar的。ar的高4位被称为扩展访问权,是在80286之后才出现的,由GD00构成。G就是用于表示limit单位的Gbit,D是指段的模式,1表示32位模式,0表示16位模式。这里的16位其实是为了兼容80286的CPU。因此除了运行80286的程序外,都会使用D=1的模式。
而ar的低8位的内容,作者在这里也只是简单进行了介绍。取值有以下几种:
0x00:未使用的记录表
0x92:系统专用,可读写的段,不可执行
0x9a:系统专用,可执行的段,可读不可写
0xf2:应用程序用,可读写的段,不可执行
0xfa:应用程序用,可执行的段,可读不可写
这就涉及到CPU运行是的系统模式和应用模式了。操作系统是“管理程序”,应用程序是“被管理程序”,运行时处于不同的模式。因为操作系统具有很高的权限,其中的指令如LGDT等,如果被恶意的应用程序随意调用,很容易就会对操作系统造成破坏。根据程序所位于的段的访问权限,可以判断CPU处于系统模式还是应用模式,对于权限不足的访问会予以阻挡。
2. PIC介绍与初始化
关于GDT的补充说明介绍完了。GDT不仅使应用程序访问内存更加方便,还提供了一种保护机制。
接下来我们还是回到中断,来详细地说明中断的使用。
首先是实现中断的硬件基础——PIC,programmable interrupt controller。CPU本身的设计只能处理一个中断信号(interrupt request,IRQ),而PIC是将8个中断信号集合成一个中断信号的装置,只要输入有一个中断信号出发,PIC就将输出一个中断信号给CPU,这样CPU就可以实现更多的中断信号处理了。实际应用中,使用了2个PIC,共可以处理15个中断信号。CPU与2个PIC的硬件连接关系如图所示:
与CPU直接相连的PIC称为主PIC,与主PIC相连的PIC称为从PIC。主PIC负责处理0-7号中断,从PIC负责处理8-15号中断,二者之间是通过2号IRQ进行连接的。
PIC的初始化程序如下:
void init_pic(void)
/* PIC的初始化 */
{
io_out8(PIC0_IMR, 0xff ); /* 禁止所有中断 */
io_out8(PIC1_IMR, 0xff ); /* 禁止所有中断 */
io_out8(PIC0_ICW1, 0x11 ); /* 边沿触发模式(edge trigger mode) */
io_out8(PIC0_ICW2, 0x20 ); /* IRQ0-7由AINT20-27接收 */
io_out8(PIC0_ICW3, 1 << 2); /* PIC1由IRQ2连接 */
io_out8(PIC0_ICW4, 0x01 ); /* 无缓冲区模式 */
io_out8(PIC1_ICW1, 0x11 ); /* 边沿触发模式(edge trigger mode) */
io_out8(PIC1_ICW2, 0x28 ); /* IRQ8-15由AINT28-2f接收 */
io_out8(PIC1_ICW3, 2 ); /* PIC1由IRQ2连接 */
io_out8(PIC1_ICW4, 0x01 ); /* 无缓冲区模式 */
io_out8(PIC0_IMR, 0xfb ); /* 11111011 PIC1以外全部禁止 */
io_out8(PIC1_IMR, 0xff ); /* 11111111 禁止所有中断 */
return;
}
PIC对于CPU来说是外设,因此这里还是使用io_out8对端口号进行写入,端口号的定义如下:
#define PIC0_ICW1 0x0020
#define PIC0_OCW2 0x0020
#define PIC0_IMR 0x0021
#define PIC0_ICW2 0x0021
#define PIC0_ICW3 0x0021
#define PIC0_ICW4 0x0021
#define PIC1_ICW1 0x00a0
#define PIC1_OCW2 0x00a0
#define PIC1_IMR 0x00a1
#define PIC1_ICW2 0x00a1
#define PIC1_ICW3 0x00a1
#define PIC1_ICW4 0x00a1
IMR是interrupt mask register的缩写,意思是中断屏蔽寄存器。8位分别对应8路IRQ信号,哪一位置为1,则对应的IRQ信号被屏蔽。在某些情况,如进行中断设置时,是需要屏蔽中断信号以防止干扰的。
ICW是initial control word的缩写,意为初始化控制数据。ICW共有4个,编号为1-4,共4个字节的数据。其中ICW3是有关主从连接的设定,每一位对应一个IRQ信号。而实际上硬件已经通过IRQ2连接了,软件设置如果不一致会发生错误,因此ICW3设置为0x00000100。
ICW2是不同的操作系统可以进行不同设定的,决定了IRQ通知CPU的中断号。PIC产生中断时,发送给CPU两个字节的数据。其中第一个字节0xcd与调用BIOS的INT指令一致,而第二个字节就是PIC发送中断号,这样类似与调用BIOS,CPU就执行了对应中断号的INT指令。这里用设置INT 0x20-0x2f接收中断信号IRQ0-IRQ15。(在32位模式下,INT 0x00-0x1f是CPU的系统保护中断,这里不能使用)
介绍完了PIC,就可以进行中断处理程序的编写了。
鼠标是IRQ12,键盘是IRQ1,分别对应INT 0x2c 和INT 0x21,中断处理程序如下:
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();
}
}
void inthandler2c(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 2C (IRQ-12) : PS/2 mouse");
for (;;) {
io_hlt();
}
}
中断处理程序处理完成后,需要执行IRETD指令进行返回,这里还需要借助汇编语言:
EXTERN _inthandler21, _inthandler27, _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
_asm_inthandler2c:
PUSH ES
PUSH DS
PUSHAD
MOV EAX,ESP
PUSH EAX
MOV AX,SS
MOV DS,AX
MOV ES,AX
CALL _inthandler2c
POP EAX
POPAD
POP DS
POP ES
IRETD
这里需要说明以下栈。栈是一种先入后出的数据结构,PUSH将数据压入栈顶,POP将数据从栈顶弹出。
执行PUSH EAX指令,相当于:
ADD ESP, -4
MOV [SS:ESP], EAX
将ESP的值减去4,所得结果作为地址值,将寄存器中的值保存在该地址所对应的内存中;
POP EAX指令,相当于:
MOV EAX, [SS:ESP]
ADD ESP, 4
在CALL _inthandler执行之前的各种PUSH操作,即是将此时的运行状态保存在栈中,中断处理结束后再通过POP从栈中恢复,这样可以接着中断处理前的状态继续执行。
中断函数完成了,接下来还要将它注册到IDT中。
/* IDT设置*/
set_gatedesc(idt + 0x21, (int) asm_inthandler21, 2 * 8, AR_INTGATE32);
set_gatedesc(idt + 0x2c, (int) asm_inthandler2c, 2 * 8, AR_INTGATE32);
这里28中,2表示段号,而28实际是2<<3,因为低3位有其他用途,必须设置为0。而AR_INTGATE32表示IDT的属性,设置为0x008e,表示用于中断处理的有效设定。
最后在主程序中设置MIR,接受键盘与鼠标的中断。
io_out8(PIC0_IMR, 0xf9);
io_out8(PIC1_IMR, 0xef);
按照上面的程序,按下键盘按键或者移动鼠标,屏幕上就会输出一行信息。
实际按下键盘:
在QEMU画面中点击鼠标,就将鼠标绑定到QEMU,鼠标的事件会有QEMU接收并进行处理,按下ctrl + alt可以退出。尝试移动鼠标,画面上并未出现提示信息。这是什么原因呢?下一篇继续讲解。敬请期待。