跳入保护模式
首先是GDT以及对应的选择子,我们只定义三个描述符,分别是0~4GB的可执行段、0~4GB可读写段和一个指向显存开始地址的段。
因为段地址已经被确定为BaseOfLoader,所以Loader中出现的标号的物理地址可以用下面的公式表示:1
物理地址 = BaseOfLoader x 10h + 变量的偏移
然后运行后,如果看到字母“p”则代表我们进入了保护模式,如图所示:
重新放置内核以及控制器的转让
下图是一个内存的使用分布图。
虽然引导扇区将剩余的内存空间分割成了两块,但实际上引导扇区在完成了它的使命之后就没有用了,可以视为空闲内存。
运行之后,可以看到K字母,即代表成功由内核在控制了。
扩充内核
切换堆栈和GDT
代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32SELECTOR_KERNEL_CS equ 8
; 导入函数
extern cstart
; 导入全局变量
extern gdt_ptr
[SECTION .bss]
StackSpace resb 2 * 1024
StackTop: ; 栈顶
[section ; 代码在此 ]
global _start ; 导出 _start
_start:
mov esp, StackTop ; 堆栈在 bss 段中
sgdt [gdt_ptr] ; cstart() 中将会用到 gdt_ptr
call cstart ; 在此函数中改变了gdt_ptr,让它指向新的GDT
lgdt [gdt_ptr] ; 使用新的GDT
;lidt [idt_ptr]
jmp SELECTOR_KERNEL_CS:csinit
csinit: ; “这个跳转指令强制使用刚刚初始化的结构”——<<OS:D&I 2nd>> P90.
push 0
popfd ; Pop top of stack into EFLAGS
hlt
从上可知,用简单的4个语句就完成了切换堆栈和更换GDT的任务。其中,StackTop定义在.bass段中,堆栈大小为2KB。操做GDT时用到的gdt_ptr和cstart分别是一个全局变量和全局函数,他们定义在start.c中,如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28include "type.h"
#include "const.h"
#include "protect.h"
PUBLIC void* memcpy(void* pDst, void* pSrc, int iSize);
PUBLIC void disp_str(char * pszInfo);
PUBLIC u8 gdt_ptr[6]; /* 0~15:Limit 16~47:Base */
PUBLIC DESCRIPTOR gdt[GDT_SIZE];
PUBLIC void cstart()
{
disp_str("\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"
"-----\"cstart\" begins-----\n");
/* 将 LOADER 中的 GDT 复制到新的 GDT 中 */
memcpy(&gdt, /* New GDT */
(void*)(*((u32*)(&gdt_ptr[2]))), /* Base of Old GDT */
*((u16*)(&gdt_ptr[0])) + 1 /* Limit of Old GDT */
);
/* gdt_ptr[6] 共 6 个字节:0~15:Limit 16~47:Base。用作 sgdt/lgdt 的参数。*/
u16* p_gdt_limit = (u16*)(&gdt_ptr[0]);
u32* p_gdt_base = (u32*)(&gdt_ptr[2]);
*p_gdt_limit = GDT_SIZE * sizeof(DESCRIPTOR) - 1;
*p_gdt_base = (u32)&gdt;
}
cstart()首先把位于Loader中的原GDT全部复制给心得GDT,然后把gdt_ptr中的内容换成新的GDT的基地址和界限。复制GDT使用的是函数memcpy。
运行结果如下:
整理文件
- boot.asm和loader.asm放在单独的目录/boot中,相应的头文件也放在里面;
- klib.asm和string.asm放在/lib中,作为库;
- kernel.asm和start.c放在/kernel里面
目录树如下:Makefile
当我们把文件放在不同的文件夹了以后,编译的命令会更加的复杂,所以,我们将使用Makefile,从而输入一行命令就可以完成整个编译过程,相关的文件放在/boot中,具体代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25# Makefile for boot
# Programs, flags, etc.
ASM = nasm
ASMFLAGS = -I include/
# This Program
TARGET = boot.bin loader.bin
# All Phony Targets
.PHONY : everything clean all
# Default starting position
everything : $(TARGET)
clean :
rm -f $(TARGET)
all : clean everything
boot.bin : boot.asm include/load.inc include/fat12hdr.inc
$(ASM) $(ASMFLAGS) -o $@ $<
loader.bin : loader.asm include/load.inc include/fat12hdr.inc include/pm.inc
$(ASM) $(ASMFLAGS) -o $@ $<
以字符#开头的行是注释、=用来定义变量,ASM和ASMFLAGS就是两个变量,使用他们的时候要用$(ASM)和$(ASMFLAGS)。Makefile的最重要的语法如下:1
2target: prerequisites
command
上面这样的形式表示:
- 要想得到target,需要执行命令command.
- target依赖prerequisites,当prerequisites中至少有一个文件比target文件新时,command才被执行。
比如这个Makefile的最后两行,翻译出来就是: - 要想得到loader.bin,需要执行“$(ASM) $(ASMFLAGS) -o $@ $<”。
- loader.bin依赖一下文件:
- loader.asm
- include/load.inc
- include/pm.inc
- include/fat12hdr.inc
当他们中至少有一个比loader.bin新的时候,command被执行。
其中 $@代表target $<代表prerequisites的第一个名字。
所以执行“make clean”,将会执行“rm -f $(TARGET)”也就是“rm -f boot.bin loader.bin”
以下是执行make all和make的结果图:
接着对makefile扩展之后,我们可以通过make building 和make image很方便的把引导扇区、load.bin和kernel。bin写入虚拟软盘。如下所示:
然后再启动bochs,同时在start.c加上显示cstart end,来表示我们的make正常的进行了编译连接了,结果如图所示:添加中断处理
中断要做的工作为:设置8259A和建立IDT。先写函数设置8259A:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32PUBLIC void init_8259A()
{
/* Master 8259, ICW1. */
out_byte(INT_M_CTL, 0x11);
/* Slave 8259, ICW1. */
out_byte(INT_S_CTL, 0x11);
/* Master 8259, ICW2. 设置 '主8259' 的中断入口地址为 0x20. */
out_byte(INT_M_CTLMASK, INT_VECTOR_IRQ0);
/* Slave 8259, ICW2. 设置 '从8259' 的中断入口地址为 0x28 */
out_byte(INT_S_CTLMASK, INT_VECTOR_IRQ8);
/* Master 8259, ICW3. IR2 对应 '从8259'. */
out_byte(INT_M_CTLMASK, 0x4);
/* Slave 8259, ICW3. 对应 '主8259' 的 IR2. */
out_byte(INT_S_CTLMASK, 0x2);
/* Master 8259, ICW4. */
out_byte(INT_M_CTLMASK, 0x1);
/* Slave 8259, ICW4. */
out_byte(INT_S_CTLMASK, 0x1);
/* Master 8259, OCW1. */
out_byte(INT_M_CTLMASK, 0xFF);
/* Slave 8259, OCW1. */
out_byte(INT_S_CTLMASK, 0xFF);
}
相应端口的宏如下:1
2
3
4
5
6
7
8
9/* 8259A interrupt controller ports. */
......
/* 中断向量 */
这个函数只用到了一个函数,就是用来写端口的out_byte,该函数体位于kliba.asm。其中不仅有out_byte对端口进行写操作还有in_byte对端口进行读操作,由于端口操作可能需要时间,所以两个函数中加了点空操作表,以便有微笑的延迟。代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21; ========================================================================
; void out_byte(u16 port, u8 value);
; ========================================================================
out_byte:
mov edx, [esp + 4] ; port
mov al, [esp + 4 + 4] ; value
out dx, al
nop ; 一点延迟
nop
ret
; ========================================================================
; u8 in_byte(u16 port);
; ========================================================================
in_byte:
mov edx, [esp + 4] ; port
xor eax, eax
in al, dx
nop ; 一点延迟
nop
ret
这两个函数放在include/proto.h中,这是一个新建立的头文件,用来存放函数声明。
然后,还要修改Makefile,不但要添加新的目标kernel/i8259.o,而且由于头文件的变化,kernel/start.o的依赖关系也稍微有变化:1
2
3
4
5
6
7
8
9
10OBJS = kernel/kernel.o kernel/start.o kernel/i8259.o kernel/global.o kernel/protect.o lib/klib.o lib/kliba.o lib/string.o
DASMOUTPUT = kernel.bin.asm
......
kernel/start.o: kernel/start.c include/type.h include/const.h include/protect.h \
include/proto.h include/string.h
$(CC) $(CFLAGS) -o $@ $<
kernel/i8259.o : kernel/i8259.c include/type.h include/const.h include/protect.h \
include/proto.h
$(CC) $(CFLAGS) -o $@ $<
我们使用gcc -M自动生成依赖关系如下图:
。
下面初始化IDT,它和初始化GDT类似,所以初始化GDT的方法可以拿着用,先前的gdt[]等变量都在头文件global.h中了,这样增加了代码的美感和可读性。
GATE定义在protect.h中,如下:1
2
3
4
5
6
7
8
9
10
11
12
13/* 门描述符 */
typedef struct s_gate
{
u16 offset_low; /* Offset Low */
u16 selector; /* Selector */
u8 dcount; /* 该字段只在调用门描述符中有效。如果在利用
调用门调用子程序时引起特权级的转换和堆栈
的改变,需要将外层堆栈中的参数复制到内层
堆栈。该双字计数字段就是用于说明这种情况
发生时,要复制的双字参数的数量。*/
u8 attr; /* P(1) DPL(2) DT(1) TYPE(4) */
u16 offset_high; /* Offset High */
}GATE;
接着,在kernel.asm中添加两句,导入idt_ptr这个符号,并加载IDT。
我们对异常的处理总体是,如果有错误码,则直接把向量号压栈,然后执行一个函数exception_handler;如果没有错误码,则现在栈中压入一个0xfffffff,再把向量号压栈并随后执行exception_handler。
下面是该函数:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48PUBLIC void exception_handler(int vec_no,int err_code,int eip,int cs,int eflags)
{
int i;
int text_color = 0x74; /* 灰底红字 */
char * err_msg[] = {"#DE Divide Error",
"#DB RESERVED",
"-- NMI Interrupt",
"#BP Breakpoint",
"#OF Overflow",
"#BR BOUND Range Exceeded",
"#UD Invalid Opcode (Undefined Opcode)",
"#NM Device Not Available (No Math Coprocessor)",
"#DF Double Fault",
" Coprocessor Segment Overrun (reserved)",
"#TS Invalid TSS",
"#NP Segment Not Present",
"#SS Stack-Segment Fault",
"#GP General Protection",
"#PF Page Fault",
"-- (Intel reserved. Do not use.)",
"#MF x87 FPU Floating-Point Error (Math Fault)",
"#AC Alignment Check",
"#MC Machine Check",
"#XF SIMD Floating-Point Exception"
};
/* 通过打印空格的方式清空屏幕的前五行,并把 disp_pos 清零 */
disp_pos = 0;
for(i=0;i<80*5;i++){
disp_str(" ");
}
disp_pos = 0;
disp_color_str("Exception! --> ", text_color);
disp_color_str(err_msg[vec_no], text_color);
disp_color_str("\n\n", text_color);
disp_color_str("EFLAGS:", text_color);
disp_int(eflags);
disp_color_str("CS:", text_color);
disp_int(cs);
disp_color_str("EIP:", text_color);
disp_int(eip);
if(err_code != 0xFFFFFFFF){
disp_color_str("Error code:", text_color);
disp_int(err_code);
}
}
以下就是中断异常的情况:
所有的中断都会触发一个函数spurious_irq(),这个仅仅是把IRQ号打印出来。
接着,我们向主8259A相应端口写入了0xFD,由于0XFD对应的二进制是11111101,于是键盘中断被打开,而其他中断任然处于屏蔽状态,最后在kernel.asm中添加sti指令设置IF位,然后make后,当我们敲击键盘任意键就如出现如下图: