一.为什么要有保护模式
1)实模式下操作系统和用户程序属于同一特权级
2)用户程序所引用的地址都是指向真实物理地址的,也就是说逻辑地址等于物理地址,实实在在的指哪打哪
3)用户程序可以自由修改段基址,可以访问所有内存
4)访问超过64kb的内存区域时要切换段基址,转来转去容易晕乎
5)一次只能运行一个程序,无法充分利用计算机资源
6)共20条地址线,最大可访问的内存为1MB
二.初见保护模式
1.保护模式之寄存器
-
保护模式大大提高了安全性,其中很大一部分的安全就体现在了内存段的描述方面,偏移地址还是和实模式下一样,但是段寄存器保存的再也不是段基址了,为了更安全添加了约束条件,这些“约束条件”便是对内存段的描述信息,由于信息太多,一个寄存器肯定放不下,所以专门找了数据结构——全局描述符表,其中每一个表项都称为段描述符,其大小为64字节,用来描述各内存段的起始地址、大小、权限等信息,全局描述符表很大所以放在内存中,由GDTR寄存器指向它。
-
段寄存器里面保存的内容叫“选择子”,selector,选择子其实就是个数,用来索引全局描述符表中的段描述符。把全局描述符表当成数组,选择子就像是数组的下标一样
2.保护模式之寻址运行模式
- [bis16] 是告诉编译器,下面的代码帮我编译成16位的机器码
- [bis32]是告诉编译器,下面的代码帮我编译成32位的机器码
3.保护模式之指令扩展
push虽说可以压入8位立即数,但实际上,对CPU来说,出于对齐的考虑,操作数要么是16位,要么是32位,所以8位立即数会扩展成各模式下的默认操作数宽度,即实模式下8位立即数扩展成16位后再入栈,保护模式下扩展成32位后再入栈
在实模式下:
- 当压入8位立即数时, 由于实模式下默认操作数是16位,CPU将其扩展为16后入栈,esp指针减2
- 当压入16位立即数时,CPU直接压入2字节,sp-2
- 当压入32位立即数时,CPU直接压入4字节,sp-4
在保护模式下
- 当压入8位立即数时, 由于保护模式下默认操作数是32位,CPU将其扩展为32后入栈,esp指针减4
- 当压入16位立即数时,CPU直接压入2字节,esp-2
- 当压入32位立即数时,CPU直接压入4字节,esp-4
三.全局描述符表
1.段描述符
实模式下存在的问题:
- 实模式下的用户程序可以随意破坏存储代码的内存区域,所以要添加个内存段类型属性来阻止这种行为。
- 实模式下的用户程序和操作系统是同一级别的,所以要添加个特权级属性来阻止这种行为。
其次,是一些访问内存段的必要属性条件:
- 内存段是一片内存区域,访问内存就要提供段基址,所以要有段基址属性。
- 为了限制程序访问内存的范围,还要对段大小进行约束,所以要有段界限属性
段描述符是8字节大小,图中为了方便展示,才“人为的”分成了低32位和高32位。其实他们不能分成两部分,必须是连续的8字节,这样CPU才能读取到正确的段信息
- 段基址:保护模式下地址总线宽度是32位,段基址需要用32位地址来表示
- 段界限:20位,段界限表示段边界的扩展最值。内存访问需要用到“段基址:段内偏移地址”,段界限其实是用来限制段内偏移地址的
实际的段界限边界值=(描述符中段界限+1)*(段界限的颗粒大小:4KB或1)-1
- TYEP字段:4位,用来指定本描述符的类型。段描述符在CPU眼里分为两大类,要么描述的是系统段,要么描述的是数据段,这是由段描述符中S位决定的,用它指示是否是系统段。
在CPU眼里,凡是硬件运行需要用到的东西都可以称之为系统,凡是软件(操作系统也属于软件)需要的东西都称为数据,我们主要关注非系统段
-A位表示accessed位,这是由CPU来设置的,每当该段被CPU访问后,CPU就将此位置1。
-C表示一致性代码
-R表示可读,R为1表示可读,R为0表示不可读
-X表示该段是否可执行。X为1表示可执行,0表示不可执行
-E用来标识段的扩展方向,E为0表示向上扩展,即地址越来越高,通常用于代码段和数据段。E为1表示向下扩展,地址越来越低,通常用于栈段。
-W指是否可写,W为1表示可写,0表示不可写
- S字段:S为1表示数据段,S为0表示系统段
- DPL字段:描述符特权级,这两位能表示4种特权级,分别是0、1、2、3、4级特权。数字越小权力越大
- P字段:段是否存在于内存中,存在P为1,不存在P位0。P字段是由CPU来检查的。
- AVL字段:可用的,是针对用户来说的,也就是操作系统可以随意用此位。
- L字段:用来设置是否64位代码段。L为1表示64位代码段,L为0表示32位代码段
- D/B字段:用来指示有效地址(段内偏移地址)及操作数的大小。
对于代码段来说,此位是D位,若D为0,表示指令中的有效地址和操作数都是16位,指令有效地址用IP寄存器。若D为0,表示指令中的有效地址及操作数为32位,指令有效地址用EIP寄存器
对于栈段来说,此位是B位,用来指定操作数大小。若B为0,使用SP寄存器,也就是栈的起始地址是16位寄存器的最大寻址范围,0xFFFF。若B为1,使用的是ESP寄存器,也就是栈的起始地址是32寄存器的最大寻址范围,0xFFFFFFFF。 - G字段:粒度,它与段界限一起来决定段的大小。若G为0,表示段界限的单位是1字节,这样的段最大是2的20次方*1字节,即1MB。若G为1,表示段界限的单位是4KB,这样段最大是2的20次方*4KB,即4GB。
2.全局描述符表GDT
- 段描述符存放在全局描述符表GDT中,全局描述符表位于内存中,需要专门的寄存器指向它,这个寄存器便是GDTR,专门用来存储GDT的内存地址及大小,GDTR是个48位寄存器。对此寄存器的访问用lgdt指令。
- lgdt指令格式是:lgdt48位内存数据
- 这48位内存数据划分为两部分,其中前16位是GDT以字节为单位的界限值,其范围是2的16次方等于65536个字节,每个描述符的大小是8字节,故GDT最多可容纳的描述符数量是65536/8=8192个,即GDT中可容纳8192个段或门。后32位是GDT起始地址
3.段选择子
- 段寄存器CS、DS、ES、FS、GS、SS,在实模式下时,段中存储的是段基址,即内存段的起始地址。而在保护模式下时,由于段基址已经存入段描述符中,所以段寄存器中再存放段基址是没有意义的,在段寄存器中存放的是一个叫做段选择子的东西——selector。
- 段选择子:第0~1位用来存储RPL。第2位是TI位,用来指示选择子是在GDT中,还是在LDT中索引描述符。第3~15位是描述符的索引值,用此值在GDT中索引描述符,由于选择子的索引部分是13位,即2的13次方等于8192,故最多可索引8192个段,这和GDT中最多定义8192个描述符吻合。
- 注意:GDT中的第0个段描述符是不可用的,若选择到了GDT中的第0个描述符,处理器将发出异常
4.局部描述符表LDT
- 局部描述符表LDT,它是CPU厂商为硬件一级原生支持多任务而创造的表,按照CPU的设想,一个任务对应一个LDT。其实现代操作系统中很少有用LDT。
- CPU厂商建议每个任务的私有内存段都应该放到自己的段描述符表中,该表就是LDT。
四. 打开A20地址线
实模式下内存访问是采用“段基址:段内偏移地址”的形式,如果段基址和段内偏移地址都为16位的最大值,即0xFFFF:0XFFFF,最大地址是0xFFFF0+0xFFFF=0X10FFEF。由于实模式下的地址线是20位,最大寻址空间1MB,即0x00000~0xFFFFF。超出1MB的部分在物理内存没有与之对应的部分,为了让“段基址:段内偏移地址”策略继续可用,CPU采取的做法是将超过1MB的部分自动回绕到0地址
- 如果A20Gate被打开,当访问到0x100000~0x10FFEF之间的地址时,CPU将真正访问这块物理内存。
- 如果A20Gate被禁止,当访问x100000~0x10FFEF之间的地址时,CPU将采用8086/8088的地址回绕
打开A20Gate的方式,将端口0x92第一位置1
in al,0x92
or al,0000_0010B
out 0x92,al
五. 保护模式的开关,CR0寄存器的PE位
-
PE为0表示在实模式下运行,PE为1表示在保护模式下运行,我们需要把此位置1:
mov eax,cr0 or eax,0x00000001 mov cr0,eax
六.让我们进入保护模式
;mbr.s
;主引导程序
;------------------------------------------------------------
%include "boot.inc"
SECTION MBR vstart=0x7c00
mov ax,0
mov ds,ax
mov es,ax
mov ss,ax
mov fs,ax ;将ax,dx,es,ss,fs初始化为0
mov sp,0x7c00
mov ax,0xb800
mov gs,ax
; 清屏 利用0x06号功能,上卷全部行,则可清屏。
; -----------------------------------------------------------
;INT 0x10 功能号:0x06 功能描述:上卷窗口
;------------------------------------------------------
;输入:
;AH 功能号= 0x06
;AL = 上卷的行数(如果为0,表示全部)
;BH = 上卷行属性
;(CL,CH) = 窗口左上角的(X,Y)位置
;(DL,DH) = 窗口右下角的(X,Y)位置
;无返回值:
mov ax, 0x600
mov bx, 0x700
mov cx, 0 ; 左上角: (0, 0)
mov dx, 0x184f ; 右下角: (80,25),
; VGA文本模式中,一行只能容纳80个字符,共25行。
; 下标从0开始,所以0x18=24,0x4f=79
int 0x10 ; int 0x10
;输出背景色绿色,前景色红色,并且跳动的字符串”1 MBR“
mov byte [gs:0x00],'1'
mov byte [gs:0x01],0xA4
mov byte [gs:0x02],' '
mov byte [gs:0x03],0xA4
mov byte [gs:0x04],'M'
mov byte [gs:0x05],0xA4
mov byte [gs:0x06],'B'
mov byte [gs:0x07],0xA4
mov byte [gs:0x08],'R'
mov byte [gs:0x09],0xA4
mov eax, LOADER_START_SECTOR ;loader加载到的扇区
mov bx , LOADER_BASE_ADDR ;lodaer写入的地址
mov cx , 4 ;待读入的扇区数
call rd_disk_m_16
jmp LOADER_BASE_ADDR
;-----------------------------------------------
;功能:读取硬盘n个扇区
rd_disk_m_16:
;-----------------------------------------------
mov esi,eax ;备份eax
mov di,cx ;备份cx
;读取硬盘:
;第1步:设置要读取的扇区数
mov dx,0x1f2
mov al,cl
out dx,al ;读取的扇区数
mov eax,esi ;恢复ax
;第2步:将LBA地址存入0x1f3~0x1f6
;LBA地址7~0位写入0x1f3
mov dx,0x1f3
out dx,al
;LBA地址15~8位写入0x1f4
mov cl,8
shr eax,cl
mov dx,0x1f4
out dx,al
;LBA地址23~16位写入0x1f5
;mov cl,8
shr eax,cl
mov dx,0x1f5
out dx,al
;LBA地址24~27位写入端口0x1f6低4位,高四位设置主盘和LBA模式
shr eax,cl
;and al,0x0f
or al,0xe0
mov dx,0x1f6
out dx,al
;第3步:向0x1f7端口写入读扇区命令,0x20
mov dx,0x1f7
mov al,0x20
out dx,al
;第4步:检查硬盘状态
.not_ready:
;0X17F同一端口,写时表示写入命令字,读时表示读入硬盘状态
nop
;mov dx,0x1f7
in al,dx
and al,0x88 ;和0x88进行与操作,保留第3位和第7位
cmp al,0x08 ;0x08表示硬盘准备就绪
jnz .not_ready
;第5步:从0x1f0端口读取数据
mov ax,di
mov dx,256
mul dx
mov cx,ax
;di为读取的扇区数,一个扇区512个字节,每次读入的两个字节共需 di*512/2次,所以是di*256
mov dx,0x1f0
.go_on_read:
in ax,dx
mov [bx],ax
add bx,2
loop .go_on_read
ret
times 510-($-$$) db 0
db 0x55,0xaa
;boot.inc
;-------------------loader和kernel--------------------
LOADER_BASE_ADDR equ 0x900 ;LOADER加载地址
LOADER_START_SECTOR equ 0x2 ;LOADER开始的扇区
;------------------- gdt描述符属性 ---------------------
DESC_G_4K equ 1_00000000000000000000000b
DESC_D_32 equ 1_0000000000000000000000b
DESC_L equ 0_000000000000000000000b
DESC_AVL equ 0_00000000000000000000b
DESC_LIMIT_CODE2 equ 1111_0000000000000000b
DESC_LIMIT_DATA2 equ DESC_LIMIT_CODE2
DESC_LIMIT_VIDEO2 equ 0000_0000000000000000b
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
;代码段可执行的,非一致性,不可读,已访问位a清0
DESC_TYPE_DATA equ 0010_00000000b
;数据段不可执行的,向上扩展,可写,已访问位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
;loader.s
%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR
jmp near 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的大小
GDT_LIMIT equ GDT_SIZE - 1 ;GDT的界限
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
loader_start:
mov byte [gs:0xA0],'L'
mov byte [gs:0xA1],0xA4
mov byte [gs:0xA2],'O'
mov byte [gs:0xA3],0xA4
mov byte [gs:0xA4],'A'
mov byte [gs:0xA5],0xA4
mov byte [gs:0xA6],'D'
mov byte [gs:0xA7],0xA4
mov byte [gs:0xA8],'E'
mov byte [gs:0xA9],0xA4
mov byte [gs:0xAA],'R'
mov byte [gs:0xAB],0xA4
;---------------------------------------- 准备进入保护模式 ------------------------------------------
;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跳转,
; 这将导致之前做的预测失效,从而起到了刷新的作用。
[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:0x140], 'P'
jmp $
1 .使用远跳转指令清空流水线,更新段描述符缓冲器
- 段描述符缓冲器未更新,它的值还是实模式下的值,进入保护模式后需要填入正确的信息。
- 段描述符缓冲寄存器在CPU的实模式和保护模式中都同时使用,在不重新引用一个段时,段描述符缓冲器中的内容是不会更新的,无论是实模式还是保护模式下,CPU都以段描述符缓冲寄存器中的内容为主。实模式进入保护模式时,由于段描述符缓冲寄存器中的内容仅仅是实模式下的20位的段基址,很多属性位都是错误的值,这对保护模式来说必然是错误,所以需要马上更新段描述符缓冲寄存器,也就是要想办法往相应段寄存器中加载选择子。
七.保护模式之内存段的保护
1.向段寄存器加载选择子时的保护
- 当引用一个内存段时,实际上就是往段寄存器中加载一个选择子,为了避免出现非法引用内存段的情况,在这时候,处理器会在以下几方面做检查。
- 首先根据选择子的值验证段描述符是否超越界限。
描述符表基地址+选择子中的索引值*8+7 <= 描述符表基地址+描述符表界限值
检查过程如下:处理器先检查TI的值,如果TI是0,则从全局描述符表寄存器GDTR中拿到GDT基地址和GDT界限值。如果TI是1,则从局部描述符表寄存器LDTR中拿到LDT基地址和LDT界限值。有了描述符表基地址和描述符表界限值后,把选择子的高13位代入上面的表达式,若不成立,处理器则抛出异常。
在选择子检查后,就要检查段的类型了(段描述符中type字段)
- 只有具备可执行属性的段(代码段)才能加载到 CS 段寄存器中。
- 只具备执行属性的段(代码段)不允许加载到除 CS 外的段寄存器中。
- 只有具备可写属性的段(数据段)才能加载到 SS 栈段寄存器中。
- 至少具备可读属性的段才能加载到 DS、ES、FS、GS 段寄存器中。
- 在检查完type后,还会检查段是否存在(段描述符中的P位)
CPU 通过段描述符中的 P 位来确认内存段是否存在,如果P 位为 1,则表示存在
这时候就可以将选择子载入段寄存器了,同时段描述符缓冲寄存器也会更新为选择子对应的段描述符的内容,随后处理器将段描述符中的 A 位置为1,表示已经访问过了。如果 P 位为 0,则表示该内存段不存在,不存在的原因可能是由于内存不足,操作系统将该段移出内存转储到硬盘上了。这时候处理器会抛出异常,自动转去执行相应的异常处理程序,异常处理程序将段从硬盘加载到内存后并将 P 位置为 1,随后返回。CPU 继续执行刚才的操作,判断 P 位。
2. 代码段和数据段的保护
对于代码段和数据段来说,CPU每访问一个地址,都要确认该地址不能超过其所在内存段的范围。
- 实际段界限的值为:(描述符中段界限+1)*(段界限的粒度大小:4k或者1)-1
- 对于G位为1的4K粒度大小的段公式为:(描述符中段界限+1)4k-1 = 描述符中段界限4k-4k-1 = 描述符中段界限 *0x1000+0xFFF
- 实际的段界限大小,是段内最后一个可访问的有效地址。由于有了段界限的限制,我们给CPU提交的每一个内存地址,无论是指令地址,还是数据地址,CPU都要帮我们检查地址的有效性。首先地址指向的数据是有宽度的,CPU要保证该数据一定要落在段内,不能“骑”在段边界上。
- 对于数据段和代码段的访问,都要用“段基址:段内偏移地址”的形式
- 对于代码段要满足的条件:EIP中的偏移地址+指令长度-1 <= 实际段界限大小。如果不满足条件,指令未完整的落在本段内,CPU则会抛出异常。
- 对于数据段要满足的条件:偏移地址+数据长度-1 <= 实际段界限大小。如果不满足条件,数据未完整的落在本段内,CPU则会抛出异常。
例如:
假设数据段描述符的段界限是 0x12345,段基址为 0x00000000。
如果 G 位为 0,那么实际段界限便是 0x12345。如果 G 位为 1,那么实际段界限便是 0x12345* 0x1000+0xFFF=0x12345FFF。如果访问的数据地址是 0x12345FFF,还要看访问的数据宽度。若数据大小是 1 字节,如 mov ax,byte [0x12345fff],这种内存操作一点问题都没有,数据完全在实际段界限之内。若该数据大小是 2 字节,如 mov ax,word [0x12345fff],这种内存操作超过了实际的段界限,数据所在地址分别是 0x12345FFF 和 0x12346000 这两个字节,CPU 会抛异常。
3. 栈段的保护
-
虽然段描述符type中的e位用来表示段的扩展方向,但它和别的描述符属性一样,仅仅是用来描述段的性质,即使e=1向下扩展,依然可以引用不断向上递增的内存地址,即使e=0向上扩展,也依然可以引用不断向下递增的内存地址。栈顶指针esp的值逐渐降低,这是push指令的作用,与描述符是否向下扩展无关,也就是说,是数据段就可以用作栈。
-
CPU对数据的检查,其中一项就是看地址是否超越界限。段的扩展方向决定了CPU对数据的检查方式:
- 对于向上扩展的段,实际的段界限是段内可以访问的最后一个字节(上一节数据段的方式来检查,偏移地址+数据段长度-1 <= 实际段界限大小)
- 对于向下扩展的段,即栈段。段界限本质上就是段的范围大小,范围没有负数只说。栈的段界限是以栈段的基址为准的,并不是栈底,因此栈的段界限肯定是位于栈顶之下。为了避免碰撞,将段界限地址+1视为栈可以访问的下限。段界限+1,才是栈指针可达的下边界。
- 32位保护模式下栈的栈顶指针是esp,栈的操作数大小是由B位决定的,假设B=1,表示32位操作数,所以栈指针最大可访问的地址是0xFFFFFFFF
- 每次向栈段中压入数据时就是CPU检查栈段的时机,它必须满足:
实际段界限+1 <= esp-操作数大小 <= 0xFFFFFFFF