【自制操做系统04】从实模式到保护模式

经过前三章的努力,咱们成功将控制权转交给了 loader.asm 这个程序。具体说就是 bios 经过加载并跳转到 0x7c00(IMB大叔们定的) 把控制权转交给了咱们操做系统的第一个汇编程序 mbr.asm,而后 mbr.asm 里作的事就是经过加载 loader 程序并跳转到 0x900(这个是咱们本身定的)把控制权转交给了 loader.asm 程序,目前这个程序里还只是向屏幕输出一行字符串“loader”,今天咱们就将扩展它。而且今天咱们要作的事,是操做系统中的第一个精彩之处,就是从实模式跨越到保护模式html

1、实模式与保护模式鸟瞰

我这人喜欢直面问题,其实本章只须要搞明白三个主要问题就好了,什么是实模式和保护模式,实模式与保护模式的区别是什么,怎么进入保护模式。我先来简单阐述下这三个问题ios

什么是实模式和保护模式

Intel 8086 是一个由 Intel 于 1978 年所设计的 16 位微处理器芯片,是 x86 架构的鼻祖。紧接着 Intel 又推出了第一款 32 位的 cpu Intel 80286(很快被淘汰,80386更经典一些),这款 cpu 因为和以前有不少不一样的“保护”特性,因此称为保护模式,也是与此同时,以前的 8086 这个 16 位 cpu 才有了实模式的叫法。git

因此什么是实模式和保护模式,其实就是 Intel 给本身的处理器特性命的一个名字而已,具体有哪些特性那就是细节问题了,但最起码有一点刚刚已经有所透露,那就是保护模式至少是 32 位的,而实模式是 16 位的(即便一个 32 位的 cpu 也有实模式)编程

实模式与保护模式的区别是什么

  1. 实模式 16 位,保护模式 32 位
  2. 实模式下的地址是段寄存器地址偏移4位+偏移地址获得物理地址。保护模式下段寄存器存入了段选择子,在段描述符表中寻找段基址,再加上偏移地址获得物理地址(开启分页下为逻辑地址)
  3. 这个我以为是个 1 的推论,就是实模式寻址空间是 1M,保护模式是 4G
  4. 这个我以为是 2 的推论,就是段描述符表记录了段的权限,改变了实模式下能够随意访问全部内存的隐患(这也是保护这两个字的体现)

怎么进入保护模式

进入保护模式有三步:数组

  1. 打开 A20
  2. 加载 gdt
  3. 将 cr0 的 pe 位置 1

能够看出进入保护模式的操做是很简单的,但提早要作好准备工做,最重要的就是 gdt(Global Descriptor Table 全局描述表)的准备。数据结构

2、代码鸟瞰

loader.asm

section loader vstart=0x900

jmp protect_mode

gdt:
;0描述符
    dd  0x00000000
    dd  0x00000000
;1描述符(4GB代码段描述符)
    dd  0x0000ffff
    dd  0x00cf9800
;2描述符(4GB数据段描述符)
    dd  0x0000ffff
    dd  0x00cf9200
;3描述符(28Kb的视频段描述符)
    dd  0x80000007
    dd  0x00c0900b

lgdt_value:
    dw $-gdt-1  ;高16位表示表的最后一个字节的偏移(表的大小-1) 
    dd gdt      ;低32位表示起始位置(GDT的物理地址)

SELECTOR_CODE   equ 0x0001<<3
SELECTOR_DATA   equ 0x0002<<3
SELECTOR_VIDEO  equ 0x0003<<3

protect_mode:
;进入32位
    lgdt [lgdt_value]
    in al,0x92
    or al,0000_0010b
    out 0x92,al
    cli
    mov eax,cr0
    or eax,1
    mov cr0,eax
    
    jmp dword SELECTOR_CODE:main
    
[bits 32]
;正式进入32位
main:
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:0xa0],'3'
mov byte [gs:0xa2],'2'
mov byte [gs:0xa4],'m'
mov byte [gs:0xa6],'o'
mov byte [gs:0xa8],'d'

jmp $

这里说说个人心得体会,如今看整段的代码虽不能说每一行让我本身写能写出来,但如今看起来极为清晰。我如今其实已经想不起来当时为何理解了很久很久就是理解不了,调试了好半天也总是有各类问题。不过这个代码是我去掉了一些无关紧要影响理解的部分,只留下了最精华的部分,我不知道若是我一开始接触的是这样的代码是否可以理解到位。架构

鸟瞰整段代码,大概分为三块。学习

  • 第一块用二进制方式网内存中写了数据(四个段描述符),并定义了三个常量
  • 第二块其实仔细观察会发现就是进入保护模式的步骤(打开A20、加载gdt、将cr0的pe位置1)
  • 第三块仍是一个在屏幕上输出“32mod”字符串,与以前不一样的是这是在保护模式下的输出

3、代码第一块解读:全局段描述符表(GDT)

cpu 与操做系统打配合的方式

有件事如今说可能体会不大,写到后面好多地方你会发现,像加载 gdt 这种操做模式好多地方都是通用的,咱先不用管 gdt 是什么,总之 cpu 会有不少与操做系统相互打配合的地方,这个就是其中之一。配合怎么打呢,那就是 cpu 定义好一个数据结构,再给你一个寄存器。操做系统通常负责作三件事情操作系统

  1. 负责在内存中某位置按照这个数据结构写一堆数据(如本讲的段描述符表gdt,以及以后要说的页表)
  2. 而后再把你写在内存的哪一个位置这个信息(起始地址),存在 cpu 给你预留的一个寄存器里,这通常会有一条专门的指令,好比本讲的 lgdt,不会说让你用 mov 操做的
  3. 操做系统将 cpu 某寄存器中的某位置 1

而后就开启了这个功能,段描述符表如此,页表如此,TSS亦是如此,这个以后讲到会深有体会。我如今已经有所体会了,但还没整理出所有的这种打配合的地方,等我再深刻些再给你们整理一份。设计

先说说什么是段描述符

直接上干货,还记不记得第一节课说的内容

在你开机的一瞬间,CPU 的 PC 寄存器被强制初始化为 0xFFFF0。若是再说具体些,CPU 将段基址寄存器 cs 初始化为 0xF000,将偏移地址寄存器 IP 初始化为 0xFFF0,根据实模式下的最终地址计算规则,将段基址左移 4 位,加上偏移地址,获得最终的物理地址也就是抽象出来的 PC 寄存器地址为 0xFFFF0。

这种段基址左移 4 位,加上偏移地址,获得物理地址的方式,就是实模式下的地址转换方式。

然而保护模式下不同了

在保护模式下,段基址寄存器中存的数据,被理解为段选择子,根据这个值去咱们本身在内存中写好的段描述符表中找,找到对应的段描述符,从中取出段基址。用这个段基址加上偏移地址,最终获得物理地址(逻辑地址和页表的事之后再说,不冲突)。

就这么点区别

那天然就有两个问题,一个是段描述符表长什么样子呀?决定了咱们往内存中写的数据结构是什么。另外一个就是去哪找段描述符表压,这个就须要告诉 cpu 为咱们提早预留好的寄存器,也就是 lgdt 指令。下面咱们就分别看着两个问题

段描述符表长什么样子

首先段描述符表是一张表,在内存中也就是个数组,是一个个的段描述符一个个紧挨着的结果。因此咱们要了解段描述符长什么样就行了

这里我顺便把选择子GDTR 寄存器的结构也列出来了,这些就是所有的须要咱们本身写数据的地方了,也是 cpu 和操做系统配合中须要约定的所有事情

;0描述符
    dd  0x00000000
    dd  0x00000000
;1描述符(4GB代码段描述符)
    dd  0x0000ffff
    dd  0x00cf9800
;2描述符(4GB数据段描述符)
    dd  0x0000ffff
    dd  0x00cf9200
;3描述符(28Kb的视频段描述符)
    dd  0x80000007
    dd  0x00c0900b

咱们看看这些直接在内存中写死的常量,就是按照段描述符的数据结构写的

代码段描述符转化为二进制是 00000000_00000000_11111111_11111111_00000000_11001111_10011000_00000000
数据段描述符转为为二进制是 00000000_00000000_11111111_11111111_00000000_11001111_10010010_00000000
视频段描述符转化为二进制是 10000000_00000000_00000000_00000111_00000000_11000000_10010000_00000000

这里咱们拿视频段描述符来分析,提取(拼凑)出段基址的数据,00000000_00000000_10000000_00000000,转换为十六进制是 0x80000。怎么样熟不熟悉,这刚好是显卡黑白模式在内存中的映射的起始地址。能够看下第一章的内容,不过我这里仍是把图贴出来。

接下来的几个常量定义,很容易明白它们的意思

lgdt_value:
    dw $-gdt-1  ;高16位表示表的最后一个字节的偏移(表的大小-1) 
    dd gdt      ;低32位表示起始位置(GDT的物理地址)

SELECTOR_CODE   equ 0x0001<<3
SELECTOR_DATA   equ 0x0002<<3
SELECTOR_VIDEO  equ 0x0003<<3

lgdt_value 就是按照 lgdt 寄存器规定的数据结构拼凑出来的,下面的三个常量其实就是对应上面定义的三个段描述符的偏移量,因为每一个描述符占 64 位,也就是占 8 个地址单元,因此索引下标的计算就是第几个描述符 * 8就行了,相信这个不难理解。

4、代码第二块解读:进入保护模式三步走

代码直接对应上面的三步

加载 gdt

lgdt [lgdt_value]

打开 A20

in al,0x92
or al,0000_0010b
out 0x92,al
cli    ;禁止中断,先不用管

将 cr0 的 pe 位置 1

mov eax,cr0
or eax,1
mov cr0,eax

此时已经进入保护模式了,段基址寄存器的意义已经变了,因此跳转指令变成了

jmp dword SELECTOR_CODE:main

5、代码第三块解读:保护模式下的简单代码

前面就是将数据段寄存器赋值给一些段基址寄存器用于访问数据段,而后将栈基址赋值位本次加载到的内存位置,重点是下面几句

mov ax,SELECTOR_VIDEO
mov gs,ax
mov byte [gs:0xa0],'3'
...

这段将咱们刚刚写好的常量 SELECTOR_VIDEO 写入了段基址寄存器 gs,并在其后用了这个基址寄存器去进行 mov 操做。经过这个段选择子,在段描述符表里寻找出来的段基址是咱们写好的显卡的内存映射的起始地址,因此同前几章在实模式下的输出就同样了。

6、运行代码

咱们并无增长新文件,因此Makefile和上一篇同样,不用变,直接运行看效果,make brun

能够看到,咱们的段基址寄存器没有直接写显卡的起始地址,而是经过段选择子索引的,但依然正常输出了 "32mod" 字符串,说明成功了

写在最后:开源项目和课程规划

若是你对自制一个操做系统感兴趣,不妨跟随这个系列课程看下去,甚至加入咱们,一块儿来开发。

参考书籍

《操做系统真相还原》这本书真的赞!强烈推荐

项目开源

项目开源地址:https://gitee.com/sunym1993/flashos

当你看到该文章时,代码可能已经比文章中的又多写了一些部分了。你能够经过提交记录历史来查看历史的代码,我会慢慢梳理提交历史以及项目说明文档,争取给每一课都准备一个可执行的代码。固然文章中的代码也是全的,采用复制粘贴的方式也是彻底能够的。

若是你有兴趣加入这个自制操做系统的大军,也能够在留言区留下您的联系方式,或者在 gitee 私信我您的联系方式。

课程规划

本课程打算出系列课程,我写到哪以为能够写成一篇文章了就写出来分享给你们,最终会完成一个功能全面的操做系统,我以为这是最好的学习操做系统的方式了。因此中间遇到的各类坎也会写进去,若是你能持续跟进,跟着我一块写,必然会有很好的收货。即便没有,交个朋友也是好的哈哈。

目前的系列包括

相关文章
相关标签/搜索