简易操作系统
一个简单的操作系统,简单到名字都没有(大概3000行不到),我完成了操作系统最基础的内容:内核引导,保护模式开启,中断,页式内存管理,创建进程与切换,没有磁盘系统与调度系统。 原本是要完成一个可供本科生学习操作系统的教学代码,现草草完成,无力维护,研究生方向不是操作系统,将其开源。我从博客中学到这一切,现在还给博客。 代码原型是清华大学的UCore,在启动引导结束后,基本不再一致。这里感谢清华大学UCore,参照其代码做出了这个操作系统,我原本也想让东北大学拥有一个教学操作系统,但能力不足失败了。 Gitee源码地址:源代地址+配套手册 https://gitee.com/wei_kangnings_small_874180837/ucore-os/tree/USing_LD/
GitHub源代码地址GitHub链接 https://github.com/Weikangning/Operation-System
两个git上内容都一样,哪个方便用哪个
内容过多,这里只展示第一部分(是从Gitee中的配套手册中复制过来的),其余部分大家可以在git上获取。
简介
四岁第一次见到计算机,就被PC所吸引,也一直想实现一个OS,但看了看自己的能耐,也就一直搁置了。本科是计算机专业,毕设的时候选择了基于UCore来制作一个教学操作系统,最后做的也马马虎虎,所以研究生又开始把这个坑挖了出来,想做出最简单的操作系统。 本教程是一个菜鸡的操作系统之路,是面向小白的入门。本人水平也不高,只想尽力将自己所学的按大白话解释出来,如有错误,还希望指正。本系统是对UCore的个人注解。在此基础上想做出一个OS。 下面开始正文,先说说个人历程,做了一圈下来,发现最难的不是代码,是对各种工具的使用,主要是:编译器(GCC,LD),CMAKE/MAKEFILE,脚本控制。我开始并不在意这些,后来才慢慢从windows的集成环境脱离,适应了Linux一切自己动手丰衣足食的状态,我姑且将这些非代码的部分称为外围环境(本质是软件工程的项目管理)。因为操作系统是要与硬件直接交互的,故要面对硬件的复杂性(本教程中对硬件的支持都是最简单的状态),硬件容忍不了任何错误,很多工作要强制指定程序的分布状况,这些都与Windows下由IDE一键生成不同,一个最简单的问题:在windows怎么让代码的地址可以从0x7C00处开始排列,并获知这段代码占用的总长度并在这段代码中使用这个长度???我反正没有在windows解决这个问题,在Linux下这些都依靠GCC组件来实现,在后面会向大家介绍。说到这里能看到,其实代码的底层也没有那么神秘,不过繁琐是真的,所以研发人员先开发了汇编,C语言,后来是JAVA,再到现在的Python,目标都是一个,屏蔽底层,更快更简洁的实现更多的功能,可能有一天,C语言都变成了像汇编一样古老神秘的语言。 操作系统的概念不多说,百度百科有,这里说说操作系统为什么出现:没有操作系统,只有一堆硬件,能用吗?能,至少还有一个BIOS,但能多高效的使用PC,基本是0,不对,好像能,CPU只认二进制,所以只要能在内存中放一段二进制代码,再让CPU的IP寄存器指向代码的起始处,这段代码就跑起来了呀,还能显示字符,还能画图像,不过再来一份代码呢?谁先运行,谁控制CPU,最关键的是,这段代码的运行地址和先前一份代码地址有重复地址怎么办?打一架?不知道各位在学C语言的时候有没有这样的想法:指针能获取地址,每个程序都有地址,而我可以让指针指向我想要的地址,那么,我不可以窃取别的程序的数据了吗?我就是下一个黑客了呀,但实际结果是内存指向错误。后来才知道,操作系统把各个程序都虚化成独立运行了,早将这低端思想扼杀了。以上是操作系统最基本的功能,算是对OS概念的简单补充。 接下来说说我们目标:实现一个简单的操作系统,从代码角度学习操作系统,面向和我一样的小白。一个操作系统最难的地方在于没有开发方向,而不是技术实现,另一个难点在于对C语言的掌握。 限于我的个人能力,讲解的有不充分之处,可以在网络上搜索困惑之处的一些关键字或者专业名词,通过网络博客学习。 黄色背景是关键字,可以网络搜索关键字获取相关知识。
所需基础知识
- 汇编语言,是MASM还是AT&T汇编风格并不重要,重要的是你要能理解CPU的运作原理,这很重要,因为这里没有几行汇编代码,而且我会给处汇编的每一行注释,但需自行理解CPU的工作过程和原理。
- GCC编译器,具体到gcc的每一个命令行参数怎么倒不至于,但至少要能清楚编译过程是啥样,参考学科是:编译原理(这个学好了我认为学啥语言都快,重要的还是它的思想)。
- C语言,这自然是主力,可能会问为什么不用C++,这是因为,C++的符号表和组织结构比C复杂(体现在C++生成的汇编代码上),在和硬件层贴合的时候,会难度陡增。事实上,将C语言和汇编衔接难度不亚于弄清楚C语言的编译流程(后面会一直遇见这个状况,会详细结合代码说明的)。
- QEMU,一个计算机模拟器,这东西只需会学安装和使用就行了,虽然它能看到CPU各个寄存器的数值,并且支持逐步调试,但,如果用过OLLY那种反汇编软件会发现,这功能基本实用不大,毕竟突然冒出来几千个16进制数没人会耐心看完这些数值并反向分析。
- Lds脚本,负责编译最后一步的重定位功能了,比如我想让代码的地址从0x53处开始,到0x99停,余下的代码地址从0xfff开始继续编译,直到结束。操作系统需要通过这种方式实现,需重点掌握。
说了一些与操作系统代码无关的开发环境,接下来说说操作系统代码长啥样,它和你用C语言写出来的代码没有区别,都是汇编生成的二进制,不过它能一直占据CPU的控制权,只是经常让出控制权,让别的代码也用CPU。理论上,你可以让OS只实现”hello world”,但那意义何在。操作系统其实拥有这么霸道的特权还有硬件给他撑腰,不然,它早就被我上面那个摇篮中的思想击垮了。所以操作系统特殊在我们愿意让它特权化,给他很多特权,才铸造了它,不然他和hello world代码没有区别。
环境配置:Linux,在其上安装QEMU,GCC,CLion,make,我们可以上路了。
简单介绍一下各个东西:QEMU,一个简易的虚拟机,和VM等价,主要是为了降低我们的开发难度;GCC,编译器,对应的是Windows下的MinGw;CLion,代码编译环境,和VS有些像,是Linux中比较舒适的C语言开发IDE;make:管理工程,组织各个.c .h .lds文件。在Windows下,这种工作被集成环境自动完成,就像操作系统屏蔽了底层硬件一样,现在我们需要正视他,而且它并不简单。 听完上面的东西,可能会有畏难情绪,我也一样,要不是不写没有毕业证,我早跑路了。
启动过程 先看看操作系统的启动过程,他是如何从寄人(BIOS)篱下,又崛起成为整个PC的最高拥有者的。 ? 当PC电源打开后,80x86结构的CPU自动进入实模式。并从地址0xFFFF0 (0xFFFF:0) 开始自动执行程序代码,这个地址通常是是ROM-BIOS中的地址。 ? PC机的BIOS将执行某些系统检测,并在物理地址0处开始初始化中断向量。 此后,它将可启动设备的第一个扇区(磁盘引导扇区,512字节)读入绝对内存地址 0x7C00 处,并跳转到这个地方。 这个过程中,出现的关键字:
80x86架构:可以粗略理解为就是Intel的CPU,当年IBM为了防止Intel垄断供应CPU,要求Intel也把X86架构(即它的设计思想与CPU架构)提供给了AMD,这也是AMD和Intel CPU非常相似的原因,他两相互抄作业。
实模式:CPU主要使用实模式和保护模式两种工作方式,个人感觉更像是CPU发展历史造成的。实模式出现于早期8088CPU时期。当时由于CPU的性能有限,一共只有20位地址线(所以地址空间只有1MB),以及8个16位的通用寄存器,以及4个16位的段寄存器。所以为了能够通过这些16位的寄存器去构成20位的主存地址,必须采取一种特殊的方式。当某个指令想要访问某个内存地址时,它通常需要用下面的这种格式来表示:(段基址:段偏移量),也就是在汇编课程中学习的那种CPU寻址方式,实模式会在不同的段基址和偏移量的情况下,访问到同一个物理内存地址(想不到吧)。“实”的意思更多是访问的地址就是物理真实的地址,实模式访问的地址空间也受地址总线的限制而只能访问1MB大小。保护模式是扩增后的内存访问模式,CPU为操作系统安全也提供了更多的硬件支持。保护模式会在后面专门介绍。实模式和保护模式可以切换,在后面都会通过代码来展现。
0xFFFF0地址:这个地址单独列出来原因是,它其实没有存在于物理内存中,而是通过内存地址映射访问的(详见第二章 显示小节下的1MB内存分布)。CPU的IP就会指向这里,这里是ROM-BIOS的地址。为什么不是别的地址,因为BIOS地址在这里,指向别的地址读取到错误的指令。
ROM-BIOS:ROM是只读内存,BIOS(基本输出输入系统)是由厂商刷进ROM中的一个系统,帮你封装一些底层硬件操作。可以通过INT指令来执行BIOS中的功能,它比操作系统还底层,访问它是通过内存地址映射实现。
磁盘引导扇区:磁盘中的第一个扇区(扇区,自己查询是什么),用于加载并转让CPU控制权给操作系统。它里面存的是一段代码,大小不可以超过512B,它为操作系统设置一些基本的标志位,以确保操作系统有正常的工作环境。为什么不把第一个扇区直接整合到操作系统中?原因是:BIOS不知道你的操作系统有多大,故BIOS只会读第一个扇区加载到内存中,其余代码通过第一个扇区的代码加载进入内存,这样所有扇区的代码都可以被加载,这也就回答了为什么第二个扇区BIOS不管。还有一点是有的电脑会安装双系统。
0x7C00:低于这个值的内存被BIOS占用了。
- 此时BIOS将磁盘第一扇区的代码加载进了内存中0x7C00起始的空间,这个代码即:bootloader
- Bootloader知道操作系统存放位置,并将OS加载进来,然后将CPU的控制权交给了OS
Bootloader在操作系统是由我们实现的,故知道OS的各种数据。bootloader在实模式下运行,访问内存上限只有1MB,所以boot loader还肩负着进入保护模式的功能,然后加载操作系统,并跳转执行OS,不然内存不够用(看似1MB内存,实际640KB~1MB空间还是映射各种其它设备的地址,操作系统也无法使用,0X7C00以下还被占用了)。
至此操作系统进入内存并接管计算机,之后的路怎么走,就自由发挥了。可以看到这个过程几经周折,在实现代码前,我们有个问题:
怎么让bootloader的编译起始地址为0X7C00,我们以往编程从来不管程序内存地址排列。指定编译地址我们在后面还要多次用到。这需要通过编译器来实现。
Bootloader和OS明显是两个程序,工程层面怎么组织,这需要MAKE来解决。
以上两个问题很难,我们需要详细了解GCC的流程和MAKE的使用,如果忽略这两个问题,你将看不懂代码的整体结构与实现流程。
GCC的编译过程 C语言写代码的时候我们会有一个头文件和.c文件,他们有啥区别吗?答案是没有,在GCC看来都是文本文件而已,你也可以将他们的后缀改成.qqqq都没事,在gcc看来还是文本。这样做的初衷只是一种约定。 GCC先将#include<.h>处的命令宏替换,根据.h所指的文件,将*.h中的代码全部复制过来,这也是为什么.h会有预编译指令防止你多次添加同一个*.h文件,不然一个.c文件中会重复n次.h中的代码,你也会疯掉。合并后就只有.c文件,.h文件就没用了。 然后将这个添加完.h的.c文件各自进行编译,编译成汇编代码,什么叫各自?意思就是有多少个.c文件就编译出多少个汇编代码文件,你可能会问,有的.c文件中会用到别的.c中的函数,怎么办?答案是不管,先放着(这时其实GCC也不知道一个.c文件中提到的另一个.c文件中的函数到底存不存在,也不知道那个函数的格式到底是什么样,这也凸显出了头文件的作用),最后一步会处理这个问题。从这里能看出来C语言其实就是汇编转换器,所以GCC是可以将汇编和C语言文件混在一起编译成一个程序的。 然后将各个汇编代码各自编译(各自的含义同上,即:有多少个.c文件就生成多少个二进制文件)成二进制文件(也就是目标文件.o),也叫机器码。最后一步,重定位和链接。链接的意思,就是上一步中各自编译中涉及使用别的文件中的函数或者变量,现在彼此都开始找这些函数在哪,并且指向它(函数就是一个特殊指针而已),这样,之前各个文件的.c代码都组合起来了,这也就是为什么两个.c文件中有同样的函数,编译报错由ld(GCC的链接器)报出(直到最后一步才能发现两个同样的函数,不知道用哪个),而不是第一步预编译爆出。重定位:各个独立的二进制文件要编成一个二进制文件(也就是最终程序),地址肯定会冲突,毕竟之前的内存地址各自为战,各二进制文件编译地址都是从0开始的,为了解决地址冲突,就需要将一些二进制文件的地址重新调整一下,让彼此内存地址不冲突,这个过程就可以指定代码的内存排列了。
通过上面的流程,我们看到GCC的编译操作可以让我们实现任何形式的代码,细节过程可以网络搜索。 再提及以下题外话,为什么会有目标文件和库文件这些东西,GCC为什么不一次将各.c文件直接生成一个可执行文件?有一种情况,我们有时希望别人用我们的代码,却不想公开自己源代码,就可以使用头文件+库文件的形式(库文件可以通过反汇编生成汇编代码,但阅读起来很晦涩,基本没有人这么做)。 头文件告诉别人我们的代码有哪些函数,各类函数的声明是什么样的,别人通过在自己的代码中添加这个头文件就可以使用我们的函数,但头文件只有声明,没有实际代码,实际代码编译成二进制码存放在库文件中,在最后链接步骤中,别人就可以将我们的库文件加入他们的代码了,这样在没有泄露源代码的情况下传播了我们的贡献。 MakeFile:在Windows下,管理一个工程的所有工作都由IDE帮我们做了,我们总以为是GCC有什么特殊功能,其实不是。windows 下是工程编译文件的作用,在VS中是.sln(WPF的工程管理文件后缀)或别的后缀。在linux下,由我们使用各种复杂的GCC命令完成,但那样每次都需要给GCC传递各种命令参数,于是makefile出现了,它足够灵活,可以认为是shell命令合集。 至此,对于开发小的操作系统简介结束了,综上所述,我们需要配置哪些东西: Linux虚拟机,推荐用Ubuntu 虚拟机中需要安装GCC,Make,QEMU 虚拟机中的开发IDE是CLion 需要学习的知识:简单的汇编,GCC,makefile。 现在可以进行开发了。
|