本文是 《【实现操作系统 04】完善 Loader 程序,并加载内核(上)》的下半部分,文章具有耦合度较强,建议先阅读上半部文章再选择阅读此文章。
1. IA-32e Mode
为了对操作系统提供更加广泛支持和系统软件开发,Intel
从 Intel 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~264,16777216 TB),同时还扩展了页管理单元的组织结构和页面大小,推出新的系统调用方式和高级可编程中断控制器。
所有的 Intel 64
(IA-32e
) 和 IA-32
处理器在上电或复位后会进入实模式,若需要切换到 IA-32e
模式操作,则首先需要从实模式切换到保护模式 (Protected Mode
) ,再由保护模式切换到 IA-32e
操作模式。
2. 从实模式 (Real Mode) 切换到保护模式 (Protected Mode)
处理器在切换到保护模式之前,为了支持处理器可靠的运行保护模式,需要在内存中创建一段用于加载保护模式的数据结构以及一段可运行在保护模式下的代码。其中相关的数据结构有:Interrupt Descriptor Table——IDT
、Global Descriptor Table——GDT
、Task-State Segments——TSS
、Local Descriptor Table——LDT
(可选),如果使用分页机制,则至少需要一个页目录和一个页表,一个或多个包含必要中断和异常处理程序的代码模块。
并且在切换之前需要初始化 GDTR (Gloal Descriptor Table Register)
寄存器,也可选的初始化 IDTR (Interrupt Descriptor Table Register)
,但在切换之前为初始化 IDTR
,那就必须在切换之后使能中断之前立马将其初始化,还需要初始化 CR0 ~ CR4
寄存器、内存类型范围寄存器 MTRR
(仅限 Pentium 4
、Intel Xeon
和 P6
系列处理器)。
当准备工作完成后,则通过将 CR0
的 PE
(Protected Enabel
) 标志位置位,即可切换到保护模式。
3. 内存管理寄存器(GDTR、LDTR、IDTR)
Intel
处理器提供了 4
个内存管理寄存器(GDTR
、LDTR
、IDTR
、TR
)来指定控制分段内存管理数据结构的位置。先来看 GDTR
和 IDTR
的结构。
[注]:个寄存器英文原名如下:
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
表需要用特殊的指令来操作,LGDT
和 SGDT
指令分别是加载和存储 GDT
表到 GDTR
寄存器。 在上电或复位处理器后,GDTR
基地址设置为默认值 0
,限长默认设置为 0FFFFh
。
在切换到保护模式之前必须用一个新的基地址初始化 GDTR
。
4. 段选择器 (Segment Selectors)
由 Index
、TI
和 RPL
三部分组成的 16
位长度的段选择器,用于索引 GDT/LDT
表中描述信息和填充段寄存器。
- Index:
Index
字段长度为13 bits
,可索引GDT/LDT
表共 213 = 8192 项,处理器会将Index
的值乘以8
作为偏移地址,并从GDTR/LDTR
中获取GDT/LDT
的段基地址,从而查找到GDT/LDT
中的段描述信息; - TI: 长度
1 bit
,指定使用的描述符表,0
:选择GDT
,1
:选择LDT
; - RPL: 长度
2 bits
,指定选择器的特权等级,特权等级分别由0 ~ 3
共有4
个,0
为最高等级,3
为最低等级。
5. 保护模式下的段寄存器
为了减少地址转换时间和编码的复杂性,处理器为保护模式提供了最多可容纳 6
个的段寄存器 CS
、DS
、SS
、ES
、FS
、GS
。
每个段寄存器都有一个“可见”部分和一个“隐藏”部分。 (隐藏部分有时被称为“描述符缓存”或“影子寄存器”。)当段选择器被加载到段的可见部分时寄存器,处理器会自动从 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 ~ 3
,0
为最高特权; - G: 指定限长的颗粒度。
0:
表示颗粒度大小以字节描述;1:
颗粒度大小以4 KB
描述; - LIMIT: 段寄存器的限长,在其中由两部分组成了一个
20
位长度的字段。该字段依赖于G
标志位,G=0 :
此处段限长为1B ~ 1MB
;G=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. 各标识符的描述
这里首先定义了用于存储 GDT
的 LABEL_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
,具体是使用 LGDT
将 GDT
表地址加载到 GDTR
,然后将数据段的寻址限长设为 (0xF_FFFF + 1) * 4 KB = 4GB
,因此 FS
段寄存器便可寻址到 4GB
大小的内存空间。
这里的 SelectorCode32
和 SelectorData32
分别是代码段和数据段的段选择器,也常被称作段选择子。
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
的第 0
位 PE
切换到保护模式,到目前为止我们切换到保护模式之前所要求的准备工作已经完成,现在只需要按照正确的步骤来切换即可。
根据 Intel
白皮书卷三所述,从实模式切换到保护模式为了确保 Intel 64
和 IA-32
处理器的向上和向下代码兼容性,建议我们遵循以下步骤:
- 1. 使用
CLI
指令屏蔽硬件中断,对于不可屏蔽中断NMI
只能借助外部电路才能禁止;(软件必须保证在运行过程中不会产生异常或中断模式切换操作。) - 2. 执行
LGDT
指令将GDT
的基地址装入GDTR
寄存器; - 3. 执行
MOV CR0
指令,设置控制寄存器CR0
中的PE
标志(以及可选的PG
标志); - 4. 紧跟
MOV CR0
指令,执行远跳转far JMP
或远调用far CALL
指令。 (这个操作是通常是远跳转或调用指令流中的下一条指令。) - 5. 通过执行
JMP
或CALL
指令,改变处理器的执行流水线,进而使处理器加载执行保护模式的代码段; - 6. 如果启用分页,
MOV CR0
指令和JMP
或CALL
指令的代码必须来自一个标识映射的页面(即启用分页和保护模式后跳转前的线性地址与物理地址相同)。JMP
或CALL
指令的目标指令不需要进行身份映射。 - 7. 如需使用
LDT
,则必须借助LLDT
指令将GDT
内的LDT
段选择子加载到LDTR
寄存器中; - 8. 执行
LTR
指令,将带有段选择器的任务寄存器加载到初始保护模式任务中或可用于在任务切换上存储TSS
信息的可写内存区域; - 9. 进入保护模式后,数据段寄存器仍旧保留实模式的段数据,必须重新加载数据段选择子或使用
JMP/CALL
指令执行新任务,便可将其更新为保护模式。(在第4
步改变CR0
寄存器并执行远跳指令,已经将代码段寄存器更新到保护模式)对于不使用的数据段寄存器(DS
和SS
寄存器除外),可将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
,这个字节是 LGDT
和 LIDT
指令的前缀,用于修饰当前指令,表示其操作数是 32
位宽的。最后一条远跳指令已明确跳转的目标段选择器和段内偏移地址。
9.2. 简单验证
在上文代码远跳指令前加入 JMP $
将程序停下来。
mov cr0, eax
jmp $ ; test
jmp dword SelectorCode32:GO_TO_TMP_Protect
运行 bochs
观察段寄存器 CS
此时 CS
的基地址和限长分别为 0x1000
,0x0000FFFF
,属性为可读写可访问的。
再来验证远跳之后,在其后实现简单的 GO_TO_TMP_Protect
GO_TO_TMP_Protect:
jmp $
然后运行 bochs
查看当前段寄存器的信息,观察 CS
这个时候的 CS
基地址和限长分别为 0x00000000
,0xFFFFFFFF
,属性为可执行可读,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
寄存器加载的是16
位TSS
段结构。
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
寄存器传入的基础功能号查询处理器相关信息,并将其结果保存在 EAX
、EBX
、ECX
和 EDX
中。
有关 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
地址处,并相继配置各级页表项的值(该值由页表起始地址和页属性组成)。
本文更新中…