第五章 内核雏形大总结(转载)

【总结】【操作系统内核工程】
2008年04月27日 星期日 00:37

【总结内核框架】

×麻雀虽小五脏俱全,这个系统框架主要分3大块。下面就一个一个来细说:

一、Boot.bin区(引导代码块):

从开机到BIOS自检,然后BIOS把主控制权交给Boot.bin!!!

Boot.bin的设计是这样的:

【×头文件区】

1、fat12hdr.inc(FAT12磁盘格式。这是我们文件系统格式头).

里面就是一个简单的FAT12文件系统的引导扇区格式结构体.它决定了整个文件系统。

如果想要新建或者查找某个文件。那么必需遵守FAT12的文件格式。

2、Load.inc(Loader.bin、Kernel.bin被加载到的地址宏)

它是用来表示Loader代码和Kernel(内核)代码被分配的地址区域信息宏。

【×代码文件区】

1、boot.asm(引导程序)

这个代码区会包含两个头,也就是:fat12hdr.inc 、load.inc。

它是被BIOS装载到0000:7c00处的引导程序,这个时候CPU还不是保护模式。

它的任务是根据FAT12文件系统的结构,在A盘根目录下查找Loader.bin文件,具体查找的方式也就是2个循环。

×外循在以根目录区为基地址,每次循环增加一个扇区

×内循环以32位根目录项为单位。每次加一个根目录项也就是+=32;

那么经过多次循环后就会在根目录项中找到Loader.bin的信息。如果没找到那么就提示没找到。无法加载Loader.bin装载器。

找到Loader.bin信息后,会根据簇号与FAT1对应读出文件的所以数据,然后根据load.inc里面的BaseLoaderPhyAddr地址。连续读出到这段内存。在进行FAT1数据判断时代码有点复杂。不过原理还是根据FAT12文件格式来处理的。

这样Loader.bin就被正常到装载到 BaseLoadPhyAddr位置了。这是个物理地址。下面Boot.bin的使命就完成了。接着跳转到BaseLoadPhyAddr:0处,从此 Loader.bin获得主控权!!!

二、Loader.asm区(装载内核的代码块)

从Boot.bin接手。Loader.bin的设计是这样的:

【×头文件区】

1、fat12hdr.inc(FAT12文件格式头)

因为Loader.bin也需要在FAT12文件系统的A盘寻找Kernle.bin文件。所以这个FAT12格式是必不可少的头。它有一些有用的FAT12信息。

2、Load.inc(Loader.bin、Kernel.bin被加载到的地址宏)

上面就有说明了它是用来控制Loader.bin、Kernel.bin被加载到的位置宏。

3、pm.inc(保护模式用的宏)

它是在Loader.inc进入保护模式时要用到的,比如GDT结构体、段属性等宏。

【×代码区】

1、Loader.asm(装载器)

从boot引导区拿到主控制权,哈现在是我的天下了!

那么现在我就先找到Kernel.bin文件。查找方式与在A盘查找Loader.bin是一样的。只不过找到后是将Kernel.bin的数据块 以扇区为单位,连续装载到BaseKernelPhyAddr处。这个时Loader.bin并不是马上就退位让kernel.bin掌权!

Loader.bin是个好人。好事做到低先把一些保护模式的初始化工作做了先。在做这些工作之前,它要确保Kernel.bin已经被正常加载到BaseKernelPhyAddr处。不然不是白费功夫!!!

好了要做的保护模式工作是:

×GDT全局描述符定义:

; GDT ------------------------------------------------------------------------------------------------------------------------------------------------------------
;                                                段基址            段界限     , 属性
LABEL_GDT:    Descriptor             0,                    0, 0       ; 空描述符
LABEL_DESC_FLAT_C:   Descriptor             0,              0fffffh, DA_CR | DA_32 | DA_LIMIT_4K    ; 0 ~ 4G
LABEL_DESC_FLAT_RW:   Descriptor             0,              0fffffh, DA_DRW | DA_32 | DA_LIMIT_4K    ; 0 ~ 4G
LABEL_DESC_VIDEO:   Descriptor 0B8000h,               0ffffh, DA_DRW                         | DA_DPL3 ; 显存首地址
; GDT ------------------------------------------------------------------------------------------------------------------------------------------------------------

