x86汇编语言-从实模式到保护模式笔记

全局描述符表(GDT)

Posted by BINBIN Blog on October 24, 2019

全局描述符表(GDT)

在进入保护模式之前。首先来介绍一下,全局描述符表(Global Descriptor Table,GDT)。

在实模式下,处理器将内存分为逻辑上的段,在访问内存时,在指令中,使用段内偏移地址。这在之前的文章学习的很深刻了。

在保护模式下,就不太一样了。对内存的访问,依然使用段地址加偏移地址,但是,在每个段能够进行访问之前,必须先进行登记。

但在保护模式下就不行了,开公司之前必须先登记,登记的信息包括住址(段的起始地址)、经营项目(段的界限等各种访问属性)。这样,每当你做的买卖和项目不符时,就会被阻止。对段的访问也是一样,当你访问的偏移地址超出段的界限时,处理器就会阻止这种访问,并产生一个叫做内部异常的中断。

和一个段有关的信息需要 8 个字节来描述,所以称为段描述符(Segment Descriptor),每个段都需要一个描述符。为了存放这些描述符,需要在内存中开辟出一段空间。在这段空间里,所有的描述符都是挨在一起,集中存放的,这就构成一个描述符表!

最主要的描述符表是全局描述符表(Global Descriptor Table, GDT),所谓全局,意味着该表是为整个软硬件系统服务的。在进入保护模式前,必须要定义全局描述符表。

如图 11-1 所示,为了跟踪全局描述符表,处理器内部有一个 48 位的寄存器,称为全局描述符表寄存器(GDTR)。

该寄存器分为两部分,分别是 32 位的线性地址和 16 位的边界。

32 位的处理器具有 32 根地址线,可以访问的地址范围是 0x00000000 到 0xFFFFFFFF,共 232字节的内存,即 4GB 内存。

GDTR 的 32 位线性基地址部分保存的是全局描述符表在内存中的起始线性地址!

16 位边界部分保存的是全局描述符表的边界(界限),其在数值上等于表的大小(总字节数)减一! (作者提到 “全局描述符表的界限值就是表内最后 1 字节的偏移量” 我也没搞懂1字节的偏移量是什么鬼!反正总字节数减1就行了! )

因为 GDT 的界限是 16 位的,所以,该表最大是 216 字节,也就是 65536 字节(64KB)。又因为一个描述符占 8 字节,故最多可以定义 8192 个描述符。实际上,不一定非得这么多,到底有多少,视需要而定,但最多不能超过 8192 个!

理论上,全局描述符表可以位于内存中的任何地方。但是,如图 11-2 所示,由于在进入保护模式之后,处理器立即要按新的内存访问模式工作,所以,必须在进入保护模式之前定义 GDT。但是,由于在实模式下只能访问 1MB 的内存,故 GDT 通常都定义在 1MB 以下的内存范围中。当然,允 许在进入保护模式之后换个位置重新定义 GDT。

存储器的段描述符

在程序的开始部分要初始化段寄存器。代码清单 11-1 第 7~9 行用于初始化堆栈,使堆栈段的逻辑段地址和代码段相同,并使堆栈指针寄存器 SP 指向 0x7c00。这是个分界线,从这里,代码向上扩展,而堆栈向下扩展!

代码如下:

1 			 ;代码清单11-1
2 			 ;文件名:c11_mbr.asm
3 			 ;文件说明:硬盘主引导扇区代码 
4 			 ;创建日期:2011-5-16 19:54
5 
6 			 ;设置堆栈段和栈指针 
7 			 mov ax,cs      
8 			 mov ss,ax
9 			 mov sp,0x7c00
10		  
11			 ;计算GDT所在的逻辑段地址 
12			 mov ax,[cs:gdt_base+0x7c00]        ;低16位 
13			 mov dx,[cs:gdt_base+0x7c00+0x02]   ;高16位 
14			 mov bx,16        
15			 div bx            
16			 mov ds,ax                          ;令DS指向该段以进行操作
17			 mov bx,dx                          ;段内起始偏移地址 
18		  
19			 ;创建0#描述符,它是空描述符,这是处理器的要求
20			 mov dword [bx+0x00],0x00
21			 mov dword [bx+0x04],0x00  
22
23			 ;创建#1描述符,保护模式下的代码段描述符
24			 mov dword [bx+0x08],0x7c0001ff     
25			 mov dword [bx+0x0c],0x00409800     
26
27			 ;创建#2描述符,保护模式下的数据段描述符(文本模式下的显示缓冲区) 
28			 mov dword [bx+0x10],0x8000ffff     
29			 mov dword [bx+0x14],0x0040920b     
30
31			 ;创建#3描述符,保护模式下的堆栈段描述符
32			 mov dword [bx+0x18],0x00007a00
33			 mov dword [bx+0x1c],0x00409600
34
35			 ;初始化描述符表寄存器GDTR
36			 mov word [cs: gdt_size+0x7c00],31  ;描述符表的界限(总字节数减一)   
37												 
38			 lgdt [cs: gdt_size+0x7c00]
39		  
40			 in al,0x92                         ;南桥芯片内的端口 
41			 or al,0000_0010B
42			 out 0x92,al                        ;打开A20
43
44			 cli                                ;保护模式下中断机制尚未建立,应 
45												;禁止中断 
46			 mov eax,cr0
47			 or eax,1
48			 mov cr0,eax                        ;设置PE位
49		  
50			 ;以下进入保护模式... ...
51			 jmp dword 0x0008:flush             ;16位的描述符选择子:32位偏移
52												;清流水线并串行化处理器 
53			 [bits 32] 
54
55		flush:
56			 mov cx,00000000000_10_000B         ;加载数据段选择子(0x10)
57			 mov ds,cx
58
59			 ;以下在屏幕上显示"Protect mode OK." 
60			 mov byte [0x00],'P'  
61			 mov byte [0x02],'r'
62			 mov byte [0x04],'o'
63			 mov byte [0x06],'t'
64			 mov byte [0x08],'e'
65			 mov byte [0x0a],'c'
66			 mov byte [0x0c],'t'
67			 mov byte [0x0e],' '
68			 mov byte [0x10],'m'
69			 mov byte [0x12],'o'
70			 mov byte [0x14],'d'
71			 mov byte [0x16],'e'
72			 mov byte [0x18],' '
73			 mov byte [0x1a],'O'
74			 mov byte [0x1c],'K'
75
76			 ;以下用简单的示例来帮助阐述32位保护模式下的堆栈操作 
77			 mov cx,00000000000_11_000B         ;加载堆栈段选择子
78			 mov ss,cx
79			 mov esp,0x7c00
80
81			 mov ebp,esp                        ;保存堆栈指针 
82			 push byte '.'                      ;压入立即数(字节)
83			 
84			 sub ebp,4
85			 cmp ebp,esp                        ;判断压入立即数时,ESP是否减4 
86			 jnz ghalt                          
87			 pop eax
88			 mov [0x1e],al                      ;显示句点 
89		  
90	  ghalt:     
91			 hlt                                ;已经禁止中断,将不会被唤醒 
92
93	;-------------------------------------------------------------------------------
94		 
95			 gdt_size         dw 0
96			 gdt_base         dd 0x00007e00     ;GDT的物理地址 
97								 
98			 times 510-($-$$) db 0
99							  db 0x55,0xaa

在保护模式下,内存的访问机制完全不同,即,必须通过描述符来进行。所以,这些段必须重新在 GDT 中定义。

先是确定 GDT 的起始线性地址。代码清单 11-1第 96 行,声明了标号 gdt_base 并初始化了一个双字0x00007e00,我们决定从这个地方开始创建全局描述符表(GDT)。这是有意的,如图 11-3 所示,在实模式下,主引导程序的加载位置是 0x0000:0x7c00,也就是物理地址 0x07c00。

因为现在的地址是 32 位的,所以它现在对应着物理地址 0x00007c00。主引导扇区程序共 512(0x200)字节,所以,我们决定把 GDT 设在主引导程序之后,也就是物理地址 0x00007e00 处。 因为 GDT 最大可以为 64KB,所以,理论上,它的尺寸可以扩展到物理地址 0x00017dff 处。

因为堆栈指针寄存器 SP 被初始化为0x7c00,和 CS 一样,堆栈段寄存器 SS 被初始化为0x0000,而且堆栈是向下扩展的,所以,从 0x00007c00 往下的区域是实际上可用的堆栈区域。只不过,该区域包含了很多 BIOS 数据,包括实模式下的中断向量表,所以一定要小心。这是没有办法的事,在实模式下,处理器不会为此负责,只能靠你自己。

一旦确定了 GDT 在内存中的起始位置,下一步的工作就是确定要访问的段,并在 GDT 中为这些段创建各自的描述符。

如图 11-4 所示,每个描述符在 GDT 中占 8 个字节,也就是 2 个双字,或者说是 64 位。图中,下面是低 32 位,上面是高 32 位。

很明显,描述符制定了32位的段起始地址(段基地址),以及20位的段边界。

在32位模式下,段地址与实模式下的段地址不一样。在实模式先段地址不是真正的物理地址,它还需要左移4位。而保护模式下,段地址是32位的线性地址,它就是真实的物理地址(未开启分页功能,分页功能后面学习)。

描述符中段基址和段界限不是连续的。这是历史的问题与兼容的问题。

20位的段界限是用来限制段的扩展范围。因为访问内存的方法是用段基地址加上偏移量,所以,对于向上扩展的段,如代码段和数据段来说,偏移量是从 0 开始递增, 段界限决定了偏移量的最大值; 对于向下扩展的段,如堆栈段来说, 段界限决定了偏移量的最小值。

下面来介绍段描述符各个字段的意思。

段描述符各个字段的意义

下面的表格列出了上述段描述符的各个位的意思,如果现在不理解这些位也无所谓,后面的学习会慢慢深入理解。

G: 粒度位 用于解释段界限的含义。当G位是0时,段界限以字节为单位。此时段的扩展范围是从1字节到1M字节,因为描述符中的界限值是20位的。相反,如果该位是1,那么段界限是以4KB位单位。这样段的扩展范围是4KB到4GB
S: 类 型 位 当该位是0时,表示是一个系统;为1时,表示是一个代码段或者数据段(栈段也是特殊的数据段)。系统段将在后面的文章中学习
DPL 特权级 这两位用于指定描述符的特权级。共有4中处理器支持的特权级别,分别是0、1、2、3 ,其中0是最高的特权级,3是最低的特权级别。刚进入保护模式时执行的代码具有最高特权级0(可以看成是从处理器那里继承来的)这些代码通常是操作系统代码,因此它的特权级最高。每当操作系统加载一个用户程序,它通常都会制定一个低的特权级,比如3特权级。不同特权级的程序是互相隔离的,其访问是严格限制的,而且有些处理器指令只能由0特权级的程序来执行,为的就是安全。在这里,描述符的特权级是用于指定访问该段所必须具有的最低特权级。
P: 段存在位 P位用于指示描述符所对应的段是否存在。一般来说,描述符所对应的段是在内存中。但是当内存空间紧张时,有可能指示建立了描述符,对应的内存空间并不存在,这时就应当把描述符的P位清零,表示段并不存在。另外,同样是在内存空间紧张的情况下,会把很少用到的段换出到硬盘中,腾出空间给当前急需内存的进程使用,这时同样要把P位清零,当再次轮到它执行时,再将其装入内存,然后P位置1。 P是由处理器负责检查的。每当通过描述符访问内存段时,如果P位是0,处理器就会产生一个异常中断。通常,该中断处理过程是由操作系统提供的,该处理过程的任务是将该段从硬盘换回内存,并将P位置1。在多用户、多任务的系统中,这是一种常用的虚拟内存调度策略。
D/B: 默认的操作数大小 设置该标志位,主要是为了能够在32位处理器上兼容运行16位保护模式的程序。 该标志位对不同的段有不同的效果。对于代码段,此位称为D位,用于指定指令中默认的的偏移地址和操作数尺寸。D=0时表示指令中的偏移地址或者操作数是16位的。D=1时,表示32位的偏移地址和操作数。对于栈段来说,该位是B位,用于在进行隐式的栈操作中(push,pop,call等),是使用SP寄存器还是使用ESP寄存器。当B=0时,在访问哪个段时,使用SP寄存器,否则就使用ESP寄存器。同时,B位的值,也决定了栈段的上边界。如果B=0,那么栈的上边界是0xFFFF;如果B=1,那么栈段的上边界是0xFFFFFFFF。
L:64位代码段标志 保留此位给64位处理器使用。目前我们将它置0即可
TYPE:描述符子类型 对于数据段来说,这4位分别是X,E,W,A,对于代码段来说这4位分别是X,C,R,A。他们具体的含义见下面的表格。
AVL:软件可以使用的位 通常由操作系统来用,处理器并不使用它。

