该文系列文章为记录作者学习并摘自于彭东的操作系统实战45讲的笔记https://time.geekbang.org/column/article/369502
04 | 震撼的Linux全景图:业界成熟的内核架构长什么样?
关于linus
Linus Benedict Torvalds,这个名字很长,下面简称 Linus,他 1969 年 12 月 28 日出生在芬兰的赫尔辛基市,并不是美国人。Linus 非常具有黑客精神,后来有了自己的计算机更是痴迷其中,开始自己控制计算机做一些事情,并深挖其背后的原理。就是这种黑客精神促使他后来写出了颠覆世界的软件——Linux,也因此登上了美国《时代》周刊。
Linux 内核
支持类 UNIX、POSIX 标准接口,也支持多用户、多进程、多线程,可以在多 CPU 的机器上运行。
Linux 的基本思想是一切都是文件,它是数据的容器,Linux 发展到今天其代码量近 2000 万行,我们可以先看看 Linux 内部的全景图,从全局了解一下 Linux 的内部结构,如下图。
上图仍然不足于描述 Linux 的全部,只是展示了重要且显而易见的部分。上图中大致分为五大重要组件,每个组件又分成许多模块从上到下贯穿各个层次,每个模块中有重要的函数和数据结构。具体每个模块的主要功能,你可以详细看看后面这张图。
Linux 这么多模块挤在一起,之间的通信主要是函数调用,而且函数间的调用没有一定的层次关系,更加没有左右边界的限定。
这些纵横交错的路径上有一个函数出现了问题,就麻烦大了,它会波及到全部组件,导致整个系统崩溃,模块之间没有隔离,安全隐患也是巨大的。
当然,这种结构不是一无是处,它的性能极高,而性能是衡量操作系统的一个重要指标。这种结构就是传统的内核结构,也称为宏内核架构。
下面我们就拿出 Windows 和 macOS 进行对比,注意我们只是对比它们的内核架构。
Darwin-XNU 内核
Darwin 是由苹果公司在 2000 年开发的一个开放源代码的操作系统。Darwin 作为 macOS 与 iOS 操作系统的核心,从技术实现角度说,它必然要支持 PowerPC、x86、ARM 架构的处理器。
Darwin 使用了一种微内核(Mach)和相应的固件来支持不同的处理器平台,并提供操作系统原始的基础服务,上层的功能性系统服务和工具则是整合了 BSD 系统所提供的。苹果公司还为其开发了大量的库、框架和服务,不过它们都工作在用户态且闭源。下面我们先从整体看一下 Darwin 的架构。
显然它有两个内核层——Mach 层与 BSD 层。
Mach 内核是卡耐基梅隆大学开发的经典微内核,意在提供最基本的操作系统服务,从而达到高性能、安全、可扩展的目的,而 BSD 则是伯克利大学开发的类 UNIX 操作系统,提供一整套操作系统服务。
MAC OS X(2011 年之前的称呼)的发展经过了不同时期,随着时代的进步,产品功能需求增加,单纯的 Mach 之上实现出现了性能瓶颈,但是为了兼容之前为 Mach 开发的应用和设备驱动,就保留了 Mach 内核,同时加入了 BSD 内核。
Mach 内核仍然提供十分简单的进程、线程、IPC 通信、虚拟内存设备驱动相关的功能服务,BSD 则提供强大的安全特性,完善的网络服务,各种文件系统的支持,同时对 Mach 的进程、线程、IPC、虚拟内核组件进行细化、扩展延伸。
那么应用如何使用 Darwin 系统的服务呢?应用会通过用户层的框架和库来请求 Darwin 系统的服务,即调用 Darwin 系统 API。在调用 Darwin 系统 API 时,会传入一个 API 号码,用这个号码去索引 Mach 陷入中断服务表中的函数。此时,API 号码如果小于 0,则表明请求的是 Mach 内核的服务,API 号码如果大于 0,则表明请求的是 BSD 内核的服务,它提供一整套标准的 POSIX 接口。
Mach 中还有一个重要的组件 Libkern,它是一个库,提供了很多底层的操作函数,同时支持 C++ 运行环境。
依赖这个库的还有 IOKit,IOKit 管理所有的设备驱动和内核功能扩展模块。驱动程序开发人员则可以使用 C++ 面向对象的方式开发驱动,这个方式很优雅,你完全可以找一个成熟的驱动程序作为父类继承它,要特别实现某个功能就重载其中的函数,也可以同时继承其它驱动程序,这大大节省了内存,也大大降低了出现 BUG 的可能。
Windows NT 内核
接下来我们再看下 NT 内核。现代 Windows 的内核就是 NT,我们不妨先看看 NT 的历史。
如果你是 90 后,大概没有接触过 MS-DOS,它的交互方式是你在键盘上输入相应的功能命令,它完成相应的功能后给用户返回相应的操作信息,没有图形界面。在 MS-DOS 内核的实现上,也没有应用现代硬件的保护机制,这导致后来微软基于它开发的图形界面的操作系统,如 Windows 3.1、Windows95/98/ME,极其不稳定,且容易死机。
加上类 UNIX 操作系统在互联网领域大行其道,所以微软急需一款全新的操作系统来与之竞争。所以,Windows NT 诞生了。
现在,NT 内核在设计上层次非常清晰明了,各组件之间界限耦合程度很低。下面我们就来看看 NT 内核架构图,了解一下 NT 内核是如何“庄严宏伟”。如下图:
Linux 性能良好,结构异常复杂,不利于问题的排查和功能的扩展,而 Darwin-XNU 和 Windows 结构良好,层面分明,利于功能扩展,不容易产生问题且性能稳定。
ps:window NT是混合内核
05 | CPU工作模式:执行程序的三种模式
按照 CPU 功能升级迭代的顺序,CPU 的工作模式有实模式、保护模式、长模式,这几种工作模式下 CPU 执行程序的方式截然不同,下面我们一起来探讨这几种工作模式。
实模式
实,即真实,这个真实分为两个方面,一个方面是运行真实的指令,对指令的动作不作区分,直接执行指令的真实功能,另一方面是发往内存的地址是真实的,对任何地址不加限制地发往内存。
实模式寄存器
指令的操作数,可以是寄存器、内存地址、常数,其实通常情况下是寄存器,AX、CX 就是 x86 CPU 中的寄存器。
下面我们就去看看 x86 CPU 在实模式下的寄存器。表中每个寄存器都是 16 位的。
实模式下访问内存
虽然有了寄存器,但是数据和指令都是存放在内存中的。通常情况下,需要把数据装载进寄存器中才能操作,还要有获取指令的动作,这些都要访问内存才行,而我们知道访问内存靠的是地址值。
那问题来了,这个值是如何计算的呢?计算过程如下图。
地址 = CS(段寄存器) 16 + IP(程序指针寄存器) 1.为啥是 16 , 因为左移 4 位, 2的4次方就是16 2.为啥是左移4位 ,本来寄存器16位,左移4位就刚好20位, 和地址总线宽度一致。 注意:有可能 两种CS + IP组合 表示 同一个地址,例如, (注意这里H是表示16进制的意思) 情况1 : CS = 1000H, IP = 0010H 情况2 : CS = 1001H, IP = 0000H 根据公式 CS(段寄存器) * 16 + IP(程序指针寄存器) 情况1 和情况2 的地址是一样的。
结合上图可以发现,所有的内存地址都是由段寄存器左移 4 位,再加上一个通用寄存器中的值或者常数形成地址,然后由这个地址去访问内存。这就是大名鼎鼎的分段内存管理模型。只不过这里要特别
只不过这里要特别注意的是,代码段是由 CS 和 IP 确定的,而栈段是由 SS 和 SP 段确定的。也就是说只要给这四个寄存器分配值就可以完成对不同的内存进行分配
下面是一个 DOS 下的 Hello World 应用程序,这是一个工作在实模式下的汇编代码程序,一共 16 位,具体代码如下:
data SEGMENT ;定义一个数据段存放Hello World!
hello DB 'Hello World!$' ;注意要以$结束
data ENDS
code SEGMENT ;定义一个代码段存放程序指令
ASSUME CS:CODE,DS:DATA ;告诉汇编程序,DS指向数据段,CS指向代码段
start:
MOV AX,data ;将data段首地址赋值给AX
MOV DS,AX ;将AX赋值给DS,使DS指向data段
LEA DX,hello ;使DX指向hello首地址
MOV AH,09h ;给AH设置参数09H,AH是AX高8位,AL是AX低8位,其它类似
INT 21h ;执行DOS中断输出DS指向的DX指向的字符串hello
MOV AX,4C00h ;给AX设置参数4C00h
INT 21h ;调用4C00h号功能,结束程序
code ENDS
END start
上述代码中的结构模型,也是符合 CPU 实模式下分段内存管理模式的,它们被汇编器转换成二进制数据后,也是以段的形式存在的。
实模式中断
中断即中止执行当前程序,转而跳转到另一个特定的地址上,去运行特定的代码。在实模式下它的实现过程是先保存 CS 和 IP 寄存器(IP 是指令指针寄存器,它只和CS一起使用,而且只有处理器才能直接改变它的内容。当一段代码开始执行时,CS指向代码段的起始地址,IP指向段内偏移。这样CS和IP形成了逻辑地址,由总线接口部件变换成物理地址来取得指令。处理器会自动根据当前指令的长度改变IP值,使它指向下一条指令。),然后装载新的 CS 和 IP 寄存器,那么中断是如何产生的呢?
第一种情况是,中断控制器给 CPU 发送了一个电子信号,CPU 会对这个信号作出应答。随后中断控制器会将中断号发送给 CPU,这是硬件中断。
硬件中断,是通过两根信号线引入处理器的。一根是intr(interrupt require)负责可屏蔽中断,一根nmi(nonmaskable interrupt)负责不可屏蔽中断。对于不可屏蔽中断,处理器接收就放弃正常工作也不修复。对于可屏蔽中断,intel处理器允许有256个中断,中断号0~255。中断控制器由两个8259a芯片级联,可管理15个中断信号。对于如何屏蔽中断和不屏蔽,由8259a芯片和处理器共同作用。8259a芯片内部有一个8位中断屏蔽寄存器,对应芯片的8个输入引脚,0允许,1表示屏蔽。除了芯片还要看cpu内部标志寄存器中IF位,为0,所有来自intr中的中断都被忽略,为1正常接收和处理。
第二种情况就是 CPU 执行了 INT 指令,这个指令后面会跟随一个常数,这个常数即是软中断号。这种情况是软件中断。
为了实现中断,就需要在内存中放一个中断向量表,这个表的地址和长度由 CPU 的特定寄存器 IDTR 指向。实模式下,表中的一个条目由代码段地址和段内偏移组成,如下图所示。
有了中断号以后,CPU 就能根据 IDTR 寄存器中的信息,计算出中断向量中的条目,进而装载 CS(装入代码段基地址)、IP(装入代码段内偏移)寄存器,最终响应中断。
保护模式
随着软件的规模不断增加,需要更高的计算量、更大的内存容量。
内存一大,首先要解决的问题是寻址问题,因为 16 位的寄存器最多只能表示 64kb 大小的地址,所以 CPU 的寄存器和运算单元都要扩展成 32 位的(4G)。
不过,虽然扩展 CPU 内部器件的位数解决了计算和寻址问题,但仍然没有解决前面那个实模式场景下的问题(写死循环清空整个内存的指令),导致前面场景出问题的原因有两点。第一,CPU 对任何指令不加区分地执行;第二,CPU 对访问内存的地址不加限制。
基于这些原因,CPU 实现了保护模式(对CPU进行抽象虚拟化处理)。保护模式是如何实现保护功能的呢?我们接着往下看。
保护模式寄存器
保护模式相比于实模式,增加了一些控制寄存器和段寄存器,扩展通用寄存器的位宽,所有的通用寄存器都是 32 位的,还可以单独使用低 16 位,这个低 16 位又可以拆分成两个 8 位寄存器,如下表。
其实就是多了CR0 CR1 CR2 CR3 以及除了段寄存器都32位还增加了两个段寄存器
保护模式特权级
为了区分哪些指令(如 in、out、cli)和哪些资源(如寄存器、I/O 端口、内存地址)可以被访问,CPU 实现了四个特权级。
R0~R3,每个特权级执行指令的数量不同,R0 可以执行所有指令,R1、R2、R3 依次递减,它们只能执行上一级指令数量的子集。而内存的访问则是靠后面所说的段描述符和特权级相互配合去实现的。
保护模式段描述符
目前为止,内存还是分段模型,要对内存进行保护,就可以转换成对段的保护。
由于 CPU 的扩展导致了 32 位的段基地址和段内偏移,还有一些其它信息,所以 16 位的段寄存器肯定放不下。放不下就要找内存借空间,然后把描述一个段的信息封装成特定格式的段描述符,放在内存中,其格式如下。(因为段寄存器只有16位,不能存放32位的段基地址:偏移地址+其他信息;于是将段信息封装成特定格式(段描述符,64位)后经过数据总线存入内存,形成段描述符表;该表的基地址和长度由 GDTR 寄存器指示,可通过GDTR 寄存器+CS:IP等段寄存器(存的段描述符的索引)得到段描述符所在的内存地址。)
一个段描述符有 64 位 8 字节数据,里面包含了段基地址、段长度、段权限、段类型(可以是系统段、代码段、数据段)、段是否可读写,可执行等。虽然数据分布有点乱,这是由于历史原因造成的。 (ps:段基址和段长度不连续存放)
多个段描述符在内存中形成全局段描述符表,该表的基地址和长度由 CPU 和 GDTR 寄存器决定。如下图所示。
GDTR寄存器存储全局段描述符表GDT的地址和长度 段寄存器保存段描述符索引 所以保护模式下的内存地址访问如下: 1. 从GDTR中找到GDT的地址 2. 从段寄存器中找到要访问的段描述符索引 3. 用该描述符索引去GDT中找到具体的段描述符 4. 根据描述符中的段信息判断能否访问成功(ps:段描述符中有两个位(DPL)描述这个段的特权级别(ring0-3),段寄存器也有两个位(RPL)描述应用程序的特权级别。如果段寄存器中的特权级别 高于 段描述符的特权级别(RPL<DPL),那么允许访问,否则拒绝访问。这样就实现了对数据的保护)
保护模式段选择子
CS、DS、ES、SS、FS、GS 这些段寄存器,其实它们是由影子寄存器、段描述符索引、描述符表索引、权限级别组成的。如下图所示。
上图中影子寄存器(arm特有)是靠硬件来操作的,对系统程序员不可见,是硬件为了减少性能损耗而设计的一个段描述符的高速缓存,不然每次内存访问都要去内存中查表,那性能损失是巨大的,影子寄存器也正好是 64 位,里面存放了 8 字节段描述符数据。
低三位之所以能放 TI 和 RPL,是因为段描述符 8 字节对齐,每个索引低 3 位都为 0,我们不用关注 LDT,只需要使用 GDT 全局描述符表,所以 TI 永远设为 0。(这部分的意思是指,因为段描述符8字节对齐,所以它的索引都是8的整数倍,因此低3位都为0。类似1000,10000,11000这样的。所以低3位可以用来做其他事,这里就用来放TI和RPL。)
保护模式平坦模型
分段模型有很多缺陷,这在后面课程讲内存管理时有详细介绍,其实现代操作系统都会使用分页模型(这点在后面讲 MMU 那节课再探讨)
但是 x86 CPU 并不能直接使用分页模型,而是要在分段模型的前提下,根据需要决定是否要开启分页。因为这是硬件的规定,程序员是无法改变的。但是我们可以简化设计,来使分段成为一种“虚设”,这就是保护模式的平坦模型。
根据前面的描述,我们发现 CPU32 位的寄存器最多只能产生 4GB 大小的地址,而一个段长度也只能是 4GB,所以我们把所有段的基地址设为 0,段的长度设为 0xFFFFF,段长度的粒度设为 4KB,这样所有的段都指向同一个((段的长度 +1)* 粒度 - 1)字节大小的地址空间。
下面我们还是看一看前面 Hello OS 中段描述符表,如下所示。
GDT_START:
knull_dsc: dq 0
;第一个段描述符CPU硬件规定必须为0
kcode_dsc: dq 0x00cf9e000000ffff
;段基地址=0,段长度=0xfffff
;G=1,D/B=1,L=0,AVL=0
;P=1,DPL=0,S=1
;T=1,C=1,R=1,A=0
kdata_dsc: dq 0x00cf92000000ffff
;段基地址=0,段长度=0xfffff
;G=1,D/B=1,L=0,AVL=0
;P=1,DPL=0,S=1
;T=0,C=0,R=1,A=0
GDT_END:
GDT_PTR:
GDTLEN dw GDT_END-GDT_START-1
GDTBASE dd GDT_START
//没看很懂
上面代码中注释已经很明白了,段长度需要和 G 位配合,若 G 位为 1 则段长度等于 0xfffff 个 4KB。上面段描述符的 DPL=0,这说明需要最高权限即 CPL=0 才能访问。
保护模式中断
因为实模式下 CPU 不需要做权限检查,所以它可以直接通过中断向量表中的值装载 CS:IP 寄存器就好了。
而保护模式下的中断要权限检查,还有特权级的切换,所以就需要扩展中断向量表的信息,即每个中断用一个中断门描述符来表示,也可以简称为中断门,中断门描述符依然有自己的格式,如下图所示。(是软中断)
同样的,保护模式要实现中断,也必须在内存中有一个中断向量表,同样是由 IDTR 寄存器指向,只不过中断向量表中的条目变成了中断门描述符,如下图所示。
产生中断后,CPU 首先会检查中断号是否大于最后一个中断门描述符,x86 CPU 最大支持 256 个中断源(即中断号:0~255),然后检查描述符类型(是否是中断门或者陷阱门)、是否为系统描述符,是不是存在于内存中。
接着,检查中断门描述符中的段选择子指向的段描述符。
最后做权限检查,如果 CPL 小于等于中断门的 DPL,并且 CPL 大于等于中断门中的段选择子所指向的段描述符的 DPL,就指向段描述符的 DPL。
(CPL 小于等于中断门的 DPL,说明有权限能执行中断。 CPL 大于等于所指向的段描述符的 DPL,如果不提升 CPL,就会导致没有权限调到该内存,所以指向段描述符的 DPL。)
进一步的,CPL 等于中断门中的段选择子指向段描述符的 DPL,则为同级权限不进行栈切换,否则进行栈切换。如果进行栈切换,还需要从 TSS 中加载具体权限的 SS、ESP,当然也要对 SS 中段选择子指向的段描述符进行检查。
(每个进程都有内核栈,用户栈,这些栈的栈段寄存器ss、栈顶指针esp都存在进程描述符pcn中tss字段中,直接从中取即可。)
做完这一系列检查之后,CPU 才会加载中断门描述符中目标代码段选择子(段选择子就是段寄存器里面放的东西,指示段描述符索引和一些其他的信息,通过和CIDR结合生成段描述符)到 CS 寄存器中,把目标代码段偏移加载到 EIP 寄存器中。
切换到保护模式
x86 CPU 在第一次加电和每次 reset 后,都会自动进入实模式,要想进入保护模式,就需要程序员写代码实现从实模式切换到保护模式。切换到保护模式的步骤如下。
第一步,准备全局段描述符表,代码如下。
GDT_START:
knull_dsc: dq 0
kcode_dsc: dq 0x00cf9e000000ffff
kdata_dsc: dq 0x00cf92000000ffff
GDT_END:
GDT_PTR:
GDTLEN dw GDT_END-GDT_START-1
GDTBASE dd GDT_START
第二步,加载设置 GDTR 寄存器,使之指向全局段描述符表。
lgdt [GDT_PTR]
第三步,设置 CR0 寄存器,开启保护模式。
;开启 PE
mov eax, cr0
bts eax, 0 ; CR0.PE =1
mov cr0, eax
//bts指令的意思是bit test and set 位测试并设置 在此处的作用是:判断eax与0,若eax == 0:bts会将CF = 1,并将eax置位(置位的意思就是设置为1) 然后把CR0的最低位设为1之后,就表示开启了保护模式
第四步,进行长跳转,加载 CS 段寄存器,即段选择子。
jmp dword 0x8 :_32bits_mode ;_32bits_mode为32位代码标号即段偏移
//这里填入CS的值是0x8,根据段选择子的格式定义,0x8就翻译成: INDEX TI CPL 0000 0000 1 00 0 INDEX代表GDT中的索引,TI代表使用GDTR中的GDT, CPL代表处于特权级,即GDT表中的第1个(index从0开始)描述符。 _32bits_mode作为段内偏移,然后得 GDT第一个1段描述符 + _32bits_mode的内存地址 = 段基址 + 段内偏移地址 顺带说一句,作者用的是NASM Intel格式的汇编指令,中间的dword限定符表示要访问完整的32位地址。
为什么要进行长跳转,这是因为我们无法直接或间接 mov 一个数据到 CS 寄存器中,因为刚刚开启保护模式时,CS 的影子寄存器还是实模式下的值,所以需要告诉 CPU 加载新的段信息。(重点是,新架构下的影子寄存器只会在段寄存器接收到新的段选择子的时候才会更新。这里,因为我们修改了段描述符,所以需要重载刷新。)
接下来,CPU 发现了 CRO 寄存器第 0 位的值是 1,就会按 GDTR 的指示找到全局描述符表,然后根据索引值 8,把新的段描述符信息加载到 CS 影子寄存器,当然这里的前提是进行一系列合法的检查。
到此为止,CPU 真正进入了保护模式,CPU 也有了 32 位的处理能力。
长模式
长模式又名 AMD64,因为这个标准是 AMD 公司最早定义的,它使 CPU 在现有的基础上有了 64 位的处理能力,既能完成 64 位的数据运算,也能寻址 64 位的地址空间。这在大型计算机上犹为重要,因为它们的物理内存通常有几百 GB。
长模式寄存器
长模式相比于保护模式,增加了一些通用寄存器,并扩展通用寄存器的位宽,所有的通用寄存器都是 64 位,还可以单独使用低 32 位。
这个低 32 位可以拆分成一个低 16 位寄存器,低 16 位又可以拆分成两个 8 位寄存器,如下表。
长模式段描述符
长模式依然具备保护模式绝大多数特性,如特权级和权限检查。相同的部分就不再重述了,这里只会说明长模式和保护模式下的差异。
在长模式下,CPU 不再对段基址和段长度进行检查,只对 DPL 进行相关的检查,这个检查流程和保护模式下一样。
当描述符中的 L=1,D/B=0 时,就是 64 位代码段,DPL 还是 0~3 的特权级。然后有多个段描述在内存中形成一个全局段描述符表,同样由 CPU 的 GDTR 寄存器指向。
下面我们来写一个长模式下的段描述符表,加深一下理解,如下所示
ex64_GDT:
null_dsc: dq 0
;第一个段描述符CPU硬件规定必须为0
c64_dsc:dq 0x0020980000000000 ;64位代码段
;无效位填0
;D/B=0,L=1,AVL=0
;P=1,DPL=0,S=1
;T=1,C=0,R=0,A=0
d64_dsc:dq 0x0000920000000000 ;64位数据段
;无效位填0
;P=1,DPL=0,S=1
;T=0,C/E=0,R/W=1,A=0
eGdtLen equ $ - null_dsc ;GDT长度
eGdtPtr:dw eGdtLen - 1 ;GDT界限
dq ex64_GDT
段长度和段基址都是无效的填充为 0,CPU 不做检查。但是上面段描述符的 DPL=0,这说明需要最高权限即 CPL=0 才能访问。若是数据段的话,G、D/B、L 位都是无效的。
长模式中断
保护模式下为了实现对中断进行权限检查,实现了中断门描述符,在中断门描述符中存放了对应的段选择子和其段内偏移,还有 DPL 权限,如果权限检查通过,则用对应的段选择子和其段内偏移装载 CS:EIP 寄存器。
如果你还记得中断门描述符,就会发现其中的段内偏移只有 32 位,但是长模式支持 64 位内存寻址,所以要对中断门描述符进行修改和扩展,下面我们就来看看长模式下的中断门描述符的格式,如下图所示。
结合上图,我们可以看出长模式下中断门描述符的格式变化。
首先为了支持 64 位寻址中断门描述符在原有基础上增加 8 字节,用于存放目标段偏移的高 32 位值。其次,目标代码段选择子对应的代码段描述符必须是 64 位的代码段。最后其中的 IST 是 64 位 TSS 中的 IST 指针,因为我们不使用
这个特性,所以不作详细介绍。
切换到长模式
我们既可以从实模式直接切换到长模式,也可以从保护模式切换长模式。切换到长模式的步骤如下。
第一步,准备长模式全局段描述符表。
ex64_GDT:
null_dsc: dq 0
;第一个段描述符CPU硬件规定必须为0
c64_dsc:dq 0x0020980000000000 ;64位代码段
d64_dsc:dq 0x0000920000000000 ;64位数据段
eGdtLen equ $ - null_dsc ;GDT长度
eGdtPtr:dw eGdtLen - 1 ;GDT界限
dq ex64_GDT
第二步,准备长模式下的 MMU 页表,这个是为了开启分页模式,切换到长模式必须要开启分页,想想看,长模式下已经不对段基址和段长度进行检查了,那么内存地址空间就得不到保护了。
而长模式下内存地址空间的保护交给了 MMU,MMU 依赖页表对地址进行转换,页表有特定的格式存放在内存中,其地址由 CPU 的 CR3 寄存器指向,这在后面讲 MMU 的那节课会专门讲。
而长模式下内存地址空间的保护交给了 MMU,MMU 依赖页表对地址进行转换,页表有特定的格式存放在内存中,其地址由 CPU 的 CR3 寄存器指向,这在后面讲 MMU 的那节课会专门讲。
mov eax, cr4
bts eax, 5 ;CR4.PAE = 1
mov cr4, eax ;开启 PAE
mov eax, PAGE_TLB_BADR ;页表物理地址
mov cr3, eax
3.加载 GDTR 寄存器,使之指向全局段描述表:
lgdt [eGdtPtr]
开启长模式,要同时开启保护模式和分页模式,在实现长模式时定义了 MSR 寄存器,需要用专用的指令 rdmsr、wrmsr 进行读写,IA32_EFER 寄存器的地址为 0xC0000080,它的第 8 位决定了是否开启长模式。
开启长模式,要同时开启保护模式和分页模式,在实现长模式时定义了 MSR 寄存器,需要用专用的指令 rdmsr、wrmsr 进行读写,IA32_EFER 寄存器的地址为 0xC0000080,它的第 8 位决定了是否开启长模式。
进行跳转,加载 CS 段寄存器,刷新其影子寄存器。
jmp 08:entry64 ;entry64为程序标号即64位偏移地址
切换到长模式和切换保护模式的流程差不多,只是需要准备的段描述符有所区别,还有就是要注意同时开启保护模式和分页模式。原因在上面已经说明了。