一个操作系统的设计与实现——第20章 加载64位内核,64位显卡驱动与内存管理系统
20.1 64位ELF格式
在64位模式下,由于内存地址变宽,ELF格式中的内存地址也要跟着变宽。这并不是一个麻烦的问题,因为ELF格式的整体结构没有发生变化,仍然由一个文件头,加上若干程序头表组成。
对于64位ELF格式的文件头,我们需要关注的信息如下表所示:
偏移量 | 字节数 | 含义 |
---|---|---|
0x18 |
8 | 程序入口地址 |
0x20 |
8 | 程序头表在ELF文件中的偏移量 |
0x36 |
2 | 程序头表中每个表项的大小 |
0x38 |
2 | 程序头表中表项的数量 |
对于64位ELF格式的程序头表项,我们需要关注的信息如下表所示:
偏移量 | 字节数 | 含义 |
---|---|---|
0x0 |
4 | 表项类型,1为可加载的段 |
0x8 |
8 | 当前段在ELF文件中的偏移量 |
0x10 |
8 | 当前段需要被加载到的虚拟地址 |
0x20 |
8 | 当前段在ELF文件中的大小 |
0x28 |
8 | 当前段在内存中的大小 |
20.2 加载内核
请看本章代码20/Mbr.s
。
第78~111行,读取硬盘的2~97号扇区,并加载到0x80000
处。2~97号扇区存放的是内核ELF文件,1号扇区与98~99号扇区保留给后续章节使用。[0x80000, 0x90000)
为内核缓冲区。
第113~116行,从ELF文件头中读取并计算程序头表的地址,表项的大小与数量。这些立即数都没有超过32位,因此是可以直接使用的。
第118~138行,读取每个程序头表项,并将其中的段展开到目标位置。这部分的实现思路与32位操作系统一致,这里不再赘述。
第140行,跳转至内核。同样的,这个立即数没有超过32位,因此是可以直接使用的。
20.3 64位显卡驱动
请看本章代码20/Util.h
。
第13~16行,定义了va_xxx
系列宏。64位GCC使用的是System V AMD64 ABI,该ABI混合使用寄存器与栈传参,笔者不清楚如何在不借助编译器的前提下实现这套功能,因此,直接将这些宏转发到GCC内建的对应功能上。
20/Util.h
的剩余部分,以及本章代码20/Util.hpp
的实现思路与32位操作系统一致,这里不再赘述。
64位显卡驱动的实现位于本章代码20/Print.h
与20/Print.hpp
中,其实现思路与32位操作系统一致,但有以下区别:
0xb8xxx
的虚拟地址是0xffff8000000b8xxx
- 64位模式下可以使用
movsq/stosq
指令,一次操作8字节 printHex
函数,以及printf
的%x
用于打印64位无符号整数,可用于打印指针
20.4 64位内存管理系统
想要实现内存管理系统,就需要先实现位图。位图的实现位于本章代码20/Bitmap.h
与20/Bitmap.hpp
中,其实现思路与32位操作系统一致,这里不再赘述。
接下来,请看本章代码20/Memory.h
。
第5~9行,声明了内存管理系统的各个接口。
接下来,请看本章代码20/Memory.hpp
。
第7行,定义了内核虚拟地址的分配起点。
第8行,定义了物理地址的分配起点。
第10行,定义了内核虚拟内存池位图,以及物理内存池位图。
第11行,定义了上述两个位图的缓冲区,每个位图使用一页内存。缓冲区位于BSS段,因此无需手动清零。
memoryInit
函数用于初始化两个内存池位图。
__allocateAddr
函数用于从内存池位图中分配地址。
__installPage
函数用于在虚拟地址与物理地址之间安装映射关系。这个函数的实现原理与二级分页模式一致:反复利用最后一个PML4E进行空兜。只是在四级分页模式下,虚拟地址转换的步骤更多。建议读者使用带二进制功能的计算器构造与检查函数中用到的各种掩码。
allocateKernelPage
函数用于在内核地址空间分配连续的pageCount
页虚拟地址,这些内存在返回前会被清零。
installTaskPage
函数用于在任务地址空间安装虚拟地址。这个函数有两个用途:
- 解析任务的ELF文件时,按ELF文件中的要求预先安装虚拟地址
- 安装任务的3特权级栈
其中,第二个用途是一定会超出任务的内存池位图范围的,因此,在操作位图时应注意边界。
虚拟地址的安装是以页为单位的,但startAddr
与startAddr + memorySize
均不保证对齐到页边界,因此,应将startAddr
向下对齐到页边界,并将startAddr + memorySize
向上对齐到页边界,再进行地址安装。
__deallocateAddr
函数用于在内存池位图中回收地址。
deallocateKernelPage
函数用于回收内核虚拟页地址。回收地址时,只需要操作PTE即可。
deallocateTaskCR3
函数用于回收cr3
中前256个PML4E。这个函数的实现原理与二级分页模式一致:反复利用最后一个PML4E进行空兜。只是在四级分页模式下,虚拟地址转换的步骤更多。建议读者使用带二进制功能的计算器构造与检查函数中用到的各种掩码。
20.5 编译与测试
请看本章代码20/Makefile
。
第4行,编译内核。-fPIC
选项用于打开RIP相对寻址。
第5行,链接内核。-N --no-relax
均为与-fPIC
配合使用的选项,代码段的起始地址为0x0
。
第7行,将内核从2号扇区开始写入。
本章代码20/Kernel.c
测试了显卡驱动与内存管理系统的一些功能。