2016-05-08
参考书籍:《30天自制操作系统》、《自己动手写操作系统》
分割编译主要围绕这两张图:
然后是Makefile的内容:
OBJS = c_main.obj assemblyFunc.obj hankaku.obj graphic.obj dsctbl.obj int.obj TOOLPATH = ../z_tools/ INCPATH = ../z_tools/haribote/ MAKE = $(TOOLPATH)make.exe -r NASK = $(TOOLPATH)nask.exe NASM = $(TOOLPATH)nasm.exe CC1 = $(TOOLPATH)cc1.exe -I$(INCPATH) -Os -Wall -quiet GAS2NASK = $(TOOLPATH)gas2nask.exe -a OBJ2BIM = $(TOOLPATH)obj2bim.exe MAKEFONT = $(TOOLPATH)makefont.exe BIN2OBJ = $(TOOLPATH)bin2obj.exe BIM2HRB = $(TOOLPATH)bim2hrb.exe RULEFILE = $(TOOLPATH)haribote/haribote.rul EDIMG = $(TOOLPATH)edimg.exe IMGTOL = $(TOOLPATH)imgtol.com COPY = copy DEL = del default : $(MAKE) img ipl.bin : ipl.asm Makefile $(NASM) ipl.asm -o ipl.bin main.bin : main.nas Makefile $(NASK) main.nas main.bin main.lst hankaku.bin : hankaku.txt Makefile $(MAKEFONT) hankaku.txt hankaku.bin hankaku.obj : hankaku.bin Makefile $(BIN2OBJ) hankaku.bin hankaku.obj _hankaku c_main.bim : $(OBJS) Makefile $(OBJ2BIM) @$(RULEFILE) out:c_main.bim stack:3136k map:c_main.map $(OBJS) # 3MB+64KB=3136KB c_main.hrb : c_main.bim Makefile $(BIM2HRB) c_main.bim c_main.hrb 0 haribote.sys : main.bin c_main.hrb Makefile copy /B main.bin+c_main.hrb haribote.sys haribote.img : ipl.bin haribote.sys Makefile $(EDIMG) imgin:../z_tools/fdimg0at.tek wbinimg src:ipl.bin len:512 from:0 to:0 copy from:haribote.sys to:@: imgout:haribote.img %.gas : %.c Makefile $(CC1) -o $*.gas $*.c %.nas : %.gas Makefile $(GAS2NASK) $*.gas $*.nas %.obj : %.nas Makefile $(NASK) $*.nas $*.obj $*.lst img : $(MAKE) haribote.img run : $(MAKE) img $(COPY) haribote.img ..z_toolsqemufdimage0.bin $(MAKE) -C ../z_tools/qemu install : $(MAKE) img $(IMGTOL) w a: haribote.img clean : -$(DEL) *.bin -$(DEL) *.lst -$(DEL) *.gas -$(DEL) *.obj -$(DEL) c_main.nas -$(DEL) c_main.map -$(DEL) c_main.bim -$(DEL) c_main.hrb -$(DEL) haribote.sys src_only : $(MAKE) clean -$(DEL) haribote.img
dsctbl.c:
#include "c_head.h" /** *author: 无 名 *date: 2016.06.08 *description: init GDT IDT **/ void init_gdtidt(void) { struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR *) ADR_GDT; struct GATE_DESCRIPTOR *idt = (struct GATE_DESCRIPTOR *) ADR_IDT; int i; /* GDT初始化 */ for (i = 0; i <= LIMIT_GDT / 8; i++) { set_segmdesc(gdt + i, 0, 0, 0); } set_segmdesc(gdt + 1, 0xffffffff, 0x00000000, AR_DATA32_RW); set_segmdesc(gdt + 2, LIMIT_BOTPAK, ADR_BOTPAK, AR_CODE32_ER); load_gdtr(LIMIT_GDT, ADR_GDT); /* IDT初始化 */ for (i = 0; i <= LIMIT_IDT / 8; i++) { set_gatedesc(idt + i, 0, 0, 0); } load_idtr(LIMIT_IDT, ADR_IDT); /* IDT 设定 */ 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); return; } 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 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; }
其中作为参数传入的构造函数在c_head.h:
/* dsctbl.c */ struct SEGMENT_DESCRIPTOR { short limit_low, base_low; char base_mid, access_right; char limit_high, base_high; }; struct GATE_DESCRIPTOR { short offset_low, selector; char dw_count, access_right; short offset_high; };
SEGMENT_DESCRIPTOR里,段地址为base,分为low(二字节)、mid(一字节)、high(一字节)三部分,共32位(4GB)。
分为3段是为了与80286时代的程序兼容。
段上限表示一个段可以有多少字节。段上限最大是4GB,也就是一个32位的数值,如果直接放进去,这个数值本身就要占用4字节,再加上基址,一共要8字节,会把结构体占满。所以段上限只能20位。即1MB。
因此在段的属性里设了一个标志位,叫做Gbit。这个标志位是1的时候,limit的单位不解释成字节(byte),而解释成页(page)。1页为4KB。4KB*1M=4GB。
这20位的段上限分别写到limit_low和limit_high里。看起来它们好像是总共有3字节,即24位,但实际上我们接着要把段属性写入limit_high的高4位里,所以最后段上限还是只有20位。
最后12位的段属性。段属性又称为“段的访问权属性”,在程序中用变量名access_right或ar来表示。因为12位段属性中的高4位放在limit_high的高4位里,所以程序有意把ar当作如下的16位构成来处理:
xxxx0000xxxxxxxx
ar的高4位为“扩展访问权”。为什么这么说呢?因为这高四位的访问属性在80286的时代还不存在,到386以后才可以使用。这4位是“GD00”构成的,其中G是指刚才所说的G bit,D是指段的模式,1是指32位模式,0是指16位模式。这里出现的16位模式主要只运用于运行80286的程序,不能用于调用BIOS。所以除了运行80286程序以外,通常都用D=1模式。
ar的低8位从80286时代就已经有了:
00000000(0x00):未使用的记录表
10010010(0x92):系统专用,可读写的段。不可执行。
10011010(0x9a):系统专用,可执行的段。可读不可写。
11110010(0xf2):应用程序用,可读写的段。不可执行。
11111010(0xfa):应用程序用,可执行的段。可读不可写。
关于gdtr的加载的代码:
_load_gdtr: ; void load_gdtr(int limit, int addr); MOV AX,[ESP+4] ; limit MOV [ESP+6],AX LGDT [ESP+6] RET
书中这样说:
“这个函数用来指定的段上限和地址值赋值给名为GDTR的48位寄存器。这是一个很特别的48位寄存器,并不能用我们常用的MOV指令来赋值。给它赋值的时候,唯一的方法就是指定一个内存地址,从指定的地址读取6个字节(也就是48位),然后赋值给GDTR寄存器。完成这一任务的指令,就是LGDT。
该寄存器的低16位(即内存的最初2个字节),是段上限,它等于“GDT的有效字节数-1”。今后我们还会偶尔用到上限这个词,意思都是表示量的大小,一般为“字节数-1”。剩下的高32位(即剩余的4个字节),代表GDT的开始位置。
在最初执行这个函数的时候,DWORD[ESP+4]里存放的是段上限,DWORD[ESP+8]里存放的是地址。具体到实际的数值,就是0x0000ffff和0x00270000 。把它们按字节写出来的话,就成了[FF FF 00 00 00 00 27 00](要注意低位存放在内存地址小的字节里)。为了执行LGDT,笔者希望把它们排列成[FF FF 00 00 27 00]的样子,所以就先用"MOV AX,[ESP+4]"读取最初的0xfffff,然后再写到[ESP+6]里。这样,结果就成了[FF FF 00 00 27 00],如果从[ESP + 6]开始读6字节的话,正好是我们想要的结果。“
如果你不了解保护模式下的寻址原理的话,是读不懂这些文字的,http://www.techbulo.com/708.html读这篇文章后再对照这段文字,就可以理解了。
初始化PIC:
与CPU直接相连的PIC称为主PIC(0-7号中断),与主PIC相连的PIC称为从PIC(8-15号中断)。从PIC通过第二号IRQ与主PIC相连。
int.c:
#include "c_head.h" 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由INT20-7接收 */ 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; } void inthandler21(int *esp) /* 来自PS/2键盘的中断 */ { struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO; draw_box(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; draw_box(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(); } } void inthandler27(int *esp) { io_out8(PIC0_OCW2, 0x67); return; }
PIC初始化部分配合c_head.h下面的声明:
/* int.c */ void init_pic(void); void inthandler21(int *esp); void inthandler27(int *esp); void inthandler2c(int *esp); #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
可以看出是通过向内存指定位置中写入内容来作PIC初始化。
IMR是“interrupt mask register”的缩写,意思是“中断屏蔽寄存器”。8位分别对应8路IRQ信号。
ICW是“initial control word”的缩写,意为“初始化控制数据”。
其中有这样一句
io_out8(PIC0_ICW2,0x20);表明IRQ7-14由INT20-27接收。
io_out8(PIC0_ICW2,0x28);表明IRQ8-15由INT28-2f接收。
而后具体的中断调用还需要汇编语句:
_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_inthandler27: PUSH ES PUSH DS PUSHAD MOV EAX,ESP PUSH EAX MOV AX,SS MOV DS,AX MOV ES,AX CALL _inthandler27 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
通过汇编语句调用对应的C语言函数。
上面代码是中断函数,首先是汇编语言部分,汇编函数中调用C语言函数。而这个中断函数是怎样和之前初始化的PIC联系起来呢?
这便要通过IDT的设定。dsctbl.c:
set_gatedesc(idt + 0x21, (int) asm_inthandler21, 2 * 8, AR_INTGATE32);
asm_inthandler21注册在idt的第0x21号,这样一来如果发生中断了。CPU就会调用asm_inthandler21。