【实现操作系统 05】完善 Loader 程序,并加载内核(下)

本文是 《【实现操作系统 04】完善 Loader 程序,并加载内核(上)》的下半部分,文章具有耦合度较强,建议先阅读上半部文章再选择阅读此文章。

1. IA-32e Mode

为了对操作系统提供更加广泛支持和系统软件开发,IntelIntel 386 处理器开始使用 IA-32 架构。这种架构提供多种操作模式,包含有:实模式 (Real Mode/Real Address Mode)、保护模式 (Protected Mode)、虚拟 8086 模式 (Virtual 8086 Mode) 以及系统管理模式 (System Management Mode)。

在后来的发展中,为了支持 64-bit 的编程环境,在 IA-32 架构的基础上推出了一款兼容 IA-32 架构的 Intel 64 架构,其拥有 IA-32 的所有特性和所支持的操作模式,并且还支持了一种新的操作模式 IA-32e Mode,以此来支持 64-bit 的开发环境,因此这种 64 位的架构也被称为 IA-32e 架构。

IA-32e 模式不仅简化了段级保护措施的复杂性,升级了内存寻址能力 (0~26416777216 TB),同时还扩展了页管理单元的组织结构和页面大小,推出新的系统调用方式和高级可编程中断控制器。

所有的 Intel 64 (IA-32e) 和 IA-32 处理器在上电或复位后会进入实模式,若需要切换到 IA-32e 模式操作,则首先需要从实模式切换到保护模式 (Protected Mode) ,再由保护模式切换到 IA-32e 操作模式。

2. 从实模式 (Real Mode) 切换到保护模式 (Protected Mode)

处理器在切换到保护模式之前,为了支持处理器可靠的运行保护模式,需要在内存中创建一段用于加载保护模式的数据结构以及一段可运行在保护模式下的代码。其中相关的数据结构有:Interrupt Descriptor Table——IDTGlobal Descriptor Table——GDTTask-State Segments——TSSLocal Descriptor Table——LDT(可选),如果使用分页机制,则至少需要一个页目录和一个页表,一个或多个包含必要中断和异常处理程序的代码模块。

并且在切换之前需要初始化 GDTR (Gloal Descriptor Table Register) 寄存器,也可选的初始化 IDTR (Interrupt Descriptor Table Register),但在切换之前为初始化 IDTR,那就必须在切换之后使能中断之前立马将其初始化,还需要初始化 CR0 ~ CR4 寄存器、内存类型范围寄存器 MTRR(仅限 Pentium 4Intel XeonP6 系列处理器)。

当准备工作完成后,则通过将 CR0PE (Protected Enabel) 标志位置位,即可切换到保护模式。

在这里插入图片描述

3. 内存管理寄存器(GDTR、LDTR、IDTR)

Intel 处理器提供了 4 个内存管理寄存器(GDTRLDTRIDTRTR)来指定控制分段内存管理数据结构的位置。先来看 GDTRIDTR 的结构。
[注]:个寄存器英文原名如下:
Global Descriptor Table Register (GDTR);
Local Descriptor Table Register (LDTR);
Interrupt Descriptor Table Register (IDTR);
Task Register (TR)

在这里插入图片描述

GDTR 寄存器保存段基地址(保护模式下为 32 位;IA-32e 模式下为 64 位)和 16 位 GDT 表的限长。 基地址指定 GDT 字节 0 的线性地址; 表限长指定表中的字节数。

加载 GDT 表需要用特殊的指令来操作,LGDTSGDT 指令分别是加载和存储 GDT 表到 GDTR 寄存器。 在上电或复位处理器后,GDTR 基地址设置为默认值 0,限长默认设置为 0FFFFh

在切换到保护模式之前必须用一个新的基地址初始化 GDTR

4. 段选择器 (Segment Selectors)

IndexTIRPL 三部分组成的 16 位长度的段选择器,用于索引 GDT/LDT 表中描述信息和填充段寄存器。

在这里插入图片描述

  • Index: Index 字段长度为 13 bits,可索引 GDT/LDT 表共 213 = 8192 项,处理器会将 Index 的值乘以 8 作为偏移地址,并从 GDTR/LDTR 中获取 GDT/LDT 的段基地址,从而查找到 GDT/LDT 中的段描述信息;
  • TI: 长度 1 bit,指定使用的描述符表,0:选择 GDT1:选择 LDT
  • RPL: 长度 2 bits,指定选择器的特权等级,特权等级分别由 0 ~ 3 共有 4 个, 0 为最高等级, 3 为最低等级。

