操作系统实验三_操作系统内核
[toc]
实验目的
1、加深理解操作系统内核概念
2、了解操作系统开发方法
3、掌握汇编语言与高级语言混合编程的方法
4、掌握独立内核的设计与加载方法
5、加强磁盘空间管理工作
实验要求
1、知道独立内核设计的需求
2、掌握一种x86汇编语言与一种C高级语言混合编程的规定和要求
3、设计一个程序,以汇编程序为主入口模块,调用一个C语言编写的函数处理汇编模块定义的数据,然后再由汇编模块完成屏幕输出数据,将程序生成COM格式程序,在DOS或虚拟环境运行。
4、汇编语言与高级语言混合编程的方法,重写和扩展实验二的的监控程序,从引导程序分离独立,生成一个COM格式程序的独立内核。
5、再设计新的引导程序,实现独立内核的加载引导,确保内核功能不比实验二的监控程序弱,展示原有功能或加强功能可以工作。
6、编写实验报告,描述实验工作的过程和必要的细节,如截屏或录屏,以证实实验工作的真实性
实验环境
- Windows 10
- WSL (Windows Subsystem for Linux) [Ubuntu 18.04.2 LTS]:WSL 是以软件的形式运行在 Windows 下的 Linux 子系统,是近些年微软推出来的新工具,可以在 Windows 系统上原生运行 Linux。
- gcc version 7.5.0:C 语言程序编译器,Ubuntu 自带。
- ld version 2.3.0: 链接器,Ubuntu自带
- NASM version 2.13.02:汇编程序编译器,通过
sudo apt install nasm
安装在 WSL 上。 - Oracle VM VirtualBox :轻量开源的虚拟机软件,安装在Windows下。
- VSCode - Insiders v1.33.0:好用的文本编辑器,有丰富的插件,可以用它来打开WSL中的文件夹,用它自带的终端执行make命令。
- GNU Make 4.1:安装在 Ubuntu 下,一键编译并连接代码,生成最终的文件。
- Bochs 2.1.1:安装在Windows下,用于调试代码。
自制工具
由于我的虚拟机和Bochs都安装在Windows下,所以需要将WSL中生成的文件写入至Windows的磁盘,这可以用我编写的工具 do 来解决,只需要执行
1 | ./do 文件名 写入的扇区 |
就可以了。
实验内容
概述
首先了解c语言程序与汇编语言程序混合编译的方法,再开发操作系统内核,它具有两个主要功能:
提供加载用户程序的方法,用户可以将程序写入磁盘,然后让操作系统执行这些用户程序。
控制键盘输入和屏幕输出,使得用户可以与操作系统交互。
再将上一次实验的4个程序放进磁盘,让操作系统执行它们,查看执行结果是否正确。
c与汇编混合编译
由于操作系统内核非常复杂,只用汇编语言是很难完成的,因此需要使用c语言和汇编语言的混合编译,生成可执行的二进制文件。
可以用gcc将一个c语言程序编译生成汇编语言文件,如对下面这个test.c:
1 | int f(int val){ |
执行
1 | gcc -march=i386 -m16 -ffreestanding -fno-PIE -masm=intel -S test.c -o test.asm |
会生成一个x86格式的汇编语言文件test.asm。它的内容很复杂,但我们只用关注一些关键的地方:
1 | main: |
f 函数是这样的:
1 | f: |
看来c语言生成的汇编程序也不算神秘,除了一些奇奇怪怪的指令,跟我们写的汇编程序也没有很大差别。通过上述分析,我们对函数调用和传递参数过程有了更深入的了解。
接着,我们编写一个汇编程序(msg.asm)和c程序(count.c)混合编程实例。汇编模块中定义一个字符串(为了方便,假设它以’\n’结尾),调用C语言的函数,统计其中某个字符出现的次数,汇编模块显示统计结果。
c程序如下:
1 | int count(char* str){ |
其中,汇编程序调用c程序的函数的过程如下:
1 | push 0 |
函数返回会把返回值放在eax寄存器,然后汇编程序可以调用 10h 号中断把它显示出来。
编写makefile文件(makefile2)如下:
1 | kernel:msg.o count.o my_mbr |
在VSCode自带终端中输入:
1 | make -f makefile2 |
就完成了编译、链接和写入磁盘的工作,非常方便。得到的符号表如下:
1 | 0000001a t _end #因为msg.asm里将_start声明为全局变量,_end没有,所以这一行是 小写字母t 表示这是局部变量 |
用bochs运行,结果如下:
运行结果正常。
开发操作系统内核
理论上来说,用纯C语言开发内核也是可以的,但要用到很多内嵌汇编,会使程序看起来令人不适。为此,我把 C 程序中需要用到的大量汇编语言代码放置在entry.asm中,c程序只需要执行
1 | call 标号 |
就可以调用entry.asm 里的各种过程。
汇编程序部分
这部分的内容在entry.asm这个文件中,主要分为三部分。从第6行到第13行是第一部分,主要负责将控制权交给C程序中的main函数,当main函数返回时停机。
从第15行到第71行_load_program过程,用于加载用户程序。将程序从磁盘加载到内存可以调用 16h 号中断来实现,但我一调用就会出bug,于是我只能用《x86汇编语言:从实模式到保护模式》这本书里提供的代码。
调用这个过程之前要将用户程序被加载到的位置放置在dx中,用户程序在磁盘中的起始扇区放置在si中,用户程序的所占扇区数放置在bx中。
从第73行到第111行是clear过程,用于将屏幕清空。这个过程十分简单,不必赘述。
C程序部分
main.c这个文件中的内容是操作系统内核的主要部分。它包括基础I/O操作、工具函数和各种用户交互命令。
I/O 操作
为了方便输入输出,我首先编写了getchar()、putchar()、getline()、put()这4个函数。
第23行到第32行的内容是 getchar() 函数,它通过调用 16h 号中断来得到一个输入字符。如果这个字符的值是13(回车符的键盘码),则返回 ‘\n’ 。
第34行到第86行的内容是putchar() 函数,它接受一个字符类型的参数,将字符打印到屏幕上。打印的位置由locr 、 locc 这两个全局变量来决定。
当要打印的字符是一个普通的字符时,调用 10h 号中断在当前光标处输出这个字符,并调整locr 和 locc 两个变量的值;
当要打印的字符是回车符时,将locc置0,将locr++,调用 10h 号中断设置光标的位置为 locc 和 locr 指定的位置;
当要打印的字符是退格符时,首先修改 locc 和 locr 两个变量的值,保证这两个值始终都是下一次打印的字符在的屏幕上的位置。然后调用10h号中断修改当前光标的位置为为 locc 和 locr 指定的位置,将这个位置的值清0。
getline()函数接受一个字符类型的指针,不断调用getchar(),把读进来的字符存到字符串里,读到回车符就终止,给字符串加上一个 ‘\0’ 。
put()函数接受一个字符类型的指针,不断调用putchar()输出它,遇到 ‘\0’ 就终止。
工具函数
为了方便,我编写了 int_to_str () , str_to_int () 和 strcmp () 这几个函数。意思很明显,内容也缺乏技术含量,不再赘述。
用户交互
程序会不断循环,每次都用getline读取用户输入的命令,并进行交互,共有clear、help、load 和 quit 4种交互命令。
clear命令会将屏幕清空,这只需调用前面说过的entry.asm里的_clear过程就可以了。
help命令会打印提示信息。
quit命令会终止操作系统内核的执行。
load命令是最重要的命令。设计这个命令的初衷是,假设用户有一块装有我的操作系统内核的硬盘,但他完全不懂电脑,只会将程序写入硬盘。这个命令可以让他将程序加载到内存中并运行,而完全不需要修改操作系统的代码。
load命令会打印一条提示信息提示,提示用户输入程序在磁盘中的起始位置,再打印一条提示信息,提示用户输入程序在磁盘中占用的扇区数。然后调用 _load_program 过程将程序加载进内存0xa000处,设置es、ds寄存器的值,并将控制权交给用户程序。用户程序运行结束后,操作系统会将es、ds寄存器的值清0,并清空屏幕。
实验过程
编写一个makefile文件,然后就可以在VSCode自带的终端里输入make完成大量的工作:
1 | user_programs=user_program_1 user_program_2 user_program_3 user_program_4 |
用bochs运行,依次输入load,10,1(第一个用户程序在磁盘的第10个扇区,大小为1个扇区):
效果拨群:
按下Ctrl+C,返回操作系统:
再依次输入load,20,2 加载第二个程序:
效果如下:
输入 load 40 4 后效果如下:
输入load 30 3 后效果如下:
输入help打印提示信息:
输入clear清空:
输入quit可以退出操作系统:
实验总结
这次实验算得上是很硬核了,用到了很多x86汇编的知识,还要用 ld 这种完全不熟的工具,用gcc里各种奇奇怪怪的参数,甚至为了方便我还学了一下makefile (好像早就该学了吧) ,遇到的困难也有很多,主要有:
对汇编语言不够了解。主要是函数调用和传参那里,非常麻烦,之前从没有深入了解过。而且C语言程序编译生成的汇编代码跟自己写的在风格上有很大差别,让我很不适应。一开始的时候只能看懂一些关键的语句的意思,好在我看多了之后还是克服了心中的 “恐惧感” 。
debug实在是太麻烦了。虽然我会使用bochs调试程序,但很多bug非常隐蔽,包括但不限于:
在键盘中输入alt+tab切换屏幕,导致后面的输入无法被bochs读入。至今我都没想到解决的办法,好在不影响我写程序。
向c语言的函数传字符串常量会出错。这个问题好像不止我一个遇到,其他的同学和网上的博客也有这样的问题。我至今也没找到原因,只能把字符串常量改为char*类型。
向指向int类型的指针传short类型变量的地址。我声明了一个short变量,调用 str_to_int (char* s,int* val) 函数的时候把它的地址传了进去,本来我觉得反正都会进行类型转换,没啥问题。可是我执行了
1
*val=0;
之后,问题就来了: short类型变量是2个字节,但 val 指针是int类型的指针,上面的操作会把内存中4个字节全部置为0。更要命的是,由于第一个参数先压栈,第二个字符后压栈,val指向的后面两个字节刚好就是 s 字符串的前两个字节,于是这个函数就不会得到正确的结果。当我想到这个问题时,我不禁为这世上有如此巧妙的bug而感到震惊。
int 16h 读键盘会导致光标位置出错。这是一个很奇怪的bug,不调用int 16h时,用int 10h 中断(ah=3)来读取光标位置可以正常运行,当我用 int 16h 读键盘输入后,用int 10h 中断(ah=03)就完全得不到正确结果,但在光标处打印字符却没有问题。我最终也没整明白这其中的缘由,只能放弃使用int 10h 中断来读取光标位置,改用 locc locr 这两个变量。
我后来想到,debug不一定要对着bochs的那些汇编代码一行行看,用内核输入输出函数也能帮助我debug,这样稍微缓解了我的压力。
- 很多工具不会用。说出来有点丢脸,在这次实验之前我从来没用过 ld 这个工具,当我看到老师给的ppt里那一串参数时,突然认识到自己是多么不学无术。经过我反复地尝试、不断地失败后,我总算学会了如何使用ld和gcc完成C程序与汇编程序混合编译
事实上只是能跑起来而已,学会是不可能学会的。
虽然困难很多,但收获也同样不少。通过这次实验,我大大加强了对汇编语言和C语言的了解,对操作系统的工作方式的认识也更加深入了。同时,完成操作系统内核的开发也算是一件很鼓舞人心的事情,这让我有了更多的勇气和兴趣来进一步学习更深的知识。
参考资料
- https://wu-kan.cn/_posts/2019-03-28-%E7%94%A8%E6%B1%87%E7%BC%96%E4%B8%8EC%E8%AF%AD%E8%A8%80%E5%BC%80%E5%8F%91%E7%8B%AC%E7%AB%8B%E5%86%85%E6%A0%B8/ (今年大三的学长,博客写的很不错)
- https://blog.csdn.net/a200710716/article/details/45936643 (关于键盘输入的ASCII码的资料)
- https://www.cnblogs.com/johnshao/archive/2011/06/13/2079638.html (int 10h 中断的详细介绍)
- https://blog.csdn.net/ZCMUCZX/article/details/80462394?locationNum=4&fps=1 (int 16h 中断的详细介绍)
- https://blog.csdn.net/daydayup654/article/details/78630341 ( ld 工具的详细介绍)
- http://c.biancheng.net/view/661.html (gcc 的各种使用姿势)