自制 OS 极简教程 3:你离内核还差十万八千里

来自公众号:低并发编程

在上一篇《自制 os 极简教程 2:史上最难的 hello world》中,我们已经完成了最基本的环境搭建并实现了从零自制操作系统的 hello world 程序,下面我急速过一遍主要步骤:

急速回顾

第一步:新建一个文件 boot.s

;BIOS把启动区加载到内存的该位置
;所以需设置地址偏移量
section mbr vstart=0x7c00

;直接往显存中写数据
mov ax,0xb800 ;这条就是第一条指令
mov gs,ax
mov byte [gs:0x00],'h'
mov byte [gs:0x02],'e'
mov byte [gs:0x04],'l'
mov byte [gs:0x06],'l'
mov byte [gs:0x08],'o'
mov byte [gs:0x0a],' '
mov byte [gs:0x0c],'w'
mov byte [gs:0x0e],'o'
mov byte [gs:0x10],'r'
mov byte [gs:0x12],'l'
mov byte [gs:0x14],'d'

jmp $

;512字节的最后两字节是启动区标识
times 510-($-$$) db 0
db 0x55,0xaa

第二步:编译它

nasm -o boot boot.s

第三步:创建虚拟磁盘映像,并填充第一扇区

创建虚拟磁盘映像:
bximage -mode=create -hd=60 -q os.raw

填充第一扇区:
dd if=boot of=os.raw bs=512 count=1

第四步:用 bochs 启动它

bochs -f bochs.properties

然后就该写内核了吧

你很自然地就想到,最最开始的 hello world 流程打通了,之后要做的就是把之前在屏幕上打印 hello world 的代码,替换成操作系统代码就好了呀,好简单。

那我们就把刚刚的 boot.s 里的汇编代码删掉,开始写内存管理,写文件系统,写进程管理,写设备驱动,最后来个死循环的 shell 程序让系统怠速运行,等待用户输入命令,一个麻雀操作系统就此完成!完美!

No No No

你离真正开始写这些内核程序,还差十万八千里呢。因为在此之前,你还需要用程序对一些硬件进行调教,才能让硬件配合我们现在要求越来越多的操作系统。

那我不调教硬件,直接上来就写内核程序不可以吗?当然可以!

这样你就写出了一个,在实模式下运行的、没有分页机制、没有中断机制(键盘鼠标都没用)的 16 位操作系统,香么?(这句话并不严谨,只是想表达这个意思)

所以,还是乖乖学习如何调教 Intel 大佬们设计出的硬件吧,其实本质上就是照着 Intel 手册上的说明,写对应的程序就好了。

GO

开始调教硬件

这部分对于初次接触的人,可以说是非常非常难!所以我先把要做的全部事情列举出来:

  • 打开 A20 地址线

  • 开启分段机制

    • 内存某位置写好段描述符表

    • 加载段描述符表地址到 gdtr 寄存器

    • 将 cr0 寄存器的 pe 位置 1

  • 从实模式切换到保护模式

  • 开启分页机制

    • 在内存某位置写好页目录表和页表

    • 加载页目录表地址到 cr3 寄存器

    • 将 cr0 寄存器的 pg 位置 1

  • 开启中断机制

    • 在内存某位置写好中断描述符表

    • 初始化可编程中断控制器 PIC

    • 加载中断描述符表(idt)地址到 idtr 寄存器

我是不是把你给劝退了?别急呀,睁大眼睛好好看下,有没有发现一个规律?是不是基本都长这个样子

  • 开启 XXX 机制
    • 在内存某位置写好 XXX
    • 加载 XXX 地址到 XXX 寄存器
    • 将 XXX 寄存器的 XXX 位置 1

没错,这个基本上就是好多时候软件和硬件协同配合的一种方式,软件在内存某个位置,按照硬件要求的数据结构写上数据,然后通过专有的寄存器告诉硬件内存的起始地址,然后再通过另一个专有寄存器某一位上是 1 还是 0 来控制该功能关闭还是开启。

按照这个思路看,整个过程就清晰很多啦,接下来我们把整个过程揉碎了看。

这里我们不能按照代码执行顺序,得先看最后一项

从实模式到保护模式

  • 打开 A20 地址线

  • 开启分段机制

    • 内存某位置写好段描述符表

    • 加载段描述符表地址到 gdtr 寄存器

    • 将 cr0 寄存器的 pe 位置 1

  • 从实模式切换到保护模式

  • 开启分页机制

    • 在内存某位置写好页目录表和页表

    • 加载页目录表地址到 cr3 寄存器

    • 将 cr0 寄存器的 pg 位置 1

  • 开启中断机制

    • 在内存某位置写好中断描述符表

    • 初始化可编程中断控制器 PIC

    • 加载中断描述符表(idt)地址到 idtr 寄存器

我这人喜欢直面问题,什么是实模式和保护模式?实模式与保护模式的区别是什么?怎么进入保护模式?我先来简单阐述下这三个问题

什么是实模式和保护模式

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

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

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

  1. 位数:实模式 16 位,保护模式 32 位
  2. 地址计算:实模式下的地址是段寄存器地址左移 4 位 + 偏移地址得到物理地址。保护模式下段寄存器存入了段选择子,在段描述符表中寻找段基址,再加上偏移地址得到物理地址(如果开启分页下这里为虚拟地址,还需要再次经过 MMU 转换为最终的物理地址)
  3. 寻址空间:这个我觉得是 1 的推论,就是实模式寻址空间是 1M,保护模式是 4G
  4. 安全性:保护模式下分了四个特权级(0、1、2、3),有两个是我们熟悉的用户态(3)和内核态(0)。比如全局描述符表的段描述符中就有记录特权级的位,配合硬件机制可以在某些情况对程序做到保护,这种'保护'我们得之后慢慢体会。

怎么进入保护模式

进入保护模式在 CPU 层面特别简单,它只是一个开关,即把 cro 寄存器(机器状态字)的位 0 置为 1 即可。

一段几乎固定的代码即可完成( lmsw 汇编指令即是加载机器状态字的指令)

mov ax, 0x0001lmsw ax

这很简单,但重点确是进入保护模式之前的那些准备工作。这一步就好比你和你女朋友/男朋友结婚一样,实际操作上来说,就是去民政局领个证,但领证之前的相识、表白、相爱、分手、复合,这些才是关键。

所以,我们接下来看一看,进入保护模式之前,都需要做哪些准备工作。

打开 A20 地址线

  • 打开 A20 地址线

  • 开启分段机制

    • 内存某位置写好段描述符表

    • 加载段描述符表地址到 gdtr 寄存器

    • 将 cr0 寄存器的 pe 位置 1

  • 从实模式切换到保护模式

  • 开启分页机制

    • 在内存某位置写好页目录表和页表

    • 加载页目录表地址到 cr3 寄存器

    • 将 cr0 寄存器的 pg 位置 1

  • 开启中断机制

    • 在内存某位置写好中断描述符表

    • 初始化可编程中断控制器 PIC

    • 加载中断描述符表(idt)地址到 idtr 寄存器

简单理解,这一步就是为了突破地址信号线 20 位的宽度,变成 32 位可用。这是由于 8086 CPU 只有 20 位的地址线,所以如果程序给出 21 位的内存地址数据,那多出的一位就被忽略了,比如如果经过计算得出一个内存地址为  1 0000 00000000 00000000 ,那实际上内存地址相当于 0
当 CPU 到了 32 位时代之后,由于要考虑兼容性,还必须保持一个只能用 20 位地址线的模式,所以如果你不手动开启的话,即使地址线已经有 32 位了,仍然会限制只能使用其中的 20 位。
开启 A20 地址线也很简单,同样也是一段几乎固定的代码即可完成
in al,0x92
or al,0000_0010b
out 0x92,al
cli

开启分段机制

  • 打开 A20 地址线

  • 开启分段机制

    • 内存某位置写好段描述符表

    • 加载段描述符表地址到 gdtr 寄存器

    • 将 cr0 寄存器的 pe 位置 1

  • 从实模式切换到保护模式

  • 开启分页机制

    • 在内存某位置写好页目录表和页表

    • 加载页目录表地址到 cr3 寄存器

    • 将 cr0 寄存器的 pg 位置 1

  • 开启中断机制

    • 在内存某位置写好中断描述符表

    • 初始化可编程中断控制器 PIC

    • 加载中断描述符表(idt)地址到 idtr 寄存器

