第五章 内核雏形大总结(转载)
【总结内核框架】
×麻雀虽小五脏俱全,这个系统框架主要分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 .....