[不是科普向?] RE: 从零开始的操作系统开发 第二集

[不是科普向?] RE: 从零开始的操作系统开发 第二集

[本文概念性内容较多, 看图党慎入]

概要

Hmm, 第一集中我们已经学会了如何让计算机在启动的时候加载并运行我们写好的bootsector, 并且通过使用BIOS提供的中断控制显卡在屏幕上输出特定的字符串, 慢慢的我们已经熟悉了16bit下的底层程序的开发, 不过就如我们综述中所说, 16bit下我们能够访问的内存十分有限, 因此我们不得不离开这个我们熟悉但是却不好用(qwq)的16bit模式,切换到32bit下, 切换之后我们就可以在C语言内继续我们的操作系统的开发了, 我们的引导程序实际上做的事情就是, 将模式切换到32位,并且将操作系统所在的磁盘扇区装载到内存里, 然后call我们的操作系统的main函数所在的地址, 这样, 整个引导程序的使命就完成了, 下面的工作就是操作系统的事情了, 本文我们主要介绍如何切换到32bit下的具体操作, 以及在32bit下控制显卡输出特定的字符

本文最后实现的效果的具体代码在github上均可获得 代码仓库地址见第一集

全局描述符表(Global Descriptor Table GDT)

我们已经熟悉了16bit下的内存的分段结构以及逻辑地址转换为内存的物理地址的方式, 现在再来回顾一下, 16位下我们想访问内存需要提供两个信息, 段地址+偏移量,  这两个信息决定了物理内存的地址, 决定的方法是 Segment Addr * 16 + Segment Offset  这个可以在16进制上形象的表示为 Segaddr << 4 + Offset, 那么现在我们要切换到 32bit保护模式下, 之前的这个内存的段-偏移量模型已经不适用了, 取而代之使用的是个功能更加强大的模型, 支持对内存的某个部分进行保护, 权限控制, 以及提供虚拟地址访问的内存模型. 另外,在CPU由实模式(16bit, 下略)切换到保护模式(32bit, 下略)下后, 将逻辑地址映射到物地址的方式也发生了改变, 这个改变, 和我们这个小标题提到的GDT密切相关, 下面我们就来具体的介绍一下 GDT

 

什么是GDT

GDT是由多个SD组成的一个数据结构, 每一个SD, 称作Segment Descriptor, 包含了一个段的各种元信息如下:

  1.  基地址(BaseAddress) 定义了这个段在内存中的物理的起始地址
  2.  段上限(Segment Limit)定义了段的大小
  3. 标志位数组, 这些标志位向CPU说明CPU应该如何处理这个段中的内容, 如本段只读, 或者本段可执行等, 相当于这个Segment的属性

每一个SD是一个8Byte长的数据结构, 通过64个bit存储了所有上述信息, SD的结构如下:

GDT_Entry

我们可以看出来比较反人类的地方是这个数据结构存储的不同类型的数据信息并不是连续的= = 推测是因为为了向下兼容才这么做的, 具体的原因为何还没有调查过, 从上述结构中, 如果我们想查询出这个SD对应的段的基地址, 我们需要将SD的16 — 31 ,32 — 39 56 — 63位拼接到一起, 得到基地址的值. 我们再来看一下这个结构, 这个结构中描述了 BaseAddr, Limit 还有Flags& AccessByte, 其中的Flags & AccessByte如下图所示

SD(本图出自 该链接)

这里的Flag的bit位比较多, 我们挑几个重要的介绍一下

L位是表示我们的内存使用64bit的代码段模式还是32bit, 这里我们使用32位, 因而此为置零,

DPL 指的是该段的代码拥有的CPU的权限级别,因为我们现在要使用程序直接操作CPU, 编写的操作系统也要操作CPU的很多特权指令, 因而该段的权限设置为 最高权限(ring 0, highest privilidge)

P 指的是这个段在内存中是否存在(?待确定)

具体每一个位表示什么意思的细节我们先不深究,想要了解的孩子可以看 “Intel® 64 and IA-32 Architectures Developer’s Manual: Vol. 3A”, 下面我们要使用汇编语言定义整个由DATA_SEG和CODE_SEG组成的GDT, 代码如下

我们定义了两个段, 而且这两个段是重叠在一起的, 基地址相同, 大小也相同, 只不过一个是代码段一个数据段, 为了简便起见, 我们目前只需要这两个段即可, 通过代码我们可以看出来, 在两个段的上面还有一个全为0的段, 是用来做保护的,防止用户使用未初始化过的段寄存器访问内存获得随机脏数据. 具体为何说这个是用来保护的, 我们下面马上就要介绍

 

保护模式下的逻辑地址和物理地址的对应关系

在继续之前, 我们先补充一个刚刚没有说到的一个问题, 即目前的逻辑和物理地址是如何对应的.

在切换到保护模式下之后, 我们要指定访问的内存需要给出的是Segment Index + 偏移量, 之前在段寄存器中存储的是段地址, 目前这个地址变为了Segement Index,所谓的Segment Index的意思其实就是, 我们要访问第几个段, 然后, CPU会将GDT中对应段的描述符取出来, 找到这个段的基地址(Base Address),以及获得它的各种状态位, 然后加上提供的偏移量,最后对应到物理地址.  下面我们来解释一下为何要设置64bit的0在GDT的开头, 因为, 如果错误的使用了某个未初始化的段寄存器, 这个段寄存器的内容为 0 的话, 如果不设置64bit的0的话, 就会默认去找GDT里的第一个Entry 也就是我们定义的CODE_SEG段,这样就会读到开发者不期待得到的数据, 而且因为这个行为是合法的, 允许访问第零段,不会报错, 但是, 这个访问对于用户来说是没有意义的,因而这个地方会对之后的程序造成不可估量的影响, 那么合理的解决办法是, 我们让第零段变为无意义的段, 这样只要访问第零段就会直接报错, 因而我们将第零段设为了64bit全为零, CPU在访问到这里的时候就会panic而终止掉下面的执行

 

切换到保护模式需要做的另一项工作

设置好了GDT之后, 我们要将这个表使用lgdt装载, 为了装载GDT,我们需要一个用来描述我们GDT的结构, 叫做GDT-descriptor, 这个描述符只存储两个信息, 即GDT的地址, 以及GDT的size – 1, 为什么是size – 1 而不是size大家可以思考一下. [实际上, 我们不会将GDT descriptor的size属性设置为0,那样表示指向一个空的GDT, 没有意义, 而且, GDT Descriptor 能够表示的GDT的大小在 0 — 65535之间, 如果我们的GDT大小是65536的话, 就无法表示, 这样我们就可以做个平移, 让 GDT存储大小的时候 0 表示 1 , 65535表示 65536.]

设置好这个GDT Descriptor之后, 我们将这个地址作为操作数, 传递给 lgdt指令, 就可以将GDT装载好了

 

以上是我们切换到保护模式下要做的最重要的一步,  切换到保护模式下还要做一些其他的操作, 首先就是 “关中断” , 保护模式下中断的实现方式和实模式下的完全不同, 如果我们还使用由BIOS提供的这套简单的中断(实际上是提供的中断向量表), 就会出现问题, 因而在我们自己设置好中断向量表之前, 不应该允许中断的发生

关闭中断之后, 我们就可以装载我们上边准备了好久的GDT了, 装载了GDT之后, CPU才知道保护模式下该如何寻址

然后, 打开保护模式切换的开关, 我们要将控制寄存器 CR0的最低位置为 1 , 开启保护模式.

开启保护模式之后, CPU流水线中的所有在执行, 等待执行的指令, 或者缓存的微指令,这些信息都是实模式下的逻辑处理的, 而我们现在切换为了保护模式, 为了保证不让这些缓存影响CPU的正常运行, 我们需要flush缓存 ,  为了flush缓存,我们需要执行一个long jump,注意,这里跳多远不重要, 重要的是”如何跳”, 我们使用long jump跳到当前指令的下一条也可以, 跳到比较远的地方也可以, 但是 使用short jump跳到比较远的地方, 也不会flush缓存, 这里主要是为了让long jump对缓存的flush起作用 , 同时为了方便起见,这个long jump我们一般就直接跳到32bit模式下的入口代码的地址了~

为了完成以上操作, 实现的代码如下

 

在在保护模式下对显卡进行操作

现在我们已经切换到保护模式下了, 很明显我们不能使用中断来操作显卡了, 不过幸运的是, 我们可以直接操作显卡对应的内存控制显示的内容, 它在内存中被映射到0xb80000, 我们每次写两个Byte, 第一个Byte是要显示的字符的ASCII码, 第二个Byte是字符的颜色和背景颜色, 然后就可以显示在屏幕上了, 向0xb8000开始的不同地址写入不同的内容会在屏幕的不同坐标出显示相应的内容, 这里给出显示满屏的@符号的代码

 

完整的代码

实现到此,我们这一次的任务就完成了, 完整的代码在github的repo可以获得, commit id 为  7e6abac774439e0c234a1fe5a40755f609db780a  执行make boot即可看到效果哦~

欢迎大家在评论区对文章进行吐槽或者评价, 这样我就能和大家交流学到更多的东西了~ 不是很开心的事情嘛

发表评论

电子邮件地址不会被公开。 必填项已用*标注

3 × 2 =

This site uses Akismet to reduce spam. Learn how your comment data is processed.