第12章 进入保护模式
章节一开始说明了为什么要有保护模式?后续介绍了如何进入保护模式。
实模式:在实模式下,程序是可以“为所欲为”的。它想访问内存的哪一部分,都可以很轻松地通过设置段地址和偏移地址来办到。
保护模式:在多用户、多任务时代,内存中会有多个用户(应用)程序在同时运行。为了使它们彼此隔离,防止因某个程序的编写错误或者崩溃而影响到操作系统和其他用户程序,使用保护模式是非常有必要的。
代码清单12-1
该章代码的功能:程序运行后从实模式进入保护模式,并在屏幕上输出字符:Protect Mode OK.
全局描述符表
段描述符(Segment Descriptor):每个段都需要一个描述符,来描述段的信息,长度为8字节。段的信息包括起始地址,段的界限等各种访问属性。
描述符表(Descriptor Table):所有段描述符都集中放在一起,称为描述符表。
全局描述符表(Global Descriptor Table,GDT):该表是为整个软硬件系统服务的。
后面还有一个局部描述符表是为(Local Descriptor Table,LDT)是为单个应用服务的。
全局描述符表寄存器(GDTR):处理器内部用于跟踪全局描述符表的48位寄存器。该寄存器包含两个部分:
- 32位的线性地址:范围0x00000000到0xFFFFFFFF,2^32次方,4GB内存的范围。
- 16位的边界:最大2^16字节,每个描述符占8个字节,所以总共可以存储8192个描述符。
定义GDT:进入保护模式之前需要定义GDT,在实模式下只能访问1MB的内存,所以GDT一般定义在1MB以下的内存范围。
存储器的段描述符
段描述符格式:每个描述符在GDT中占8字节(64位),分为低32位和高32位。
- 段基地址:32位,可以是0~4GB的任意范围;未开启分页功能,就是物理地址。
- 段界限:20位,指定段的边界,也就是大小,范围:0x00000~0xFFFFF。
- 偏移量从0开始:代码段、数据段和栈段,偏移量从0开始,偏移量的最大值就是段段界限。
- 偏移量从最大值开始:专为栈段设计,段界限就为段内不可使用的最小偏移量。
- G位:粒度(Granularity)位,用于解释段界限的含义。
- 0:段界限以字节为单位,段的扩展范围是从1字节到1兆字节(1B~1MB)。
- 1:界限是以4KB为单位,的扩展范围(段的大小)是从4KB到4GB)。
- D/B位:默认操作尺寸”(Default Operation Size)或者“默认的栈指针尺寸”(Default Stack Pointer Size),又或者“上部边界”(Upper Bound)标志。主要是为了兼容16位保护模式。
- L位:4位代码段标志(64-bit Code Segment),保留此位给64位处理器使用。
- AVL位:软件可以使用的位(Available),通用由操作系统安排。
- P位:段存在位(Segment Present),用于指示描述符所对应的段是否存在。
- DPL:描述符的特权级(Descriptor Privilege Level, DPL),分别是0、1、2、3,其中0是最高特权级别,3是最低特权级别。
- S位:指定描述符的类型(Descriptor Type)。
- 0:系统段。
- 1:码段或者数据段(栈段也是特殊的数据段)
- TYPE位:用于指示描述符的子类型,有数据和代码两种。
书中一开始设置了堆栈段和栈指针、GDT。相关段:
- 代码段段地址:0x0000。
- 栈段段地址:0x0000,栈指针 0x7c00,向下扩展。
- GDT:0x07e0,实际物理地址从 0x00007e00 开始,最大偏移地址:0x00007e00 + 0x0000FFFF = 0x00017DFF,总计64KB的空间。
代码我加了一些注释:
;设置堆栈段和栈指针
mov ax, cs ;cs代码段为0x0000
mov ss, ax ;ss栈段位0x0000
mov sp, 0x7c00 ;sp栈指针0x7c00,向下扩展
......
......
gdt_size dw 0
gdt_base dd 0x00007e00 ;GDT的物理地址
此时内存映像:
为什么GDT保存在 0x00007e00 这个位置?
主引导扇区程序起始地址为0x00007c00,共512(0x200)字节。
结束地址就是:0x00007c00 + 512 = 0x00007e00
安装存储器的段描述符并加载GDTR
计算GDT所在的逻辑段地址和偏移地址:代码我加了一些注释。
;计算GDT所在的逻辑段地址
mov ax, [cs: gdt_base + 0x7c00] ;低16位, 值为 0x7e00
mov dx, [cs: gdt_base + 0x7c00 + 0x02] ;高16位, 值位 0x0000
mov bx, 16 ;段地址要16位对齐
div bx ;ax存储商,dx存储余数
mov ds, ax ;令DS指向该GDT段以进行操作
mov bx, dx ;段内起始偏移地址
......
......
gdt_size dw 0
gdt_base dd 0x00007e00 ;GDT的物理地址
这里之所以要加上0x7c00,是因为目前代码段cs地址为:0x0000。
安装空描述符:处理器规定,GDT中的第一个描述符必须是空描述符,或者叫“哑描述符”、NULL描述符。
;创建0#描述符,它是空描述符,这是处理器的要求
mov dword [bx+0x00],0x00 ;低32位
mov dword [bx+0x04],0x00 ;高32位
创建1#描述符:进入保护模式要在屏幕上显示一行文本,显示文本需要访问显存,要访问显存就必须将它定义成一个段,并创建一个描述符。
;创建#1描述符,保护模式下的数据段描述符(文本模式下的显示缓冲区)
mov dword [bx+0x08],0x8000ffff ;低32位
mov dword [bx+0x0c],0x0040920b ;高32位
该描述符在内存中的映像:
根据段描述符的格式,可以分析得出:
- 线性基地址:0x000B8000;
- 段界限:0x0FFFF;
- 粒度为字节(G=0),即,该段的长度为64KB;
- 属于存储器的段(S=1);
- 这是一个32位的段(D=1);
- 该段目前位于内存中(P=1);
- 段的特权级为0(DPL=00);
- 这是一个可读可写、向上扩展的数据段(TYPE=0010);
初始化描述符表寄存器GDTR:加载描述符表的线性基地址和界限到寄存器GDTR,这要使用lgdt指令。
lgdt m ;有效地址m处,包含了GDT的32位线性地址和16位界限值,共6字节。
; 前(低)16位是GDT的界限值。
; 后(高)32位是GDT的基地址。
相关代码:
;初始化描述符表寄存器GDTR
mov word [cs: gdt_size+0x7c00],15 ;描述符表的界限(总字节数减一)
lgdt [cs: gdt_size+0x7c00] ;将gdt_size开始的6个字节加载到gdtr
......
......
gdt_size dw 0
gdt_base dd 0x00007e00 ;GDT的物理地址
关于第21条地址线A20的问题
回绕特性:8086只有20根地址线,当物理地址达到最高端0xFFFFF时,再加1,结果为0x100000。但因为它只能维持20位的地址,故进位自然丢失,地址又绕回最低地址端0x00000。
20根地址线最大地址 + 1
= 0xFFFFF + 0x1
= 0x100000 (因为只有20根地址线,高位丢弃)
= 0x000000
A20控制策略:80286有24条地址线,地址回绕就不行了。为了兼容8086,强制第21根地址线恒为0,通过键盘控制器接口进行设置。
改进后的A20控制策略:80486有了A20M#引脚(A20屏蔽,A20 Mask),通过输入输出控制器集中芯片ICH的端口0x92设置A20线。
书中涉及的代码是这三行:
in al,0x92 ;南桥芯片内的端口,读取0x92端口的数据
or al,0000_0010B ;第2位(位1)置为1,表示打开A20线
out 0x92,al ;写回0x92端口,打开A20
保护模式下的内存访问
CR0寄存器:CR0是处理器内部的控制寄存器(Control Register, CR)。是32位的,包含了一系列用于控制处理器操作模式和运行状态的标志位。
它的第1位(位0)是保护模式允许位(Protection Enable, PE),为1则表示进入保护模式。
关中断:保护模式下,原有的中断向量表不再适用。
cli ;保护模式下中断机制尚未建立,应禁止中断
CR0寄存器位0置1:
mov eax,cr0 ;将cr0读入eax
or eax,1 ;第1位(位0)置1
mov cr0,eax ;写回cr0,设置PE位
在Bochs中可以通过creg命令查看控制寄存器的内容。
段寄存器在实模式和保护模式下的工作方式:在32位处理器上,段寄存器CS、DS、ES、FS、GS、SS,有相应的描述符高速缓存器,用来缓存计算过后的线性地址。
1)实模式:段寄存器存储的是逻辑段地址,采用段地址和偏移地址的寻址方式,实际物理地址为:段地址0x10+偏移地址。段地址0x10 的结果会保留描述符高速缓存器,下次用的时候直接从这里取就可以了。
例如:
mov cx,0x2000 ;设置ds为0x2000
mov ds,cx
mov [0xc0],al ;寻址:0x2000*0x10+0xc0,
;0x20000 这个结果会保存在描述符高速缓冲器
2)保护模式:段寄存器存储的是段选择子。段选择子包含:描述符在描述符表中的索引号、TI、RPL。
设置数据段描述符选择子:将描述符选择子0x0008(二进制数0000_0000_00001_0_00)传送到段选择器DS中。
mov cx,00000000000_01_000B ;加载数据段选择子(0x08)
mov ds,cx
段选择子的工作方式:每个描述符占8字节,描述符在描述符表内的偏移地址是索引号乘以8,又因为GDT的线性基地址在GDTR中,所以描述符的实际物理地址为:
描述符的实际物理地址 = GDTR的线性基地址 + 描述符索引号*8
从GDT表中获取到段描述符后,会将段的线性基地址、段界限和段的访问属性保存到描述符高速缓存器中。后续用到该段寄存器时,就会直接从缓存器中获取内容。
画个示意图:
在屏幕上打印字符:在屏幕上显示"Protect mode OK."
;以下在屏幕上显示"Protect mode OK."
mov byte [0x00],'P' ;默认使用ds寄存器,下同
mov byte [0x02],'r'
mov byte [0x04],'o'
mov byte [0x06],'t'
mov byte [0x08],'e'
mov byte [0x0a],'c'
mov byte [0x0c],'t'
mov byte [0x0e],' '
mov byte [0x10],'m'
mov byte [0x12],'o'
mov byte [0x14],'d'
mov byte [0x16],'e'
mov byte [0x18],' '
mov byte [0x1a],'O'
mov byte [0x1c],'K'
mov byte [0x1e],'.'
数据段访存方式:每当有访问内存的指令时,就不再访问GDT中的描述符,直接用当前段寄存器描述符高速缓存器提供线性基地址。
代码段访存方式:不单是访问数据段,即使在处理器取指令执行时,也采用了相同的方法。一开始执行代码的时候,处理器将逻辑段地址左移4次,形成20位地址,左侧补0,补足32位后再传送到CS段描述符高速缓存器。此后,就直接使用这个32位的线性基地址访问内存(包括取指令)。
为什么不用更改CS段选择器:实模式和16位保护模式都是16位的工作模式,在这两种工作模式下,指令的格式和寻址方式相同,执行相同的操作,默认的数据操作尺寸都是16位的,使用16位有效地址访问内存。
如果后面要更改CS段选择器,就要用选择子的方式了。
程序的运行和调试
运行程序并观察结果
程序运行后从实模式进入保护模式,并在屏幕上输出字符:Protect Mode OK.
处理器刚加电时的段寄存器状态
自测试(Build-In Self-Test, BIST):在x86处理器加电后,它的固件会对自身进行初始化,还可以选择执行一个内置的自测试。
预置值:当处理器初始化完成后,它内部的各个寄存器,包括通用寄存器、段寄存器、控制寄存器、指令指针寄存器EIP、栈指针寄存器ESP等都会有一个预置的值。
预置值查询:
- 查阅手册:NTEL公司的手册Intel® 64 and IA-32 Architectures Software Developer’s Manual;
- Bochs用软件来模拟处理器的工作。
使用sreg查看段寄存器状态:
- "dh"和"dl"的内容是Bochs根据段寄存器描述符高速缓存器的内容构造的。dh是段描述符的高32位;dl是段描述符的低32位。
- “Data segment” 表示该段是数据段;
- “base” 指示段的基地址;
- “limit” 指示段的界限;
- “Read/Write” 表示段可读可写;
- “Accessed” 指示段曾经被访问过。
CS预置值含义:CS段描述符高速缓存器中的基地址被预置为0xFFFF0000,EIP的预置内容是0x0000FFF0,所以处理器第一次取指令时发出的地址是0xFFFFFFF0。
处理器的设计者希望把ROM-BIOS放到4GB可寻址内存范围的最高端,4GB以下,连同传统的低端1MB都是连续的RAM区,连续的、不间断的RAM能为操作系统管理内存带来方便。
兼容低端1MB的ROM-BIOS:执行的第一条指令: jmp far f000:e05b(jmp 0xf000:0xe05b),CS段描述符高速缓存器中的基地址就变成了 0x000F0000 ,就转到低地址端的BIOS执行了。
设置PE位后的段寄存器状态
刚设置好PE位时,尽管进入了保护模式,但显示的依然是实模式。
代码段寄存器CS描述符高速缓存器的dh为0x00009300,即,G=0,D=0,L=0,P=1,DPL=00,S=1,TYPE=0011。通俗地说,这是一个粒度为字节的数据段。
刚进入保护模式后,CS描述符高速缓存器中的D位是0,所以是工作在16位保护模式下。只要处理器执行的是16位代码,就不会有问题。
加载段寄存器DS之后的状态
进入保护模式后,用段选择子0x0008来加载段寄存器DS,处理器用这个选择子从全局描述符表GDT中取出对应的描述符,并刷新DS描述符高速缓存器。
为什么ds的dh是0040930b?正常应该是0040920b?
前面都还正常,执行了 mov ds,cx 之后就变成 0040930b 了。
在操作过程中,发现这个值变成了0040930b,前面设置的时候是0040920b,怎么变了呢?后面查询段描述符结构才发现是描述符TYPE位的Access标志被置为1了,所以加1就变成3了。
查看全局描述符表GDT
查看指令:info gdt
- 第一行:GDT的线性地址基地址(0x7e00)及界限值(15),
- 第二行开始:Bochs给出了每个描述符的索引和相关信息。
- 第一个表项的索引号是0,它是空描述符;
- 第二个表项的索引号是0x0008,在中括号里给出了。在这一行里,“Data segment”表明这是数据段的描述符;“base”表示段的32位基地址;“limit”表示段界限值;“Read/Write”表示段是可读可写的;“Accessed”表示段被访问过。
查看控制寄存器的内容
查看指令:creg
CR0的内容:0x60000011,为了方便起见,Bochs以大小写的形式给出了各个控制位的状态,小写表示该位是“0”,大写表示该位是“1”,即处于置位状态。图中显示了大写的“PE”,表明此位是“1”,处于保护模式。
本章习题
如果一个数据段描述符的G位是1,E位是0,段的基地址为0,段界限为1,该段一共有多少字节?段的线性地址范围是什么?
答:根据段描述符格式可以得出:
- G=1表示界限是以4KB为单位。
- E=0这里应该是D位?
该段一共有 1*4KB = 4KB,段的线性地址范围是:0x00000000 ~ 0x00001000 ,4KB的空间。