在这里插入图片描述

5. 保护模式下的段寄存器

为了减少地址转换时间和编码的复杂性,处理器为保护模式提供了最多可容纳 6 个的段寄存器 CSDSSSESFSGS

在这里插入图片描述
每个段寄存器都有一个“可见”部分和一个“隐藏”部分。 (隐藏部分有时被称为“描述符缓存”或“影子寄存器”。)当段选择器被加载到段的可见部分时寄存器,处理器会自动从 GDT 表中获取信息并加载到段寄存器的隐藏部分,包括基地址、段限制和来自段选择器指向的段描述符的访问控制信息。 缓存的信息在段寄存器中(可见和隐藏)允许处理器在不占用额外总线的情况下转换地址周期从段描述符中读取基地址和限制。 在多个处理器的系统中可以访问相同的描述符表,软件有责任重新加载段寄存器描述符表被修改。 如果不这样做,缓存在段寄存器中的旧段描述符可能在其内存驻留版本被修改后使用。

6. 保护模式下的段管理机制

在保护模式下,所有的程序和数据都必须使用段描述符修饰。在保护模式下分页管理机制是可选的,而分段管理机制是必选的,保护模式下的段管理描述符的具体位的功能如下图 (摘自: Intel 白皮书卷3)。
在这里插入图片描述
[注]:需要注意一点,上图由于长度原因将一个完整的 64 位段寄存器切分为两段 32 说明,上半部为高 32 位,下半部为低 32 位。

对于其中个字段的解释:

  • L: 该位仅在 IA-32e 模式下使用,保护模式设为 0 即可;
  • AVL: 该位由系统软件使用,通常情况下设为 0
  • Base / Base Address: 段基地址描述的是段的起始地址,这里由 3 部分字段拼接而成的 32 位线性地址。 (字节高低不变),官方建议段基地址设置为以 16 B 对齐从而保证处理器的高速执行;
  • D/B: 描述代码段或堆栈段的操作数位宽以及上边界,1: 表示 32 位代码和数据段;0: 表示 64 位代码和数据段;
  • DPL: 段描述符的特权等级,等级范围 0 ~ 30 为最高特权;
  • G: 指定限长的颗粒度。0: 表示颗粒度大小以字节描述;1: 颗粒度大小以 4 KB 描述;
  • LIMIT: 段寄存器的限长,在其中由两部分组成了一个 20 位长度的字段。该字段依赖于 G 标志位,G=0 : 此处段限长为 1B ~ 1MBG=1: 此处段限长为 4KB ~ 4GB
  • P: P=1: 表示段已在内存中,若段寄存器加载一个不在内存中的段描述符 (P=0) 将会除法 #NP 异常。
  • S: 指定段描述符的类型,1: 代码或数据段;0: 系统段;
  • Type: 指定段/门描述符的类型。

其中 Type 表示描述符类型,用由 4 位组成,这 4 位的排列组合可描述不同功能的代码段和数据段,具体描述如下图。

在这里插入图片描述

7. 创建临时 GDT 结构——切换前的准备阶段

正如上文所说在切换到保护模式前,需要创建一些保护模式使用的数据结构。

7.1. 代码实现

创建临时 GDT 表具体代码如下:

[SECTION gdt]

LABEL_GDT:                      dd              0, 0
LABEL_DESC_CODE32:      		dd              0x0000FFFF, 0x00CF9A00
LABEL_DESC_DATA32:      		dd              0x0000FFFF, 0x00CF9200

GdtLen                          equ             $ - LABEL_GDT
GdtPtr                          dw              GdtLen - 1
                                dd              LABEL_GDT

SelectorCode32          		equ             LABEL_DESC_CODE32 - LABEL_GDT
SelectorData32          		equ             LABEL_DESC_DATA32 - LABEL_GDT

7.2. 各标识符的描述

这里首先定义了用于存储 GDTLABEL_GDT、代码段 LABEL_DESC_CODE32 = 0x00CF9A00_0000FFFF、数据段 LABEL_DESC_DATA32 = 0x00CF9200_0000FFFF