下面表格是代码段和数据段的TYPE字段 TYPE 字段共 4 位,用于指示描述符的子类型,或者说是类别。如表 11-1 所示,对于数据段来说, 这 4 位分别是 X、 E、 W、 A 位;而对于代码段来说,这 4 位则分别是 X、 C、 R、 A 位。

X E W A 描述符类别 含义
0 0 0 X 数据段 只读
0 0 1 X 数据段 读、写
0 1 0 X 数据段 只读,向下扩展
0 1 1 X 数据段 读、写,向下扩展
  • X 表示是否可执行。对于数据段,总是不可执行。所以为0
  • E 表示段的扩展方向。E=0是向上扩展的。E=1是向下扩展的
  • W 指示段的读写属性,W=0是不允许写入的,W=1是可以正常写入的
  • A 是已访问位,用于指示它所指向的段最近是否被访问过。
X C R A 描述符类别 含义
1 0 0 X 代码段 只执行
1 0 1 X 代码段 执行、读
1 1 0 X 代码段 只执行、依从的代码段
1 1 1 X 代码段 执行、读、依从的代码段
  • X 表示是否可执行。代码段总是可执行,所以为1
  • C 指示段是否为特权级依从的。C=0时,表示非依从的代码段,这样的代码段是可以从与它特权级相同的代码段调用,或者通过门调用;C=1时表示允许从低特权级的代码转移到该段执行。
  • R 指示代码段是否允许读出。代码段总是可以执行的,但是为了防止程序破坏,它是不允许写入的。至于是否有读出的可能,由R位决定。R=0时表示不能读出。R=1时,则代码段是可以读出的。
  • A 是已访问位,用于指示它所指向的段最近是否被访问过。

也许有人会问,既然代码段是不可读的,那处理器怎么从里面取指令执行呢?事实上,这里的R 属性并非用来限制处理器, 而是用来限制程序和指令的行为。一个典型的例子是使用段超越前缀“CS:”来访问代码段中的内容。

数据段和代码段的 A 位是已访问(Accessed)位,用于指示它所指向的段最近是否被访问过。 在描述符创建的时候,应该清零。之后,每当该段被访问时,处理器自动将该位置“1”。对该位的清零是由软件(操作系统)负责的,通过定期监视该位的状态,就可以统计出该段的使用频率。当内存空间紧张时,可以把不经常使用的段退避到硬盘上,从而实现虚拟内存管理。

安装存储器的段描述符并加载 GDTR

现在开始安装各个描述符,让我们回到代码清单 11-1。 不要忘了,我们现在还处于实模式下。因此,在 GDT 中安装描述符,必须将 GDT 的线性地址转换成段地址和偏移地址。第 12 行,将 GDT 线性基地址的低 16 位传送到寄存器 AX 中。和从前一样,这里使用了段超越前缀“cs:”,表明是访问代码段中的数据;又因为主引导程序的实际加载 位置是逻辑地址 0x0000:0x7c00,故标号 gdt_base 处的偏移地址是 gdt_base+0x7c00。

同样地,第 13 行将 GDT 线性基地址的高 16 位传送到寄存器 DX。 第 14~17 行将线性基地址转换成逻辑地址,方法是将 DX:AX 除以 16,得到的商是逻辑段地址,余数是偏移地址。接着,将 AX 中的逻辑段地址传送到数据段寄存器 DS 中,将偏移地址传送到寄存器 BX 中。

处理器规定, GDT 中的第一个描述符必须是空描述符,或者叫哑描述符或 NULL 描述符,相信后者对于有 C 语言经历的读者来说更容易接受。

为此,第 20、 21 行将两个全 0 的双字分别写入偏移地址为 BX 和 BX+4 的地方。 进入保护模式之后必然要从一个代码段开始执行。现在就来定义代码段描述符。 第 24、25 行,接着安装代码段描述符,该描述符的低 32 位是 0x7c0001ff,高 32 位是 0x00409800。 结合图 11-4 可以分析出,该段的基本情况为:

线性基地址为 0x00007C00。
段界限为 0x001FF,粒度为字节(G=0)。该段的长度为 512 字节。
属于存储器的段(S=1)。
这是一个 32 位的段(D=1)。
该段目前位于内存中(P=1)。
段的特权级为 0(DPL=00)。
这是一个只能执行的代码段(TYPE=1000)。

