操作系统的设计与实现——BootLoader引导加载程序
《一个64位操作系统的设计与实现-田宇》学习笔记(一)BootLoader引导加载程序
第一篇笔记主要内容是BootLoader的原理与实现
操作系统概述
操作系统的组成结构如下图,分为内核层与应用层。内核层主要负责控制硬件设备、分配系统资源、为应用层提供健全的接口支持、保证应用程序稳定运行、执行流调度等等。而应用层则主要负责人机交互。
引导启动
是指,计算机从BIOS上电自检后到跳转至内核程序执行前这一期间执行的一段或几段程序 这些程序主要用于检测计算机硬件,并配置内核运行时所需的参数 然后再把检测信息和参数提交给内核解析,内核会在解析数据的同时对自身进行配置。引导启动程序曾经分为两部分,Boot和Loader,现在通常合二为一,统称为BootLoader
内存管理
主要作用是有效管理物理内存
异常/中断处理
进程管理
设备驱动
文件系统
文件系统用于把机械硬盘的部分或全部扇区组织成一个便于管理的结构化单元
编写一个操作系统需要的知识:
硬件方面
需要指导处理器和外围设备的电路组成,也就是处理器和外围设备是怎么连接的,进而可以知道处理器如何控制外围设备,以及如何与之通信。
需要阅读硬件设备的芯片手册,芯片手册会详细描述芯片的硬件特性、通信方式、芯片内部的寄存器功能,以及控制寄存器的方法。但是和电子工程师关注的点不一样,作为操作系统开发人员更关注处理器如何与硬件设备通信、如何控制它们的寄存器状态。
所以在硬件方面, 掌握硬件电路、处理器和外围设备的芯片手册即可。其中,处理器芯片手册会介绍如何初始化处理器 如何切换处理器工作模式等一系列操作处理器的信息与方法,这些知识为操作系统运行提供技术指导 硬件芯片手册会对设备上的所有寄存器功能进行描述,我们根据这些寄存器功能方可编写出驱动程序。
软件方面
汇编语言和C语言,还有数据结构。
虚拟机及开发系统平台介绍
开发和编译环境书里用的CentOS 6,我用的是MacOS 10.15。虚拟运行环境书里用的Bochs,我可能也用用QEMU。在mac上可以很方便的用brew安装bochs和qemu,不用折腾编译的问题
1 | # 安装qemu |
汇编语言的书写格式大体分两种,一种是AT&T汇编语言格式,一种是Intel汇编语言格式。AT&T的格式相对复杂,Intel格式相对简洁。
Boot引导程序
Boot原理
BIOS自检结束后会根据启动选项设置去选择启动设备。比如如果是软盘启动的话,就去检测第0磁头第0磁道第1个扇区,是否以数值0x55 0xaa两字节作为结尾如果是,那么BIOS就认为这个扇区是 Boot Sector (引导扇区),进而把此扇区的数据复制到物理内存地址0x7c00处,随后将处理器的执行权移交给这段程序(跳转至0x7C00地址处执行)
因为这个扇区只有512B,所以只能装的下一个boot程序,由boot程序将Loader程序装到内存中,再由Loader程序装载内核。这个过程也可以看作是硬件设备向软件设备移交控制权。
写一个Boot引导程序
这里通过BIOS的中断服务,实现了往屏幕显示字符串的功能:
需要注意开头的org 0x7c00
起始地址,因为BIOS会将这段程序装载到物理内存的0x7c00地址,如果不指定这个起始地址的话可能会导致寻址错误。而末尾的最后两个字节0x55 0xAA
则表明这是一个引导扇区
BIOS中断向量表:https://blog.csdn.net/piaopiaopiaopiaopiao/article/details/9735633
通过BIOS中断来实现各种功能
在虚拟机中运行Boot
QEMU运行
原书用的bochs,一开始我在ubuntu18上装最新版的bochs有问题没解决,所以用了qemu
1 | sudo apt-get install qemu |
编译boot.asm
1 | nasm ./boot.asm -o boot.bin |
将二进制文件写入软盘映像
1 | dd if=boot.bin of=./boot.img bs=512 count=1 conv=notrunc |
qemu启动脚本:
1 | boot.sh |
Run:
1 | ./boot.sh ./boot.img |
鼠标点进去QEMU窗口后会锁定出不来,我是mac上开的vmware ubuntu,直接Ctrl+空格的话是鼠标回到mac主机,要从QEMU回到ubuntu虚拟机的话,得按ctrl+option+空格
Bochs运行
运行bochs的配置文件bochsrc:
1 | # configuration file generated by Bochs |
bochs运行bochs -f ./bochsrc
:
加载Loader到内存中
创建FAT12文件系统
为方便后续加载loader和加载kernel,为软盘创建了FAT12文件系统。
这样引导扇区的结构也要改变,在boot代码之前要加入FAT12文件系统的控制信息:
对应的代码:
FAT12文件系统下的软盘可以分为这几个区:
其中FAT1和FAT2是两个一样的FAT表,相当于有一个是备份的作用。
FAT12文件系统以簇分配数据区的存储空间。一个文件存储的时候,占用的磁盘空间不一定是连续的,FAT表就是用于将这些不连续的文件片段按照簇号链接起来,就像单向链表一样。所以可以看到FAT表项中的值,其实就是下一个簇的簇号,这样只需要给出文件对应存储空间的第一个簇号,就可以根据FAT表一直检索出后续的簇了。FAT表项如下:
根目录区保存着目录项信息。用来描述文件和目录的,最重要的是起始簇号,根据起始簇号可以像上面说的那样,根据FAT表定位到文件的存储位置。目录项结构如下:
然后借助BIOS中断服务来写一个从磁盘读取扇区数据到内存的函数:Func_ReadSector()
用到的中断:
关于扇区、磁道、磁头等,参考磁盘基本知识
函数接收三个参数,起始扇区号、读入扇区数、目标缓冲区地址。其中,起始扇区号用的是LBA格式(Logic Block Address,逻辑块格式),而INT 13H,AH=02H中断服务需要的是CHS格式(Cylinder/Head/Sector,柱面/磁头/扇区格式)的磁盘扇区号,因此在函数中要进行转换:
Func_ReadSector()
函数:
寻找loader
然后可以可以通过Func_ReadSector()
方法,将根目录区中的扇区读入内存缓冲区,来寻找目录文件名称为”LOADER BIN”的目录项。这里需要说明的是,FAT12文件系统的目录项中目录文件名规定是11位,其中,8字节为文件名,3字节为扩展名,而且FAT12是不区分文件名大小写的。比如一个小写字母组成的文件“loader.bin”,会创建一个文件名为大写”LOADER BIN”的目录项,数据都是放在大写名称的目录项中的。而小写文件名的目录项尽作为其显示的名称来用,因此这里我们搜索的是大写名称的目录项。
搜索loader.bin文件目录项的代码如下,代码逻辑主要是:从根目录区读取扇区到内存,然后遍历扇区内的每一个目录项,将目录项内的文件名字符串和”LOADER BIN”比对,如果在这个扇区中找到了对应的目录项,则跳转至成功分支,否则,继续读取下一个扇区,直到搜索完根目录区的14个扇区为止。(根目录所容纳的目录项数=224, 目录项大小=32,224*32/512 上取整 = 14个扇区)
搜索失败的话在频幕上显示搜索失败的字符串:
这里还没有写loader.bin程序,所以当然会搜索失败了,在Bochs中运行:
找到对应的目录项后,就可以从里面拿出起始的簇号。
我们知道,要想从磁盘中读取整个文件,需要从起始簇号开始,通过FAT表不断的检索下一个簇号,因此可以先实现一个函数Func_ParseFATEntry()
,其功能是用于解析FAT表项,输入一个簇号(FAT表项号),输出下一个簇号(FAT表项号)。代码如下,主要逻辑就是通过FAT表项号计算出目标FAT表项所在的扇区及扇区内偏移,通过Func_ReadSector()
方法把FAT表项所在的扇区加载进内存,再根据扇区内偏移读取FAT表项中的内容,以得到下一个簇的簇号。这里需要注意的是,FAT12文件系统的一个FAT表项是12位即1.5个字节,3个字节存储2个FAT表项。所以需要判断表项的奇偶性,才能准确的读出表项内容,具体的操作看代码:
有了Func_ParseFATEntry()
和Func_ReadSector()
,就可以根据目录项中的起始簇号,查询fat表,按顺序将loader.bin加载进内存了。代码如下,代码逻辑主要就是读取出对应目录项中的DIR_FstClus
字段,得到loader.bin的起始簇号,然后根据FAT表,按顺序一个簇一个簇地将文件内容加载到内存中,直到FAT表项中的值为0xfff,表示这是文件的最后一个簇,然后就jmp到loader的地址去执行loader的代码。这里给loader安排的内存地址是0x10000,因为现在CPU是以实模式运行的,是通过段寄存器<<4 + 段内偏移得到物理地址的,所以这里将段寄存器内容设置为LoaderBase=0x1000
,段内偏移设置为LoaderOffset=0
:
从Boot跳转到Loader
将loader加载到内存后,通过jmp LoaderBase:LoaderOffset
跳转到loader地址处。这里使用的是段间的跳转,所以需要指明段基地址。这条代码执行后,CS寄存器的值就会设置为LoaderBase的值,即0x1000,在实模式下段寄存器的值需要左移四位,即得到Loader的段基地址0x10000。
写一个简单的Loader,在屏幕上显示开始执行loader的信息,这里我将loader填充为5个扇区大小,是为了检测上面boot中的按顺序读取扇区功能是否正常工作
编译boot.asm和loader.asm
1 | nasm boot.asm -o boot.bin |
用bximage创建一个1.44M大小的软盘镜像:
1 | liuzhongxin@MacBook-Pro boot % bximage |
将boot.bin写入软盘:
1 | dd if=boot.bin of=./boot.img bs=512 count=1 conv=notrunc |
然后将loader.bin放进软盘中,这里因为在boot.bin中已经为软盘创建文件系统了,所以只需要挂载软盘镜像,然后用cp
命令,就可以将loader.bin复制进去,cp
命令是会自动识别文件系统并做出正确的操作的,而不需要我们自己去设置目录表项、FAT表项啥的。
在mac下,可以直接使用hdiutil mount img
来装载软盘镜像到/Volumes/
目录下,推出软盘用hdiutil eject /Volumes/bootloader
。其实也可以直接双击它,mac就会将它装载了,就像拔插U盘一样方便
1 | liuzhongxin@MacBook-Pro boot % hdiutil mount ./boot.img |
然后将loader.bin直接copy进去就行了
1 | cp ./loader.bin /Volumes/bootloader/ |
用bochs运行:
根据boot.asm中的代码,每读取loader.bin的一个扇区就输出一个"."
,这里输出了五个,正好符合Loader.bin的大小,说明boot.asm的代码没有问题~
Boot引导总结
总结一下boot引导程序的过程:
- 创建引导扇区(第0磁头、第0磁道、第1扇区以0x55 0xAA结尾)
- 引导程序以0x7c00作为起始地址
- 在引导扇区中建立FAT12文件系统(FAT12文件系统的整个组成结构信息,包括FAT表份数、根目录区容纳的目录项数、总扇区数等)
- 编写函数
Func_ReadSector()
,实现从磁盘读取扇区到内存的功能 - 使用
Func_ReadSector()
函数,将根目录区中的扇区逐个读入内存,寻找loader.bin目录项 - 编写函数
Func_ParseFATEntry()
,输入FAT表项号,输出下一个簇的簇号(FAT表项号) - 使用
Func_ParseFATEntry()
函数和Func_ReadSector()
函数,将loader.bin从磁盘中加载进内存 - jmp到Loader引导加载程序
Loader引导加载程序
Loader原理
Loader引导加载程序的任务主要有三个:检测硬件信息、处理器模式切换、向内核传递数据。这些工作能够让内核初始化之后正常运行。
检测硬件信息
Loader引导加载程序需要检测的硬件信息很多,主要是通过BIOS 中断服务程序来获取和检测硬件信息 由于BIOS在上电自检出的大部分信息只能在实模式下获取,而且内核运行于非实模式下,那么就必须在进入内核程序前将这些信息检测出来,再作为参数提供给内核程序使用在这些硬件信息中,最重要的莫过于物理地址空间信息,只有正确解析出物理地址 间信息,才能知道ROM RAM 设备寄存器 间和内存空洞等资源的物理地址范围,进而将其交给内存管理单元模块加以维护。
处理器模式切换
本书第六章就是处理器体系结构相关的基础内容
从起初BIOS运行的实模式( real mode ),到32位操作系统使用的保护模式( protect mode ),再到64位操作系统使用的IA-32e模式( long mode ,长模式), Loader引导加载程序必须历经这三个模式,才能使处理器运行于64位的IA-32e模式。
向内核传递数据
这里的数据一部分是控制信息,这部分是纯软件的,比如启动模式之类的东西,另一部分是硬件数据信息,通常是指Loader引导加载程序检测出的硬件数据信息 Loader引导加载程序将这些数据信息多半都保存在固定的内存地址中,并将数据起始内存地址和数据长度作为参数传递给内核,以供内核程序在初始化时分析 配置和使用,典型的数据信息有内存信息、 VBE信息等
装载Kernel,突破1MB寻址限制
Loader最终要将Kernel装载到内存地址0x100000处,即1MB处,而实模式下,物理地址寻址位宽是20位,刚好上限就是1MB,因此,要想将Kernel装载到内存地址0x100000处,需要突破这个寻址限制。
首先,我们需要开启地址A20功能,这是一个历史遗留问题。最初的处理器只有20根地址线,只能寻址1MB以内的物理内存,为了保证硬件平台的向下兼容,便出现了一个控制开启或禁止1MB以上地址空间的开关。这个开关引脚称之为A20。如果A20引脚位低电平,那么只有低20位地址有效,其它位均为0。开启A20功能这里使用了操作I/O端口0x92的方法,其实也可以通过BIOS中断服务程序INT 15h来做。
开启了地址A20功能后,就要想办法突破保护模式的逻辑地址寻址位宽只有20位的限制了。这里,我们采用的方法是:先进入保护模式,在保护模式中直接给fs段寄存器载入段选择子(该段选择子指向的段描述符的段基址为0,段限长为4GB),然后处理器在装入段选择子的同时还会把段描述符给装入fs段寄存器(隐藏部分),然后我们再切回实模式,接下来再使用fs段寄存器访问内存时,处理器就会按照保护模式的逻辑地址寻址方式,即段基地址+32位段内偏移,这样就可以把寻址范围从1MB扩大到4GB了
。
开启地址A20功能和为fs段寄存器加载段选择子、段描述符的代码如下,这里进入保护模式是通过mov cr0, eax
指令将CR0.PE标志位置1,回到实模式则反之。注意lgdt
指令前的0x66
,这是因为当前代码处于16位宽状态下(在代码开始使用了[SECTION .16]定义了一个新的segment,并使用[BITS 16]伪指令通知NASM编译器生成的代码将运行在16位宽的处理器上),使用32位宽数据指令时需要在指令前加入前缀0x66。cli
和sti
指令则分别用于关闭外部中断和开启外部中断。
代码中用到的段描述符和段选择子等相关数据结构如下:
通过boot装载loader的过程我们知道,将磁盘中的数据装进内存是借助BIOS中断服务程序完成的,而实模式下的BIOS中断服务程序只支持1MB以内的物理地址空间寻址。因此,我们可以取一个中转的地址0x7E00,先用BIOS中断服务程序将扇区中的数据读入这个1MB内的缓冲区,再使用mov
和loop
指令将这个扇区的内容复制到0x100000地址处即可。
这里可以直接使用boot.asm中寻找loader.bin的代码,将其装载入内存的代码也可以copy过来,稍作修改、增加内存复制的代码即可:
最后,完成装载后在屏幕第一行中间输出一个G
字母,标识装载完成kernel,这里没有用INT 10h中断来完成输出,而是用了更符合操作显卡内存的方法。在最开始的1MB物理地址空间内,不仅有显示字符的内存空间,还有显示像素的内存空间以及其它用途的内存空间。这段代码只为展示操作显示内存的方法,毕竟不能长期依赖BIOS中断服务程序。知乎-关于内存地址和显存地址
编译后运行,这里我随便写了个kernel.bin,以验证装载是否正常运行:
编译运行:
利用bochs的内存查看功能,看一下0x10000处的内容是否正确:
正确!说明kernel.bin已经被正确的加载到了物理地址0x100000处
同时,通过sreg命令查看段寄存器,可以看到fs段寄存器与其它实模式下的段寄存器的不同(段基地址和段限长等):
获取物理地址空间信息
参考资料
Detecting Memory(x86) BIOS Function: INT 0X15, EAX=0XE820#BIOS_Function:_INT_0x15.2C_EAX_.3D_0xE820)
在x86平台下,通过BIOS中断服务程序INT 15h, EAX=0xE820
可以获取物理地址空间的信息,物理地址空间信息由一个结构体数组构成,它描述了计算机平台的地址空间划分情况,它记录的地址空间类型包括可用物理内存空间、设备寄存器地址空间、内存空洞等…
这个中断服务程序的具体用法可以参考Detecting Memory(x86) BIOS Function: INT 0X15, EAX=0XE820#BIOS_Function:_INT_0x15.2C_EAX_.3D_0xE820)
By far the best way to detect the memory of a PC is by using the INT 0x15, EAX = 0xE820 command. This function is available on all PCs built since 2002, and on most existing PCs before then. It is the only BIOS function that can detect memory areas above 4G. It is meant to be the ultimate memory detection BIOS function.
基本用法:
对于第一次调用该函数,将ES:DI指向列表的目标缓冲区。清除EBX。将EDX设置为幻数0x534D4150。将EAX设置为0xE820(请注意,EAX的高16位应设置为0)。将ECX设置为24。执行INT 0x15。如果第一次调用该函数成功,则EAX将设置为0x534D4150,并且进位标志将被清除。 EBX将被设置为某个非零值,在下次调用该函数时必须保留该值。 CL将包含实际存储在ES:DI的字节数(可能为20)。
对于随后的函数调用:将DI增加您的列表条目大小,将EAX重置为0xE820,将ECX重置为24。到达列表末尾时,EBX可能重置为0。如果再次使用EBX =调用该函数0,列表将重新开始。如果EBX未重置为0,则当您在最后一个有效条目之后尝试访问该条目时,该函数将返回进位设置。
这里我们通过”0xE820”将物理地址空间信息保存在0x7E00地址处的临时转存空间里(这块地址刚才用来作为临时的kernel缓冲区了,现在kernel已经装载完毕,所以可以将这块地址另作他用)操作系统会在初始化内存管理单元时解析该结构体数组。
在Ubuntu虚拟机中,通过dmesg | grep e820
可以看到这个结构体数组的内容:
结构体数组的表项如下(取自Linux kernel):
1 | struct e820entry { |
每进行一次调用,就会往ES:DI
写入一个这种结构体,然后需要我们手动增加DI
的值,继续读取下一个结构体到内存,直到EBX
返回0为止。
代码实现如下,这里面我把利用中断服务程序打印字符串的功能封装成了一个函数
编译运行,查看0x7e00处的内存,可以看到已经将内存信息结构体数组读到了物理地址0x7e00处
从实模式进入保护模式
完成这部分功能之前,建议先学习x86处理器的几种模式,可以阅读本书第六章(学习笔记:https://www.sunxiaokong.xyz/2020-09-05/lzx-babyos-2/ )
如上图所展示的处理器运行模式转换图可以看到,通过置位CR0
控制寄存器的PE
标志位可以使处理器进入保护模式。另外,在CR0.PE
置位的情况下,再置位CR0.PG
即可开启分页模式。
而在进入保护模式之前,还需要准备一些必要的全局数据结构:
- GDT,全局段描述符表
- LDT,局部段描述符表(可选)
- IDT,中断描述符表
- 页表,可选,如果要开启分页模式的话必须要有一个页表
在这里,由于只是中间过渡(切换到保护模式后马上就再切换进入长模式),因此先不开启分页模式,也不用准备LDT,因此必须要准备的数据结构只剩下GDT和IDT。
GDT中要有两个描述符,一个32位代码段描述符,一个32位数据段描述符;而由于在进行模式切换前会使用cli
指令(clear interrupt)屏蔽可屏蔽中断,因此这里的中段描述符表IDT只需要分配好空间即可,暂时不需要设置中段描述符。
目前的GDT如下,这个表在前面开启Big Real模式的时候也使用到了
临时的IDT如下:
设置好这两个表后,就可以通过lgdt
和lidt
指令将表的指针和长度信息载入gdtr
和idtr
寄存器了,而在使用这两个指令前要注意加上前缀0x66
,因为现在还是运行在实模式下,所以需要用这个前缀来修饰当前指令的操作符是32位。然后,就可以置位CR0.PE
,进入保护模式。代码如下:
1 | ; Init IDT GDT then goto protect mode |
可以看到,切换到保护模式之后,使用了一个jmp dword SelectorCode32:Label_TMP_Protect_mode
长跳转,这个指令明确指明了段选择子,其目的是将SelectorCode32
代码段选择子载入CS寄存器(同时会往CS寄存器的隐藏部分载入32位代码段描述符),这是由于刚进入保护模式,段寄存器仍旧保留着实模式的段数据,因此手动加载段选择子,或者使用JMP/CALL指令,便可将其更新为保护模式,以切换到保护模式的代码段去执行。
编译并在bochs中运行,在Label_TMP_Protect_mode
处断下来,使用sreg
命令查看寄存器,可以看到,此时的cs寄存器已经载入了我们在GDT中存放的32位段描述符的段选择子,处在32位模式下。
在bochs中输入q命令退出时,也会输出处理器的信息,可以看到,此时cpu处在保护模式下。
从保护模式进入IA-32e模式
进入保护模式后,先初始化各个数据段寄存器以及栈指针,然后需要使用cpuid
指令来判断是否支持IA-32e模式:
这里我将使用cpuid
指令的扩展功能项0x80000001查询是否支持IA-32e模式的代码封装成了一个函数,参考http://www.flounder.com/cpuid_explorer2.htm#CPUID(0x80000000)
首先使用功能0x80000000判断扩展功能项的最大值是否超过了0x80000001,然后再使用CPUID.80000001h:EDX[29]
即可最终判断是否支持IA-32e模式了,代码如下:
如果检测结果表明支持IA-32e模式,则继续往下进行。
IA32_EFER
寄存器的LME
标志位用于控制IA-32e模式的开启与关闭。
IA-32e模式的标准初始化步骤如下:
- 在保护模式下,复位
CR0.PG
,以关闭分页机制,此后的指令必须位于同一线性地址映射的页面内。 - 置位
CR4.PAE
,开启物理地址扩展功能(PAE)。 - 将页目录表(顶层页表PML4)的物理基地址加载到CR3寄存器中。
- 置位
IA32_EFER.LME
,开启IA-32e模式 - 置位
CR0.PG
,开启分页机制,此时处理器还会自动置位IA32_EFER.LMA
然后由于IA-32e模式的分段机制和保护模式略有不同,因此需要重新准备一个IA-32e模式下使用的GDT:
然后可以开始编写代码进入IA-32e模式了!
首先初始化一套页表,这里直接用了书中的代码,直接往内存中写入:
然后使用64位的GDT结构初始化GDTR,并按照上面介绍的步骤,进入IA-32e模式:
可以看到,开启PAE、初始化CR3、开启LME、PG后,还是和之前从实模式进入保护模式一样,使用了一个远跳转指令,指明了使用64位代码段的段选择子,这条跳转指令执行后,CS寄存器中就会载入64位代码段选择子以及描述符了。
然后和刚才刚进入保护模式后一样,也为各个数据段寄存器加载了64位代码段的选择子和描述符。
编译并在bochs中运行,在最后的jmp $
断下来,查看各个段寄存器的信息:
可以看到,各个段寄存器中已经是64位代码/数据段的选择子和描述符了,可以看到在IA-32e模式下,段的基地址和段限长被忽略掉了,默认覆盖了整个地址空间,这使得IA-32e模式下的段管理更简单、平坦。
在bochs中输入quit
退出后,从调试信息中可以看到此时CPU确实处在IA-32e模式(长模式)下了。
总结
那么至此,bootloader程序的基本功能已经全部实现了!最后只需要使用一条远跳转指令,将处理器的控制权交到内核程序即可!
总结一下这里实现的bootloader:
Boot :
- 创建文件系统
- 寻找Loader程序并将其加载到内存中
- 跳转到Loader执行
Loader :
- 寻找Kernel并将其加载到内存中
- 检测硬件信息(这里暂时只实现检测了最重要的物理地址空间信息)
- 向内核传递数据(控制信息、硬件数据信息等,这里暂时只将物理地址空间信息存储在了内存中)
- 处理器模式切换,从实模式切换到保护模式再进入IA-32e模式
- 跳转到Kernel
当然了,这里实现的只是bootloader最基本的功能,后面我们还会通过学习,丰富并完善bootloader的功能。
而且目前系统虽已进入IA-32e模式,但只是临时中转模式,接下来的内核程序需要为系统重新创建完整的段结构和页表结构。
参考资料
主要参考学习《一个64位操作系统的设计与实现》
其他参考资料:
感谢阅读 ♪(^∇^*)
- 本文链接:http://www.sunxiaokong.xyz/2020-08-08/lzx-babyos-1/
- 版权声明:本站文章除特别声明外,均为本站原创或翻译,采用 知识共享署名 CC BY 4.0 国际协议进行许可,转载前请务必署名及注明出处。