拼一个自己的操作系统(SnailOS 0.03的实现)

拼一个自己的操作系统 SnailOS 0.03的实现

拼一个自己的操作系统SnailOS0.03源代码-Linux文档类资源-CSDN下载

操作系统SnailOS学习拼一个自己的操作系统-Linux文档类资源-CSDN下载

SnailOS0.00-SnailOS0.00-其它文档类资源-CSDN下载

保护模式及其重新设置

 

https://pan.baidu.com/s/19tBHKyzOSKACX-mGxlvIeA?pwd=i889

看到这里,相信80x86的保护模式大家都应该听说过吧。是啊,为什们我们不象其他讲操作系统实践的书那样,在很早的时候就大讲特讲,intel体系结构中的保护模式呢?好了不卖关子了。原因其实也是很简单的因为我们已经进入了保护模式,而且grub已经为我们准备了全局描述符表以及表中的几个关键描述符了。现在就让我们用virtual box的调试模式看看这张表格吧,下面是调试器返回的信息,我们粘在了这里。

 

VBoxDbg> info gdt

Guest GDT (GCAddr=00000000000010b0 limit=20):

0010 - 0000ffff 00cf9b00 - base=00000000 limit=ffffffff dpl=0 CodeER Accessed Present Page 32-bit

0018 - 0000ffff 00cf9300 - base=00000000 limit=ffffffff dpl=0 DataRW Accessed Present Page 32-bit

 

我们用在command框中输入info gdt命令就会得到GDT的基地址,以及GDT的长度,接下来的两条信息就是两个描述符的信息了。同时,详细信息也告诉我们,两个段的选择符分别是0x0010和0x0018,接下来是描述符的16进制形式,基地址是0,段的长度是4G,描述符的特权级是0级,分别是已被访问可读可执行的代码段和已被访问能够被读写的数据段。

我知道,在没有讲解全局描述符表、描述符结构、已经全局描述符寄存器之前,跟大家说上述的东西,简直是让大家读天书一样,会丈二和尚摸不着头脑。不过话又说回来,那些有些难度的问题,早晚是要出现在笔者的书中的,还不如早说出来,一吐为快,让大家觉得也没有什么难度——根本不是什么难题就好了。下面我们就要说清楚。

为了保持兼容性,intel的老爷爷们,真的是没少下功夫,这不依然延续了,以前16位cpu的分段机制。而且在默认的情况下,cpu是处于实模式的。即使当我们进入了保护模式,分段机制仍然是我们不得不用的,这个没有什么选择权了。那么这次的分段机制还是有些不同的。为了描述一个段,设计了8字节的描述符结构,为了存储这些8字节的结构,设计了一个大数组连续存储这些描述符,即所谓的描述符表。那么表的起始地址从哪里得来呢?没有办法,只能从内存中得来,在内存得什么地方呢?哦,这是个好问题呦?这就是那些书上都不会讲得东西了。当然是我们操作系统自己指定了。言外之意是我们在编程的代码中指定。可是这么重要得结构放在内存中,cpu怎么就知道了呢?全局描述符表寄存器应运而生,在cpu中把它存储下来,为了这个设计者还专门得研究了两条指令,lgdr和sgdt,分别是加载内存中的描述符表的数据结构和填充覆盖这个数据结构的指令。这样大家明白了吧,描述符是详细说明一个段的数据结构,描述表是一个又很多这样的数据结构的大数组。数组的初始地址,在内存中,被gdtr加载后,供cpu使用。

