X86汇编语言从实模式到保护模式10:进入保护模式

目录

1. 全局描述符表GDT

1.1 段描述符与描述符表

1.2 全局描述符表的定义

1.3 全局描述符表寄存器GDTR

1.3.1 GDTR用途

1.3.2 GDTR构成

1.3.3 lgdt指令

2. 存储器段描述符详解

2.1 段基地址

2.2 段界限

2.3 G位粒度(Granularity)位

2.4 S位类型位

2.5 DPL描述符特权级

2.6 P位存在(Present)位

2.7 D/B位操作数大小位

2.8 L位64位代码段标志位

2.9 TYPE描述符子类型

2.9.1 X(eXecutable)执行位

2.9.2 E(Expand)扩展位

2.9.3 W(Writable)可写位

2.9.4 C(Conforming)依从位

2.9.5 R(Readable)可读位

2.9.6 A(Accessed)已访问位

2.10 AVL(Available)位

3. 进入保护模式示例程序分析

3.1 进入保护模式前的内存布局

3.2 创建GDT表

3.2.1 计算GDT表逻辑地址

3.2.2 设置GDT表项

3.2.3 设置GDT表大小

3.3 加载GDTR

3.4 开启A20地址线

3.4.1 A20地址线问题

3.4.2 传统开启A20方法

3.4.3 快速开启A20方法

3.5 进入保护模式

3.6 保护模式下的内存访问

3.6.1 段选择器与描述符高速缓存器

3.6.2 段选择符(Segment Selector)

3.6.3 保护模式访问内存过程

3.6 保护模式下的长跳转

3.6.1 长跳转目的地分析

3.6.2 远跳转的其他功能

3.7 打印字符串

3.8 验证32位栈操作

3.8.1 栈段描述符分析

3.8.2 验证方式分析


1. 全局描述符表GDT

1.1 段描述符与描述符表

① 为了让程序在内存中能自由浮动而又不影响他的正常执行,处理器将内存划分为逻辑上的段,并在指令中使用段内偏移地址

在保护模式下,对内存的访问仍然使用段地址 + 偏移地址,只是段地址由段描述符提供,而不是直接由段寄存器提供

③ 和一个段有关的信息需要8B来描述,称作段描述符(Segment Descriptor)

④ 将所有段的段描述符在内存中连续存放,就构成描述符表

1.2 全局描述符表的定义

① 全局描述符表(Global Descriptor Table,GDT)是最主要的描述符表,在进入保护模式之前,必须定义全局描述符表

② 理论上,GDT可以位于内存中的任何地方,但是初始设置的GDT一般位于1MB以下的内存范围内

这是因为设置初始GDT是在进入保护模式之前,而在进入保护模式之前,处理器只能访问1MB以下的内存空间

说明1:允许在进入保护模式之后,重新定义GDT

说明2:GDT表不是由用户程序自己建立的,而是在加载用户程序时,由操作系统根据用户程序的结构建立的,而用户程序通常无法建立和修改GDT,处理器借此实现保护功能

1.3 全局描述符表寄存器GDTR

X86汇编语言从实模式到保护模式10:进入保护模式_第1张图片

1.3.1 GDTR用途

为了跟踪GDT,处理器内部设置了一个48位的GDTR寄存器,同时提供了lgdt指令用于将GDT信息加载到GDTR中

GDT与GDTR的关系如下图所示,

X86汇编语言从实模式到保护模式10:进入保护模式_第2张图片

1.3.2 GDTR构成

① 32位GDT线性基地址

对于32位的处理器,GDTR中32位的线性基地址确保可以将GDT部署在4GB范围内的任何位置

② 16位边界值

GDT边界值 = 表内最后一个字节相对于表开始处的偏移量,在数值上为表的总字节数减1

16位的边界值最大为65535,因此表的总字节数为65536。又因为每个段描述符需要8B,因此GDT表中最多可以定义(65536 / 8 = 8192)个描述符

说明:预留一个伏笔,由于GDT表中最多可以有8192个描述符,那么索引这些描述符就需要至少13位(详见段选择符的构成)

1.3.3 lgdt指令

lgdt指令用于将GDT表的基地址和界限值加载到GDTR中,指令格式如下,

lgdt m48

这里m48表示该指令的操作数是一个48位的内存区域,且要求低16位为GDT界限值,高32位是GDT线性基地址

X86汇编语言从实模式到保护模式10:进入保护模式_第3张图片

说明1:lgdt指令在实模式和保护模式下均可执行

说明2:对于m48这个内存地址的有效地址(EA)的给出方式,在16位实模式下,EA是16位的;在32位保护模式下,EA是32位的

说明3:lgdt指令不影响任何标志位

2. 存储器段描述符详解

X86汇编语言从实模式到保护模式10:进入保护模式_第4张图片

存储器段描述符格式如上图所示,下面逐一说明其中的字段

2.1 段基地址

① 段基地址为32位,确保段基址可以是0 ~ 4GB范围内的任意地址

② 32位段基地址分段保存,是源于80286处理器的历史遗留问题

说明:虽然段基地址可以是0 ~ 4GB范围内的任意地址,而且Intel处理器也允许地址不对齐访问。但是还是建议选取16B对齐的地址作为段基址,因为对齐可以使程序在访问代码和数据时的性能最大化

2.2 段界限

① 20位的段界限用来限制段的扩展范围,越过段界限的访问将被处理器阻止并触发异常

② 对于向上扩展的段,偏移量从0开始递增,段界限决定了偏移量的最大值(包含该值)

X86汇编语言从实模式到保护模式10:进入保护模式_第5张图片

由于偏移量从0开始,所以(段界限 + 1)就是段的大小。因此,如果指定的段界为0,则段的大小为1

③ 对于向下扩展的段,段界限决定了偏移量的最小值(不包含该值)。段界限是偏移量所不允许的最小值,小于或等于就会触发异常。

X86汇编语言从实模式到保护模式10:进入保护模式_第6张图片

段的大小为0xFFFF或0xFFFFFFFF到设置的段界限处,这里之所以分为16位和32位的偏移量最大值,是因为偏移量可能是16位寄存器提供的(e.g. 使用SP指针),也可能是32位寄存器提供的(e.g. 使用ESP寄存器)

如果指定的段界限为0,此时段的大小是最大的

说明1:向下扩展的段不一定用作栈段,但是通常用作栈段

说明2:使用向下扩展的段作为栈段的缺点

向下扩展的段的段内偏移量最大值一定是0xFFFF或0xFFFFFFFF,而SP或ESP寄存器一般并不设置为该值。但是即使不使用0xFFFF(FFFF)到SP(ESP)之间的范围,这段内存也包含在当前段中

X86汇编语言从实模式到保护模式10:进入保护模式_第7张图片

2.3 G位粒度(Granularity)位

① 粒度用于解释段界限的单位

② 当G = 0,段界限以字节为单位;当G = 1,段界限以4KB为单位

2.4 S位类型位

① S位用于指定描述符的类型(Descriptor Type)

② 当S = 0,表示是一个系统描述符,且TYPE字段用来指定系统段的类型或门的类型

③ 当S = 1,表示是一个存储器的段描述符,且TYPE字段用来区分代码段和数据段(栈段也是特殊的数据段)

说明1:描述符的分类

X86汇编语言从实模式到保护模式10:进入保护模式_第8张图片

其中系统描述符有如下类型,

X86汇编语言从实模式到保护模式10:进入保护模式_第9张图片

说明2:处理器对描述符的处理过程

处理器拿到描述符后,先根据S位判断其分类,然后根据TYPE字段得到子类型,之后解析描述符的其他字段

2.5 DPL描述符特权级

① DPL表示描述符的特权级(Descriptor Privilege Level),指定要访问该段所必须具有的最低特权级

② 处理器共有4种特权级别,分别为0(最高特权级别)、1、2、3(最低特权级别)。刚进入保护模式时执行的代码具有最高特权级,可以看成是从处理器那里继承来的

③ 基于特权级的保护体现在如下2个方面,

a. 不同特权级别的程序是相互隔离的,其互访是严格限制的

b. 有些处理器指令(特权指令)只能由0特权级的程序来执行

说明:为何CS寄存器的上电初始值为0xF000 ?

8086处理器[CS : IP]寄存器的初始值为[0xFFFF : 0x0000],但是后续的X86处理器将上电初始值修改为[0xF000 : 0xFFF0]

修改前后,处理器上电后访问的第1条指令的地址是相同的,该修改就是为了兼容后续X86处理器引入的特权级模式

修改之后的CS寄存器初值为0xF000,当进入保护模式时,CS将被作为段选择器,其中的最低2位作为CPL,此时CPL = 0,为最高特权级,可以跳转到0特权级的操作系统代码中运行

如果不修改,当进入保护模式时,CPL = 3,为最低特权级,将无法跳转到0特权级的操作系统代码中运行

2.6 P位存在(Present)位

① P位用于指示描述符对应的段是否在内存中

② P位为0的2种情况,

a. 当内存紧张时,可能创建了段描述符,但是尚未准备对应的内存空间

b. 当内存紧张时,将不常使用的段交换到硬盘上,此时需要将P位置为0

③ P位由处理器负责检查,每当通过描述符访问内存中的段时,如果P位为0,处理器会差生一个异常中断。该中断对应的中断处理程序通常由操作系统提供,用于将该段从硬盘换回内存,并将P位置为1

说明1:这里描述的是以段为交换单位的虚拟内存管理机制,由于以段为单位进行交换性能消耗较大且容易导致内存碎片,后续被以页为单位的交换机制替代

说明2:一个段是否常用的标准,就需要依靠段描述符中TYPE字段中的A位(详见下文)

2.7 D/B位操作数大小位

① D/B位有3种功能,

a. 默认的操作数大小(Default Operation Size)标志

b. 默认的栈指针大小(Default Stack Pointer Size)标志

c. 栈上部边界(Upper Bound)标志

② D/B位的设立,主要是为了能够在32位处理器上兼容运行16位保护模式的程序(这种程序已经非常罕见,我们不做讨论)

③ 该标志对不同的段有不同的效果

a. 对于代码段,此位称作D位,用于指示指令中默认的偏移地址和操作数尺寸,其中D = 0表示默认16位;D = 1表示默认32位

e.g. 如果代码段的D位为0,处理器在该段上执行时,将使用IP寄存器,而不是EIP寄存器

b. 对于栈段,此位称作B位,用于在进行隐式的栈操作时,是使用SP寄存器还是ESP寄存器

同时,B位也决定了栈的上部边界,如果B = 0,那么栈段的上部边界为0xFFFF;如果B = 1,那么栈段的上部边界为0xFFFFFFFF

说明1:此处的栈段,更准确的说是向下扩展的数据段

说明2:在本课程中,D/B位均设置为1,不讨论16位保护模式的情况

2.8 L位64位代码段标志位

① L(Long mode, 64-bit Code Segment)位是64位代码段标志,保留此位给64位处理器使用

② 本课程中,讨论32位保护模式,该位均设置为0

2.9 TYPE描述符子类型

TYPE字段共4位,用于指示描述符的子类型,对于数据段和代码段,他们表示的类型不同,如下图所示,

X86汇编语言从实模式到保护模式10:进入保护模式_第10张图片

2.9.1 X(eXecutable)执行位

① X位表示是否可执行

② 数据段总是不可执行的,X = 0;代码段总是可执行的,X = 1

2.9.2 E(Expand)扩展位

① 用于不可执行段,E位指示段的扩展方向

② E = 0,向上(高地址方向)扩展;E = 1,向下(低地址方向)扩展

2.9.3 W(Writable)可写位

① 用于不可执行段,W位指示段是否可写(不可执行段总是可读的)

② W = 0,不允许写入,如写入将触发异常;W = 1,允许写入

2.9.4 C(Conforming)依从位

① 用于可执行段,C位指示段是否是特权级依从的

② C = 0,表示非依从代码段,这样的代码段可以从与他特权级相同的代码段调用,或者通过门调用

C = 1,表示依从代码段,这样的代码段允许从低特权级的程序转移到该段执行

2.9.5 R(Readable)可读位

① 用于可执行段,R位指示代码段是否允许读出(为了防止程序被破坏,代码段总是不可写入的)

② R = 0,不允许读出,如读出将触发异常;R = 1,允许读出

说明:R标志不是用于限制处理器读取指令的行为,而是用来限制程序像访问数据段一样访问代码段的内容。一个典型的例子,是只用mov指令和"CS:"段超越前缀读取代码段中的内容

2.9.6 A(Accessed)已访问位

① A位表示该描述符描述的段,最近是被否访问过

② 在创建描述符时,A位应该清零。之后每当该段被访问时,处理器自动将A位置1。对该位的清零由软件(操作系统)负责

③ 通过定期监视该位的状态,就可以统计出该段的使用频率。当内存紧张时,可以把不常用的段交换到硬盘上,从而实现虚拟内存管理

2.10 AVL(Available)位

① AVL位是软件可以使用的位,通常由操作系统使用,处理器并不使用该位

3. 进入保护模式示例程序分析

3.1 进入保护模式前的内存布局

① 通过定义gdt_base标号,将GDT表部署在0x7E00处

② 进入MBR执行时,[CS:IP]寄存器值为[0x0000:0x7C00],因此此处将栈设置为[SS:SP] = [0x0000:0x7C00]

完成上述设置后,内存布局如下图所示,

X86汇编语言从实模式到保护模式10:进入保护模式_第11张图片

3.2 创建GDT表

X86汇编语言从实模式到保护模式10:进入保护模式_第12张图片

3.2.1 计算GDT表逻辑地址

我们在gdt_base标号处存储的是GDT表的线性地址,而我们目前是在16位实模式下设置GDT表,所以需要先将线性地址转换为逻辑地址,也就是[段基址 : 偏移地址]的形式

3.2.2 设置GDT表项

此处共设置了4个段描述符,我们2号描述符按位展开分析,该段对应文本模式下的显示缓冲区

X86汇编语言从实模式到保护模式10:进入保护模式_第13张图片

X86汇编语言从实模式到保护模式10:进入保护模式_第14张图片

注意:课程配图中没有建立1号描述符,但本图不影响理解

3.2.3 设置GDT表大小

此处共设置了4个段描述符,共32B,因此GDT表的大小为(32 - 1 = 31)

3.3 加载GDTR

这一步完成后,在Bochs虚拟机中查看gdtr寄存器的状态,可见是符合预期的

X86汇编语言从实模式到保护模式10:进入保护模式_第15张图片

使用info gdt指令可以查看GDT表的信息

说明:上电后的GDTR寄存器值

我们先来查看处理器上电后GDRT寄存器的值

X86汇编语言从实模式到保护模式10:进入保护模式_第16张图片

接着查看开始执行MBR时GDTR寄存器的值

X86汇编语言从实模式到保护模式10:进入保护模式_第17张图片

X86汇编语言从实模式到保护模式10:进入保护模式_第18张图片

可见BIOS中设置了GDT,这是因为BIOS要检测内存1MB以上的内存信息。而且BIOS中进入过保护模式运行,并在将控制权交给MBR时重新进入了16位实模式

3.4 开启A20地址线

3.4.1 A20地址线问题

① 8086只有20根地址线(A0 ~ A19),如果地址累加超过20位,地址值将绕回。例如逻辑地址[0xFFFF : 0x0010]对应的物理地址为0xFFFF0 + 0x0010 = 0x100000,在8086中该地址值为0x00000

② 8086中有程序依靠地址绕回的特性工作

③ 从80286开始,增加了地址线个数,累加超过20位的地址值不会发生绕回。那么在兼容16位实模式时,就会影响那些依靠地址绕回特性工作的程序

④ 因此需要有一种方法,在兼容16位实模式时,能够处理A20地址线问题

3.4.2 传统开启A20方法

