搜档网
当前位置:搜档网 › 03 进入保护模式

03 进入保护模式



CHAPTER 3 进入保护模式



前面我们看到,通过一些很简单的代码,我们做到了启动一个微型系统,加载文件系统中的文件进入内存并运行的功能。应该注意的是,在前面的代码中我们使用的内存空间都很小。我们看一下boot.bin 和LOADER.BIN 的大小就能感觉出来(当然,可执行文件小未必使用内存空间小,但是这两个文件也太小了^-^)。

$ ls -l boot.bin LOADER.BIN
-rwxr-xr-x 1 solrex solrex 512 2008-04-26 16:34 boot.bin
-rwxr-xr-x 1 solrex solrex 15 2008-04-26 16:34 LOADER.BIN

boot.bin 是512 个字节(其中还有我们填充的内容,实际指令只有480 个字节),而LOADER.BIN更过分,只有15 个字节大小。可想而知这两个文件在内存中能使用多大的空间吧。如果读者有些汇编语言经验的话,就会发现我们在前面的程序中使用的存储器寻址都是在实模式下进行的,即:由段寄存器(cs, ds: 16-bit)配合段内偏移地址(16-bit)来定位一个实际的20-bit 物理地址,所以我们前面的程序最多支持220 = 210 * 210 = 1024 * 1024 bytes = 1MB 的寻址空间。

哇, 1MB 不小了,我们的操作系统加一起连1KB 都用不到, 1MB 寻址空间足够了。但是需要考虑到的一点是,就拿我们现在用的1.44MB的(已经被淘汰的)软盘标准来说,如果软盘上某个文件超过1MB ,我们的操作系统就没办法处理了。那么如果以后把操作系统安装到硬盘上之后呢?我们就没办法处理稍微大一点的文件了。

所以我们要从最原始的Intel 8086/8088 CPU 的实模式中跳出来,进入Intel 80286 之后系列CPU给我们提供的保护模式。这还将为我们带来其它更多的好处,具体内容请继续往下读。


3.1 实模式和保护模式

如果您需要更详细的知识,也许您更愿意去读Intel 的手册,本节内容主要集中在:Intel R ° 64 and IA-32 Architectures Software Developer’s Manual, Volume 3A: System Programming Guide, 第2 章和第3 章.


3.1.1 一段历史

Intel 公司在1978 年发布了一款16 位字长CPU: 8086 ,最高主频5 MHz?10 MHz ,集成了29,000个晶体管,这款在今天感觉像玩具一样的CPU 却是奠定今天Intel PC 芯片市场地位的最重要的产品之一。虽然它的后继者8088 ,加强版的8086 (增加了一个8 比特的外部总线)才是事实上的IBM 兼容机(PC,个人电脑)雏形的核心,但人们仍然习惯于用8086 作为厂商标志代表Intel 。


因为受到字长(16 位)的限制,如果仅仅使用单个寄存器寻址, 8086 仅仅能访问64KB(216) 的地址空间,这显然不能满足一般要求,而当时1MB(220) 对于一般的应用就比较足够了,所以8086 使用了20 位的地址线。

在8086 刚发布的时候,没有“实模式”这个说法,因为当时的Intel CPU 只有一种模式。在Inte

l 以后的发布中, 80286 引入了“保护模式”寻址方式,将CPU 的寻址范围扩大到16(224) MB ,但是80286仍然是一款16 位CPU ,这就限制了它的广泛应用。但是“实模式”这个说法,就从80286 开始了。

接下来的发展就更快了,1985 年发布的i386 首先让PC CPU 进入了32 位时代,由此而带来的好处显而易见,寻址能力大大增强,但是多任务处理和虚拟存储器的需求仍然推动着i386 向更完善的保护模式发展。下面我们来了解一下“实模式”和“保护模式”的具体涵义。


3.1.2 实模式
实模式(real mode),有时候也被成为实地址模式(real address mode)或者兼容模式(compatibilitymode)是Intel 8086 CPU 以及以其为基础发展起来的x86 兼容CPU 采用的一种操作模式。其主要的特点有:20 比特的分段访问的内存地址空间(即1 MB 的寻址能力);程序可直接访问BIOS 中断和外设;硬件层不支持任何内存保护或者多任务处理。80286 之后所有x86 CPU 在加电自举时都是首先进入实模式; 80186 以及之前的CPU 只有一种操作模式,相当于实模式。


3.1.3 保护模式
保护模式(protected mode),有时候也被成为保护的虚拟地址模式(protected virtual address mode),也是一种x86 兼容CPU 的工作模式。保护模式为系统软件实现虚拟内存、分页机制、安全的多任务处理的功能支持,还有其它为操作系统提供的对应用程序的控制功能支持,比如:特权级、实模式应用程序兼容、虚拟8086 模式。


3.1.4 实模式和保护模式的寻址模式

前面提到过,实模式下的地址线是20 位的,所以实模式下的寻址模式使用分段方式来解决16 位字长机器提供20 位地址空间的问题。这个分段方法需要程序员在编制程序的过程中将存储器划分成段,每个段内的地址空间是线性增长的,最大可达64K(216),这样段內地址就可以使用16 位表示。段基址( 20-bit )的最低4 位必须是0 ,这样段基址就可以使用16 位段地址来表示,需要时将段地址左移4位就得到段起始地址。除了便于寻址之外,分段还有一个好处,就是将程序的代码段、数据段和堆栈段等隔离开,避免相互之间产生干扰。

当计算某个单元的物理地址时,比如汇编语言中的一个Label ,就通过段地址( 16-bit )左移4 位得到段基址( 20-bit ),再加上该单元( Label )的段內偏移量( 16-bit )来得到其物理地址( 20-bit),如图3.1a 所示。

Fig 3.1: 实模式与保护模式寻址模型比较(参见图片)

一般情况下,段地址会被放在四个段寄存器中,即:代码段CS,数据段DS,堆栈段SS 和附加段ES 寄存器。这样在加载数据或者控制程序运行的时候,只需要一个偏移量参数,CPU 会自动用对

应段的起始地址加上偏移量参数来得到需要的地址。(后继CPU 又加上了两个段寄存器FS 和GS ,不过使用方式是基本一样的。)

由此可见,实模式的寻址模式是很简单的,就是用两个16 位逻辑地址(段地址:偏移地址)组合成一个20 位物理地址,而保护模式的寻址方式就要稍微复杂一点了。

TIPS---------
Intel 的CPU 在保护模式下是可以选择打开分页机制的,但为了简单起见,我们先不开启分页机制,所以下面的讲解针对只有分段机制的保护模式展开。
-------------

在保护模式下,每个单元的物理地址仍然是由逻辑地址表示,但是这个逻辑地址不再由(段地址:偏移地址)组成了,而是由(段选择子:偏移地址)表示。这里的偏移地址也变成了32 位的,所以段空间也比实模式下大得多。偏移地址的意思和实模式下并没有本质不同,但段地址的计算就要复杂一些了,如图3.1b 所示。段基址(Segment Base Address)被存放在段描述符(Segment Descriptor)中,GDT(Global Descriptor Table,全局段选择子表)是保存着所有段选择子的信息,段选择子(Segment Selector)是一个指向某个段选择子的索引。

如图3.1b 所示,当我们计算某个单元的物理地址时,只需要给出(段选择子:偏移地址),CPU会从GDT 中按照段选择子找到对应的段描述符,从段描述符中找出段基址,将段基址加上偏移量,就得到了该单元的物理地址。



3.2 与保护模式初次会面

介绍完了保护模式和实模式的不同,下面我们就尝试一下进入保护模式吧。在上一章我们已经实现了用启动扇区加载引导文件,所以这里我们就不用再去管启动扇区的事情了,下面的修改均在loader.S中进行。上一章的loader.S 仅仅实现在屏幕的上方中间打印了一个L ,下面我们的loader.S 要进入保护模式来打印一些新东西。

首先,我们来理清一下应该如何进入保护模式:

1. 我们需要一个GDT。由于保护模式的寻址方式是基于GDT 的,我们得自己写一个GDT 数据结构并将其载入到系统中。

2. 我们需要为进入保护模式作准备。由于保护模式和实模式运行方式不同,在进入保护模式之前,我们需要一些准备工作。

3. 我们需要一段能在保护模式下运行的代码demo,以提示我们成功进入了保护模式。

下面我们就来一步步完成我们的第一个保护模式loader 。


3.2.1 GDT 数据结构

要写GDT,首先得了解GDT 的数据结构。GDT 实际上只是一个存储段描述符的线性表(可以理解成一个段描述符数组),对它的要求是其第一个段描述符置为空,因为处理机不会去处理第一个段描述符,所以理解GDT 的数据结构难点主要在于理解段描述符的

数据结构。

段描述符主要用来为处理机提供段位址,段访问控制和状态信息。图3.2 显示了一个基本的段描述符结构:

Fig 3.2: 段描述符(参见图片)

看到上面那么多内容,是不是感觉有点儿恐怖啊!其实简单的来看,我们现在最关注的是段基址,就是图3.2 中标记为Base 的部分。可以看到,段基址在段描述符中被分割为三段存储,分别是:Base 31:24, Base 23:16, Base Address 15:0,把这三段拼起来,我们就得到了一个32 位的段基址。

有了段基址,就需要有一个界限来避免程序跑丢发生段错误,这个界限就是图3.2 中标记为Limit的部分,将Seg. Limit 19:16 和Segment Limit 15:0 拼起来我们就得到了一个20 位的段界限,这个界限就是应该是段需要的长度了。

下面还要说的就是那个D/B Flag ,D/B 代表Default Operation Size ,0 代表16 位的段,1 代表32 位的段。为了充分利用CPU ,我们当然要设置为32 位模式了。剩下那些乱七八糟的Flag 呢,无非就是提供段的属性(代码段还是数据段?只读还是读写?),我们将在第3.3.2 节为大家详细介绍。

这些东西那么乱,难道要每次一点儿一点儿地计算吗?放心,程序员自有办法,请看下面的程序:

-------------------
56 /* MACROS */
57
58 /* Segment Descriptor data structure.
59 Usage: Descriptor Base, Limit, Attr
60 Base: 4byte
61 Limit: 4byte (low 20 bits available)
62 Attr: 2byte (lower 4 bits of higher byte are always 0) */
63 .macro Descriptor Base, Limit, Attr
64 .2byte \Limit & 0xFFFF
65 .2byte \Base & 0xFFFF
66 .byte (\Base >> 16) & 0xFF
67 .2byte ((\Limit >> 8) & 0xF00) | (\Attr & 0xF0FF)
68 .byte (\Base >> 24) & 0xFF
69 .endm
-------------------
Fig 3.3: 自动生成段描述符的宏定义(节自chapter3/1/pm.h)

图3.3 中所示, 就是自动生成段描述符的汇编宏定义。我们只需要给宏Descriptor 三个参数:Base(段基址), Limit(段界限[段长度]), Attr(段属性),Descriptor 就会自动将三者展开放到段描述符中对应的位置。看看我们在程序中怎么使用这个宏:

--------------------
21
22 /* Global Descriptor Table */
23 LABEL_GDT: Descriptor 0, 0, 0
24 LABEL_DESC_CODE32: Descriptor 0, (SegCode32Len - 1), (DA_C + DA_32)
25 LABEL_DESC_VIDEO: Descriptor 0xB8000, 0xffff, DA_DRW
26
--------------------
Fig 3.4: 自动生成段描述符的宏使用示例(节自chapter3/1/loader.S)

图3.4 中,就利用Descriptor 宏生成了三个段描述符,形成了一个GDT。注意到没有,第一个段描述符是空的(参数全为0)。这里LABEL DESC CODE32 的段基址为0 是因为我们无法确定它的准确位置,它将在运行期被填入。

有人可能会产生疑问,段基址和段界限什么意思我们都知道了,那段属性怎么回事呢? DA C,DA 32, DA DRW 都是什么东西

啊?是这样的,为了避免手动一个一个置段描述符中的Flag ,我们预先定义了一些常用属性,用的时候只需要将这些属性加起来作为宏Descriptor 的参数,就能将段描述符中的所有flag 置上(记得C 语言中fopen 的参数吗?)。这些属性的定义如下(没必要细看,用的时候再找即可):

---------------------
11 /* Comments below accords to "Chapter 3.4.5: Segment Descriptors" of "Intel
12 64 and IA-32 Arch. SW Developer’s Manual: Volume 3A: System Programming
13 Guide". */
14
15 /* GDT Descriptor Attributes
16 DA_ : Descriptor Attribute
17 D : Data Segment
18 C : Code Segment
19 S : System Segment
20 R : Read-only
21 RW : Read/Write
22 A : Access */
23 .set DA_32, 0x4000 /* 32-bit segment */
24
25 /* Descriptor privilege level */
26 .set DA_DPL0, 0x00 /* DPL = 0 */
27 .set DA_DPL1, 0x20 /* DPL = 1 */
28 .set DA_DPL2, 0x40 /* DPL = 2 */
29 .set DA_DPL3, 0x60 /* DPL = 3 */
30
31 /* GDT Code- and Data-Segment Types */
32 .set DA_DR, 0x90 /* Read-Only */
33 .set DA_DRW, 0x92 /* Read/Write */
34 .set DA_DRWA, 0x93 /* Read/Write, accessed */
35 .set DA_C, 0x98 /* Execute-Only */
36 .set DA_CR, 0x9A /* Execute/Read */
37 .set DA_CCO, 0x9C /* Execute-Only, conforming */
38 .set DA_CCOR, 0x9E /* Execute/Read-Only, conforming */
39
40 /* GDT System-Segment and Gate-Descriptor Types */
41 .set DA_LDT, 0x82 /* LDT */
42 .set DA_TaskGate, 0x85 /* Task Gate */
43 .set DA_386TSS, 0x89 /* 32-bit TSS(Available) */
44 .set DA_386CGate, 0x8C /* 32-bit Call Gate */
45 .set DA_386IGate, 0x8E /* 32-bit Interrupt Gate */
46 .set DA_386TGate, 0x8F /* 32-bit Trap Gate */
47
48 /* Selector Attributes */
49 .set SA_RPL0, 0
50 .set SA_RPL1, 1
51 .set SA_RPL2, 2
52 .set SA_RPL3, 3
53 .set SA_TIG, 0
54 .set SA_TIL, 4
55
--------------------
Fig 3.5: 预先设置的段属性(节自chapter3/1/pm.h)


3.2.2 保护模式下的demo

为什么把这节提前到第3.2.3 节前讲呢?因为要写入GDT 正确的段描述符,首先要知道段的信息,我们就得先准备好这个段:

-------------------
82 LABEL_SEG_CODE32:
83 .code32
84 mov $(SelectorVideo), %ax
85 mov %ax, %gs /* Video segment selector(dest) */
86
87 movl $((80 * 10 + 0) * 2), %edi
88 movb $0xC, %ah /* 0000: Black Back 1100: Red Front */
89 movb $’P’, %al
90
91 mov %ax, %gs:(%edi)
92
93 /* Stop here, infinite loop. */
94 jmp .
95
96 /* Get the length of 32-bit segment code. */
97 .set SegCode32Len, . - LABEL_SEG_CODE32
------------------
Fig 3.6: 第一个在保护模式下运行的demo(节自chapter3/1/loader.S)

其实这个段的作用很简单,通过操纵视频段数据,在屏幕中间打印一个红色的”P”(和我们前面使用BIOS 中断来打印字符的方式有所不同)。


3.2.3 加载GDT

GDT 所需要的信息我们都知道了,GDT 表也通过图3.4 中的代码实现了。那么,我们应该向GDT 中填

入缺少的信息,然后载入GDT 了。将GDT 载入处理机是用lgdt 汇编指令实现的,但是lgdt 指令需要存放GDT 的基址和界限的指针作参数,所以我们还需要知道GDT 的位置和GDT 的界
限:

--------------
17 /* NOTE! Wenbo-20080512: Actually here we put the normal .data section into
18 the .code section. For application SW, it is not allowed. However, we are
19 writing an OS. That is OK. Because there is no OS to complain about
20 that behavior. :) */
21
22 /* Global Descriptor Table */
23 LABEL_GDT: Descriptor 0, 0, 0
24 LABEL_DESC_CODE32: Descriptor 0, (SegCode32Len - 1), (DA_C + DA_32)
25 LABEL_DESC_VIDEO: Descriptor 0xB8000, 0xffff, DA_DRW
26
27 .set GdtLen, (. - LABEL_GDT) /* GDT Length */
28
29 GdtPtr: .2byte (GdtLen - 1) /* GDT Limit */
30 .4byte 0 /* GDT Base */
31
32 /* GDT Selector */
33 .set SelectorCode32, (LABEL_DESC_CODE32 - LABEL_GDT)
34 .set SelectorVideo, (LABEL_DESC_VIDEO - LABEL_GDT)
35
36 /* Program starts here. */
37 LABEL_BEGIN:
38 mov %cs, %ax /* Move code segment address(CS) to data segment */
39 mov %ax, %ds /* register(DS), ES and SS. Because we have */
40 mov %ax, %es /* embedded .data section into .code section in */
41 mov %ax, %ss /* the start(mentioned in the NOTE above). */
42
43 mov $0x100, %sp
44
45 /* Initialize 32-bits code segment descriptor. */
46 xor %eax, %eax
47 mov %cs, %ax
48 shl $4, %eax
49 addl $(LABEL_SEG_CODE32), %eax
50 movw %ax, (LABEL_DESC_CODE32 + 2)
51 shr $16, %eax
52 movb %al, (LABEL_DESC_CODE32 + 4)
53 movb %ah, (LABEL_DESC_CODE32 + 7)
54
55 /* Prepared for loading GDTR */
56 xor %eax, %eax
57 mov %ds, %ax
58 shl $4, %eax
59 add $(LABEL_GDT), %eax /* eax <- gdt base*/
60 movl %eax, (GdtPtr + 2)
61
62 /* Load GDTR(Global Descriptor Table Register) */
63 lgdtw GdtPtr
-----------------
Fig 3.7: 加载GDT(节自chapter3/1/loader.S)

图3.7 中GdtPtr 所指,即为GDT 的界限和基址所存放位置。某段描述符对应的GDT 选择子,就是其段描述符相对于GDT 基址的索引(在我们例子里GDT 基址为LABEL GDT 指向的位置)。这里需要注意的是,虽然我们在代码中写:

.set SelectorCode32, (LABEL_DESC_CODE32 - LABEL_GDT)

但实际上段选择子在使用时需要右移3 个位作为索引去寻找其对应的段描述符,段选择子的右侧3 个位是为了标识TI 和RPL 的,如图3.12 所示,这点我们将在第3.3.1 节和第3.3.2 节中详细介绍。但是这里为什么能直接用地址相减得到段选择子呢?因为段描述符的大小是8 个字节,用段描述符的地址相减的话,地址差的最右侧三个位就默认置0 了。

在图3.7 中所示的代码,主要干了两件事:第一,将图3.6 所示demo 的段基址放入GDT 中对应的段描述符中;第二,将GDT 的基址放到GdtPtr 所指的数据结构中,并加载GdtPtr 所指的数据结构到GDTR 寄存器中(使用lgdt 指令)。


3.2

.4 进入保护模式

进入保护模式前,我们需要将中断关掉,因为保护模式下中断处理的机制和实模式是不一样的,不关掉中断可能带来麻烦。使用cli 汇编指令可以清除所有中断flag。

由于实模式下仅有20 条地址线:A0, A1, . . . , A19,所以当我们要进入保护模式时,需要打开A20 地址线。打开A20 地址线有至少三种方法,我们这里采用IBM 使用的方法,通常被称为:“Fast A20 Gate”,即修改系统控制端口92h ,因为其端口的第1 位控制着A20 地址线,所以我们只需要将0b00000010 赋给端口92h 即可。

当前面两项工作完成后,我们就可以进入保护模式了。方法很简单,将cr0 寄存器的第0 位PE 位置为1 即可使CPU 切换到保护模式下运行。

------------------
64
65 /* Clear Interrupt Flags */
66 cli
67
68 /* Open A20 line. */
69 inb $0x92, %al
70 orb $0b00000010, %al
71 outb %al, $0x92
72
73 /* Enable protect mode, PE bit of CR0. */
74 movl %cr0, %eax
75 orl $1, %eax
76 movl %eax, %cr0
77
-------------------
Fig 3.8: 进入保护模式(节自chapter3/1/loader.S)

3.2.5 特别的混合跳转指令

虽然已经进入了保护模式,但由于我们的CS 寄存器存放的仍然是实模式下16 位的段信息,要跳转到我们的demo 程序并不是那么简单的事情。因为demo 程序是32 位的指令,而我们现在仍然运行的是16 位的指令。从16 位的代码段中跳转到32 位的代码段,不是一般的near 或far 跳转指令能解决得了的,所以这里我们需要一个特别的跳转指令。在这条指令运行之前,所有的指令都是16 位的,在它运行之后,就变成32 位指令的世界。

在Intel 的手册中,把这条混合跳转指令称为far jump(ptr16:32) ,在NASM 手册中,将这条指令称为Mixed-Size Jump ,我们就沿用NASM 的说法,将这条指令称为混合字长跳转指令。NASM 提供了这条指令的汇编语言实现:

jmp dword 0x1234:0x56789ABC

NASM 的手册中说GAS 没有提供这条指令的实现,我就用.byte 伪代码直接写了二进制指令:

/* Mixed-Size Jump. */
.2byte 0xea66
.4byte 0x00000000
.2byte SelectorCode32

但是有位朋友提醒我说现在的GAS 已经支持混合字长跳转指令(如图3.9),看来NASM 的手册好久没有维护喽, 。

--------------
77
78 /* Mixed-Size Jump. */
79 ljmpl $SelectorCode32, $0 /* Thanks to earthengine@gmail, I got */
80 /* this mixed-size jump insn of gas. */
81
82 LABEL_SEG_CODE32:
Fig 3.9: 混合字长跳转指令(节自chapter3/1/loader.S)
--------------

执行这条混合字长的跳转指令时,CPU 就会用段选择子SelectorCode32 去寻找GDT 中对应的段,由于段偏移是0 ,所以CPU 将跳转到图3.6 中demo 程序的开头。为了方便阅读,整个loader.S 的代码附在图3.10 中:

-------------------------
1 /* chapter3/1

/loader.S
2
3 Author: Wenbo Yang
4
5 This file is part of the source code of book "Write Your Own OS with Free
6 and Open Source Software". Homepage @ .
7
8 This file is licensed under the GNU General Public License; either
9 version 3 of the License, or (at your option) any later version. */
10
11 #include "pm.h"
12
13 .code16
14 .text
15 jmp LABEL_BEGIN /* jump over the .data section. */
16
17 /* NOTE! Wenbo-20080512: Actually here we put the normal .data section into
18 the .code section. For application SW, it is not allowed. However, we are
19 writing an OS. That is OK. Because there is no OS to complain about
20 that behavior. :) */
21
22 /* Global Descriptor Table */
23 LABEL_GDT: Descriptor 0, 0, 0
24 LABEL_DESC_CODE32: Descriptor 0, (SegCode32Len - 1), (DA_C + DA_32)
25 LABEL_DESC_VIDEO: Descriptor 0xB8000, 0xffff, DA_DRW
26
27 .set GdtLen, (. - LABEL_GDT) /* GDT Length */
28
29 GdtPtr: .2byte (GdtLen - 1) /* GDT Limit */
30 .4byte 0 /* GDT Base */
31
32 /* GDT Selector */
33 .set SelectorCode32, (LABEL_DESC_CODE32 - LABEL_GDT)
34 .set SelectorVideo, (LABEL_DESC_VIDEO - LABEL_GDT)
35
36 /* Program starts here. */
37 LABEL_BEGIN:
38 mov %cs, %ax /* Move code segment address(CS) to data segment */
39 mov %ax, %ds /* register(DS), ES and SS. Because we have */
40 mov %ax, %es /* embedded .data section into .code section in */
41 mov %ax, %ss /* the start(mentioned in the NOTE above). */
42
43 mov $0x100, %sp
44
45 /* Initialize 32-bits code segment descriptor. */
46 xor %eax, %eax
47 mov %cs, %ax
48 shl $4, %eax
49 addl $(LABEL_SEG_CODE32), %eax
50 movw %ax, (LABEL_DESC_CODE32 + 2)
51 shr $16, %eax
52 movb %al, (LABEL_DESC_CODE32 + 4)
53 movb %ah, (LABEL_DESC_CODE32 + 7)
54
55 /* Prepared for loading GDTR */
56 xor %eax, %eax
57 mov %ds, %ax
58 shl $4, %eax
59 add $(LABEL_GDT), %eax /* eax <- gdt base*/
60 movl %eax, (GdtPtr + 2)
61
62 /* Load GDTR(Global Descriptor Table Register) */
63 lgdtw GdtPtr
64
65 /* Clear Interrupt Flags */
66 cli
67
68 /* Open A20 line. */
69 inb $0x92, %al
70 orb $0b00000010, %al
71 outb %al, $0x92
72
73 /* Enable protect mode, PE bit of CR0. */
74 movl %cr0, %eax
75 orl $1, %eax
76 movl %eax, %cr0
77
78 /* Mixed-Size Jump. */
79 ljmpl $SelectorCode32, $0 /* Thanks to earthengine@gmail, I got */
80 /* this mixed-size jump insn of gas. */
81
82 LABEL_SEG_CODE32:
83 .code32
84 mov $(SelectorVideo), %ax
85 mov %ax, %gs /* Video segment selector(dest) */
86
87 movl $((80 * 10 + 0) * 2), %edi
88 movb $0xC, %ah /* 0000: Black Back 1100: Red Front */
89 movb $’P’, %al
90
91 mov %ax, %gs:(%edi)
92
93 /* Stop here, infinite loop. */
94 jmp .
95
96 /* Get the length of 32-bit segment code. */
97 .set SegCode32Len, . - LABEL_SEG_CODE32
------------------
Fig 3.10: chapter3/1/loader.S


3.2.6 生成镜像并测试

使用与第2.3.6 节完全相同

的方法,我们可以将代码编译并将LOADER.BIN 拷贝到镜像文件中。利用最新的镜像文件启动VirtualBox 我们得到图3.11 。

可以看到,屏幕的左侧中央打出了一个红色的P ,这就是我们那个在保护模式下运行的简单demo所做的事情,这说明我们的代码是正确的。从实模式迈入保护模式,这只是一小步,但对于我们的操作系统来说,这是一大步。从此我们不必再被限制到20 位的地址空间中,有了更大的自由度。

3.3 段式存储
如果您仔细阅读了图3.1b ,您就会发现图中并未提到GDT ,而是使用的Descriptor Table(DT) 。这是因为对于x86 架构的CPU 来说, DT 总共有两个:我们上节介绍过的GDT 和下面要介绍的LDT。这两个描述符表构成了x86 CPU 段式存储的基础。顾名思义, GDT 作为全局的描述符表,只能有一个,而LDT 作为局部描述符表,就可以有很多个,这也是以后操作系统给每个任务分配自己的存储空间的基础。


3.3.1 LDT 数据结构

事实上, LDT 和GDT 的差别非常小, LDT 段描述符的数据结构和图3.2 所示是一样的。所不同的就是, LDT 用指令lldt 来加载,并且指向LDT 描述符项的段选择子的TI 位置必须标识为1 ,如图3.12 所示。这样,在使用TI flag := 1 的段选择子时,操作系统才会从当前的LDT 而不是GDT 中去寻找对应的段描述符。

这里值得注意的一点是:GDT 是由线性空间里的地址定义的,即lgdt 指令的参数是一个线性空间的地址;而LDT 是由GDT 中的一个段描述符定义的,即lldt 指令的参数是GDT 中的一个段选择子。这是因为在加载GDT 之前寻址模式是实模式的,而加载GDT 后寻址模式变成保护模式寻址,将LDT 作为GDT 中的段使用,也方便操作系统在多个LDT 之间切换。


3.3.2 段描述符属性

我们在介绍图3.2 时,并没有完全介绍段描述符的各个Flag 和可能的属性,这一小节就用来专门
介绍段描述符的属性,按照图3.2 中的Flag 从左向右的顺序:

2 G: G(Granularity,粒度):如果G flag 置为0 ,段的大小以字节为单位,段长度范围是1 byte?1MB ;如果G flag 置为1 ,段的大小以4 KB 为单位,段长度范围是4 KB ? 4 GB 。

2 D/B:D/B(Default operation size/Default stack pionter size and/or upper Bound,默认操作大小),其意思取决于段描述符是代码段、数据段或者堆栈段。该flag 置为0 代表代码段/数据段为16 位的;置为1 代表该段是32 位的。

2 L:L(Long, 长), L flag 是IA-32e(Extended Memory 64 Technology) 模式下使用的标志。该flag置为1 代表该段是正常的64 位的代码段;置为0 代表在兼容模式下运行的代码段。在IA-32 架构下,该位是保留位,并且永远被置为0 。

2 AVL:保留给操作系统软件使用的位。

2 P:P(se

gment-Present,段占用?) flag 用于标志段是否在内存中,主要供内存管理软件使用。如果P flag 被置为0 ,说明该段目前不在内存中,该段指向的内存可以暂时被其它任务占用;如果P flag 被置为1 ,说明该段在内存中。如果P flag 为0 的段被访问,处理机会产生一个segment-not-present(#NP) 异常。


2 DPL:DPL(Descriptor Privilege Level)域标志着段的特权级,取值范围是从0?3(2-bit) ,0 代表着最高的特权级。关于特权级的作用,我们将在下节讨论。

2 S:S(descriptor type) flag 标志着该段是否系统段:置为0 代表该段是系统段;置为1 代表该段是代码段或者数据段。

2 Type:Type 域是段描述符里最复杂的一个域,而且它的意义对于代码/数据段描述符和系统段/门描述符是不同的,下面我们用两张表来展示当Type 置为不同值时的意义。

表3.1 所示即为代码/数据段描述符的所有Type 可能的值(0-15, 4-bit)以及对应的属性含意,表3.2 所示为系统段/门描述符的Type 可能的值以及对应的属性含意。这两张表每个条目的内容是自明的,而且我们在后面的讨论中将不止一次会引用这两张表的内容,所以这里对每个条目暂时不加详细阐述。

表3.1: 代码/数据段描述符的Type 属性列表(参见图片)
表3.2: 系统段/门描述符的Type 属性列表(参见图片)


3.3.3 使用LDT

从目前的需求来看,对LDT 并没有非介绍不可的理由,但是理解LDT 的使用,对理解段式存储和处理机多任务存储空间分配有很大的帮助。所以我们在下面的代码中实现几个简单的例子:一,建立32 位数据和堆栈两个段并将描述符添加到GDT 中;二,添加一段简单代码,并以其段描述符为基础建立一个LDT;三,在GDT 中添加LDT 的段描述符并初始化所有DT ;四,进入保护模式下运行的32位代码段后,加载LDT 并跳转执行LDT 中包含的代码段。

首先,建立32 位全局数据段和堆栈段,并将其描述符添加到GDT 中:
-------------------------
50 /* 32-bit global data segment. */
51 LABEL_DATA:
52 PMMessage: .ascii "Welcome to protect mode! ^-^\0"
53 LDTMessage: .ascii "Aha, you jumped into a LDT segment.\0"
54 .set OffsetPMMessage, (PMMessage - LABEL_DATA)
55 .set OffsetLDTMessage, (LDTMessage - LABEL_DATA)
56 .set DataLen, (. - LABEL_DATA)
57
58 /* 32-bit global stack segment. */
59 LABEL_STACK:
60 .space 512, 0
61 .set TopOfStack, (. - LABEL_STACK - 1)
62
22 /* Global Descriptor Table */
23 LABEL_GDT: Descriptor 0, 0, 0
24 LABEL_DESC_CODE32: Descriptor 0, (SegCode32Len - 1), (DA_C + DA_32)
25 LABEL_DESC_DATA: Descriptor 0, (DataLen - 1), DA_DRW
26 LABEL_DESC_STACK: Descriptor 0, TopOfStack, (DA_DRWA + DA_32)
27 LABEL_DESC_VIDEO: Descriptor 0xB8000, 0xffff, DA_DRW
28 LABEL_DESC_LDT: Descriptor 0, (LDTLen - 1), DA_LDT
29


30 .set GdtLen, (. - LABEL_GDT) /* GDT Length */
31
32 GdtPtr: .2byte (GdtLen - 1) /* GDT Limit */
33 .4byte 0 /* GDT Base */
34
35 /* GDT Selector(TI flag clear) */
36 .set SelectorCode32, (LABEL_DESC_CODE32 - LABEL_GDT)
37 .set SelectorData, (LABEL_DESC_DATA - LABEL_GDT)
38 .set SelectorStack, (LABEL_DESC_STACK - LABEL_GDT)
39 .set SelectorVideo, (LABEL_DESC_VIDEO - LABEL_GDT)
40 .set SelectorLDT, (LABEL_DESC_LDT - LABEL_GDT)
41
--------------------
Fig 3.13: 32 位全局数据段和堆栈段,以及对应的GDT 结构(节自chapter3/2/loader.S)

在图3.13 中,我们首先建立了一个全局的数据段,并在数据段里放置了两个字符串,分别用来进
入保护模式后和跳转到LDT 指向的代码段后作为信息输出。然后又建立了一个全局的堆栈段,为堆栈段预留了512 字节的空间,并将栈顶设置为距栈底511 字节处。然后与上节介绍的类似,将数据段和堆栈段的段描述符添加到GDT 中,并设置好对应的段选择子。


要注意到数据段、堆栈段和代码段的段描述符属性不尽相同。数据段的段描述符属性是DA DRW ,
回忆我们前面pm.h 的内容(图3.5 ), DA DRW 的内容是0x92,用二进制就是10010010,其后四位就对应着图3.1 中的第二2(0010) 项,说明这个段是可读写的数据段;前四位对应着P|DPL|S 三个flag,即P:1, DPL:00, S:1 ,与第3.3.2 节结合理解,意思就是该段在内存中,为最高的特权级,非系统段。所以我们可以看到pm.h 中的各个属性变量定义,就是将二进制的属性值用可理解的变量名表示出来,在用的时候直接加上变量即可。

同理我们也可以分别来理解GDT 中堆栈段和代码段描述符的属性定义。因为不同类型的属性使用的是段描述符中不同的位,所以不同类型的属性可以直接相加得到复合的属性值,例如堆栈段的(DA DRWA + DA 32) ,其意思类似于C++ 中fstream 打开文件时可以对模式进行算术或(ios base::in| ios base::out)来得到复合参数。

其次,添加一段简单的代码,并以其描述符为基础建立一个LDT:

------------------
114 /* 32-bit code segment for LDT */
115 LABEL_CODEA:
116 .code32
117 mov $(SelectorVideo), %ax
118 mov %ax, %gs
119
120 movb $0xC, %ah /* 0000: Black Back 1100: Red Front */
121 xor %esi, %esi
122 xor %edi, %edi
123 movl $(OffsetLDTMessage), %esi
124 movl $((80 * 12 + 0) * 2), %edi
125 cld /* Clear DF flag. */
126
127 /* Display a string from %esi(string offset) to %edi(video segment). */
128 CODEA.1:
129 lodsb /* Load a byte from source */
130 test %al, %al
131 jz CODEA.2
132 mov %ax, %gs:(%edi)
133 add $2, %edi
134 jmp CODEA.1
135 CODEA.2:
136
137 /* Stop here, infinite loop. */
138 jmp .
139 .set CodeALen, (. - LABEL_CODEA)
140
42 /* LDT segment */
43 LABEL_LDT:
44 LABEL_LDT_DESC_CODEA: Descriptor 0, (CodeALen - 1), (DA_C + D

A_32)
45
46 .set LDTLen, (. - LABEL_LDT) /* LDT Length */
47 /* LDT Selector (TI flag set)*/
48 .set SelectorLDTCodeA, (LABEL_LDT_DESC_CODEA - LABEL_LDT + SA_TIL)
49
------------
Fig 3.14: 32 位代码段,以及对应的LDT 结构(节自chapter3/2/loader.S)

LABEL CODEA 就是我们为LDT 建立的简单代码段,其作用就是操作显存在屏幕的第12 行开始用红色的字打印出偏移OffsetLDTMessage 指向的全局数据段中的字符串。下面就是以LABEL CODEA 为基础建立的LDT ,从LDT 的结构来说,与GDT 没有区别,但是我们不用像GdtPtr 再建立一个LdtPtr ,因为LDT 实际上是在GDT 中定义的一个段,不用实模式的线性地址表示。

LDT 的选择子是与GDT 选择子有明显区别的,图3.12 清楚地解释了这一点,所以指向LDT 的选择子都应该将TI 位置1 ,在图3.14 的最后一行也实现了这一操作。

第三,在GDT 中添加LDT 的段描述符(在图3.13 中我们已经能看到在GDT 中添加好了LDT 的段描述符),初始化所有段描述符。由于初始化段描述符属于重复性工作,我们在pm.h 中添加一个汇编宏InitDesc 来帮我们做这件事情。
-----------
84 /* Initialize descriptor.
85 Usage: InitDesc SegLabel, SegDesc */
86 .macro InitDesc SegLabel, SegDesc
87 xor %eax, %eax
88 mov %cs, %ax
89 shl $4, %eax
90 addl $(\SegLabel), %eax
91 movw %ax, (\SegDesc + 2)
92 shr $16, %eax
93 movb %al, (\SegDesc + 4)
94 movb %ah, (\SegDesc + 7)
95 .endm
96
-------------
Fig 3.15: 自动初始化段描述符的宏代码(节自chapter3/2/pm.h)

-----------------
63 /* Program starts here. */
64 LABEL_BEGIN:
65 mov %cs, %ax /* Move code segment address(CS) to data segment */
66 mov %ax, %ds /* register(DS), ES and SS. Because we have */
67 mov %ax, %es /* embedded .data section into .code section in */
68 mov %ax, %ss /* the start(mentioned in the NOTE above). */
69
70 mov $0x100, %sp
71
72 /* Initialize 32-bits code segment descriptor. */
73 InitDesc LABEL_SEG_CODE32, LABEL_DESC_CODE32
74
75 /* Initialize data segment descriptor. */
76 InitDesc LABEL_DATA, LABEL_DESC_DATA
77
78 /* Initialize stack segment descriptor. */
79 InitDesc LABEL_STACK, LABEL_DESC_STACK
80
81 /* Initialize LDT descriptor in GDT. */
82 InitDesc LABEL_LDT, LABEL_DESC_LDT
83
84 /* Initialize code A descriptor in LDT. */
85 InitDesc LABEL_CODEA, LABEL_LDT_DESC_CODEA
86
87 /* Prepared for loading GDTR */
88 xor %eax, %eax
89 mov %ds, %ax
90 shl $4, %eax
91 add $(LABEL_GDT), %eax /* eax <- gdt base*/
92 movl %eax, (GdtPtr + 2)
93
94 /* Load GDTR(Global Descriptor Table Register) */
95 lgdtw GdtPtr
96
97 /* Clear Interrupt Flags */
98 cli
99
100 /* Open A20 line. */
101 inb $0x92, %al
102 orb $0b00000010, %al
103 outb %al, $0x92
104
105 /* Enable protect mode, PE bit of CR0. */
106 movl %cr0, %eax
107 orl $1, %eax
108 movl %eax, %cr0
109
110 /* Mixed-Size Jump. */
111 ljmpl $S

electorCode32, $0 /* Thanks to earthengine@gmail, I got */
112 /* this mixed-size jump insn of gas. */
------------------
Fig 3.16: 在实模式代码段中初始化所有段描述符(节自chapter3/2/loader.S)

初始化各个段描述符的方式与上一节介绍的初始化GDT 描述符的方式没有什么本质不同,因为属性都已经预设好,运行时只需要将段地址填入描述符中的地址域即可,代码都是重复的。我们引入宏InitDesc的帮助,能大大缩短代码长度,增强代码的可读性。

第四,进入保护模式下运行的32 位代码段后,加载LDT 并跳转执行LDT 中包含的代码段:

-------------------
141 /* 32-bit code segment for GDT */
142 LABEL_SEG_CODE32:
143 mov $(SelectorData), %ax
144 mov %ax, %ds /* Data segment selector */
145 mov $(SelectorStack), %ax
146 mov %ax, %ss /* Stack segment selector */
147 mov $(SelectorVideo), %ax
148 mov %ax, %gs /* Video segment selector(dest) */
149
150 mov $(TopOfStack), %esp
151
152 movb $0xC, %ah /* 0000: Black Back 1100: Red Front */
153 xor %esi, %esi
154 xor %edi, %edi
155 movl $(OffsetPMMessage), %esi
156 movl $((80 * 10 + 0) * 2), %edi
157 cld /* Clear DF flag. */
158
159 /* Display a string from %esi(string offset) to %edi(video segment). */
160 CODE32.1:
161 lodsb /* Load a byte from source */
162 test %al, %al
163 jz CODE32.2
164 mov %ax, %gs:(%edi)
165 add $2, %edi
166 jmp CODE32.1
167 CODE32.2:
168
169 mov $(SelectorLDT), %ax
170 lldt %ax
171
172 ljmp $(SelectorLDTCodeA), $0
173
174 /* Get the length of 32-bit segment code. */
175 .set SegCode32Len, . - LABEL_SEG_CODE32
------------------
Fig 3.17: 在保护模式代码段中加载LDT 并跳转执行LDT 代码段(节自chapter3/2/loader.S)

在LABEL SEG CODE32 中前几行,我们可以看到非常熟悉的汇编指令,和一般汇编程序开头初始化数据/代码/堆栈段寄存器的指令非常像,只不过这里赋给几个寄存器的参数都是段选择子,而不是一般的地址。该代码段剩下的内容和前面图3.14 中LABEL CODEA 一样,都是打印一个字符串,只不过这里选择在第10 行(屏幕左侧中央)打印。


为了方便阅读,整个loader.S 的代码附在图3.18 中。
-----------------
1 /* chapter3/2/loader.S
2
3 Author: Wenbo Yang
4
5 This file is part of the source code of book "Write Your Own OS with Free
6 and Open Source Software". Homepage @ .
7
8 This file is licensed under the GNU General Public License; either
9 version 3 of the License, or (at your option) any later version. */
10
11 #include "pm.h"
12
13 .code16
14 .text
15 jmp LABEL_BEGIN /* jump over the .data section. */
16
17 /* NOTE! Wenbo-20080512: Actually here we put the normal .data section into
18 the .code section. For application SW, it is not allowed. However, we are
19 writing an OS. That is OK

. Because there is no OS to complain about
20 that behavior. :) */
21
22 /* Global Descriptor Table */
23 LABEL_GDT: Descriptor 0, 0, 0
24 LABEL_DESC_CODE32: Descriptor 0, (SegCode32Len - 1), (DA_C + DA_32)
25 LABEL_DESC_DATA: Descriptor 0, (DataLen - 1), DA_DRW
26 LABEL_DESC_STACK: Descriptor 0, TopOfStack, (DA_DRWA + DA_32)
27 LABEL_DESC_VIDEO: Descriptor 0xB8000, 0xffff, DA_DRW
28 LABEL_DESC_LDT: Descriptor 0, (LDTLen - 1), DA_LDT
29
30 .set GdtLen, (. - LABEL_GDT) /* GDT Length */
31
32 GdtPtr: .2byte (GdtLen - 1) /* GDT Limit */
33 .4byte 0 /* GDT Base */
34
35 /* GDT Selector(TI flag clear) */
36 .set SelectorCode32, (LABEL_DESC_CODE32 - LABEL_GDT)
37 .set SelectorData, (LABEL_DESC_DATA - LABEL_GDT)
38 .set SelectorStack, (LABEL_DESC_STACK - LABEL_GDT)
39 .set SelectorVideo, (LABEL_DESC_VIDEO - LABEL_GDT)
40 .set SelectorLDT, (LABEL_DESC_LDT - LABEL_GDT)
41
42 /* LDT segment */
43 LABEL_LDT:
44 LABEL_LDT_DESC_CODEA: Descriptor 0, (CodeALen - 1), (DA_C + DA_32)
45
46 .set LDTLen, (. - LABEL_LDT) /* LDT Length */
47 /* LDT Selector (TI flag set)*/
48 .set SelectorLDTCodeA, (LABEL_LDT_DESC_CODEA - LABEL_LDT + SA_TIL)
49
50 /* 32-bit global data segment. */
51 LABEL_DATA:
52 PMMessage: .ascii "Welcome to protect mode! ^-^\0"
53 LDTMessage: .ascii "Aha, you jumped into a LDT segment.\0"
54 .set OffsetPMMessage, (PMMessage - LABEL_DATA)
55 .set OffsetLDTMessage, (LDTMessage - LABEL_DATA)
56 .set DataLen, (. - LABEL_DATA)
57
58 /* 32-bit global stack segment. */
59 LABEL_STACK:
60 .space 512, 0
61 .set TopOfStack, (. - LABEL_STACK - 1)
62
63 /* Program starts here. */
64 LABEL_BEGIN:
65 mov %cs, %ax /* Move code segment address(CS) to data segment */
66 mov %ax, %ds /* register(DS), ES and SS. Because we have */
67 mov %ax, %es /* embedded .data section into .code section in */
68 mov %ax, %ss /* the start(mentioned in the NOTE above). */
69
70 mov $0x100, %sp
71
72 /* Initialize 32-bits code segment descriptor. */
73 InitDesc LABEL_SEG_CODE32, LABEL_DESC_CODE32
74
75 /* Initialize data segment descriptor. */
76 InitDesc LABEL_DATA, LABEL_DESC_DATA
77
78 /* Initialize stack segment descriptor. */
79 InitDesc LABEL_STACK, LABEL_DESC_STACK
80
81 /* Initialize LDT descriptor in GDT. */
82 InitDesc LABEL_LDT, LABEL_DESC_LDT
83
84 /* Initialize code A descriptor in LDT. */
85 InitDesc LABEL_CODEA, LABEL_LDT_DESC_CODEA
86
87 /* Prepared for loading GDTR */
88 xor %eax, %eax
89 mov %ds, %ax
90 shl $4, %eax
91 add $(LABEL_GDT), %eax /* eax <- gdt base*/
92 movl %eax, (GdtPtr + 2)
93
94 /* Load GDTR(Global Descriptor Table Register) */
95 lgdtw GdtPtr
96
97 /* Clear Interrupt Flags */
98 cli
99
100 /* Open A20 line. */
101 inb $0x92, %al
102 orb $0b00000010, %al
103 outb %al, $0x92
104
105 /* Enable protect mode, PE bit of CR0. */
106 movl %cr0, %eax
107 orl $1, %eax
108 movl %eax, %cr0
109
110 /* Mixed-Size Jump. */
111 ljmpl $SelectorCode32, $0 /* Thanks to earthe

ngine@gmail, I got */
112 /* this mixed-size jump insn of gas. */
113
114 /* 32-bit code segment for LDT */
115 LABEL_CODEA:
116 .code32
117 mov $(SelectorVideo), %ax
118 mov %ax, %gs
119
120 movb $0xC, %ah /* 0000: Black Back 1100: Red Front */
121 xor %esi, %esi
122 xor %edi, %edi
123 movl $(OffsetLDTMessage), %esi
124 movl $((80 * 12 + 0) * 2), %edi
125 cld /* Clear DF flag. */
126
127 /* Display a string from %esi(string offset) to %edi(video segment). */
128 CODEA.1:
129 lodsb /* Load a byte from source */
130 test %al, %al
131 jz CODEA.2
132 mov %ax, %gs:(%edi)
133 add $2, %edi
134 jmp CODEA.1
135 CODEA.2:
136
137 /* Stop here, infinite loop. */
138 jmp .
139 .set CodeALen, (. - LABEL_CODEA)
140
141 /* 32-bit code segment for GDT */
142 LABEL_SEG_CODE32:
143 mov $(SelectorData), %ax
144 mov %ax, %ds /* Data segment selector */
145 mov $(SelectorStack), %ax
146 mov %ax, %ss /* Stack segment selector */
147 mov $(SelectorVideo), %ax
148 mov %ax, %gs /* Video segment selector(dest) */
149
150 mov $(TopOfStack), %esp
151
152 movb $0xC, %ah /* 0000: Black Back 1100: Red Front */
153 xor %esi, %esi
154 xor %edi, %edi
155 movl $(OffsetPMMessage), %esi
156 movl $((80 * 10 + 0) * 2), %edi
157 cld /* Clear DF flag. */
158
159 /* Display a string from %esi(string offset) to %edi(video segment). */
160 CODE32.1:
161 lodsb /* Load a byte from source */
162 test %al, %al
163 jz CODE32.2
164 mov %ax, %gs:(%edi)
165 add $2, %edi
166 jmp CODE32.1
167 CODE32.2:
168
169 mov $(SelectorLDT), %ax
170 lldt %ax
171
172 ljmp $(SelectorLDTCodeA), $0
173
174 /* Get the length of 32-bit segment code. */
175 .set SegCode32Len, . - LABEL_SEG_CODE32
--------------------
Fig 3.18: chapter3/2/loader.S

3.3.4 生成镜像并测试

使用与第2.3.6 节完全相同的方法,我们可以将代码编译并将LOADER.BIN 拷贝到镜像文件中。利用最新的镜像文件启动VirtualBox 我们得到图3.19 。

Fig 3.19: 第一次进入保护模式
可以看到,该程序首先在屏幕左侧中央(第10 行)打印出来"Welcome to protect mode!^-^",这是由GDT 中的32 位代码段LABEL SEG CODE32 打印出来的,标志着我们成功进入保护模式;然后在屏幕的第12 行打印出来"Aha, you jumped into a LDT segment." ,这个是由LDT 中的32 位代码段LABEL CODEA 打印出来的,标志着LDT 的使用正确。因为这两个字符串都是被存储在32 位全局数据段中,这两个字符串的成功打印也说明在GDT 中添加的数据段使用正确。


3.3.5 段式存储总结

段式存储和页式存储都是最流行的计算机内存保护方式。段式存储的含义简单来说就是先将内存分为各个段,然后再分配给程序供不同用途使用,并保证对各个段的访问互不干扰。x86 主要使用段寄存器(得到的段基址) + 偏移量来访问段中数据,也简化了寻址过程。

在x86 的初期实模式

下就使用着非常简单的段式存储方式,如图3.1a 所示,这种模式下分段主要是为了方便寻址和隔离,没有提供其它的保护机制。x86 保护模式下采用了更高级的段式存储方式:用全局和局部描述符表存储段描述符信息,使用段选择子表示各个段描述符,如图3.1b 所示。

由于保护模式使用段描述符来保存段信息而不是像实模式一样直接使用段地址,在段描述符中就可以添加一些属性来限制对段的访问权限,如我们在第3.3.2 节中讨论的那样。这样,通过在访问段时检查权限和属性,就能做到对程序段的更完善保护和更好的内存管理。

x86 使用全局描述符表(GDT)和局部描述符表(LDT)来实现不同需求下对程序段的控制,操作系统使用唯一的一个GDT 来维护一些和系统密切相关的段描述符信息,为不同的任务使用不同的LDT 来实现对多任务内存管理的支持,简化了任务切换引起的内存切换的难度。


3.4 特权级

如果您需要更详细的知识,也许您更愿意去读Intel 的手册,本节内容主要集中在:Intel R ° 64 and IA-32 Architectures Software Developer’s Manual, Volume 3A: System Programming Guide, 第4 章.

特权级是为了保护处理机资源而引入的概念。将同一个处理机上执行的不同任务赋予不同的特权级,可以控制该任务可以访问的资源,比如内存地址范围、输入输出端口、和一些特殊指令的使用。在x86 体系结构中,共有4 个特权级别,0 代表最高特权级,3 代表最低特权级。由于在x86 体系结构中,n 级可以访问的资源均可以被0 到n 级访问,这个模式被称作ring 模式,相应地我们也将x86 的对应特权级称作ring n。

现代的PC 操作系统的内核一般工作在ring 0 下,拥有最高的特权级,应用程序一般工作在ring 3下,拥有最低的特权级。虽然x86 体系结构提供了4 个特权级,但操作系统并不需要全部使用到这4 个级别,可以根据需要来选择使用几个特权级。比如Linux/Unix 和Windows NT ,都是只使用了0 级和3 级,分别用于内核模式和用户模式;而DOS 则只使用了0 级。

为了实施对代码段和数据段的特权级检验,x86 处理机引入了以下三种特权级类型(请注意这里提到的特权级高低均为实际高低,而非数值意义上的高低):

2 CPL(Current Privilege Level):当前特权级,存储在CS 和SS 的0, 1 位。它代表当前执行程序或任务的特权级,通常情况下与当前执行指令所在代码段的DPL 相同。当程序跳转到不同特权级的代码段时,CPL 会随之修改。当访问一致代码段(Conforming Code Segment)时,对CPL的处理有些不同。一致代码段可以被不高于(数值上大于等于)该段DPL 的特权级代码访问,但是,CPL 在访问

一致代码段时不会跟随DPL 的变化而更改。

2 DPL(Descriptor Privilege Level):描述符特权级,定义于段描述符或门描述符中的DPL 域(见图3.2),它限制了可以访问此段资源的特权级别。根据被访问的段或者门的不同,DPL 的意义也不同:

– 数据段:数据段的DPL 限制了可以访问该数据段的最低特权级。假如数据段的DPL 为1,
那么只有CPL 为0,1 的程序才能访问该数据段。
– 非一致代码段(不使用调用门):非一致代码段就是一般的代码段,它的DPL 表示可以访
问该段的特权级,程序或者任务的特权级必须与该段的DPL 完全相同才可以访问该段。
– 调用门:调用门的DPL 限制了可以访问该门的最低特权级,与数据段DPL 的意思一样。
– 一致代码段和使用调用门访问的非一致代码段:这种代码段的DPL 表示可以访问该段的最
高特权级。假如一致代码段的DPL 是2,那么CPL 为0,1 的程序就无法访问该段。
– TSS(Task State Segment):任务状态段的DPL 表示可以访问该段的最低特权级,与数
据段DPL 的意思一样。

2 RPL(Requested Privilege Level):请求特权级,定义于段选择子的RPL 域中(见图3.12)。它限制了这个选择子可访问资源的最高特权级。比如一个段选择子的RPL 为2 ,那么使用这个段选择子只能访问DPL 为2 或者3 的段,即使使用这个段选择子的程序当前特权级(CPL)为0 。就是说,max (CPL,RPL) · DPL 才被允许访问该段,即当CPL 小于RPL 时,RPL 起决定性作用,反之亦然。使用RPL 可以避免特权级高的程序代替应用程序访问该应用程序无权访问的段。比如在系统调用时,应用程序调用系统过程,虽然系统过程的特权级高(CPL = 0),但是被调用的系统过程仍然无法访问特权级高于应用程序的段(DPL < RPL = 3),就避免了可能出现的安全问题。

在将段描述符对应的段选择子加载到段寄存器时,处理机通过将CPL, 段选择子的RPL 和该段的DPL 相比较,来判断程序是否有权访问另外一个段。如果CPL > max (RPL,DPL) ,或者max (CPL,RPL) > DPL,那么该访问就是不合法的,处理机就会产生一个常规保护异常(#GP,General Protection Exception)。



3.4.1 不合法的访问请求示例

我们来看一个不合法的访问请求的例子,在上一节的loader.S 中把LABEL DESC DATA 对应的描述符的DPL 设置为1,然后将该数据段对应的段选择子的RPL 设置为3,即修改以下两行:

LABEL_DESC_DATA: Descriptor 0, (DataLen - 1), (DA_DRW + DA_DPL1)
.set SelectorData, (LABEL_DESC_DATA - LABEL_GDT + SA_RPL3)

再make, sudo make copy,用VirtualBox 加载生成的镜像运行一下,就会发现虚拟机黑屏一会儿就会退出(如图3.20),然后VirtualBox 主窗口中显示该

虚拟机Aborted(如图3.21)。这是因为我们违反特权级的规则,使用RPL=3 的选择子去访问DPL=1 的段,这个不合法的访问请求引起处理机产生常规保护异常(#GP)。而我们又没有准备对应的异常处理模块,当处理机找不到异常处理程序时就只好退出了


3.4.2 控制权转移的特权级检查

在将控制权从一个代码段转移到另一个代码段之前,目标代码段的段选择子必须被加载到CS 中。处理器在这个过程中会查看目标代码段的段描述符以及对其界限、类型(见图3.2)和特权级进行检查。如果没有错误发生,CS 寄存器会被加载,程序控制权被转移到新的代码段,从EIP 指示的位置开始运行。

JMP, CALL, RET, SYSENTER, SYSEXIT, INT n 和IRET 这些指令,以及中断和异常机制都会引起程序控制权的转移。

JMP 和CALL 指令可以实现以下4 种形式的转移:

2 目标操作数包含目标代码段的段选择子。
2 目标操作数指向一个包含目标代码段段选择子的调用门描述符。
2 目标操作数指向一个包含目标代码段段选择子的任务状态段。
2 目标操作数指向一个任务门,这个任务门指向一个包含目标代码段段选择子的任务状态段。

下面两个小节将描述前两种转移的实现方法,后两种控制权转移方法我们将在用到时再进行解释。

用JMP 或CALL 直接转移

用JMP, CALL 和RET 指令在段内进行近跳转并没有特权级的变化,所以对这类转移是不进行特
权级检查的;用JMP, CALL 和RET 在段间进行远跳转涉及到其它代码段,所以要进行特权级检查。
对不通过调用门的直接转移来说,又分为两种情形:

2 访问非一致代码段:当目标是非一致代码段时(目标段段描述符的C flag 为0 ,见图3.1),特权级检查要求调用者的CPL 与目标代码段的DPL 相等,而且调用者使用的目标代码段段选择子的RPL 必须小于等于目标代码段的DPL。我们之前的代码都属于这种情形,其中CPL = DPL = RPL = 0。

2 访问一致代码段:当目标是一致代码段时(目标段段描述符的C flag 为1 ,见图3.1),特权级检查要求CPL ? DPL,RPL 不被检查,而且转移时并不修改CPL。总的来说,通过JMP 和CALL 实行的都是一般的转移,最多从低特权级转移到高特权级的一致代码段,CPL 总是不变的。


3.4.3 使用调用门转移

调用门是x86 体系结构下用来控制程序在不同特权级间转移的一种机制。它的目的是使低特权级的代码能够调用高特权级的代码,这一机制在使用了内存保护和特权级机制的现代操作系统中非常有用,因为它允许应用程序在操作系统控制下安全地调用内核例程或者系统接口。

Fig 3.22: 调用门描述符(参见图片)

门其实也是一种描述符

,和段描述符类似。调用门描述符的数据结构如图3.22 所示。其实看起来这个调用门描述符的数据结构要比段描述符简单一些,至少从它的属性来说,没有段描述符多。我们仍然只关注最重要的部分:首先是段选择子(Segment Selector),指定了通过这个调用门访问的代码段;其次是段偏移量(Offset in Segment),指定了要访问代码段中的某个入口偏移;描述符特权级(DPL),代表此门描述符的特权级;P,代表此调用门是否可用;参数计数(Param. Count)记录了如果发生栈切换的话,有多少个选项参数会在栈间拷贝。

简单来说,调用门描述了由一个段选择子和一个偏移所指定的目标代码段中的一个地址,程序通过调用门将转移到这个地址。下面我们通过一个简单的例子来介绍一下调用门的基本使用方法。

简单的调用门转移举例

为了使用调用门,我们首先要给出一个目标段,然后用该目标段的信息初始化调用门的门描述符,最后用调用门的门选择子实现门调用。

添加一个目标段我们已经做过很多次,非常简单。首先在上一节loader.S 最后添加一个打印一个字符的代码段LABEL SEG CODECG,接着将该段的段描述符LABEL DESC CODECG 添加到GDT 中,然后为该段准备一个段选择子electorCodeCG,最后加入初始化该段描述符的代码:
--------------
197 /* 32-bit code segment for call gate destination segment */
198 LABEL_SEG_CODECG:
199 mov $(SelectorVideo), %ax
200 mov %ax, %gs
201
202 movl $((80 * 11 + 0) * 2), %edi /* line 11, column 0 */
203 movb $0xC, %ah /* 0000: Black Back 1100: Red Front */
204 movb $’C’, %al /* Print a ’C’ */
205
206 mov %ax, %gs:(%edi)
207 lret
208
209 /* Get the length of 32-bit call gate destination segment code. */
210 .set SegCodeCGLen, . - LABEL_SEG_CODECG
28 LABEL_DESC_LDT: Descriptor 0, (LDTLen - 1), DA_LDT
29 LABEL_DESC_CODECG: Descriptor 0, (SegCodeCGLen - 1), (DA_C + DA_32)
43 .set SelectorLDT, (LABEL_DESC_LDT - LABEL_GDT)
44 .set SelectorCodeCG, (LABEL_DESC_CODECG - LABEL_GDT)
92 /* Initialize call gate dest code segment descriptor. */
93 InitDesc LABEL_SEG_CODECG, LABEL_DESC_CODECG
94
----------
Fig 3.23: 添加调用门的目标段(节自chapter3/3/loader.S)

总的来看, LABEL SEG CODECG 指向的这个段和我们以前为了打印程序运行结果所使用的段没有本质不同,为了简单起见,这里我们仅仅让它打印一个字符’C’ 就返回。

用目标代码段LABEL SEG CODECG 的信息初始化调用门的门描述符LABEL CG TEST,以及门选择子SelectorCGTest。与汇编宏Descriptor 类似,我们这里使用汇编宏Gate 来初始化门描述符,宏Gate的定义可以在头文件pm.h 中找到:
--------------
71 /* Gate Descriptor data structure.
72 Usage: Gate Selector, Offset, PCount, Attr
73 Sel

ector: 2byte
74 Offset: 4byte
75 PCount: byte
76 Attr: byte */
77 .macro Gate Selector, Offset, PCount, Attr
78 .2byte (\Offset & 0xFFFF)
79 .2byte \Selector
80 .2byte (\PCount & 0x1F) | ((\Attr << 8) & 0xFF00)
81 .2byte ((\Offset >> 16) & 0xFFFF)
82 .endm
--------------
Fig 3.24: 汇编宏Gate 定义(节自chapter3/3/pm.h)

------------
29 LABEL_DESC_CODECG: Descriptor 0, (SegCodeCGLen - 1), (DA_C + DA_32)
30 /* Gates Descriptor */
31 LABEL_CG_TEST: Gate SelectorCodeCG, 0, 0, (DA_386CGate + DA_DPL0)
32
33 .set GdtLen, (. - LABEL_GDT) /* GDT Length */
43 .set SelectorLDT, (LABEL_DESC_LDT - LABEL_GDT)
44 .set SelectorCodeCG, (LABEL_DESC_CODECG - LABEL_GDT)
45 .set SelectorCGTest, (LABEL_CG_TEST - LABEL_GDT)
-----------------
Fig 3.25: 设置调用门描述符及选择子(节自chapter3/3/loader.S)

我们可以看到,宏Gate 的四个参数分别为:段选择子、偏移量、参数计数和属性,它们在存储空间中的分布与图3.22 中介绍相同。由于这个例子仅仅介绍调用门的简单使用,并不涉及特权级切换,所以也不发生栈切换,这里我们将参数计数设置为0;门描述符的属性为(DA 386CGate + DPL) ,表明它是一个调用门(属性定义见图3.3),DPL 为0 ,与我们一直使用的特权级相同;目标代码段选择子是SelectorCodeCG ,偏移是0 ,所以如果该调用门被调用,将转移到目标代码段的开头,即LABEL SEG CODECG 处开始执行。

使用远调用lcall 指令调用该调用门的门选择子SelectorCGTest:

185 CODE32.2:
186
187 lcall $(SelectorCGTest), $0 /* Call CODECG through call gate */
188
189 mov $(SelectorLDT), %ax
190 lldt %ax

Fig 3.26: 调用门选择子(节自chapter3/3/loader.S)

由于对调用门的调用往往涉及到段间转移,所以我们通常使用gas 的lcall 远跳转指令和lret 远返回指令进行调用和返回。

这样我们就完成了使用调用门进行简单控制权转移的代码, make, sudo make copy 之后,用VBox 虚拟机载入生成的镜像,运行结果如图3.27 所示。由于我们仅仅是在加载LDT 之前添加了一个门调用,而且门调用的目标段在屏幕的第11 行第0 列打印了一个’C’ 后就返回到了调用处,所以加载LDT 的代码继续运行,就是图中所示结果。


涉及特权级变化的调用门转移

在上面例子中我们只是使用调用门取代了传统的直接跳转方式,并没有涉及到特权级的变化。显然调用门不是用来做这种自找麻烦的事情的,其设计的主要目的是实现从低特权级代码跳转到高特权级的非一致代码的功能。

在使用调用门进行转移时,处理机会使用四个特权级值来检查控制权的转移是否合法:
1. CPL:当前特权级;
2. RPL:调用门选择子的请求特权级;
3. DPL:调用门描述符的描述符特权级;
4. DPL:目标代码段的段描述符特权级

相关主题