目录
背景
30制作操作系统?这个听起来像天方夜谭,但是真有一本书就叫做《30天自制操作系统》,当然,自制操作系统的目的不是要去对标Windows、Linux、MacOS等流行的现代化操作系统,而是了解操作系统制作与运行的整个流程与细节,了解汇编与C语言。
下图是该书作者最终制作的操作系统的模样,至少我看到有图像查看器、调色板、文本阅读器与console终端,麻雀虽小五脏俱全,这张图就是我坚持的动力(笑)。
本系列文章的目的不是教你怎么一天天去实现上述操作系统,而是笔者在阅读并实践了《30天自制操作系统》一书后的一些笔记和整理,本文后面会提供该书以及所有光盘附带源码,感兴趣的读者可以自己动手实践,如果遇到不了解的内容,也可以结合本笔记查看。
操作系统,英文(Operating System,下文简称OS),是一个特殊的软件,往上运行着应用软件(比如我们常用的QQ,微信),往下和硬件打交道(比如CPU、内存、鼠标等),它就像是硬件和应用软件之间的桥梁与纽带。
仓库中包含该书以及所有源码文件:
https://github.com/scriptwang/30DaysMakeOS
电脑启动过程
怎么才能让电脑通电后再启动我们自制的OS?首先一定要先明白电脑是怎么启动的:
- 按下开机键通电
- BIOS启动并自检(检查硬件设备是否就绪),新版的BIOS也叫UEFI,不要纠结,本质是一样的
- BIOS读取硬盘/软盘/光驱的第一个扇区(512字节)到内存0x7c00~0x7dff处(0x7dff-0x7c00+1 刚好等于512)
- 执行0x7c00~0x7dff代码启动相应的OS
这512字节的程序就叫做bootloader,在书中叫做IPL(initial program loader),叫啥不重要,总之开机就会执行这512字节的指令,这512字节的指令再去将OS kernel加载到内存,然后启动真正的OS!
如下图所示
需要注意的是BIOS的一些设置信息和系统时钟信息存储在CMOS中,CMOS全称Complementary Metal Oxide Semiconductor(互补金属氧化物半导体),它需要通电,台式机主板上的纽扣电池就是给CMOS供电的,如果没电了,BIOS设置信息就会丢失然后恢复到默认值(假设设置了BIOS密码拔掉纽扣电池密码就没有啦),系统时钟也会停止
为啥有这样的设计?想一下为什么每次电脑开机时钟总是正确的?那么一定有一个地方一直在运行着,然后开机时告诉OS当前的系统时钟,要一直运行就得一直通电。
BIOS
参考:
https://zh.wikipedia.org/wiki/BIOS
BIOS(英文:Basic Input/Output System),即基本输入输出系统,BIOS是16位汇编语言程序,只能运行在16位实模式,可访问的内存只有1MB,而UEFI是32位或64位高级语言程序(C语言程序),突破实模式限制,可以达到要求的最大寻址。实模式的内容后面会详细讲到
当电脑的电源开启,BIOS就会从主板上的ROM芯片运行,运行加电自检(POST),测试和初始化CPU、RAM、直接存储器访问控制器、芯片组、键盘、软盘、硬盘等设备。当所有的Option ROM被加载后,BIOS就试图从引导设备(如硬盘、软盘、光盘)加载引导程序,由引导程序加载操作系统。BIOS也可从网卡等设备引导。
下图是BIOS自检过程中检查出的硬盘错误,这个界面有没有很熟悉的感觉。
启动引导程序编写
启动引导程序即上文所说的512字节的bootloader,在书中叫做IPL,启动引导程序后文用IPL代替
汇编的必要性
这里讲的是用汇编来写IPL程序的必要性,编程语言那么的多,为什么要选汇编?要讲明白这个问题首先明白几个概念。
机器语言
参考:
https://zh.wikipedia.org/wiki/%E6%9C%BA%E5%99%A8%E8%AF%AD%E8%A8%80
机器语言(machine language)是一种指令集的体系。这种指令集称为机器码(machine code),是电脑的CPU可直接解读的资料。
所谓机器语言说白了就是一堆0101…的数字,无论任何语言最终都要翻译成机器码才能被CPU执行,下面来个示例感受一下性感的机器语言
- 1011 代表赋值操作(MOV)
- 0000 代表寄存器AL
- 01100001代表数字97(十六进制表示为0x61)
所以要给寄存器AL赋值十进制数97用机器码写成
10110000 01100001
怎么样,性感不?任何程序最后在CPU看来都是0和1的排列组合,来看看上古时期怎么编程,就下图这玩意叫做打孔带:带孔为1,无孔为0,上古时期的程序就是一条条纸带,看得见,摸得着,怎么样,纸带是否更性感?
汇编语言
定义
汇编语言(Assembly Language)是任何一种用于电子计算机、微处理器、微控制器或其他可编程器件的低级语言,亦称为符号语言。
在汇编语言中,用助记符代替机器指令的操作码,用地址符号或标号代替指令或操作数的地址。
在不同的设备中,汇编语言对应着不同的机器语言指令集,通过汇编过程转换成机器指令。
特定的汇编语言和特定的机器语言指令集是一一对应的,不同平台之间不可直接移植。
定义很复杂,其实明白了机器码那么汇编其实很简单,汇编本质上就是机器码的助记符!比如上文提到的赋值操作1011 给它取个名儿就叫做MOV,MOV就是汇编语言,1000 代表寄存器AX,那么就给1000取个名儿叫AX,97的二进制为01100001,这个不好记,我们用十六进制表示成0x61,所以上文中性感的机器码
10110000 01100001
用汇编来表示就是
MOV AL,0x61
有没有感觉清爽了很多?至少看到汇编能猜到大概意思,看到机器码那就一脸懵逼。
但是汇编方便了人们记忆和理解,但是不利于CPU执行啊,所以产生了将汇编助记码翻译成机器码的东西,这个东西就叫做编译器。
所以IPL程序为什么要选汇编?
CPU只认识机器码,无论什么语言最后都变成机器码才能喂给CPU,编译器就是专门干这个事情的,看看主流的编程语言是怎么被CPU执行的
- 汇编 → 机器码
- C/C++ → 汇编 → 机器码
- Java → 字节码 → JVM虚拟机(编译/解释) → 汇编 → 机器码
- …
可以看到汇编离CPU最近(汇编本质上就是机器码的助记符而已),意味着更少的依赖以及可以操作更加底层的东西(比如直接操作CPU的寄存器)
假设用Java来编写IPL,那么它要依赖JVM虚拟机,JVM虚拟机装在哪?现在我们要制作OS,而JVM需要OS,这不就循环依赖了吗?那肯定不行!
假设用C/C++,C/C++要编译成机器码,中间也是要经过汇编的,而且很多C/C++做不到的事情还得需要汇编来做,那么为啥不直接用汇编呢?
所以经过上述分析,IPL程序用汇编编写是具有必要性的!
CPU构成
既然讲到汇编,那就不得不提CPU的构成,如下图
CPU构成包括计算单元(ALU)、寄存器(Register)、缓存(Cache)、程序计数器(Program Counter),寄存器和缓存都是存东西的,不过寄存器更快,大概比喻如下:
寄存器就好像你房间里床边的小抽屉,比较小,但伸手就能打开,速度贼快;
缓存(特指CPU的缓存)就像你房间里的衣柜,比较大,但是要起床才能打开,速度十分快;
物理内存(就是RAM,内库条,俗称条子)就像是你房间对面的仓库,容量很大,但是你要起床,开门,再去库房,拿完东西回来再关门,速度也挺快;
硬盘就像天安门广场,很宽很大很广,可以放下很多东西,但是你得起床,出门做公交或者地铁才能到,拿完东西再回家,那速度就比你家库房(内存)拿东西慢多了。
寄存器
那我们就来讲讲你房间床边的小抽屉,因为汇编里面全是对寄存器的操作,如果不清楚寄存器,会很懵
寄存器就是右边红框内的那一坨,这里展示的是32位的寄存器,寄存器都有自己的名字,所谓寄存器,简单来讲就是临时存下数据的,就像炒菜需要几个碗分别放葱姜蒜,炒的时候一股脑锅里倒就完事,那几个临时放放葱姜蒜的碗就是寄存器。
E为Extended的缩写,表示扩展;H为High的缩写,表示高位;L为Low的缩写,表示低位,例:AH表示AX寄存器的高8位
- AX、BX、CX、DX四个16位寄存器用于通用目的,比如计数、存储数据等,其实就是有4个4字节的存储空间随便取个名字叫A、B、C、D而已,然后把前8位,前8~16位,前16位分别取个个名字而已AH、BH、CH、DH表示高8位AL、BL、CL、DL表示低8位EAX、EBX、ECX、EDX,把原来的16位扩展成32位
- SI、DI、BP、SP四个寄存器都是16位的,各有各的用途,前面加个E表示32位的SI:Source Index,源变址寄存器,可以用来存放数据、地址等DI:Destination Index,目的变址寄存器,可以用来存放数据、地址等BP:Base Pointer,基数指针寄存器SP:Stack Pointer,堆栈寄存器,存放栈的偏移地址可以看到这几个寄存器都和内存地址有关系(指针),在32位的寄存器中能表示的最大内存就是232/1024/1024/1024=4GB,所以32位系统能寻址的最大内存就是4GB,超过4GB无法使用,现在的机器一般都是64位的寄存器了,64位的可以算算264/1024/1024/1024=17179869184GB=16777216TB,就目前而言,可以算作无限大了。
- FLAGS/EFLAGS:表示当前运行状态的标识
- IP/PC:IP即Instruction Pointer,PC即Program Counter,两者其实是一个东西,程序计数器,保存的是下一条指令的执行地址
你的床边就有这么多个小抽屉,汇编就是可以直接操作这些小抽屉的
二/八/十六进制
虽然汇编让我们脱离了阅读机器码的苦海,但是很多时候书写内存地址等的时候依然会用到十六进制来表示(因为写二进制太长了),比如如下表示写入0x55,0xaa两个字节
DB 0x55,0xaa
那为什么是十六进制而不是十五进制?十四进制呢?因为4个bit刚好有十六种排列组合,8个bit就是一个字节,所以两个十六进制的数就可以表示一个字节,下面是二进制-十六进制对照表,一般十六进制数前面有个0x,比如0x45表示十六进制的45而不是十进制的
二进制 | 十六进制 | 二进制 | 十六进制 | 二进制 | 十六进制 | 二进制 | 十六进制 |
---|---|---|---|---|---|---|---|
0000 | 0 | 0100 | 4 | 1000 | 8 | 1100 | C |