而一个段描述符又有那些要说的呢?这里,我们只说段的描述符,而且是全局描述表中的。也就是说还有其他类型的描述符,还有其他类型的描述符表,这个大家一定要注意,那些负复杂的东西,还是等到用的时候在细细将来吧。说到段,这里的段与16位cpu的段并没有什么根本的区别,之所以分段,还是要让为我们编程者处理不同类型的地址所提供的功能。更深层次的含义是,我们没有必要总使用物理地址来编程,如果那样的话,4G字节的物理内存,我们不知道要记住多少的地址了。分段了以后,所有到的地址都是以段为起点,所以这样无论对于操作系统的编写者,还是应用程序的编写者都是福音吧!而且大家注意了,分段是随意的,并不是说这个段的地址与那个段的地址不能有重叠,而是说你分你的,我分我的,重叠就重叠,干扰就干扰,就看大家怎么用了,这不,上面的代码段和数据段基地址和段限长都是完全重合的,只不过属性中说了你是代码段,我是数据段。看来分段的目的并不是为了隔绝,而是另有一番想法了。正是这样,我们可以看到,代码段是可执行只读的,因此当我们试图向代码段中写入数据时,因为现在还没有异常处理的代码加入,所以,cpu会毫不留情的宕机。可是我们代码一点问题都没有啊,聪明的你通过上面的提示一定已经想到了,写入内存的操作当然是通过数据段的读写来操作的了。可是我们根本没有指定当前的代码段和数据段的代码呀。这其实还是grub的功劳了。在进入保护模式之前,这些烦心的工作都要不折不扣的做了,否则哈哈,否则就不可能做到这里了。

现在告诉大家也不晚吧,进入保护模式前,在grub中一定会有类似下面这样的指令组合。

mov ax,0x0018

mov ds, ax

mov es, ax

mov fs, ax

mov gs, ax

mov ss, ax

mov esp, stack

 

jmp dword 0x0010:kernel_start

上面的ax、ds、es、fs、gs等都是cpu中的寄存器,寄存器大家都知道吧,不知道也没有关系,cpu做运算可不会轻易使用内存这些低速的存储装置来操作,首先是用寄存器,但是这些内部存储单元,就那么几个,只是来回的复用,因此,又有片内缓存就是某些内存的镜像。

ds、es、fs、gs、ss分别就是段寄存器了,只不过这次,它们中装的不是段的基地址,而是描述符在描述表中的相对地址。这个地址我们怎么算来呢?当然是用减法了,就是用某个描述表项的逻辑地址减去描述符表的初始逻辑地址。为什么又不是简单的物理地址了呢?这还要往前说,在16位模式我们已经使用那时的分段机制了,因此它并不等价于物理地址的减法,很可能是两个逻辑地址的减法,当然如果段的基地址是从0开始的,那物理地址和逻辑地址也就一样了。

终于到了该详细的讲解描述符这个数据结构的时候了。不过这个东西还是蛮枯燥的,真的是让人头痛的地方呀。

3305e088d66852ef61f0bed2e0ea6e28.png

 

图中清晰的展示了段描述符的每一个细节,并且精细到了每一位。然而,我们的大脑却不清晰了。仔细看来,段基地址被拆成了三部分,段限长被拆成了两部分,还有很多莫名奇妙的属性位。本来很整齐的描述符为什么要搞得如此复杂,以致于我们刚刚厘清的头绪一下子又乱作一团。

谁说不是呢?Intel的老爷爷们为了兼容性,把简单的东西搞得如此复杂,也不得不教人佩服地五体投地了。好了,光是心烦意乱也于事无补,无奈之下,我们还是要详细地看看每个属性代表的意义。

让我们从高处的位开始说起吧。属性所在的那个双字中,第23位是G位,被称为粒度位。这真是个无厘头的名称呀,这和粒度有什么关系。但当你看到段限长的位数时你就会有那么一点想法了吧,段的最大长度可以是4G段限长居然是20位的,这又是在搞什么鬼?熟悉16位编程的你,一定想到了什么吧,那时候段寄存器的长度是16位的却能够访问20位的物理地址。现在段限长是20位,又要表示长度位4G的段。作弊,他们每次都在作弊吗!这个粒度位就给它们作弊创造了条件,当该位为0时,段限长的数值就乘以1字节;而为1时,段限长的数值就要乘以4k字节。想必大家现在像清楚了吧,intel的老爷爷们是真会省啊!D/B位是一个不好解释的问题,首先说这个位之所以有两个名字的原因,是因为在该段作为代码段或是数据段时,该位的含义有明显的不同。

