[CMU OS lab] P1 (part 1): x86硬件

P1的主要内容是写一个以核心态 Privilege Level 0 直接运行在 x86 硬件上的游戏 Sokoban (推箱子)。同样,出于学术规范考虑,本文不会透露项目的全部代码和 handout ,但可能会在必要时节选少量代码片段。代码片段版权所有为CMU 15-410/605 OS 课程。另外也可能节选少量清华 OS 课程代码片段,版权所有为清华大学计算机系 OS 课程。

作为第一部分,本文主要介绍 x86 的基础硬件(体系结构要求,如分段机制、特权级等)。正如 CMU OS 课程所述,并非所有 OS 都要满足这样的要求,这是利用 x86 平台学习 OS 必须的代价。

特权级 Privilege Level

x86 特权级共有 ring 0 - ring 3 四等,OS 课程中称为 PL0 - PL3。kernel 运行在 PL0,用户代码运行在 PL3,PL1 2 本课程不涉及。

分段机制 Segmentation

段机制的实现基于段寄存器 segment register,段选择子 segment selector,段描述符(表) segment descriptor (table)。描述符表分为全局 global descriptor table (GDT) 和本地 local descriptor table (LDT)。本课程只使用GDT,清华OS课程也只使用GDT。

段选择子如图所示:

段选择子被存储在段寄存器中。段寄存器共有以下几种: - %CS:代码段寄存器,当前运行的代码所在的段 - %SS: 栈(stack)段寄存器,栈所在的段 - %DS, %ES, %FS, %GS: 数据段寄存器,一般用%DS

段描述符如图所示:

段描述符被存储在内存中。Type 字段规定了段的读写权限。因为 x86 体系规定一个段不能既有“读/写”权限又有“读/执行”权限,因此至少需要两个段:代码段和数据段。为了简便(因为段本身的保护能力被移交给了分页机制),所以这两个段的基址都为0,大小都为 4G (即 limit 为 0xFFFFFFFF)。

注意, limit 字段为 20 bits,表示上限为 1 MB,如何表示 4 GB 呢?在于其 G (granularity) 字段。G 设置时粒度为 4 Kb,那么 \(1M \times 4KB = 4GB\). 同时应该设置 D/B (default operation size/default stack pointer size and/or upper bound)1 表示 32 bits。

关于 Type

解释了为什么“一个段不能既有‘读/写’权限又有‘读/执行’权限”。CMU中将代码段 type 设为 b,数据段设为 3,这是额外把 A 也置为了 1。A 表示 accessed,即已经访问过,所以这里初始化时设不设置其实没什么影响(似乎)。但THU课程中初始化没有设置 A.

另外吐槽一点,CMU 的 GDT 表完全是 magic number 写的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
init_gdt: /* 0x100900 */
.long 0x00000000
.long 0x00000000
/* Next two lines need to be kept in sync with smp/smp.c */
.long 0x09500067
.long 0x00008910
.long 0x0000ffff
.long 0x00cf9b00
.long 0x0000ffff
.long 0x00cf9300
.long 0x0000ffff
.long 0x00cffb00
.long 0x0000ffff
.long 0x00cff200

然而 THU 的 GDT 是通过定义了各种宏,生成的可读代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#define SEG_ASM(type,base,lim)                                  \
.word (((lim) >> 12) & 0xffff), ((base) & 0xffff); \
.byte (((base) >> 16) & 0xff), (0x90 | (type)), \
(0xC0 | (((lim) >> 28) & 0xf)), (((base) >> 24) & 0xff)

/* Normal segment */
#define SEG_NULLASM \
.word 0, 0; \
.byte 0, 0, 0, 0

.data
# Bootstrap GDT
.p2align 2 # force 4 byte alignment
gdt:
SEG_NULLASM # null seg
SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff) # code seg for bootloader and kernel
SEG_ASM(STA_W, 0x0, 0xffffffff) # data seg for bootloader and kernel

gdtdesc:
.word 0x17 # sizeof(gdt) - 1
.long gdt # address gdt

