接我的上一 篇博客
当成功跳转到loader时,所有的指挥权就都在loader手中,因为上文boot.bin中我们只实现了寻找,加载并跳入loader,而在操作系统内核执行之前的加载内核,跳入保护模式等步骤都没有完成。可想而知,这些任务都要交给Loader来完成。
一.加载内核
1.认识内核格式elf
elf文件由4个部分组成,分别是ELF header,Program header table,Sections和Section header table,其中只有ELF头的位置是固定的。
ELF的格式大致如下:
而ELF头格式如代码所示:
#define EI_NIDENT 16
typedef struct{ //大小
unsigned char e_ident[EI_NIDENT]; //16 包含用以表示ELF文件的字符及其他
Elf32_Half e_type; //2 文件类型(可执行文件为2)
Elf32_Half e_machine; //2 该程序的体系结构
Elf32_word e_version; //4 文件版本
Elf32_Addr e_entry; //4 程序入口地址
Elf32_Off e_phoff; //4 Program header table在文件的偏移
Elf32_Off e_shoff; // Section header table的偏移
Elf32_word e_flags; // 为0
Elf32_Half e_ehsize; // Elf header大小
Elf32_Half e_phentsize; // Program header table每个条目大小
Elf32_Half e_phnum; // Program header table的条目数
Elf32_Half e_shentsize; // Section header table条目大小
Elf32_Half e_shnum; // Section header table的条目数
Elf32_Half e _shstrndx; // 包含节名称的字符串表是第几个节
}Elf32_Ehdr;
为了完成加载并跳入内核,我们暂时只需要知道以上的e_entry,e_phoff,e_ehsize,e_phentsize,e_phnum
以及Program header 的结构
typedef struct{
Elf32_word p_type; //所描述的段的类型
Elf32_Off p_offset; //段的第一个字节在文件中的偏移
Elf32_Addr p_vaddr; //段的第一个字节在内存中的虚拟地址
Elf32_Addr p_paddr; //为物理地址保留
Elf32_word p_filesz; //段在文件中的长度
Elf32_word p_memsz; //段在内存中的长度
Elf32_word p_flags; //与段相关的标志
Elf32_word p_align; //根据此项值来确定段在文件以及内存中如何对齐
}Elf32_Phdr;
所以Program header描述了一个段的信息,我们把文件加载进内存就靠这些信息。
其他我们先不管。
假设我们已经有一个内核代码kernel.asm,我们用nasm的选项-f elf指定输出文件为elf文件格式。
nasm -f elf -o kernel.o kernel.asm
ld -s kernel.o -o kernel.bin
2.寻找并加载内核
我们把生成的内核拷贝到软盘上,然后修改loader.asm实现在软盘上寻找并加载内核到指定位置。
步骤同上文boot中寻找loader并加载。加载完成后关闭软驱马达,并显示一个字符串,具体代码如下:
二.跳入保护模式
因为一开始CPU是工作在实模式下的,在实模式下CPU为16位,有着16位的寄存器,16位的数据总线及20位的地址总线。只能寻址1MB,所以内存最大也只为1MB。从80386始,intel的CPU开始进入32位,有32位的地址线,可以寻址4GB。
在实模式下CPU寻址是通过段:偏移,段值由16位的CS,DS,SS等寄存器表示。每一个段的最大长度为64K,物理地址的计算遵循以下公式:物理地址=段值*10h+偏移。而保护模式下CPU寻址虽然也是段:偏移,不过此时的段已经不是实模式下的段了,尽管它的值也由段寄存器表示。此时它变成了一个索引,指向一个数据结构中的表项。这个数据结构我们称之为GDT
所以为了跳入保护模式,我们需要以下步骤:
1.准备GDT
具体见代码:
初看感觉GDT是一个结构数组,数组的每一个元素就是类型为Descriptor的段,以上代码初始化了段的基址,界限及属性。
Descriptor的定义如下:
可以看出Descriptor是一个宏
代码段和数据段描述符的具体结构如下:
现在看GDT表中各描述符的属性,分别有DA_CR,DA_DRW,DA_32,DA_LIMIT_4K,DA_DPL3.
DA_CR=9Ah,存在的可执行可读代码段;DA_DRW=92h,存在的可读写数据段;DA_LIMIT_4K,段界限粒度为4k;DA_DPL3=60h,特权值为3
而GdtLen是整个GDT表的长度,GdtPtr也是一个数据结构,前2个字节为GDT长度,后4个字节为GDT表的基址。
以Selector开头的称为选择子,看上去好像是段在GDT中的索引。CPU寻址的时候就是靠这个从GDT表中得到段的信息,从而正确寻址。Selector存储在CS,DS,ES等段寄存器中,类似于实模式下的段基址。
最后通过一个命令:lgdt [GdtPtr];加载GdtPtr的值到CPU的gdtr寄存器。该寄存器的结构与GdtPtr完全相同。
2.将CPU的工作状态转换为实模式
首先关中断,因为实模式下中断处理机制和保护模式下不同,然后打开A20地址线,通过操作端口92h,最后将cr0寄存器的第零位置为1,该位为1时,cpu运行于保护模式下。
实现代码如下
3.从实模式跳入保护模式
跳转只需要一句代码: jmp dword SelectorFlatC:(PM_START)
因为该跳转是在实模式下,而目的地址是在保护模式下,如果偏移超过64K,则可能被截断,所以在前面加dword
三.打开分页机制
分页机制就像一个函数,将物理地址映射为线性地址,那么如何映射呢?我相信看了下面的图就明白了:
在80386中每一个页的大小是4096字节,转换使用2级页表,每个表项4字节长,所以一个页表中最多有1024个表项。进行转换时,先从寄存器cr3指定的页目录中根据线性地址的高10位得到页表地址,再根据线性地址第12到21位得到物理页地址,最后根据低12位加上物理页首地址得到物理地址。
分页机制生效与否还取决于寄存器cr0的第31位称为PG位是否为1,若为1,则分页机制启动。关键代码如下:
以上的程序实现了最简单的映射,将线性地址映射成相同的物理地址,若要映射成不同的物理地址,可以修改初始化页表时该页表指向的物理页地址。
四.重新放置内核并跳入内核
我们的内核已经被加载到内存中,但是我们并不能直接跳转到内核开始处执行,我们得重新放置我们的内核。
为了使内核放在指定的地址,在生成elf文件时就要带上参数,-s -Ttext 0x30400将程序入口地址变成30400,关键实现代码如下:
根据elf文件的elf头和程序头表中的信息将内核复制到指定地址,最后跳转到该地址处,内核真正开始执行。