5

汇编语言之保护模式

 1 year ago
source link: https://www.junz.org/post/asm_protectmode/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

保护模式与实模式

我们这里说的保护模式特指 IA-32 处理器上的32位保护模式。在保护模式下,所有的32位处理器都可以访问最多2^32字节,也就是4GB的内存。我们的段寄存器中保存的也不再是段地址,也不再需要左移与偏移量拼接。需要注意的是,32位处理器是兼容16位的实模式的,而在刚加电时,处理器默认也是跑在实模式下的,我们需要经过一番设置才能让他进入保护模式。

寄存器的变化

在16位处理器中,有8个通用寄存器,32位处理器在此基础上,拓展了这8个通用寄存器的长度,使之达到了32位。

为了兼容性,前四个寄存器的低16位就是16位寄存器,此时它的高16位没有用:

31   16 15    0
|      |  AX  | => EAX
|      |  BX  | => EBX
|      |  CX  | => ECX
|      |  DX  | => EDX

因为32位处理器拥有32位的段寄存器,所以原则上它不需要分段就可以访问到所有地址。但是,IA-32 架构的处理器是基于分段模型的,它仍然需要通过段为单位访问内存。不过也可以指分一个段,段的基地址为0x00000000,段的长度为4GB, 这种情况下可以视为没有分段,也被称为平坦模式 (Flat mode)。

15   0
| CS | 描述符高速缓冲器 |
| SS | 描述符高速缓冲器 |
| DS | 描述符高速缓冲器 |
| ES | 描述符高速缓冲器 |
| FS | 描述符高速缓冲器 |
| GS | 描述符高速缓冲器 |

在32位模式下,传统的段寄存器保存的不再是16位的段基地址,而是段的选择子。严格来说,它的新名字叫做段选择器。每个段寄存器还包括一个不可见的部分,称为描述符高速缓冲器,里面保存着段的基地址和段的访问方法。注意这部分内容对程序员不可见,由处理器自动控制。

在32位处理器访问内存时,同样需要在程序中给出段地址和偏移量。段的管理由处理器的段部件负责进行的,段部件将段地址和偏移地址相加,得到访问内存的地址。一般来说,段部件产生的地址就是物理地址。

然而,32位处理器还支持分页。当页功能开启时,段部件产生的地址就不是物理地址了,而成为线性地址 (Linear address)。线性地址还要经过页部件转换后才能得到真实的物理地址,这个下面会专门详细解释。

全局描述符表

上面我们说了段部件将段地址和偏移地址相加得到访问内存的地址,当然,实际情况要比这说的复杂的多。

在32位保护模式下,我们不能随心所欲地访问任何段。在此之前,我们必须记录下我们所拥有的所有段以及每个段的基本信息(能不能执行,被谁执行等)。而存放这些信息的地方,就被称为全局描述符表 (Global Descriptor Table), 简称为 GDT。注意 GDT 是需要我们人为定义在内存中的某个地方的,而且必须要在进入保护模式之前就定义好。可以想到,因为 GDT 不是处理器自动控制的,所以处理器需要一个寄存器来追踪它的基本信息,就像我们在实模式对段的处理一样。

为了做到这一点,处理器内部有一个48位的寄存器,称为全局描述符表寄存器 (GDTR)。该寄存器分为两个部分,高32位的线性地址用来保存 GDT 在内存中的起始地址,低16位用来保存表的大小(总字节数减去一,16位全为0表大小则为1)。因为表大小用16位来存储,所以表最多有2^16字节。表中每个项为8字节,故最多定义8192个描述符(段)。

上面说到了段描述符有8个字节,也就是2个双字,64位。这是它的高32位布局:

|端基地址31-24位|G|D/B|AVL|段界限19-16位|P|DPL|S|TYPE|端基地址23-16位|

这是它的低32位布局:

31            16 15           0
| 段基地址15-0位 | 段界限15-0位 |

可以看到段描述符中指定了32位的段基地址(段开始的地址)和20位的段界限。

下面给出段描述符中的其他信息含义:

  • G 粒度位。表示段界限的单位。为0以字节为单位,为1以4KB(页)为单位。
  • D/B 用来兼容16位保护程序,不做过多赘述。
  • AVL 可用位。由操作系统决定如何使用,没有特别的实际用途。
  • P 段存在位。用于指示段是否存在,某些情况下如段被置换到硬盘时该位则为0
  • S 描述符类型。该位为0时表示则是一个系统段,为1则表示是一个代码段或数据段
  • TYPE 用来指示该段的读写权限。

特权级是存在于描述符和段选择子中的一个数值。

Intel 处理器可以识别4个特权级别,越小的数值权限越大:

  • 0 一般用于操作系统的主体部分
  • 1 一般用于驱动程序
  • 2 一般用于驱动程序
  • 3 一般用于应用程序

在上面介绍段描述符的结构时我们可以看到,有一个叫 DPL (Descriptor Privilege Level) 的字段。它有2位组成,可以取值为00,01,10,11,刚好对应了4个特权级。对于数据段来说,DPL 决定了他们所应当访问的最低特权级别,也就是只有特权级小于它的程序才可以访问这个段。

代码段的特权级检查是很严格的。一般来说,控制转移只发生在两个特权级相同的代码段之间。不过,为了让特权级低的应用程序可以调用特权级高的程序,处理器也提供了处理办法:

  • 将高特权级的代码段定义为依从的。简单来说,如果一个段的段描述符中的 TYPE 字段中的 C 位为1,则这个段为依从的代码段,可以从特权级比他低的程序调用并进入。注意这也是有条件的,要求当前特权级 CPL 必须低于或和目标代码段描述符的 DPL 相同。注意,我们在转到依从的代码段后,不改变当前特权级 CPL,这也是为什么叫它依从的。

  • 门 (gate)。门是另一种的描述符,称为门描述符。事实上门的类型有好几种,我们这里只介绍调用门 (call gate)。所有的门描述符都是64位的,当然也包括调用门描述符。在调用门描述符中定义了目标过程所在代码段的选择子和段内偏移。要想通过调用门进行控制转移,可以用:

    • jmp far 指令。不改变当前特权级别。
    • call far 指令。改变当前特权级别。

当同时运行的程序很多时,内存就有可能不够用了。这时,我们可以将一些段的描述符的 P 位清零,然后将其换出到硬盘中,这样我们就腾出了一些空间给其他程序使用。但是。段的长度是不确定的,如果换出的段长度太小则不够用,但如果换出的段过大,又会造成浪费。为了解决这个问题,我们引用了分页机制。

我们知道在此之前我们访问内存时都是使用“段基地址:偏移量”的方式,段部件会根据一定的规则构造出实际访问的地址。(这对实模式和保护模式都是成立的,只不过段部件构造的方式不同,保护模式下限制更多)而当我们引入了分页机制后,用段部件得到的内存就不再是真实的物理内存了,而是虚拟内存。

虚拟内存是真实内存的一层抽象,一层封装,并不真实存在也不能储存任何数据。在分页机制中,我们将真实内存分成了一定大小的块(一般是4KB)。举个具体的例子,假如我们需要分配一个段,首先我们会向虚拟内存请求一个连续的内存空间,接着虚拟内存就会在真实内存中寻找足够可用的空闲页,将要分配的内存拆开分别映射到各个页上。所以,虽然我们所看到的内存(虚拟内存)是连续的,但在实际的物理内存上它可能由多个不连续的页组成。而当我们找不到足够多的空闲页时,我们就会按照一定的规则将一些页置换到硬盘上,然后再使用。

可以看到,页面大小的选择至关重要。如果说页面大小过小,那么就可能需要分配很多页降低性能,但是如果页面太大,又会造成大量内存内部碎片。(我们将段映射到页上,即使再小的段也必须使用一个页,页中剩下的内容只能白白浪费)

页目录和页表

我们将各个段映射到一个或多个页面上,很显然我们需要一个表来保存这个映射关系。因为可访问的内存有4GB, 一个页为4KB, 所以共有2^20个页。我们需要在表中储存页的起始地址,所以一个页要4字节(32位地址),所以页表的总大小为4MB。这显然是一个很大的表需要很多页来储存。

所以,我们采取了结构化的方式,采用多级页表来解决这个问题。首先我们用一个页建立一个页目录,其中存放的是另一些页表的地址(32位地址所以每一项4字节共可以存放1024项)而在这些页表中,我们又可以存放1024个页面,这样我们便可以表示1024*1024共2^20个页,刚刚好!用高级编程语言来表示下这个关系:

// 页表
struct PageTable {
  std::array<Page, 1024> pages;
}
// 页目录
struct PageDirectory {
  std::array<PageTable, 1024> page_tables;
}

而为了记录页目录的信息,处理器中专门又有了一个控制寄存器 CR3, 也叫页目录基址寄存器 (Page Directory Base Register) 负责存放当前任务页目录的物理地址。每当任务发生切换时,这个寄存器就会被更新,指向新任务的页目录地址。

地址变换具体过程

假设有这样一条指令(段的起始地址为0x00800000):

mov edx, [0x1050]

正常情况下没有越界且具备所需权限时,段部件会产生0x00801050这个虚拟内存地址。页部件接着将这个地址分成3段:

| 页目录索引 | 页表索引 | 页内偏移 |
|0000000010|0000000001|000001010000|

这样我们就找到了它真实所在的页面和偏移量,并找到真实的物理地址了。

在上面的解释中,可以看出页目录和页表是建立在虚拟内存之上的,也只是普通的页,只不过被用于记录其他页的信息了!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK