4.1 保护模式概述
实模式下,操作系统中的安全性和使用性不佳
1)系统程序和用户程序在同一级别
2)逻辑地址等于物理地址
3)用户程序可以自由修改段基址
4)访问超过64KB的内存区域时就要切换段基址
5)一次只能运行一个程序
6)共20条地址线,最大可用内存为1MB
目前,我们虚拟机上的实模式是指64位的CPU运行在16位模式下的状态
4.2 初见保护模式
4.2.1 保护模式之寄存器扩展
CPU发展到32位后,地址总线和数据总线也发展到32位,其寻址空间达到了2的32次方,4GB。同样的寄存器宽度也变成了32位。
通用寄存器扩展到32位,而下面的6个段寄存器仍然是16位。
偏移地址还和实模式下的一样,但段基址可不是简单的一个地址的事了。需要添加点约束条件,这些“约束条件”就是对内存段的描述信息。由于信息太多了,肯定用一个寄存器是放不下了,所以专门找了一个数据结构——全局描述表。既然叫表了,说明里面肯定有表项,其中每一个表项称为段描述符,其大小为64字节(16位),用来描述各个内存段的起始地址、大小、权限等信息。该全局描述符表很大,所以放在了内存,由GDTR寄存器指向它。
这样,段寄存器中保存的再也不是段基址,里面保存的内容叫“选择子”,选择子其实就是一个数,用这么数来索引全局描述符表中段描述符,把全局描述符表当做数组,选择子就像数组下标。
由于访问内存中的段描述符效率十分低,对段寄存器应用了缓存技术,将段信息用一个寄存器来缓存,这就是段描述符缓冲寄存器,CPU在获取内吨短信息后,会存入段描述符缓冲寄存器,类似快表。每次加载选择子时,CPU都会更新段描述符缓冲寄存器,此后对该段的访问才会从描述符缓冲寄存器中取。
一下列出三个段描述符缓冲寄存器结构
段基址是32位,单独寄存器也是32位,也就是说即使段基址是0,光用段内偏移就可以指向4GB空间任意角落。
4.2.2 保护模式之寻址扩展
在实模式下,寄存器有固定的使命,对于寻址来说,基址寄存器只能是bx,bp,变址寄存器只能使si、di。但在保护模式下,一切都不同了,基址寄存器是所有32位通用寄存器,变址寄存器也一样,是出esp之外的所有32位的通用寄存器。
4.2.3 保护模式之运行模式反转
之前我们提到,当前的CPU运行模式有实模式和保护模式两种,由于CPU的资源是互通的,那么问题来了,同样的一句汇编代码,他是属于实模式?还是保护模式?因此我们需要人为的告诉编译器一些信息,编译器才知道如何生成机器码。
编译器提供了伪指令bits,用它来相编译器传达,下面的指令都要编译成xx位的,这是因为实模式下的指令都是16位的,而保护模式下指令都是32位的。
bits的指令格式是[bits 16]或[bits 32]。范围是当前bits标签知道下一个bits标签。
反转前缀意在方便我们在当前模式下方便使用另一个模式的资源,操作数反转前缀0x66和寻址方式反转前缀0x67。
具体看书p144页。
总结,bits伪指令用于指定运行模式,而操作数反转前缀0x66和寻址方式反转前缀0x67是本节的重点。
4.2.4 保护模式之指令扩展
这里着重讲解push指令,根据其操作数的类型分别讨论
1) 立即数
对于8位立即数,对于CPU而言出于对齐得考虑,操作数要么是16位的要么是32位的,所以实模式下push 8位立即数和push 16位立即数,sp都-2,对于32位的则反转前缀66,sp-4.。而在保护模式下,8位和32位立即数都是sp-4,而16位立即数则反转前缀66,sp-2。
2) 寄存器
3) 内存
具体看书p147
4.3 全局描述符表
4.3.1 段描述符
保护模式下,内存访问依然是“段基址:段内偏移”的形式,但有效地提高了安全性。为了安全,所以为内存段添加了一些额外的安全属性,由于不可能放在寄存器,因为只有32位,所以放在内存中。由于内存十分大,干脆多添加一些信息。
首先,解决实模式下存在的问题:用户程序可以随意访问内存;区分用户程序和系统程序。其次是访问内存段的必要属性条件:段基址属性,段界限属性,所以构建了一个8字节大小的结构。
虽然图片人为的分为了两个结构,但实际上在内存中必须是连续的8个字节。保护模式下地址总线宽度是32位,段基址需要用32位地址来表示。段界限表示段边界的扩展最值,20位,值得注意的是段界限只是个单位量,它的单位要么是字节,要么是4kb,所以段的大小要么是1MB,要么是4GB。
内存访问需要用到“段基址:段内偏移地址”,段界限其实是用来限制段内偏移地址,段内偏移地址必须位于段界限边界值内,否则CPU会抛异常。段基址分三段是因为兼容问题。
主要属性在高32位中,S字段指示是否是系统段,在CPU严重,凡事硬件运行需要用的都可称之为系统,凡事软件需要的称之为数据。S为0时表示系统段,1时表示数据段。
type字段共四位,用来表示内存段或门的子类型,我们这里先学习费系统段。
x表示该段是否可执行,用来区分代码段和数据段,其他参数看书p153。
第13~14位是特权级,0、1、2、3,数字越小,特权级越大。第15位是P字段,若存在于内存,p为1否则p为0。
第21位为L字段,为1表示64位代码段,否则表示32位段码段。
第22位是D/B字段,用来指示有效地址和操作数大小,这是为了兼容80286的16位保护模式。对于代码段,此位是D位,若为0,表示指令的有效地址和操作数不是16位,指令有效地址用ip寄存器;否则32位,用eip寄存器。对于栈段,此位是B位,用来指定操作数大小,此操作数涉及到栈指针寄存器的选择和栈的地址上限。若为0,使用的是sp寄存器,最大寻址位0xFFFF;否则esp寄存器,0xFFFFFFF。
第23位G字段,若G位为0,表示段界限粒度大小为1字节否则为4KB。
4.2.3 全局描述符表GDT、局部描述符LDT及选择子
全局描述符表GDT是用来存放全局描述符的,相当于描述符的数组,数组中的每个元素都是8字节的描述符。可以用选择子中提供的下标在GDT中索引描述符。全局体现在多个程序都可以在里面定义自己的段描述符,是公用的。GDT位于内存中,需要使用专门的寄存器GDTR指向它后,CPU才知道它在哪里。GDTR是48位的寄存器,结构如下。
对于GDTR寄存器(48位),需要有专门的指令——lgdt指令。前16位是GDT以字节为单位的界限值,后32位是GDT的起始地址。由于GDT的大小是16位二进制,所以其表示的范围是2的16次方等于65536字节,每个描述符大小是8字节也就是65536/8 = 8192 个段或者门。
lgdt48位内存数据。
现在段描述符有了,描述符表有了,我们该如何使用呢?接下来将讲述段的选择子。在保护模式下,由于段基址已经存入段描述符中,所以段寄存器在存放段基址是没有意义的,在段寄存器存入的是一个叫做选择子的东西——selector。选择子“基本上”是一个索引值,其中除了下标还有一些属性。用选择子的索引值在GDT中索引相应的段描述符,这样就在段描述符中得到了内存段的起始地址和段界限值等相关信息。
由于段寄存器是16位的,所以选择子也是16位的,第0~1位用来存储RPL,即请求特权级,本章并不重要。第2位是TI位,用来指示选择子是在GDT中(为0),还是LDT中索引描述符;第3~15位是描述符的索引值,即描述符的下标,13位,2的13次方是8192,与上述的个数吻合。
段基址在段描述符中,用给出的选择子索引到描述符后,CPU自动从段描述符中取出段基址,这样再加上段内偏移地址,变凑成了“段基址:段内偏移地址”的形式。
例如选择子是0x8,将其加载到ds寄存器后,访问ds:0x9,其过程是0x8 0*12+1000,RPL为00,TI为0说明是GDT。高13位0x1在GDT索引,即下标为1,也就是GDT中的第一个段描述符(第0个段描述符不可用),假设其段基址位0x1234,所以0x1234+0x9 = 0x123d,用所得0x123d访问内存。
第0个段描述符不可用的原因是,若选择子忘记初始化,其值就为0,为了避免这种情况所以第0个段描述符不可用。
4.3.3 打开A20地址线
之前实模式下,我们并不担心0xFFFF0+0xFFFF,这是因为即使它超过了0xFFFFF,也会回到0x00000处继续开始,这被称为地址回绕。后续因为地址总线的扩展,当超过0xFFFFF后可以真正的访问后续的物理地址空间,所以,为了兼容设计了A20Gate。若A20Gate被打开,则后续的物理内存能被访问,否则,CPU将采取地址回绕。
打开A20Gate的方式其实很简单,将端口0x92的第1位置1就可以了,以下步骤就能完成。
in al,0x92
or al,0000_0010B
out 0x92,al
4.3.4 保护模式的开关,CR0寄存器的PE位
控制寄存器系列CRx。控制寄存器是CPU的窗口,即可以用来展示CPU的内部状态,也可用于控制CPU的运行机制。这次我们要用的就是CR0寄存器。更准确的说,我们要用到CR0寄存器的第0位,即PE位,次位用于启动保护模式,是保护模式的开关。
mov eax,cr0
or eax,0x00000001
mov cr0,eax
该进入代码部分了。
4.3.5 让我们进入保护模式
保护模式是在loader.bin中进入的,除了源程序loader.S要i更新外,还要更新相关的两个文件。
第一个是mbr.S,由于loader.bin超过了512字节,所以我们要把mbr.s中加载loader.bin的读入扇区数增大,目前是1扇区,直接改为4扇区。
mbr.S
;主引导程序
;---------
%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
mov eax,LOADER_START_SECTOR ;初始扇区lba地址,第2个扇区,即0x2
mov bx,LOADER_BASE_ADDR ;写入的地址 0x900
mov cx,4 ;待读入的扇区数
call rd_disk_m_16 ; 以下读取程序的起始部分(一个扇区)
jmp LOADER_BASE_ADDR ;来到loader部分0x900
;-------------------
;功能:读取硬盘n个扇区
rd_disk_m_16:
;-------------------
; eax=LBA扇区号
; ebx=将数据写入的内存地址
; ecx=读入的扇区数
mov esi,eax ;备份eax,LBA的扇区号
mov di,cx ;备份cx,读入的扇区数
;读写硬盘
;第一步:选择通道,往该通道的sector count寄存器写入待操作的扇区数
mov dx,0x1f2
mov al,cl ;在8086中,cx高位两个8位寄存器,高8位的ch和低8位的cl
out dx,al ;往sector count寄存器中打入1,代表待操作一个扇区
mov eax,esi ;回复ax
;第二步,往通道上的三个LBA寄存器写入扇区起始地址的低24位
;LBA地址的7~0位写入端口0x1f3
mov dx,0x1f3
out dx,al
;LBA地址15~8位写入端口0x1f4
mov cl,8
shr eax,cl ;逻辑右移8位
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模式 主盘,LBA模式
mov dx,0x1f6
out dx,al
;第3步:向0x1f7端口写入读命令,0x20 0x20读扇区命令
mov dx,0x1f7
mov al,0x20
out dx,al
;第4步:检测硬盘状态
.not_ready:
;同一端口,写时表示写入命令字,读时表示读入硬盘状态
nop ;相当于sleep一下
in al,dx ;这里dx不需要改变command和status是同一个寄存器
and al,0x88 ;将寄存器信息读取到al中
cmp al,0x08 ;与第四位相减,若第四位都等于1,则zf=0
jnz .not_ready ;若未准备好,继续等。如果zf!=0则跳转
;第5步:从0x1f0端口读数据
mov ax, di ;di当中存储的是要读取的扇区数
mov dx, 256 ;每个扇区512字节,一次读取两个字节因为data寄存器是16位,所以一个扇区就要读取256次,与扇区数相乘,就等得到总读取次数
mul dx
mov cx, ax ;得到了要读取的总次数,然后将这个数字放入cx中
mov dx, 0x1f0
.go_on_read:
in ax,dx
mov [bx],ax
add bx,2
loop .go_on_read ;loop循环时会将cx-1
ret
编译代码:nasm -I include/ -o mbr.bin mbr.S
写入磁盘:dd if=/home/moyao/Desktop/bochs/boot/mbr.bin of=/home/moyao/Desktop/bochs//hd60M.img bs=512 count=1 conv=notrunc
include/boot.inc
另一个要更新的文件时 include/boot.inc,里面是一些配置信息。
;------------- loader和kernel ----------
LOADER_BASE_ADDR equ 0x900
LOADER_START_SECTOR equ 0x2
;-------------- 模块化的gdt描述符字段宏-------------
DESC_G_4K equ 1_00000000000000000000000b ;设置段界限的单位为4KB
DESC_D_32 equ 1_0000000000000000000000b ;D/B字段,用来只是有效地址及操作数的大小,若为1则表示32位
DESC_L equ 0_000000000000000000000b ;段描述符的第21位,L字段,用来设置是否是64位代码段我们现在是在编写32位操作系统,此处标记为0便可。
DESC_AVL equ 0_00000000000000000000b ;此标志位是为了给操作系统或其他软件设计的一个自定义位,
;可以将这个位用于任何自定义的需求。
;比如,操作系统可以用这个位来标记这个段是否正在被使用,或者用于其他特定的需求。
;这取决于开发者如何使用这个位。但从硬件的角度来看,AVL位没有任何特定的功能或意义,它的使用完全由软件决定。
DESC_LIMIT_CODE2 equ 1111_0000000000000000b ;定义代码段要用的段描述符高32位中16~19段界限为全1
DESC_LIMIT_DATA2 equ DESC_LIMIT_CODE2 ;定义数据段要用的段描述符高32位中16~19段界限为全1
DESC_LIMIT_VIDEO2 equ 0000_000000000000000b ;定义我们要操作显存时对应的段描述符的高32位中16~19段界限为全0
DESC_P equ 1_000000000000000b ;定义了段描述符中的P标志位,表示该段描述符指向的段是否在内存中
DESC_DPL_0 equ 00_0000000000000b ;定义DPL为0的字段,特权级
DESC_DPL_1 equ 01_0000000000000b ;定义DPL为1的字段
DESC_DPL_2 equ 10_0000000000000b ;定义DPL为2的字段
DESC_DPL_3 equ 11_0000000000000b ;定义DPL为3的字段
DESC_S_CODE equ 1_000000000000b ;无论代码段,还是数据段,对于cpu来说都是非系统段,所以将S位置为1,见书p153图
DESC_S_DATA equ DESC_S_CODE ;无论代码段,还是数据段,对于cpu来说都是非系统段,所以将S位置为1,见书p153图
DESC_S_sys equ 0_000000000000b ;将段描述符的S位置为0,表示系统段,系统段就是与硬件相关的
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.
;定义代码段,数据段,显存段的高32位
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 ;定义选择字的RPL为0
RPL1 equ 01b ;定义选择子的RPL为1
RPL2 equ 10b ;定义选择字的RPL为2
RPL3 equ 11b ;定义选择子的RPL为3
TI_GDT equ 000b ;定义段选择子请求的段描述符是在GDT中
TI_LDT equ 100b ;定义段选择子请求的段描述符是在LDT中
loader.S
%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR
jmp loader_start ;loader一进来是一大堆GDT段描述符数据,无法执行,所以要跳过
GDT_BASE: ;构建gdt及其内部的描述符,GDT是段描述符的数组,所以8字节,即48位
dd 0x00000000
dd 0x00000000
CODE_DESC:
dd 0x0000FFFF
dd DESC_CODE_HIGH4 ;平坦模式,将段基址设置为0,段界限设置为最大值
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个描述符的空间
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_ptr dw GDT_LIMIT ;定义加载进入GDTR的数据,前2字节是gdt界限,后4字节是gdt起始地址,
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
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' ;一行80个字符,每个字符两个字节,所以160是第二行第一个
jmp $
nasm -I include/ -o loader.bin loader.S
dd if=/home/moyao/Desktop/bochs/boot/loader.bin of=/home/moyao/Desktop/bochs//hd60M.img bs=512 count=4 seek=2 conv=notrunc
4.4 处理器微架构简介
4.4.1 流水线
流水线,CPU是按照程序中的指令顺序来填充流水线的,所以当CPU执行jmp的时候,虽然下一条指令也被送上流水线译码,第三条指令已经被送上流水线取值了,但因为CPU已经跳转到别处去执行了,第二、三条指令就用不上了,所以CPU再遇到无条件跳转指令jmp是,会清空流水线。
4.4.2 乱序执行
代码中后续的操作可以放到前面来,利于装载到流水线上提高效率。
4.4.3 缓存
局部性原理
4.4.4 分支预测
p170页
4.5 使用远跳转指令清空流水线,更新段描述符缓冲寄存器
段描述符缓冲寄存器在CPU的实模式和保护模式中都同时使用,在不重新引用一个段时,段描述符缓冲寄存器中的内容是不会更新的。实模式下只使用20位,虽然原先的只使用16位,但兼容不代表32位的CPU变成16位的CPU,所以会直接使用20位,实模式进入保护模式时,由于段描述符缓冲寄存器中的内容仅仅是实模式下的20位段基址,很多属性位都是错误的值,所以需要马上更新段描述符寄存器。
其次流水线中指令译码错误。