跟我一起写操作系统

    返回首页    发表留言
本文作者:李德强
          第四节 进入保护模式
 
 

        这一节我们就来一起学习如何设置并使用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

    返回首页    返回顶部
#1楼  漫步者  于 2018年05月03日14:01:05 发表
 
博主 打开cr0后是保护模式 原来的取指令cs ip里的cs是_SEG_MAIN(0x9000) 这样的话无法执行将kernel程序copy到0x0处的指令代码
  看不清?点击刷新

 

  Copyright © 2015-2018 问渠网 辽ICP备15013245号