操作系统——内核雏形
实验目的:
如何生成一个内核,能引导该内核,并进行扩展
实验内容:
- 汇编和C的互相调用方法
- ELF文件格式
- 使用Loader加载ELF文件
- 如何加载并扩展内核
- 设计题:修改启动代码,在引导过程中在屏幕上画出一个你喜欢的ASCII图案,并将第三章的内存管理功能代码、你自己设计的中断代码集成到你的kernel文件目录管理中,并建立makefile文件,编译成内核,并引导
实验环境:
VMware+Ubuntu32位
实验步骤:
1.汇编和C的互相调用方法
调用关系示意图如上,我们来看一下代码。
将foo.asm中定义的函数设为global模式,使得可以被调用,同时导入choose函数 int choose(int a ,int b)。
在bar.c中调用myprint函数,并定义choose函数。
编译,链接,运行:
可以发现调用是成功的!
2.ELF文件格式
格式图如上所示,其中我们来分别分析每一个部分的含义和数据结构。
ELF header:
Program header:
Program header描述的是一个段在文件中的位置、大小以及它被放进内存后所在的位置和大
小。如果我们想把一个文件加载进内存的话,需要的正是这些信息。
3.使用Loader加载ELF文件
我们的期望是使用Loader加载ELF文件,在之后肯定是要加载内核文件的。加载一个文件无非就是寻找文件、定位文件、读入内存三个步骤,所以在这次的loader中也是遵循这个步骤。
和之前实验的思路是类似的,只不过我们这次寻找的是kernel.bin文件。
在这里我们先随便写一个简单的kernel.asm文件来测试一下:
就是,现实一个字母K罢了。
我们来跑一跑这个loader程序
可以看见是成功的,因为出现了ready. 字符
4.如何加载并扩展内核
在上一步骤中,我们已经把kernel加载进了内存,但要想使内核运行并移交控制权,我们还需要先进入保护模式。
进入保护模式的代码已经很常见了,如下所示:
但与之前不一样的是,之前大多数描述符的段基址都是运行时计算后填入相应位置的,此时我们不需要这样了,因为我们自己加载了loader,已经确定了段地址并定义为了BaseOfLoader,所以在Loader中出现的变量的物理地址可以由下公式计算:
标号物理地址 = BaseOfLoader * 10h + 标号的偏移
于是我们将BaseOfLoader定义在一个文件里:
我们直接用了一个宏BaseOfLoaderPhyAddr来表示BaseOfLoader * 10h,当然是为了方便啦。
保护模式的代码如下,仅仅只是打印一个字符P
运行一下:
看到P,证明跳入保护模式成功。
成功跳入保护模式之后,我们获得内存信息、开启分页操作、最终整理内核并移交控制权。
获得内存信息是我们之前实验的内容,我们使用15h中断来完成,代码如下:
当然我们不能比之前的实验差,所以我们也把他加载进显存并打印:
和之前的代码基本就是从第三章中拿来直接使用的,别忘了将调用的函数DispInt,DispStr等包含在32位代码段中。
得到内存信息后,启动分页。
运行:
在之前的基础上出现了第三章那样的内存信息。
内存信息获取成功,分页开启成功,接下来我们将内存中的kernel程序整理,并移交控制权。
别忘了,我们的内核程序是一个elf程序,elf程序的program header table字段是有重要含义的,让我再来复习一下。
之前说到,我们要整理kernel程序,也就是说我们要把之前导入内存的代码整理到一个指定位置,这里我们需要使用memcpy函数。
具体实现如下:
但是,由于ld生成的可执行文件中p_vaddr的值较大,在这里超过我们的内存范围,所以我们需要修改一下ld指令时的参数。
解决了内存的问题之后,我们就可以向内核交出控制权了。
运行一下试试:
可以看到,出现了K字符。证明此时内核成功运行了!
接下来我们来进行内核扩展,先跑一下代码看看结果:
可以看到最下面一行出现一行字符串,但其实并没有那么简单。
首先,在Kliba.asm中定义了一个函数disp_str,用来打印字符串,并且将这个函数导出。
然后在kernel.asm中引入刚刚定义的函数,并将kernel,kliba,string,start链接成kernel.bin并挂载,作为真正的内核程序。
这个代码不仅定义了函数,还使用memcpy函数切换了堆栈和GDT,和之前仅仅打印一个字符的代码相比复杂了许多,但并没有花费很大精力,可见内核的扩展在C语言的帮助下已经简单了许多了。
5. 设计题:修改启动代码,在引导过程中在屏幕上画出一个你喜欢的ASCII图案,并将第三章的内存管理功能代码、你自己设计的中断代码集成到你的kernel文件目录管理中,并建立makefile文件,编译成内核,并引导
1)画出一个ASCII图案
在引导的时候,让他打印两个由字符构成的表情,调用一个打印函数打印如下定义的字符串:
函数还是使用的例子程序中提供的,结果如下:
打印其他的应该是差不多的,就是要记得构造好这个字符的形状。
2)Kernel扩展
扩展原理很简单,主要是中断处理程序的编写问题。之前我们的实验中有过类似的步骤,我们可以直接调用当时的函数。
当然,这里我们不再使用键盘中断,而是使用时钟中断,所以记得修改初始化的值:
其他地方与例子程序没什么很大差别,我们来看一下中断处理程序部分:
为了减少代码修改,我直接把整个函数复制进来了。。。
也就是说,遇到时钟中断的时候,我们的屏幕将根据时钟中断的“节奏”来打印字符串,在一段时间之后的截图如下:
看上去其实挺吓人的,但是从代码实现的角度来说应该是成功了吧。
实验问题:
1.汇编和C的调用方法是怎样的?
将汇编中定义的函数定义为global形式进行导出,将需要使用的函数进行导入;C语言在使用函数之前进行定义。在编译过程中先生成.o文件,并链接所有.o文件成为新文件,作为可执行程序,这个程序既包含汇编文件中的函数也包含C语言中的参数。
2.描述ELF文件格式以及作用
ELF header:用来定义ELF文件各信息,例如Program header table个数与偏移地址。
Program header table:
描述的是一个段在文件中的位置,位置、大小以及它被放进内存后所在的位置和大小。如果我们想把一个文件加载进内存的话,需要的正是这些信息。
3.如何从Loader引导ELF的原理
从Loader引导ELF文件需要经过几个步骤:寻找文件、定位文件、读入内存、转交控制权,其中前三个步骤与是否为ELF文件关系不大,对于要读入内存的文件来说都是需要经过这三个步骤的;最后一个步骤转交控制权则是根据ELF文件头与Program header table中的信息,将内存中的段进行整合并执行、转交控制权。
4.一个内核要能基本使用应该扩展哪些功能,怎么扩展
一个内核是在保护模式中被引导、运行,从实验的角度上说,内核需要接管电脑的控制权,管理计算机上运行的所有程序、进行的所有操作,所以至少应该具有中断管理,线程管理,地址空间和进程间通信等功能。
5.怎么管理内核文件目录?
在之前实验中我们的文件都是散落在一个文件夹里,编译生成的文件也是很混乱,最痛苦的是每次编译、挂载都有很多指令要打。所以我们需要使用make指令来管理内核文件目录。
目前为止我们可以整理成如下所示的结构:
然后,在a.img的路径下添加makefile文件,完成编译、链接、挂载的任务,最后启动bochs则可以达到目的。
Makefile如上所示,使用如上所述的方法和工具则可以很好的管理内核文件和目录。