很明显,该描述符所指向的段,就是现在正在执行的主引导程序所在的区域。如图 11-5 所示,这是描述符各字节在内存中的映象。 Intel 处理器是低端字节序的,所以低双字在低地址端,高双字在高地址端;低字在低地址端,高字在高地址端;低字节在低地址端,高字节在高地址端。

第 28、 29 行,用于安装一个数据段的描述符。对照图 11-4,很明显,这个段具有以下性质:


线性基地址为 0x000B8000。
段界限为 0x0FFFF,粒度为字节(G=0)。即,该段的长度为 64KB。
属于存储器的段(S=1)。
这是一个 32 位的段(D=1)。
该段目前位于内存中(P=1)。
段的特权级为 0(DPL=00)。
这是一个可读可写、向上扩展的数据段(TYPE=0010)。

很容易看出,线性地址 0x000b8000 就是显存的起始地址,看起来,我们要用这个段来显示字符。

第 32、 33 行,用于安装堆栈段的描述符。对照图 11-4,该段的性质如下

线性基地址为 0x00000000。
段界限为 0x07A00,粒度为字节(G=0)。
属于存储器的段(S=1)。
这是一个 32 位的段(D=1)。
该段目前位于内存中(P=1)。
段的特权级为 0(DPL=00)。
这是一个可读可写、向下扩展的数据段,即堆栈段(TYPE=0010)

在这里,段界限的值 0x07a00 加上 1(0x07a01), 就是 ESP 寄存器所允许的最小值。当执行 push、call 这样的隐式堆栈操作时,处理器会检查 ESP 寄存器的值,一旦发现它小于等于这里指定的数值,会引发异常中断。

现在所有的描述符都已经安装完毕,接下来的工作是加载描述符表的线性基地址和界限到 GDTR 寄存器,这要使用 lgdt 指令,该指令的格式为 lgdt m48 ;lgdt m16&m32

这就是说,该指令的操作数是一个内存地址,指向一个包含了 48 位(6 字节)数据的内存区域。 在 16 位模式下,该地址是 16 位的;在 32 位模式下,该地址是 32 位的。该指令在实模式和保护模式下都可以执行。

在这 6 字节的内存区域中,前 16 位是 GDT 的界限值,高 32 位是 GDT 的基地址。在初始状态下(计算机启动之后), GDTR 的基地址被初始化为 0x00000000;界限值为 0xFFFF。

代码清单 11-1 第 36 行,将 GDT 表的界限值 31 写入标号 gdt_size 所在的内存单元。这里共有 4 个描述符(包括空描述符),每个描述符占 8 字节,一共是 32 字节。 GDT 表的界限值是表的总字节数减去一,所以是 31.

第 38 行,把从标号 gdt_size 开始的 6 字节加载到 GDTR 寄存器。注意,到目前为止,我们依然工作在实模式下,而且不要忘了,指令中的偏移地址都要加上 0x7c00。

第 21 条地址线 A20 的问题

A20 就是第 21 根地址线。

输入输出控制器集中芯片 ICH 的处理器接口部分,有一个用于兼容老式设备的端口 0x92,第 7~2 位保留未用,第 0 位叫做 INIT_NOW,意思是“现在初始化”,用于初始化处理器,当它从 0 过渡到 1 时, ICH 芯片会使处理器 INIT#引脚的电平变低(有效),并保持至少16 个 PCI 时钟周期。通俗地说,向这个端口写 1,将会使处理器复位,导致计算机重新启动。

端口 0x92 是可读写的,第 40~42 行,先从该端口读出原数据,接着,将第 2 位(位 1)置“1”,然后再写入该端口,这样就打开了 A20。

保护模式下的内存访问

控制这两种模式切换的开关原是在一个叫 CR0 的寄存器。 CR0 是处理器内部的控制寄存器(Control Register, CR)。之所以有个“0”后缀,是因为还有CR1、 CR2、 CR3 和 CR4 控制寄存器,甚至还有 CR8。

CR0 是 32 位的寄存器,包含了一系列用于控制处理器操作模式和运行状态的标志位。如图 11-8所示,它的第 1 位(位 0)是保护模式允许位(Protection Enable, PE),是开启保护模式大门的门把手,如果把该位置“1”,则处理器进入保护模式,按保护模式的规则开始运行。你可能会问,为什么只标识了一个 PE 位,还把图画那么大。很简单,随着讲解的深入, 我们还要接触其他标志位,把图的比例画得一致更好一些。

保护模式下的中断机制和实模式不同,因此,原有的中断向量表不再适用,而且,必须要知道的是,在保护模式下, BIOS 中断都不能再用,因为它们是实模式下的代码。在重新设置保护模式下的中断环境之前,必须关中断,这就是第 44 行的用意。

第 46 行,将 CRO 寄存器中的原有内容传送到寄存器 EAX,准备修改它;第 47 行,将它的第1 位(位 0)置“1”,其他各位保持原来的状态不变;第 48 行,将修改之后的内容重新写回 CR0,这直接导致处理器的运行变成保护模式。

在实模式下,处理器访问内存的方式是将段寄存器的内容左移 4 位,再加上偏移地址,以形成 20 位的物理地址。 8086 处理器的段寄存器是 16 位的,共有 4 个: CS、 DS、 ES 和 SS。而在 32 位处理器内,段寄存器是 80 位的(16 位段选择器和 64 位描述符高速缓存器)。而且,在原先的基础上又增加了两个段寄存器 FS 和 GS。

如图 11-9 所示, 32 位处理器的这 6 个段寄存器又分为两部分,前 16 位和 8086 相同,在实模式下,它们用于按传统的方式寻址 1MB 内存,使用方法也没有变化,所以使得 8086 的程序可以继续在 32 位处理器上运行。同时,每个段寄存器还包括一个不可见的 64 位部分,称为描述符高速缓存器,用来存放段的线性基地址、段界限和段属性。既然不可见,那就是处理器不希望我们访问它。 事实上,我们也没有任何办法来访问这些不可见的部分,它是由处理器内部使用的!

实模式下 6 个段寄存器 CS、 DS、 ES、 FS、 GS 和 SS,在保护模式下叫做段选择器。 在保护模式下,尽管访问内存时也需要指定一个段,但传送到段选择器的内容不是逻辑段地址,而是段描述符在描述符表中的索引号。

如图 11-10 所示,在保护模式下访问一个段时,传送到段选择器的是段选择子。它由三部分组成,第一部分是描述符的索引号,用来在描述符表中选择一个段描述符。 TI 是描述符表指示器(Table Indicator), TI=0 时,表示描述符在 GDT 中; TI=1 时,描述符在 LDT 中。 LDT 的知识将在后面进行介绍,它也是一个描述符表,和 GDT 类似。 RPL 是请求特权级,表示给出当前选择子的那个程序的特权级别,正是该程序要求访问这个内存段。每个程序都有特权级别,也将在后面慢慢介绍,现在只需要将这两位置成“00”即可。

为了说明保护模式下的内存访问,让我们回到代码清单 11-1。前面已经创建了全局描述符表(GDT),而且在表中定义了 4 个段描述符。数据段描述符在 GDT 中的顺序是第 3 个,因为编号都是从 0 开始的,所以它的索引号(或者叫编号、 槽位号)是 2。

代码清单 11-1 第 56、 57 行,将描述符选择子 0x0010(二进制数 00000000 00010_0_00)传送到段选择器 DS 中。从选择子的二进制形式可以看出,指定的描述符索引号是 2,指定的描述符表是 GDT,请求特权级 RPL 是 00。

GDT 的线性基地址在 GDTR 中,又因为每个描述符占 8 字节,因此,描述符在表内的偏移地址是索引号乘以 8。如图 11-11 所示,当处理器在执行任何改变段选择器的指令时(比如 pop、 mov、jmp far、 call far、 iret、 retf),就将指令中提供的索引号乘以 8 作为偏移地址,同 GDTR 中提供的线性基地址相加,以访问 GDT。如果没有发现什么问题(比如超出了 GDT 的界限),就自动将找到的描述符加载到不可见的描述符高速缓存部分。

加载的部分包括段的线性基地址、段界限和段的访问属性。在当前的例子中,线性基地址是0x000b8000,段界限是 0x0ffff,段的属性是向上扩展,可读写的数据段,粒度为字节。

每当有访问内存的指令时,就不再访问 GDT 中的描述符,直接用当前段寄存器描述符高速缓存器提供线性基地址。因此,第 60 行,因为指令中没有段超越前缀,故默认使用数据段寄存器DS。如图 11-12 所示,执行这条指令时,处理器用 DS 描述符高速缓存中的线性基地址加上指令中给出的偏移量 0x00,形成 32 位物理地址 0x000b8000,并将字符“P”的 ASCII 码写入该处。

不单单是访问数据段,即使是处理器取指令执行时,也采用了相同的方法。如图 11-13 所示,在 32 位保护模式下,处理器使用的指令指针寄存器是 EIP。假设已经从描述符表中选择了一个段描述符, CS 描述符高速缓存器已经装载了正确的 32 位线性基地址,那么,当处理器取指令时,会自动用描述符高速缓存器中的 32 位线性基地址加上指令指针寄存器 EIP 中的 32 位偏移量,形成 32 位物理地址,从内存中取得执令并加以执行。同时, EIP 的内容自动增加以指向下一条指令。当前指令执行完毕之后,处理器接着按上述方式取下一条指令加以执行。

清空流水线并串行化处理器

一般模式切换,流水线的指令要清空的!

请看书详细描述,不做记录!

保护模式下的堆栈

第 77~79 行用于初始化保护模式下的堆栈。堆栈段描述符是 GDT 中的第 4 个(3 号)描述符,堆栈的 32 位线性基地址是 0x00000000,段界限为 0x07a00,粒度为字节,属于可读可写、向下扩展的数据段。 堆栈是向下扩展的,因此,描述符中的段界限, 和向上扩展的段含义不同。 对于向上扩展的段,段内偏移量是从 0 开始递增,偏移量的最大值是界限值和粒度的乘积;而对于向下扩展的段来说,因为它经常用做堆栈段,而堆栈是从高地址向低地址方向推进的,故段内偏移量的最小值是界限值和粒度的乘积加一。 在 32 位代码中,是用 ESP 作为堆栈指针的。因此,这里的段界限,用来和段粒度一起,决定 ESP 寄存器所能具有的最小值。即,堆栈操作时,必须符合条件:

ESP > 段界限×粒度值

对于描述符中 G 位是“0”的段来说, 粒度值是 1(字节);而对于 G 位是“1”的段来说, 粒度值是 4096(4KB)。

在当前代码中, ESP 寄存器的内容被初始化为 0x00007c00。假如此时执行以下指令: push edx 那么,因为要压入一个 32 位数,所以处理器先将 ESP 的内容减去 4,再压入数据。此时, ESP寄存器的内容为(扩展到 32 位): 0x00007C00-4=0x00007BFC 在当前堆栈段的描述符中,段界限为 0x07a00,粒度是字节,故作为堆栈的界限,实际使用的数值是(扩展到 32 位): 0x07A00×1=0x00007A00 对于堆栈段来说,段界限的值加一,就是段内偏移量的最小值。因为要访问的段内偏移量0x00007BFC 大于实际使用的段界限值 0x00007A00,故处理器允许执行该操作,并用描述符高速缓存中的 32 位基地址 0x00000000 加上这里的偏移量 0x00007BFC,共同形成 32 位线性地址访问堆栈,将寄存器 EDX 的内容压入。否则,处理器阻止当前操作,引发一个异常中断。

很明显,我们的本意是要定义一个只有 512 字节的堆栈空间,从物理地址 0x00007A00 开始,到物理地址 0x00007C00 结束,如图 11-14 所示。

尽管我们的本意是定义一个只有 512 个字节的堆栈,但是,从该段的描述符来看,这个段的空间却是非常巨大的。 假如一切正常,特别是指令执行正常,那不会有什么问题。但是,在程序失控的情况下, ESP 的内容可能会是任何预料不到的值,比如 0xFFFFFFFF。即使是这样,它也是合法的值,毕竟它大于 0x00007A00。因为当前堆栈段的线性基地址为 0x00000000,所 以,实际可以访问的空间是从物理地址 0xFFFFFFFF 到 0x00007A00。显然,这超出了我们的预期。在下一章里,我们将继续讨论如何用更好的方法来创建堆栈。

检验 32 位下的堆栈操作

第 81 行,先保存当前堆栈指针的内容到 EBP 寄存器;接着,第 82 行,向堆栈中压入立即数。该立即数为字符“.”的 ASCII 码,这个值是在编译阶段计算的.

因为当前正在执行的代码段是 32 位的,其描述符的 D 位是“1”,故 push 指令默认的操作数大小是 32 位。

当前指令执行时,所访问的堆栈,其描述符的 B 位也是“1”,故处理器在进行堆栈操作时,用的是 32 位堆栈指针寄存器 ESP。它首先将 ESP 的内容减去 4,再写入数值,数据保存的位置是SS:ESP。

程序的编译和运行