《ORANGE-s-一个操作系统实现》内核雏形(3-2)

跳入保护模式

首先是GDT以及对应的选择子,我们只定义三个描述符,分别是0~4GB的可执行段、0~4GB可读写段和一个指向显存开始地址的段。

因为段地址已经被确定为BaseOfLoader,所以Loader中出现的标号的物理地址可以用下面的公式表示:

1
物理地址 = BaseOfLoader x 10h + 变量的偏移

然后运行后,如果看到字母“p”则代表我们进入了保护模式,如图所示:
mark

重新放置内核以及控制器的转让

下图是一个内存的使用分布图。
mark
虽然引导扇区将剩余的内存空间分割成了两块,但实际上引导扇区在完成了它的使命之后就没有用了,可以视为空闲内存。
运行之后,可以看到K字母,即代表成功由内核在控制了。
mark

扩充内核

切换堆栈和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
32
SELECTOR_KERNEL_CS	equ	8

; 导入函数
extern cstart

; 导入全局变量
extern gdt_ptr

[SECTION .bss]
StackSpace resb 2 * 1024
StackTop: ; 栈顶

[section .text] ; 代码在此

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
28
include "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。
运行结果如下:
mark

整理文件

  • boot.asm和loader.asm放在单独的目录/boot中,相应的头文件也放在里面;
  • klib.asm和string.asm放在/lib中,作为库;
  • kernel.asm和start.c放在/kernel里面
    目录树如下:
    mark

    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
2
target: prerequisites
command

上面这样的形式表示:

  1. 要想得到target,需要执行命令command.
  2. target依赖prerequisites,当prerequisites中至少有一个文件比target文件新时,command才被执行。
    比如这个Makefile的最后两行,翻译出来就是:
  3. 要想得到loader.bin,需要执行“$(ASM) $(ASMFLAGS) -o $@ $<”。
  4. 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的结果图:
    mark
    mark
    接着对makefile扩展之后,我们可以通过make building 和make image很方便的把引导扇区、load.bin和kernel。bin写入虚拟软盘。如下所示:
    mark
    然后再启动bochs,同时在start.c加上显示cstart end,来表示我们的make正常的进行了编译连接了,结果如图所示:
    mark

    添加中断处理

    中断要做的工作为:设置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
    32
    PUBLIC 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. */
#define INT_M_CTL 0x20 /* I/O port for interrupt controller <Master> */
#define INT_M_CTLMASK 0x21 /* setting bits in this port disables ints <Master> */
#define INT_S_CTL 0xA0 /* I/O port for second interrupt controller<Slave> */
#define INT_S_CTLMASK 0xA1 /* setting bits in this port disables ints <Slave> */
......
/* 中断向量 */
#define INT_VECTOR_IRQ0 0x20
#define INT_VECTOR_IRQ8 0x28

这个函数只用到了一个函数,就是用来写端口的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
10
OBJS		= 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自动生成依赖关系如下图:
mark
下面初始化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
48
PUBLIC 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);
}
}

以下就是中断异常的情况:
mark
所有的中断都会触发一个函数spurious_irq(),这个仅仅是把IRQ号打印出来。
接着,我们向主8259A相应端口写入了0xFD,由于0XFD对应的二进制是11111101,于是键盘中断被打开,而其他中断任然处于屏蔽状态,最后在kernel.asm中添加sti指令设置IF位,然后make后,当我们敲击键盘任意键就如出现如下图:
mark

您的支持将鼓励我继续创作!