保护模式概述
为什么要有保护模式?
- 实模式下操作系统和用户程序属于同一特权级,平起平坐,没有区别对待;
- 用户程序所引用的地址都是指向真实的物理地址,也就是说逻辑地址等于物理地址,实实在在的指哪打哪;
- 用户程序可以自由修改段基址,可以不亦乐乎地访问所有内存,没人拦得住;
以上是一些安全方面的考量,还有以下一些使用上的缺陷:
- 访问超过64KB的内存区域是要切换段基址,转来转去容易晕呼;
- 一次只能运行一个程序,无法充分利用计算机资源;
- 共20条地址线,最大可用内存为1MB,这即使在20年前也不够用。
注:实模式的CPU运行环境是16位,保护模式的运行环境是32位。
初见保护模式
保护模式之寄存器扩展
进入保护模式后,段寄存器中保存的再也不是段基址了,里面保存的内容叫选择子 selector,该选择子其实就是个数,用这个数来索引全局描述符表中的段描述符,把全局描述符表当成数组,选择子就像数组下标一样。
在 80286的保护模式中,为了提高获取段信息的效率,对段寄存器率先应用了缓存技术,将段信息用一个寄存器来缓存,这就是段描述符缓冲寄存器( Descriptor Cache Registers )。对程序员而言它是不可见的。 CPU 每次将千辛万苦获取到的内存段信息,整理成“完整的、通顺、不鳖脚”的形式后,存入段描述符缓冲寄存器,以后每次访问相同的段时,就直接读取该段寄存器对应的段描述符缓冲寄存器。
保护模式之寻址扩展
保护模式之运行模式反转
bits 的指令格式是 [bits 16]或 [bits 32]。
[bits 16]是告诉编译器,下面的代码帮我编译成 1 6 位的机器码 。
[bits 32]是告诉编译器,下面的代码帮我编译成 32 位的机器码。
注:
进入保护模式需要三个步骤。
(1) 打开 A20 。(2) 加载 gdt 。(3) 将 cr0 的 pe 位置 1 。
全局描述符表
全局描述符表( Global Descriptor Table, GDT )是保护模式下内存段的登记表,这是不同于实模式的显著特征之一 。
段描述符
8~ 11 位是 type 字段 , 共 4 位,用来指定本描述符的类型。这里要提前说下段描述符的 S 宇段了 。 是这样的, 一个段描述符,在 CPU 眼里分为两大类,要么描述的是系统段,要么描述的是数据段,这是由段描述符中的 S 位决定的,用它指示是否是系统段。在 CPU 眼里,凡是硬件运行需要用到的东西都可称之为系统,凡是软件(操作系统也属于软件, CPU 眼中,它与用户程序无区别)需要的东西都称为数据,无论是代码,还是数据,甚至包括栈,它们都作为硬件的输入,都是给硬件的数据而己,所以代码段在段描述符中也属于数据段(非系统段)。 S 为 0 时表示系统段, S 为 1 时表示数据段。 type 字段是要和 S 字段配合在一起才能确定段描述符的确切类型,只有 S 宇段的值确定后, type 宇段的值才有具体意义。
什么是系统段?
各种称为“门”的结构便是系统段,也就是硬件系统需要的结构,非软件使用的,如调用门、任务门。简而言之,门的意思就是入口,它通往一段程序。关于系统段这里咱们不再多说,目前主要是关注 S 为 1 时,非系统段的。type 子类型。
-
表中的 A 位表示 Accessed 位,这是由 CPU 来设置的,每当该段被 CPU 访问过后, CPU 就将此位置 1.所以,创建一个新段描述符时,应该将此位置 0。我们在调试时,根据此位便能判断该描述符是否可用啦。
-
C 表示一致性代码段,也称为依从代码段, Conforming 。 一致性代码段是指如果自己是转移的目标段,并且自己是一致性代码段,自己的特权级一定要高于当前特权级,转移后的特权级不与自己的 DPL 为主,而是与转移前的低特权级一致,也就是听从、依从转移前的低特权级。 C 为 1 时则表示该段是一致性代码段, C 为 0 时则表示该段为非一致性代码段。
-
R 表示可读, R 为 1 表示可读, R 为 0 表示不可读。这个属性一般用来限制代码段的访问。如果指令执行过程中, CPU 发现某些指令对 R 为 0 的段进行访问,如使用段超越前缀 cs 来访问代码段, CPU 将抛出异常。啰嗦一小下,内存中的数据对 CPU 来说是要处理的数据,仅仅是 CPU 的输入而己, CPU 的铁骑可以踏遍任意角落。所以,不可读的代码段只是来限制代码指令的,并不是连 CPU 也不能看。
-
X 表示该段是否可执行, EXecutable 。我们所说的指令和数据,在 CPU 眼中是没有任何区别的,都是010101 这样类似的二进制。所以要用 type 中的 X 位来标识出是否是可执行的代码。代码段是可执行的,即 X 为 1 。 而数据段是不可执行的,即 X 为 0
-
E 是用来标识段的扩展方向, Extend 。 E 为 0 表示向上扩展,即地址越来越高,通常用于代码段和数据段。 E 为 1 表示向下扩展,地址越来越低,通常用于梳段。
-
W 是指段是否可写, Writable 。 W 为 1 表示可写,通常用于数据段。 W 为 0表示不可写入,通常用于代码段。对于 W 为 0 的段有写入行为,同样会引发 CPU 抛出异常。
-
段描述符的第 15 位是 P 字段, Present,即段是否存在。如果段存在于内存中, P 为 1 ,否则 P 为 0 。
全局描述符表GDT、局部描述符表LDT及选择子
一个段描述符只用来定义(描述)一个内存段。代码段要占用一个段描述符、数据段和战段等,多个内存段也要各自占用一个段描述符,这些描述符放在哪里呢?答案是放在全局描述符表,就是本节开头所说的 GDT (Global Descriptor Table)。全局描述符表 GDT 相当于是描述符的数组,数组中的每个元素都是8 宇节的描述符。可以用选择子(马上会讲到)中提供的下标在 GDT 中索引描述符。
段寄存器 CS 、 DS、 ES 、 FS 、 GS 、 SS,在实模式下时,段中存储的是段基地址,即内存段的起始地址。而在保护模式下时,由于段基址已经存入了段描述符中,所以段寄存器中再存放段基址是没有意义的,在段寄存器中存入的是一个叫作选择子的东西一selector。选择子“基本上”是个索引值,这里说的是基本上,其中还有其他属性咱们一会再说。用此索引值在段描述符表中索引相应的段描述符,这样,便在段描述符中得到了内存段的起始地址和段界限值等相关信息。
由于段寄存器是 16 位,所以选择子也是 16 位,在其低 2 位即第 0~ 1 位,用来存储 RPL,即请求特权级,可以表示 0、 1 、 2、 3 四种特权级。关于 RPL 我们会在专门讲特权级的章节中详尽说明,此处可以理解为请求者的当前特权级(不理解也没关系,因为在本章中它不重要〉。在选择子的第 2 位是 TI 位,即 Table Indicator,用来指示选择子是在 GDT 中,还是 LDT 中索引描述符。 TI为 0 表示在 GDT 中索引描述符, TI 为 1 表示在 LDT 中索引描述符。选择子的高 13 位,即第 3~ 15 位是描述符的索引值,用此值在 GDT 中索引描述符。前面说过 GDT 相当于一个描述符数组,所以此选择子中的索引值就是 GDT 中的下标。
选择子的作用主要是确定段描述符,确定描述符的目的,一是为了特权级、界限等安全考虑,最主要的还是要确定段的基地址。
虽然到了保护模式,但 IA32 架构始终脱离不了内存分段,即访问内存必须要用“段基址:段内偏移地址”的形式。保护模式下的段寄存器中已经是选择子,不再是直接的段基址。段基址在段描述符中,用给出的选择子索引到描述符后, CPU 自动从段描述符中取出段基址,这样再加上段内偏移地址,便凑成了“段基址:段内偏移地址”的形式。
例如选择子是 0x8,将其加载到 ds 寄存器后,访问 ds: 0x9 这样的内存,其过程是: 0x8 的低 2 位是RPL,其值为 00。第 2 是 TI ,其值 0,表示是在 GDT 中索引段描述符。用 0x8 的高 13 位 0x1 在 GDT 中索引,也就是 GDT 中的第 1 个段描述符( GDT 中第 0 个段描述符不可用)。假设第 1 个段描述符中的 3个段基址部分,其值为 0x1234。CPU 将 0x1234 作为段基址,与段内偏移地址 0x9 相加, 0x1234+0x9=0x123d。用所得的和 0x123d 作为访存地址。
打开A20地址线
如今我们是在保护模式下,我们需要突破第 20 条地址线( A20 )去访问更大的内存空间 。 而这一切,只有关闭了地址回绕才能实现 。而关闭地址回绕,就是上面所说的打开 A20Gate 。
保护模式的开关, CRO 寄存器的 PE 位
;------------- loader和kernel ----------
LOADER_BASE_ADDR equ 0x900
LOADER_START_SECTOR equ 0x2
;-------------- gdt描述符属性 -------------
DESC_G_4K equ 1_00000000000000000000000b
DESC_D_32 equ 1_0000000000000000000000b
DESC_L equ 0_000000000000000000000b ; 64位代码标记,此处标记为0便可。
DESC_AVL equ 0_00000000000000000000b ; cpu不用此位,暂置为0
DESC_LIMIT_CODE2 equ 1111_0000000000000000b
DESC_LIMIT_DATA2 equ DESC_LIMIT_CODE2
DESC_LIMIT_VIDEO2 equ 0000_000000000000000b
DESC_P equ 1_000000000000000b
DESC_DPL_0 equ 00_0000000000000b
DESC_DPL_1 equ 01_0000000000000b
DESC_DPL_2 equ 10_0000000000000b
DESC_DPL_3 equ 11_0000000000000b
DESC_S_CODE equ 1_000000000000b
DESC_S_DATA equ DESC_S_CODE
DESC_S_sys equ 0_000000000000b
DESC_TYPE_CODE equ 1000_00000000b ;x=1,c=0,r=0,a=0 代码段是可执行的,非依从的,不可读的,已访问位a清0.
DESC_TYPE_DATA equ 0010_00000000b ;x=0,e=0,w=1,a=0 数据段是不可执行的,向上扩展的,可写的,已访问位a清0.
DESC_CODE_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_CODE2 + DESC_P + DESC_DPL_0 + DESC_S_CODE + DESC_TYPE_CODE + 0x00
DESC_DATA_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_DATA2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x00
DESC_VIDEO_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_VIDEO2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x0b
;-------------- 选择子属性 ---------------
RPL0 equ 00b
RPL1 equ 01b
RPL2 equ 10b
RPL3 equ 11b
TI_GDT equ 000b
TI_LDT equ 100b
%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR
jmp loader_start ; 此处的物理地址是:
;构建gdt及其内部的描述符
GDT_BASE: dd 0x00000000
dd 0x00000000
CODE_DESC: dd 0x0000FFFF
dd DESC_CODE_HIGH4
DATA_STACK_DESC: dd 0x0000FFFF
dd DESC_DATA_HIGH4
VIDEO_DESC: dd 0x80000007 ;limit=(0xbffff-0xb8000)/4k=0x7
dd DESC_VIDEO_HIGH4 ; 此时dpl已改为0
GDT_SIZE equ $ - GDT_BASE
GDT_LIMIT equ GDT_SIZE - 1
times 60 dq 0 ; 此处预留60个描述符的slot
SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0 ; 相当于(CODE_DESC - GDT_BASE)/8 + TI_GDT + RPL0
SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0 ; 同上
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0 ; 同上
;以下是定义gdt的指针,前2字节是gdt界限,后4字节是gdt起始地址
gdt_ptr dw GDT_LIMIT
dd GDT_BASE
loadermsg db '2 loader in real.'
loader_start:
;------------------------------------------------------------
;INT 0x10 功能号:0x13 功能描述:打印字符串
;------------------------------------------------------------
;输入:
;AH 子功能号=13H
;BH = 页码
;BL = 属性(若AL=00H或01H)
;CX=字符串长度
;(DH、DL)=坐标(行、列)
;ES:BP=字符串地址
;AL=显示输出方式
; 0——字符串中只含显示字符,其显示属性在BL中。显示后,光标位置不变
; 1——字符串中只含显示字符,其显示属性在BL中。显示后,光标位置改变
; 2——字符串中含显示字符和显示属性。显示后,光标位置不变
; 3——字符串中含显示字符和显示属性。显示后,光标位置改变
;无返回值
mov sp, LOADER_BASE_ADDR
mov bp, loadermsg ; ES:BP = 字符串地址
mov cx, 17 ; CX = 字符串长度
mov ax, 0x1301 ; AH = 13, AL = 01h
mov bx, 0x001f ; 页号为0(BH = 0) 蓝底粉红字(BL = 1fh)
mov dx, 0x1800 ;
int 0x10 ; 10h 号中断
;---------------------------------------- 准备进入保护模式 ------------------------------------------
;1 打开A20
;2 加载gdt
;3 将cr0的pe位置1
;----------------- 打开A20 ----------------
in al,0x92
or al,0000_0010B
out 0x92,al
;----------------- 加载GDT ----------------
lgdt [gdt_ptr]
;----------------- cr0第0位置1 ----------------
mov eax, cr0
or eax, 0x00000001
mov cr0, eax
;jmp dword SELECTOR_CODE:p_mode_start ; 刷新流水线,避免分支预测的影响,这种cpu优化策略,最怕jmp跳转,
jmp SELECTOR_CODE:p_mode_start ; 刷新流水线,避免分支预测的影响,这种cpu优化策略,最怕jmp跳转,
; 这将导致之前做的预测失效,从而起到了刷新的作用。
[bits 32]
p_mode_start:
mov ax, SELECTOR_DATA
mov ds, ax
mov es, ax
mov ss, ax
mov esp,LOADER_STACK_TOP
mov ax, SELECTOR_VIDEO
mov gs, ax
mov byte [gs:160], 'P'
jmp $
保存模式之内存段的保护
向段寄存器加载选择子时的保护
首先根据选择子的值验证段描述符是否超越界限。
检查过程如下:处理器先检查 TI 的值,如果 TI是0 ,则从全局描述符表寄存器gdtr中拿到 GDT 基地址和 GDT 界限值。如果TI是1,则从局部描述符表寄存器 ldtr 中拿到 LDT 基地址和 LDT 界限值。有了描述符表基地址和描述符表界限值后,把
选择子的高 13 位代入上面的表达式,若不成立,处理器则抛出异常。
在选择子检查过后,就要检查段的类型了。
这里主要是检查段寄存器的用途和段类型是否匹配。大的原则如下:
• 只有具备可执行属性的段(代码段)才能加载到 cs 段寄存器中。
• 只具备执行属性的段(代码段)不允许加载到除 cs 外的段寄存器中。
• 只有具备可写属性的段(数据段)才能加载到 SS 栈段寄存器中。
• 至少具备可读属性的段才能加载到 DS 、 ES 、FS、 GS 段寄存器中。
检查完 type 后,还会再检查段是否存在。 CPU 通过段描述符中的 P 位来确认内存段是否存在,如果P 位为 1 ,则表示存在,这时候就可以将选择子载入段寄存器了,同时段描述符缓冲寄存器也会更新为选择子对应的段描述符的内容,随后处理器将段描述符中的 A 位置为 1 ,表示己经访问过了。如果 P 位为 0,则表示该内存段不存在,不存在的原因可能是由于内存不足,操作系统将该段移出内存转储到硬盘上了。这时候处理器会抛出异常,自动转去执行相应的异常处理程序,异常处理程序将段从硬盘加载到内存后井将 P 位置为 1 ,随后返回。 CPU 继续执行刚才的操作,判断 P 位。
注意啦,以上所涉及到的 P 位,其值由软件(通常是操作系统)来设置,由 CPU 来检查。 A 位由 CPU来设置。
代码段和数据段的保护
对于代码段和数据段来说, CPU 每访问一个地址,都要确认该地址不能超过其所在内存段的范围 。