本篇简单介绍了保护模式和实模式的异同、全局描述符表 GDT ,以及如何进入保护模式。
进入保护模式的三个步骤:
- 打开A20地址线
- 初始化全局描述符表 GDT(lgdt指令)
- 将控制寄存器CR0的PE位置1开启CPU的保护模式
本篇难点:
- 全局描述符表的使用原理
初见保护模式
保护模式的概念
保护模式首次出现在80286 CPU上;在保护模式下,物理内存地址不能被直接访问,需要经过地址转换之后才能进行访问,地址转换是处理器和操作系统共同完成的,处理器硬件上提供转换部件,操作系统提供转化过程中所需要的页表。
实模式和保护模式是CPU的概念,实模式的CPU运行环境是16位,保护模式是32位,实模式指的是32位CPU在16位模式下运行的状态。
段寄存器的变化
在保护模式下,除了段寄存器,其他寄存器都被扩展成了32位,保护模式提供的安全性很大一部分体现在了内存段的描述方面;
偏移地址和实模式一样,但段基址会有很大不同,为了给不同的内存段添加不同的属性,专门找了个数据结构--全局描述符表来描述,每一个表项称为段描述符,64字节,用来描述各个内存段的起始地址、大小、权限等信息;
全局描述符表很大,所以放在内存中,用专门的寄存器---GDTR寄存器指向这个数据结构;
段寄存器里保存的就不再是段基址了,里面保存的是段选择子,selector,用来在全局描述符表中索引段描述符;
由于访问内存对CPU来说很慢,所以这里应用了缓存技术,将段信息用一个寄存器来缓存(段描述符缓冲寄存器),CPU每次获取到整理好的段描述符信息,都会存在这里,直到下一次获取新的段描述符信息
因为CPU是兼容实模式,所以在实模式下,段描述符缓冲寄存器里存放的是段基址左移4位的值
地址访问
IA-32体系的CPU访问内存的方式还是分段策略,段基址:段内相对偏移地址,从80386CPU开始,段基址和段内偏移地址都是32位的了,仅用段内偏移即可访问完整4GB内存了
在实模式与保护模式运行的中间,有一个过度模式--虚拟8086模式;
寄存器的变化
在实模式下,寄存器都有固有使命,比如基址寄存器bx,bp,变址寄存器si,di
到保护模式了之后,寄存器的功能就不是那么固定了,所有通用寄存器都可以当基址寄存器,除了esp以外都可以做变址寄存器。
全局描述符表 GDT
到了保护模式下,不再是简单的用段寄存器加载一下段基址就能用了,段的信息增加了很多,需要提前定义好段才能用,全局描述符表 GDT 就是存储段信息的数据结构。
全局描述符表 GDT 的表项,段描述符,就是用来描述段的属性的,该结构是8字节大小。段描述符格式如下图所示:
段基址 与 段界限 和 G位
保护模式总线宽度32位,所以段基址也是要用32位表示;
段界限表示段边界的扩展最值,分为向上扩展(段界限表示最大值,例如数据段,代码段)和向下扩展(段界限表示最小值,例如栈段)两种,段界限是个单位量,要么是1字节(G=0)要么是4KB(G=1),取决于描述符中的G位;
段界限边界值 = (描述符中的段界限值 + 1) * (段界限颗粒度大小) - 1
段界限是用来限制段偏移地址的(如果不限制,则仅用偏移地址即可访问全部内存)
如果访问的地址超过了段界限的最值,则CPU会抛出异常,程序员负责写相应异常的处理程序。
至于段描述符为啥把段基址,段界限分散成这么多段来存放,是历史原因,当时为了兼容80286这个半成品而导致的。
字段 S
段描述符在CPU眼里分为两大类,系统段和数据段
- 凡是硬件运行需要的东西都是系统,代码段属于系统段
- 凡是软件运行需要的东西都是数据,代码段属于非系统段(数据段)
S位决定段是否属于系统段,S=0则表示系统段,S=1则是数据段
S位要和Type字段配合在一起才能知道确定的段描述符的类型,只有S位确定了,Type字段才有意义
各种称为门的结构都是系统段,任务门,调用门等,门便是入口。
字段 Type
Type字段共4位,目前主要关注非系统段,表中的4位值分别含义是:
-
A位,Accessed位,由CPU设置,每当该段被CPU访问后,CPU就将此置1;
在创建新的段描述符的时候,应该将此置0,在调试是能判断该描述符是否可用。
-
C位,Conforming位,一致性代码段,也叫依从代码段;
如果自己是转移的目标段,并且自己是一致性代码段,自己的特权级一定要高于当前特权级,转移后的特权级与转移前的特权级一致,也就是依从转移前的特权级
C=1表示是一致性代码段,C=0表示不是
-
R位,表示可读,R=1可读,R=0不可读
不可读代码段是用来限制代码指令的,如果读了不可读的内存,CPU会抛出异常
-
X位,表示可执行,X=1可执行(代码段),X=0不可执行(内存段)
-
E位,Extend,表示扩展方向,0为向上扩展,地址越来越高(代码段,数据段),1为向下扩展,地址越来越低(栈段)
-
W位,Writeable,表示写入,1可写入,0不可写入,写入不可写入的数据段CPU会抛出异常
字段 DPL
Descriptor Privilege Level,描述符特权级
因为DPL字段占2位,所以分为0,1,2,3一共四种特权级,CPU从实模式进入保护模式后,特权级自动变为0,保护模式下的代码是操作系统的一部分,操作系统应该处于最高特权级,而用户程序处于3特权级
特权级具体指的是描述符所指的内存段的特权级别
字段 P
Present,表示段是否存在,如果段存在于内存,P=1,否则P=0
P字段是CPU来检查的,如果为0,CPU会抛出异常,CPU只负责检查,不负责处理
设计之初是用于当内存不够用的时候,将内存不常用的段放到硬盘里,有了分页功能后,就用分页功能来完成这件事了
字段 AVL
Available,可用的,操作系统可以随意使用,对硬件来说没有专门的用途
字段 L
用来设置是不是64位代码段,1表示是,0表示否
字段 D/B
用来指示有效地址及操作数的大小,这是兼容80286CPU的产物,与指令有关的段是代码段和栈段:
- 对代码段来说,此位是D位,D为0表示16位,D为1表示32位
- 对栈段来说,此位是B位,B为0表示16位,B为1表示32位
全局描述符表 GDT、局部描述符表 LDT 及选择子
全局描述符表 GDT
全局描述符表就像是一个数组,而其中的段描述符就是数组中的元素,通过段选择子进行索引,可以获取指定的段描述符
全局描述符表的全局,指的是多个程序都可以在里面定义自己的段描述符,是公用的
全局描述符表位于内存中,通过专门的寄存器 GDTR 指向他的位置
使用之前需要对该寄存器进行初始化,指令为:lgdt 48位内存数据
这个指令在进入保护模式前后都可以执行,进入保护模式需要有GDT,在保护模式中还可以换个GDT加载
GDTR寄存器分为两部分,前16位是GDT的界限,后32位是GDT的起始地址,由于GDT的大小是16位二进制,界限最大值是2^16=65536,而每个描述符是8字节大小,则最多可存下65536/8=8192个段或门。
选择子 Selector
原本在实模式下的段寄存器里存储的是段基址,在保护模式下,段寄存器里存入的内容变成了选择子:
段寄存器是16位,选择子也是16位:
第0-1位存入的是RPL,Request Privilege Level,请求特权级,请求者的特权级,可以表示为 0,1,2,3,4 四种特权级
第2位TI位,Table Indicator,用来指示选择子在GDT(TI=0)中还是在LDT(TI=1)中索引描述符。
剩下的13位是索引值,最大位2^13=8192个
选择子的作用
,是确定段描述符,确定段的基地址,特权级,界限等
在保护模式下,IA-32 CPU依然是通过段基址:段内偏移地址的方式进行寻址,不过计算方式有所变化,因为段基址和段内偏移地址都是32位了,所以实际地址直接将选择子确定段描述符后,CPU自动从段描述符中取出段基址
,直接加上偏移地址
GDT中第0个段描述符不可用,主要是为了避免选择子未初始化就使用的情况,如果用了则CPU会抛出异常
局部描述符表 LDT
LDT是一个系统段,也是一个局部描述符表,是全局描述符表的一个表项,是个嵌套的描述符表,用于程序内部进行段属性的设置
描述符种类很多,但描述符高32位中的第8~12位始终是不变的,12位必须是S,8-11位必须是type,其他位没有强制要求,所以很多系统段如中断门,陷阱门都是在中断描述符表中的。
打开 A20 地址线
实模式下存在一个A20地址回绕,超过1M的内容会被忽略,只有打开A20地址线,才能访问1M以外的内存空间:
向端口 0x92 的第1个位置写入1即可
in al, 0x92
or al, 0000_0010B
out 0x92, al
保护模式的开关,CR0 寄存器的 PE 位
这是进入保护模式的最后一步(第三步)
程序员不可见寄存器--控制寄存器CR0-CR3,可用来展示或者控制CPU的运行状态,其中的CR0寄存器中的第0位,便是保护模式的开关,将此位置一即可开启CPU的保护模式:
mov eax, cr0
or eax, 0x00000001
mov cr0, eax
进入保护模式
接下来通过实践来真正进入保护模式~
由于保护模式是在loader.bin中进入的,而loader.bin将会超过512字节,所以需要先对之前的程序进行一个小的修改
mbr.S
首先是 mbr.S(完整代码在上一篇笔记中有):
mov eax ,LOADER_START_SECTOR ;起始扇区lba地址
mov bx ,LOADER_BASE_ADDR ;写入的地址
mov cx ,4 ;待读入的扇区数
call rd_disk_m_16 ;以下读取程序的起始部分
这里将cx,也就是待读入的扇区数由1改到4
boot.inc
下一个要更新的文件是 boot.inc :
;---------------loader and kernel---------------
LOADER_BASE_ADDR equ 0x900
LOADER_START_SECTOR equ 0x2
;---------------GDT 描述符属性------------------
DESC_G_4K equ 1_00000000000000000000000b ;颗粒度:4K
DESC_D_32 equ 1_0000000000000000000000b ;操作数和地址大小:32位
DESC_L equ 0_000000000000000000000b ;是否是64位代码段:否
DESC_AVL equ 0_00000000000000000000b ;不用此位,暂设置为:0
DESC_LIMIT_CODE2 equ 1111_0000000000000000b ;段界限19-16位
DESC_LIMIT_DATA2 equ DESC_LIMIT_CODE2 ;段界限19-16位
DESC_LIMIT_VIDEO2 equ 0000_000000000000000b ;?????
DESC_P equ 1_000000000000000b ;表示段存在
DESC_DPL_0 equ 00_0000000000000b ;特权级:0
DESC_DPL_1 equ 01_0000000000000b ;特权级:1
DESC_DPL_2 equ 10_0000000000000b ;特权级:2
DESC_DPL_3 equ 11_0000000000000b ;特权级:3
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 ;Type字段-代码段:x=1,c=0,r=0,a=0
DESC_TYPE_DATA equ 0010_00000000b ;Type字段-数据段:x=0,e=0,w=1,a=0
DESC_CODE_HIGH4 equ (0x00<<24) + DESC_G_4G + 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_4G + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_DATA2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x00
DESC_VIDEO2_HIGH4 equ (0x00<<24) + DESC_G_4G + 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
注意!!!
这里书上有个问题,会导致后续程序运行出错,也算是个坑吧
在boot.inc的代码中,其中的显卡段的高4字节部分:
DESC_VIDEO2_HIGH4 equ (0x00<<24) + DESC_G_4G + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_VIDEO2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x00
显存的起始地址是0xb8000,在段描述符低4字节中段基址0-15位存储的是0x8000,所以段描述符高4字节最初8位是段基址的23-16位的值应该是0xB,而不是0x00,所以这一行应该改成:
DESC_VIDEO2_HIGH4 equ (0x00<<24) + DESC_G_4G + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_VIDEO2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x0B
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 = 7
dd DESC_VIDEO_HIGH4;此时dpl为0
GDT_SIZE equ $ - GDT_BASE ;获取 GDT 大小
GDT_LIMIT equ GDT_SIZE - 1 ;获取 段界限
times 60 dq 0 ;预留60个空位,为以后填入中断描述符表和任务状态段TSS描述符留空间
;times 60 表示后面的内容循环60次,是nasm提供的伪指令
SELECTOR_CODE equ (0x0001 << 3) + 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.'
;---------------------------------------------------------
;INT 0x10 功能号:0x13 功能描述符:打印字符串
;---------------------------------------------------------
;输入:
;AH 子功能号=13H
;BH = 页码
;BL = 属性(若AL=00H或01H)
;CX = 字符串长度
;(DH,DL)=坐标(行,列)
;ES:BP=字符串地址
;AL=显示输出方式
;0——字符串中只含显示字符,其显示属性在BL中。显示后,光标位置不变
;1——字符串中只含显示字符,其显示属性在BL中。显示后,光标位置改变
;2——字符串中只含显示字符和显示属性。显示后,光标位置不变。
;3——字符串中只含显示字符和显示属性。显示后,光标位置改变。
;无返回值
loader_start:
;显示字符串,表示当前在实模式
mov sp, LOADER_BASE_ADDR
mov bp, loadermsg ;ES:BP 字符串地址
mov cx, 17 ;字符串长度
mov ax, 0x1301 ;AH=13h,AL=01h
mov bx, 0x001f ;页号为0(BH=0h),蓝底粉红字(BL=1fh)
mov dx, 0x1800 ;
int 0x10 ;int 10 BIOS中断
;准备进入保护模式
;1.打开A20地址线
in al, 0x92
or al, 00000010B
out 0x92, al
;2.加载GDT
lgdt [gdt_ptr]
;3.将CR0的PE位置1
mov eax, cr0
or eax, 0x00000001
mov cr0, eax
jmp dword SELECTOR_CODE:p_mode_start ;刷新流水线
;流水线是CPU 的工作方式,会把当前指令和后面的几个指令同时放在流水线中重叠执行,由于之前的代码是16位,接下来的代码变成32位了,指令按照16位进行译码会出错,通过刷新流
水线可以解决这个问题
[bits 32] ;编译成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 $
编译,写入到硬盘:
nasm -I include/ -o mbr.bin mbr.S
nasm -I include/ -o loader.bin loader.S
dd if=boot/mbr.bin of=hd60M.img bs=512 count=1 conv=notrunc
dd if=boot/loader.bin of=hd60M.img bs=512 count=4 seek=2 conv=notrunc
运行Bochs
远跳转指令jmp
从实模式进入保护模式有两个问题需要解决:
-
一是段描述符缓冲寄存器的值需要改成32位的值,因为在保护模式下,段描述符缓冲寄存器的值还是16位的话,如果使用了这个寄存器就会出现问题,可以通过call,jmp,ret等指令来间接实现
-
二是需要将CPU流水线清空,这就只能使用jmp来实现了
所以用jmp一举两得。
保护模式下内存段的保护
加载选择子时候的保护:判断选择子的索引是否在范围内:
描述符表基地址 + 选择子中的索引值 * 8 + 7 <= 描述符表基地址 + 描述符表界限值
代码段和数据段的保护:检查地址边界
EIP 中的偏移地址 + 指令长度 - 1 <= 实际段界限大小
偏移地址 + 数据长度 - 1 <= 实际段界限大小
机器码不能跨段,否则CPU会抛出异常
栈段的保护:栈的地址范围
实际段界限+1<=esp-操作数大小<=0xFFFFFFFF
参考资料
-
操作系统真象还原第四章笔记:https://blog.csdn.net/zhwenx3/article/details/108969801
-
《操作系统 真象还原》