x86从实模式到保护模式 pdf_从 0 实现操作系统 实模式与保护模式初探

本文介绍实模式与保护模式的基础知识与汇编代码实现。

实模式和保护模式的基础知识

在 IA32 中,CPU 有两种工作模式:实模式和保护模式。在保护模式下,CPU 有更强的寻址能力。

保护模式的起源

最开始的程序寻址是直接的“段:偏移”模式,这样的好处是所见即所得,程序员指定的地址就是物理地址,物理地址对程序员是可见的。但是,由此也带来两个问题

  1. 无法支持多任务
  2. 程序的安全性无法得到保证(用户程序可以改写系统空间或者其他用户的程序内容)

实模式将整个物理内存看成分段的区域,程序代码和数据位于不同区域,系统程序和用户程序没有区别对待,而且每一个指针都是指向"实在"的物理地址。

这样一来,用户程序的一个指针如果指向了系统程序区域或其他用户程序区域,并改变了值,那么对于这个被修改的系统程序或用户程序,其后果就很可能是灾难性的。

为了克服这种低劣的内存管理方式,处理器厂商开发出保护模式:物理内存地址不能直接被程序访问,程序内部的地址(虚拟地址)要由操作系统转化为物理地址去访问,从而保护进程地址空间,程序对此一无所知。

保护模式和实模式不同的寻址方式

从80386开始,cpu有三种工作方式:实模式,保护模式和虚拟8086模式。只有在刚刚启动的时候是real-mode,等到linux操作系统运行起来以后就运行在保护模式。虚拟8086模式是运行在保护模式中的实模式,为了在32位保护模式下执行纯16位程序。它不是一个真正的CPU模式,还属于保护模式。

两者的区别主要体现在寻址方式上。

在实模式情况下,一个地址有段和偏移两部分组成,物理地址的计算公式为:

Physical Address = Segment * 16 + Offset

对 80286 或更高系列的PC中,即使A20Gate被打开,在实模式下所能够访问的内存最大也只能为10FFEFH,尽管它们的地址总线所能够访问的能力都大大超过这个限制。为了能够访问10FFEFH以上的内存,则必须进入保护模式。

在保护模式中,仍然采用了“段+偏移”的模式。虽然段值仍然由原来的16cs/ds等寄存器指定,但此时这些寄存器中存放的不再是段基址,而是一个索引:从这个索引,可以找到一个表项,里面存放了段基址等很多属性,这个表项称为段描述符,这个表就称为GDT[1]

GDT

现在看[SECTION .gdt]这个段,这里SECTION定义了一个gdt段,SECTION并没有什么特别的语义,仅仅是一种组织代码和存储的方式,这里这样做是为了之后很好的扩展,其中的 Descriptor 是一个数据结构,是一个宏,大小为 8 字节。

bfbf9b575b9371f9388079b9c32252de.png
Descriptor 的定义

Descriptor 代码解析:

  • 第一行表示取第二个参数的0-15位(2个字节的大小)作为宏的第一、二个字节
  • 第二行表示取第一个参数的0-15位(2个字节的大小)作为宏的第三、四个字节
  • 第三行先把第一个参数右移16位(2字节)最低位就变成了之前的第16位,在和0FF求与,就是取第一个参数的16-23位,作为宏的第五个字节。
  • 第四行“|”号前半部分,参数2右移8位(1字节)最低为变为之前的第8位,和0F00求与,0F00为1的位置为第8-11位,在原始的第二个参数中就变成了16-19位((8-11)+8)。后半部分第三个参数和0F0FF求与,就是第三个参数的0-7位及12-15位,之后前后部分求或得到宏的第六七字节。
  • 第五行第一个参数先右移24位,低位变成原来的第24位,然后与0FF求与,就是第一个参数的24-31位作为第宏的第八个字节。[2]

[SECTION .gdt] 段并列定义了 3 个 Descriptor,GdtLen 表示 GDT 的长度,GdtPtr 是个小的数据结构,前两个字节表示 GDT 界限,后四个字节表示 GDT 基地址。

