操作系统是用来管理与协调硬件工作的,开发一款操作系统有利于理解底层的运转逻辑,本篇内容主要用来理解操作系统是如何启动的,又是如何加载磁盘中的内核的,该系列文章参考各类底层书籍,通过自己的理解并加以叙述,让内容变得更加简单,一目了然,即可学到知识又能提高自己的表述能力。
注释: 该系列笔记是在学习《操作系统真相还原》时通过阅读后简化并适当描述整理的学习笔记,首先,致敬作者郑刚博士,在读本书时能深刻的感觉到作者写书时一丝不苟的态度,书很厚写的,讲解细致幽默,很能让人愿意继续读下去,同时也不得不佩服作者计算机底层功力的深厚,转载本文请一并附带郑刚版权信息。
1. BOIS 是如何苏醒的
BIOS 基本输入输出系统,BIOS代码所做的工作是一成不变的,所以他是被固化到ROM中的一块只读区域中,在开机时此ROM会被映射到低端1MB内存的顶部,原因是系统在开启时默认是实地址模式(该模式最大寻址范围0-fffff),所以其寻址范围也就被限制在了0xF0000-x0xFFFFF
区域中,这64KB的内存就是BIOS的执行代码.
在开机的一瞬间,CPU的CS:IP
寄存器会被强制初始化为0xF000:0xFFF0
,在实地址模式下该地址需要乘以16也就是左移四位加上偏移地址得到,于是0xF000:0xFFF0
就等效于0xFFFF0
此处的地址距离0xFFFFF
只有16个字节的空间,里面存放着一条jmp far f000:e05b = fe05b
的汇编指令,该指令将跳转到真正的BIOS开始的位置.
接着BIOS将会通过自身的代码对硬件进行自检测,在初始化硬件后,则开始向内存0x000-0x3ff
中初始化数据结构以及拷贝中断向量表,紧接着BIOS将会通过调用int 19h
中断,此中断用以检测计算机中的硬盘,如果检测到0盘0道1扇区末尾的两个字节是0x55,0xaa
则认为此扇区确实存在,于是就会将此区域中的内容,加载到内存0x7c00
的位置,并通过一条jmp far 0:0x7c00h
的指令跳转到该位置执行,这样BIOS就将CPU控制权交给了MBR了,而BIOS将会再次睡去.
2. MBR 继续执行引导
如上提到过的0x7c00
就是MBR代码的开始位置,之所以是0x7C00
是因为,DOS中要求最小内存是32KB
而MBR大小必须是512字节,所以选择32kB
中的最后1KB的位置最为合适(32KB(0x8000)-1KB(0x400)=>0x7c00)
,这就是0x7C00
的由来,同时还需要保证第510-511
字节必须为0x55,0xaa
才可以,这就需要在末尾部分自动补齐两字节的填充.
简单的引导MBR的代码如下,首先我们需要先初始化每个段寄存器DS,ES,SS,FS,SP
然后通过调用两次int 0x10
中断对命令行进行置空操作,最后通过mov ax,01301h
也就是13号中断,打印出字符串.
SECTION MBR vstart=0x7c00 ; 告诉编译器加载到7c00内存处
mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov fs,ax
mov sp,0x7c00
mov ax,0x600 ; 清屏范围,也就是宽度
mov bx,0x0
mov cx,0x0 ; 清屏 左上角(0,0)
mov dx,0x184f ; 清屏 右下角(80=0x4f,25=0x18)
int 0x10
mov dh,0x0 ; 设置光标列号
mov dl,0x0 ; 设置光标行号
mov bh,0x0 ; 页码
int 0x10
mov ax,Message
mov bp,ax ; 保存字符串地址
mov cx,15 ; 保存字符串长度
mov ax,01301h ; 子功能号13是显示字符及属性
mov bx,000ch ; 页号位0,使用黑色为背景色,红色为字体颜色
mov dl,0
int 10h ; 调用10h号中断,用来显示字符
hlt
ret
Message: db "hello lyshark !"
times 510-($-$$) db 0 ; 填充剩余的510字节的空间为0
db 0x55,0xaa ; mbr的结束标志
我们直接将其保存为mbr.asm
文件,通过Nasm汇编器编译为二进制文件,然后再通过dd命令写入到一个镜像文件中,具体编译流程如下,这里需要下载好Windows版本的dd命令.
# 首先编译为二进制文件
nasm mbr.asm -o mbr.bin
# 将镜像写入到kernel.img镜像中,写入长度512字节,循环1次
dd if=mbr.bin of=kernel.img bs=512 count=1 conv=notrunc
# 通过seek跳过第一个扇区,然后向后填充4096字节的0
dd if=/dev/zero of=kernel.img seek=1 bs=512 count=4096
由于我们使用的模拟器是Bochs x86
所以,在制作好镜像后,需要在编写一个虚拟机配置文件,该配置文件命名为mbr.src
其内部需要定义好虚拟机的类型,启动方式,镜像位置等基本参数,一个简易版定义语句如下.
megs:32
romimage:file=./BIOS-bochs-latest
vgaromimage:file=./VGABIOS-lgpl-latest
boot:disk
mouse:enabled=0
ata0-master: type=disk, path="kernel.img", mode=flat, status=inserted
keyboard: keymap=./x11-pc-de.map
启动时可以直接调用bochsdbg -q -f mbr.src
命令,使用调试模式运行,并通过语句vb sp:0x7c00
在开头下断点,使用c
命令可运行到MBR代码处,单步n执行,即可输出一段话,标志着MBR已经成功被加载.
3. 让MBR直接驱动显卡
如上代码,我们通过调用BIOS提供的int 0x10
中断来实现打印字符操作,但我们在后期必须要借助显卡来输出图像,而显卡是外部设备,必须通过总线来操作。
由于CPU使用的信号是TTL电平,而外部设备都是机械设备,故他们不会使用该电平驱动,这就导致CPU与硬件设备没有办法实现沟通,硬件工程师们提供的方法是,在这两者之间架起一座桥,也就是在CPU和外设之间加上一层IO接口,该接口的作用就是实现CPU和外设之间相互做协调转换。
其次外部设备的种类也是多种多样的,其输出的信号可能是数字信号,也可能是模拟信号,而我们的CPU只能处理数字信号,数字信号需要经过数模转换器<D/A>
成模拟量才能送到外设来驱动硬件工作,模拟量也同样需要经过模数转换器<A/D>
转换成数字量才能被CPU直接处理,所以接口电路中需要包括A/D转换器和D/A转换器。
转换后的数字信号,会经过总线进行传递,总线的别名是BUS,之所以叫做BUS是因为其是公共线路,所有硬件设备都会走此线路,但同一时刻,CPU只能和一个IO接口(寄存器/端口)通信,当有多个IO接口同时想和CPU通信时,那么IO仲裁模块会对其进行竞争与选优,仲裁模块固化到,输入输出控制中心(ICH)也就是南桥芯片上的。
多数情况下,南桥和北桥是成对出现的,南桥主要负责连接PCI,PCI-Express,AGP
等低速设备,而北桥则用于链接高速设备,如内存等。
IO接口都是串行口,其在设计之初就是负责与CPU进行通信的,我们想要与CPU通信,其实是向这些接口中写入数据,同时为了区别CPU中的寄存器,所以把IO接口叫做端口,某些外设可以通过内存映射来访问,即把某些端口映射到指定内存中,访问某个内存区域就相当于访问了指定的端口。
由于显卡的起始地址为0xb800
向该地址写入数据即可回显在显示器上,如下代码是一个简单的填充过程。
SECTION MBR vstart=0x7c00 ; 告诉编译器加载到7c00内存处
mov ax,cs
mov sp,0x7c00
mov ax,0xb800
mov gs,ax
mov ax,0x600 ; 清屏范围,也就是宽度
mov bx,0x0
mov cx,0x0 ; 清屏 左上角(0,0)
mov dx,0x184f ; 清屏 右下角(80=0x4f,25=0x18)
int 0x10
mov dh,0x0 ; 设置光标列号
mov dl,0x0 ; 设置光标行号
mov bh,0x0 ; 页码
int 0x10
mov byte [gs:0x00],'L'
mov byte [gs:0x01],0xa4 ; 显示A=绿色闪烁 4=红色
mov byte [gs:0x02],'y'
mov byte [gs:0x03],0xa5
mov byte [gs:0x04],'S'
mov byte [gs:0x05],0xa6
mov byte [gs:0x6],'h'
mov byte [gs:0x7],0xa7
mov byte[gs:0x8],'a'
mov byte [gs:0x9],0xa6
mov byte[gs:0xa],'r'
mov byte [gs:0xb],0xa5
mov byte[gs:0xc],'k'
mov byte [gs:0xd],0xa4
hlt
ret
times 510-($-$$) db 0 ; 填充剩余的510字节的空间为0
db 0x55,0xaa ; mbr的结束标志
编译并运行这段代码,由于使用的是显卡输出,所以在输出色彩上,我们的选择余地更多了。
如上代码中需要注意,偶数行gs:0x04
代表的是输出数据,奇数行gs:0x05
则代表颜色背景色,如果需要实现循环输出,那么我们除需要考虑循环条件外,还应把基数偶数行也考虑进来。
SECTION MBR vstart=0x7c00 ; 告诉编译器加载到7c00内存处
; 清屏和设置光标位置
mov ax,0x600 ; 清屏范围,也就是宽度
mov bx,0x0
mov cx,0x0 ; 清屏 左上角(0,0)
mov dx,0x184f ; 清屏 右下角(80=0x4f,25=0x18)
int 0x10
mov dh,0x0 ; 设置光标列号 左上角(0,0)
mov dl,0x0 ; 设置光标行号 右下角(0,0)
mov bh,0x0 ; 页码
int 0x10
; 初始化,使SP寄存器指向段基址0X7C0处,GS指向显存基地址
mov ax,cs
mov sp,0x7c00
mov ax,0xb800
mov gs,ax ; 设置显存地址
; 设置字符串长度与字符串基地址
mov cx, msglen ; 获取字符串长度
mov si, message ; 设置字符串基址
xor di, di ; 每次清空di寄存器
loop_str:
mov al, [si] ; 每次取出一个字符
mov [gs:di], al ; 将字符逐一赋值到显存中
inc si
inc di
mov byte [gs:di], 000ch ; 设置字体颜色
inc di
loop loop_str
hlt ; 程序在此处终止
;message db "Loading MBR...",0ah,0dh
message db "Loading MBR..."
msglen equ $ - message
times 510-($-$$) db 0 ; 填充剩余的510字节的空间为0
db 0x55,0xaa ; mbr的结束标志
4. 让MBR直接驱动硬盘
既然显卡中存在端口可以被操作,那么硬盘也同样存在,硬盘控制器属于IO接口,如果想让硬盘工作,我们需要通过读写硬盘控制器上的端口,此处的端口指的就是硬盘控制器上的寄存器组。
- 硬盘控制器中的端口可被分为两种,最主要的是 Command Block registers 组中的寄存器
- Command Block registers 用于向硬盘驱动器写入命令字或者从硬盘控制器获得硬盘状态
- Control Block registers 用于控制硬盘工作状态
一般硬盘中的一个通道包括两片硬盘,其中0为主盘,1为从盘,硬盘控制器中的主要寄存器如下,其中主盘所对应的通道为Primary
,后面的那个Secondary
则是从盘通道,主从盘调用中断号完全不同:
-
DATA 寄存器主要负责管理数据,相当于数据的门,作用是读取或写入数据
-
读硬盘时: 硬盘准备好数据后,硬盘控制器将其放在内部的缓冲区中,不断读此寄存器便是读出缓冲区中的全部数据。
-
写硬盘时: 把数据源源不断地输送到此端口,数据便被存入缓冲区里,硬盘控制器发现这个缓冲区中有数据了,便将此处的数据写入相应的扇区中。
-
ERROR/FEATURES 由于环境不同用途不同,所以两个寄存器名字指的是同一个
-
读硬盘时: 端口0x171或0x1F1的寄存器名字叫Error寄存器,若读取失败,里面存储的是失败状态信息,并且0x1F2端口中存储未读的扇区数。
-
写硬盘时: 就变成feauture寄存器,用于写命令的参数,有些命令需要指定额外参数,这些参数就写在 Feature 寄存器中。
-
SectorCount 寄存器用来指定待读取或待写入的扇区数
-
硬盘每完成1个扇区,就会将此寄存器的值减1,如果中间失败了,此寄存器中的值便是尚未完成的扇区。
-
LBA 逻辑块地址,解决了磁盘在柱面磁头扇区上寻址的麻烦(CHS),寻址时不用再考虑扇区所在的物理结构,当今LBA有两种,一种是LBA28最大支持128GB的寻址,另外一种是LBA48,最大支持128PB寻址.
-
LBA寄存器,有三种不同的形式: LBA low、LBA mid、LBA high
-
LBA low 寄存器用来存储28位地址的第0-7位
-
LBA mid 寄存器用来存储第8-15位
-
LBA high 寄存器存储第16-23位
-
Device 寄存器是个杂项,宽度8位,此寄存器的低4位用来存储LBA地址的第24-27位
-
第4位用来指定通道上的主盘或从盘
-
第6位用来设置是否启用LBA方式
-
第5位和第7位是固定为1的,称为MBS位
-
Status 状态寄存器,控制端口0x1F7或0x177,它是8位宽度寄存器,用来给出硬盘的状态信息
-
第0位是ERR位,为1表示命令出错,具体原因可见error寄存器。
-
第3位是data request位,为1表示硬盘已经把数据准备好了机现在可以把数据读出来。
-
第6位是DRDY,表示硬盘就绪表示硬盘检测正常,可以继续执行一些其他命令。
-
第7位是BSY位,表示硬盘是否繁忙,为1表示硬盘正忙。
-
注释: 状态位与Error寄存器一样,在写硬盘时寄存器就变成Command,此寄存器用来存储让硬盘执行的命令,只要把命令写进此寄存器,硬盘就开始工作。
由于MBR受制于只能容纳512字节大小的数据,没法为内核准备好环境,更没法将内核成功加载到内存并运行,此时我们需要让MBR实现从硬盘加载Loader程序到内存,加载完成后再将接力棒交给Loader继续运行,MBR加载磁盘代码如下。
%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
; 清屏
mov ax, 0600h
mov bx, 0700h
mov cx, 0
mov dx, 184fh
int 10h
mov eax,LOADER_START_SECTOR ; 起始扇区lba地址
mov bx,LOADER_BASE_ADDR ; 写入的地址
mov cx,1 ; 待读入的扇区数
call rd_disk_m_16 ; 以下读取程序的起始部分(一个扇区)
jmp LOADER_BASE_ADDR
;功能:读取硬盘n个扇区
rd_disk_m_16:
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
由于MBR是占据了硬盘的第0扇区不可再使用,从第1扇区之后的扇区均可使用,此处我们把loader放到第2扇区,MBR从第2扇区中把它读出来,并将loader的加载地址选为0x900的位置,编译镜像需要注意扇区位置。
# 编译并连接include目录
nasm -I include/ mbr.asm -o mbr.bin
# 第一个扇区中写入512字节的mbr引导
dd if=mbr.bin of=kernel.img bs=512 count=1 conv=notrunc
dd if=/dev/zero of=kernel.img seek=1 bs=512 count=4096
# 第二个loader加载器,seek跳过两个扇区并写入
nasm -I include/ loader.asm -o loader.bin
dd if=loader.bin of=kernel.img bs=512 count=1 seek=2 conv=notrunc
至此虽然输出效果与在MBR中直接操作显卡输出结果一致,但本质是不同的,此处代码中MBR主要负责从硬盘中的第3个扇区中读入Loader加载器到内存,并将CPU指针指向它,后期的输出纯粹是Loader加载器所为。
5. 实模式切入保护模式
保护模式最早出现在80286系列处理器中,之所以会出现保护模式是因为实地址模式中存在以下问题:
1.实模式下操作系统与用户程序属于同一特权级R0,无法区分系统程序与用户程序。
2.用户程序引用的地址都是指向真实的物理地址,所以逻辑地址就等于物理地址。
3.用户程序可以直接修改段基址,当访问超过64KB的内存区域时需要手动切换段基址。
4.共20条地址线最大可用1MB内存,且一次只能运行1个程序,无法充分利用计算资源。
为了克服内存访问限制,CPU厂商则开发出保护模式,在保护模式下物理地址不能被程序直接访问,在访问时需要将虚拟地址转换为物理地址再去访问,而对于程序而言这一系列操作都是透明的。
这个地址转换过程是由操作系统与处理器共同协作完成的,处理器在硬件上提供地址转换部件,操作系统提供转换过程中所需要的页表。
实模式与保护模式
相对于实地址模式,保护模式对寄存器进行了一定的扩展,CPU扩展为32位后,其地址总线和数据总线也变为32位,寻址空间达到了4GB,为了让一个寄存器可以访问到4GB空间,需要将寄存器宽度提升到32位。
除段寄存器外,通用寄存器、指令指针寄存器、标志寄存器都由原来的16位扩展到了32位,段寄存器16位就够用了。
相对于实地址模式,保护模式大大提高了对内存段的保护能力,GDT全局描述符就是对特定内存段属性进行描述的数据结构,该数据结构中的每一个表项称为段描述符,大小为64字节,用来描述各个内存段的起始地址、大小、权限等信息。
由于全局描述符表GDT很大,所以默认将其放在了内存中,由GDTR寄存器指向它,GDTR是个48位的寄存器,通常使用lgdtr指令操作,控制该寄存器。
这样,段寄存器中保存的再也不是段基址了,里面保存的内容叫 段选择子(selector)
该选择子其实就是个数,用这个数来索引全局描述符表中的段描述符,如果把全局描述符表当成数组,那么选择子就是数组的下标。
GDT 全局描述符表
全局描述符表GDT是保护模式下内存段的登记表,这是不同于实地址模式下的显著特征。
局部描述符表LDT是CPU厂商为了在硬件层面支持多任务的一个表,当今操作系统不使用。
在实地址模式下,寻址是按照[段基址+段内偏移]
的形式进行,而在保护模式下为了保证兼容性,其也必须遵循这一规范。
在实地址模式下,访问内存时只要将段基址加载到段寄存器中,再结合偏移地址就行,段寄存器太小了,只能存储 16 位的信息,甚至连 20 位地址都要借助左移 4 位来实现。
而进入到保护模式,各个寄存器都提升到了32位,且还需对特定的内存段增加一些额外的安全属性,那么将这些属性放在内存中是最好的选择。
- 之所以需要增加全局描述符表,并为每个段增加段描述符,是因为实模式下存在以下问题。
- 实模式下的用户程序可以破坏存储代码的内存区域,所以要添加个内存段类型属性来阻止这种行为。
- 实模式下的用户程序和操作系统是同一级别的,所以要添加个特权级属性来区分用户程序和系统。
- 内存段是一片内存区域,访问内存就要提供段基址,所以要有段基址属性。
- 为了限制程序访问内存的范围,还要对段大小进行约束,所以要有段界限属性。
段描述符: 一个段描述符只用来描述一个内存段的属性,这些描述符被依次排列在GDT中,GDT全局描述符表相当于描述符数组,数组中每个元素都是一个描述符,每个描述符大小是8字节,分为高32位与低32位,即两个四字节,GDT中最多可容纳的描述符数量是65536/8=8192个,即 GDT 中可容纳 8192 个段或门。
如下,段描述符结构示意参考,以及每个字段的大体含义。
-
段界限
- 第0-15位与16-19位共同构成段界限,表示段的边界,大小,范围,段界限用20个二进制位来表示。
-
段基址
- 第0-7是段基址的16-23位,第24-31位是段基址的高32位,加上段描述符低32位中的段基址0-15位,就构成了32位的基地址。
-
Type字段
- 第8-11位是type字段,共占用4位,用于表示内存段或调用门的子类型。
-
S字段
- 第12位是S字段,用于指示是否为系统段,为0是系统段,为1是数据段,通常与Type字段配合使用。
-
DPL字段
- 第13-14位是DPL,即描述符特权级,通常是指所代表的内存段的特权级。
-
Present字段
- 第15位是P字段,标志着指定段是否存在,如果段存在于内存中,P为1否则为0。
-
AVaiLable字段
- 第20位是AVL字段,无用途,可随意使用此位。
-
L字段
- 第21位是L字段,用于设置是否是64位代码段,L为1表示64位代码段,为0则为32位。
-
D/B字段
- 第22位是DB字段,用来指示有效地址(段内偏移地址)及操作数的大小。
-
Granularity字段
- 第23位是G字段,用来指定段界限的单位大小,若G为0表示段界限的单位是1字节,若G为1表示段界限的单位是4KB。
段选择子: 保护模式下段寄存器中存储的就是段选择子,选择子是一个索引值,用此索引值在段描述符表中索引相应的段描述符,这样,便可以在段描述符中得到了内存段的起始地址和段界限值等相关信息。
如下,段选择子结构示意参考,以及每个字段的大体含义。
由于段寄存器是16位,所以选择子也是16位,每一个选择子都会被分为3块。
-
RPL字段
- 第0-1位,用来存储RPL(请求特权级) 通常为0、1、2、3四种特权级。
-
TI字段
- 第2位,用来指示选择子是在GDT还是LDT中索引描述符,为0在GDT中,为1在LDT中。
-
描述符索引
- 第3-15位是描述符的索引值,此值主要用于在GDT中索引符合条件的段描述符。
选择子的作用主要是确定段描述符,确定描述符的目的,一是为了特权级、界限等安全考虑,最主要的还是要确定段的基地址。
由于保护模式下段寄存器中已经默认是选择子了,在寻址时直接用选择子对应的[段描述符中的段基址+段内偏移地址]
就是要访问的内存地址。
A20Gate 地址回绕线
地址回绕线是为了兼容8086实模式而增加的,在实模式下地址线只有20条,寻址空间只能是1MB(0x00000 - 0xFFFFF)
如果超出1MB的寻址范围,那么在默认开启地址回绕的CPU上,会自动将超出1MB的部分回绕到0地址处,继续从0地址处开始映射,地址回绕如下图。
对于只有20位地址线的8086系列CPU而言,A20地址线默认是开启的,不需要任何操作即可实现地址回绕,但80286有24 条地址线,即A0-A23
,也就是说A20 地址线是开启的,如果访问0x100000-0x10FFEF
之间的内存,系统将直接访问这块物理内存,并不会像8086那样回绕到0,反之如果是关闭的,则访问超出0x00000 - 0xfffff
的地址范围后会自动回绕到0处。
如果A20Gate被打开,当访问到
0x100000-0x10FFEF
之间的地址时,CPU将真正访问这块物理内存。
如果A20Gate被禁止,当访问0x100000-0x10FFEF
之间的地址时,CPU将采用8086的地址回绕。
我们想进入保护模式,就需要突破第20条地址线(A20)去访问更大的内存空间。而这一切,只有关闭了地址回绕才能实现,而关闭地址回绕,就需要打开A20Gate,打开A20Gate地址线只需要将端口0x92
的第1位置1即可。
CR0 控制寄存器
想要进入保护模式还差最后一步,通过控制CR系列寄存器切换CPU模式,CR寄存器是CPU的控制窗口,即可用来查询CPU的内部状态,也可用于直接控制CPU的运行机制,切入保护模式最需要关注的是CR0寄存器中的PE位。
如下图是完整的CR0寄存器,以及重要寄存器解释:
-
保护允许位PE (Protedted Enable)
- 0位用于启动保护模式,如果PE置1则保护模式启动,PE置0则实模式启用。
-
监控协处理位MP (Moniter coprocessor)
- 1位与3位配合,当TS=1时操作码WAIT是否产生一个协处理器不能使用的出错信号。
-
任务转换位TS (Task Switch)
- 3位当一个任务转换完成之后,自动将它置1,随着TS=1就不能使用协处理器。
-
模拟协处理器位EM (Emulate coprocessor)
-
2位如果EM=1则不能使用协处理器,如果EM=0则允许使用协处理器。
-
微处理器扩展类型位ET (Processor Extension Type)
- 4位保存着处理器扩展类型的信息,如果ET=0使用287协处理器,ET=1使用387浮点协处理器。
-
写保护位WP
- 16位这一位置0就可以禁用写保护,置1则可恢复写保护。
-
分页允许位PE (Paging Enable)
- 31位表示芯片上的分页部件是否允许工作。
正式切入保护模式
在保护模式中,内存段都是平坦模式,也就是整个内存都在一个段内,进入保护模式之前我们需要手动在内存中构建出GDT及其内部的描述符,GDT只是一片内存区域,里面每隔8字节即是一个段描述符,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
GDT中的每个描述符简单介绍.
-
GDT_BASE
- 构建GDT的起始地址(此位是0位,不可用,所以直接填充为全0即可)
-
CODE_DESC/DATA_STACK_DESC/VIDEO_DESC
- 代码段描述符,数据段和栈段描述符,显存描述符
-
GDT_SIZE/GDT_LIMIT
- 计算出GDT大小,GDT_LIMIT得到段界限,为后续加载GDT做准备.
-
SELECTOR_CODE/SELECTOR_DATA/SELECTOR_VIDEO
- 分别构建代码段,数据段,显存段的选择子.
-
gdt_ptr
- 定义GDT指针,此指针是lgdt加载GDT到gdtr寄存器用的.
接下来就是进入保护模式,进入保护模式需要三步,1.打开A20,2.加载GDT,3.CR0第0位置1,代码如下。
in al,0x92
or al,0000_0010B
out 0x92,al
lgdt [gdt_ptr]
mov eax, cr0
or eax, 0x00000001
mov cr0, eax
最后还需要使用jmp SELECTOR_CODE:p_mode_start
指令来实现刷新流水线。
jmp SELECTOR_CODE:p_mode_start
[bits 32]
p_mode_start:
mov ax, SELECTOR_DATA
jmp $
流水线是CPU为了提高执行效率而发展起来的加速技术,通常执行指令需要经过取指令,译码,执行指令,等操作,而运用流水线技术则将当前指令及其后面的几条指令同时放在流水线中重叠执行。
由于实模式是16位的,而保护模式是32位,在切换时必须要清空当前流水线上面所有的16位指令集,以及错误的段属性,只有这样才能保证后面的32位指令能够被正确的执行。
此时我们既要改变代码段描述符缓冲寄存器的值,又要清空以前的流水线,使用JMP指令则可以达到这两种效果,JMP指令在执行无条件跳转时会自动的将所有段寄存器初始化并清空当前流水线上的指令集。