GdtLen   equ $ - LABEL_GDT
GdtPtr   dw GdtLen - 1     ; 段界限
   dd BaseOfLoaderPhyAddr + LABEL_GDT   ; 这个是被我们自己加载的地址并不是DOS系统了。

; GDT 选择子 ----------------------------------------------------------------------------------
SelectorFlatC   equ LABEL_DESC_FLAT_C - LABEL_GDT
SelectorFlatRW   equ LABEL_DESC_FLAT_RW - LABEL_GDT
SelectorVideo   equ LABEL_DESC_VIDEO - LABEL_GDT + SA_RPL3
; GDT 选择子 ----------------------------------------------------------------------------------

可以看到定义了3个段,分别是:0-4GB的平坦可执行可读代码段、0-4GB的平坦可读写数据段。 0b8000h-(0b8000h+0ffffh)的可读写彩色显示内存段。有了这3个段描述符,那么我们就可以尽情的发挥保护模式的寻址功能了。至于堆 栈段,Loader.bin在装载的时候就预留了一段空间作为堆栈段,它可以用SelectorFlatRW平坦段来访问。

×准备打开分页机制的函数

还记得吗,为了节省内存我们需要实现计算一下计算器的物理内存,这个要在实模式的时候调用int 15 bios中断来处理。那么在Loader.bin开始就先从int 15h获取一些信息,把它保存在保护模式的全局数据段里面。虽然是保护模式的数据段,不过在没有置PE位时,程序还是以实模式方式访问内存变量的。那么就 现在在实模式下写入这些保护模式的全局变量。int 15h具体方法在内存分页那里就有解说!

得到这些信息后,再写一个函数(SetupMagping)对需要的页目录表以及页表进行初始化.页目录的位置宏在load.inc里面,它的基地 址是2MB,也就是代表这CR3是指向这个地址的。在这个函数里把页目录表以2MB为基地址定义4KB个页目录项,并且每个页目录项里的内容是页目录表随 后以2MB+4KB位基址,以4KB为对齐方式的赋值!!也就是说 PDE0=2MB+4KB,那么PDE1=2MB+4KB+4KB,其中PDE0里面又包含了1024个PTE .,PDE0的PTE0的值是0.这个0表示是物理页基地址,这要根据分页机制来说,假如一个线性地址的值是

[h10]              [c10]            [l12]

0000000000 0000000000 000000000000h,这是一个32位的线性地址。是一个分页地址。

首先是以高10为目录项的位置,也就是h10,它的值是0那么就表示应该在CR3对应的页目录表其中位置是0的页目录当中,得到PDE0.

接下来以中间10位为表项的位置也就是c10,它的值是0,所以也就是PTE0.。那么到现在我们已经知道这个线性地址的物理基地址。它是 PDE0->PTE0的值。低12位是物理基地址的偏移。那么l12也等于0那么偏移也就是0所以它的物理地址经过一系列转换也是0,如果想改变0 这个线性地址对应1物理地址的话,那么只需要把PDE0的值改成1就行了。

还了现在页目录表已经初始化了,那么就启用它吧.是在SetupPaging函数里面启动分页:

mov     eax,BasePageDir;2MB

mov    cr3,eax

mov eax,cr0

or   eax,80000000h ;PG

mov cr0,eax

jmp        short .3 ;分页正是开启。这个跳转就用到了CR3的表

.3:

nop      

×进入保护模式

准备工作都做的差不多了 GDT有 启动分页的函数也有了那么我们就开始冲刺吧:

在正确装入Kernel.bin后:

;加载GDT

lgdt [GdtPtr]

cli ;置IF为0 关中断

in    al,92h      ;得到92h端口返回的数据

or al,2h

out 92h,al      ;打开20地址线。使CPU可以用32位地址线

mov eax,cr0

or al,01       ;PE

mov cr0,eax     ;开启保护模式

jmp     SelectorFlatC:(BaseOfLoaderPhyAddr+LABEL_PM_START) ;go to保护模式

×保护模式的初始化