这个是本文最难理解的地方了!还记得上面说的地址线有 20 位这个事么?不知道你有没有想过,16 位的实模式怎么可能产生 20 位的地址线呢?这又是一个历史遗留问题。

当时的 CPU 只有 16 位的,但已经可以生产出 1 MB 大小的内存了,CPU 的位数没有跟上内存大小的发展,那 16 位的 CPU 如何产生 20 位的地址信号给内存呢?大佬们想出了如下办法。

在实模式下,程序员给出的线性地址并不是最终的物理地址,物理地址是由段基址左移 4 位 + 段内偏移地址(程序员给出的线性地址)计算得出的,所以才勉强可以表示 20 位。

之后当然又是由于兼容性问题,这个别扭的设计一直保留了下来,包括你现在正在用的 CPU (x86 架构的并且在实模式下的)也是如此。

然而保护模式下不一样了

在保护模式下,段基址寄存器中存的数据,被理解为段选择子,根据这个值去我们自己在内存中写好的全局描述符表中,找到对应的段描述符,从中取出段基址。用这个段基址加上偏移地址,得到最终的物理地址(如果开启了分页则还需要分页机制的转换,不过逻辑地址和页表的事以后再说,不冲突,因为我们还没有开启分页,目前就是这么转换的)。

物理地址的计算过程,就这么点区别,原来是段寄存器左移四位,现在是段寄存器里的值是用于在全局描述符表中找到相应的段描述符,进而找到段基址。

但段描述符中可不只有段基址,还有很多其他信息,正是这些信息使得 CPU 和我们程序配合可以玩出很多花样,那接下来我们就看看段描述符的具体结构。

段描述符是存储在全局描述符表中的

全局描述符表是一张表,在内存中也就是个数组,里面是一个个的段描述符紧挨着。

段描述符里面各个位的含义,要是展开讲能说上两三篇博客,这里就不完全展开了,只简单关注重点几项。
  • 段基址:用于计算物理地址用的,上面说过了

  • 段界限:可以设定范围,防止程序访问超出这个范围的地址

  • TYPE:描述符有很多类型,包括后面要说的中断门、陷阱门、LDT,但这里我们只用到了代码段类型和数据段类型。

按照这个结构,我们通过汇编语言直接在内存的某位置把它写出来(这里的值只是其中一种比较合理的设计方案,不用究其细节)
;这里用一个标识表示全局描述符表的内存起始位置gdt:;0描述符(没用)dd0x00000000dd0x00000000;1描述符(4GB代码段描述符)dd0x0000ffffdd0x00cf9800;2描述符(4GB数据段描述符)dd0x0000ffffdd0x00cf9200;3描述符(28Kb的视频段描述符)dd0x80000007dd0x00c0920b

这里我们拿视频段描述符来分析,提取(拼凑)出段基址的数据,转换为十六进制是 0xb8000。怎么样熟不熟悉,这恰好是显卡黑白模式在内存中的映射的起始地址。忘了的同学还是要回顾一下《计算机启动流程》

这样我们之后再访问内存的时候,就可以指定段选择子是 0x3 << 3 这个值了(全局描述符表中第三个描述符代表的段描述符),然后偏移地址就可以从 0 开始写了啦,这样就很方便,也不用担心越界,如下

;定义一个常量表示视频段选择子
SELECTOR_VIDEOequ0x0003<<3
;把视频段选择子加载到段寄存器gs
mov eax, SELECTOR_VIDEO
mov gs, ax
;直接往显卡内存上写数据
mov byte [gs:0x00],'h'
mov byte [gs:0x02],'e'
mov byte [gs:0x04],'l'
mov byte [gs:0x06],'l'
mov byte [gs:0x08],'o'

这样我用最简单的方式介绍了保护模式下分段机制的工作机理,后面还有很多细节,只能在后面学习的过程中慢慢体会了,不要着急。

下面我们看进入保护模式后进入内核前的下一个准备工作。

开启内存分页机制

  • 打开 A20 地址线

  • 开启分段机制

    • 内存某位置写好段描述符表

    • 加载段描述符表地址到 gdtr 寄存器

    • 将 cr0 寄存器的 pe 位置 1

  • 从实模式切换到保护模式

  • 开启分页机制

    • 在内存某位置写好页目录表和页表

    • 加载页目录表地址到 cr3 寄存器

    • 将 cr0 寄存器的 pg 位置 1

  • 开启中断机制

    • 在内存某位置写好中断描述符表

    • 初始化可编程中断控制器 PIC

    • 加载中断描述符表(idt)地址到 idtr 寄存器

开启内存仍然很简单,就是把 cr0 寄存器(机器状态字)的 pg 位置 1 即可。
但还是如保护模式一样,最重要的不是开启那一下,而是之前做的准备工作。我们先从什么是分页机制讲起。

什么是分页机制

首先要明确一件事,分段机制和分页机制并不是二选一的关系,分页机制也要建立在分段机制的基础之上。

也就是说,在没有开启分页机制时,由程序员给出的逻辑地址,需要先通过分段机制转换成物理地址。但在开启分页机制后,逻辑地址仍然要先通过分段机制进行转换,只不过转换后不再是最终的物理地址,而是虚拟地址(或者叫线性地址),然后再通过一次分页机制转换,得到最终的物理地址。

分页机制的原理

计算机中有一个硬件,叫 MMU,中文名字叫内存管理单元,有时也叫 PMMU,分页内存管理单元。由这个部件来负责将虚拟地址转换为物理地址。

想让这个分页机制生效,除了开启分页机制开关之外,我们程序员还需要在内存中写入页目录表页表,这种页表方案叫做二级页表,第一级叫页目录表PDE,第二级叫页表PTE。他们的结构如下。

准备好这些之后,MMU 就可以帮我们进行转换了。首先把线性地址被拆分成

高10位:中间10位:后12位

高 10 位负责在页目录表中找到一个页目录项,这个页目录项的值加上中间 10 位拼接后的地址去页表中去寻找一个页表项,这个页表项的值,再加上后 12 位偏移地址,就是是最终的物理地址。

比如我们的虚拟地址(已经经过了分段机制的转换)是

0xC02509

二进制表示就是

0000000011 0000000010 010100001001

我们看一下它的转换过程

设置页表并开启分页

说了那么多,其实代码就几行搞定了,主要是设计页表的方式,这里我们效仿 linux-0.11 的做法。
当时 linux-0.11 认为,总共可以使用的内存不会超过 16M,也即最大地址空间为 0xFFFFFF。
而按照当前的页目录表和页表这种机制,1 个页目录表最多包含 1024 个页目录项(也就是 1024 个页表),1 个页表最多包含 1024 个页表项(也就是 1024 个页),1 页为 4KB(因为有 12 位偏移地址),因此,16M 的地址空间可以用 1 个页目录表 + 4 个页表搞定。
4(页表数)* 1024(页表项数) * 4KB(一页大小)= 16MB

所以,下面这段代码就是,将页目录表放在内存地址的最开头(0x0)处。之后紧挨着这个页目录表,放置 4 个页表。最终将页目录表和页表填写好数值,来覆盖整个 16MB 的内存。最后,开启分页。

;内存开始 0x000 处设置页目录表_pg_dir:

.org 0x1000 pg0: ;第1个页表在内存0x1000位置.org 0x2000 pg1: ;第2个页表在内存0x2000位置.org 0x3000 pg2: ;第3个页表在内存0x3000位置.org 0x4000 pg3: ;第4个页表在内存0x4000位置

...

;将4个页目录项填写好  mov dword [_pg_dir], pg0+7  mov dword [_pg_dir+4], pg1+7  mov dword [_pg_dir+8], pg2+7  mov dword [_pg_dir+12], pg3+7

;设置4个页表中所有项的内容  mov ecx, 1000  mov edi, pg3+4092  mov eax, 0xfff007  mov dword [edi],eax  cp:    sub eax,0x1000    sub edi,4    mov dword [edi],eax    cmp eax,0x007    jne cp

;开启分页;即设置 cr3 寄存器(页目录表基址寄存器)   xor eax,eax   mov cr3,eax   mov eax,cr0   or eax,0x80000000   mov cr0,eax

开启中断机制

  • 打开 A20 地址线

  • 开启分段机制

    • 内存某位置写好段描述符表

    • 加载段描述符表地址到 gdtr 寄存器

    • 将 cr0 寄存器的 pe 位置 1

  • 从实模式切换到保护模式

  • 开启分页机制

    • 在内存某位置写好页目录表和页表

    • 加载页目录表地址到 cr3 寄存器

    • 将 cr0 寄存器的 pg 位置 1

  • 开启中断机制

    • 在内存某位置写好中断描述符表

    • 初始化可编程中断控制器 PIC

    • 加载中断描述符表(idt)地址到 idtr 寄存器

这部分的主体是在 c 语言中实现的(初始化 PIC 用汇编实现更方便),也可以理解为进入内核后(c 语言的 main 方法)才实现的。但由于我认为它同样属于进入内核前干的一些 脏活累活,所以就划到内核前讲啦~

这部分重在理解原理,由于代码会在之后的 C 语言中实现,所以先不用管具体实现哦~

简单说,中断就是,由外部设备经过一个中断控制器给 CPU 一个电信号,或者由软件执行某些指令给 CPU 一个电信号,表示中断来啦。然后 CPU 根据这个电信号所表示的中断号,去由我们提前设置好的中断向量表中寻找中断服务程序,跳转过去执行。

中断的分类

> 外部中断

外部中断通过两个引脚连接到 CPU 上,一个是可屏蔽中断 INTR,一个是不可屏蔽中断 NMI

  • INTR:硬盘、打印机、网卡等设备发出的中断信号,可通过 eflags 寄存器的 IF 位将所有这些外部设备的中断屏蔽
  • NMI:电源掉电、内存读写错误、总线奇偶校验错误等灾难性的错误,不可屏蔽,CPU 必须立刻处理

对于可屏蔽中断,Linux 的处理方式是分成 上半部 和 下半部。上半部执行时关闭中断,立刻执行完毕;下半部执行时打开中断,此时如果有其他中断进来,则让给其他中断。

> 内部中断

内部中断可分为软中断异常,二者均是不可屏蔽的(即不受 eflags 的 IF 位影响)

  • 软中断:就是软件发起的中断,最常见的也是我们之后进行系统调用的,就是 int 8 位立即数,可表示 256 中中断。还有一些不常用的,甚至可以叫做异常,下面简单列出
    • int3:中断向量号3,调试断点指令
    • into:中断向量号4,中断溢出指令
    • bound:中断向量号5,检查数组索引越界指令
    • ud2:中断向量号6,未定义指令,常用于软件测试中主动发起这个中断
  • 异常:指令执行期间 CPU 内部产生的错误引起,如分母为 0 将发起 6 号中断(异常),未定义的指令发起 6 号中断
    • Fault(故障):可恢复的错误。发生此中断时,CPU 将机器状态恢复到异常之前的状态,之后调用中断处理程序,结束后返回。常见的如 缺页异常
    • Trap(陷阱):有意的异常。通常是调试程序中用 int3 指令主动触发。
    • Abort(终止):不可恢复的异常。直接将此程序从进程表中去掉。

中断号

我们知道一个中断对应着一个中断号(中断向量号),后面我们可以设定可编程中断控制器 PIC 来指定键盘产生什么中断号,鼠标产生什么中断号,等等。这些都是我们自己可以定的。

当然 Intel 爷爷指定了一些出厂就设定好的中断号,不用你管,你也改不了。比如 0 表示除零异常,6表示非法指令,等等。

中断描述符表 IDT

一个中断号来了之后,怎么对应到它要执行哪一段程序呢?就是通过查找中断描述符表 IDT

IDT 与 GDT(全局描述符表)类似,也是由 8 字节长的描述符组成的一个数组。所以我们关心的其实是里面的中断描述符的样子。中断描述符属于门描述符的一种,所以也叫中断门描述符。

通过门描述符可以找到一段程序的入口地址,同时也可以实现特权级的翻越。(比如 linux 的系统调用是通过中断门实现的,从而特权级可以从用户态翻越到内核态)

但不同门描述符的触发条件不同,由于现代操作系统基本不用任务门和调用门(调用门本来是用来进行系统调用的,但 linux 通过中断来实现了),陷阱门只在调试的时候会用到,所以这里我们只关注中断门即可,而中断门的触发只能通过中断信号,具体就是上面说的各种产生中断的类型。

中断门描述符的结构如下(这里顺带写出其他门描述符,不用管)

如何找到中断描述符表呢?你猜的没错,也是正如找全局描述符表,页表等一样,有个专门的 IDTR寄存器 存储它的内存起始地址,有个 lidt  指令负责加载 IDTR。经典做法,我们见过太多次了,就不多说啦,不理解的可以从本系列开头开始看哟。

中断处理过程

上图就表示了整个中断处理的过程。

首先处理器根据中断向量号,在中断描述符表中找到对应的中断描述符,从中断描述符中提取出段选择子和段内偏移地址,之后就按照段机制所要求的那样,转换为最终的物理地址,这个地址就是中断服务程序的内存地址。

中断控制器 8259A 芯片

我们之前说过,外部设备发出中断信号,进入 CPU 的 INT 引脚上。但如果有多个外部设备近乎同时发送中断信号,CPU 先处理哪一个呢?未被处理的中断信号又记录在哪里呢?这时候就需要有个中间的代理设备来负责这个事情。

这个代理设备叫做可编程中断控制器 PIC,其中 8259A 芯片是最常见的一种,我们这里把它的内部结构展示出来。

由于是硬件相关,就不展开细说了, 作为程序员我们必须进行一个非常无聊的步骤,就是要为其进行重新编程,不然 PIC 就默认会将 IRQ0x00 ~ IRQ0x0F 引脚对应在中断号 0x00 ~ 0x0F ,而 0x00 ~ 0x1F 这些中断号是被 Intel 保留作为自己保留的中断号,所以这些中断号不能动。

所以这里我们对其重新编程,将 IRQ0x00 ~ IRQ0x0F 对应到中断号 0x20 ~ 0x2F 上。
moval,0x11
out0x20,al
dw0x00eb,0x00eb
out0xA0,al
dw0x00eb,0x00eb
moval,0x20
out0x21,al
dw0x00eb,0x00eb
moval,0x28
out0xA1,al
dw0x00eb,0x00eb
moval,0x04
out0x21,al
dw0x00eb,0x00eb
moval,0x02
out0xA1,al
dw0x00eb,0x00eb
moval,0x01
out0x21,al
dw0x00eb,0x00eb
out0xA1,al
dw0x00eb,0x00eb
moval,0xFF
out0x21,al
dw0x00eb,0x00eb
out0xA1,al

这段给硬件编程的代码,就没必要在这里细考究了,完成这一步之后,我们的 PIC 所能采集到的中断以及映射关系如下。

PIC 请求号 中断号 用途
IRQ0 0x20 时钟中断
IRQ1 0x21 键盘中断
IRQ2 0x22 接连从芯片
IRQ3 0x23 串口2
IRQ4 0x24 串口1
IRQ5 0x25 并口2
IRQ6 0x26 软盘驱动器
IRQ7 0x27 并口1
IRQ8 0x28 实时钟中断
IRQ9 0x29 保留
IRQ10 0x2a 保留
IRQ11 0x2b 保留
IRQ12 0x2c 鼠标中断
IRQ13 0x2d 数学协处理器
IRQ14 0x2e 硬盘中断
IRQ15 0x2f 保留

具体中断相关的代码,就留在下一章的进入内核之后再说啦,这里先了解原理就好。

万事俱备,进入内核!

进入内核开始实现主要功能之前,我们已经把 各种和硬件打交道的脏活累活 做得差不多了,这些部分基本比较固定,不太会发挥自己的主观思想,顶多也就是某些数据结构的内存位置自己来定,段选择子按照自己方便来分而已。

下面把我们今天完成的列出来!给自己鼓鼓掌吧!

  • 打开 A20 地址线

  • 开启分段机制

    • 内存某位置写好段描述符表

    • 加载段描述符表地址到 gdtr 寄存器

    • 将 cr0 寄存器的 pe 位置 1

  • 从实模式切换到保护模式

  • 开启分页机制

    • 在内存某位置写好页目录表和页表

    • 加载页目录表地址到 cr3 寄存器

    • 将 cr0 寄存器的 pg 位置 1

  • 开启中断机制

    • 在内存某位置写好中断描述符表

    • 初始化可编程中断控制器 PIC

    • 加载中断描述符表(idt)地址到 idtr 寄存器

写在最后

本章的内容真的是非常非常多,涉及的知识点也是非常多、杂、细,包括分段、分页、中断、以及 Intel 遗留下来的实模式与保护模式这个历史。

但这些内容,又在真正的操作系统实现中,占了非常少的一部分,就好比在打地基吧,对了解操作系统的思想其实贡献不大。

所以我把这么多的内容,都放在一个章节来讲了,以便用更多的章节来疏通主流程,因为我们这个教程毕竟不是手把手教你写,所以这些细节的内容,大家理解就好。

如果你看的一头雾水,那就对了,你需要花非常多的时间,才能把这些琐碎的事搞明白。如果这一章你看得明明白白,而且觉得我好多地方说的都不够准确,那建议直接啃大部头的书籍,完全没问题~

所以要是实在不懂,那这章就过了,留个印象就好啦,问题不大。

最后给大家留个作业,请下载文末的配套代码,运行今天课程的示例,并回答下面的问题(通过率低于 60% 下一章我就不写了哈哈哈~)

(0)

相关推荐

  • x86_64内存寻址 - 一个完整的例子

    之前的章节中了解了逻辑地址通过分段到线性地址的转换,线性地址通过分页到物理地址的转换,在这个章节讲以linux的一个内存寻址将之前的知识写成一个完整的例子.在学习的过程中,学习的知识不是很重要,我了解 ...

  • x86_64内存寻址 - 分页

    通过前面两个章节我们知道了实地址寻址和分段寻址,在这个章节我们将介绍分页寻址,在前面的文章我们知道,处理器访问内存的一个字节,首先将逻辑地址转换为线性地址,如果分页功能打开,那么线性地址会转换为最终的 ...

  • 实模式和保护模式区别及寻址方式

    转载自:http://blog.csdn.net/rosetta 64KB-4GB-64TB? 我记得大学的汇编课程.组成原理课里老师讲过实模式和保护模式的区别,在很多书本上也有谈及,无奈本人理解和感 ...

  • 自制 os 极简教程1:写一个操作系统有多难

    不知道正在阅读本文的你,是否是因为想自己动手写一个操作系统.我觉得可能每个程序员都有个操作系统梦,或许是想亲自动手写出来一个,或许是想彻底吃透操作系统的知识.不论是为了满足程序员们自带的成就感,还是为 ...

  • 自制 os 极简教程 2:史上最难的 hello world

    来自公众号:低并发编程 然后这一期重新做了这个系列的封面,显得高科技一点,嘿~ 前言 在上一篇<自制 os 极简教程 1:写一个操作系统有多难>中,以我的从小白到大白的入坑经历,聊了下我在 ...

  • 菜鸟记290-用数据透视表做统计之极简教程1

    万一您身边的盆友正好用得着呢 关键词:EXCEL2016:数据透视表:频数:频次:分组统计:操作难度*** 某次闲聊时候,有一位同事和小菜说看你的公众号,如果能写写如何统计就好了. 小菜以教学任务表为 ...

  • 菜鸟记291-用数据透视表做统计之极简教程2

    也许您身边的朋友用得着呢? 关键词:EXCEL2016:数据透视表:排序:分组统计:操作难度*** 还记得昨天小菜和您分享的统计极简教程吗? 请参阅<菜鸟记290-用数据透视表做统计之极简教程1 ...

  • 菜鸟记292-用数据透视表做统计之极简教程3

    也许您身边的朋友用得着呢? 关键词:EXCEL2016:数据透视表:标准差:方差:操作难度** 还记得前两天小菜和您分享的统计极简教程吗? 请参阅<菜鸟记290-用数据透视表做统计之极简教程1& ...

  • Typora极简教程

    Typora for windows 使用教程 1. 基本使用语法1.1 段落与字体1.1.1 段落1.1.2 字体1.1.3 分割线1.2 标题1.3 块元素1.3.1 代码块1.3.2 公式块1. ...

  • 实用 Nginx 极简教程,覆盖了常用场景

    更多干货等你发现! 作者:dunwu https://github.com/dunwu/nginx-tutorial 概述 什么是 Nginx? Nginx (engine x) 是一款轻量级的 We ...

  • 极简教程:怎么和家人一起看星星?

    文丨张是之 不得不承认,这里的文章有很多批评,但这种批评算不上是对生活的不满,更没有叹息社会之不公. 相反,我认识的经济学思考者,大都热爱生活,珍惜生活.我也一样. 正是源于对生活的热爱和珍惜,所有基 ...

  • 海竿极简线组,不仅信号灵敏还能双保险,学会了就是高手

    海竿极简线组,不仅信号灵敏还能双保险,学会了就是高手