Oranges 读书笔记之加载内核

来源:互联网 发布:数据库的审计类型有 编辑:程序博客网 时间:2024/05/20 13:16

接我的上一 篇博客

        当成功跳转到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头和程序头表中的信息将内核复制到指定地址,最后跳转到该地址处,内核真正开始执行。


原创粉丝点击