欢迎来到保护模式,AI32将带给你安全感。` 0 `。

接下来就是保护模式的初始化工作了。值得一提的是:在Loader.bin我们只要将加载一个GDT和开启分页机制就够了。省下的还是让Kernel.bin去自由发挥吧。

一些寄存器的初始化工作还是要做的。比如:

mov ax,SelectorFlatRW

mov ds,ax

mov es,ax

mov ss,ax

mov fs,ax

mov ax,SelectorVideo

mov gs,ax

mov esp,TopKernelStack    ;这个在堆栈段就在数据段的末尾 预留了1000h空间

call SetupMaping ;初始并启动分页

InitKernel          ;从新分配Kernel.bin的位置。之所以要有这个函数是因为这个Kernel.bin可并不是跟Loader.bin一类的了,它是ELF格式的文 件。并不是纯二进制文件。当它被Loader.bin装载到内存的时候它的数据是原始的按扇区单位对齐的。而这个时候它的开始是一个ELF_header ,并不是开始执行代码。那么就需要一个InitKernel函数来分析这个头。把根据Program_header的具体信息。把它装载到相应的内存地 址。这里它的地址被迁移到了0x30400出 这行并不是文件头,而是可执行代码处。那么在0x30400前面就是ELF_header信息了。

经过这个函数后。Kernel.bin以被调整好入口地址了。那么接下来就:

jmp         SelectorFlatC:KernelEntryPointPhyAddr 跳到了0x30400处

到这里Loader,bin的使命完成了,。它培养出来的Kernel.bin就要上任了。在交主控权之前。它已经为Kernel.bin 做了平坦GDT初始化与智能的分页初始化了。那么现在它可以放心的交给Kernel.bin了。

三、Kernel.bin区(内核代码区)

长江后浪推前浪,` 0 `这个形容好像不怎么恰当。不过没关系反正现在是Kernel.bin获得了主控制权!

那么得到这个主控制权以后,该干点什么呢?首先还是先跟其他不同语言的民族打下交道吧。建立好外交关系!来看看Kernel.bin的设计:

【×头文件区】(也许文件有点多。但是这样排列是有意义的)

以.h开头的都是C代码要用到文件,与汇编有联系的代码需要在ld链接的时候才能结合起来,现在只能声明一些导入与导出的函数名为链接提供说明。

1、type.h(定义类型)

typedef 自定义说明基本类型头文件,这个文件显示性的表示了常规数据类型但意义不同的字符名。

比如: #define t_32 unsigned int     ;显示的说明这t_32是32位无符号整数!

#define t_prot unsigned int     ;显示的说明这t_prot(端口)是32位无符号整数!

2、const.h(常量宏)

一些经常要用的常量,以及宏说明!

比如,特定的地址。数值大小,特权级别,IO端口等,。

3、protect.h(保护模式要用的数据类型以及属性宏)

比如: DESCRIPTOR 、GATE等结构体。

#define DA_32 0x4000 //段属性宏

4、string.h(内存数据串复制操作)

内存数据memcpy(dst , src ,count);

5、proto.h(函数声明)

klib.asm函数里面的声明。

在klib汇编会导出它的汇编函数,那么我们在proto.h声明这样就可以可C语言来使用。

6、global.h(全局变量)

这个头记录着汇编代码与C代码共享使用的全局变量。

t_8 gpt_ptr[6]; DESCRIPTOR gdt[GDT_SIZE],GDT共享使用

t_8 ipt_ptr[6];   GATE idt[IDT_SIZE],IDT共享使用

【×代码文件区】

Kernel.bin的代码区可就没上面那二个代码区简单了。因为这个代码区是C语言与汇编代码混合编程的。从Loader.bin跳转到Kernel.bin的入口代码区先来看:

1、kernel.asm(kernel.bin入口)

这个是最直接的入口,是直接从Loader,bin跳过来的。它的入口_start 是导出的,。并且指定是0x30400地址的。在这里有三个导入函数跟三个导入变量。 导入导出只要指定名字符号就行了。这些符号链接信息保存在ELF格式里。

;导入函数

extern cstart

extern exception_headler

extern spurious_irq                  ;INTR,8259A中断处理

