由于我不太懂怎么用C语言来设置GDT或者IDT之类的东西,所以我先学习了james先生的这篇教程:JamesM’s kernel development tutorials

GDT的实现基本都是james先生的代码,一切先从模仿开始,只有熟悉了基本的规则,后面才能自己发挥。由于我有《实模式到保护模式》这本书学到的基础,理解这些东西还算比较容易。

类型定义

这些类型定义是为了方便跨平台,如果换了平台只需要修改这些类型即可。

1
2
3
4
5
6
typedef unsigned char     uint8_t;
typedef unsigned short uint16_t;
typedef unsigned long uint32_t;
typedef uint32_t u32;
typedef uint16_t u16;
typedef uint8_t u8;

gdt.h

首先用结构来表示GDT的描述符,这个结构包含了段基地址,段界限,和各种位。(这些位的作用说起来很繁琐,所以不详细介绍了)

1
2
3
4
5
6
7
8
struct gdt_descriptor{
u16 limit_low;
u16 base_low;
u8 base_mid;
u8 access;
u8 granularity;
u8 base_high;
} __attribute__((packed));

每个描述符在GDT中占8个字节,也就是64位。结构的成员和这些块块分别对应。比如最下面的“段限长(LIMIT) 15..0”,对应结构中的u16 limit_low。

然后是定义GDTR的结构,这个是用来更新GDTR寄存器的。
这些字段显然是不言而喻的,表示GDT的基地址和GDT的界限。

1
2
3
4
struct gdtr_struct{
u16 limit;
u32 base;
}__attribute__((packed));

最后定义初始化函数,后面只需要在kmain中调用该函数即可安装GDT。

1
void init_gdt();

gdt.c

然后我们要定义相关初始化函数。

1
2
3
4
5
6
7
#include "gdt.h"

extern void gdt_flush(u32);
static void gdt_set_descriptor(int num,u32 base,u32 limit,u8 access,u8 gran);

struct gdt_descriptor gdt[5];
struct gdtr_struct gdtr;

gdt_flush 因为要用汇编来实现,所以用extern声明。
gdt_set_descriptor 是安装描述符的函数。

struct gdt_descriptor gdt[5]; 表示我们要安装5个描述符,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
void init_gdt(){

gdtr.limit = (sizeof(struct gdt_descriptor) * 5 - 1);
gdtr.base = (u32)&gdt;

gdt_set_descriptor(0,0,0,0,0);
gdt_set_descriptor(1,0,0xFFFFFFFF,0x9A,0xCF); //内核代码段
gdt_set_descriptor(2,0,0xFFFFFFFF,0x92,0xCF); //内核数据段
gdt_set_descriptor(3,0,0xFFFFFFFF,0xFA,0xCF); //用户代码段
gdt_set_descriptor(4,0,0xFFFFFFFF,0xF2,0xCF); //用户数据段

gdt_flush((u32)&gdtr);
}

void gdt_set_descriptor(int num,u32 base,u32 limit,u8 access,u8 gran){

gdt[num].base_low = (base & 0xFFFF);
gdt[num].base_mid = (base >> 16) & 0xFF;
gdt[num].base_high = (base >> 24) & 0xFF;

gdt[num].limit_low = (limit & 0xFFFF);
gdt[num].granularity = (limit >> 16) & 0xF;
gdt[num].granularity |= gran & 0xF0;
gdt[num].access = access;

}

gdt_set_descriptor 的功能就是把传入的参数,比如基地址和界限,按位分割好,分别传给base_low,base_mid等,在设置的时候就要自己算好,每个位是1还是0,否则会出现问题。

1
2
3
4
gdt_set_descriptor(1,0,0xFFFFFFFF,0x9A,0xCF); //内核代码段
gdt_set_descriptor(2,0,0xFFFFFFFF,0x92,0xCF); //内核数据段
gdt_set_descriptor(3,0,0xFFFFFFFF,0xFA,0xCF); //用户代码段
gdt_set_descriptor(4,0,0xFFFFFFFF,0xF2,0xCF); //用户数据段

可以看到内核的描述符和用户的描述符个别参数是不一样的,这是因为DPL和类型这些”位“的差异,比如用户的段运行在特权级3上,而内核段运行在特权级0上。具体细节可以参考GDT的文档。

1
2
3
4
gdtr.limit = (sizeof(struct gdt_descriptor) * 5 - 1);
gdtr.base = (u32)&gdt;
....
gdt_flush((u32)&gdtr);

最后是设置gdtr,将gdt的地址传给base,gdt的界限就是所有描述符长度的总和-1。因为每个描述符8字节,有5个描述符所以*5,最后-1。

因为设置gdtr要用到汇编执行,所以gdt_flush采用汇编编写。

gdt_s.s

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[global gdt_flush]


gdt_flush:
mov eax, [esp+4]
lgdt [eax]

mov ax,0x10
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
jmp 0x08:.flush //清空流水线并串行化处理器

.flush:
ret

首先要声明global,不然其他代码找不到它。

最后这段汇编的意思就是将栈中的&gdtr取出来,用lgdt指令将gdt的信息写入gdtr寄存器。

第8行到最后,因为我们内核数据段的选择子是0x10,所以先对各个段选择器进行初始化,它们的基地址全部变成0x00000000,界限都为0xFFFFFFFF,属性都是数据段。

end

最后在内核入口调用init_gdt();

1
2
3
4
5
6
7
8
9
int kmain(void){

init_gdt();

kwrite("gdt setting done!");
return;


}

还可以用gdb调式观察实际的情况: