Project 5: ucore lab1

记录uCore的笔记

前置知识:

处理器硬件:

intel 80386:

该硬件拥有四种运行模式,分别为:实模式、保护模式、SMM模式和虚拟8086模式。

在80386机器启动时,其一开始会运行在实模式之中,此时能访问的空间为20-bit,即1MB。在处理了基本的操作后,机器将会进入保护模式。保护模式需要在实模式完成控制寄存器初始化和页表初始化后,通过设置CR0寄存器才能进入。

保护模式拥有很多强大的功能,如虚存与页表机制,以及优先级机制。其还包含0~3四个特权级别,而OS往往运行在最高的特权级0上。

为了满足兼容性需求,Intel 80386允许虚存的页机制和段机制共存,因此在该机器中往往存在三个地址:物理地址,线性地址和逻辑地址。

下面展示地址的对应关系:

仅启动分段机制:
           ___________
逻辑地址 -> |   段机制  | -> 线性地址=物理地址

同时启动分段机制和分页机制:
           ___________                ___________
逻辑地址 -> |   段机制  | -> 线性地址 -> |   页机制  | -> 物理地址

第三讲课堂笔记:

BIOS启动后发生了什么:

  • 将加载程序加载磁盘引导扇区到0x7c00

  • 加载程序:将操作系统代码的代码和数据从硬盘加到内存中

  • 跳转到操作系统的起始位置

为什么不让BIOS直接读取操作系统镜像到内存中去?

因为磁盘的文件系统是可能不一样的,BIOS不太可能认得了全部磁盘内的文件系统格式。但如果是同一个磁盘对应的加载程序来干这事就能解决了,因为他们都是原装配套的。

BIOS启动流程:

加电后BIOS自0xffff0启动 -> BIOS内硬件POST自检 -> 为读入多分区利用主引导记录MBR格式(检查分区表正确性)-> 跳转到活动分区

加载程序:

加载可选系统内核参数 -> 依据配置文件跳转到内核执行

在BIOS启动时存在着大量的脏活和统一约定规范,因此其实这里的细节很难顶。

中断与系统调用:

系统调用:应用程序主动向内核发送服务请求。-> 异步或者同步

异常:非法指令或者其他原因导致当前指令执行失败后的处理请求。-> 当前指令需要处理完,所以同步

中断:来自硬件设备的处理请求。-> 异步,因为硬件不知道上层的结构。

中断的处理流程:

  • 硬件:在CPU初始化时,设置中断使能标志。

    • 根据内部或者外部事件设置中断标志

    • 根据中断向量调用相应中断服务例程

  • 软件:保存现场 -> 处理中断服务 -> 清除中断标记 -> 恢复现场

系统调用:与函数调用相比

  • INT和IRET -> 系统调用,涉及堆栈切换和特权级的转换

  • CALL和RET -> 常规调用,常规调用时没有堆栈切换

第四讲课堂笔记略过:

陈渝老师的课讲的不错,不过作为学生,课下还是多看一下相关的参考资料。在uCore的文档之中也有较为详尽的资料可供参考,相对于6.828来说学习体验似乎顺滑了不少。

控制寄存器CR0

进入保护模式需要操作控制寄存器CR0,我们简单过一下这边的知识点。

首先,控制寄存器仅在取得Ring 0权限下才可以进行读取和写入的操作,且这些寄存器有部分特殊的位是始终需要维持0以提供一些简单的安全检查。

更进一步,CR0寄存器是控制寄存器中控制操作系统模式和处理器状态的寄存器,在Boot阶段发挥着模式切换的功效,因此它的内部结构需要做简单的研究和探讨。

其中的一些位分别能实现页表机制,缓存机制等等功能,在BOOT阶段用到的其实只有CR0.PE这个位置。在PE位被设置为1的时候,系统将会启动保护模式。反之,系统将会启动实模式。

补充细节:RTFM Intel 80386. (随着进度更新)

指导书推荐看一下部分章节,于是简要在此做一些摘记。

第四章:系统结构