代码段 LABEL_DESC_CODE32 = 0x00CF9A00_0000FFFF,根据段各字段描述的信息可分析到如下内容:

  • 基地址(Base Address): 0x0000_0000;
  • 颗粒度(G): 值为 1,表示颗粒度大小为 4KB
  • 限长(LIMIT): 值为 0xF_FFFF,依赖于 G 计算,(0xF_FFFF + 1) * 4KB = 4GB
    [注]:由于寻址起始为 0x00,因此这里需要加 1 才可计算出正确结果。

数据段 LABEL_DESC_DATA32 = 0x00CF9200_0000FFFF 同理可得出一样的结果,两者不同的只有 Type 字段而已。

这里定义的标识符 GdtPtr 将会配合 LGDT 指令使用,用来加载 GDT 表。GdtPtr 低位定义了一个长度为单字 dw (16 bits) 的限长,在高位定义了一个长度为双字 dd (32 bits) 的 GDT 线性基地址。在上一篇文章中便已经使用到了 GdtPtr,具体是使用 LGDTGDT 表地址加载到 GDTR,然后将数据段的寻址限长设为 (0xF_FFFF + 1) * 4 KB = 4GB,因此 FS 段寄存器便可寻址到 4GB 大小的内存空间。

这里的 SelectorCode32SelectorData32 分别是代码段和数据段的段选择器,也常被称作段选择子。

8. 为 IDT 开辟空间

从上文我们了解到,除了需要创建 GDT 表结构外,还需要创建一个 IDT 表。

8.1. 代码实现

;====== tmp IDT
IDT:
        times           0x50    dq              0
IDT_END:

IDT_POINTER:
        dw                      IDT_END - IDT - 1
        dd                      IDT

此处代码仅仅是为了 IDT 表开辟所需空间。该表是作用于中断处理,由于切换保护模式前使用 CLI 禁止中断,因此整个过程不会产生中断和异常,因此仅开辟出空间不必完全初始化也是可以的。

9. 切换到保护模式

处理器主要通过置位 CR0 的第 0PE 切换到保护模式,到目前为止我们切换到保护模式之前所要求的准备工作已经完成,现在只需要按照正确的步骤来切换即可。

根据 Intel 白皮书卷三所述,从实模式切换到保护模式为了确保 Intel 64IA-32 处理器的向上和向下代码兼容性,建议我们遵循以下步骤:

  • 1. 使用 CLI 指令屏蔽硬件中断,对于不可屏蔽中断 NMI 只能借助外部电路才能禁止;(软件必须保证在运行过程中不会产生异常或中断模式切换操作。)
  • 2. 执行 LGDT 指令将 GDT 的基地址装入 GDTR 寄存器;
  • 3. 执行 MOV CR0 指令,设置控制寄存器 CR0 中的 PE 标志(以及可选的 PG 标志);
  • 4. 紧跟 MOV CR0 指令,执行远跳转 far JMP 或远调用 far CALL 指令。 (这个操作是通常是远跳转或调用指令流中的下一条指令。)
  • 5. 通过执行 JMPCALL 指令,改变处理器的执行流水线,进而使处理器加载执行保护模式的代码段;
  • 6. 如果启用分页,MOV CR0 指令和 JMPCALL 指令的代码必须来自一个标识映射的页面(即启用分页和保护模式后跳转前的线性地址与物理地址相同)。 JMPCALL 指令的目标指令不需要进行身份映射。
  • 7. 如需使用 LDT,则必须借助 LLDT 指令将 GDT 内的 LDT 段选择子加载到 LDTR 寄存器中;
  • 8. 执行 LTR 指令,将带有段选择器的任务寄存器加载到初始保护模式任务中或可用于在任务切换上存储 TSS 信息的可写内存区域;
  • 9. 进入保护模式后,数据段寄存器仍旧保留实模式的段数据,必须重新加载数据段选择子或使用 JMP/CALL 指令执行新任务,便可将其更新为保护模式。(在第 4 步改变 CR0 寄存器并执行远跳指令,已经将代码段寄存器更新到保护模式)对于不使用的数据段寄存器(DSSS 寄存器除外),可将 NULL 段选择子加载到其中;
  • 10. 执行 LIDT 指令,将保护模式下的 IDT 表的基地址和长度加载到 IDTR 寄存器;
  • 11. 执行 STI 指令,使能硬件中断,并执行必要的硬件操作使能 NMI 不可屏蔽中断。