而且 CMU 的代码把这一堆 hard code 的 GDT 写在了 .text 段...不过 CMU 做的好的一个方面是,封装了一个 lgdt 的函数,通过 asm 代码里一通操作,可以随意 load gdt。THU 倒是把初始化的 GDT 通过 hard code 给 load 了。

关于 lgdt,它要的是一个 6B 的参数,前 2B (一个 word)是 limit,后 4B 是 GDT base。因此 CMU 操作里先 pushlpushw 然后 lgdt (%esp) 把我整懵了,没想到 pushw 真就 %esp 减2而非减4...这也对应于 THU 代码里的 gdtdesc 是一个 word 一个 long。

lgdt 以后 GDT base address 被存在 %CR3 寄存器里。

CPL RPL DPL

  • CPL: %CR0 寄存器里的处理器 current 特权级
  • RPL: 段寄存器里(段选择子)的 required 特权级
  • DPL: 段描述符里段本身的 descriptor 特权级

有什么联系:

访问数据段

把一个段选择子加载到数据段寄存器时,需要一个 privilege level check:

The processor loads the segment selector into the segment register if the DPL is numerically greater than or equal to both the CPL and the RPL. Otherwise, a general-protection fault is generated and the segment register is not loaded. -- Intel 2001 ed Volume 3 4-9

也就是说,RPL 的目的是可以降低当前的特权级(制约 CPL)。利用一个低特权级的选择子访问低特权级的内存是可行并且更安全的。

访问代码段

通过门 (Gate) 访问代码段也要做检查,这里直接复制我在 THU 上课时在 Piazza 上回答一个问题的内容关于x-86访问时特权级的问题

关于上面提到的最后一点,即“Example of Accessing Call Gates At Various Privilege Levels”,对应于PPT中“x86访问门时的特权级:CPL <= DPL[门] & CPL >= DPL[段]”,我的理解如下:

首先手册中Table 5-1给出了访问门时的特权级检查规则(此处仅考虑使用call而非jmp):

可以看到,对于DPL有两个概念需要区分:其一是门本身的gate DPL,其二是门对应的目标代码段的code segment DPL. 此外,这里还有两个名词需要解释:

  • conforming code segment:一致代码段,可以被低特权级的用户直接调用访问的代码,但是特权级不会改变,用户态还是用户态
  • nonconforming code segment:非一致代码段,只允许同级间访问,绝对禁止不同级访问

这样就可以理解PPT中提到的“CPL <= DPL[门] & CPL >= DPL[段]”的含义如下:

  • 对于第一条“CPL <= DPL[门]”,对应的是Table 5-1中第一条条件,即门本身是可以在当前特权级下被访问到的
  • 对于第二条“CPL >= DPL[段]”,对应的是Table 5-1中第二条条件,即门对应的目标代码段(可以理解为门指向的中断服务例程代码)所满足的条件,也就是要求目标代码的DPL不大于当前特权级;这一点很好理解,因为目标代码段一般为核心代码,其本身的DPL为核心等级,但存在处于用户态的进程需要执行这些代码的需求(如系统调用),因此允许目标代码DPL小于CPL的情况出现。但针对代码段是conforming还是nonconforming,会涉及到是否要变更CPL

下面举出手册中的例子。作为例子的Figure 5-12如下:

仅以在ring 3访问Gate A为例:

  • 当前处于用户态的代码段A,CPL=3
  • 通过Gate Selector A以RPL=3访问门A,门A的DPL=3,这满足Table 5-1的第一条检查条件,即“CPL <= call gate DPL, RPL <= call gate DPL”
  • 在满足上面的条件后,则可以跳转并执行门A对应的核心态代码段E(可以理解为门A对应的中断服务例程),代码段E的DPL=0,小于CPL,对于call gate而言这是允许的,满足Table 5-1的第二条检查条件
  • 由于代码段E是nonconforming code segment,所以会要求CPL的特权级变更(需要变为代码段E的同级即ring 0),因而会导致stack switch(作为对比,访问代码段D时不需要stack switch)

(参考:Intel® 64 and IA-32 Architectures Software Developer’s Manual 第5.8.4节 Accessing a Code Segment Through a Call Gate)