进入保护模式的三个步骤:
保护模式首次出现在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以外都可以做变址寄存器。
到了保护模式下,内存段(如数据段、代码段等)不再是简单地用段寄存器加载一下段基址就能用啦,段的信息增加了很多,需要提前把段定义好才能使用。就像家庭成员需要上户口一样,在户口簿上登记过才算合法。
全局描述符表(Global Descriptor Table,GDT)是保护模式下内存段的登记表,这是不同于实模式的显著特征之一。
全局描述符表 GDT 的表项,段描述符,就是用来描述段的属性的,该结构是8字节大小。段描述符格式如下图所示:
段描述符是8字节大小,在图我们为了方便展示,才将其“人为地”分成了低32位和高32位,即两个4字节部分。其实它们不能分成两部分,必须是连续的8字节,这样CPU才能读取到正确的段信息。
保护模式总线宽度32位,所以段基址也是要用32位表示;
段界限表示段边界的扩展最值,分为向上扩展(段界限表示最大值,例如数据段,代码段)和向下扩展(段界限表示最小值,例如栈段)两种,段界限是个单位量,要么是1字节(G=0)要么是4KB(G=1),取决于描述符中的G位;
段界限边界值 = (描述符中的段界限值 + 1) * (段界限颗粒度大小) - 1
段界限是用来限制段偏移地址的(如果不限制,则仅用偏移地址即可访问全部内存)
如果访问的地址超过了段界限的最值,则CPU会抛出异常,程序员负责写相应异常的处理程序。
至于段描述符为啥把段基址,段界限分散成这么多段来存放,是历史原因,当时为了兼容80286这个半成品而导致的。
段描述符在CPU眼里分为两大类,系统段和数据段
S位决定段是否属于系统段,S=0则表示系统段,S=1则是数据段
S位要和Type字段配合在一起才能知道确定的段描述符的类型,只有S位确定了,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会抛出异常
Descriptor Privilege Level,描述符特权级
因为DPL字段占2位,所以分为0,1,2,3一共四种特权级,CPU从实模式进入保护模式后,特权级自动变为0,保护模式下的代码是操作系统的一部分,操作系统应该处于最高特权级,而用户程序处于3特权级
特权级具体指的是描述符所指的内存段的特权级别
Present,表示段是否存在,如果段存在于内存,P=1,否则P=0
P字段是CPU来检查的,如果为0,CPU会抛出异常,CPU只负责检查,不负责处理
设计之初是用于当内存不够用的时候,将内存不常用的段放到硬盘里,有了分页功能后,就用分页功能来完成这件事了
Available,可用的,操作系统可以随意使用,对硬件来说没有专门的用途
用来设置是不是64位代码段,1表示是,0表示否
用来指示有效地址及操作数的大小,这是兼容80286CPU的产物,与指令有关的段是代码段和栈段:
全局描述符表就像是一个数组,而其中的段描述符就是数组中的元素,通过段选择子进行索引,可以获取指定的段描述符
全局描述符表的全局,指的是多个程序都可以在里面定义自己的段描述符,是公用的
全局描述符表位于内存中,通过专门的寄存器 GDTR
指向他的位置
使用之前需要对该寄存器进行初始化,指令为:lgdt 48位内存数据
这个指令在进入保护模式前后都可以执行,进入保护模式需要有GDT,在保护模式中还可以换个GDT加载
GDTR
寄存器分为两部分,前16位是GDT的界限,后32位是GDT的起始地址,由于GDT的大小是16位二进制,界限最大值是2^16=65536,而每个描述符是8字节大小,则最多可存下65536/8=8192个段或门。
原本在实模式下的段寄存器里存储的是段基址,在保护模式下,段寄存器里存入的内容变成了选择子:
段寄存器是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是一个系统段,也是一个局部描述符表,是全局描述符表的一个表项,是个嵌套的描述符表,用于程序内部进行段属性的设置
描述符种类很多,但描述符高32位中的第8~12位始终是不变的,12位必须是S,8-11位必须是type,其他位没有强制要求,所以很多系统段如中断门,陷阱门都是在中断描述符表中的。
实模式下存在一个A20地址回绕
,超过1M的内容会被忽略,只有打开A20地址线,才能访问1M以外的内存空间:
向端口 0x92 的第1个位置写入1即可
in al, 0x92
or al, 0000_0010B
out 0x92, al
这是进入保护模式的最后一步(第三步)
程序员不可见寄存器–控制寄存器CR0-CR3
,可用来展示或者控制CPU的运行状态
,其中的CR0寄存器中的第0位,便是保护模式的开关,将此位置一即可开启CPU的保护模式:
mov eax, cr0
or eax, 0x00000001
mov cr0, eax
接下来通过实践来真正进入保护模式~
由于保护模式是在loader.bin
中进入的,而loader.bin
将会超过512字节,所以需要先对之前的程序进行一个小的修改
首先是 mbr.asm
mov eax, LOADER_START_SECTOR ; 起始扇区LBA地址
mov bx, LOADER_BASE_ADDR ; 写入的地址
mov cx, 4 ; 待读入扇区数
call rd_disk_m_16 ; 调用函数读取硬盘
这里将cx
,也就是待读入的扇区数由1改到4
下一个要更新的文件是 boot.inc :
;------------- loader和kernel ----------
; loader 在内存中的位置
LOADER_BASE_ADDR equ 0x900
; loader 在硬盘上的逻辑扇区地址(LBA)
LOADER_START_SECTOR equ 0x2
;-------------- gdt描述符属性 -------------
; 查下划线的作用 其实没有任何作用 这里仅仅为了方便 确定哪些位为我们想要设置数而专门用的下划线分割
DESC_G_4K equ 1_00000000000000000000000b ; 第23位G 表示4K或者1MB位 段界限的单位值 此时为1则为4k
DESC_D_32 equ 1_0000000000000000000000b ; 第22位D/B位 表示地址值用32位EIP寄存器 操作数与指令码32位
DESC_L equ 0_000000000000000000000b ; 第21位 设置成0表示不设置成64位代码段 忽略
DESC_AVL equ 0_00000000000000000000b ; 第20位 是软件可用的 操作系统额外提供的 可不设置
DESC_LIMIT_CODE2 equ 1111_0000000000000000b ; 第16-19位 段界限的最后四位 全部初始化为1 因为最大段界限*粒度必须等于0xffffffff
DESC_LIMIT_DATA2 equ DESC_LIMIT_CODE2 ; 数据段与代码段段界限相同
DESC_LIMIT_VIDEO2 equ 0000_0000000000000000b ; 第16-19位 显存区描述符 VIDEO2 这里的全是0为高位 低位即可表示段基址
DESC_P equ 1_000000000000000b ; 第15位 P present判断段是否存在于内存
DESC_DPL_0 equ 00_0000000000000b ; 第13-14位 Privilege Level 0-3
DESC_DPL_1 equ 01_0000000000000b ; 0为操作系统 权力最高 3为用户段 用于保护
DESC_DPL_2 equ 10_0000000000000b
DESC_DPL_3 equ 11_0000000000000b
DESC_S_sys equ 0_000000000000b ; 第12位为0 则表示系统段 为1则表示数据段
DESC_S_CODE equ 1_000000000000b ; 第12位与type字段结合 判断是否为系统段还是数据段
DESC_S_DATA equ DESC_S_CODE
;x=1 e=0 w=0 a=0
DESC_TYPE_CODE equ 1000_00000000b ; 第9-11位表示该段状态 1000 可执行 不允许可读 已访问位0
;x=0 e=0 w=1 a=0
DESC_TYPE_DATA equ 0010_00000000b ; 第9-11位type段 0010 可写
; 代码段描述符高位4字节初始化 (0x00共8位 <<24 共32位初始化0), 0x00 代表16进制, 一个十六进制可表示4个二进制位, 0x00共8位 8+24=36位
; 4KB为单位 Data段32位操作数 初始化的部分段界限 最高权限操作系统代码段 P存在表示 状态
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
; 数据段描述符高位4字节初始化
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
; 显存段描述符高位4字节初始化
; 显存的起始地址是0xb8000, 在段描述符低4字节中段基址0-15位存储的是0x8000, 所以段描述符高4字节最初8位是段基址的23-16位的值应该是0xB
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
;-------------------- 选择子属性 --------------------------------
;第0-1位 RPL 特权级比较是否允许访问 第2位TI 0表示GDT 1表示LDT 第3-15位索引值
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 ; loader在保护模式下的栈指针地址,esp
jmp loader_start ; 下面存放数据段 构建gdt 跳跃到下面的代码区
; 构建GDT及其内部描述符, 每个描述符8个字节, 拆分为高低各4字节(32位)
GDT_BASE: dd 0x00000000 ; 第0个描述符,不可用
dd 0x00000000
CODE_DESC: dd 0x0000ffff ; 低32位31~16位为段基址15~0位, 15~0位为段界限15~0位
dd DESC_CODE_HIGH4
DATA_STACK_DESC: dd 0x0000ffff ; 数据段(栈段)描述符
dd DESC_DATA_HIGH4
VIDEO_DESC: dd 0x80000007 ; 0xB8000 到0xBFFFF为文字模式显示内存 段界限:limit=(0xbffff-0xb8000) / 4k=0x7
dd DESC_VIDEO_HIGH4 ; 0xB
GDT_SIZE: equ $ - GDT_BASE ; 当前位置减去GDT_BASE的地址 等于GDT的大小
GDT_LIMIT: equ GDT_SIZE - 1 ; SIZE - 1即为最大偏移量
times 60 dq 0 ; 预留60个 四字型 描述符空位, 用于后续扩展
SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0 ; 段选择子: 低3位为TI RPL状态, 其余为描述符索引值
SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0
;gdt指针, 前2字节为gdt界限, 后4字节为gdt起始地址(共48位)
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 字符串地址, 在mbr es设置位了0, 现在在平坦模式下cs=es=0
mov cx, 17 ; 字符串长度
mov ax, 0x1301 ; AH=13h,AL=01h
mov bx, 0x001f ; 页号为0(BH=0h),蓝底粉红字(BL=1fh)
mov dx, 0x1800 ; (DH,DL)=坐标(行,列) (24,0), 意思是最后一行 0列开始
int 0x10 ; int 10 BIOS中断
; --------------------------------- 设置进入保护模式 -----------------------------
; 1 打开A20 gate
; 2 加载gdt
; 3 将cr0 的 pe位(第0位)置1
; ----------------- 打开A20 ----------------
in al, 0x92 ; 端口号0x92 中的第1位变成 1 即可
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 ; 刷新流水线
[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 $
编译,写入到硬盘:
nasm -I include/ -o mbr.bin mbr.asm
nasm -I include/ -o loader.bin loader.asm
dd if=/home/steven/source/os/bochs/code/mbr.bin of=/home/steven/source/os/bochs/hd60M.img bs=512 count=1 conv=notrunc
dd if=/home/steven/source/os/bochs/code/loader.bin of=/home/steven/source/os/bochs/hd60M.img bs=512 count=4 seek=2 conv=notrunc
从实模式进入保护模式有两个问题需要解决:
所以用jmp一举两得。
加载选择子时候的保护:判断选择子的索引是否在范围内:
描述符表基地址 + 选择子中的索引值 * 8 + 7 <= 描述符表基地址 + 描述符表界限值
代码段和数据段的保护:检查地址边界
EIP 中的偏移地址 + 指令长度 - 1 <= 实际段界限大小
偏移地址 + 数据长度 - 1 <= 实际段界限大小
机器码不能跨段,否则CPU会抛出异常
栈段的保护:栈的地址范围
实际段界限+1<=esp-操作数大小<=0xFFFFFFFF
书上面写着 把4GB全部当作一个段, 难道之后每次我们出现一个新进程的时候我们会在GDT中每次都新放进去一个段描述符 然后里面的基址再重新填写吗?
书上后面写的 段基址所有设置成0
段界限全部设置成0xFFFFF
在以前我们在上操作系统理论课时可能认为分段分页基址 每个进程用不同的段 并且独立受到保护
在IA32
结构中是逃不掉分段内存访问的 但这里确实还是用的分段访问只不过用的是一个大段, 保护是通过检测段描述符的属性来相对应看是否能够加载或者读取访问来进行保护而不是从物理层面上每个进程都独自享有自己的段
所谓平坦模型,就是是分段成为一种“虚设”,表现为所有段都从同一个地址开始,因此,基于32bit的寄存器,最大地址空间为4G,把所有段的基地址设为 0,段的长度设为 0xFFFFF,段长度的粒度设为 4KB*,这样所有的段都指向同一个*(2^20*4KB=4GB)字节大小的地址空间,这就是平坦模型,*在这种情况下,CS:IP寄存器组指向的地址实际为IP寄存器指向的地址。
根本原因是为了向下兼容不支持分段机制的体系架构,因此在平坦模式下:
段基址+段偏移=线性地址 =》 0+段偏移=线性地址 =》偏移地址=线性地址
这样就直接实现了虚拟地址到线性地址的映射,寄存器中的虚拟地址和最终的线性地址就是同一地址。
段基址+段偏移 = 线性地址
线性地址 + MMU分页机制 = 物理地址
CPU工作模式:执行程序的三种模式