这一节我们就来一起学习如何设置并使用GDT,打开A20,关闭中断并进入保护模式。好消息是现在我们可以使用C语言来编写程序代码了,这比使用汇编指定简单、易懂的多。但是有时候还得写一些汇编代码,或是在C代码中内嵌一些汇编代码,比如说我们要读写计算机的硬件接口,只能通过汇编代码来进行操作,那么我们可以写这样的内嵌指令:
//向端口写一个字节
static inline void outb_p(u8 val, u16 port)
{
__asm__ volatile("outb %0, %1" : : "a" (val), "dN" (port));
}
//从端口中读入一个字节
static inline u8 inb_p(u16 port)
{
u8 val;
__asm__ volatile("inb %%dx, %%al" :"=a"(val) : "dx"(port));
return val;
}
在kernel内核程序被执行之前我们首先要做的第1件事是:关中断。是不是没有想到?因为原来用于16位实时模式下的BIOS中断程序已经完全不能使用了,原因有两个。第一,我们的boot引导程序将会把CPU带入保护模式,在保护模式下原来的16位BISO中断程序不再能够使用。第二,boot引导程序会将kernel内核代码复制到0x0地址,这里原来存放的就是BISO的中断向量。很明显,这些中断向量被kernel内核程序所覆盖,于是BISO中断程序不能够使用。关闭中断程序的内嵌汇编代码如下:
__asm__ volatile("cli");
前面内容提到,CPU在32位保护模式下可以使用32位内存地址线。那么,如何让CPU的地址线从20根变为32根呢?答案是A20。简单的来说就是启用CPU的32根内存寻址线,可让CPU进行4GB内存寻址,具体方法如下:
u8 port_a;
//从0x92端口读入数据
port_a = inb_p(0x92);
//打开A20
port_a |= 0x02;
//不重置电脑
port_a &= ~0x01;
//向0x92输出设定后的值
outb_p(port_a, 0x92);
再来看一下GDT这个描述符的C语言数据结构:
//GDT全局描述符
typedef struct gdt_s
{
u16 limit;
u16 baseaddr;
u8 baseaddr2;
u8 p_dpl_type_a;
u8 uxdg_limit2;
u8 baseaddr3;
} s_gdt;
这个结构体与前面小节所讲述的GDT结构是完全一致的,里面包括了内存地址区域大小、基地址、访问权限和一些其它参数。还要定义一个GDT_PTR数据结构,它GDT描述符,用来指定GDT的大小和在内存中的位置:
//GDT全局描述符
typedef struct gdt_ptr
{
u16 gdt_lenth;
u16 gdt_addr;
u16 gdt_addr2;
} s_gdtp;
有了GDT表和GDT_PTR的数据结构,现在还要在main函数之前定义3个GDT和一个GDT_PTR。3个GDT分别用来存放默认段地址、内核代码段地址和内核数据段地址。GDT_PTR用来存放GDT的大小和地址:
//全局描述符表个数
#define GDT_MAX_SIZE (3)
//全局描述符表
s_gdt gdts[GDT_MAX_SIZE];
//全局描述符
s_gdtp gdtp;
接下来定义一个C语言函数,用来将一个32位的物理地址转为GDT全局描述符表:
/*
* addr_to_gdt : 将物理地址转为gdt描述地址,并存放到gdt全局描述符当中
* - u32 addr : 32位物理地址
* - s_gdt *gdt : gdt全局描述符
* - u8 cs_ds : 0为代码段,1为数据段
* return : void
*/
void addr_to_gdt(u32 addr, s_gdt *gdt, u8 cs_ds)
{
//最大尺寸低16位
gdt->limit = 0xffff;
//基地址低16位
gdt->baseaddr = addr & 0xffff;
//基地址中8位
gdt->baseaddr2 = (addr >> 16) & 0xff;
//如果是代码段
if (cs_ds == 0)
{
//代码段描述
gdt->p_dpl_type_a = 0x9a;
}
//如果是数据段
else
{
//数据段描述
gdt->p_dpl_type_a = 0x92;
}
//相关标识和最大尺寸高位
gdt->uxdg_limit2 = 0xcf;
//基地址高8位
gdt->baseaddr3 = (addr >> 24) & 0xff;
}
有这个函数就可以定义默认段地址、内核代码段地址和内核数据段这3个GDT了:
//默认地址
u32 addr = 0x0;
//默认空描述符0x0
gdts[0].gdt = 0x0;
gdts[0].gdt2 = 0x0;
//设置kernel的全局描述符0x8
addr = 0x0;
addr_to_gdt(addr, &gdts[1], 0);
//设置kernel data的全局描述符0x10
addr = 0x0;
addr_to_gdt(addr, &gdts[2], 1);
3个GDT定义完毕,还要对GDT_PTR进行赋值:
//设置gdt描述符 //gdt总数减1 gdtp.gdt_lenth = sizeof(s_gdt) * GDT_MAX_SIZE - 1; //gdt全局描述符的高16位地址 gdtp.gdt_addr2 = ds() * 0x10 >> 16; //gdt全局描述符地址低16位 gdtp.gdt_addr = (u32) gdts;
准备工作完毕,调用汇编过程,跳转到保护模式:
//跳转到保护模式,不再返回,直接启动内核程序 _to_the_protect_mode();
_to_the_protect_mode是一个汇编过程它的具体内容如下:
//跳转到保护模式 _to_the_protect_mode: //保存现场 pushl %ebp movl %esp, %ebp //载入gdt全局描述符 lgdt gdtp //打开保护模式,将cr0的0位置成1 movl %cr0, %eax orl $0x1, %eax movl %eax, %cr0 //将0x9c00处的kernel程序copy到0x0处 movw $_GDT_IND_KERNEL_DATA, %ax movw %ax, %ds movw %ax, %es movl $_SEG_KERNEL_PH, %esi movl $0x0, %edi //将%ecx寄存器置成内核程序大小 movl $_KERNEL_SIZE, %ecx _copy_kernel: movw %ds:(%esi), %ax movw %ax, %es:(%edi) //每次加2 add $0x2, %esi add $0x2, %edi sub $0x2, %ecx cmp $0x0, %ecx jne _copy_kernel jmp _copy_kernel_end _copy_kernel_end: nop //处理所有寄存器,为跳转到保护模式做准备 movw $_GDT_IND_KERNEL_DATA, %ax movl $_SEG_KERNEL_DATA_OFFSET, %ebx //设定全局选择子 movw %ax, %ds movw %ax, %es movw %ax, %fs movw %ax, %gs movw %ax, %ss //设定相对内存地址 xorl %eax, %eax movl %eax, %edi movl %eax, %esi //设定堆栈地址 movl %ebx, %esp movl %ebx, %ebp //跳转到(0x000000000)处,不再返回这里 //内核实际地址在_SEG_KERNEL_OFFSET _ljmp: .byte 0xea _ljmp_offset: .word _SEG_KERNEL_OFFSET _ljmp_section: .word _GDT_IND_KERNEL //恢复现场 popl %ebp retl _to_the_protect_mode_end: nop
需要说明的是:我们将boot引导程序复制kernel内核程序的代码移植到了打开A20和设置GDT之后,并在跳转到内核程序之前。原因是在16位模式下,我们只能使用16位的CPU寄存器,而在后续的内核程序编写过程中,kernel内核程序将会不断的增大,我们为它预留的大小是0x80000个字节,也就是512KB,在boot程序复制kernel程序时,不能使用cx寄存器存放0x80000这个数值(在16位实时模式下也有办法复制比较大的数据,可以采用双循环嵌套方式。这里不再提供例子),但是在打开了A20和设置了GDT之后我们就可以使用ecx(注意是%ecx而不是%cx)来存放0x80000这么大的数值了。可以方便的复制0x9c00处的kernel内核程序到0x0处。
再看一下kernel内核程序中的代码,注意:所有kernel中的代码已经是32位程序了:
#include <kernel/kernel.h> //全局字符串指针变量 char *str = "Hello World!"; //内核启动程序入口 int start_kernel(int argc, char **args) { //显存地址 char *p = (char *) 0xb8000; //显示str的内容到显示器上 for (int i = 0; str[i] != '\0'; i++) { p[i * 2] = str[i]; } //永无休止的循环 for (;;) { } return 0; }
最后执行make all来看一下运行结果:
源代码的下载地址为:
https https://github.com/magicworldos/lidqos.git git git@github.com:magicworldos/lidqos.git subverion https://github.com/magicworldos/lidqos branch v0.6
Copyright © 2015-2023 问渠网 辽ICP备15013245号