主要姓名:冯成 学号:19021221183 学院:电子工程学院
【嵌牛导读】
操作系统是一个重要的研究和应用领域,基于Linux,Windows,Mac的操作系统已经占据绝对市场地位。嵌入式与桌面端不同,其具有功耗低,成本低的特点,广泛应用于日常生活中。由于目前嵌入式还没有统一的,没有绝对优势的核心系统,在不同领域都有一定的专业应用。本文以X86架构为例,讲解一个操作系统启动的过程,这是理解操作系统的基础。
在本文中,我们主要基于清华大学操作系统UCore的lab1对理论基础进行说明。我们实验基于Ubuntu20.04和qemu模拟器。
1. 环境配置
执行sudo apt-get install qemu-system,安装qemu程序,为执行uCore做准备
下载该github上的master分支(注意默认分支不是master分支)的uCore代码,解压使用。
2. BIOS中断、DOS中断、Linux中断的区别
BIOS和DOS都存在于实模式下,由它们建立的中断调用都是建立在中断向量表(Interrupt Vector Table,IVT)中的,都是通过软中断指令 int 中断号来调用。
BIOS 中断调用的主要功能是提供了硬件访问的方法,该方法使对硬件的操作变得简单易行。
DOS 是运行在实模式下的,故其建立的中断调用也建立在中断向量表中,只不过其中断向量号和BIOS的不能冲突。
Linux 内核是在进入保护模式后才建立中断例程的,不过在保护模式下,中断向量表已经不存在了,取而代之的是中断描述符表(Interrupt Descriptor Table,IDT)。Linux 的系统调用和DOS中断调用类似,不过Linux是通过int 0x80指令进入一个中断程序后再根据eax寄存器的值来调用不同的子功能函数的。
3. 操作系统如何识别文件系统
各分区都有超级块,一般位于本分区的第2个扇区。超级块里面记录了此分区的信息,其中就有文件系统的魔数,一种文件系统对应一个魔数,通过比较即可得知文件系统类型。
4. CPU的实模式(重要)
CPU中的寄存器分为两大类:程序可见寄存器(例如通用寄存器、段寄存器)和程序不可见寄存器(例如中断描述符寄存器IDTR)。
实模式的主要特性是:程序用到的地址都是真实的物理地址。同时,实模式下的地址寻址空间只有1MB(20bit)
从intel 80386开始的CPU,只要进入实模式,地址寻址空间就限制在1MB。
实模式下的地址计算方式为16*段寄存器值+段内偏移地址,其CPU寻址方式为
a. CPU实模式下的1MB内存
CPU初始状态为16位实模式,在实模式下只能访问1MB(20bits)内存。而硬件工程师将1MB的内存空间分成多个部分。
其中地址0-0x9ffff的640KB内存是DRAM,即插在主板上的内存条。
顶部0xf0000-0xfffff的64KB内存是ROM,存放BIOS代码。
BIOS检测并初始化硬件,同时建立中断向量表,并保证能运行一些基本硬件的IO操作
CPU中,插在主板上的物理内存并不是眼中“全部的内存”。地址总线宽度决定可以访问的内存空间大小。
并不是只有插在主板上的内存条需要通过地址总线访问,还有一些外设同样是需要通过地址总线来访问。
故地址总线上会提前预留出来一些地址空间给这些外设,其余的可用地址再指向DRAM。
5. CPU的分段机制(重要)
a. 内存访问为什么要分段
以前程序都是直接访问物理内存,所以编译出的两个程序如果内存冲突,则无法同时运行。
CPU采用“段基址+段内偏移地址”的方式来访问任意内存。好处是程序可以重定位,可以执行多个程序。
加载用户程序时,只要将整个段的内容复制到新的位置,再将段基址寄存器中的地址改成该地址,程序便可准确无误地运行,因为程序中用的是段内偏移地址。
改变段基址,通过在内存中一个段来回挪位置的方式可以访问到任意内存位置。程序分段可以将大内存分成可以访问的小段,访问到所有内存。
通过分段,在早期CPU实模式16位寄存器的情况下,计算段基址 << 4 + 段内偏移地址,即可访问到20位地址空间。
代码中的分段与CPU的分段不同。编译器负责挑选出数据具备的属性,从而根据属性将程序片段分类,比如划分出了只读属性的代码段和可写属性的数据段。编译器并没有让段具备某种属性,对于代码段,编译器只是将代码归类到一起,并没有为代码段添加额外的信息。
在实模式下,段基址直接写在段寄存器中;而在保护模式下,段寄存器中的不再是段基址,而是段选择子。
分段机涉及4个关键内容:逻辑地址、段描述符(描述段的属性)、段描述符表(包含多个段描述符的“数组”)、段选择子(段寄存器,用于定位段描述符表中表项的索引)。只有在保护模式下才能使用分段存储管理机制。
b. 将逻辑地址转换为物理地址的两步操作
逻辑地址是程序员能看到的虚拟地址。
分段地址转换:CPU把逻辑地址(由段选择子selector和段偏移offset组成)中的段选择子的内容作为段描述符表的索引,找到表中对应的段描述符,然后把段描述符中保存的段基址加上段偏移值,形成线性地址(Linear Address)。
分页地址转换,这一步中把线性地址转换为物理地址。
c. 段描述符
在分段存储管理机制的保护模式下,每个段由如下三个参数进行定义:段基地址(Base Address)、段界限(Limit)和段属性(Attributes)
段基地址:规定线性地址空间中段的起始地址。任何一个段都可以从32位线性地址空间中的任何一个字节开始,不用像实模式下规定边界必须被16整除。
段界限:规定段的大小。可以以字节为单位或以4K字节为单位。
段属性:确定段的各种性质。
段属性中的粒度位(Granularity),用符号G标记。G=0表示段界限以字节位位单位,20位的界限可表示的范围是1字节至1M字节,增量为1字节;G=1表示段界限以4K字节为单位,于是20位的界限可表示的范围是4K字节至4G字节,增量为4K字节。
类型(TYPE):用于区别不同类型的描述符。可表示所描述的段是代码段还是数据段,所描述的段是否可读/写/执行,段的扩展方向等。其4bit从左到右分别是
执行位:置1时表示可执行,置0时表示不可执行;
一致位:置1时表示一致码段,置0时表示非一致码段;
读写位:置1时表示可读可写,置0时表示只读;
访问位:置1时表示已访问,置0时表示未访问。
描述符特权级(Descriptor Privilege Level)(DPL):用来实现保护机制。
段存在位(Segment-Present bit):如果这一位为0,则此描述符为非法的,不能被用来实现地址转换。如果一个非法描述符被加载进一个段寄存器,处理器会立即产生异常。操作系统可以任意的使用被标识为可用(AVAILABLE)的位。
已访问位(Accessed bit):当处理器访问该段(当一个指向该段描述符的选择子被加载进一个段寄存器)时,将自动设置访问位。操作系统可清除该位。
段描述符的格式
段描述符的结构
```c
/* segment descriptors */
structsegdesc{
unsignedsd_lim_15_0 :16;// low bits of segment limit
unsignedsd_base_15_0 :16;// low bits of segment base address
unsignedsd_base_23_16 :8;// middle bits of segment base address
unsignedsd_type :4;// segment type (see STS_ constants)
unsignedsd_s :1;// 0 = system, 1 = application
unsignedsd_dpl :2;// descriptor Privilege Level
unsignedsd_p :1;// present
unsignedsd_lim_19_16 :4;// high bits of segment limit
unsignedsd_avl :1;// unused (available for software use)
unsignedsd_rsv1 :1;// reserved
unsignedsd_db :1;// 0 = 16-bit segment, 1 = 32-bit segment
unsignedsd_g :1;// granularity: limit scaled by 4K when set
unsignedsd_base_31_24 :8;// high bits of segment base address
};
```
d. 全局描述符表
全局描述符表(GDT)是一个保存多个段描述符的“数组”,其起始地址保存在全局描述符表寄存器GDTR中。GDTR长48位,其中高32位为基地址,低16位为段界限。
e. 选择子
线性地址部分的选择子是用来选择哪个描述符表和在该表中索引哪个描述符的。选择子可以做为指针变量的一部分,从而对应用程序员是可见的,但是一般是由连接加载器来设置的。
段选择子结构
索引(Index):在描述符表中从8192个描述符中选择一个描述符。处理器自动将这个索引值乘以8(描述符的长度),再加上描述符表的基址来索引描述符表,从而选出一个合适的描述符。
表指示位(Table Indicator,TI):选择应该访问哪一个描述符表。0代表应该访问全局描述符表(GDT),1代表应该访问局部描述符表(LDT)。
请求特权级(Requested Privilege Level,RPL):保护机制。
全局描述符表的第一个描述符无法被CPU使用,所以当一个段选择子的索引(Index)部分和表指示位(Table Indicator)都为0的时(即段选择子指向全局描述符表的第一项时),可以当做一个空的选择子。当一个段寄存器被加载一个空选择子时,处理器并不会产生一个异常。但是,当用一个空选择子去访问内存时,则会产生异常。
6. BIOS是如何苏醒的(重要)
BIOS代码被写进ROM中,该ROM被映射到低端1M内存的顶部,即地址0xF0000~0xFFFFF。BIOS的入口地址为0xFFFF0。
开机接电的一瞬间,CPU的CS:IP寄存器被强制初始化为0xF000:0xFFF0,即0xFFFF0。
由于实模式下最高寻址1MB,故0xFFFF0处是一条跳转指令jmp far f000:e05b,跳转至BIOS真正的代码。之后便开始检测并初始化外设、与0x000-0x3ff建立数据结构,中断向量表IVT并填写中断例程。
BIOS最后校验启动盘中位于0盘0道1扇区(MBR)的内容。如果此扇区末尾两个字节分别是魔数0x55和0xaa,则BIOS认为此扇区中存在可执行的程序,并加载该512字节数据到0x7c00,随后跳转至此继续执行。使用的跳转指令为jmp 0:0x7c00,该指令是jmp指令的直接绝对远转移用法。
磁盘与磁道的编号从0开始,扇区编号从1开始。
选择0x7c00是避免覆盖已有的数据以及被其他数据覆盖。
7. MBR/Bootloader
bootloader的作用
切换保护模式 & 段机制
从硬盘上读取kernel in ELF格式的ucore kernel(跟在MBR后面的扇区),并放到内存中固定。
跳转到ucoreOS的入口点执行,将控制权移交给ucore OS。
MBR是主引导记录(Master Boot Record),也被称为主引导扇区,是计算机开机以后访问硬盘时所必须要读取的第一个扇区。其内部前446字节存储了bootloader代码,其后是4个16字节的“磁盘分区表”。
MBR是整个硬盘最重要的区域,一旦MBR物理实体损坏时,则该硬盘基本报废。
bootloader的入口点为0x7c00。
8. 中断与异常(重要)
在操作系统中,有三种特殊的中断事件:
异步中断(asynchronous interrupt)。这是由CPU外部设备引起的外部事件中断,例如I/O中断、时钟中断、控制台中断等。
同步中断(synchronous interrupt)。这是CPU执行指令期间检测到不正常的或非法的条件(如除零错、地址访问越界)所引起的内部事件。
陷入中断(trap interrupt)。这是在程序中使用请求系统服务的系统调用而引发的事件。
当CPU收到中断或者异常的事件时,它会暂停执行当前的程序或任务,通过一定的机制跳转到负责处理这个信号的相关处理例程中,在完成对这个事件的处理后再跳回到刚才被打断的程序或任务中。
其中,中断向量和中断服务例程的对应关系主要是由IDT(中断描述符表)负责。操作系统在IDT中设置好各种中断向量对应的中断描述符,留待CPU在产生中断后查询对应中断服务例程的起始地址。而IDT本身的起始地址保存在idtr寄存器中。
当CPU进入中断处理例程时,eflags寄存器上的IF标志位将会自动被CPU置为0,待中断处理例程结束后才恢复IF标志。
a. 中断描述符表
中断描述符表(Interrupt Descriptor Table, IDT)把每个中断或异常编号和一个指向中断服务例程的描述符联系起来。同GDT一样,IDT是一个8字节的描述符数组,但IDT的第一项可以包含一个描述符。
IDT可以位于内存的任意位置,CPU通过IDT寄存器(IDTR)的内容来寻址IDT的起始地址。
b. IDT gate descriptors
中断/异常应该使用Interrupt Gate或Trap Gate。其中的唯一区别就是:当调用Interrupt Gate时,Interrupt会被CPU自动禁止;而调用Trap Gate时,CPU则不会去禁止或打开中断,而是保留原样。
这其中的原理是当CPU跳转至Interrupt Gate时,其eflag上的IF位会被清除。而Trap Gate则不改变。
IDT中包含了3种类型的Descriptor
Task-gate descriptor
Interrupt-gate descriptor (中断方式用到)
Trap-gate descriptor(系统调用用到)
下图图显示了80386的中断门描述符、陷阱门描述符的格式:
c. 中断处理过程
1) 起始阶段
CPU执行完每条指令后,判断中断控制器中是否产生中断。如果存在中断,则取出对应的中断变量。
CPU根据中断变量,到IDT中找到对应的中断描述符。
通过获取到的中断描述符中的段选择子,从GDT中取出对应的段描述符。此时便获取到了中断服务例程的段基址与属性信息,跳转至该地址。
CPU会根据CPL和中断服务例程的段描述符的DPL信息确认是否发生了特权级的转换。若发生了特权级的转换,这时CPU会从当前程序的TSS信息(该信息在内存中的起始地址存在TR寄存器中)里取得该程序的内核栈地址,即包括内核态的ss和esp的值,并立即将系统当前使用的栈切换成新的内核栈。这个栈就是即将运行的中断服务程序要使用的栈。紧接着就将当前程序使用的用户态的ss和esp压到新的内核栈中保存起来;
CPU需要开始保存当前被打断的程序的现场(即一些寄存器的值),以便于将来恢复被打断的程序继续执行。这需要利用内核栈来保存相关现场信息,即依次压入当前被打断程序使用的eflags,cs,eip,errorCode(如果是有错误码的异常)信息;
CPU利用中断服务例程的段描述符将其第一条指令的地址加载到cs和eip寄存器中,开始执行中断服务例程。这意味着先前的程序被暂停执行,中断服务程序正式开始工作。
2) 终止阶段
每个中断服务例程在有中断处理工作完成后需要通过iret(或iretd)指令恢复被打断的程序的执行。CPU执行IRET指令的具体过程如下:
程序执行这条iret指令时,首先会从内核栈里弹出先前保存的被打断的程序的现场信息,即eflags,cs,eip重新开始执行;
如果存在特权级转换(从内核态转换到用户态),则还需要从内核栈中弹出用户态栈的ss和esp,即栈也被切换回原先使用的用户栈。
如果此次处理的是带有错误码(errorCode)的异常,CPU在恢复先前程序的现场时,并不会弹出errorCode,需要要求相关的中断服务例程在调用iret返回之前添加出栈代码主动弹出errorCode。
9. 特权级
特权级共分为四档,分别为0-3,其中Kernel为第0特权级(ring 0),用户程序为第3特权级(ring 3),操作系统保护分别为第1和第2特权级。
特权级的区别
一些指令(例如特权指令lgdt)只能运行在ring 0下。
CPU在如下时刻会检查特权级:访问数据段,访问页,进入中断服务例程(ISRs)
如果检查失败,则会产生保护异常(General Protection Fault).
1. CPL、DPL、RPL与IOPL
DPL存储于段描述符中,规定访问该段的权限级别(Descriptor Privilege Level),每个段的DPL固定。
当进程访问一个段时,需要进程特权级检查。
CPL存在于CS寄存器的低两位,即CPL是CS段描述符的DPL,是当前代码的权限级别(Current Privilege Level)。
RPL存在于段选择子中,说明的是进程对段访问的请求权限(Request Privilege Level),是对于段选择子而言的,每个段选择子有自己的RPL。而且RPL对每个段来说不是固定的,两次访问同一段时的RPL可以不同。RPL可能会削弱CPL的作用,例如当前CPL=0的进程要访问一个数据段,它把段选择符中的RPL设为3,这样虽然它对该段仍然只有特权为3的访问权限。
IOPL(I/O Privilege Level)即I/O特权标志,位于eflag寄存器中,用两位二进制位来表示,也称为I/O特权级字段。该字段指定了要求执行I/O指令的特权级。如 果当前的特权级别在数值上小于等于IOPL的值,那么,该I/O指令可执行,否则将发生一个保护异常。
只有当CPL=0时,可以改变IOPL的值,当CPL<=IOPL时,可以改变IF标志位。
2. 特权级检查
在下述的特权级比较中,需要注意特权级越低,其ring值越大。
访问门时(中断、陷入、异常),要求DPL[段] <= CPL <= DPL[门]
访问门的代码权限比门的特权级要高,因为这样才能访问门。
但访问门的代码权限比被访问的段的权限要低,因为通过门的目的是访问特权级更高的段,这样就可以达到低权限应用程序使用高权限内核服务的目的。
访问段时,要求DPL[段] >= max {CPL, RPL}
只能使用最低的权限来访问段数据。
3. 切换特权级的过程
a. 特权级提升
当通过陷入门从ring3切换至ring0(特权提升) 时
在陷入的一瞬间,CPU会因为特权级的改变,索引TSS,切换ss和esp为内核栈,并按顺序自动压入user_ss、user_esp、user_eflags、user_cs、old_eip以及err。
需要注意的是,CPU先切换到内核栈,此时的esp与ss不再指向用户栈。但此时CPU却可以再将用户栈地址存入内核栈。这种操作可能是依赖硬件来完成的。
如果没有err,则CPU会自动压入0。
之后CPU会在中断处理例程入口处,先将剩余的段寄存器以及所有的通用寄存器压栈,构成一个trapframe。然后将该trapframe传入给真正的中断处理例程并执行。
该处理例程会判断传入的中断数(trapno)并执行特定的代码。在提升特权级的代码中,程序会处理传入的trapframe信息中的CS、DS、eflags寄存器,修改上面的DPL、CPL与IOPL以达到提升特权的目的。
将修改后的trapframe压入用户栈(这一步没有修改user_esp寄存器),并设置中断处理例程结束后将要弹出esp寄存器的值为用户栈的新地址(与刚刚不同,这一步修改了将要恢复的user_esp寄存器)。
注意此时的用户栈地址指向的是修改后的trapframe。
这样在退出中断处理程序,准备恢复上下文的时候,首先弹出的栈寄存器值是修改后的用户栈地址,其次弹出的通用寄存器、段寄存器等等都是存储于用户栈中的trapframe。
为什么要做这么奇怪的操作呢? 因为恢复esp寄存器的指令只有一条pop %esp
(当前环境下的iret指令不会弹出栈地址)。
正常情况下,中断处理例程结束,恢复esp寄存器后,esp指向的还是内核栈。
但我们的目的是切换回用户栈,则此时只能修改原先要恢复的esp值,通过该指令切换到用户栈。
思考一下,进入中断处理程序前,上下文保存在内核栈。但将要恢复回上下文的数据却存储于用户栈。
在内核中,将修改后的trapframe压入用户栈这一步,需要舍弃trapframe中末尾两个旧的ss和esp寄存器数据,因为iret指令的特殊性:
iret指令的功能如下
iret指令会按顺序依次弹出eip、cs以及eflag的值到特定寄存器中,然后从新的cs:ip处开始执行。如果特权级发生改变,则还会在弹出eflag后再依次弹出esp与ss寄存器值。
由于iret前后特权级不发生改变([中断中]ring0 -> ring0 [中断后]),故iret指令不会弹出esp和ss寄存器值。如果这两个寄存器也被复制进用户栈,则相比于进入中断前的用户栈地址,esp最终会抬高8个字节,可能造成很严重的错误。
b. 特权级降低
通过陷入门从ring0切换至ring3(特权降低) 的过程与特权提升的操作基本一样,不过有几个不同点需要注意一下
与ring3调用中断不同,当ring0调用中断时,进入中断前和进入中断后的这个过程,栈不发生改变。
因为在调用中断前的权限已经处于ring0了,而中断处理程序里的权限也是ring0,所以这一步陷入操作的特权级没有发生改变,故不需要访问TSS并重新设置ss 、esp寄存器。
修改后的trapFrame不需要像上面那样保存至将要使用的栈,因为当前环境下iret前后特权级会发生改变,执行该命令会弹出ss和esp,所以可以通过iret来设置返回时的栈地址。