前面我已经写完了boot程序,搭建好了FAT文件系统,系统的控制权已经移交给了Loader程序。
Loader程序的功能
Loader程序的主线功能就是检测硬件信息、切换处理器模式、向内核传递数据。
检测硬件信息
由于BIOS自检得到的大部分的信息只能在实模式下获取,因此我们需要在进入内核之前,把这些信息读取出来,传递给内核程序来使用。
切换处理器模式
要使得操作系统运行在64位的环境下,就需要loader进行切换。
最开始,BIOS运行在实模式,提供20位的寻址空间。然后我们的loader需要把处理器切换到保护模式,这样就有了32位的寻址空间。最终再切换到IA-32e模式(长模式),获得64位的寻址空间。
在各个模式的切换之中,loader程序需要创建一些临时数据,然后按照标准流程进行切换。其中包括的配置系统临时页表的工作,保证页表覆盖的地址空间能满足应用程序的使用要求。临时的段结构也是一个道理的。
向内核传递数据
这里讲的数据包括了控制信息和硬件数据信息两部分。
地址空间的设置
在Loader引导加载程序部分,先设定将来内核要被放置的空间的起始地址是0x100000(1MB)处。原因是,在实模式下,BIOS的最大寻址空间是20位,也就是0xFFFFF,在这以下的空间内,成分比较复杂,可以是内存空间、非内存空间以及地址空洞。1MB往上的空间比较干净。因此内核的起始地址就选在这里。
并且,我们定义0x7E00为内核程序的临时转存空间,到时候会先把内核程序加载到这里,再通过Big Real Mode,将内核程序转存到1MB的地址上。
进入Big Real Mode
Big Real Mode虽然还是在实模式下运行,但是fs寄存器拥有32位的寻址空间。我们将来要通过这种方式,来把内核程序转移到1MB地址上。
概括起来由几个步骤组成:
- 开启A20地址线(启用32位寻址)
- 进入保护模式
- 配置fs寄存器
- 退出保护模式
然后fs寄存器就拥有了32位的寻址空间了。但是,需要注意的是,我们不能在实模式下再次对fs寄存器赋值,否则它就会失去这种32位寻址能力。
部分代码如下:
代码语言:javascript复制; 使用A20快速门来开启A20信号线
push ax
in al, 0x92 ; A20快速门使用I/O端口0x92来处理A20信号线
or al, 0x02 ; 通过将0x92端口的第1位置1,开启A20地址线
out 0x92, al
pop ax
cli ; 关闭外部中断
db 0x66
lgdt [GdtPtr] ; LGDT/LIDT - 加载全局/中断描述符表格寄存器
; 置位CR0寄存器的第0位,开启保护模式
mov eax, cr0
or eax, 1
mov cr0, eax
; 为fs寄存器加载新的数据段的值
mov ax, SelectorData32
mov fs, ax
; fs寄存器加载完成后,立即从保护模式退出。 这样能使得fs寄存器在实模式下获得大于1MB的寻址能力。
mov eax, cr0
and al, 11111110b ; 将第0位置0
mov cr0, eax
sti ; 开启外部中断
jmp $
接着我们在qemu中启动操作系统,按Ctrl Alt 2进入调试窗口,输入info registers,查看寄存器的值,如下图所示
可以看到FS寄存器的基地址为0x00000000 ,段限长为0xffffffff,也就是拥有了32位的寻址能力(4GB)。
进入保护模式之前需要做的工作
把kernel.bin转存到1MB以上的内存空间
这里就可以复用boot.asm中,搜索loader.bin的代码。将其改成搜索kernel.bin即可。由于我们还没有写内核程序,因此这里用一个空的kernel.bin来占位,方便后面的开发。
当找到内核程序文件后,就逐个簇地读取内核文件到临时地址,再立即移动到1MB以上的空间去。
获取物理地址空间的信息
由于物理地址空间信息等数据,只能在实模式下获取,因此loader必须先读取这部分的信息,然后暂存到一个位置(这里我选择的是0x7e00),以供将来操作系统内核读取。
这里面借助了BIOS的INT0x15的子功能号0xE820来获取内存信息。
获取SVGA芯片的信息
这是一个显示芯片,为了能正确显示图像,我们获取了它的信息。并且设置它的显示模式为0x180,也就是1440*900,32位宽。
从实模式进入保护模式
这里的具体流程需要参考英特尔的开发人员手册Volume3 的9.8.1-9.8.4节以及9.9.1节。在切换之前,需要我们在内存中创建一段可在保护模式下执行的代码,以及必要的系统数据结构。包括了GDT、LDT、IDT表各一个(IDT是可选的),以及任务状态段结构TSS、临时页目录和页表、中断处理模块。
大致流程如下
- 屏蔽外部中断
- 加载GDT的基地址和长度到GDTR寄存器
- 置位CR0的PE标志位
- 执行远跳转,切换到保护模式的代码段(将代码段寄存器更新为保护模式)
- 重新加载数据段选择子,或使用jmp/call执行新任务,更新数据段寄存器为保护模式
- 执行LIDT
- 启用外部中断
从保护模式进入IA-32e模式
从保护模式进入IA-32e模式的过程与上面的类似。也是要重新加载64位的页表、GDT、LDT、IDT。具体流程要看英特尔开发人员手册Volume3的9.8.5节。
这里涉及到了IA32_EFER寄存器,它位于MSR寄存器组内。(英特尔开发手册volume4 chapter2 页码2-60)
初始化IA-32e的标准步骤
- 复位CR0的PG标志位,关闭分页机制
- 置位CR4的PAE标志位,开启物理地址扩展。
- 将页目录的物理基地址加载到CR3中
- 置位IA32_EFER寄存器的LME标志位,开启IA-32e模式
- 置位CR0的PG标志位,开启分页机制,此时处理器会自动置位IA32_EFER寄存器的LMA标志位
最后一个远跳转指令,跳转到内核程序去执行,就成功将处理器切换到IA-32e模式了。
转载请注明来源:https://longjin666.cn/?p=1315