75664a27fd904150d261263c0eb9240a.png

这里需要补充说明一下,db、dw、dd 的含义。

  • 首先,它们都是汇编伪指令。
  • db 定义字节类型变量,一个字节数据占 1 个字节单元,读完一个,偏移量加 1 。
  • dw 定义字类型变量,一个字数据占 2 个字节单元,读完一个,偏移量加 2 。
  • dd 定义双字类型变量,一个双字数据占 4 个字节单元,读完一个,偏移量加 4 。

然后,还定义了两个 Selector

0c52c4994d04187f99c6863c516070f9.png

Intel 8086 是 16 位的 CPU,由 16 位的寄存器、16 位的数据总线,和 20 位的地址总线,以及 1MB 的寻址能力。一个地址由段和偏移两部分组成,物理地址遵循这样的计算公式:

Physical Address = Segment * 16 + Offset

其中,段值和偏移都是 16 位的。

从 80386 开始,Intel 家族的 CPU 进入 32 位时代,所以可以寻址 4GB 的空间。在 32 位的情况下,地址仍然使用“段:偏移量”的形式来表示,但“段”的概念发生了根本性的变化。在实模式下,段值还是可以看作是地址的一部分;而在保护模式下,虽然段值仍然由 16 位的 cs、ds 寄存器表示,但它实际上成了一个索引,指向数据结构中的一个表项。表项中详细定义了段的起始地址、界限、属性等内容。

这个数据结构就是 GDT,而 GDT 中的表项被称为描述符(Descriptor)。

下面示意图表示的是代码段和数据段描述符。第二部分的属性最为复杂。

1ece70748f461cbc0f36c284794fef61.png
代码段数据段描述符

本例的 GDT 共有 3 个描述符,分别是 DESC_DUMMY、DESC_CODE32 和 DESC_VIDEO。其中,DESC_VIDEO 的段基址是 0B8000h,指向的是显存。

32 位的代码分析

68773b86bb6450cdc86aada946fb0d3c.png

这里的[BITS 32]指定了这一段的代码是32位代码段,即保护模式段:

61d0212025f8ab77d2ed665d6ebc02bc.png

这两句将段寄存器gs的值变成了 SelectorVideo,由上面的解释可以看到 SelectorVideo 就是LABEL_DESC_VIDEO 相对于 GDT 的偏移,这里 GDT 的基址为 0,基本就可以认为gs现在就是对应显存的描述符 LABEL_DESC_VIDEO。

8f071422b6680fdecb30147994ea275b.png

GTD 中的每个描述符定义一个段。那么 cs、ds 等段寄存器是如何与具体的段联系起来的呢?看上去段寄存器 gs 的值变成了 SelectorVideo,而在上文中 SelectorVideo 是这样定义的:

SelectorVideo equ LABEL_DESC _VIDEO - LABEL_GDT

看上去,SelectorVideo 是 DESC_VIDEO 这个描述符相对于 GDT 基址的偏移。而实际上,SectorVideo 有一个专门的名称:选择子(Selector)。段选择子就是一个数字,一共有16位。结构如下:

78fa2e57786ec4777196e8a0398bf0bd.png
选择子结构
  • INDEX:在GDT数组或LDT数组的索引号
  • TI:Table Indicator,这个值为0表示查找GDT,1则查找LDT
  • RPL:请求特权级。以什么样的权限去访问段[3]

倘若 TI 和 RPL 都为 0,那么选择子就是对应描述符相对于 GDT 基址的偏移。

16 位实模式代码分析——从实模式到保护模式的转换

c975f5ed6fc07d9d0604b8ce18e28c3d.png
实模式代码

首先是初始化相关段寄存器和对堆栈初始化,就是对 ss 和 sp 赋值。其中,SS 存放栈的段地址;SP(stack pointer) 存放栈的偏移地址。

786f9c2203be3c10cc3512d4d989f6c1.png

