各位同学大家好
那我们接下来给大家介绍一下Lab 1的实验过程
Lab 1主要是关于操作系统如何启动
以及如何去和中断函数调用栈
相关的一些知识 给大家做一个介绍
这里面呢 会从以下几个方面
比如说X86的启动顺序
这里面涉及到当硬件一启动之后
怎么把操作系统放到内存中去运行
这是讲这一块涉及到的一些基本的知识
第二个是讲C函数调用
因为我们知道
在操作系统里面有很多的函数之间（的调用）
C以及函数之间 C和汇编之间的调用关系
那么它到底在具体细节上面
怎么来实现一个函数调用呢
我们要有所了解
以及还有一部分是关于GCC
GCC是我们编译内核的一个编译器
它里面有很方便的内联汇编
有了内联汇编之后我们可以在C语言里面
就是C的文件里面嵌入汇编代码
就不用单独写一个汇编的一个文件
这样相对来说编起来会更加容易
但是它有它的一些相应的规则
这样的规则给大家做一个简单的介绍
使得大家能够看懂 在我们的Lab 1中出现的汇编代码
再接下来是关于中断这一块
一旦我们的操作系统启动之后
它很重要的事情就是要能够接管中断
中断有很多功能 我们这边会讲一下
在X86-32这个硬件下 它的中断处理
也就是uCore的中断处理是怎么来完成的
它怎么和硬件的这些特征来结合
来完成跟中断相关的一系列的管理工作
最后是小结
这是我们Lab 1大致要讲的内容
首先我们看一下这个启动过程
那对于X86的启动过程来说
我们需要了解它这个硬件平台从加电一下开始
你一按电源开关之后
那大致的一个整个执行过程是怎么一回事
这需要有了解 怎么就启动到我们的uCore操作系统去了
第二个要理解这个X86的一个实模式 保护模式
我们前面在lab0的时候给大家介绍过
实模式 保护模式有它的不同的特征
那我们在这里面 为什么一开始的实模式
后面又进入了保护模式 在保护模式下干什么事情
以及去理解一旦进入保护模式之后
段机制是怎么一回事
这个是X86启动 在启动相关的一些知识需要去了解
首先看一下 当我们的X86硬件加电之后
会出现什么现象
这个其实是讲了关于因特尔不同的处理器
基本上是80386以后出现的处理器
当一加电 相当于做了一次热起之后
会出现什么现象
那么它首先肯定会有一个初始状态
那么可以看到这所有的寄存器列在这
它的初始状态下 都有一个缺省的值
那么跟我们uCore相关的寄存器有哪些
大家需要注意
这里面最主要的是它的启动地址
那启动地址在哪呢
第一个 段地址CS
CS和EIP结合在一起
来决定它启动的第一条地址
那么EIP在这儿
这两个结合在一起
形成了启动之后 加电之后的一个地址
只要一开电 那么CS EIP就设置成这么一个值
这个值就决定了它在哪个地址取得相应的指令去执行
这也是需要注意的
其它还有一些标志位等等也很重要
比如EFLAGS 我们前面介绍的标志位
还有一些控制寄存器
比如说我们后面会讲到CR0控制寄存器
一般应用程序都不会碰到
但是在我们操作系统里面会需要跟它打交道
还有其他一些通用寄存器 也都会有相应的初始值
这个比较简单都是零
那我们重点关注的是什么呢
先看一下CS和EIP
那刚才已经看到了
CS在初始的时候是F000
EIP它是32位的 所以说四个零
三个F一个0
但是我们需要注意
当X86一开始加电时候 启动是实模式
早期的为了向下兼容
以前的80 86一开始启动是16位的实模式
在这种情况下 寻址按照实模式的寻址方式
所以说 它是Base加上EIP
CS的Base是多少呢
这里要介绍一下
CS是段寄存器
段寄存器里面有一个隐含的叫做Base的内容
这个Base代表基址
这个基址 其实就是存的值
刚才已经看到了是FFFF0000
再加上刚才的EIP是FFF0
所以说它们启动最终的一个地址是FFFFFFF0这个地址
这个地址 其实就是我们加电之后
要去取得那个内存所在的地址
这个内存什么地方呢 比较奇怪
这个内存是BIOS我们说 在PC中的一个固件
叫EPROM 它所在的一块内存区域中的一个地址
但是需要注意这个地址是只读的一块地址
然后从这个地址会取得第一条指令
这条指令一般是一条长跳转指令
会跳到BIOS中去做初始化工作
所以你可以看到 一开始的时候
它会从这么一个特殊的地址
会跳到一个可以被访问的1M的内存空间里面去执行
这其实也是符合我们说的 在实模式环境下
其实它的寻址空间只有1M这么一个特征
我们接下来的实验其实有一个环节
看看到底这个BIOS从第一条指令
到底从哪开始执行 跳到哪去
我们是在Lab 1有一个小的练习
让大家去尝试去找一找
好 当处于实模式
根据我们说的CS EIP它的寻址
CS它包含的是什么呢
到了实模式里面 它这里面一共是
CS是16位的一个段寄存器
然后加上一个offset offset就是EIP
里面也是有16位的地址
一共为什么形成20位的地址
是在于我们16位的段寄存器左移了四位
左移了四位之后再叠加上我们的IP地址 IP里面的值
才形成了所谓的最终的在实模式下的寻址方式
所以它的寻址CS：IP
其实这个：IP的意思就相当于CS左移了四位
再加上IP形成的地址
这是在实模式情况下的寻址方式
这里面没有后面讲到的段机制 或者页机制
因为还没有进入所谓的保护模式
好 那假设BIOS完成它的工作
BIOS做什么工作呢 其实主要是做一些硬件的初始化工作
底层的硬件初始化工作
保证这个机器能够进行后续的正常工作
完成很多各种外设 CPU内存的质检
完成这个检查之后它会干什么事情
很重要 在这点呢
这个固件会去加载磁盘或者硬盘的第一个主引导扇区
这个主引导扇区是零号扇区
把这里面的内容读到内存中来
一个扇区的内容是多少呢
一个扇区是512个字节
会把这512个字节的内容
读到一个固定的地址 0X7C00处
读到这个地址 相当于是把这一块磁盘
空间的信息加载到内存中去
同时把它的IP地址跳到这个地方来 跳到0X7C00
这样 使得它可以去执行这个扇区里面的代码
那这个扇区里面的代码是什么呢
其实我们说Lab 1一里面的bootloader
就属于512个字节这么一个特殊的执行代码
它完成什么工作呢
它完成来对我们说的这个操作系统 uCore的进一步加载
有同学比较好奇
说那为什么我们的BIOS不直接加载uCore操作系统
大家想想 这里面其实取决于BIOS的能力问题
因为刚开始在设计的时候
它完成的功能就只是加载一个扇区
而我们知道一个操作系统
它的代码容量是大于512个字节 它会比较大
那这种情况下 你如果说靠BIOS
来负责加载一个复杂的很大容量的一个OS的话
其实不太现实
增加BIOS工作的难度
所以说干脆BIOS只加载一个扇区
而这个扇区里面的代码会完成后续的加载工作
那这个扇区我们称之为Bootloader
好 既然我们说这个主引导扇区里面存着Bootloader的代码
那么它要干什么事情呢
别小看这512个字节
在这里面真正执行的代码可能只有不到512个字节
可能只有400多个字节
这么点字节数 它能实现什么功能
大家想想 其实它完成的事还挺多的
第一个它要干的事情 首先要从实模式切换到保护模式
为后续我操作系统的执行做准备
这是干的第一个事情
就是从实模式的16位的寻址空间切换到了32位的寻址空间
从1M的寻址到了4G的寻址
这是它干的第一个事情
一旦Enable（使能）了这个保护模式
也就意味着这个所谓的段机制也就自动的加载上来了
就也使能了 也就是段机制可以正常工作了
为此你要让它正常工作 必然要做相应的一些初始化工作
这个事我们后面会讲到
假设它进入了保护模式
干的第二件事情干什么呢
就是读取kernel 就是uCore的代码
又从哪读呢 也是一样 存在我们的硬盘中
它需要从硬盘里面把uCore的代码
再从我们的硬盘的扇区里面读到内存中来
但是这里面也可以看到
即使是我们最开始的Lab 1涉及到的uCore代码量
也不仅仅是一个扇区
它会涉及到多个扇区
所以它会去读取多个扇区的内容
然后读到内存的固定地点
然后也一样 和类似于BIOS干的工作一样
最后把控制权交给uCore操作系统进一步执行
怎么交 就只是把它所谓的EIP的值
就是CS EIP的值指向我们操作系统内核所在内存中的起始点
这个entry 有个入口点 跳这来
之后就相当于是把控制权交给了uCore OS去执行
这是Bootloader做的事情
最后我们会结合代码给大家展示一下
就是它到底怎么来完成它的功能
好 在这里面呢
首先需要理解个概念
我们基本上是按照Bootloader这个执行过程
来讲解这里面涉及到的一些
跟X86相关的一些硬件细节
那这些细节呢
和我们操作系统里面对应的内存访问机制
中断管理机制有直接的联系
所以大家可以去看一看
是否这里面讲的内容和那里面能够有个对应
但是这里面的内容
相对来说跟我们原理课讲的信息相比
它更加琐碎一些 更加细节一些
但这个琐碎和细节 可以让你更清楚的了解
一个OS怎么来完成相应的中断管理
怎么来完成相应的段的处理机制
第一个可以看看 所谓的段机制
段机制什么意思呢 其实你看看
这里面段寄存器起了一个特殊的作用
它这里面呢 它起了一个指针的作用
它指向了段描述符
在段描述符里面呢
描述了一个段落的起始地址和它的大小
这是最直接的两个特点
所以说 我们说可以根据CS里面的Index的值
来找到uCore代码段的起始地址在什么地方
它的大小是什么地方
这是我们认为可以这么来做的
同理 数据段也可以用其它的一些特定的一些寄存器
段寄存器 比如ES 或者DS等等来表述
我们的堆栈段也可以用SS来表述
这样看起来比较自然
但是需要注意 但是由于我们后面还有页机制
所以在段机制这一块 它的映射关系就搞得尽量简单
什么叫简单呢 就是说它的限制
在uCore设置里面它的限制是4G
也就是说它的段落大小是4G
它的段的起始地址从零开始
其实也是意味着它的空间顶满4G空间
应该说它是想起到一个分段的功能
这个功能把它弱化了
基本上是没有这个功能了
它后面还有其他的功能
还有特权级的一个保护
想通过一个段 比如代码段 数据段
想用段机制来实现这种分割
在uCore里面没有做到
这里面也有一个因是在于
这份功能的实现 和后续的页机制的实现
在功能上有一定的重叠
我可以用段机制来实现这种分段
我也可以用页机制来实现分段
所以说相对而言
我们更倾向于用页机制来实现分段
这是我们后面 再讲Lab 2的时候会涉及到的内容
这里面是给大家提醒一下
好 但是这个段模式你不能消掉
因为只要起了保护模式
在X86里面 规定这个段就Enable了
而且你的页机制还是建立在段机制的基础之上实现的
那我们可以看到 能够把段机制建立好
虽然它完成的功能是一个
近似于对等的这么一个映射关系
但是我们要把映射关系给建立好
在一个段寄存器里面 会保存一块区域叫做段选择址
这个选择址就是我们刚才说的是Index
它的值就代表Index
这个Index会查找一个在段描述符表里面的一个项
叫做段descripter 就是段描述符
找到段描述符之后 这个描述符是对应什么来的
它是对应Index 因为这可以意味是一个数组
找到这么一项
然后这里面 段描述符里面会存着刚才说的很重要的两个信息
就是它的起始地址和它的大小
那起始地址加上一个Offset
这里Offset就是我们的EIP
就是偏移量 EIP偏移量
那么这个EIP加上由CS 或者说其他段寄存器所指出来的基址
叠加在一起形成线性地址
在这里面 我们前面讲到
由于还没有启动页机制
所以说线性地址就等同于物理地址
所以说我们可以看到
所谓的段机制其实是一种映射关系
如果我们在这个段描述符里面
把这个Base的Address设成零
那也意味着你的EIP的这个值也就是它的物理基址
这是最简洁的一种设置方式
好 我们另外也可以看看
怎么能够把这个机制给建立好
这里面很重要一点就是你需要有一个大的数组
把各个段描述符给装进去
数组在哪 数组是由我们操作系统来建立的
那我们把它称之为全局描述符表 简称GDT
那么这个GDT是由我们Bootloader来建立的
虽然只有不到512个字节
但是它可以把这个建立好
它会描述好段描述符表的一个大致的空间
然后给出它的位置和它的大小
然后通过一个特殊的指令 比如GDT
然后把这个GDT Descriptor这个地方列出来
这里面给出了一个GDT的一个描述
就可以让我们的CPU能够找到段表的起始地址
GDT全局描述表我们也简称段表
这个表的起始地址之后
通过内部的寄存器叫做GDTR这个寄存器
来保存这个相应的地址信息
然后使得我们各个CS DS SS等等
它可以和我们GDT表建立对应关系
当然这个GDT表是你设置好的
假设有四项 有五项
那么你得CS可能二 三 四 五都有可能
从而可以指向GDT大数组里面所对应的描述符
当然在这里面你首先理解GDT之后
再看GDT里面的每一项
这个表称之为段描述符的一个很详细的表述
虽然细节很多 但其实我们最主要关注两项
我们前面说过的
第一个就是基址（Base）在什么地方
第二个这个段的长度（Limit）是多大
我们也讲到了 在uCore里面把这个功能给它弱化
基址都是零 段的长度都是4G
所以在这里面也等同于一个最简单的对等映射
好 我们在看看这边
这边表明刚才说到的各个段基寄存器
它怎么来产生这个Index
就是这里面的全局描述表中的一个索引值
其实大家都知道 前面说到了段寄存器
它一共大致有16位
其中的高13位放的就是GDT的Index
然后接下来的两位 放的是一个叫做RP
表明这个段当前的段的优先级的级别
在X86里面 它用了两个bit来表示这个优先级别
意味着它可以表示0 1 2 3四个特权级
优先级 或者叫特权级
那我们一般说我们操作系统放在最高的级别是0特权级
而我们的应用程序会放在3这个特权级里面
所以说这个标记 段寄存器里面的这个标记
表明了它当前执行的时候
特别是CS 当前执行的时候所处的特权级
这个TI我们暂时忽略 我们一般设置为零
因为我们这边用到是GDT
我们这里面整个没有用到所谓的LDT
本地描述符表没有使用
我们使用的是全局描述符表
好 解释了段选择址 全局描述符表
然后是什么呢 这个是段描述符
这个是指向全局描述符表起始地址GDTR的一个寄存器
有了这些之后呢 我们就可以看到
映射关系就建立好了
建立好这个映射关系之后
你还没有使能（Enable）它
使能代表现在进入了保护模式
段机制也能够正常工作
当你建好所有的这些前期的准备工作之后
我们还差最后一步 最后一步是靠什么呢
靠对一个特定的寄存器 系统寄存器CRO
这个寄存器我们称之为系统性寄存器
或者控制寄存器 对于这个寄存器
把它的第0号bit置成1
那么就意味着现在的系统
我们的CPU会进入到保护模式
好 可以看到我们前面再总结一下
你需要建好一个GDT
这里面GDT里面的每一项是一个段描述符
然后我还要把相应的段的段寄存器
CS DS等设置好对应的Index
使得CS DS等这些段寄存器
能够指向全局描述符表GDT对应的项
这个项我们称之为段描述符
这个描述符指出映射关系
从而可以在使能了保护机制之后
使段机制能够正常的工作
这实际上就是说启动保护模式
在X86里面启动保护模式
Bootloader要干的基本的一些事情
好 进入了保护模式之后
Bootloader要干第二个很重要的事情
干什么呢 就是要加载uCore OS
那我们的uCore OS编译出来
后面会给大家讲Demo这个的源代码
编译完之后会生成一个elf格式的一个执行程序
elf格式的执行程序是我们在Linux里面
很常用的一种执行文件的格式
那么我们需要了解到uCore 它的名字叫kernel
kernel这个文件它的内部的信息
从而使得我们Bootloader能够根据这个文件的格式
能够把uCore相应的代码 数据
给放到内存中相应的地址
这是我们说接下来BootLoader要干的很重要的第二个事情
那它怎么能放呢
需要去解析这个ELF格式的信息
ELF里面有一个头 叫ELFheader
ELFheader里面呢 指出了一个Program Header
程序段的头
程序段里面包含了代码段 数据段等等
各种各样的程序段
它会把这个头这个信息给表述出来
比如说这里面有一个叫做Program Header的Table的Offset
相当于这个ELFheader的偏移地址
它的起始地址在什么地方
以及它的个数 这里面有个个数PHnumber
有了这两个信息之后
我们可以进一步查找Program header这个结构
知道它的偏移位置之后
把结构里的信息读出来
很重要的两点
它的虚地址要往哪个地方放
因为你这个编出来的代码是要在某一个特定的地址上运行的
它的位置在什么地方 这有个va
以及这一块 这个Program Header
比如我们说的可能是一个代码段
它的起始地址在什么地方
它的size是多少 代码段有多大
这两个信息可以便于把内存中的相应一块区域
用于存放我们的uCore代码段或者数据段
还是从这个文件的哪个位置开始读呢
这里有个Offset
从这可以知道我们从文件哪个位置把代码段读进来
把代码段读进来 把数据段读进来
这是说我们这个加载ELF格式的uCore OS的一个大致的流程
大致就是说它能够识别出很重要的一些关键信息
然后把相应的代码段 数据段
从我们的文件读到我们的内存中来
另一个问题 我们这边还没讲到文件系统
它读的是什么
其实它读的是最原始的磁盘扇区
它是把一个一个的磁盘扇区
刚才说的Bootloader之后的连续的磁盘扇区
连续读了四个磁盘扇区 读到内存里面来
然后开始完成相应的分析工作
但是需要注意 随着里面的uCore的size进一步增加
也许后面还不止四个扇区
可能还有更多的扇区 这是很正常的
好 那我们大致就把Bootloader启动的过程
它进入保护模式 加载uCore操作系统
给大家做了简单介绍
这里面会涉及到了一些保护模式 段机制的建立等等
这一块的信息大家可以阅读相关的一些文档和网站
这里面确实和硬件相关的信息比较大
我希望大家能够读懂代码
能够理解它什么意思
基本上就OK了