9.1. 代码实现

;====== tmp IDT
IDT:
        times           		0x50    dq              0
IDT_END:

IDT_POINTER:
        dw                      IDT_END - IDT - 1
        dd                      IDT

;====== init IDT GDT goto protect mode

        cli
        db                      0x66
        lgdt            		[GdtPtr]

   		;db                  	0x66
   		;lidt                	[IDT_POINTER]

        mov                     eax,    cr0
        or                      eax,    1
        mov                     cr0,    eax

        jmp                     dword   SelectorCode32:GO_TO_TMP_Protect

该代码执行加载描述表指令之前均插入一个字节 db 0x66,这个字节是 LGDTLIDT 指令的前缀,用于修饰当前指令,表示其操作数是 32 位宽的。最后一条远跳指令已明确跳转的目标段选择器和段内偏移地址。

9.2. 简单验证

在上文代码远跳指令前加入 JMP $ 将程序停下来。

     mov                     cr0,    eax
	 jmp					 $	; test

     jmp                     dword   SelectorCode32:GO_TO_TMP_Protect

运行 bochs 观察段寄存器 CS
在这里插入图片描述
此时 CS 的基地址和限长分别为 0x10000x0000FFFF,属性为可读写可访问的。

再来验证远跳之后,在其后实现简单的 GO_TO_TMP_Protect

GO_TO_TMP_Protect:
	jmp					$

然后运行 bochs 查看当前段寄存器的信息,观察 CS

在这里插入图片描述
这个时候的 CS 基地址和限长分别为 0x000000000xFFFFFFFF,属性为可执行可读,32 位模式。

10. 切换到 IA-32e 模式

根据 Intel 官方白皮书的介绍,进入 IA-32e 模式时处理器必须处于保护模式。 IA32_EFER 寄存器(位于 MSR 寄存器组内)的 LME 标志位用于控制 IA-32e 模式的使能或关闭,该寄存器会伴随着处理器的重启而清零。IA-32e 模式的也管理机制将物理地址扩展为四层页表结构。在 IA-32e 模式激活前(CR0.PG = 1,处理器运行在 32 位兼容模式),CR3 控制寄存器仅有低 32 位可写入数据,从而限制页表只能寻址 4GB 物理内存空间,即在初始化 IA-32e 模式时,分页机制只能使用前 4GB 物理地址空间。当激活了 IA-32e 模式后,软件便可重定位页表到物理内存空间的任何地方。

Intel 对切换到 IA-32e 模式的步骤是这样描述的:

  • 1. 在保护模式下,使用 MOV CR0 指令复位 CR0 寄存器的 PG 标志位,从而关闭分页机制;
  • 2. 通过设置 CR4.PAE = 1 启用物理地址扩展(PAE)。如果 PAE 功能开启失败,将会产生通用保护性异常 #GP
  • 3. 将页目录(顶层页表 PML4)的物理基地址加载到 CR3 控制寄存器中;
  • 4. 置为 IA-32_EFER 寄存器的 LME 标志位,开启 IA-32e 模式;
  • 5. 设置 CR0.PG = 1 开启分页机制,此时处理器会自动置位 IA32_EFER 寄存器的 LMA 标志位。处理器会进行 64 位模式的一致性检测,以确保处理器不会进入未定义模式或不可预测的运行状态。如果一致性检测失败,处理器将会产生通用保护性异常 #PG

64 位模式一致性检查会在以下几种情况下失败:

  • 1. 当开启分页机制后,再试图使能或禁止 IA-32e 模式;
  • 2. 当开启 IA-32e 模式后,试图在开启物理地址扩展(PAE)功能前使能分页机制;
  • 3. 在激活 IA-32e 模式后,试图禁止物理地址扩展(PAE);
  • 4.CS 段寄存器的 L 位被置位时,再试图激活 IA-32e 模式;
  • 5.TR 寄存器加载的是 16TSS 段结构。

10.1. 创建 IA-32e 模式临时 GDT 表结构

代码实现如下:

[SECTION gdt64]

LABEL_GDT64:            dq              0x0000000000000000
LABEL_DESC_CODE64:      dq              0x0020980000000000
LABEL_DESC_DATA64:      dq              0x0000920000000000

GdtLen64                equ             $ - LABEL_GDT64
GdtPtr64                dw              GdtLen64 - 1
                        dd              LABEL_GDT64