这里要补充一点的时,在全局描述符表中,从大类上来说,可以有两种不同的描述符,一种是系统段描述符,另一种就是我们现在讲的非系统段描述符。而非系统段描述符就是我们现在讲的代码段和数据段的描述符。

好的,让我们接着非系统段的代码段和数据段的描述符来说一说D/B这一位吧。如果是代码段这一位就应该称之为D位,D位为0是处理器认为该代码段中的偏移地址或操作数是16位的,而为1时,认为是32位的。如果是数据段这一位则应称之为B为,该段是可以作为程序运行中的栈使用的,如果为0,像call、push、pop、ret等可以操作栈的指令对该段进行操作时用的是sp指针,否则就会用32位的esp指针。

L位是预留给64位CPU使用的64位代码标志,虽然我们现在用的绝大多数CPU都是64位的,但是我们为了简单起见,仍然是用32位指令,所以,该位应该被简单的置0就好了。

AVL是一个预留给软件使用的位,如果你的操作系统软件对段有什么特殊的要求,是可以用该位来标记的。

P为是段的存在位,显然我们应该把正在使用的段描述的该位置为1。否则处理器会产生异常,当然现在的我们还没有处理异常的等代码存在,所以处理器会宕机。

DPL这两位是描述符的特权级,特权级3到0依次升高,关于特权级的对段的访问,由于过于复杂我们还是在用到的时候再讲吧。

S是指定描述符大类的标志,就是我们上面所说的系统段描述或非系统段描述符,为0时表示系统段描述符,否则为1时表示非系统段描述符,也就是我们这里的代码段或数据段描述符。

TYPE这四位正是用于对描述符的进一步分类,也就是到了这里我们才把非系统段区分为代码段或是数据段,这是通过TYPE的最高位来实现的,该位为0,则是数据段,为1则是代码段。根据此位的不同,接下来紧挨着的3位又会有不同含义。如果被TYPE最高位(置0)确定了数据段的地位,则次高位表示段的读写方向,正常情况下,我们的数据当然是从低地址往高地址的读写顺序为妙,可是如果是纯堆栈段就是向下生长的,因此,这里可能特指定义了一个纯正的堆栈段供程序使用。次高位为0是一般的数据段,为1是向下读写的数据段(堆栈段),不过我觉得很少有人专门设立堆栈段。而是通常是把堆栈指针esp指向普通数据段的高处就好了。TYPE中次高位后面的1位是可读写标志,这意味着该位置0时,是不可以写这个数据段的,但是可读,只有置1了才能够又读又写该数据段。最后一位是访问与否标志,由处理器维护,通常用于统计该段的使用频次,一般的,我们在初始时置0,表示该段未被访问过。

当TYPE的最高位置1时,该段就是代码段,次高位的涵义由此变成了一致代码段和非一致代码段,这个问题比较深奥,还是等将来在讲吧。而次高位之后的位代表是否代码可读。为0时表示该段不可以像数据一样被读出,为1时则表示可以读出。至于是否可以写入,相信大家没有异议吧,代码段在保护模式下,始终时认为不可写入的。如果你想要写入,则必须定义一个重合的数据段来操作。而最后一位与数据段的最后一位完全相同。

也许你差不多忘了吧,系统还新增了一个全局描述表寄存器了,它可是装着重要的东西,也就是全局描述符表的物理基地址以及长度哦。奇怪的是,在32位体系结构中它是一个6字节的装置,他获得物理地址的方式是读取内存,也就是说,在开始16位实模式时也用不到该表,如果没有全局描述符表,我们就必须事先准备好一个在内存中的描述结构供加载全局描述符表的指令使用,我们把这个6字节的结构称之为伪寄存器描述符,够绕嘴的吧。该描述符的低2字节是全局描述符表的长度,高4字节是全局描述符表的物理基地址。长度为2字节,大家是不是想到了什么。对头,晓得,也就是说最多可以装8192(2的16次幂再除以8)个描述符。

哦对了,最后的最后还有一个段选择符的事情。下面的是选择符的结构。

8af3a03b70d472e1290f28f586d02d80.png

 

选择符在很多书上称为选择子,不过我习惯称之为选择符。这里重点说的就是描述符的偏移地址,书上大多都称之为描述符索引,但我认为,该数值已经天然的左移了三位(乘以8),所以就是一个偏移地址吧。TI位是表示的是在全局描述符表中还是在局部描述表中的描述符,由于我们在今后始终不用局部描述符所以,该位始终为0,即是全局描述符表中的描述符。RPL被成为请求特权级,关于这个,现在用不着,这两位都置0好了,表示在最高特权级0下工作。

哎呀妈呀,可算是拽完了,真的是又臭又长啊。讲到这里,初次接触保护模式的你,一定会有一种感觉,那就是——也太特么难了吧。如果没有,恭喜你,天才呀,稍加努力,以后一定是顺风顺水了。

如果你脸有难色,又非常郁闷的话,让我来告诉你一个好消息吧。虽然咱们讲的挺热闹,甚至是口水满天飞。但是编起程序来,也就那么简单的几十行代码了。在这里我们还是新添加了sysasm.asm、gdt_idt_init.h、gdt_idt_init.c三个文件,还是请看下面吧!

【system.asm】

; system.asm 创建者:至强 创建时间:2022年8月

bits 32

 

global _lgdtr, _reset_gdt

 

; 下面的函数是加载伪寄存器描述符到全局描述表寄存器中的函数

align 16

_lgdtr: ; void lgdtr(struct gdtr* pgdtr)

; 由于没有对此时的堆栈进行任何操作,所以当前栈指针

; 指向函数的返回地址。栈指针加4后,则指向第一个参数

; 这个参数正是伪寄存器描述符的地址。

mov eax, [esp + 1 * 4]

lgdt [eax]

ret

 

align 16

_reset_gdt: ; void reset_gdt(void)

; 通过段选择符来更新各段寄存器的内容,0x10是全局描述

; 表中的第三个选择符,所有的数据段都更新为该段。由于

; intel处理器的"怪癖",往段寄存器中写入数据,必须借助

; 其他的寄存器,这里按照惯例使用了ax。还是"怪癖",更新

; cs(代码段寄存器)不能使用mov指令,而要jmp指令,并且

; 采用了双字修饰符形式的段间跳转,双字形式不是必须的,但段

; 间跳转是必须的,因为只有这样处理器才会更新cs。在32位

; 模式下,nasm会把不带双字修饰符的段间跳转指令默认汇编成

; 正确的格式。

mov ax, 0x10

mov ds, ax

mov es, ax

mov fs, ax

mov gs, ax

mov ss, ax

jmp dword 0x08 : .1

.1:

ret

 

【gdt_idt_init.h】

// gdt_idt_init.h 创建者:至强 创建时间:2022年8月

#ifndef __GDT_IDT_INIT

#define __GDT_IDT_INIT

 

// 定义了全局描述符表寄存器的伪描述符,并且告诉编译器

// 尊重我们对结构的定义,不要做任何尺寸的改动。

struct gdtr {

unsigned short limit;

unsigned int base;

}__attribute__((packed));

 

void lgdtr(struct gdtr* pgdtr);

void reset_gdt(void);

void create_gdt_desc(unsigned short gdt_nr, unsigned int base,

unsigned short attr, unsigned int limit);

void gdt_init(void);

 

#endif

 

【gdt_idt_init.c】

// gdt_idt_init.c 创建者:至强 创建时间:2022年8月

#include "gdt_idt_init.h"

 

// 构造一个全局描述符的函数,该函数需要4个参数,

// 一是描述符在全局描述符表中的下标,二是描述符所描述段

// 的基地址,三是段的各种属性,四是段限长。

void create_gdt_desc(unsigned short gdt_nr, unsigned int base,

unsigned short attr, unsigned int limit) {

// 凭感觉确定的全局描述表的基地址。 、

unsigned long long* gdt_start = (unsigned long long*)(0x6000);

unsigned long long base_low = base & 0x000000000000ffff;

unsigned long long base_mid = base & 0x0000000000ff0000;

unsigned long long base_high = base & 0x00000000ff000000;

unsigned long long limit_low = limit & 0x000000000000ffff;

unsigned long long limit_high = limit & 0x00000000000f0000;

unsigned long long attrib = attr & 0x000000000000f0ff;

gdt_start[gdt_nr] = limit_low | (limit_high << 32) |

(base_low << 16) | (base_mid << 16) | (base_high << 32) |

(attrib << 40);

}

 

void gdt_init(void) {

struct gdtr gdtr_;

int i;

// 与上边函数的基地址相对应。

gdtr_.base = 0x6000;

gdtr_.limit = 256 * 8 - 1;

 

// 全局描述表中第一个描述符必须初始化为0

create_gdt_desc(0, 0, 0, 0);

// 0x08是全局描述符表中第一个描述符的选择符(从0开始计算)

create_gdt_desc(1, 0x0, 0xc09a, 0xffffffff); //代码段

create_gdt_desc(2, 0x0, 0xc092, 0xffffffff); //数据段

// 其余预留的描述符空间目前全部初始化为0。

for(i = 3; i < 256; i++) {

create_gdt_desc(i, 0, 0, 0);

}

// 重新加载全局描述符表寄存器。

lgdtr(&gdtr_);

// 更新内核所用的各段地址,其实就是说说罢了。我们虽然更新

// 了,但是更新后的段跟grub甚至的基地址以及段限长,完全一样

// 的。同时这个函数的名字,也是文不对题,暂且就这样吧。

reset_gdt();

}

 

相信通过上述代码的注释以及之前对于保护模式下全局描述符表的有关解释,大家应该可以初步的了解这些设置的作用了。哦,对了除了上述代码,大家还要在kernel.c中加入相应的头文件以及函数调用。下面就让我们通过virtual box的调试功能进一步了解一下,到底我们做了些什么。下面是调试器返回的最新的全局描述符表的信息。

VBoxDbg> info gdt

Guest GDT (GCAddr=0000000000006000 limit=7ff):

0008 - 0000ffff 00cf9b00 - base=00000000 limit=ffffffff dpl=0 CodeER Accessed Present Page 32-bit

0010 - 0000ffff 00cf9300 - base=00000000 limit=ffffffff dpl=0 DataRW Accessed Present Page 32-bit

通过和上面同样的信息来对比,大家发现了吧,全局描述符表的基地址已经从0x10b0变成了我们凭感觉自定义的0x6000了,段限长也从原来的0x20变为0x7ff。这说明我们的操作是成功的。

 

到了这里大家可能有一个问题要问了,为什么我们在创建描述符的函数中把段属性设置为0xc09a(用代码段举例,数据段也一样的),到了调试信息中却变成了0xcf9b了呢?如果你有真个问题说明你非常认真仔细的对比了代码和调试信息,真是辛苦了。那就要对照描述结构一位一位的抠出个所以然了。咱们先说我们的设置,0xc09a的二进制形式是:

1100_0000_1001_1010 B(可以通过windows自带的计算器获得,也可以用咱们提供的printf_()函数在信息去打印出来。)通过对照描述符结构你会发现,该段是一个4k粒度的、指令和操作数长度为32位的、存在的、特权级为0、非系统段的、非一致的、可读和执行的、未被访问过的代码段。而0xcf9b的二进制形式是:1100_1111_1001_1011 B,与上面的比起来,第一个下划线后面的四位全部变成了1,也是对照描述符结构,这里原来是段限长的高4位,不属于属性位,之所以后来变成了0xf,相信大家已经知道了吧,因为我们的段限长设置的就是4G-1(从零开始计算),所以这里必须是全部为1。再一个变化就是最后一位(从最表高位开始计算)变成了1,对照一下,恍然大悟吧,这是由处理器维护的已访问位,显然程序运行到这里该段已经被访问无数次了。

 

总结......保护模式中即使是最原始的部分对我们初学来说,也是有着极大的挑战性的,大家要是想继续的话,一定要坚持呀,坚持就是胜利。翻过了这座山,跨过了那条河,美景才能在心中留下深刻地印象,不经历风雨怎么见彩虹吗?

 

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

weixin_39410618

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值