;导入变量

extern gdt_ptr     ;这个kernel全局的GDT指针,它导入的目的就是让Kernel.asm把在Loader.bin的GDT传给这个Kernel全局GDT。

extern idt_ptr     ;这个也是kernel全局的IDT指针,但是它在Loader.bin并不存在。它是在 cstart函数里面初始化的。

extern disp_pos ;这个是全局记录这彩显的位置。显示缓冲区地址。

接着定义Kernel.bin的堆栈,这是一个程序的节区。

[section .bss]

StackSpace resb 2*1024 ;定义非初始化的空间

TopStack:        ;栈顶 ,在boot.bin区它有自己的堆栈,loader.bin有两个模式的堆栈(Real Protect)。

以上是定义,下面再来看看具体的步骤:

×初始化Kernel环境

mov esp,TopStack ;用上最新的属于内核自己的堆栈。

mov disp_pos,0 ;全局显示位置到屏幕开始

sgdt[gdt_ptr] ;这句是从gdtr寄存器获取信息,这时的GDTR值是Loader.bin状态的,现在把它哪下来转存到Kernel的全局变量里面.

call cstart;                        ;start.c代码区讲解

lgdt [gdt_ptr]   ;加载被cstart函数处理后的gdt_ptr

lidt [idt_ptr] ;加载中断描述符到IDTR 这idt_ptr当然也在csart 函数中搞鬼了。

jmp      SELECTOR_KERNEL_CS:csint ;这个跳转用的GDT不在是Loader.bin的了 已经是Kernel自己的全局GDT,并且Kernel现在已经拥有自己的IDT,分页表还是在Loader,bin的指定处.

csint:

     sti        ;开中断IF=1

hlt          ;CPU停止等待中断唤起

接着说下对应硬件8259A与软件异常的处理:

global divide_handler

..... 这里导出了很多软中断处理的模块偏移值。这个值是要赋给IDT中对应的VECTOR

global hwint00

.....这里导出了很多硬件8259A中断处理模块偏移值,这个值也是要赋给IDT中对应的VECTOR

以下是被导出的函数具体定义:

;主8259A片中断处理宏.

%macro hwint_master 1

       push %1         ;将传来的参数压栈,在这之前 %1,eip cs eflags 已经被压栈

       call spurious_irq , 这个函数是导入函数,。在i8259.C里

       add   esp,4 ; esp指向eip 硬件8259A中断后都是Fault可以修复的错误,并不是Trap 没有错误代码,

      hlt          ;等待中断

%endmacro

align 16   ;8259A是实模式处理的按16位对齐提高访问速度.

hwint00:    ;主片IRQ0中断

             hwint_master 0

........

;从8259A片中断处理宏.

%macro hwint_slave 1

       push %1         ;将传来的参数压栈,在这之前 %1,eip cs eflags 已经被压栈

       call spurious_irq ,这个函数是导入函数,。在i8259.C里

       add   esp,4 ; esp指向eip 硬件8259A中断后都是Fault可以修复的错误,并且它 没有错误代码,

      hlt          ;等待中断

%endmacro

hwint08:

             hwint08 08

....

;---------软中断-----------

divide_err:

                push 0xffffffff    ;无错误码 ,如果有错误码并不是在这里指定的。是在中断发生的时候。压栈的。那么错误码也就是程序自己定义的。并不需要IDT定义。

              push 0        ;向量号

             jmp exception

double_falut:; 双重错误,

            push 8;

           jmp exception    ;这里没有将0xffffffff压栈证明需要自己在中断时发生时push err_code.

                 ...........

exception:

           ; 所有中断处理程序最终都到这里来进行filter筛选

          call     exception_handler   ;这个函数是导入函数。是C代码处理的。

          add   esp,2*4 ;esp 指向eip

     hlt

好了现在来总结一下 8259A的中断是没有错误代码的。因为它是硬件中断不用区分了,它都对应了相应的IRQ值。而int 软件中断需要错误代码。并且是在程序那变压栈的。,   

2、start.c(从Kernel.asm 跳入)

