前面我已经写完了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地址上。

概括起来由几个步骤组成:

  1. 开启A20地址线(启用32位寻址)
  2. 进入保护模式
  3. 配置fs寄存器
  4. 退出保护模式

然后fs寄存器就拥有了32位的寻址空间了。但是,需要注意的是,我们不能在实模式下再次对fs寄存器赋值,否则它就会失去这种32位寻址能力。

部分代码如下:

; 使用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、临时页目录和页表、中断处理模块。

大致流程如下

  1. 屏蔽外部中断
  2. 加载GDT的基地址和长度到GDTR寄存器
  3. 置位CR0的PE标志位
  4. 执行远跳转,切换到保护模式的代码段(将代码段寄存器更新为保护模式)
  5. 重新加载数据段选择子,或使用jmp/call执行新任务,更新数据段寄存器为保护模式
  6. 执行LIDT
  7. 启用外部中断

从保护模式进入IA-32e模式

从保护模式进入IA-32e模式的过程与上面的类似。也是要重新加载64位的页表、GDT、LDT、IDT。具体流程要看英特尔开发人员手册Volume3的9.8.5节。

这里涉及到了IA32_EFER寄存器,它位于MSR寄存器组内。(英特尔开发手册volume4 chapter2 页码2-60)

初始化IA-32e的标准步骤

  1. 复位CR0的PG标志位,关闭分页机制
  2. 置位CR4的PAE标志位,开启物理地址扩展。
  3. 将页目录的物理基地址加载到CR3中
  4. 置位IA32_EFER寄存器的LME标志位,开启IA-32e模式
  5. 置位CR0的PG标志位,开启分页机制,此时处理器会自动置位IA32_EFER寄存器的LMA标志位

最后一个远跳转指令,跳转到内核程序去执行,就成功将处理器切换到IA-32e模式了。

转载请注明来源:https://longjin666.cn/?p=1315

欢迎关注我的公众号“灯珑”,让我们一起了解更多的事物~

你也可能喜欢

发表评论