前言:
兜兜转转,经历了前几天的磨练,我们拳打显卡,脚踢硬盘,手搓MBR,终于要从野蛮的实模式进入先进发达的保护模式了。今天我们将深入学习保护模式,GDT,以及让我们的操作系统进入保护模式,废话不多说现在开始吧!
参考资料:
《操作系统真象还原》
一,什么是保护模式?
在回答这个问题之前,我们先来看看远方的实模式吧家人们。
实模式缺点:
(1)可用内存只有1MB,对于现代操作系统来说肯定是不够用的。
(2)采用段基址+段偏移地址访问,可以随意访问任何内存并且篡改,十分不安全,设想一下你下载的一个软件,可以随意访问你的任何内存并篡改,这还得了。
(3)用户程序和系统程序平起平坐,应该要有阶级之分。
(4)访问超过64KB就要切换段基址,很累。
看看这些问题确实让人头疼呢,其他的还好,最重要的是内存只有1MB,这也太离谱了,要知道现在开机加载个操作系统都有几百MB内存要占用,但是没关系,保护模式帮我们一一解决了这些
①,保护模式的产生
实际上保护模式是在16位CPU80286上面诞生的,但是由于其还是16位CPU,最终还是被淘汰了。划时代的32位CPU80386完美的接过了他的接力棒,开启了真正的保护模式时代。
当CPU发展到32位时,可访问的内存范围达到了 4GB,运行模式和以前也发生了变化,并且随着硬件发展,高等级的硬件都要向下兼容,于是CPU便有了两种运行模式:实模式和保护模式
实模式:32位CPU16位下运行
保护模式:32位CPU32位下运行
ps:实模式和保护模式是只在32位CPU中才存在的,以前在8086的16位CPU是无实模式这一说的
32位CPU16位: 对于CPU来说32位的CPU可以变为16位模式是完全没问题的,你在16位模式下使用32位立即数都是能被允许的。
在你开机的时候都是由16位实模式转换为32位保护模式的
②,保护模式4大变化
-
寄存器扩展
1,容量扩展:除段寄存器外,通用寄存器、指令指针寄存器、标志寄存器都由原来的 16 位扩展到了 32 位,也就是说通常的寄存器都可以直接访问4G的内存,再也不用像实模式那样不断地变换段基址了。
2,名称变化:扩展后的寄存器在其前面加上了一个e代表extend,32位寄存器的低16位可以单独使用,高16位是无法单独使用的。
3,段寄存器变化: 段寄存器不在保存段基址了,而是保存选择子(在GDT中的索引,之后会介绍)。
4,缓存技术:更新了一个新的寄存器段描述符缓存寄存器,保护模式下负责缓存段描述符,实模式下负责缓存段基址*4的结果
-
寻址扩展
寻址模式与寄存器改变:
在实模式下有三种寻址方式:基址寻址,变址寻址,基址变址寻址。其基址寄存器只能是bx,bp,变址寄存器只能是si,di。
而在保护模式下,因为已经是32位寄存器,无需再段基址*4来访问内存,更重要的是基址寄存器可以是任何32位通用寄存器,变址寄存器可以是除32位以外的所有通用寄存器,变址寄存器还可以乘以一个比例因子
-
运行模式反转
编译器模式指明:
在CPU中,32位模式下和16位模式下有着完全不同的机器码格式,所以对于编译器来说将代码编译成16位机器码还是32位机器码就需要人为来指出,编译器提供了一个伪指令bits
bits 的指令格式 bits 16]或[bits 32]。 [bits 16]是告诉编译器,下面的代码帮我编译成 16 位的机器码 [bits 32]是告诉编译器,下面的代码帮我编译成 32 位的机器码。
bits指令范围是从当前bits的标签直到下一个bits的指令标签
反转前缀:
🌟!实模式和保护模式可以互相使用对方的资源,例如16位实模式可以使用32位寄存器!🌟
但是要如此访问的话就必须要使用一个前缀,来使当前模式转换为另一个模式,即模式转换
- 操作数(0x66):
前缀为0x66,16位实模式使用,操作数变为32位。32位同理
- 寻址方式反转(0x67):
-
指令扩展
双操作指令:add,sub可以支持8,16,32
单操作指令:inc,dec支持8,16,32
loop指令:实模式cx,保护模式ecx
mul指令:
push:
- 立即数
对于CPU来说,操作数有一个对其的功能,将8位立即数对其到目前模式的位数再入栈。 #实模式下: 当压入8 位立即数时,由于实模式下默认操作数是 16 位, CPU 会将其扩展为 16 位后再将其入栈, sp-2 当压入 16 位立即数时, CPU 会将其直接入栈, sp-2 当压入 32 位立即数时, CPU 会将其直接入栈, sp-4 #保护模式下: 当压入 8 位立即数时,由于保护模式下默认操作数是 32 位, CPU 将其扩展为 32 位后入栈, esp-4 当压入 16 位立即数时, CPU 直接压入 2 宇节, esp-2 当压入 32 位立即数时, CPU 直接压入 4 字节, esp-4
- 寄存器
#段寄存器: 无论那种模式下,都是按当前模式的默认操作数大小压入 16位:2字节 sp-2 32位:4字节 esp-4 #通用寄存器 无论那个模式下 压入16位栈指针-2 压入32位栈指针-4
- 内存
与通用寄存器同理👆
二,全局描述符
全局描述符表( Global Descriptor Table, GDT )是保护模式下内存段的登记表,这是不同于实模式的 显著特征之一。
①,段描述符
段描述符的结构中,顾名思义,该结构专门用来描述一个内存段,该结构是 8 字节大小
接下来我们来一一介绍段描述符里面的各个位的意义
- 段基址(32位):描述一个段的位置
- 段界限(20位):段的界限可能向上扩展或者向下扩展
公式:(段界限+1)*段粒度-1
- S(1位):0代表系统段(硬件用到的都是系统段),1代表数据段(软件用到的都是数据段)
- TYPE(4位):描述TYPE的类型,其中也根据系统段和数据段来区分
我们先关注非系统段
A:accessed,该段被CPU访问过后置为1,未被访问或者新建置为0
C:Conforming,一致性代码段,1为一致性代码,0为非一致性代码段
R:可读段,1可读,0不可读
X:可执行段,1可执行,0不可执行
- DPL(2位):代表段特权,将计算机权力分为不同的等级
这两位能表示 4 种特权级,分别是0 1 2 3 级特权,数字越小,特权级越大。特权级是保护模式 下才有的东西, CPU 由实模式进入保护模式后,特权级自动为 0。因为保护模式 的代码已经是操作系统 的一部分啦,所以操作系统应该处于最高的0 特权级。用户程序通常处于3 特权级,权限最小。某些指令 只能在 特权级下执行,从而保证了安全。
- P(1位):Present,是否存在于内存,存在为1,不存在为0,P字段由CPU检查,如果P字段为0,就需要抛给异常程序去处理,这个异常程序由我们来编写。
通常情况下,段都在内存中,只有少数情况内存不够用,段在磁盘中,需要进行置换。但是如果是平坦模式下4GB是无法置换的,因为大家都用你这4GB内存,而且4GB置换到磁盘中十分麻烦。置换只在开启分页功能时按页(4KB)置换是最好的。
- AVL(1位):avaliable,可用的,对于用户态来说的。
- L(1位):1代表是否设为64位代码段,我们写32位的所以先不考虑这些
- D/B字段(1位):指定操作数和有效地址的大小
对于代码段来说他是D:0是16位,1是32位
对于栈段来说他是B:0是sp寄存器16位,1是esp寄存器32位
- G(1位):粒度Granularity
0代表段界限的单位是1字节 最大界限是 2^20*1 = 1MB
1代表段界限的单位是4KB 最大界限是 2^20*4KB=4GB
三,GDT,LDT,选择子
讲完表里面的元素,我们就应该来讲表了。
① GDT:全局描述符表
寄存器:GDTR(48位)
存储指令:lgdt 48位内存数据
存储段描述符数量:2^16/64=8192个段
GDT起始地址从1开始,0为空描述符
简单来说,段描述符就是一个人的身份证,人们可以通过你的身份证得知你的信息,而GDT就是一个装满身份证的钱包,人们从钱包可以得到各种身份证。
② 选择子
在实模式中,采用的是段基址模式去寻址,而保护模式下采用的是选择子。
选择子位数:16位
0~1位:存储RPL即特权级0,1,2,3 2位:0是GDT,1是LDT 3~15位:代表索引值,刚好是2^13 = 8192
寻址方式变为:段描述符中的段基址+段内偏移地址(注意无需*4了)
例如:选择子为0x8将选择子加载到ds中,GDT第一个描述符的段基址是0x1234,ds:0x9访问内存段
从选择子可得 RPL为0,访问GDT,且索引为1,于是访问的内存段为 0x1234+0x9=0x123d
③ LDT
支持多任务而创造的表,但是用的比较少,就不重点介绍了,
他的索引是从0 开始
寄存器为ldtr
加载指令为 lldt 16 位寄存器116 位内存
四,进军保护模式
有了以上的了解,我们就要准备进军保护模式了,但是先别急,还不准备写代码,再次之前我们总要知道如何进入保护模式吧,给你一分钟的时间思考如何进入保护模式。哈哈想不出来吧,因为我还有东西没写的😋,那就是打开地址线和设置CR0寄存器。
① 打开A20地址线
至于为什么要打开A20地址线,其实实际上就是做地址回绕,你说你不知道什么是地址回绕?我再Day 0 博客有说哦,快去看看吧。
有的人看完回来就可能会思考,地址回绕是针对20位CPU的呀,我们现在都32位了,如果还是使用实模式,该咋回绕啊。
对咯,所以32位CPU下的A20地址线的作用就是:
A20Gate被打开,访问0x10000~0x10FFFF时,CPU将真正访问这块物理内存
A20Gate被关闭时,访问0x10000~0x10FFFF时,将进行地址回绕
in al,0x92
or al,00000_0010B
out 0x92,al
② 保护模式开关,CR0寄存器的PE位
CRX寄存器即控制寄存器是CPU的窗口,展示CPU的内部状态控制CPU的运行机制
这次我们将打开控制寄存器的PE位也就是第0位,打开这个就可以启用保护模式,芜湖完事具备只欠写代码了,准备冻手吧!!
mov eax, cr0
or eax, 0x0000000l
mov cr0, eax
③ 准备冻手写代码
我们先给boot.inc写一些全局变量(已修复)
LOADER_BASE_ADDR equ 0x900
LOADER_START_SECTOR equ 0x2
; GDT 描述符属性
DESC_G_4K equ 00000000_10000000_00000000_00000000b ; 段界限粒度为 4KB
DESC_D_32 equ 00000000_01000000_00000000_00000000b ; 指令中的有效地址及操作数是 32 位,指令有效地址用 EIP 寄存器
DESC_L equ 00000000_00000000_00000000_00000000b ; 32 位代码段
DESC_AVL equ 00000000_00000000_00000000_00000000b ; AVL 位无用途,置为 0
DESC_LIMIT_CODE2 equ 00000000_00001111_00000000_00000000b ; 段界限 19 ~ 16 位
DESC_LIMIT_DATA2 equ DESC_LIMIT_CODE2
DESC_LIMIT_VIDEO2 equ 00000000_00000000_00000000_00001011b ; 段界限 19 ~ 16 位
DESC_P equ 00000000_00000000_10000000_00000000b ; 段存在于内存中
DESC_DPL_0 equ 00000000_00000000_00000000_00000000b ; 特权级 0
DESC_DPL_1 equ 00000000_00000000_00100000_00000000b ; 特权级 1
DESC_DPL_2 equ 00000000_00000000_01000000_00000000b ; 特权级 2
DESC_DPL_3 equ 00000000_00000000_01100000_00000000b ; 特权级 3
DESC_S_CODE equ 00000000_00000000_00010000_00000000b ; 数据段
DESC_S_DATA equ DESC_S_CODE ; 数据段
DESC_S_SYS equ 00000000_00000000_00000000_00000000b ; 系统段
DESC_TYPE_CODE equ 00000000_00000000_00001000_00000000b ; 可执行,非一致性,不可读,已访问位清 0
DESC_TYPE_DATA equ 00000000_00000000_00000010_00000000b ; 不可执行,向上扩展,可写,已访问位清 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 + 0x00
DESC_TYPE_CODE equ 1000_00000000b ;code sector x=1,c=0,r=0,a=0
DESC_TYPE_DATA equ 0010_00000000b ;data sector x=0,c=0,r=1,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 + 0x00
;选择子属性
RPL0 equ 00b
RPL1 equ 01b
RPL2 equ 10b
RPL3 equ 11b
TI_GDT equ 000b
TI_LDT equ 100b
;loader和kernel
PAGE_DIR_TABLE_POS equ 0x100000
PG_P equ 1b
PG_RW_R equ 00b
PG_RW_W equ 10b
PG_US_S equ 000b
PG_US_U equ 100b
;kernel
KERNEL_START_SECTOR equ 0x6
KERNEL_BIN_BASE_ADDR equ 0x70000
KERNEL_ENTRY_POINT equ 0xc0001500
PT_NULL equ 0
再在loader.S中加入GDT载入代码和进入保护模式代码
;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],'G'
mov byte [gs:0xA1],0xA4
mov byte [gs:0xA2],'e'
mov byte [gs:0xA3],0xA4
mov byte [gs:0xA4],'n'
mov byte [gs:0xA5],0xA4
mov byte [gs:0xA6],'i'
mov byte [gs:0xA7],0xA4
mov byte [gs:0xA8],'u'
mov byte [gs:0xA9],0xA4
mov byte [gs:0xAA],'s'
mov byte [gs:0xAB],0xA4
mov byte [gs:0xA8],'O'
mov byte [gs:0xA9],0xA4
mov byte [gs:0xAA],'S'
mov byte [gs:0xAB],0xA4
;准备进入保护模式
;----------------- 打开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;刷新流水线, 更新段描述符缓存寄存器
[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 $
由于loader.S的字节已经超过512KB,所以在mbr中mov cx,1要改为mov cx,4
mov cx, 4
call rd_disk_m_16
jmp LOADER_BASE_ADDR
然后在我们将loader.bin加载到img中的时候 count=1,要改为count=4,否则会加载不完全
#!/bin/bash
#rm -rf /usr/geniux/img/geniusos.img
nasm -I /usr/geniux/include/ -o mbr.bin mbr.S
dd if=mbr.bin of=/usr/os/bochs/share/bochs/geniusos.img bs=512 count=1 conv=notrunc
echo "disk write success!!"
nasm -I /usr/geniux/include/ -o loader.bin loader.S
dd if=loader.bin of=/usr/os/bochs/share/bochs/geniusos.img bs=512 count=2 seek=2 conv=notrunc
;注 loader的载入 count要改为 2,因为loader的大小已经超过512字节,所以需要变为复制两个扇区
成功运行截图
🌈 !congratulations! 🌈
现在可以说,我们已经慢慢探索到操作系统深处的入口了,明天会有什么更加有意思的东西等着我们呢,敬请期待吧!!
bug修复
在4day的时候,我们出现了跳转loader失败的现象,再次我来讲一下如何修复
- bug 出现的原因:
软盘不规则,导致不支持in out指令,使得磁盘读取失败,在其他虚拟机不存在该问题
- 解决方法:
- 采用CHS方式读取硬盘,这是我上一个操作系统采用的读取方式,在此不细讲了,有兴趣的可以根据博客所说,自己实现一下
- 用bximage 生成一个规则的image
在你的bochs文件夹下使用 bximage 命令
bximage
然后选择【1】回车,一直往下回车
输入60 (都🆗,我自己是60)回车
自己给自己的硬盘取个好听的名字,我叫geniusos.img
取完后就会在你当前bochs的文件夹下生成一个规则的img,然后就通过这个img去写入你的loader和mbr吧,记得改你bash脚本还有配置文件的img路径,改为当前这个img的路径,好的快去试试吧!