cstart()函数直接在这里定义就行了 用GCC编译后会有cstart 链接符号,在Kernel.asm已经声明了外部链接符cstart.那么就可以直接找到这个函数的位置。那么在C代码不存在导出(Global)因为只 有在汇编里使用Global才会生成链接符,在C代码里就算不使用Global 也有链接符。如果要用C代码里的东西只需要extern声明导入链接符合就可!

它将包含一些需要的头文件:

#include "type.h" ,#include "const.h",#include "protect.h",#include "string.h",#include "proto.h" #include "global.h",包含了如上头文件.

首先调用memcpy(&gdt,(void*)*((t_32*)(&gdt_ptr[2])),*((t_16*) (&gdt_ptr[0])) + 1);它的作用是把被sgdt保存的Loader.bin时候的GDTR指向的GDT表全部复制到Kernel内核区的全局变量gdt里面。

定义两个属于Kernel局部GDT指针结构体,用作lgdt 、sgdt的参数

t_16 *p_gdt_limit = (t_16*)(&gdt_ptr[0]) ;得到Loader.bin状态的GDT_limit

t_32 *p_gdt_base =(t_32*)(&gdt_ptr[2]) ; 得到 Loader.bin状态的GDT_base

p_gdt_limit = GDT_SIZE *sizeof (DESCRIPTOR) - 1 ;得到Kernel状态的GDT_limit

p_gdt_base = (t_32)(&gdt) ;得到Kernel状态的GDT_base

定义两个属于Kernel局部IDT指针结构体,用作lidt 、sidt的参数

t_16 *p_idt_limit = (t_16*)(&idt_ptr[0]) ;得到Loader.bin状态的IDT_limit

t_32 *p_idt_base =(t_32*)(&idt_ptr[2]) ; 得到 Loader.bin状态的IDT_base

p_idt_limit = IDT_SIZE *sizeof (GATE) - 1 ;得到Kernel状态的IDT_limit

p_idt_base = (t_32)(&idt) ;得到Kernel状态的IDT_base

init_prot();//中断IDT需要用的一些初始化工作了。这个函数在protect.c里面.

disp_str("哈哈 GDT与IDT完全初始化完毕准备回到kernel.asm去,等待hlt");

3、protect.c(保护模式功能代码)

这里主要有两个功能模块,一个是初始化8259A.二个是初始化IDT中断描述符表。

它将包含一些需要的头文件:

#include "type.h" ,#include "const.h",#include "protect.h "、 #include "global.h",包含了如上头文件.

//初始化IDT的中的某个GATE描述符。

PRIVATE void idt_init_desc(unsigned char vector,t_8 desc_type,t_pf_int_handler handler,unsigned char privilege){

GATE *p_gate = idt[sector]; //根据向量号对齐方式,得到对应的GATE位置。

t_32 base = t_32(handler) ;定义一个局部变量来拆分函数的偏移地址

p_gate->offset_low= base & 0xffff

.... //对此向量号对应的GATE字段进行初始化

}

PUBLIC void init_prot()   //这个函数就被cstart()调用的。

{

          init_8259A();       //初始化8259A外部设备中断.在i8259.c里定义

//下面这条就是在初始化向量号为DIVIDE的中断描述符。其类型都为门。 这些常量在const.h和protect.h里

//而divide_error是kernel.asm那边导出来的函数。是DIVIDE对应的中断处理代码偏移值。

            idt_init_desc(INT_VECTOR_DIVIDE,DA_386IGate,divide_error,PRIVILEGE_KRNL) ;

             ...............类似的定义剩下的IDT项其类型都是GATE.特权都是0,

//值得一提的是下面这个IDT初始化。它是一个硬中断。8259A主IRQ0是0x20,同样hwint00是处理中断的代码偏移位置

          idt_init_desc(INT_VECTOR_IRQ0,DA_386IGate,hwint00,PRIVILEGE_KRNL)

                         ........类似的与hwint00一样的定义。

}

接下来是定义我们的异常处理程序了,它并不是属于硬件8259A中断系列的。

PUBLIC void exception_handler(int vec_no,int err_code,int eip,int cs,int eflags)

{

     //当中断产生时,CPU会自动:push eflags、push cs、push eip。

我们手动:push err_code(如果没有错误码那么就直接压栈中断向量号)、push vec_no.

       //那么CPU自动了压入push eflags、push cs、push eip。 根据C语言调用规则。eflags算是最后一个参数了。以此类推!如果先前没有压栈err_code.只需要判断 err_code的值不是vec_no的范围就行了.

Disp_ptr("异常出现了");这里我就不写真正的显示代码了 ,那些代码比较繁杂。

}

以上这些就是protect.c的函数模块。

4、i8259A.c(8259A外部中断模块)

它是用来初始化硬件中断的,在init_prot()函数中首先就调用了这里的初始化模块。

PUBLIC void init_8259A()

{

     //以下这些 out_byte in_byt 函数都在klib.asm 库文件中.

//而INT_M_CTL 、INT_S_CTL的值是0x20 、0xA0。代表的是写ICW1要用的端口

     out_byte(INT_M_CTL,0x11) //ICW1         使用主片ICW4,并且激活从片

   out_byte(INT_S_CTL,0x11)//ICW1            使用从片ICW4

//INT_M_CLTMASK 、INT_S_CTLMASK对应 0x21、0xA1 代表写ICW2-ICW4要用的端口

out_byte (INT_M_CTLMASK,INT_VECTOR_IRQ0) //ICW2   定义主片的IRQ0号对应的VECTOR

out_byte (INT_S_CTLMASK,INT_VECTOR_IRQ8)//ICW2 定义从片的IRQ8号对应的VECTOR

out_byte (INT_M_CTLMASK,0x4) //ICW3   从片与主片级连 IRQ2对应从片

out_byte (INT_S_CTLMASK,0x2)//ICW 3 重定向IRQ2=IRQ9

out_byte (INT_M_CTLMASK,0x1) //ICW4 8259A主片工作模式

out_byte (INT_S_CTLMASK,0x1)//ICW 4 8259A从片工作模式

out_byte(INT_M_CTL,0xff) //OCW1 屏蔽主片所有中断

out_byte(INT_S_CTL,0xff) //OCW1 屏蔽从片所有中断

mov al,0x20或者mov al,0xA0

out 20h或者out A0H 可以告之中断处理完毕。。EOI发送回去

}

PUBLIC spurious_irq(int irq)

{

//这个函数是处理外部中断用的。也就是8259A硬件中断。

disp_str("我是硬件中断");

}

到此i8259A.c里面就定义了两个函数 一个是init_8259A初始化可编程控制器用的。还有一个是

spurious_irq()它类似与protect.c里的exception_handler()处理函数。指不过一个是处理硬件中断一个是处理软件中断的。

5、klib.c与klib.asm ,string.asm

这几个文件是一些功能模块,也就是给其他代码提供相应的功能字符串显示。和数据处理等功能。

klib.asm:

disp_str;该函数根据全局变量disp_pos,位置,显示压栈的字符串地址。

disp_color_str;这个函数跟disp_str一样 只不过ah 的值变了。

out_byte: ;用作8259A.c里面的端口读写

mov edx,[esp + 4] ;第一个端口

mov al,[esp + 8] ;第二个参数

out dx,al     

nop

nop

ret

in_byte:

...与out_byte一样只不过是in al,dx

klib.c:

itoa(),l功能是整形转换成字符串 其转换方式是BCD解码。

disp_int();显示一个整数。,它会先调用itoa();

string.asm:

这个文件里就一个函数:

memcpy:

         push ebp

    mov ebp,esp

      .......对压栈的地址赋值到esi edi 做相应的字符串赋值操作。比如lodsb     

pop ebp

ret

【完成】:

到这里我们用配置好makefile 来进行编译!!!

如果不配置makefile话那么我就手动巧命令行吧:

nasm -o boot.bin boot.asm -I boot/include

....

gcc -c -fno-builltin -o start.o start.c -I include

....

ld -s -Ttext 0x30400 -o kernel.bin........真晕累啊还是make 吧

make all

thak`ok 。感觉很舒服,对吧!!!

go on .....

posted on 2011-05-14 11:40  wanghj_dz  阅读(586)  评论(0编辑  收藏  举报

导航