操作系统真相还原 第四章 保护模式入门
第四章 保护模式入门
实模式
什么是实模式
实模式,又叫实地址模式.从80386开始,CPU有三种工作模式:实模式、保护模式和虚拟8086模式。80286开始的CPU引入保护模式,实际上,实模式概念是在保护模式推出之后为了区别保护模式之前的8086CPU工作模式才有的,在8086时代CPU工作模式只有一种,自然没有实模式之说。
实模式的“实”体现在程序中用到的地址都是真实的物理地址,“段基址:段内偏移地址”产生的逻辑地址就是物理地址,即程序员可见的地址完全是真实的内存地址。
实模式的缺点
不安全
- 没有权限划分,操作系统程序和用户程序属于同一特权级.
- 程序中用到的地址都是真实的物理地址,逻辑地址等于物理地址.
- 程序可以自由修改段地址,也就是可以访问所有内存地址.
- 段大小受限,段内偏移最大为64K,如果程序超过64k,需要占用多个段,段寄存器需要切换段基址.
保护模式之寄存器扩展
原有寄存器宽度增加
除段寄存器外,通用寄存器、指令指针寄存器、标志寄存器都由原来的 16 位扩展到了 32 位。
经过 extend 后的寄存器,统一在名字前加了 E表示扩展.
寄存器中低 16 位的部分是为了兼容实模式,可以单独使用。高 16 位没办法单独使用,只能在用 32位寄存器时才有机会用到它们。
添加新的寄存器
除了容量的扩展,还有新的寄存器添加;
添加新寄存器的目的:解决实模式的安全问题
安全问题之一:段没有级别划分,程序可以随意访问任意段,任意内存地址;
解决段随意访问问题->段添加描述信息,即段描述符->段描述符有多个,要描述多个段,全局描述符表包括多个段描述符->全局描述符表很大,不能直接放在寄存器,只能放在内存中->为了能找到全局描述符表,把全局描述符表的地址放在GDTR寄存器->段寄存器中不再保存段基址,保存"选择子"(selector),是段描述符在全局描述符表中的索引->获取段描述符的步骤:1.读取GDTR寄存器获取全局描述符表的地址;2.读取段寄存器获取选择子;3.全局描述符表地址+选择子,从内存中读取段描述符;->获取段描述符耗时长,而且段信息在执行同一个程序时,一般不会变动,所以可以在获取段描述符后,把段描述符放到寄存器中做缓存,此寄存器是段描述符缓存寄存器->在段寄存器的选择子更新时,就会重新获取段描述符,更新段描述符缓存寄存器;
保护模式之寻址扩展
实模式下寄存器的功能比较固定.实模式下对于内存寻址来说,其中的基址寻址、变址寻址、基址变址寻址,这三种形式中的基址寄存器只能是bx,bp,变址寄存器只能si、di,也就是说,只能用这个寄存器。其中bx默认的段寄存器是ds,它经常用于访问数据段,bp默认的段寄存器是ss,它经常用于访问栈。
mov ax, [si]
mov ax, [di]
mov ax, [bx]
mov ax, [bx+si]
mov ax, [bx+si+Oxl234]
mov ax, [bx+di]
mov ax, [bx+di+Oxl234]
在保护模式下,这一切都不同了,同样是内存寻址中,基址寄存器不再只是bx,bp,而是所有32位的通用寄存器,变址寄存器也是一样,不再只是si,di,而是除esp之外的所有32通用寄存器,偏移量由实模式的16位变成了32位。并且,还可以对变址寄存器乘以一个比例因子
mov eax, [eax+edx*8+0x12345678]
mov eax, [eax+edx*2+0x8]
mov eax, [ecx*4+0x1234]
保护模式之运行反转
兼容实模式和保护模式,cpu很难区分,需要生成不同的机器码来做处理;编译器提供伪指令bits,来区分16位和32位.
[bits 16]
mov ax, Oxl234
mov dx, Oxl234
[bits 32]
mov eax , Oxl234
mov edx, Oxl234
全局描述符表
全局描述符表( Global Descriptor Table, GDT )是保护模式下内存段的登记表,这是不同于实模式的显著特征之一。
一个段描述符描述一个段的信息,一个专门的数据结构保存着多个段描述符,称为“全局描述符表”,其实就是一个保存着段描述符的数组。
段描述符
-
段描述符大小为8个字节
-
段界限被分为两部分,段基址被分为三部分,原因是为了兼容80286,80286是第一款有保护模式的cpu,但是只有16位。
-
段界限:表示段的大小,共20位。具体边界值要结合23位的G来看,G=1时,表示段界限的粒度为4KB,最大值为2的(20+12)次方,即4GB,G=0时,表示段界限的粒度为1字节,最大值为2的20次方,即1MB实际的段界限=(描述符里的段界限+1)*段界限粒度大小-1。
-
段基址:共32位。
-
S:代表一个段是系统段还是数据段,在CPU眼里,凡是硬件使用到的东西称为系统,凡是软件使用到的东西称为数据。所以代码段、数据段、栈段等也属于S中所代表的的数据段。S为0时表示系统段,S为1时表示数据段。
-
Type:共四位,指定段的类型。只有S决定了,Type才有它的意义。下图是Type在系统段和数据段里不同的意义。
- 非系统段:当段为代码段时,Type由X、R、C、A组成,分别代表是否可执行、是否可读、是否一致、是否被访问过。当段位数据段时,Type由X、W、E、A组成,分别代表是否可执行、是否可写、扩展方向、是否被访问过。
- DPL代表段的特权级。
- P代表内存段是否存在,0代表段不存在,1代表段存在。
- AVL代表可用的位,是对用户而言,但操作系统可以随意使用。
- L代表代码段是64位还是32位。
- D/B:有效地址和操作数的大小。对于代码段来说此位是D,为0时,有效地址和操作数的大小为16位,使用ip寄存器;为1时,有效地址和操作数的大小为32位,使用eip寄存器。对于栈段来说此位是B,为0时,操作数为16位,使用sp寄存器;为1时,操作数为32位,使用esp寄存器。
全局描述符表
全局描述符表示全局的,共用的,多个程序都可以在这个表定义自己的段描述符。我们进入保护模式的其中一个步骤之一就是加载全局描述符表,让CPU知道全局描述符表的位置,在操作内存的时候,CPU就会根据描述符的信息检查这操作是否有效。
全局描述符表位于内存中,由GDTR寄存器指向全局描述符表的内存地址。GDTR寄存器大小为48位,初始化GDTR的指令为lgdt 48位数值
。
这48位数值的后32位表示GDT的起始地址,前16位表示GDT的大小,单位为字节,即2的16次方,大小为65536字节;每个描述符大小为8字节,所以GDT最多容纳65536/8=8192个段。
选择子
段寄存器中存入的是选择子(selector),段寄存器是16位,所以选择子也是16位。
- RPL:特权级,有0、1、2、3,四种。
- TI:为0,描述符索引值为GDT中索引值;为1,描述符索引值为LDT中索引值。
保护模式下,使用选择子获取段描述符,从段描述符中取出段基址。
LDT:局部描述符表,很少使用。
进入保护模式
A20地址线
在实模式下,A20地址线是默认禁用的,原因是还未进入保护模式之前,地址总线还是要模拟20位的效果,即只保留20位以内的地址,如果地址超过20位,地址就会回绕到0,将地址20位(从0开始算)舍弃,所以要将A20地址线给禁用掉。但进入保护模式后,我们需要恢复地址总线的原貌,即使地址超过20位,地址也不应该回绕到0,所以此时将A20地址线打开,我们就能访问超过20位的地址了。因此,打开A20地址线,是进入保护模式的步骤之一。
CR0的PE位
进入保护模式的最后一个步骤是,打开CR0的PE位,CR0是控制寄存器。控制寄存器是CPU的窗口,它既可以展示CPU的内部状态,也可以控制CPU的运行机制。CR0的第0位,PE位,就是保护模式的开关,我们打开PE位,就是告诉CPU接下来我们要进入保护模式。
进入保护模式
由上面可以知道,进入保护模式的步骤如下:
① 打开A20地址线
② 加载GDT
③ 将CR0的PE位置为1
总共三个文件:include/boot.inc,mbr.S,loader.S
boot.inc
增加段描述符的常量
;------------- 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
mbr.S
此前只读入了一个扇区,由于 loader.bin 超过了 512 字节,52行增加读入的扇区为4
;主引导程序
;------------------------------------------------------------
%include "boot.inc"
SECTION MBR vstart=0x7c00
mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov fs,ax
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, 0600h
mov bx, 0700h
mov cx, 0 ; 左上角: (0, 0)
mov dx, 184fh ; 右下角: (80,25),
; 因为VGA文本模式中,一行只能容纳80个字符,共25行。
; 下标从0开始,所以0x18=24,0x4f=79
int 10h ; int 10h
; 输出字符串: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 ;A表示绿色背景闪烁,4表示前景色为红色
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 ; 起始扇区lba地址
mov bx,LOADER_BASE_ADDR ; 写入的地址
mov cx,4 ; 待读入的扇区数
call rd_disk_m_16 ; 以下读取程序的起始部分(一个扇区)
jmp LOADER_BASE_ADDR
;-------------------------------------------------------------------------------
;功能:读取硬盘n个扇区
rd_disk_m_16:
;-------------------------------------------------------------------------------
; eax=LBA扇区号
; ebx=将数据写入的内存地址
; ecx=读入的扇区数
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
shr eax,cl
mov dx,0x1f5
out dx,al
shr eax,cl
and al,0x0f ;lba第24~27位
or al,0xe0 ; 设置7~4位为1110,表示lba模式
mov dx,0x1f6
out dx,al
;第3步:向0x1f7端口写入读命令,0x20
mov dx,0x1f7
mov al,0x20
out dx,al
;第4步:检测硬盘状态
.not_ready:
;同一端口,写时表示写入命令字,读时表示读入硬盘状态
nop
in al,dx
and al,0x88 ;第4位为1表示硬盘控制器已准备好数据传输,第7位为1表示硬盘忙
cmp al,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
loader.S
%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 $