从实模式到保护模式的转换由 5 个步骤组成:第一步,准备 GDT,初始化 32 位代码段描述符;第二步,用 lgdt 加载 gdtr;第三步,打开地址线 A20;第四步,置 cr0 的 PE 位;第五步,跳转,进入保护模式。

第一步中的 GDTR 是全局描述符表格寄存器;第二步的 lgdt 是一个指令,用于加载 gdtr。

eax就是ax的32位版本了。在初始化 32 位代码段描述符部分,首先将 DESC_CODE_32 的物理地址(即 [SECTION .s32] 段的物理地址)赋给 eax,然后把它分为三个部分赋给描述符 DESC_CODE_32 中的相应位置。

ac8f52f30010991f53c1cca76fe49836.png

这里的 [LABEL_DESC_CODE32 + 2] 相当于在 LABEL_DESC_CODE32 描述符之后的两个字节的位置,就是 LABEL_DESC_CODE 的三四字节,就是上图中段基址的位置,也是描述符代码中dw %1 & 0FFFFh这一行,shr eax, 16 表示 eax 右移16位,之后同理,总之就是赋值了LABEL_DESC_CODE32 描述符中段基址的部分。

49fd309a0559ad8b2f23c250e6295f25.png

第三步的打开 A20 地址线,正如在前面的知识点中讲到的,IBM 为了保持向上的兼容,使用 8042 键盘控制器控制第 20 个地址位。默认情况下,该地址线是关闭的,即第 20 个地址位总是 0。

A20地址线并不是打开保护模式的关键,只是在保护模式下,不打开A20地址线,你将无法访问到所有的内存。

这里的 in 和 out 语义分别是:

  • in DES, SRC: 从 SRC 端口读取 1 字节数据到 DES
  • OUT DES, SRC: 将 SRC 的值写入 DES 端口

把 cr0 的第 0 位置为 1,我们就进入了保护模式。cr0 的内容结构如下图所示:

b361df085560adec1c9bd080644dfe5a.png
灰色是保留位

CR0 是控制寄存器,其中包含了6个预定义标志。控制寄存器是一些特殊的寄存器,它们可以控制CPU的一些重要特性。[4]

  • 第 0 位是保护允许位 PE (Protected Enable),用于启动保护模式,如果PE位置 1,则保护模式启动,如果 PE=0,则在实模式下运行。
  • 第 1 位是监控协处理位 MP(Moniter coprocessor),它与第3位一起决定:当 TS=1 时操作码 WAIT 是否产生一个“协处理器不能使用”的出错信号。
  • CR0的第2位是模拟协处理器位 EM (Emulate coprocessor),如果EM=1,则不能使用协处理器,如果EM=0,则允许使用协处理器。
  • 第 3 位是任务转换位 (Task Switch),当一个任务转换完成之后,自动将它置 1。随着TS=1,就不能使用协处理器。
  • 第4位是微处理器的扩展类型位 ET(Processor Extension Type),其内保存着处理器扩展类型的信息,如果 ET=0,则标识系统使用的是287协处理器,如果 ET=1,则表示系统使用的是 387 浮点协处理器。
  • CR0的第 31 位是分页允许位(Paging Enable),它表示芯片上的分页部件是否允许工作。

而此时 cs 的值仍然是实模式下的值,因此我们需要把代码段的选择子装入 cs。运行跳转

jmp dword SelectorCode32:0

根据寻址机制我们知道,这个跳转的目标是描述符 DESC_CODE_32 对应的段的首地址,即标号 LABEL_SEG_CODE32 的地方。

参考

  1. ^1 https://blog.csdn.net/trochiluses/article/details/8954527
  2. ^http://gloriaswaterpool.com/2018/12/05/%E5%AD%A6%E4%B9%A0%E4%B8%80%E4%B8%AA%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F-3/
  3. ^2 https://blog.csdn.net/q1007729991/article/details/52538080
  4. ^3 https://blog.csdn.net/whatday/article/details/24851197
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值