平台
处理器:Intel Celeron(R) Dual-Core CPU
操作系统:Windows7 专业版 x86
阅读书籍:《30天自制操作系统》—川合秀实[2015.03.19 –03.21]
将《30天自制操作系统》简称为“书”。对于书中的工具,可以专门对其笔记学习。
工具:../toolset/
1 实现鼠标中断涉及到的内容
要实现鼠标中断,会涉及以下内容:
- GDT和IDT
- PIC和鼠标控制电路
GDT和IDT是因为鼠标中断程序而被涉及到的,GDT和IDT需要用程序来设定;PIC是接受中断请求(IRQ)的芯片,通过配置可以使它接收或屏蔽某一个中断请求;鼠标控制电路既控制鼠标中断信号的产生也控制鼠标能否向控制电路传输信号。
总之,要想实现鼠标中断功能,就必须按照要求设置好GDT、IDT以及PIC和鼠标控制电路。
1.1 GDT
关于GDT内容,《x86汇编语言-从实模式到保护模式》的10~ 12章节。核心是x86保护模式合成内存地址的方式。我做了简要不清楚的“[x86保护模式] CPU合成内存地址的方式”笔记。
1.1.1 实现GDT
(1) 找一块8kb不用的内存地址空间
“书”依据内存地址空间的分布,找了一块还没被用的8kb大小的内存地址空间:0x270000~ 0x27ffff。
(2) 指定GDT和段的段描述符
如果将0x270000这个值赋给GDTR寄存器的高32位,把上每个段大小的上限赋给GDTR的低16位,那么CPU就把0x270000~ 0x27ffff这段内存地址空间当作GDT。并且,段号与段描述符的关系也会成一一对应的关系。
由于无对GDTR赋值的C指令,所以这个程序还得在naskfunc.nas中用汇编指令来完成:
1. _load_gdtr: ; void load_gdtr(int limit, int addr);
2. MOV AX,[ESP+4] ; limit
3. MOV [ESP+6],AX
4. LGDT [ESP+6]
5. RETDWORD[ESP+ 4]里存放的limit实参(段上限,如0x0000ffff),DWORD[ESP + 8]里存放的是addr实参(段地址,如0x00270000)。这两个实参在这16字节内存中存放的格式为[ff ff 00 00 00 00 27 00](低位放在内存地址小的字节里)。用“MOV AX, [ESP + 4], MOV [ESP + 6], AX”指令后,这16字节的内容就变成了[ff ff ff ff 00 00 27 00]。然后LGDT指令就将6字节[ESP + 6]内容即[ff ff 00 00 27 00]这6个字节的内容装载到了GDTR寄存器中。GDTR寄存器低16位表示段上限,高32位表示段地址。
在naskfunc.nas文件中定义这个函数之后,在C程序中将其声明再调用就为CPU指定了GDT。
(3) 初始化GDT
CPU已经认定0x270000~ 0x27ffff这段内存地址空间为GDT。
[1] 描述段描述符的结构体
段描述符在GDT中有8个字节,但具体怎么组织这8个字节的内容还得遵循CPU的规定(GDT内的段描述符)。这8字节组织段描述符的方式可以用结构体描述如下:
struct SEGMENT_DESCRIPTOR {
short limit_low, base_low; //段上限的低16位,段基址的低16位
char base_mid, access_right; //段基址的中间8位,段的访问权限
char limit_high, base_high; //段上限的高4位,段基址高8位,段属性高4位(limit_high高4位)
};
这有按照这样的格式组织这8字节内容,CPU读取后才能正确的解释。
[2] 初始化GDT内存块
上电后,RAM内是随机分配的高低电平,也就是乱码。确定GDT后应当给这段内存一个初始化(通过能够设置GDT段描述符的函数)。
/*设定GDT单元(8字节)短信息
* sd为短信息结构体
* 段的字节数
* 段基址
* 段的权限
*/
void set_segmdesc(struct SEGMENT_DESCRIPTOR *sd, unsigned int limit, int base, int ar)
{
if (limit > 0xfffff) {
ar |= 0x8000; /* G_bit = 1 */
limit /= 0x1000;
}
sd->limit_low = limit & 0xffff;
sd->base_low = base & 0xffff;
sd->base_mid = (base >> 16) & 0xff;
sd->access_right = ar & 0xff;
sd->limit_high = ((limit >> 16) & 0x0f) | ((ar >> 8) & 0xf0);
sd->base_high = (base >> 24) & 0xff;
return;
}
void init_gdt(void)
{
struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR *) 0x00270000;
int i;
for (i = 1; i < 8192; i++) {
set_segmdesc(gdt + i, 0, 0, 0);
}
//告诉CPU GDT的信息,从而使用段号时就能获得段号对应的消息
load_gdtr(0xffff, 0x00270000);
return;
}
调用set_segmdesc()函数将GDT内的各个字节的内容都初始化为0,再调用用汇编编写的load_gdtr()函数告知CPU关于GDT的信息。
1.1.2 CPU管理内存的分段方式的机制
GDT设置好后,给段寄存器高13位赋值一个段号(如2)后,只要在程序中使用此段寄存器来表示段地址,CPU将根据段号2到GDT的第2个8字节读出段描述符。根据段描述符的段基址(0x00280000)和段大小(0x0007ffff)将内存地址空间0x00280000~ 0x002fffff这块内存地址空间分割成一个段,凡是用此段号表示段地址的程序都会保存在这个段对应实际内存中。并且,CPU还会根据GDT第2个8字节的属性字段设置这个段的属性。
1.2 IDT
1.2.1 引出IDT和IDT的机制
IDT的机制跟GDT差不多。
(1) 中断
当某事件(除数为0,鼠标点击)发生时,让CPU去执行所发生事件对应的函数。此函数执行完毕返回到原来CPU执行程序处。
(2) 中断号
计算机允许多个中断,为了区分它们,对这些中断进行编号,这些对中断的编号就是中断号。如鼠标中断号为0x2c,键盘中断号为0x21。如果有256个可以发生中断的事件(器件),那么中断号为0 ~ 255。
(3) 中断信息
作为一个中断,每个中断需要包含以下(中断)信息:中断函数地址,中断函数所属段,中断函数属性。
(4) 表示中断号和中断信息
中断号由发生中断的器件直接(或间接)发给CPU。比如点击鼠标时,和鼠标相连的芯片会给CPU传递0x2c的中断号。
中断信息用8字节内存来表示。
(5) IDT
IDT(interruptdescriptor table,中断记录表)就是一段大小为256 * 8 = 2kb的内存(地址空间),每连续的8个字节保存一个中断信息。编写操作系统的人可以随便指一段2kb大小没被其它程序使用的内存地址空间作为IDT。
(6) IDT中中断信息和中断号的对应
中断由中断号来区分,IDT中的中断信息怎么和中断号发生联系呢?是这样的,只要将IDT的首地址赋给IDTR的高32位,每个中断程序大小的上限值赋给IDTR的低16位,CPU就将IDT第1个8个字节的内容作为中断号为0的中断的中断信息,将IDT第2个8个字节的内容作为中断号为1的中断信息,……,将IDT的256个字节的内容作为中断号为255的中断信息。
1.2.2 实现IDT
(1) 找一块2kb不用的内存(地址空间)
根据内存地址空间分布,“书”中将IDT的地址设为0x0026f800~ 0x0026ffff。
(2) 指定IDT和中断号的中断信息
根据“3.1.6”的描述,给IDTR赋值,让0x0026f800~ 0x0026ffff成为IDT。这个过程必须在naskfunc.nas用汇编指令来完成:
1. _load_idtr: ; void load_idtr(int limit, int addr);
2. MOV AX,[ESP+4] ; limit
3. MOV [ESP+6],AX
4. LIDT [ESP+6]
5. RET
此程序含义同_load_gdtr类似。
(3) 初始化IDT
[1] 描述中断信息的结构体
中断信息必须按照CPU的要求来组织。CPU所要求的8字节中断信息格式可以用以下结构体来描述:
struct GATE_DESCRIPTOR {
short offset_low, selector; //中断函数地址的低16位,所属段号
char dw_count, access_right; //属性(谁的)的高8位,属性低8位(访问权限)
short offset_high; //中断函数地址的高16位
};
[2] 初始化IDT内存块
通过IDTR指定IDT后,IDT内只是一片乱码,应该给IDT内存块一个初始化的值(通过能够为IDT指定中断信息的函数)。
void set_gatedesc(struct GATE_DESCRIPTOR *gd, int offset, int selector, int ar)
{
gd->offset_low = offset & 0xffff;
gd->selector = selector;
gd->dw_count = (ar >> 8) & 0xff;
gd->access_right = ar & 0xff;
gd->offset_high = (offset >> 16) & 0xffff;
return;
}
void init_idt(void)
{
struct GATE_DESCRIPTOR *idt = (struct GATE_DESCRIPTOR *) 0x0026f800;
int i;
for (i = 0; i < 256; i++) {
set_gatedesc(idt + i, 0, 0, 0);
}
//给IDTR赋值,告知CPU IDT的信息
load_idtr(0x7ff, 0x0026f800);
return;
}
调用set_gatedesc ()函数将IDT内的各个字节的内容都初始化为0,再调用用汇编编写的load_idtr()函数告知CPU关于IDT的信息。
1.2.3 CPU的中断机制
当中断(如除数为0,鼠标点击)发生时,CPU会收到中断号(如0x2c)。CPU根据中断号(0x2c)到IDT对应的8字节(第0x2d个8字节)读出中断信息,CPU根据段描述符中的中断函数的地址就去执行中断函数。
1.3 PIC
PIC(programmable interrupt controller,可编程中断控制器)是将8个中断信号集合成一个中断信号的装置。PIC监视着输入管脚的8个中断信号,只要有一个中断信号进来,就将唯一的输出管脚信号变成ON,并通知给CPU。
为了处理来自外部8个以上的设备的中断,增设了2个PIC,把允许的外部中断设计成了15个。“书”中的图示:
Figure2. PIC
IRQ(interruptrequest,中断请求)是发生中断时申请中断执行的一个信号。可以对PIC进行设置,使PIC的IRQ0~IRQ15对应的中断号为0x20~ 0x2f,即当中断请求IRQ0来临时,PIC输出给CPU的中断号为0x20,通知CPU去执行0x20对应的中断程序。
1.4 鼠标控制电路
鼠标控制电路在主板之上,但只要不执行激活鼠标的命令,就算点击鼠标的命令传到了鼠标控制电路,鼠标控制电路也不发给CPU。同时,鼠标控制电路也控制鼠标是否能向鼠标控制电路发送信号,默认状态下,鼠标不能往鼠标控制电路发送信号。所以,要想鼠标中断能够被CPU接收,还需要对鼠标控制电路编程。
2 实现鼠标中断
GDT/IDT已经在“1”中指定了,只是还没有往其中设定具有实际意义的内容。
2.1 初始化PIC
#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
void init_pic(void)
{
//禁止主从PIC所有中断
io_out8(PIC0_IMR, 0xff );
io_out8(PIC1_IMR, 0xff );
//主PIC设置
io_out8(PIC0_ICW1, 0x11 ); //边沿触发模式
io_out8(PIC0_ICW2, 0x20 ); //IRQ0-7由INT 20-27接收
io_out8(PIC0_ICW3, 1 << 2); //从PIC由主PIC IRQ2连接
io_out8(PIC0_ICW4, 0x01 ); //无缓冲区模式
//从PIC设置
io_out8(PIC1_ICW1, 0x11 ); //边沿触发模式
io_out8(PIC1_ICW2, 0x28 ); //IRQ8-15由INT 28-2f接收
io_out8(PIC1_ICW3, 2 ); //从PIC由主PIC IRQ2连接
io_out8(PIC1_ICW4, 0x01 ); //无缓冲区模式
io_out8(PIC0_IMR, 0xfb ); //11111011 主PIC允许IRQ2中断
io_out8(PIC1_IMR, 0xff ); // 11111111 从PIC禁止所有中断
return;
}
PIC是外部设备,CPU访问它们要通过OUT/IN指令和端口号。PIC内部有很多寄存器,用端口号码对彼此进行区分,以决定是写入哪一个寄存器。具体得看PIC的手册。
2.2 激活鼠标控制电路
鼠标控制电路包含在键盘控制电路里,如果键盘控制电路的初始化正常完成,鼠标电路控制器的激活也就完成了。
#define PORT_KEYDAT 0x0060
#define PORT_KEYSTA 0x0064
#define PORT_KEYCMD 0x0064
#define KEYSTA_SEND_NOTREADY 0x02
#define KEYCMD_WRITE_MODE 0x60
#define KBC_MODE 0x47
/*让键盘控制电路做好准备,等待控制指令的到来
*/
void wait_KBC_sendready(void)
{
//等待键盘控制电路准备完毕
for (;;) {
if ((io_in8(PORT_KEYSTA) & KEYSTA_SEND_NOTREADY) == 0) {
break;
}
}
return;
}
void init_keyboard(void)
{
//初始化键盘控制电路
wait_KBC_sendready();
io_out8(PORT_KEYCMD, KEYCMD_WRITE_MODE); //模式设定
wait_KBC_sendready();
io_out8(PORT_KEYDAT, KBC_MODE); //鼠标模式
return;
}
对于CPU来说,键盘控制器也是外部设备,也需要OUT/IN指令通过端口号操作。其中in_in8是在naskfunc.nas中用汇编编写的函数:
1. _io_in8: ; int io_in8(int port);
2. MOV EDX,[ESP+4] ; port
3. MOV EAX,0
4. IN AL,DX
5. RET激活鼠标控制器后,就开始发送激活鼠标的指令。发送激活鼠标的指令,一样要想键盘控制器发送指令。
#define KEYCMD_SENDTO_MOUSE 0xd4
#define MOUSECMD_ENABLE 0xf4
void enable_mouse(void)
{
//激活鼠标
wait_KBC_sendready();
io_out8(PORT_KEYCMD, KEYCMD_SENDTO_MOUSE);
wait_KBC_sendready();
io_out8(PORT_KEYDAT, MOUSECMD_ENABLE);
return; //顺利的话,键盘控制会返回ACK(0xfa)
}
如果往键盘控制电路发送指令0xd4,下一个数据就会自动发送给鼠标。收到激活指令的鼠标,会马上给CPU发送答复消息:0xfa。
2.3 鼠标中断程序的制作
由PIC的初始化可知,鼠标的中断请求是IRQ12,对应的中断号为0x2c。编写用于INT0x2c的中断处理程序。
void inthandler2c(int *esp)
{
char str[10];
unsigned char data;
struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO;
io_out8(PIC1_OCW2, 0x64); //告知从PIC已经接受了IRQ12,让从PIC继续监视IRQ12
io_out8(PIC0_OCW2, 0x62); //让主PIC继续监视IRQ2
data = io_in8(0x0060); //0x0060是读鼠标所发数据的端口
sprintf(str, "0x%x", data);
boxfill8(binfo->vram, binfo->scrnx, COL8_000000, 0, 0, 32 * 8 - 1, 15);
putfonts8_asc(binfo->vram, binfo->scrnx, 0, 0, COL8_FFFFFF, str);
return ;
}
中断成立完成之后,必须执行IRETD指令,这个指令只能用汇编来写,所以还得在naskfunc.nas文件中实现:
1. EXTERN _inthandler2c//表示_inthandler2c函数在其它文件中实现(在C程序中函数名就为inthandler2c)
2.
3. _asm_inthandler2c:
4. PUSH ES
5. PUSH DS
6. PUSHAD
7. MOV EAX,ESP
8. PUSH EAX
9. MOV AX,SS
10. MOV DS,AX
11. MOV ES,AX
12. CALL _inthandler2c //调用C程序中的inthandler2c函数
13. POP EAX
14. POPAD
15. POP DS
16. POP ES
17. IRETD2.4 设定GDT/IDT
现在所有的初始化和0x2c的中断程序都准备好了。此时还差两个设置:
- GDT的设置
- IDT的设置
2.4.1 设定GDT
set_segmdesc(gdt + 1, 0xffffffff, 0x00000000, 0x4092);
set_segmdesc(gdt + 2, 0x0007ffff, 0x00280000, 0x409a);
在初始化GDT的基础上,先设定两个段。以上语句对段号为1和2的两个段的短信息进行设定。
段号为1的段,段大小为0xffffffff,大小正好为4GB,段地址为0,它表示的是CPU所能管理的全部内存本身。段属性为0x092(低12位才是段属性),表80286下16位模式,系统专用,可读写的段。不可执行。
段号为2的段,段大小512KB,段基址是0x280000,段属性为0x09a,表80286下16位模式,系统专用,可执行的段。可读不可写。
2.4.2 设定IDT(中断函数注册)
set_gatedesc(idt + 0x2c, (int) asm_inthandler2c, 2 << 3, 0x008e);
asm_inthandler2c函数注册在idt的0x2c号。这样,如果发生中断了,CPU会自动调用asm_inthandler2c。这里的2<< 3表示asm_inthandler2c属于哪一个段,即段号2,往左移动3位是因为段寄存器用高13位表示段号。0x008e表示这个段是用于中断处理的有效设定。
2.5 整理程序并运行
经过2.1~ 2.4的设置,按道理来说,只要操作鼠标,中断函数inthandler2c就能够被调用了。按照各函数的功能整理一下,修改一下Makefile。然后测试一下。打开“!cons_nt.bat”,运行“Makerun”命令,在QEMU中得到以下运行结果:
Figure3. 含鼠标中断程序的操作系统运行结果
程序一运行,就执行了一次鼠标中断函数,打印出0xfa。在激活鼠标的时候,如果激活成功,鼠标会给CPU返回一个信号。所以,最开始就算不动鼠标,鼠标中断函数也会执行一次。
接下来动鼠标,中断函数打印的数据就会发生变化:
Figure4. 移动鼠标
这次的程序被整理在mouse_interrupt文件夹下。
总结
[1] 8086段的划分来自于CPU(基于8086CPU结构而得出的逻辑区域)。
[2] 了解内存地址空间分布对操作内存十分重要。
[3] GDT和IDT都是与CPU有关的设定,只需要将GDTR和IDTR寄存器赋予GDT和IDT的信息,CPU就能够自动的和GDT和IDT发生联系。
[4] PIC将IRQn中断发给CPU后,CPU要给PIC发继续监视IRQn的命令PIC才会继续监视IRQn。
[5] 在设置PIC等跟中断相关的模块时需要屏蔽中断,设置完毕后再打开中断。
[6] PIC的端口号为0x21。
[x86OS] Note Over.
[2015.04.17]