由于我不太懂怎么用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。
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调式观察实际的情况: