1.分割源文件 & 2.整理Makefile & 3.整理头文件
源文件分割图如下
这样整理一下,清爽多了。对应源文件的分割,我们还要修改Makefile,流程如下
随着bootpack.c分割成多个文件,Makefile的内容变多了许多。为了简化Makefile的内容,可将处理相同类型文件的语句规则替换为一般规则。可将
bootpack.gas : bootpack.c Makefile
$(CC1) -o bootpack.gas bootpack.c
raphic.gas : graphic.c Makefile
$(CC1) -o graphic.gas graphic.c
dsctbl.gas : dsctbl.c Makefile
$(CC1) -o dsctbl.gas dsctbl.c
替换为一般规则:
%.gas : %.c Makefile
$(CC1) -o $*.gas $*.c
其他类型的文件也可以这样写,减少了Makefile的行数。
make.exe会首先寻找普通的生成规则,如果没找到就尝试用一般规则,普通规则优于一般规则。
由于各个源文件都要重复声明所用到的函数,所以源文件总行数并没有减少。于是将各源文件重复部分去掉,归纳起来放入名为bootpack.h的文件里,虽然扩展名变了,但它也是C语言的文件。像这样,仅由函数声明和#define等组成的文件,我们称之为头文件,指放在程序头部的文件。
在编译某个源文件时,我们要让编译器去读这个头文件,做法是在这个源文件的前面加上 #include “bootpack.h” 。
4.解决上次遗留问题——GDT/IDT的设置
首先说明一下naskfunc.nas的_load_gdtr
_load_gdtr: ;void load_gdtr(int limit,int addr);
MOV AX,[ESP+4] ;limit
MOV [ESP+6],AX
LIDT [ESP+6]
RET
这个函数用来将制定的段上限(limit)和地址值赋给名为GDTR的48位寄存器。这是一个很特别的48位寄存器,不能用常用的MOV指令来赋值。给它赋值的唯一方法就是指定一个内存地址,从指定的地址读取6个字节(48位),然后赋值给GDTR寄存器。完成这一任务的指令就是LGDT。
该寄存器的低16位(即内存最初2个字节)是段上限,剩下的高32位(即剩余的4字节),代表GDT的开始地址。
代码的执行含义:
如果从[ESP+6]开始读6字节的话,正好是我们想要的结果。
下面说一下dsctbl.c中的set_segmdesc函数。
struct SEGMENT_DESCRIPTOR {
short limit_low, base_low;
char base_mid, access_right;
char limit_high, base_high;
};
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个字节的内容如下:
段的地址当然是用32位表示,在结构体中base分为了3段,合起来刚好32位。程序使用了移位运算和AND的运算符往各个字节里填入相应的值。
为了不超出8字节,段上限只能用20位表示,这样最大只能指定到1MB为止(32位 寻址范围2的32次方 等于4GB)。为了表示4G内存,我们使用了分页,1P=4KB(1MB*4KB=4GB),通过Gbit位来表示,为1表示段上限的单位是页,为0表示为Byte。这个标志位在段的属性中。
20位的段上限分别写入limit_low和limit_high中,共24位,其中把12位段属性的高4位放入limit_high的高4位里。ar高4位是“扩展访问权”,到386以后才存在,由“GD00”组成,G为刚才的G_bit,D指段的模式,1指32位,0指16位。
低8位从286时代就有了,简单介绍一下:
32位模式下,CPU有系统模式和应用模式之分,CPU到底是处于系统模式还是应用模式,取决于执行中的应用程序是位于访问权位0x9a的段,还是位于访问权为0xfa的段。
5.初始化PIC
PIC是“programmable interrupt controller”的缩写,意思是可编程中断控制器。与中断关系密切。在设计上,CPU只能处理一个中断,这不够用,所以在设计电脑时就增加了几个辅助芯片,如今已被集成在一个芯片组里。
与CPU直接相连的PIC称主PIC(master PIC),与主PIC相连的PIC称从PIC(slave PIC)。主PIC负责处理0-7号中断信号,从PIC负责处理8-15号中断信号。从PIC通过2号IRQ与主PIC相连。
PIC的初始化程序:
void init_pic(void) /* PIC初始化 PIC0指主PIC,PIC1指从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由INT20-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由INT28-2f接收 */
io_out8(PIC1_ICW3, 2); /* PIC1由接收 */
io_out8(PIC1_ICW4, 0x01); /* 无缓冲区模式 */
io_out8(PIC0_IMR, 0xfb); /* 11111011 PIC1以外全部禁止 */
io_out8(PIC0_IMR, 0xff); /* 11111111 禁止所有中断 */
return;
}
从CPU的角度来看,PIC是外部设备,CPU使用OUT指令进行操作。PIC内部有很多寄存器(都是8位),用端口号码对彼此进行区别,以决定是写入哪一个寄存器。
IMR是“interrupt mask register”的缩写,意思是“中断屏蔽寄存器”。8位分别对应8路IRQ信号。如果某一位的值是1,则该位所对应的IRQ信号被屏蔽,PIC就忽视该路信号。
ICW有4个,分别编号1~4,共有4个字节的数据。ICW1和ICW4与PIC主板配线方式、中断信号的电气特性等有关,电脑设定值都是上述程序所示的固定值。
这次是以INT 0x20~0x2f接收中断信号IRQ 0 ~15而设定的。直接用INT 0x20 ~0x2f不行吗?这样不就一致了吗。之所以不能用,是因为应用程序想要对操作系统干坏事时,CPU会自动产生INT 0x00 ~0x1f,如果IRQ与这些号码重复了,CPU就分不清它到底是IRQ还是CPU系统保护通知。
6.中断处理程序的制作
键盘是IRQ1,鼠标是IRQ12,首先编写用于INT 0x2c和INT 0x21的中断处理程序(需要在IDT里注册关联),即中断发生时所要调用的程序。
int.c节选:
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();
}
}
函数只是显示一条信息,然后保持待机状态。鼠标的程序与此类似。函数接收了esp指针但还没有用。
中断处理完成之后,不能执行return(=RET)而必须执行IRETD指令。现在用汇编语言来写这个指令。
naskfunc.nas节选:
_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
下面将函数注册到IDT中,在dsctal.c中的init_gdtidt里加入以下语句:
set_gatedesc(idt + 0x21, (int)asm_inthandler21, 2*8, AR_INTGATE32);
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。
这部分内容还是有些疑问的,往后读几天,等我回来再看看。
在执行完gdt\idt\pic初始化程序后,要执行STI指令,让IF(中断许可标志)变为1,是CPU接受来自外部的中断,CPU中断信号只有一根,所以IF也只有一个。
在HariMain的最后,修改PIC的IMR,以便接受来自键盘和鼠标的中断。
只有按下键盘某个键,或动一动鼠标,中断信号就会传到CPU,然后CPU执行中断处理程序,输出信息。