第一讲:操作系统概述
什么是操作系统
- ucore操作系统
- 操作系统内核特征
并发、共享、虚拟、异步
为什么学习操作系统
略…
操作系统实例
略…
操作系统的演变
单用户系统
批处理系统
多道程序系统
分时系统
个人计算机
分布式系统
…
操作系统结构
- 简单结构
- 分层结构
- 微内核结构
- 外核结构
类似于虚拟机
第二讲(实验):环境准备
2.1实验概述
-
8个实验
-
实验涉及内容
2.2 x86-32硬件介绍
80386的四种运行模式
实模式:16位寻址空间,无保护机制。80386加电后默认进入实模式运行
保护模式:32位寻址空间,有保护机制。支持内存分页机制,提供了对虚拟内存的良好支持;保护模式下80386支持多任务、支持优先级
关于内存地址
物理地址:提交到总线上用于访问内存和外设的最终地址,一个计算机系统只有一个物理地址空间
线性地址:在虚拟内存系统的管理之下每个程序能访问的地址空间(它是逻辑地址与物理地址的中间形式)
逻辑地址:应用程序直接使用的地址空间(即进程的地址空间)
关系
地址的转换过程:逻辑地址=> 线性地址 => 物理地址
x86-32寄存器
-
8组寄存器
通用寄存器:使用最多
段寄存器:段相关,主要用于寻址
指令指针寄存器(EIP):寻址相关
标志寄存器(Eflags):寻址相关
控制寄存器:
系统地址寄存器:
调试寄存器:开发应用用不到
测试寄存器:开发应用用不到 -
通用寄存器
EAX:累加器
EBX:基址寄存器
ECX:计数器
EDX:数据寄存器
ESI:源地址指针寄存器
EDI:目的地址指针寄存器
EBP:基址指针寄存器(注意与基址寄存器的区别)
ESP:堆栈指针寄存器 -
段寄存器
CS:代码段(实模式和保护模式下含义不同)
DS:数据段
ES:附加数据段
SS:堆栈段
FS:附加段
GS:附加段 -
指令指针寄存器(EIP)
没有分段机制时,EIP寄存器存放的就是下一条指令的地址;有分段机制时,EIP存放的是段内偏移;
实模式下(已分段),由CS和EIP共同决定地址 => 即:CS*16+IP
而保护模式下(已分段),EIP仍然存放段偏移,但是CS存放的是段选择子(通过选择子去找段基址),而不是直接存放段基址。 -
标志寄存器(EFLAGS)
EFLAGS使用不同的bit作为标志,比如:
部分bit不能由应用程序修改,只能由运行在特权态的操作系统修改
2.3 ucore相关数据结构与编程技巧
第三讲:启动、中断、异常和系统调用
3.1 BIOS(内有疑惑)
- 加电以后cpu的第一条指令来自哪里?
系统加电后的初始化代码来自ROM,它是物理内存最低地址的一小段,断电后存储内容不会丢失,BIOS即存储在ROM中。加电后ROM中的BIOS先读取磁盘的MBR,然后…
注:更确切的说,是开机时硬件将ROM中的程序读取到RAM中低地址的一小段,详见关于BIOS的入口地址0xFFFF0 - 初始化时的约定
刚加电时,处于实模式,只有20位地址空间。此时指令地址的计算方式=>
PC=16*CS:IP,即CS寄存器左移4位,再加上指令指针寄存器;其中CS代表段基址,IP代表段内偏移
为什么是20位地址空间:实模式下寄存器只有16位,但是将CS左移4位得到20位的数据,再加上段内偏移IP,结果仍是20位的地址(个人认为寄存器中值的设置应该保证计算得到的PC不会溢出)
80386reset后CS和IP的值:复位后CS的值为0xf000,IP的值为0xfff0,如下图所示
疑惑:为什么图中BIOS的开始地址是640kb,如何通过CS、IP计算得到的,按照上文,不是0xffff0吗???
- 加载过程(更详细的过程参考下一节)
1. BIOS固件将磁盘中MBR里存储的系统加载程序加载到PC=CS:IP=0000:7c00处(这个加载程序只有512B;MBR是磁盘的最开始处)
2. 然后控制跳转到0x7c00处执行刚从磁盘加载进来的系统加载程序
3. 系统加载程序将操作系统的代码和数据从磁盘加载到内存
为什么要先加载程序,再由加载程序来加载操作系统?:由于文件系统有不同的格式,BIOS并不能直接识别所有的文件系统,所以它只负责加载系统加载程序,与文件系统相关的事情再交给系统加载程序来处理
3.2 系统启动流程
启动流程
-
大体过程
注意:如上图所示,实际上BIOS不是直接就读取了加载程序的 => 由于磁盘可能有多个分区,BIOS并不能确定加载程序在哪个分区。
所以:1. 先读主引导扇区,借此找到活动分区(即系统分区)
2. 读取活动分区的引导扇区代码
3. 最后再利用引导扇区代码读取加载程序到内存 -
0:CPU初始化
-
1:BIOS初始化(硬件自检POST)
1 检测系统中内存和显卡等关键部件的存在和工作状态
2 查找并执行显卡等接口卡BIOS,进行设备初始化
3 执行系统BIOS,进行系统检测 => 检测并配置即插即用设备(比如从U盘启动),从而了解整个计算机系统连接了哪些设备
4 更新CMOS中的扩展系统配置数据ESCD(存储系统连接的设备,每次加电都会进行)
5 将控制权转到从外部读入的程序 -
主引导扇区(MBR)格式
-
分区引导扇区格式
从
-
加载程序(bootloader)
BIOS从活动分区加载bootloader后,控制转到bootloader,然后由bootloader在该分区对应的文件系统中读取启动配置信息,最后根据配置加载操作系统内核!!!
系统启动规范
BIOS:固化到计算机主板上的程序,包括系统设置,自检程序和系统启动程序,有以下扩展:
BIOS-MBR:一个MBR可以描述四个分区,BIOS读取MBR后找到存放系统的活动分区…
BIOS-GPT:用于解决多余四个分区的问题…
PXE:从网络启动的标准…
UEFI:接口标准,旨在在所有平台上提供一直的操作系统启动服务…
3.3 中断、异常和系统调用
- 内核的进入与退出
注:这里可参考csapp第8章=>广义上的异常可分为:中断、陷阱、故障、终止添加链接描述
本节讲的异常强调的是陷阱和故障; 而系统调用本质上也是通过陷阱实现的,只是它有自己单独的系统调用表,而不是直接使用中断(异常)向量表(系统调用在中断向量表中占用一个编号,发生系统调用时会从中断向量表跳转到系统调用表)
- 三者的区别
3.4 系统调用
- 系统调用的实现
- 系统调用与函数调用的不同
指令不同:系统调用使用INT指令; 函数调用使用CALL指令
堆栈切换:系统调用会切换到内核栈;函数调用不会切换堆栈
调用开销:系统调用开销大于函数调用(需要完成切换引导、内核堆栈建立、地址空间映射、安全验证等)
3.5 系统调用示例
- 例
3.6 ucore+系统调用代码
第四讲(实验1):bootloader启动ucore-os
4.1 x86启动顺序(可参考3.1、3.2)
-
x86 加电后寄存器的初始值
1. 刚加电时,处于实模式,寻址方式兼容8086,使用Base+IP(而不是16CS:IP) => 加电后要取的第一个内存地址是Base+IP(FFFF0000H+0000FFF0H=FFFFFFF0H),这个地址就是BIOS的EPROM所在地,更多参考:附录“启动后第一条执行的指令”
2. 当CS被新值加载后,16CS:IP的地址计算规则才开始使用
3. 上述的第一个地址处是一个长跳转指令,它会跳到BIOS代码中取执行…(读取长跳转指令时便会更新CS、EIP寄存器,地址计算规则开始正常使用…) -
从BIOS到bootloader
1. BIOS开始执行后,加载存储设备上的第一个删除(主引导扇区,MBR,共512字节,其中64字节用于描述4个活动分区,最后两字节是结束标志字节,为0x55aa)到内存地址0x7c000处
2. 控制转移到从MBR读进来的代码,即从0x7c00开始执行…
(注:本节认为MBR中加载进来的代码就是加载os的bootloader,忽略了3.2中描述的活动分区引导扇区) -
从bootloader到os
a.bootloader开始执行,系统从实模式切换到保护模式(通过设置CR0寄存器的某一bit实现) => 寻址空间变为4G,段机制开始工作
b.bootloader读取系统内核代码
c.跳转到系统内核的入口点,控制转移到此,系统开始执行… -
段机制
保护模式下,地址计算不再是16*CS:IP! =>此时段寄存器存放的是一个指针,指向段描述符,段描述符描述了这个段的基址和大小(当然不止这两项),如图:
- 全局描述符表(GDT) 与 地址计算
如上所述,段模式下,段寄存器存放的称为段选择子(selector),它是到段描述符的指针(准确的说存放的是是索引/下标+一些其他信息)。段描述符存放在全局描述符表(GDT),通过寄存器GDTR可以找到GDT。GDTR中的全局描述符表基址+段选择子中的下标部分,就得到段描述符的地址。
在段描述符中找到段基址,再加上EIP中的偏移即可得到线性地址(段机制下逻辑地址就是线性地址),整个过程如下:
-
段寄存器、全局描述符表项的格式
段寄存器存放的数字中除了包含GDT下标外,还有其它信息,比如特权级…
GDT表项中除了段机制、段大小外,还有其它信息…
具体格式求助网络… -
加载ELF的系统内核程序
…略
4.2 C函数调用的实现
- 函数调用栈的变化过程
调用foo后
如上图,注意调用新的函数时,EBP指向新的栈帧,但是EBP所指地址存放的内容是上一个栈帧的地址,依次类推便形成了一个调用链… - 注意
1.部分参数、返回值也可通过寄存器来保存,从而比放再内存中的栈更高效
2.保存/恢复寄存器时不一定是保存所有寄存器的值…
4.3 GCC内联汇编
- 内联汇编
直接在C中插入汇编代码
作用:完成部分C语言无法完成的功能 - 示例
语法
示例1
示例2
4.4 x86-32 中断处理过程
- 中断源
x86将(广义)中断分为两类:中断、异常 (注意与csapp中的区别)
中断的产生
外部中断:串口、硬盘、网卡、时钟等
软件中断:INT n指令 => 用于系统调用
异常的产生
程序错误:除零等
机器检查出的异常S - 1.确定中断服务例程(Interpt Service Routine)
每个中断(异常)与一个中断服务例程(ISR)相关联,关联关系存储在中断描述符表IDT中,类似于GDT,IDT的基址存放在寄存器GDTR中。
中断描述符表的表项:最重要的包括段选择子、段偏移(见下一幅图)
中断的查找过程
a.根据中断号查找中断描述符表IDT,得到中断服务例程的段索引和段偏移
b.根据a中的段索引查找全局描述符表,得到中断服务程序所处段的基址
c.根据b中所得的段基址和a中所得的段偏移,找到中断服务程序的起始地址
- 2.切换到中断服务例程
对于在内核态运行时产生的中断(图左):直接向当前使用的栈中压入部分寄存器的内容,从而完成现场保存
对于在用户态运行时产生的中断(图右):将当前部分寄存器的内容压入系统栈(而不是用户栈),从而完成现场保存; 此外,还需要压入用户栈的ESP和SS,方便返回时回到用户栈原位
如何判断内核态还是用户态:这是由优先级(特权级)决定,段寄存器中的某个低位bit表示特权级
- 3.从中断服务例程返回
iret指令完成中断的返回; ret、retf完成函数的返回!!!
对于在内核态运行时产生的中断:iret指令使得栈中所有保存的寄存器内容恢复到寄存器中,从而能够根据CS、EIP继续从被中断处执行
对于在用户态运行时产生的中断:iret指令执行时将系统栈中所有内容恢复到用户栈…
4.5-4.9 练习
参考:lab1
4.10 补充:关于第一条指令及线性地址的计算(重要)
写在前面:最好直接参考:lab1实验文档BIOS启动过程
开机后的第一条指令
你真的了解段寄存器吗?
-
地址转换过程
逻辑地址(进程地址空间) => 线性地址 => 物理地址
逻辑地址到线性地址是通过分段机制; 线性地址到物理地址是通过分页机制;如果没有分页机制,则线性地址就是物理地址
本节主要讨论分段机制下,线性地址的计算 -
16位机器实模式下的分段相关寄存器
CS:16位,存放段基址。不过段基址=16* CS
IP:16位,存放段偏移
=> 线性地址=段基址+段偏移=16*CS+IP -
32位机器保护模式下的分段相关寄存器
CS:这些段寄存器(不仅CS)除了有16位的可见部分,还有不可见的隐藏部分,称为描述符缓存“descriptor cache”或隐藏寄存器“shadow register”。当一个段选择符(segment selector)装入段寄存器的可见部分,处理器同时也把该段描述符的其它数据装入到段寄存器的隐藏部分,这包括段开始的基地址、段长度、访问控制信息等。这些信息缓存到段寄存器中,避免了处理器在转址(translate address)时花费额外的总线周期从段选择符表中读入数据。处理器指令中可以明示使用哪些段寄存器,这将替换掉默认使用的段寄存器。 ————维基百科
EIP:同样是存放段偏移
=> 线性地址=段基址+段偏移
=(通过CS中的可见部分找到段基址,将段基址存入CS的不可见部分)+EIP
可见:32位和16位下,线性地址的计算都是基址+偏移。区别在于32位下基址的计算方式发生了改变 -
为什么第一条指令是0xFFFFFFF0
32位机器reset后,
CS:可见部分被设为F000H,不可见部分被设FFFF0000H => 基址为不可见部分,即FFFF0000H
EIP:被设置为0000FFFOH => 偏移为0000FFF0H
由于没有分页机制,物理地址=线性地址=…=FFFFFFF0H -
刚开机时不是实模式吗,为什么按照保护模式的方法计算第一条指令地址?
这是规定,第一条指令按照保护模式的方式计算。之后由于CS寄存器被修改,且处于实模式,才开始使用16*CS+IP的方式计算 -
刚开机时处于16位实模式,地址空间只有1MB,为什么第一条指令地址远大于1MB?
通过BIOS映射,参考:开机后的第一条指令