操作系统对于每个开发者来说都是绕不开的门槛,不管是传统的单片机也好,还是现在分布式系统也好,都是离不开基本是计算机模型,从图灵机到冯诺依曼,从埃尼阿克到现在太湖之光,这几十年来的计算机发展都还是在这个模型下发展起来的,可以说在量子计算机大规模推广之前,现今的操作系统软件还是很值得学习借鉴。俗话说,它山之石可以攻玉,那么我们自己磨石头,或许也可以发现蕴含在石头中的璞玉,这也是一件很值得期待的事情呢,不是吗?
最近就想自己动手实验自己写个操作系统,本文权且作为本系列作品的开篇之作,按照我对操作系统的认知来层层推进,最终的期望当然是自己写出个性化的操作系统啦,有机会的话,再继续深入到分布式操作系统,进而进入云操作系统,想想也是挺刺激的,试试看呗,看看能做到多少~网络上资料这么多,牛人这么强,应该可以啦
OS之前
写Hello OS
之前,先要搞清楚所谓的操作系统在上电之后的引导流程,总结来说如下图所示:
简单来说PC机的BIOS固件是一种已经固化在PC
机主板上的 ROM
芯片中的操作系统,即使掉电也能保存,而PC机上电后的第一条指令就是在BIOS
固件中的,它负责检测和初始化 CPU、内存及主板平台,然后加载引导设备(大概率是硬盘)中的第一个扇区数据,到0x7c00
地址开始的内存空间,再接着跳转到0x7c00
处执行指令,其实就是执行GRUB
引导程序。
这次实验是用来体验一下自己编译一个.bin
文件,然后修改的Ubuntu引导程序,进而启动编译完成的这个系统文件。
Hello OS引导的汇编代码
知道PC机的上电流程之后,就可以开始进行逐步开发了,比如说利用一下汇编语言来进行引导程序的开发entry.asm
。
MBT_HDR_FLAGS EQU 0x00010003
MBT_HDR_MAGIC EQU 0x1BADB002 ;多引导协议头魔数
MBT_HDR2_MAGIC EQU 0xe85250d6 ;第二版多引导协议头魔数
global _start ;导出_start符号
extern main ;导入外部的main函数符号
[section .start.text] ;定义.start.text代码节
[bits 32] ;汇编成32位代码
_start:
jmp _entry
ALIGN 8
mbt_hdr:
dd MBT_HDR_MAGIC
dd MBT_HDR_FLAGS
dd -(MBT_HDR_MAGIC MBT_HDR_FLAGS)
dd mbt_hdr
dd _start
dd 0
dd 0
dd _entry
;以上是GRUB所需要的头
ALIGN 8
mbt2_hdr:
DD MBT_HDR2_MAGIC
DD 0
DD mbt2_hdr_end - mbt2_hdr
DD -(MBT_HDR2_MAGIC 0 (mbt2_hdr_end - mbt2_hdr))
DW 2, 0
DD 24
DD mbt2_hdr
DD _start
DD 0
DD 0
DW 3, 0
DD 12
DD _entry
DD 0
DW 0, 0
DD 8
mbt2_hdr_end:
;以上是GRUB2所需要的头
;包含两个头是为了同时兼容GRUB、GRUB2
ALIGN 8
_entry:
;关中断
cli
;关不可屏蔽中断
in al, 0x70
or al, 0x80
out 0x70,al
;重新加载GDT
lgdt [GDT_PTR]
jmp dword 0x8 :_32bits_mode
_32bits_mode:
;下面初始化C语言可能会用到的寄存器
mov ax, 0x10
mov ds, ax
mov ss, ax
mov es, ax
mov fs, ax
mov gs, ax
xor eax,eax
xor ebx,ebx
xor ecx,ecx
xor edx,edx
xor edi,edi
xor esi,esi
xor ebp,ebp
xor esp,esp
;初始化栈,C语言需要栈才能工作
mov esp,0x9000
;调用C语言函数main
call main
;让CPU停止执行指令
halt_step:
halt
jmp halt_step
GDT_START:
knull_dsc: dq 0
kcode_dsc: dq 0x00cf9e000000ffff
kdata_dsc: dq 0x00cf92000000ffff
k16cd_dsc: dq 0x00009e000000ffff
k16da_dsc: dq 0x000092000000ffff
GDT_END:
GDT_PTR:
GDTLEN dw GDT_END-GDT_START-1
GDTBASE dd GDT_START
以上的汇编代码分为 4 个部分:
- 代码 1~40 行,用汇编定义的
GRUB
的多引导协议头,其实就是一定格式的数据,Hello OS 是用 GRUB 引导的,当然要遵循GRUB
的多引导协议标准,让 GRUB 能识别的 Hello OS。而之所以有两个引导头,是为了兼容 GRUB1 和 GRUB2。 - 代码 44~52 行,关掉中断,设定 CPU 的工作模式。
- 代码 54~73 行,初始化 CPU 的寄存器和 C 语言的运行环境。
- 代码 78~87 行,
GDT_START
开始的,是 CPU 工作模式所需要的数据
主函数
上面的汇编代码调用了main
函数,而在其代码中并没有看到其函数体,而是从外部引入了一个符号。那是因为这个函数是用 C 语言写的在main.c
中,最终它们分别由nasm
和GCC
编译成可链接模块,由LD
链接器链接在一起,形成可执行的程序文件:
#include "vgastr.h"
void main(){
printf("Hello OS!");
}
这里用到的printf
也不是我们熟知的那个函数,只是碰巧名字一样罢了,这个显示函数是需要我们自己实现的。调皮一些的话,printf
还可以改成echo
,show
,kankanwo
这些,没关系的,毕竟这个函数也是我们自己定义的。
控制计算机屏幕
我们之所以可以看到的屏幕显示的内容,是因为有个硬件的来支撑的,即我们常说的显卡。如果我们要在屏幕上显示字符,本质上就是编程操作显卡。这个并不难,做完了甚至还挺有成就感。
注意到无论我们 PC 上是什么显卡,它们都支持一种叫VESA
的标准,这种标准下有两种工作模式:字符模式和图形模式。显卡们为了兼容这种标准,不得不自己提供一种叫VGABIOS
的固件程序。
这里需要补充一下在上古时代显卡的字符模式的工作细节。它把屏幕分成 24 行,每行 80 个字符,把这(24*80)个位置映射到以0xb8000
地址开始的内存中,每两个字节对应一个字符,其中一个字节是字符的ASCII
码,另一个字节为字符的颜色值。如下图所示:
了解细节之后就可以对显示程序vgastr.c
进行开发
void _strwrite(char* string)
{
char* p_strdst = (char*)(0xb8000);
while (*string)
{
*p_strdst = *string ;
p_strdst = 2;
}
return;
}
void printf(char* fmt, ...)
{
_strwrite(fmt);
return;
}
代码很简单,printf
函数直接调用了_strwrite
函数,而_strwrite
函数正是将字符串里每个字符依次找到以0xb8000
地址开始的显存中,而p_strdst
每次加2,则是为了跳过字符的颜色信息的空间。
编译和安装
Hello OS 的代码都已经写好,这时就要进入安装测试环节了。不过在安装之前,还要进行系统编译,即把每个代码模块编译最后链接成可执行的二进制文件。
make
make 历史悠久,小巧方便,也是很多成熟操作系统编译所使用的构建工具。
我们在软件开发中,make
是一个工具程序,它读取一个叫makefile
的文件,也是一种文本文件,这个文件中写好了构建软件的规则,它能根据这些规则自动化构建软件,就类似我们用docker
打包的时候,需要写dockerfile
一样。
任何一个 Linux 发行版中都默认自带这个 make 程序,所以不需要额外的安装工作,我们直接使用即可。
编译
下面我们用一张图来描述我们 Hello OS 的编译过程,如下所示
安装 Hello OS
经过上述流程,可以得到Hello OS.bin
文件,但是还要让GRUB
能够找到它,才能在计算机启动时加载它。这个过程称为安装,不过这里没有写安装程序,得我们手动来做。经研究发现,GRUB 在启动时会加载一个grub.cfg
的文本文件,根据其中的内容执行相应的操作,其中一部分内容就是启动项。GRUB 首先会显示启动项到屏幕,然后让我们选择启动项,最后 GRUB 根据启动项对应的信息,加载 OS 文件到内存。
menuentry 'HelloOS' {
insmod part_msdos #GRUB加载分区模块识别分区
insmod ext2 #GRUB加载ext文件系统模块识别ext文件系统
set root='hd0,msdos1' #注意boot目录挂载的分区,这是我机器上的情况
multiboot2 /boot/HelloOS.bin #GRUB以multiboot2协议加载HelloOS.bin
boot #GRUB启动HelloOS.bin
}
关于root
的配置情况,如果不知道自己boot
分区的挂载点,可以在grub的引导程序上面按C
进入GRUB的命令行,然后查看提供的挂载分区,这回没有巧办法了,只能一步步调试了,改挂载的分区点,最重要的是,记得把make之后生成的HelloOS.bin
文件拷贝到boot
目录下
之后就正常启动吧:)完成本次实验
总结一下
这次实验先从按下 PC 机电源开关开始,窥探了PC 机的引导过程。它从 CPU 上电,到加载 BIOS 固件,再由 BIOS 固件对计算机进行自检和默认的初始化,并加载 GRUB 引导程序,最后由 GRUB 加载具体的操作系统。其次,用汇编语言和 C 语言实现 Hello OS。第一步,用汇编程序初始化 CPU 的寄存器、设置 CPU 的工作模式和栈,最重要的是加入了 GRUB 引导协议头;第二步,切换到 C 语言,用 C 语言写好了主函数和控制显卡输出的函数,这个时候还需要了解显卡的一些工作细节。最后,就是编译和安装 Hello OS 了。我用了 make 工具编译整个代码,其实 make 会根据一些规则调用具体的 nasm、gcc、ld 等编译器,然后形成 Hello OS.bin 文件,最后把这个文件写复制到 boot 分区,写好 GRUB 启动项,这样就好了。
嗯,期待下次更深入的探索O(∩_∩)O