【读书笔记-《30天自制操作系统》-5】Day6

本篇的内容重点是中断,首先对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可以退出。尝试移动鼠标,画面上并未出现提示信息。这是什么原因呢?下一篇继续讲解。敬请期待。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值