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的值来确定,该信息将会被处理器所读取。含有这样信息的物件有:
描述符:其中包含一个名为描述符权限级的位,DPL
选择子:其中包含一个称为请求者权限级的位,RPL
处理器内部寄存器(其实就是那些段寄存器):记录当前权限级,CPL。一般来说CPL与当前处理器所执行的段有相同的DPL(因为段由选择子进行选取),CPL会受DPL所改变(这很自然哈哈哈)
为了去访问内存中的操作数,80386程序会把数据段的选择子装载到段寄存器,与此同时处理器将会检查对于数据段的访问权限是否合适。特别的,指令仅在目标段描述符的DPL高于或等于选择子RPL权限和段寄存器CPL的最大值的情况下,才能把数据段寄存器中的信息装载进来。值越大,权限越小。
换言之,程序只能访问与自己权限相同或者比自己还要低的数据。
DPL:段本身能被访问的特权级
CPL:当前程序的特权级
RPL:当前进程想要的请求权限,这个设置可以比较自由任意,更贴近于一个为了某些特殊需求所附加的设置条件
访问段时的权限判别如下所示:
堆栈段寄存器是个例外,它要求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
则是可以通过指令CLI
和STI
来关闭或者开启。这一类指令仅在CPL <= IOPL
之时才可以执行。否则,将会触发一个保护类型的中断。
此外,诸如POPF
等指令也会影响到IF
的启动与关闭。
中断与异常之间也存在着一定的优先级,如下图所示。
中断描述符表:
与GDT
和LDT
类似,IDT
是一个每个表项都是8字节这么大的一个表格。与上面提及的中断总数相印衬,一共有256个中断,对应的中断表中就不会有超过256个表项。
在物理地址中,中断描述符表的位置可以在任何一个地方,处理器有一个用于管理该数据结构的寄存器,即为IDTR
寄存器,它确实和GDTR
这样的寄存器很像,有一个Base
和一个Limit
来组成,长度也是6个字节。对于该寄存器的操作则是LIDT
和SIDT
指令,这是老生常谈的东东了。不过很自然,LIDT
不能随便用,只有在CPL
为0的时候才能操作。SIDT
则是把寄存器信息存放到内存中,相对便利一些,也不怎么需要权限。
上面的这张图现在看上去真的巨自然了。
对于中断描述符,其所主要有三种类型,分别是进程、中断门、陷门。其格式如下所示:
感觉可能用到的知识就这么多吧,剩下的看uCore的讲义即可。
处理流程也看上去不是那么难,通过IDT
顺延找到Base
地址,相加后找到处理中断的位置就好。(真这么简单吗?非也)
我们尝试对中断流程进行梳理:
利用
ID
找到处理中断与异常的GDT
或者LDT
表格,进而找到执行段内的异常或中断的函数地址,以处理中断。利用栈存放原函数的信息,以方便返回。如果是异常,则还会存放一个
Error Code
来实现。不管如何,CS
与EIP
是一定会存在栈内的,这很自然。但如果涉及权限的转换,还要存放进程相关的信息,如SS
和ESP
等。
中断会将
Trap Flag
,即TF
清空。通过该指令,在处理完中断且用IRET
指令返回之前,不会有另一个中断抢占其处理流程。中断不允许在转移后进入到一个权限更低的位置。
Last updated