SelectorCode64          equ             LABEL_DESC_CODE64 - LABEL_GDT64
SelectorData64          equ             LABEL_DESC_DATA64 - LABEL_GDT64

此处的代码和之前的创建 32 位的 GDT 基本相同。

10.2. 切换到 IA-32e 模式

当准备好段结构后,即可开始着手切换到保护模式。具体代码如下:

[SECTION .s32]
[BITS 32]


GO_TO_TMP_Protect:

;====== go to tmp long mode

        mov             ax,     0x10
        mov             ds,     ax
        mov             es,     ax
        mov             fs,     ax
        mov             ss,     ax
        mov             esp,    0x7E00

        call            support_long_mode
        test            eax,    eax

        jz              no_support


;       jmp             $

;====== test support long mode or not

support_long_mode:

        mov             eax,    0x80000000
        cpuid
        cmp             eax,    0x80000001
        setnd           al
        jb              support_long_mode_done
        mov             eax,    0x80000001
        cpuid
        bt              edx,    29
        setc            al

support_long_mode_done:

        movzx           eax,    al
        ret

;====== no support

no_support:

        jmp            $

进入保护模式后,首先要做的就是初始化各个段寄存器以及栈指针,这里的代码的作用便是如此。

10.3. 检测处理器是否支持 IA-32e 模式

除了需要初始化各个段寄存器和栈指针外,还有一个重要的工作是检测处理器是否支持 IA-32e 模式(也称长模式)。若不支持 IA-32e 模式则进入待机状态,将不做任何操作。具体代码如下:

support_long_mode:

        mov             eax,    0x80000000
        cpuid
        cmp             eax,    0x80000001
        setnd           al
        jb              support_long_mode_done
        mov             eax,    0x80000001
        cpuid
        bt              edx,    29
        setc            al

support_long_mode_done:

        movzx           eax,    al
        ret

;====== no support

no_support:

        jmp            $

CPUID 指令的扩展功能项 0x80000001,即 EAX = 0x80000001 功能位,其中第 29 位用来指示处理器是否支持 IA-32e 模式,这段代码便是通过检查当前处理器对 CPUID 指令的支持情况,判断该指令的最大扩展功能号是否超过 0X8000000。若扩展功能号大于等于 0x80000001 才表示支持 64 位架构模式。

EFLAGS 寄存器的第 21 位为 ID 标志位,表明处理器是否支持 CPUID 指令。如果程序可以操作此标志位,则表示处理器支持 CPUID 指令。(CPUID 指令在 64 位下和 32 位下的执行效果相同)

CPUID 指令会根据 EAX 寄存器传入的基础功能号查询处理器相关信息,并将其结果保存在 EAXEBXECXEDX 中。

在这里插入图片描述
有关 CPUID 的更多详情可在此处查看 https://www.felixcloutier.com/x86/cpuid

10.4. 配置临时页目录项和页表项

当检测到处理器支持 IA-32e 模式后,接着应为 IA-32e 模式配置临时页目录项和页表项,具体代码如下:

;====== init template page table 0x90000

        mov                     dword   [0x90000],      0x91007
        mov                     dword   [0x90004],      0x00000
        mov             		dword   [0x90800],  	0x91007
        mov                     dword   [0x90804],  	0x00000

        mov                     dword   [0x91000],      0x92007
        mov                     dword   [0x91004],      0x00000

        mov                     dword   [0x92000],      0x000083
        mov                     dword   [0x92004],  	0x000000
        mov                     dword   [0x92008],      0x200083
        mov                     dword   [0x9200C],  	0x000000
        mov                     dword   [0x92010],  	0x400083
        mov                     dword   [0x92014],  	0x000000
        mov                     dword   [0x92018],  	0x600083
        mov                     dword   [0x9201C],  	0x000000
        mov                     dword   [0x92020],      0x800083
        mov                     dword   [0x92024],  	0x000000
        mov                     dword   [0x92028],  	0xA00083
        mov                     dword   [0x9202C],      0x000000

此段代码将 IA-32e 模式下的页目录首地址设置在 0x90000 地址处,并相继配置各级页表项的值(该值由页表起始地址和页属性组成)。

本文更新中…

猜你喜欢

转载自blog.csdn.net/qq_36393978/article/details/126546725