X86汇编语言从实模式到保护模式10:进入保护模式_第19张图片

① 在早期处理器中,将键盘的0x60端口与处理器的A20地址线相与,在16位实模式下,只要将0x60端口的输出强制拉低,就可以确保后20位产生的进位被忽略,也就实现了地址绕回的特性

② 在进入保护模式时,需要将键盘0x60端口输出为高电平,这就是开启A20的操作

③ 通过编程控制键盘控制器的方法比较繁琐,因此后续引入了快速开启A20的方法

3.4.3 快速开启A20方法

X86汇编语言从实模式到保护模式10:进入保护模式_第20张图片

① 后续的处理器增加了A20 Mask引脚,用于控制A20地址线的开关

② ICH中的0x92端口是一个8位端口,其中bit 1连接在或门上,用于实现快速开启A20

说明1:从实现电路可见,ICH芯片中也兼容了传统开启A20的方法

说明2:0x92端口的bit 0连接到INIT#引脚,用于复位处理器

说明3:在快速开启A20的代码中,0000_0010B的写法中,下划线是比特分隔符,用于增强数字的可读性

3.5 进入保护模式

CR0寄存器的bit 0为PE(Protection Enable)位,将该位置1,则处理器进入保护模式,开始按保护模式的规则开始运行

说明1:使用creg指令可以在Bochs虚拟机中查看PE位置位前后CR0寄存器的状态

X86汇编语言从实模式到保护模式10:进入保护模式_第21张图片

说明2:在进入保护模式之前关闭中断,是因为保护模式下的中断机制和实模式不同,原有的中断向量表不再适用

同时需要注意的是,在保护模式下,BIOS中断也不能使用,因为他们是实模式下的代码

3.6 保护模式下的内存访问

3.6.1 段选择器与描述符高速缓存器

X86汇编语言从实模式到保护模式10:进入保护模式_第22张图片

在32位保护模式下,段寄存器被扩充为2部分,

① 段选择器

在16位实模式下,使用方式与8086相同,用于存放逻辑段基址;在32位保护模式下,用于存放段选择符

② 描述符高速缓存器

这是一个程序不可见的部分,用来存放被选择段的线性基地址、段界限和段属性,该部分由处理器内部使用

说明1:为什么需要描述符高速缓存器 ?

因为GDT表在内存中,如果每次内存访问都要先根据段选择符访问GDT表,再根据其中的段线性基地址计算线性地址并访问内存,效率将非常低

因此在处理器的寄存器层面进行缓存,在用段选择符设置段选择器之后,处理器将会访问GDT表并缓存该表项。后续只要段选择器中的内容不变,访问内存时就直接使用描述符高速缓存器中的内容

说明2:32处理器在16位实模式下也使用了高速缓存器

当32位处理器运行在16位实模式下时,如果设置了段寄存器,处理器会将逻辑段基址左移4位,并传送到描述符高速缓存器。此后就一直使用描述符高速缓存器中的段线性基地址

需要注意的是,在16位实模式下,段描述符高速缓存器中的线性基地址只有低20位有效,高12位全部为0

我们在Bochs虚拟机中执行如下指令,并查看段寄存器的状态

X86汇编语言从实模式到保护模式10:进入保护模式_第23张图片

说明3:32处理器在16位实模式下段长度依然只有64KB

当32位处理器运行在16位实模式下时,也可以使用32位寄存器进行寻址,但是这并不会使得段长度达到4GB

这是因为在实模式下(参考上一张截图),段界限被设置为0x0000FFFF,粒度G位被设置为0,因此段长度为64KB,超出该范围的访问会触发处理器异常

我们在实模式下运行如下代码进行验证,可见ebx提供的有效地址已经超过64KB,应该触发处理器异常

经过验证,越界访问确实触发了处理器异常,且异常原因为读取越界

X86汇编语言从实模式到保护模式10:进入保护模式_第24张图片

3.6.2 段选择符(Segment Selector)

X86汇编语言从实模式到保护模式10:进入保护模式_第25张图片

段选择符由3部分构成,

① 描述符索引

