第一到七章流程整理

本文最后更新于:1 年前

写在前面

这一章节我将把从第一章到第七章的内容串联起来,缕清思路。

正文

第一章

第二章

在电脑未开机前,BIOS就被事先写入到内存的0xF0000~0xFFFFF中。此区域为ROM。MBR被加载到磁盘的0盘0道1扇区(CHS方式),CHS 方式中扇区的编号是从1开始的。

在计算机启动之后,cs:ip寄存器会被强制置为0xF000:0xFFF0,由于在实模式下的段基址要乘以16,也就是左移四位,所以0xF000:0xFFF0的等效地址是0xFFFF0,这就是BIOS的入口地址。

0xFFFF0处的指令为跳转指令jmp far f000:e05b,跳转到BIOS真正的地址,进行检测内存、显卡等外设信息,当检测通过,并初始化好硬件后,开始在内存中 0x000~0x3FF处建立数据结构,中断向量表 IVT 并填写中断例程。

BIOS 最后一项工作校验启动盘中位于0盘0道1扇区的内容。如果此扇区末尾的两个字节分别是魔数0x55和0xaa,就会把该扇区内容加载到内存地址 0x7c00处,然后执行跳转指令jmp 0: 0x7c00,这里正是MBR的起始地址。

第三章

这一章主要是改进MBR,第一步是使其能直接访问显存,从而在显示器上打印字段。

第二步是让MBR直接操作硬盘,将硬盘中0盘0道2扇区的内容写入内存,然后跳转执行loader。这一步略微复杂,MBR需要执行以下步骤:

第1步将LBA地址、扇区数放入相应I/O端口,再将读命令0x20放入Command端口,硬盘控制器就会自动将硬盘的数据读入Data端口,再存入硬盘控制器的缓冲区。

第2步就是利用查询传输方式,这段代码一直循环,也就是一直占用cpu查询Status端口,直到规定好的硬盘数据全部读入缓冲区,Status第3位被置为1,程序才会继续执行第3步。

第3步就是将缓冲区数据以2个字节为单位循环先读入Data端口,再用in指令读入内存。

第四章

到这里我们就要从实模式进入保护模式了,首先修改boot.inc和loader.S,在内存里写好3个段描述符(代码段、数据段、显存段)、赋值好GDTR寄存器、创建好表示3个选择子的字段后,进入保护模式。

boot.inc

根据高32位的段描述符,依次定义字段最后相加,合成了代码段、数据段、显存段三个段的段描述符的高32位的字段。方便loader.S引用这些字段来构建代码段、数据段、显存段描述符。

loader.S

1、通过给出低4字节加上引用boot.inc定义好的高4字节字段来向内存写入无用的第0段和3个有用段的段描述符(代码段、数据段、显存段)。并将他们的内存地址依次标记为GDT_BASE、CODE_DESC、DATA_STACK_DESC、VIDEO_DESC。
2、GDT_BASE因此为GDT的内存起始地址,可以用来创建GDTR。根据上述标号可以计算出3个段描述符各自的索引,从而可以创建各个段的选择子。
3、内存里写好3个段描述符、赋值好GDTR寄存器、创建好表示3个选择子的字段后,在实模式下利用BIOS中断13号中断打印字符串“2 loader in real”,再执行“3步走”代码打开保护模式,然后用定义好的选择子字段初始化段寄存器,当进入保护模式后打印“P”。
4、数据段和代码段段描述符段基址均为0,段界限均为4GB,即平坦模式:段偏移寄存器32位访问范围4GB,不可能换段。ss和ds寄存器都为数据段选择子。
5、显存段段基址为0xb8000,用于访问显存,gs为显存段选择子。

第五章

这一章内容较多,我们要实现内容容量检测的函数、启用分页、加载内核。

内存检测

使用BIOS中断0x15的3个子功能0xe820、0xe801、0x88,可以获得内存容量。

1、0xe820 获得os可用内存的基址和内存长度(单位字节)所有ards结构的低32基址+低32为内存长度的最大值即为内存容量;
2、0xe801获得低15MB(单位1KB)+1MB +16MB(单位64KB)以上内存容量 AX * 1024字节+1MB+BX * 1024*64字节;
3、0xe88 获得1MB以上内存容量AX * 1024字节+1MB

loader.S在进入保护模式前测出内存容量,保存于内存0xb00处。

启用分页

对于本书实验而言,分页属性如下,

虚拟地址:32位=4GB
物理地址:32MB (但是分页机制下能访问的实地址只有低1MB)32MB是内存容量的检测结果。
一块物理页大小:4kB
一个页表大小:4kB
一个页表项大小:4B
一个页表有1024个页表项,所以一个页表可表示1024*4kB=4MB的虚拟内存。

一个页目录大小:4kB
一个页目录项大小:4B
一个页目录有1024个页目录项,所以该页目录可表示4MB*1024=4GB虚拟内存。

根据物理页大小4B 页表项个数2102^{10}个 ,页目录项个数2102^{10}个,可以将虚拟地址分成:

10位 10位 12位
页目录项索引 页表项索引 块内偏移量

具体实现步骤如下:

1.构造页目录及页表,完成从虚拟地址3GB~3GB+1MB到实地址0 ~1MB的映射以及虚拟地址0 ~ 1MB向物理地址0 ~ 1MB的映射。[注]本次实验虚拟地址3GB和虚拟地址0将会转化成相同的物理地址0。
2.开启保护模式,将页目录地址赋值给cr3寄存器,并开启保护模式的分页机制。
3.修改GDTR中的段基址,以及显存段段描述符中的段基址,确保代码 mov byte [gs:160],‘V’ 中的[gs:160]在保护模式的分段机制和分页机制下,最后会拼出虚拟地址0xc00b800+160,显然此地址在3GB~3GB+1MB的虚拟地址之间。

加载内核

gcc会自动将main.c转化成具有elf格式的kernel.bin。
将kernel.bin刻入磁盘第9扇区,修改loader.S将其从磁盘加载到内存0x70000,该地址就是elf头的首地址。
在loader.S新编一个函数kernel_init,读取elf头内的信息,再读取程序头表,根据elf规范将kernel.bin的所有段拷贝到内存中相应位置,我们这次的main.c只有一个段,段内是死循环。
loader.S最后一句代码是jmp到0xc0001500地址。该地址就是设计好的死循环函数的入口地址。
bochs模拟最后程序死循环,说明死循环段被成功加载到虚拟地址0xc0001500处。

第六章

通过c语言和汇编混合编程的方式实现打印函数,其中会使用到内联汇编的知识。具体实现没有什么问题,看看书就能理解了。

第七章

这一章的任务是实现中断,具体过程如下:
1.外设发出中断信号给8259A芯片。
2.8259A芯片处理信号,并向CPU发送中断向量号。
3.CPU根据中断向量号,在IDT表中找到对应的中断门描述符,从获取中断门描述符获取对应的中断处理程序所在代码段的选择子和偏移地址,开始执行中断处理程序。
4.中断处理程序执行结束,CPU返回原程序。
在最后一节要修改kernel.S来改进中断处理程序,并调快时钟。