文章目录
1. 例程harib03c(c源文件分割并整理makefile文件)
将源文件分割的优缺点:
将描述屏幕的函数放置在graphic.c
文件中;将GDT,IDT相关的函数放置在dsctbl.c
文件中;其他的暂时保留在bootpack.c
中。
与之对应,在Makefile中应该做出修改:
graphic.gas : graphic.c Makefile
$(CC1) -o graphic.gas graphic.c
graphic.nas : graphic.gas Makefile
$(GAS2NASK) graphic.gas graphic.nas
graphic.obj : graphic.nas Makefile
$(NASK) graphic.nas graphic.obj graphic.lst
dsctbl.gas : dsctbl.c Makefile
$(CC1) -o dsctbl.gas dsctbl.c
dsctbl.nas : dsctbl.gas Makefile
$(GAS2NASK) dsctbl.gas dsctbl.nas
dsctbl.obj : dsctbl.nas Makefile
$(NASK) dsctbl.nas dsctbl.obj dsctbl.lst
Makefile中存在一些特殊符号,可以合并重复代码,因此以上内容可以写成:
%.gas : %.c Makefile
$(CC1) -o $*.gas $*.c
%.nas : %.gas Makefile
$(GAS2NASK) $*.gas $*.nas
%.obj : %.nas Makefile
$(NASK) $*.nas $*.obj $*.lst
暂时将所有函数,宏定义和结构体的声明都归档在一个bootpack.h
的头文件中。
2. 例程harib03c(用于描述段的信息)
详细描述一下初始化时用到的load_gdtr和load_idtr函数,它定义在naskfunc.nas文件中:
# naskfunc.nas
_load_gdtr: ; void load_gdtr(int limit, int addr);
MOV AX,[ESP+4] ; limit
MOV [ESP+6],AX
LGDT [ESP+6]
RET
_load_idtr: ; void load_idtr(int limit, int addr);
MOV AX,[ESP+4] ; limit
MOV [ESP+6],AX
LIDT [ESP+6]
RET
这个函数用于将段上限(limit)和地址赋值给GDTR寄存器(和IDTR寄存器),该寄存器的低16bits(2字节)存放段上限,高32bits(4字节)存放地址。
还有一个c语言函数,位于dsctbl.c文件的set_segmdesc函数:
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;
}
这个函数是按照CPU的规格要求,将段的信息归结成8个字节写入内存的,这8个字节主要内容有,我们使用struct SEGMENT_DESCRIPTOR结构体描述这些信息:
- 段的大小
- 段的起始地址
- 段的管理属性(禁止写入,禁止执行,系统专用等)
struct SEGMENT_DESCRIPTOR {
short limit_low, base_low;
char base_mid, access_right;
char limit_high, base_high;
};
段的地址称为基址,使用变量base表示。在结构体中,将base分为三段,low(2字节),mid(1字节),high(1字节)。之所以将基址分为三个字段分别记录,主要是为了兼容80286时代的程序。也就是这样的操作可以使80286的程序可以无须修改就运行在386的CPU上。
段的大小也称为上限,段的上限是4GB字节,需要使用一个32bits的数字表示。再加上基址的32bits,直接占用了8字节,就没办法放置段的管理属性了。因此段上限被设置为20bits,即1MB大小,并且在段的属性里设置一个Gbit的标志位。当Gbit标志位为1时,limit的单位被解释为页(page,1页为4KB),否则是字节(byte)。这样以来,4KB*1MB=4GB,就可以完整表示段的大小。20bits的段上限写在limit_low和limit_high字段里,在limit_high字段的高4bits存放段属性。
段属性也称为段的访问权限,使用access_right字段或ar表示。12bits段属性的高4bits放在limit_high字段的高4bits中。这4bits被称为扩展的段属性,因为扩展的段属性在80286时代还不存在,到386时代才开始启用。这4bits是“GD00”的模式,G就是所说的Gbit标志位,M表示段的模式,D为1表示32位模式,D为0表示16位模式(主要用于80286程序)。
ar的低8bits从80286就已经开始有了,简单几个常用的:
在32位模式下,CPU有系统模式(ring0)和应用模式(ring3)之分,操作系统等作为“管理者”的程序和应用程序等作为“被管理者”的程序,运行时的模式不同。比如,如果在应用模式下,试图执行LGDT等指令的话,CPU对该指令不予执行,并上报操作系统“应用程序存在一个危险的行为”;如果应用程序想要使用系统专用的段时,CPU也会停止执行,并认为应用程序可能企图盗取操作系统信息或破坏操作系统。
CPU到底处于系统模式还是应用模式,取决于执行中的应用程序是位于访问权的0x9a的段还是0xfa的段。
3. 例程harib03d(初始化PIC)
PIC(programmable interrupt controller,可编程中断控制器)。为了解决一个CPU只能处理一个中断的限制,PIC是将8个中断信号集合成一个中断信号的装置。
一个CPU连接多个PIC也是常见的设计。与CPU直接相连的PIC称为主PIC(master),与PIC相连的PIC称为从PIC(slave),图中masterPIC负责0 ~ 7号中断,slavePIC负责8 ~ 15号中断。这种属于硬件连接,软件无法改变。
// 位于int.c文件
void init_pic(void)
/* PIC的初始化 */
{
io_out8(PIC0_IMR, 0xff ); /* 禁止所有中断 */
io_out8(PIC1_IMR, 0xff ); /* 禁止所有中断 */
io_out8(PIC0_ICW1, 0x11 ); /* 边沿触发模式 */
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 ); /* 边沿触发模式 */
io_out8(PIC1_ICW2, 0x28 ); /* IRQ8-15由INT28-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;
}
从CPU角度来说,PIC属于外设,CPU使用OUT指令进行操作。程序中的PIC0代表主PIC,PIC1代表从PIC。PIC中有很多寄存器,使用端口号码进行区别。
PIC的寄存器都是8位寄存器,IMR是“interrupt mask register”(中断屏蔽寄存器),8位分别对应8路IRQ(interrupt request,中断请求)信号,如果IMR的某一个bit为1,则这一路IRQ信号被屏蔽,PIC就忽视这一路信号。这是因为,正在对中断设定进行更改时,如果再接受别的中断会引起混乱,为了防止这种情况发生,就必须屏蔽中断。如果某个IRQ没有连接任何设备,静电干扰也可能引起反应,导致操作系统紊乱,所以也需要屏蔽这一类干扰。
ICW,“initial control word”,初始化控制数据,(这里的word与16bits没有任何关系)。共有4个,分别编号为1 ~ 4,共有4字节的数据。ICW1和ICW4与PIC主板配线方式和中断信号的电气特性有关,无需过多关注。ICW3是有关“主从连接”的设定,对master而言, 第几号IRQ与PIC相连是用8bits来设定的,如果把这些bit全部都置为1,那么master就能驱动8个slave,但是当前的电脑一般设为0000 0100。对slave而言,该slave与master的第几号相连,使用3bits来设定。因为硬件已经无法更改,所以只能维持当前。
因此不同的操作系统在软件层面只能通过设定IWC2,决定IRQ以哪一号中断通知CPU。
当中断发生后,如果CPU可以受理这个中断,CPU就会命令PIC发送过来两个字节的数据,为
0xcd 0x??
。在CPU看来这两个数据,与从内存中读进来的是一样的,可以同样执行,这恰恰就是把数据当中程序执行的情况。其中,0xcd就是调用BIOS时使用的INT指令,例如程序中如果写INT 0x10
就会被翻译为0xcd 0x10
。所以此时可以认为CPU上了PIC的当,按照PIC所期望的中断号执行了INT指令。
中断号0x00 ~ 0x1f
的作用是,当应用程序企图对操作系统做坏事时,CPU内部会自动产生这些中断号,用于保护操作系统。因此,我们本次配置的是,使用0x20 ~ 0x2f
中断号接收IRQ 1 ~ 15
。其中,鼠标是IRQ12,键盘是IRQ1。
4. 例程harib03e(中断处理程序)
针对键盘和鼠标,编写INT 0x21(键盘)和INT 0x2c(鼠标)的中断处理程序。
// int.c节选
void inthandler21(int *esp)
{
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)
{
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();
}
}
函数只是显示一条信息并保持在待机状态。esp
的值,在函数中暂时并没有使用。
中断处理完毕之后不可使用RET
指令进行返回,必须使用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
这个函数所做的动作主要是,将寄存器的值保存在栈里,然后将DS和ES调整到与SS相等,再调用_inthandler21,返回以后,将所有寄存器的值再返回到原来的值,最后执行IRETD。如此小心翼翼的保存寄存器的值,原因在于中断的处理发生在函数运行的途中,通过IRETD从中断处理返回后,需要保证寄存器可以恢复到中断处理之前的状态。
代码中用到栈(stack)的存储方式。PUSH EAX
本质上相当于ADD ESP, -4; MOVE [SS:ESP], EAX
,即将ESP的值减4(栈空间从高地址到低地址),再把EAX的内容保存在这个地址。与之对应,POP EAX
就是代表MOVE EAX, [SS:ESP]; ADD ESP, 4
。
还有一个PUSHAD
指令,它相当于:
PUSH EAX
PUSH ECX
PUSH EDX
PUSH EBX
PUSH ESP
PUSH EBP
PUSH ESI
PUSH EDI
POPAD
则是将其全部都POP出来。
C语言自以为是的认为,DS、ES和SS都是同一个段。如果不按照它的想法设定为相等的话,函数inthandler21就没办法成功被执行。
将_asm_inthandler21和_asm_inthandler2c函数注册到IDT中:
/* 位于dsctbl.c的init_gdtidt函数中 */
set_gatedesc(idt + 0x21, (int) asm_inthandler21, 2 * 8, AR_INTGATE32);
set_gatedesc(idt + 0x27, (int) asm_inthandler27, 2 * 8, AR_INTGATE32);
set_gatedesc(idt + 0x2c, (int) asm_inthandler2c, 2 * 8, AR_INTGATE32);
如果发生了0x21号中断,CPU就会自动调用asm_inthandler21。这里的2*8表示2<<3(低3bits有别的用处),即段号为2。该段为:
set_segmdesc(gdt + 2, LIMIT_BOTPAK, ADR_BOTPAK, AR_CODE32_ER);
在bookpack.c的HariMain函数中,新增了一个io_sti()
函数的调用。该函数仅仅是执行了一个STI指令,它是CLI指令的逆指令。执行STI后,IF(interrupt flag)变为1,CPU接收来自于外部的中断。在HariMain的最后,修改了PIC的IMR,以便接收来自键盘和鼠标的中断。
io_out8(PIC0_IMR, 0xf9);
io_out8(PIC1_IMR, 0xef);