共13位,正好最多可以索引8192个描述符,与GDT表的最大表项数匹配

② 描述符表指示器TI(Table Indicator)

TI = 0,表示描述符在GDT中;TI = 0,表示描述符在LDT中

③ 请求特权级RPL(Request Privilege Level)

表示给出当前段选择符的那个程序的特权级别

3.6.3 保护模式访问内存过程

X86汇编语言从实模式到保护模式10:进入保护模式_第26张图片

① 当处理器执行任何改变段选择器的指令时,就将指令中提供的索引号乘以8作为偏移地址,同GDTR中提供的线性基地址相加,以访问GDT

② 如果权限检查通过,则将描述符加载到描述符高速缓存。之后只要不改变段选择器,就直接用描述符高速缓存中的线性基地址

X86汇编语言从实模式到保护模式10:进入保护模式_第27张图片

③ 访问内存时,将描述符高速缓存中的线性基地址与寻址方式给出的段内偏移相加,构成要访问的线性地址

④ 对该线性地址的访问请求进行检查(e.g. 是否越界,是否可读写),如果检查通过,则进行内存访问

3.6 保护模式下的长跳转

X86汇编语言从实模式到保护模式10:进入保护模式_第28张图片

需要特别注意的是,在执行jmp指令时,处理器已经处于保护模式,将按照保护模式的模式运行

3.6.1 长跳转目的地分析

① 此处的jmp指令实现直接绝对远跳转,使用0x0008设置CS,使用flush的汇编地址设置EIP

此处的dword关键字用于修饰偏移地址,意思是要求使用32位的偏移量

② 设置到CS中的0x0008(0b1 0 00)为段选择符,对应段选择符的3个字段如下,

a. 描述符索引 = 1,选择第1个段描述符(从0开始)

b. TI = 0,描述符在GDT中

c. RPL = 0b00,表示最高特权级

因此跳转之后,将从flush标号处开始执行

3.6.2 远跳转的其他功能

此处的jmp远跳转除了实现跳转到flush标号,还实现了如下2个功能,

3.6.2.1 刷新描述符高速缓存器

如上文所述,32位处理器工作在16位实模式下时,也会使用描述符高速缓存器,只是使用的不完全。当处理器进入保护模式后,这些内容依然残留着,但不影响使用,程序可以继续执行

但是这些残留的内容在保护模式下是无效的,迟早会在执行某些指令时出问题。因此需要尽快刷新段选择器和描述符高速缓存器

3.6.2.2 刷新流水线

在进入保护模式前,有很多指令已经进入了流水线。因为处理器工作在实模式下,所以他们都是按16位操作数和16位地址长度进行译码的,即使是那些用bit 32编译的指令

进入保护模式后,受CS段描述符高速缓存器中实模式残留内容的影响,处理器进入16位保护模式工作,处理器会按16位的方式译码32位指令。此时通过转移指令,可以清空流水线,并串行化执行(串行化执行是为了处理已经无效的乱序执行的中间结果)

说明1:一般建议在设置了CR0寄存器的PE位之后,立即用jmp或call指令转移到目标地址执行

说明2:[bits 32]伪指令用于标识后续的指令均按32位模式编译

3.7 打印字符串

X86汇编语言从实模式到保护模式10:进入保护模式_第29张图片

此处设置到DS段选择器中的段选择符指向第2个段描述符,对应文本模式下的显示缓冲区

3.8 验证32位栈操作

X86汇编语言从实模式到保护模式10:进入保护模式_第30张图片

3.8.1 栈段描述符分析

此处段选择符对应的段描述符如下,

① 段基地址 = 0x00000000

② 段界限 = 0x07A00

③ G = 0,段界限以字节为单位

④ E = 1,向下(低地址处)扩展

⑤ D/B = 1,32位的默认栈操作

3.8.2 验证方式分析

根据上节对栈段描述符的分析,在这个栈段上的默认操作为32位的,此处验证的方式就是判断数据压栈后(即使只想压栈1B),ESP是否减4

你可能感兴趣的:(计算机体系结构)