S01. 手写x86操作系统--引导程序和加载器load的实现

一、计算机的启动流程

  1. 在开机的一瞬间,CPU的cs:ip寄存器被强制初始化为0xf000:0xfff0。由于开机的时候处于实模式,在实模式下的段基址要乘以16,也就是左移4位,于是0xf000:0xfff0的等效地址是0xffff0,此地址便是BIOS的入口地址。
  2. 地址0xffff0距1MB只有16个字节,BIOS真正的代码不在这,此处的代码只是个跳转指令jmp far f000:e05b。段基址0xf000左移4位+0xe05b,即跳向了0xfe05b处,这是BIOS代码真正开始的地方。接下来BIOS便马不停蹄地检测内存、显卡等外设信息,当检测通过,并初始化好硬件后,开始在内存中0x000-0x3ff处建立数据结构,中断向量表(IVT)并填写中断例程。BIOS最后一项工作是校验启动盘中位于0号硬盘0磁道1扇区的内容。如果此扇区末尾的两个字节分别是魔数0x550xaa,BIOS便认为此扇区中确实存在可执行的程序(MBR),便加载到物理地址0x7c00,随后跳转到此地址,继续执行。BIOS跳转到0x7c00是用jmp 0:0x7c00实现的,这是jmp指令的直接绝对远转移用法,段寄存器cs会被替换,这里的段基址是0,即cs由之前的0xf000变成了0
  3. MBR(主引导记录,Main Boot Record)的任务是加载某个程序(这个程序一般是内核加载器,很少有直接加载内核的)到指定位置,并将控制权交给它。所谓的交控制权就是jmp过去而已。MBR代码通常只有512字节,无法容纳完整的操作系统或内核加载程序,因此通常会加载一个更大的二级加载器。
  4. 引导加载程序负责进一步初始化硬件,并将操作系统内核加载到内存中。它通常会读取磁盘上的文件系统,找到内核文件,并将其加载到内存的适当位置。内核加载完成后,引导加载程序将执行权交给内核,开始执行操作系统内核代码。操作系统内核接管后,开始初始化更高层次的系统资源和服务,最终启动用户空间的应用程序。

二、实模式vs保护模式

1、实模式

x86在上电启动后自动进入实模式,即16位工作模式,这种模式是最早期的8086芯片所使用的工作模式。早期的芯片设计得较简单、工作模式也较简单,所以有诸多限制:

  • 最大只能访问1MB的内存。采用段值:偏移的方式访问,内核寄存器最大为16位宽。如段寄存器CS, DS, ES, FS, GS, SS均为16位宽,AX, BX, CX DX, SI, DI, SP等也均为16位宽
  • 所有的操作数最大为16位宽,出栈入栈也以16位为单位
  • 没有任何保护机制,意味着应用程序可以读写内存中的任意位置
  • 没有特权级支持,意味着应用程序可以随意执行任何指令,例如停机指令、关中断指令
  • 没有分页机制和虚拟内存的支持

2、保护模式

 在后续的芯片设计中,intel为处理器增加了一些新的功能,可以实现某些保护功能,即保护模式。具体的特点如下:

  • 寄存器位宽扩展至32位,例如AX扩展至32位的EAX,最大可访问4GB内存(段寄存器依旧为16位)。
  • 所有操作数最大为32位宽,出入栈也为32位。
  • 提供4种特权级。操作系统可以运行在最高特权级,可执行任意指令;应用程序可运行于最低特权级,避免其执行某些特权指令,例如停机指令、关中断指令。
  • 支持虚拟内存,可以开启分页机制,以隔离不同的应用程序。

2.1 段描述符

在80386及其后续处理器的保护模式下,段描述符是一个8字节(64位)的数据结构,用于定义内存段的基址、限长和访问权限等信息。以下是段描述符的结构及各字段的详细作用。  

位范围字段名称大小描述
0-15段界限16 bits段的低16位限长,定义段的大小(以字节为单位)。
16-31段基址 16 bits段的低16位基地址,定义段的起始地址。
32-39段基址 8 bits段的中间8位基地址。
40-43类型TYPE4 bits描述段类型,例如代码段、数据段、系统段等。
44S 位1 bit描述符类型标志,0表示系统段描述符,1表示数据段描述符。
45-46DPL2 bits特权级,定义段的特权级别(0到3)。
47P 位1 bit段存在标志,1表示段存在,0表示段不存在。
48-51段界限4 bits段的高4位限长。
52AVL1 bit可用位,系统软件可自由使用。
53L 位1 bitL为1表示64位代码段,否则表示32位代码段,仅在IA-32e模式下有效。
54D/B 位1 bit默认操作大小(0表示16位段,1表示32位段)。
55G 位1 bit粒度标志,0表示段限长以字节为单位,1表示段限长以4KB为单位。
56-63段基址 8 bits段的高8位基地址。

2.2 段选择子

x86访问内存的机制是“段基址 : 偏移地址”,无论是实模式,还是保护模式,都要遵循此方式。在实模式下,段基址直接写在段寄存器中,而在保护模式下,段寄存器中的不再是段基址,而是段选择子,通过该选择子从GDT或LDT中找到相应的段描述符,从该描述符中获取段的起始地址。由于选择子的索引值部分是13位,即2的13次方是8192,故最多可以索引8192个段段选择子的结构如下:

字段名称描述
0-1请求特权级(RPL)描述符请求特权级,表示访问权限级别,范围0-3。
2表指示符(TI)表示使用GDT还是LDT,0表示GDT,1表示LDT。
3-15描述符索引(Index)描述符在GDT或LDT中的索引,用于定位具体的段描述符。

 2.3 段描述符表&GDT寄存器

在x86架构的保护模式下,段描述符表用于管理内存段,其是一个包含多个段描述符的数组。段描述符表有两种主要类型:全局描述符表(GDT)和局部描述符表(LGDT位于内存中,需要用专门的寄存器指向它后,CPU才知道它在哪里。这个专门的寄存器便是GDTR,即GDT Register,专门用来存储GDT的内存地址及大小。GDTR是个48位的寄存器,结构如下:

字段名称大小描述
基地址32 bitsGDT的起始地址。
限长16 bitsGDT的大小(减1,以字节为单位)。

由于GDT的大小是16位二进制,其表示的范围是2的16次方等于65536字节。每个描述符大小是8字节,故GDT中最多可容纳的描述符数量是65536/8=8192个,即GDT中可容纳8192个段或门。对此寄存器的访问,不能够用mov gdtr, xxx 这样的指令为GDTR初始化,有专门的指令来做这件事,这就是lgdt指令。lgdt的指令格式是:lgdt 48位内存数据。

2.4 进入保护模式

进入保护模式的三个步骤: 

三、实模式下获取物理内存容量

BIOS中断0x15的子功能0xE820能够获取系统的内存布局,由于系统内存各部分的类型属性不同,BIOS就按照类型属性来划分这片系统内存,所以这种查询呈迭代式,每次BIOS只返回一种类型的内存信息,直到将所有内存类型返回完毕。子功能0xE820的强大之处是返回的内存信息较丰富,包括多个属性字段,所以需要一种格式结构来组织这些数据。内存信息的内容是用地址范围描述符来描述的,用于存储这种描述符的结构称之为地址范围描述符(Address Range Descriptor Structure,ARDS),格式见表5-1。

此结构中的字段大小都是4字节,共5个字段,所以此结构大小为20字节。每次int 0x15之后,BIOS就返回这样一个结构的数据。注意,ARDS结构中用64位宽度的属性来描述这段内存基地址(起始地址)及其长度,所以表中的基地址和长度都分为低32位和高32位两部分。其中的Type字段用来描述这段内存的类型,这里所谓的类型是说明这段内存的用途,即其是可以被操作系统使用,还是保留起来不能用。Type字段的具体意义见表5-2。

 BIOS中断只是一段函数例程,调用它就要为其提供参数,现在介绍下BIOS中断0x15的0xe820子功能需要哪些参数。表5-3所示是使用此中断的方法,分输入和输出两部分。

INT 0x15的子功能0xE820使用步骤:

  1. 初始化寄存器: 将EAX设置为0xE820,EDX设置为SMAP,EBX设置为0,ECX设置为20,ES设置为指向缓冲区的指针。
  2. 调用中断: 执行INT 0x15中断。
  3. 检查结果: 检查EAX是否返回SMAP以及CF是否清除。如果CF置位,表示调用失败。
  4. 处理返回的数据: 从ES指向的缓冲区读取内存映射表条目,并根据Type字段处理。
  5. 继续调用: 将EBX设置为上一次调用返回的EBX值,再次调用中断,直到EBX返回0。

四、保护模式下使用LBA读取磁盘 

进入保护模式后,无法使用实模式下的BIOS中断的磁盘读取服务。由于读取的磁盘数据会放在1MB以上的内存区域,所以也不便于在进入保护模式前使用BIOS的磁盘读取服务来读取。在保护模式下,通常通过直接与硬件通信或者使用操作系统提供的接口来读取磁盘,loader要加载操作系统内核,就必须要和磁盘打交道。打交道的方式很简单,就是通过in 与out指令与磁盘暴露在外的寄存器交互。下图是in与out指令的用法:

让硬盘工作,我们需要通过读写硬盘控制器的端口,端口就是位于I0控制器上的寄存器,此处的端口是指硬盘控制器上的寄存器。 下面列出了部分端口:

data寄存器:相当于数据的门,数据能进,也能出,所以其作用是读取或写入数据。数据的读写还是越快越好,所以此寄存器较其他寄存器宽一些,16位(表中其他寄存器都是8位的)。在读硬盘时,硬盘准备好的数据后,硬盘控制器将其放在内部的缓冲区中,不断读此寄存器便是读出缓冲区中的全部数据。在写硬盘时,我们要把数据源源不断地输送到此端口,数据便被存入缓冲区里,硬盘控制器发现这个缓冲区中有数据了,便将此处的数据写入相应的扇区中。

Sector count寄存器:用来指定待读取或待写入的扇区数。硬盘每完成一个扇区,就会将此寄存器的值减1,所以如果中间失败了,此寄存器中的值便是尚未完成的扇区。 

LBA寄存器:硬盘中的扇区在物理上是用”柱面-磁头-扇区“来定位的(Cylinder Head Sector),简称为CHS,但每次我们要事先算出扇区是在哪个盘面,咱们希望有一套对人来说较直观的寻址方法,即磁盘中扇区从0开始依次递增编号,不用考虑扇区所在的物理结构(LBA)。LBA是一种逻辑上为扇区编址的方法,全称为逻辑块地址(Logical Block Address)。 LBA有两种,一种是LBA28,用28位比特来描述一个扇区的地址。最大寻址范围是2的28次方等于268435456个扇区,每个扇区是512字节,最大支持128GB。LBA48用48位比特来描述一个扇区的地址,最大可寻址范围是2的48次方,等于281474976710656个扇区,乘以512字节后,最大支持131072TB,即128PB。

Status&Command在读硬盘时,端口0x1F7或0x177的寄存器名称是Status,它是8位宽度的寄存器,用来给出硬盘的状态信息。在写硬盘时,端口0x1F7或0x177的寄存Command,此寄存器用来存储让硬盘执行的命令,只要把命令写进此寄存器,硬盘就开始工作了。在咱们的系统中,主要使用了三个命令:0xEC(硬盘识别),0x20(读扇区),0x30(即写扇区)。

虽然操作磁盘很复杂,但都是有章可循的,按照如下步骤操作磁盘即可: 

 五、启用内存分页机制

1、一级页表 

分页机制的核心是将程序所要用到的虚拟内存空间,拆分成一个个固定大小的小块(虚拟的,4K),与物理内存上的固定大小的小块建立映射关系。就是这样的一种分离,带来了内存管理的粒度更小(原有的段机制下段可能远比4K大得多)、粒度更标准(段大小不一,现在统一成了4K大小)、更灵活(将程序暂时用不到的小块不放入内存中,等用到了再从磁盘中调入)等优点。分页机制打开前要将页表地址加载到控制寄存器cr3中,这是启用分页机制的先决条件之一。32位x86页表项结构如下:

位数范围字段名称大小描述
0P1 bitPresent 位,页是否在物理内存中,1表示存在,0表示不存在。
1R/W1 bitRead/Write 位,0表示只读,1表示读写。
2U/S1 bitUser/Supervisor 位,0表示内核模式,1表示用户模式。
3PWT1 bitPage Write Through 位,控制页的写策略。
4PCD1 bitPage Cache Disable 位,控制页的缓存策略。
5A1 bitAccessed 位,表示该页是否被访问过。
6D1 bitDirty 位,表示该页是否被写入过。
7PAT1 bitPage Attribute Table 位,控制页的缓存属性。
8G1 bitGlobal 位,全局页标志,1表示全局页,不会在上下文切换时刷新TLB。
9-11AVL3 bits可供操作系统软件使用的位。
12-31页框地址20 bits物理页框地址的高20位

CR4寄存器中的第4位(位4),即PSE(Page Size Extension)位,用于控制页的大小。当PSE位为0:页大小为4KB(传统的分页方式)。当PSE位为1:支持4MB的大页(通常用于内核或大块连续内存的映射)。在32位操作系统中,CR3寄存器中的页目录表物理地址只需要20位,是因为页目录表的地址必须是对齐到4KB(4096字节)的边界。这种对齐要求使得物理地址的低12位总是为0,因此可以省略,只需存储高20位。

线性地址转换物理地址过程:

  1. 一个页表项对应一个页,所以,用线性地址的高20位作为页表项的索引,每个页表项要占用4字节大小,所以这高20位的索引乘以4后才是该页表项相对于页表物理地址的字节偏移量。
  2. 用cr3寄存器中的页表物理地址加上此偏移量便是该页表项的物理地址,从该页表项中得到映射的物理页地址。
  3. 然后用线性地址的低12位与该物理页地址相加,所得的地址之和便是最终要访问的物理地址。

2、二级页表 

二级页表线性地址转换物理地址过程如下:

  1. 用虚拟地址的高10位乘以4,作为页目录表内的偏移地址,加上页目录表的物理地址,所得的和便是页目录项的物理地址。读取该页目录项,从中获取到页表的物理地址。
  2. 用虚拟地址的中间10位乘以4,作为页表内的偏移地址,加上在第1步中得到的页表物理地址,所得的和,便是页表项的物理地址。读取该页表项,从中获取到分配的物理页地址。
  3. 虚拟地址的高10位和中间10位分别是PDE(页目录项)和PTE(页表项)的索引值,所以它们需要乘以4。但低12位就不是索引值啦,其表示的范围是0-0xFFF,作为页内偏移最合适,所以虚拟地址的低12位加上第2步中得到的物理页地址,所得的和便是最终转换的物理地址。

 3、启用页表机制

启用分页机制,我们要按顺序做好三件事:

  1. 准备好页目录表及页表。
  2. 将页表地址写入控制寄存器cr3。
  3. 寄存器cr0的PG位置1。

为清晰起见,给出控制寄存器CR0的各位:

  1. 位数范围字段名称描述
    0PE保护模式使能,1表示启用保护模式。
    1MP协处理器监控,与EM位一起用于控制协处理器。
    2EM协处理器仿真,1表示禁止FPU操作,所有FPU指令会产生异常。
    3TS任务切换标志,用于处理器在任务切换时的协处理器状态管理。
    4ET协处理器类型,指示处理器的FPU类型。
    5NEFPU错误处理使能,1表示处理器使用内建的错误处理机制。
    16WP写保护控制,1表示在内核模式下启用对用户模式页的写保护。
    18AM对齐检查控制,1表示启用对齐检查(需要与EFLAGS中的AC位配合使用)。
    29NW禁用写通缓存,1表示禁用写通缓存。
    30CD缓存禁止,1表示禁用缓存。
    31PG分页使能,1表示启用分页机制。

六、ELF文件

 windows的可执行文件格式通常为.exe结尾的PE 文件格式,而linux/unix用得更多的则为elf文件格式。我们的工具链编译生成的即为这种文件格式。此种文件格式结构简要图如下。对我们而言,只需要关注下图右半部分(即Execution View,左半部分是给链接器用的)。

ELF文件中包含了.text/.rodata/.data/.bss各段的信息。通常解析该文件,找到程序头表,从该表中即可从中读取出相应的代码、数据段的相关信息,并可根据该信息将代码或数据加载到对应的内存中,完成整个加载过程。 

ELF 头结构

字段名称大小描述
e_ident16 bytes魔数和其他信息,标识文件是ELF文件,并提供文件的基本属性(如字节序等)。
e_type2 bytes文件类型(如可执行文件、重定位文件、共享对象等)。
e_machine2 bytes目标机器架构(如x86, ARM等)。
e_version4 bytesELF 文件版本,通常为1。
e_entry4 bytes程序入口地址,指示程序开始执行的位置。
e_phoff4 bytes程序头表在文件中的偏移。
e_shoff4 bytes节头表在文件中的偏移。
e_flags4 bytes处理器特定标志。
e_ehsize2 bytesELF 头的大小。
e_phentsize2 bytes用来指明程序头表中每个条目的字节大小。
e_phnum2 bytes用来指明程序头表中条目的数量,实际上就是段的个数。
e_shentsize2 bytes用来指明节头表中每个条目的字节大小。
e_shnum2 bytes用来指明节头表中条目的数量,实际上就是节的个数
e_shstrndx2 bytes节头字符串表索引。

程序头结构

字段名称大小描述
p_type4 bytes段类型(如LOAD, DYNAMIC等)。
p_offset4 bytes段在文件中的偏移。
p_vaddr4 bytes段在内存中的虚拟地址。
p_paddr4 bytes段在内存中的物理地址,通常用于不可分页设备。
p_filesz4 bytes段在文件中的大小。
p_memsz4 bytes段在内存中的大小。
p_flags4 bytes段标志(如可读、可写、可执行)。
p_align4 bytes段在内存中的对齐要求。

节头结构

字段名称大小描述
sh_name4 bytes节名称字符串表索引。
sh_type4 bytes节类型(如PROGBITS, SYMTAB等)。
sh_flags4 bytes节标志(如可写、可分配等)。
sh_addr4 bytes节在内存中的虚拟地址。
sh_offset4 bytes节在文件中的偏移。
sh_size4 bytes节的大小。
sh_link4 bytes链接到其他节的索引。
sh_info4 bytes额外的节信息。
sh_addralign4 bytes节的对齐要求。
sh_entsize4 bytes如果节包含表,则每个表项的大小。

常见的节类型

节名称描述
.text包含程序代码。
.data包含已初始化的全局变量。
.bss包含未初始化的全局变量。
.rodata包含只读数据(如字符串常量)。
.symtab包含符号表。
.strtab包含字符串表。
.rel.text包含对 .text 节的重定位信息。
.rel.data包含对 .data 节的重定位信息。

七、函数调用约定


八、内联汇编

在GCC中,内联汇编使用 asm__asm__ 关键字,基本语法如下:

asm [volatile] ("assembly code" : output : input : clobber/modify);

assembly code:汇编代码,可以是多条指令。output: 输出操作数列表。input:输入操作数列表。clobber/modify:汇编代码执行后会破坏一些内存或寄存器资源,通过此项通知编译器,可能造成寄存器或内存数据的破坏,这样gcc就知道哪些寄存器或内存需要提前保护起来。

 1、内联汇编之寄存器约束:

2、内联汇编之名称占位符 :

名称占位符与序号占位符不同,序号占位符靠本身出现在output和input中的位置就能被编译器辨识出来。而名称占位序需要在output和input中把操作数显式地起个名字,它用这样的格式来标识操作数:[名称]”约束名”(C变量)这样,该约束对应的汇编操作数便有了名字,在assemblycode中引用操作数时,采用%[名称]的形式就可以了。

#include <stdio.h>

int main() {
    int a = 10, b = 20, c;

    // 使用内联汇编计算 c = a + b
    __asm__ (
        "add %[result], %[value1], %[value2]" // 汇编代码
        : [result] "=r" (c)                   // 输出操作数
        : [value1] "r" (a), [value2] "r" (b)  // 输入操作数
    );

    printf("Result: %d\n", c);
    return 0;
}

 解释:

  • "add %[result], %[value1], %[value2]":汇编代码执行加法操作,将a和b的值相加,并将结果存储在c中。%[result]、%[value1]和%[value2]是占位符,用于引用在操作数列表中指定的C变量。
  • [result] "=r" (c):输出操作数部分,[result]是占位符名称,"=r"表示输出到一个通用寄存器,(c)表示将输出结果存储到C变量c中。
  • [value1] "r" (a), [value2] "r" (b):输入操作数部分,[value1]和[value2]是占位符名称,"r"表示从一个通用寄存器读取,(a)和(b)表示输入的C变量a和b。

 九、代码

1、boot部分

boot/start.S

/**
 * 自己动手写操作系统
 *
 * 系统引导部分,启动时由硬件加载运行,然后完成对二级引导程序loader的加载
 * 该部分程序存储于磁盘的第1个扇区,在计算机启动时将会由BIOS加载到0x7c00处
 * 之后,将由BIOS跳转至0x7c00处开始运行
 * 
 * 作者:李述铜
 * 联系邮箱: 527676163@qq.com
 */
	#include "boot.h"

  	// 16位代码,务必加上
  	.code16
 	.text
	.global _start
	.extern boot_entry
_start:
	// 重置数据段寄存器
	mov $0, %ax
	mov %ax, %ds
	mov %ax, %ss
	mov %ax, %es
	mov %ax, %fs
	mov %ax, %gs

	// 根据https://wiki.osdev.org/Memory_Map_(x86)
	// 使用0x7c00之前的空间作栈,大约有30KB的RAM,足够boot和loader使用
	mov $_start, %esp

	// 通过BIOS int 0x10中断,显示boot加载完成提示
	mov $0xe, %ah
	mov $'L', %al
	int $0x10

	// 通过BIOS int 0x13中断,加载位于硬盘2扇区中的loader到内存的0x8000处,只支持磁盘1
	// https://wiki.osdev.org/Disk_access_using_the_BIOS_(INT_13h)
read_loader:
	mov $0x8000, %bx	// 读取到的内存地址
	mov $0x2, %cx		// ch:磁道号,cl起始扇区号
	mov $0x2, %ah		// ah: 0x2读磁盘命令
	mov $64, %al		// al: 读取的扇区数量, 必须小于128,暂设置成64B*512B=32KB
	mov $0x0080, %dx	// dh: 磁头号,dl驱动器号0x80(磁盘1)
	int $0x13           // 出口参数:CF=0 操作成功,AH=00H,AL=传输的扇区数
	jc read_loader

	// 跳转至c部分执行,再由c部分做一些处理
	jmp boot_entry

	// 原地跳转
	jmp .

	// 引导结束段
	.section boot_end, "ax"
boot_sig: .byte 0x55, 0xaa

 boot/boot.c

__asm__(".code16gcc");

#include "boot.h"

#define	LOADER_START_ADDR	0x8000		// loader加载的地址

/**
 * Boot的C入口函数
 * 只完成一项功能,即从磁盘找到loader文件然后加载到内容中,并跳转过去
 */
void boot_entry(void) { 
	//将LOADER_START_ADDR强转为无参无返回值的函数指针,
	//然后通过这个函数指针调用代码,即跳转到loader中
	((void (*)(void))LOADER_START_ADDR)();
} 

在Linux下用于链接的程序是ld,链接有一个好处,可以用-Ttext参数指定最终生成的可执行文件的起始虚拟地址,链接部分参数如下:

-m elf_i386  -Ttext=0x7c00  --section-start boot_end=0x7dfe

2、loader部分

loader/start.S 

/**
 * 二级引导,负责进行硬件检测,进入保护模式,然后加载内核,并跳转至内核运行
 */
  	// 16位代码,务必加上
  	.code16
 	.text
 	.extern loader_entry
	.global _start
_start:
	// 栈和段等沿用之前的设置,也可以重新设置
	// 这里简单起见,就不做任何设置了
	// 你可能会想,直接跳到loader_entry,但这样需要先知道loader_entry在哪儿
	// boot没有这个能力做到,所以直接用汇编,以便_start处于整个bin文件开头,这样boot直接跳到开头就可以
    //进入loader_16中的loader_entry
	jmp loader_entry

	// 32位保护模式下的代码
	.code32
	.text
	.global protect_mode_entry
	.extern load_kernel
protect_mode_entry:
	// 重新加载所有的数据段描述符
	mov $16, %ax		// 16为数据段选择子
	mov %ax, %ds
    mov %ax, %ss
    mov %ax, %es
    mov %ax, %fs
    mov %ax, %gs

    // 长跳转进入到32位内核加载模式中,loader_32中的load_kernel
    jmp $8, $load_kernel

loader/loader_16.c

// 16位代码,必须加上放在开头,以便有些io指令生成为32位
__asm__(".code16gcc");

#include "loader.h"

boot_info_t boot_info;			 // 启动参数信息,boot_info.h中进行了相关定义
/***************************************
#define BOOT_RAM_REGION_MAX   10 // RAM区最大数量
typedef struct _boot_info_t {
    // RAM区信息
    struct {
        uint32_t start;
        uint32_t size;
    }ram_region_cfg[BOOT_RAM_REGION_MAX];
    int ram_region_count;
}boot_info_t;
****************************************/


//实模式下使用BIOS int 0x10中断写显存,显示字符串
static void show_msg (const char * msg) {
    char c;
	while ((c = *msg++) != '\0') {
		__asm__ __volatile__(
				"mov $0xe, %%ah\n\t"
				"mov %[ch], %%al\n\t"
				"int $0x10"::[ch]"r"(c));
	}
}

//调用BIOS中断0x15子功能0xe820获取内存总容量
static void  detect_memory(void) {
	uint32_t contID = 0;
	SMAP_entry_t smap_entry;
	int signature, bytes;

    show_msg("try to detect memory:");

	// 初次:EDX=0x534D4150,EAX=0xE820,ECX=24,INT 0x15, EBX=0(初次)
	// 后续:EAX=0xE820,ECX=24,
	// 结束判断:EBX=0
	boot_info.ram_region_count = 0;
	for (int i = 0; i < BOOT_RAM_REGION_MAX; i++) {
		SMAP_entry_t * entry = &smap_entry;

		__asm__ __volatile__("int  $0x15"
			: "=a"(signature), "=c"(bytes), "=b"(contID)
			: "a"(0xE820), "b"(contID), "c"(24), "d"(0x534D4150), "D"(entry));
		if (signature != 0x534D4150) {
            show_msg("failed.\r\n");
			return;
		}

		if (bytes > 20 && (entry->ACPI & 0x0001) == 0){
			continue;
		}

        // 保存RAM信息,只取32位,空间有限无需考虑更大容量的情况
        if (entry->Type == 1) {
            boot_info.ram_region_cfg[boot_info.ram_region_count].start = entry->BaseL;
            boot_info.ram_region_cfg[boot_info.ram_region_count].size = entry->LengthL;
            boot_info.ram_region_count++;
        }

		if (contID == 0) {
			break;
		} 
	}
    show_msg("ok.\r\n");
}

// GDT表 临时用,后面内容会替换成自己的
uint16_t gdt_table[][4] = {
    {0, 0, 0, 0},
    {0xFFFF, 0x0000, 0x9A00, 0x00CF},
    {0xFFFF, 0x0000, 0x9200, 0x00CF},
};

/**
 * 进入保护模式
 */
static void  enter_protect_mode() {
    // 关中断
    cli();
    //进入保护模式需要三个步骤:(1)打开A20,(2)加载GDT,(3)将CR0的PE位置1

    // 开启A20地址线,使得可访问1M以上空间
    // 使用的是Fast A20 Gate方式,见https://wiki.osdev.org/A20#Fast_A20_Gate
    uint8_t v = inb(0x92);
    outb(0x92, v | 0x2);

    // 加载GDT。由于中断已经关掉,IDT不需要加载
    lgdt((uint32_t)gdt_table, sizeof(gdt_table));

    // 打开CR0的保护模式位,进入保持模式
    uint32_t cr0 = read_cr0();
    write_cr0(cr0 | (1 << 0));


    // 长跳转进入到保护模式,start.s中的protect_mode_entry
    // 使用长跳转,以便清空流水线,将里面的16位代码给清空
    far_jump(8, (uint32_t)protect_mode_entry);
}

void loader_entry(void) {
    show_msg("....loading.....\r\n");
	detect_memory();
    enter_protect_mode();
}

loader/loader_32.c 

/*
 * 32位引导代码
 */
#include "loader.h"
#include "comm/elf.h"

// 保护模式下使用LBA48位模式读取磁盘
// 主要用于将内核的elf文件的读取到内存
static void read_disk(int sector, int sector_count, uint8_t * buf) {
    outb(0x1F6, (uint8_t) (0xE0));

	outb(0x1F2, (uint8_t) (sector_count >> 8));
    outb(0x1F3, (uint8_t) (sector >> 24));		// LBA参数的24~31位
    outb(0x1F4, (uint8_t) (0));					// LBA参数的32~39位
    outb(0x1F5, (uint8_t) (0));					// LBA参数的40~47位

    outb(0x1F2, (uint8_t) (sector_count));
	outb(0x1F3, (uint8_t) (sector));			// LBA参数的0~7位
	outb(0x1F4, (uint8_t) (sector >> 8));		// LBA参数的8~15位
	outb(0x1F5, (uint8_t) (sector >> 16));		// LBA参数的16~23位

	outb(0x1F7, (uint8_t) 0x24);

	// 读取数据
	uint16_t *data_buf = (uint16_t*) buf;
	while (sector_count-- > 0) {
		// 每次扇区读之前都要检查,等待数据就绪
		while ((inb(0x1F7) & 0x88) != 0x8) {}

		// 读取并将数据写入到缓存中,SECTOR_SIZE为512表示一个扇区的大小
		//由于一次读取16位为两字节,故一次for循环读取一个扇区
		for (int i = 0; i < SECTOR_SIZE / 2; i++) {
			*data_buf++ = inw(0x1F0);
		}
	}
}

//解析内核elf文件,提取内容到相应的内存中
static uint32_t reload_elf_file (uint8_t * file_buffer) {
    // 读取的只是ELF文件,不像BIN那样可直接运行,需要从中加载出有效数据和代码
    // 通过ELF文件魔数,简单判断是否是合法的ELF文件,其中ELF_MAGIC为 0x7F
    Elf32_Ehdr * elf_hdr = (Elf32_Ehdr *)file_buffer;
    if ((elf_hdr->e_ident[0] != ELF_MAGIC) || (elf_hdr->e_ident[1] != 'E')
        || (elf_hdr->e_ident[2] != 'L') || (elf_hdr->e_ident[3] != 'F')) {
        return 0;
    }

    // 然后从中加载程序头,将内容拷贝到相应的位置
    for (int i = 0; i < elf_hdr->e_phnum; i++) {
		// e_phoff表示程序头表在文件中的偏移,程序头表是个数组,表项是段
		//逐段解析,然后写入内存
        Elf32_Phdr * phdr = (Elf32_Phdr *)(file_buffer + elf_hdr->e_phoff) + i;
        if (phdr->p_type != PT_LOAD) { //PT_LOAD被定义为1,表示可加载类型
            continue;
        }

		// 全部使用物理地址,此时分页机制还未打开
        uint8_t * src = file_buffer + phdr->p_offset; //p_offset表示段在文件中的偏移
        uint8_t * dest = (uint8_t *)phdr->p_paddr; //p_paddr表示段在内存中的物理地址(4 bytes)
        for (int j = 0; j < phdr->p_filesz; j++) { //p_filesz表示段在文件中的大小(4 bytes)
            *dest++ = *src++; //将内核写入内存
        }

		//如果memsz大于filesz时,后续要填0
		dest= (uint8_t *)phdr->p_paddr + phdr->p_filesz;
		for (int j = 0; j < phdr->p_memsz - phdr->p_filesz; j++) {
			*dest++ = 0; //p_memsz表示段在内存中的大小(4 bytes)
		}
    }
	// e_entry表示程序入口地址,指示程序开始执行的位置
    return elf_hdr->e_entry;
}

/**
 * 开启分页机制
 * 将0-4M空间映射到0-4M和SYS_KERNEL_BASE_ADDR~+4MB空间
 * 0-4MB的映射主要用于保护loader自己还能正常工作
 * SYS_KERNEL_BASE_ADDR+4MB则用于为内核提供正确的虚拟地址空间
 */
void enable_page_mode (void) {
#define PDE_P			(1 << 0)
#define PDE_PS			(1 << 7)
#define PDE_W			(1 << 1)
#define CR4_PSE		    (1 << 4)
#define CR0_PG		    (1 << 31)

    // 使用4MB页块,这样构造页表就简单很多,只需要1个表即可。
    // 以下表为临时使用,用于帮助内核正常运行,在内核运行起来之后,将重新设置
    static uint32_t page_dir[1024] __attribute__((aligned(4096))) = {
        [0] = PDE_P | PDE_PS | PDE_W,			// PDE_PS,开启4MB的页
    };

    // 设置PSE,以便启用4M的页,而不是4KB
    uint32_t cr4 = read_cr4();
    write_cr4(cr4 | CR4_PSE);

    // 设置页表地址
    write_cr3((uint32_t)page_dir);

    // 开启分页机制
    write_cr0(read_cr0() | CR0_PG);
}

// 从磁盘上加载内核
void load_kernel(void) {
    // 读取的扇区数一定要大一些,保不准kernel.elf大小会变得很大
	//从第100个扇区开始读,即内核的elf文件事先被写到了磁盘的第100个扇区处
	//SYS_KERNEL_LOAD_ADDR被定于为 1024*1024,即将内核的elf文件加载到内存的1M处
    read_disk(100, 500, (uint8_t *)SYS_KERNEL_LOAD_ADDR);

     // 解析ELF文件,并通过调用的方式,进入到内核中去执行,同时传递boot参数
	 // 临时将elf文件先读到SYS_KERNEL_LOAD_ADDR处,再进行解析
    uint32_t kernel_entry = reload_elf_file((uint8_t *)SYS_KERNEL_LOAD_ADDR);
	if (kernel_entry == 0) {
		while(1); //死机
	}
	// 开启分页机制
	enable_page_mode();
	//将kernel_entry强转为带参(boot_info_t *)无返回值的函数指针,
	//然后通过这个函数指针调用代码,即跳转到kernel_entry中,同时boot_info也传递到了内核程序中
    ((void (*)(boot_info_t *))kernel_entry)(&boot_info);
}

 3、内核部分

kernel\init\start.S 

 	.text
 	.global _start
	.extern kernel_init
_start:
	# 将loader_32传递过来的数据&boot_info重新压入栈中,传递给kernel_init
    # 第一种方法(需要返回的函数调用一般这么做)
    # push %ebp
    # mov %esp, %ebp
    # mov 0x8(%ebp), %eax
    # push %eax

    # 第二种方法,由于不需要返回,故不需要将ebp再压入栈中备份
    # mov 4(%esp), %eax
    # push %eax

    # 第三种方法
    push 4(%esp)

    # kernel_init(boot_info)
    call kernel_init

	jmp .

 4、相关的头文件

comm\elf.h 

/*
 * ELF相关头文件及配置
 */
#ifndef OS_ELF_H
#define OS_ELF_H

#include "types.h"

// ELF相关数据类型
typedef uint32_t Elf32_Addr;
typedef uint16_t Elf32_Half;
typedef uint32_t Elf32_Off;
typedef uint32_t Elf32_Sword;
typedef uint32_t Elf32_Word;

#pragma pack(1)

// ELF Header
#define EI_NIDENT       16
#define ELF_MAGIC       0x7F

#define ET_EXEC         2   // 可执行文件
#define ET_386          3   // 80386处理器

#define PT_LOAD         1   // 可加载类型

//ELF文件头结构
typedef struct {
    char e_ident[EI_NIDENT];
    Elf32_Half e_type;
    Elf32_Half e_machine;
    Elf32_Word e_version;
    Elf32_Addr e_entry;
    Elf32_Off e_phoff;
    Elf32_Off e_shoff;
    Elf32_Word e_flags;
    Elf32_Half e_ehsize;
    Elf32_Half e_phentsize;
    Elf32_Half e_phnum;
    Elf32_Half e_shentsize;
    Elf32_Half e_shnum;
    Elf32_Half e_shstrndx;
}Elf32_Ehdr;

#define PT_LOAD         1

//ELF程序头结构
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;

#pragma pack()

#endif //OS_ELF_H

comm\cpu_instr.h 

/**
 * 汇编指令的封装
 *
 * 作者:李述铜
 * 联系邮箱: 527676163@qq.com
 */
#ifndef CPU_INSTR_H
#define CPU_INSTR_H

#include "types.h"

// 从指定的端口读取一个字节
static inline uint8_t inb(uint16_t  port) {
	uint8_t rv;
	__asm__ __volatile__("inb %[p], %[v]" : [v]"=a" (rv) : [p]"d"(port));
	return rv;
}

//从指定的端口读取一个字
static inline uint16_t inw(uint16_t  port) {
	uint16_t rv;
	__asm__ __volatile__("in %1, %0" : "=a" (rv) : "dN" (port));
	return rv;
}

// 向指定的端口写入一个字节
static inline void outb(uint16_t port, uint8_t data) {
	__asm__ __volatile__("outb %[v], %[p]" : : [p]"d" (port), [v]"a" (data));
}

// 向指定的端口写入一个字
static inline void outw(uint16_t port, uint16_t data) {
	__asm__ __volatile__("out %[v], %[p]" : : [p]"d" (port), [v]"a" (data));
}

// 禁用中断
static inline void cli() {
	__asm__ __volatile__("cli");
}

// 启用中断
static inline void sti() {
	__asm__ __volatile__("sti");
}

/**
 * 加载全局描述符表
 *
 * @param start GDT的起始地址
 * @param size GDT的大小
 */
static inline void lgdt(uint32_t start, uint32_t size) {
	struct {
		uint16_t limit;
		uint16_t start15_0;
		uint16_t start31_16;
	} gdt;

	gdt.start31_16 = start >> 16;
	gdt.start15_0 = start & 0xFFFF;
	gdt.limit = size - 1;

	__asm__ __volatile__("lgdt %[g]"::[g]"m"(gdt));
}

// 读取CR0寄存器的值
static inline uint32_t read_cr0() {
	uint32_t cr0;
	__asm__ __volatile__("mov %%cr0, %[v]":[v]"=r"(cr0));
	return cr0;
}

//写入值到CR0寄存器
static inline void write_cr0(uint32_t v) {
	__asm__ __volatile__("mov %[v], %%cr0"::[v]"r"(v));
}

static inline uint32_t read_cr2() {
	uint32_t cr2;
	__asm__ __volatile__("mov %%cr2, %[v]":[v]"=r"(cr2));
	return cr2;
}

static inline void write_cr3(uint32_t v) {
    __asm__ __volatile__("mov %[v], %%cr3"::[v]"r"(v));
}

static inline uint32_t read_cr3() {
    uint32_t cr3;
    __asm__ __volatile__("mov %%cr3, %[v]":[v]"=r"(cr3));
    return cr3;
}

static inline uint32_t read_cr4() {
    uint32_t cr4;
    __asm__ __volatile__("mov %%cr4, %[v]":[v]"=r"(cr4));
    return cr4;
}

static inline void write_cr4(uint32_t v) {
    __asm__ __volatile__("mov %[v], %%cr4"::[v]"r"(v));
}

/**
 * 进行远跳转
 *
 * @param selector 选择子
 * @param offset 偏移量
 */
static inline void far_jump(uint32_t selector, uint32_t offset) {
	uint32_t addr[] = {offset, selector };
	__asm__ __volatile__("ljmpl *(%[a])"::[a]"r"(addr));
}

/**
 * 加载中断描述符表
 *
 * @param start IDT的起始地址
 * @param size IDT的大小
 */
static inline void lidt(uint32_t start, uint32_t size) {
	struct {
		uint16_t limit;
		uint16_t start15_0;
		uint16_t start31_16;
	} idt;

	idt.start31_16 = start >> 16;
	idt.start15_0 = start & 0xFFFF;
	idt.limit = size - 1;

	__asm__ __volatile__("lidt %0"::"m"(idt));
}

// 处理器暂停执行,进入低功耗状态
static inline void hlt(void) {
    __asm__ __volatile__("hlt");
}

/**
 * 加载任务寄存器
 *
 * @param tss_selector TSS选择子
 */
static inline void write_tr (uint32_t tss_selector) {
    __asm__ __volatile__("ltr %%ax"::"a"(tss_selector));
}

/**
 * 读取EFLAGS寄存器的值
 *
 * @return EFLAGS寄存器的值
 */
static inline uint32_t read_eflags (void) {
    uint32_t eflags;

    __asm__ __volatile__("pushfl\n\tpopl %%eax":"=a"(eflags));
    return eflags;
}

/**
 * 写入值到EFLAGS寄存器
 *
 * @param eflags 要写入的值
 */
static inline void write_eflags (uint32_t eflags) {
    __asm__ __volatile__("pushl %%eax\n\tpopfl"::"a"(eflags));
}

#endif

  • 10
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值