在386机器中存在五类寄存器:

  • EFLAGS

    • IF:中断标志,该标志的设置能让CPU辨认出外部的中断请求

    • NT:嵌套任务标志,该标志影响IRET指令的工作

    • RF:恢复标志,可以用于关闭debug产生的异常

    • TF:内陷标志,开启该功能后CPU能在每执行一条指令的同时产生异常,用以更方便去进行调试

    • VM:虚拟8086机模式,可以用于让机器运行在虚拟的8086模式之中

  • 内存管理寄存器:该类别中存在四类寄存器

    • GDTR:全局描述符表寄存器,指向段描述符表格GDT

    • LDTR:局部描述符表寄存器,指向局部描述符表格LDT

    • IDTR:中断描述符表寄存器,指向中断处理这IDT

    • TR:任务寄存器,指向当前处理器完成任务所需要的信息

  • 控制寄存器:控制寄存器仅能通过general Registers,即一般寄存器进行赋值。

    • 控制寄存器即CR0,CR1,CR2,CR3

    • CR0寄存器:系统控制flags存储者

      • EM:Emulation,用于指示协处理器是否被模拟出来了

      • ET:扩展类型,同样与协处理器相关

      • MP:Math Present,控制Wait指令,同样与协处理器相关

      • PE:Protection Enable,是否开了保护模式

      • PG:是否采用页表方式进行地址映射

      • TS:与协处理器相关,现代用语为进程切换

    • CR2寄存器:在页表被设置时,用于处理页错误

    • CR3寄存器:在页表被设置时用于找到当前进程的Page Table Directory位置

  • 调试寄存器:用于提供在不修改代码段的情况下实现指令级别的断点设置

  • 测试寄存器:用于检查TLB表的可信性,作它们的测试所用

第五章:内存布局

80386相比于8086,添加了页寄存器的机制。更细致一些来说,在很多时候允许段页两个机制的共存。因此我们可以看到如下的转换图。

逻辑地址进行段转换得到进一步的地址。如果没有在CR0中启动页机制,则直接作为物理地址。反之,将先获得线性地址,通过页转换后再得到物理地址。

段地址转换:

段地址转换的总体流程图如下所示:

可以看到:首先机器使用了Selector来选择描述符表格内的选项,得到基地址后再与偏移量进行相加的操作,从而得到线性地址。

段描述符会给处理器提供映射逻辑地址到线性地址所需的数据,这一过程对于程序员来说是透明的。段描述符的格式如下所示:

段描述符的格式:

  • Base:表示段描述符在4GB内存中的逻辑地址,是基地址

  • Limit:定义了段的大小,有两种单位格式,一种是以1 Byte为单位,另一种是以4 KB为单位

  • Granularity位:决定Limit的单位,如果为0,则以1 Byte为单位,反之则以4 KB为单位

  • Type:用以区分不同的描述符

  • DPL:描述符权限等级,用以描述不同的权限等级,在保护模式中会提及

  • Segement Present位:即P位,如果为0,则说明该描述符在地址转换中并不合法,当该描述符被装载到段寄存器,处理器会触发异常。但操作系统可以自如地使用这样的段描述符,且在线性地址空间在页机制未开启的情况下,以及段不存在于内存的情况下,操作系统可以设置该位为0

  • Accesss位:当段被访问后,即描述符被装载到寄存器中时,处理器将会设置该位为1,表示以前访问过

段描述符表:

段描述符将会存放在GDT和LDT两种类型的描述符表中,GDT是全局描述符表,而LDT是局部描述符表。描述符表即为存放8 Byte大小的段描述符的一个数组,最多可以存放8192个描述符,但是往往这些描述符表不会被装满,且第一个项(即Index = 0)不会装载数据。

GDT表格和当前的LDT表格均会存放在内存之中,利用GDTR和LDTR寄存器实现索引的功能。GDTR和LDTR寄存器存放了表格的基地址和段地址limit大小。

这两个寄存器和其余相关的内存布局寄存器的格式在新Intel手册中有提及。

现在我们并不关心出了Base Address与Table Limit以外的表项,故姑且不作更多解释。

选择子:

选择子用于确定描述符,其格式如下所示:

  • Index:用于挑选表中8192个描述符

  • Table Indicator:用于选择GDT亦或是当前的LDT,GDT用1来选取

  • RPL:在保护模式中使用,表示选择子当前的权限等级

因为一般情况下第一个表项不会得到使用,因此选择子如果指向第一个表项,即INDEX=0,则可视作NULL,这一特性在初始化选择子这一需求上能得到应用。

段寄存器:

386机器中的段寄存器是兼容了8086的产物,由于其也需要应用在实模式上,它的格式和我们想象的会有点不一样(特指与王爽书中的不同之处)。

在这里,每个段寄存器均有可见部分和不可见部分,格式如下所示:

可见部分是大家喜闻乐见的16-bit选择子,在实模式中他可以直接拿去构造20-bit大小,即1 MB的物理地址。而不可见部分则是需要通过处理器自身来获取的,它将会利用可见部分从描述符表中读取真正的Base address,存放到不可见部分,并利用不可见部分去构造真正的线性地址。

页地址转换:

页地址仅在CR0寄存器中的PG位被设置成1的时候才会发挥功效。一个页的大小一般为固定的4 KB,而在386机器中的每个页对应的是一块线性地址。如下所示:

386机器中采用了二级页表,利用DIR和PAGE部分,作为页表的INDEX,分别进行页的检索,从这里我们也可以侧面看出页表应该共有1 KB个项。

完成前述索引可以找到我们想要的页,之后就可以利用OFFSET实现页内4 KB单元的检索,从而获取到相关的数据。12位恰好是4 KB,算一算正好可以覆盖全部的地址空间,所以这个设计是一个自洽的设计。

对应的原理图如下所示:

第一级的页表也被称为Page Directory,其基地址被存放在CR3寄存器中,该寄存器也被称为Page Directory Base Register (PDBR)。CR3的改变取决于进程的转换与调度。

页表项

页表项就是页表中存储的一个项。

  • PAGE FRAME ADDRESS:页所在的物理地址,如果是第一级页表项的这一部分,则为第二级页表的基地址。

  • P位表示这个页表项是否被用于地址转换中,如果最后一位为0,则该表项的其余部分均可以给软件直接拿去使用。

  • D位,即脏位,CPU会在相关的页表项中写这个位表示已经被访问过。下次操作系统读写时会根据该位确定是否把脏数据写回。

  • R/W与U/S位,在保护模式下才能发挥功效,

机制与模式:

好像很简单...那就暂时不谈了捏。

Summarized with a graph that can be easy to remember.

第六章:

保护模式用于检测与甄别漏洞,在提高系统鲁棒性上也有很好的帮助。

根据第五章和第四章的介绍,我们注意到保护位在选择子和描述符上都有体现,这也意味着在保护模式开启后系统不仅在装载内存,同样也会开始对于安全性进行检查。

Type Check:

在描述符上还拥有读写type位,其可以用于帮助用户判断某些内存信息是否自己想要的,判断是否出现了恶意使用的情况。

Limit Check:

根据描述符上的G,即粒度位来进行检查。

Privilege Level:

权限通过设置0~3的值来确定,该信息将会被处理器所读取。含有这样信息的物件有:

  1. 描述符:其中包含一个名为描述符权限级的位,DPL

  2. 选择子:其中包含一个称为请求者权限级的位,RPL

  3. 处理器内部寄存器(其实就是那些段寄存器):记录当前权限级,CPL。一般来说CPL与当前处理器所执行的段有相同的DPL(因为段由选择子进行选取),CPL会受DPL所改变(这很自然哈哈哈)

为了去访问内存中的操作数,80386程序会把数据段的选择子装载到段寄存器,与此同时处理器将会检查对于数据段的访问权限是否合适。特别的,指令仅在目标段描述符的DPL高于或等于选择子RPL权限和段寄存器CPL的最大值的情况下,才能把数据段寄存器中的信息装载进来。值越大,权限越小。

换言之,程序只能访问与自己权限相同或者比自己还要低的数据。

DPL:段本身能被访问的特权级

CPL:当前程序的特权级

RPL:当前进程想要的请求权限,这个设置可以比较自由任意,更贴近于一个为了某些特殊需求所附加的设置条件

访问段时的权限判别如下所示:

DPLMax(RPL,CPL)DPL \ge Max(RPL, CPL)

堆栈段寄存器是个例外,它要求CPL,RPL和DPL这3个值必须完全一致,才可以被加载。

第八章:

在第八章后半段对于IOPL进行了一系列的介绍。

IOPL对应了一系列与I/O权限相关的指令。如果CPL小于等于IOPL,这一部分敏感指令则会被允许执行。

每一个进程都拥有自己的flags registers,对应的也会有各自的IOPL。进程可以通过POPF来实现IOPL的改变,但必须得在CPL = 0的情况下才可以执行。进程也可以利用这条指令修改IF,但必须权限高于IOPL,虽然不会报错,但不会有任何结果。(其实这也是一个遗留问题,导致虚拟化不方便)

第九章:

该章节主要是在讲述中断与异常的信息。中断与异常之间的区别是,前者一般用于处理计算机外设所触发的异步事件,而后者则是处理由处理器自身执行指令时所触发的异常。

中断类型:可屏蔽中断(发送给INTR接口),不可屏蔽中断(发送给Non-Maskable Interrupt接口)。

异常:由处理器检测到的错误与内陷等行为,代码内手动写的INT N信息。

中断分辨:

在中断的前32位,0-31是不可屏蔽的中断,而后续的一些数字,直到255,则是可以被屏蔽的中断。对于中断,作细分其实还可以分为三类:错误、内陷(Traps)、放弃退出(aborts)。

中断启动与关闭:

对于NMI,如果当前有一个不可屏蔽的中断存在,其会屏蔽下一个不可屏蔽的中断,直到下一条IRET指令被执行。

对于INTR,即可屏蔽的中断,IF能够控制外部中断信号。当IF=0时,中断将会被抑制。当IF=1时,中断是可以运行的。IF则是可以通过指令CLISTI来关闭或者开启。这一类指令仅在CPL <= IOPL之时才可以执行。否则,将会触发一个保护类型的中断。

此外,诸如POPF等指令也会影响到IF的启动与关闭。

中断与异常之间也存在着一定的优先级,如下图所示。

中断描述符表:

GDTLDT类似,IDT是一个每个表项都是8字节这么大的一个表格。与上面提及的中断总数相印衬,一共有256个中断,对应的中断表中就不会有超过256个表项。

在物理地址中,中断描述符表的位置可以在任何一个地方,处理器有一个用于管理该数据结构的寄存器,即为IDTR寄存器,它确实和GDTR这样的寄存器很像,有一个Base和一个Limit来组成,长度也是6个字节。对于该寄存器的操作则是LIDTSIDT指令,这是老生常谈的东东了。不过很自然,LIDT不能随便用,只有在CPL为0的时候才能操作。SIDT则是把寄存器信息存放到内存中,相对便利一些,也不怎么需要权限。

上面的这张图现在看上去真的巨自然了。

对于中断描述符,其所主要有三种类型,分别是进程、中断门、陷门。其格式如下所示:

感觉可能用到的知识就这么多吧,剩下的看uCore的讲义即可。

处理流程也看上去不是那么难,通过IDT顺延找到Base地址,相加后找到处理中断的位置就好。(真这么简单吗?非也)

我们尝试对中断流程进行梳理:

  • 利用ID找到处理中断与异常的GDT或者LDT表格,进而找到执行段内的异常或中断的函数地址,以处理中断。

  • 利用栈存放原函数的信息,以方便返回。如果是异常,则还会存放一个Error Code来实现。不管如何,CSEIP是一定会存在栈内的,这很自然。但如果涉及权限的转换,还要存放进程相关的信息,如SSESP等。

  • 中断会将Trap Flag,即TF清空。通过该指令,在处理完中断且用IRET指令返回之前,不会有另一个中断抢占其处理流程。

  • 中断不允许在转移后进入到一个权限更低的位置。

Last updated