首页 > 操作系统 > x86 保护模式浅析(一)
2019
05-28

x86 保护模式浅析(一)

实模式下的内存访问

实模式下,CPU通过段寄存器(16位)和偏移地址(16位)来访问物理地址,这个过程可以抽象地简述成以下步骤:
1、将段寄存器中存放的地址,左移4位(乘以2的4次方,即乘以16)得到基址(为了方便理解暂且这么叫吧)
2、将上一步得到的基址加上偏移地址,得到真实的物理地址

在上面的步骤中,我们不难看出的这几点特性:

  • 在实模式下,当段寄存器为 0 时,物理地址所能表示的最大范围是 0 ~ FFFFF,即 1MB 连续内存空间
  • 在实模式下,无论段寄存器为何值(无论基址为何值),CPU 总是只能访问 1MB 的连续内存空间
  • 在实模式下,段寄存器的作用更像是确定一个访问起点,偏移地址是用于基于这个起点开始计算距离,最终确定内存位置

初探 GDT

x86 保护模式使得 CPU 不再局限于 1MB 的内存空间,使其寻址能力达到了 4GB(后文会解释原因),同时它提供了更多优良的特性。废话少说,我们可以先来了解一下以下概念。

段选择子(Selector)

要进入保护模式,CPU 必须要设置一些必要的参数,这个工作由程序员完成。程序员通过设置全局描述符表(简称 GDT),使得 CPU 从中获取必要的参数,这样 CPU 才能顺利进入保护模式。全局描述符表(GDT)一般由多个段选择子(Selector)组成。下面笔者使用类 C 伪代码表示单个段选择子的相关结构,并给出解释

  • 单个全局描述符表的表项(即段选择子)的数据结构(从低字节到高字节排列):
struct gdt_entry {    // 单个段选择子,也就是全局描述符表的每一个表项的结构
    WORD low_limit;        // 段界限的  0 ~ 15 位
    WORD low_base;         // 段基址的  0 ~ 15 位
    BYTE high_base;        // 段基址的 16 ~ 23 位
    BYTE access_byte;      // 访问控制(一字节)(下文有展开说明)
    HALF_BYTE high_limit;  // 段界限的 16 ~ 19 位(段界限共20位)
    HALF_BYTE flags;       // 标志(半字节)(下文有展开说明)
    BYTE high_high_base;   // 段基址的 24 ~ 31 位(段基址共32位)
}selector; // 段选择子
  • 访问控制字节(access_byte)(从低位到高位排列)
BYTE access_byte {  // 没办法,理解性地,暂且这么定义吧
    _bit AC;  // 被访问标志位:一般置为 0,当该段被访问时,CPU将其置为 1
    _bit RW;  // 读写标志位:若是代码段,则此位表示该段是否可读;若是数据段,则此位表示该段是否可写
    _bit DC;  // 方向/顺应标志位:若是代码段,则此位表示是否可以从相等或较低的特权级开始执行(否则只能被“特权级标志”中描述的特权级所执行);若是数据段,则此位表示该段是否是向下(向低地址方向)增长的
    _bit Ex;  // 可执行标志位:此位表示该段是否可被执行,若置 0 则是数据段(不可执行)
    _bit S;   // 描述符类型标志位:置 1 表示该段是代码段或数据段,置 0 表示该段是系统段(暂时先置 1 吧)
    _double_bit Privl; // 特权级标志双位:双位可置 0 ~ 3,表示该段所具有特权级(ring0 ~ ring3)
    _bit Pr;  // 存在标志位:应当永远置 1,表示该选择子(Selector)有效
};
  • 标志位半字节(flags)(从低位到高位排列)
HALF_BYTE flags {  // 没办法,理解性地,暂且这么定义吧
    _bit zero;  // 一般置为 0
    _bit zero;  // 一般置为 0
    _bit Sz;    // 宽度标志位:此位表示选择子是否定义的是 32 位保护模式(置 0 表示选择子定义了 16 位保护模式)
    _bit Gr;    // 粒度标志位:此位置 1 表示段界限粒度为页(4KB),置 0 表示段界限粒度为字节(1Byte)
};

在保护模式下,段已经不同于实模式中的段。实际上,更多地是访问方式、起到的作用发生了改变。这可能需要读者自己去循序渐进地体会。下文将会讲到全局描述符表的结构以及加载方式,段选择子的使用方式

全局描述符表(Global Descriptor Table)

我们从前述中得知,段选择子就是全局描述符表的表项,也就是说 GDT 的每一个表项都是 8 字节的段选择子。段选择子中定义了段的基址、段的界限、还有一些描述性的信息。因为系统中一般将用到多个段,所以用线性的方式去存放这些段的选择子,于是构成了 GDT。我们可以先用类 C 的伪代码来定义 GDT 的数据结构

  • GDT(全局描述符表)(从低字节到高字节)
struct gdt {
    gdt_entry selector_null;  // 第 1 个选择子(全局描述符表项)(填充为 0)
    gdt_entry selector1;      // 第 2 个选择子(全局描述符表项)
    gdt_entry selector2;      // 第 3 个选择子(全局描述符表项)
    /* ...... */
};

GDT 的一个段选择子(第一个表项),必须全部填充为 0,这是系统约定俗成

看到这里,你应该已经对 GDT 的结构豁然开朗了。下文我们将介绍如何加载 GDT,为初探 x86 保护模式的难点画上句号

加载 GDT

假设我们在内存的某个位置定义了一个包含多个段选择子的 GDT,它包含我们进入保护模式后,CPU 在保护模式正常运行的相关信息。在我们进入保护模式之前需要让 CPU 加载这个 GDT,现在,我们有两样法宝,一个是 LGDT 机器指令、另外一个是 GDTR 寄存器。

顾名思义,LGDT 指令能够将 GDT 载入到 GDTR 寄存器中,这样 CPU 就“得到”了在保护模式下正常运作的相关信息。问题是,具体该如何做到。我们先来了解 GDTR 寄存器的结构,如图所示

x86 保护模式浅析(一) - 第1张  | 阿龙咖啡
Size 即是GDT所占总字节数减 1 的值(边界值),Offset 即是 GDT 头部的线性地址(即绝对地址)

不难看出,GDT 中最多能容纳 0x10000 / 8 = 8192 个段选择子。话说回来,我们可以使用 LGDT 指令从指定内存位置读取这个 48 位(6字节)的 GDT 向量到 GDTR 寄存器中。这样,CPU 就将 GDT 的相关信息“牢记在心”

后续

下一篇文章,笔者将讲述如何为真正进入保护模式扫清障碍,有关 A20 地址线、PE 标志位的设置、以及段选择子的用法、保护模式下的内存寻址方式。感谢阅读本文!


打赏 赞(2)
微信
支付宝
微信二维码图片

微信扫描二维码打赏

支付宝二维码图片

支付宝扫描二维码打赏

最后编辑:
作者:Yenyu
Yenyu
编程爱好者

留下一个回复

你的email不会被公开。