你好,我是 LMOS。
上节课我们动手实现了自己的二级引导器。今天这节课我们将进入二级引导器,完成具体工作的环节。
在二级引导器中,我们要检查 CPU 是否支持 64 位的工作模式、收集内存布局信息,看看是不是合乎我们操作系统的最低运行要求,还要设置操作系统需要的 MMU 页表、设置显卡模式、释放中文字体文件。
检查与收集机器信息
如果 ldrkrl_entry() 函数是总裁,那么 init_bstartparm() 函数则是经理,它负责管理检查 CPU 模式、收集内存信息,设置内核栈,设置内核字体、建立内核 MMU 页表数据。
为了使代码更加清晰,我们并不直接在 ldrkrl_entry() 函数中搞事情,而是准备在另一个 bstartparm.c 文件中实现一个 init_bstartparm()。
下面我们就来动手实现它,如下所示。
void machbstart_t_init(machbstart_t* initp)
{
memset(initp,0,sizeof(machbstart_t));
initp->mb_migc=MBS_MIGC;
return;
}
void init_bstartparm()
{
machbstart_t* mbsp = MBSPADR;
machbstart_t_init(mbsp);
return;
}
目前我们的经理 init_bstartparm() 函数只是调用了一个 machbstart_t_init() 函数,在 1MB 内存地址处初始化了一个机器信息结构 machbstart_t,后面随着干活越来越多,还会调用更多的函数的。
检查 CPU
首先要检查我们的 CPU,因为它是执行程序的关键。我们要搞清楚它能执行什么形式的代码,支持 64 位长模式吗?
这个工作我们交给 init_chkcpu() 函数来干,由于我们要 CPUID 指令来检查 CPU 是否支持 64 位长模式,所以这个函数中需要找两个帮工:chk_cpuid、chk_cpu_longmode 来干两件事,一个是检查 CPU 否支持 CPUID 指令,然后另一个用 CPUID 指令检查 CPU 支持 64 位长模式。
下面我们去写好它们,如下所示。
上述代码中,检查 CPU 是否支持 CPUID 指令和检查 CPU 是否支持长模式,只要其中一步检查失败,我们就打印一条相应的提示信息,然后主动死机。这里需要你留意的是,最后设置机器信息结构中的 mb_cpumode 字段为 64,mbsp 正是传递进来的机器信息 machbstart_t 结构体的指针。
获取内存布局
好了,CPU 已经检查完成 ,合乎我们的要求。下面就要获取内存布局信息了,物理内存在物理地址空间中是一段一段的,描述一段内存有一个数据结构,如下所示。
#define RAM_USABLE 1
#define RAM_RESERV 2
#define RAM_ACPIREC 3
#define RAM_ACPINVS 4
#define RAM_AREACON 5
typedef struct s_e820{
u64_t saddr;
u64_t lsize;
u32_t type;
}e820map_t;
获取内存布局信息就是获取这个结构体的数组,这个工作我们交给 init_mem 函数来干,这个函数需要完成两件事:一是获取上述这个结构体数组,二是检查内存大小,因为我们的内核对内存容量有要求,不能太小。
下面我们来动手实现这个 init_mem 函数。
上面最难写的是 mmap 函数。不过,我们还是有办法破解的。如果你理解了前面调用 BIOS 的机制,就会发现,只要调用了 BIOS 中断,就能获取 e820map 结构数组。
为了验证这个结论,我们来看一下 mmap 的函数调用关系:
可以看到,mmap 函数正是通过前面讲的 realadr_call_entry 函数,来调用实模式下的 _getmmap 函数的,并且在 _getmmap 函数中调用 BIOS 中断的。
如果你不明白上面代码的原理,请回到“Cache 与内存:程序放在哪儿”那节课,看一下获取内存视图相关的知识点。 init_mem 函数在调用 mmap 函数后,就会得到 e820map 结构数组,其首地址和数组元素个数由 retemp,retemnr 两个变量分别提供。
初始化内核栈
因为我们的操作系统是 C 语言写的,所以需要有栈,下面我们就来给即将运行的内核初始化一个栈。这个操作非常简单,就是在机器信息结构 machbstart_t 中,记录一下栈地址和栈大小,供内核在启动时使用。
不过,就算操作再简单,我们也要封装成函数来使用。让我们动手来写出这个函数吧,如下所示。
init_krlinitstack 函数非常简单,但是其中调用了一个 move_krlimg 函数你要注意,这个我已经帮你写好啦,它主要负责判断一个地址空间是否和内存中存放的内容有冲突。
因为我们的内存中已经放置了机器信息结构、内存视图结构数组、二级引导器、内核映像文件,所以在处理内存空间时不能和内存中已经存在的他们冲突,否则就要覆盖他们的数据。0x8f000~(0x8f000+0x1001),正是我们的内核栈空间,我们需要检测它是否和其它空间有冲突。
放置内核文件与字库文件
放置内核文件和字库文件这一步,也非常简单,甚至放置其它文件也一样。
因为我们的内核已经编译成了一个独立的二进制程序,和其它文件一起被打包到映像文件中了。所以我们必须要从映像中把它解包出来,将其放在特定的物理内存空间中才可以,放置字库文件和放置内核文件的原理一样,所以我们来一起实现。
以上代码的注释已经很清楚了,都是调用 r_file_to_padr 函数在映像中查找 kernel.bin 和 font.fnt 文件,并复制到对应的空闲内存空间中。
请注意,由于内核是代码数据,所以必须要复制到指定的内存空间中。r_file_to_padr 函数我已经帮你写好了,其中的原理在前面的内容里已经做了说明,这里不再展开。
建立 MMU 页表数据
前面解决了文件放置问题,我们还要解决另一个问题——建立 MMU 页表。
我们在二级引导器中建立 MMU 页表数据,目的就是要在内核加载运行之初开启长模式时,MMU 需要的页表数据已经准备好了。
由于我们的内核虚拟地址空间从 0xffff800000000000 开始,所以我们这个虚拟地址映射到从物理地址 0 开始,大小都是 0x400000000 即 16GB,也就是说我们要虚拟地址空间:0xffff800000000000~0xffff800400000000 映射到物理地址空间 0~0x400000000。
我们为了简化编程,使用长模式下的 2MB 分页方式,下面我们用代码实现它,如下所示。
这个函数的代码写得非常简单,映射的核心逻辑由两重循环控制,外层循环控制页目录指针顶,只有 16 项,其中每一项都指向一个页目录,每个页目录中有 512 个物理页地址。
物理地址每次增加 2MB,这是由 26~30 行的内层循环控制,每执行一次外层循环就要执行 512 次内层循环。
最后,顶级页目录中第 0 项和第 ((KRNL_VIRTUAL_ADDRESS_START) >> KPML4_SHIFT) & 0x1ff 项,指向同一个页目录指针页,这样的话就能让虚拟地址:0xffff800000000000~0xffff800400000000 和虚拟地址:0~0x400000000,访问到同一个物理地址空间 0~0x400000000,这样做是有目的,内核在启动初期,虚拟地址和物理地址要保持相同。
设置图形模式
在计算机加电启动时,计算机上显卡会自动进入文本模式,文本模式只能显示 ASCII 字符,不能显示汉字和图形,所以我们要让显卡切换到图形模式。
切换显卡模式依然要用 BIOS 中断,这个调用原理我们前面已经了如指掌。在实模式切换显卡模式的汇编代码,我已经帮你写好了,下面我们只要写个 C 函数调用它们就好了,代码如下所示。
void init_graph(machbstart_t* mbsp)
{
graph_t_init(&mbsp->mb_ghparm);
get_vbemode(mbsp);
get_vbemodeinfo(mbsp);
set_vbemodeinfo();
return;
}
上面 init_graph 函数中的这些处理 VBE 模式的代码,我已经帮你写好,你可以自己在 graph.c 文件查看。
什么?你不懂 VBE,其实我开始也不懂,后来通过搜寻资料才知道。
这里我们选择使用了 VBE 的 118h 模式,该模式下屏幕分辨率为 1024x768,显存大小是 16.8MB。显存开始地址一般为 0xe0000000。
屏幕分辨率为 1024x768,即把屏幕分成 768 行,每行 1024 个像素点,但每个像素点占用显存的 32 位数据(4 字节,红、绿、蓝、透明各占 8 位)。我们只要往对应的显存地址写入相应的像素数据,屏幕对应的位置就能显示了。
每个像素点,我们可以用如下数据结构表示:
typedef struct s_PIXCL
{
u8_t cl_b;
u8_t cl_g;
u8_t cl_r;
u8_t cl_a;
}__attribute__((packed)) pixcl_t;
#define BGRA(r,g,b) ((0|(r<<16)|(g<<8)|b))
typedef u32_t pixl_t;
我们再来看看屏幕像素点和显存位置对应的计算方式:
串联
好了,所有的实施工作的函数已经完成了,现在我们需要在 init_bstartparm() 函数中把它们串联起来,即按照事情的先后顺序,依次调用它们完成相应的工作,实现检查、收集机器信息,设置工作环境。
void init_bstartparm()
{
machbstart_t *mbsp = MBSPADR;
machbstart_t_init(mbsp);
init_chkcpu(mbsp);
init_mem(mbsp);
init_krlinitstack(mbsp);
init_krlfile(mbsp);
init_defutfont(mbsp);
init_meme820(mbsp);
init_bstartpages(mbsp);
init_graph(mbsp);
return;
}
到这里,init_bstartparm() 函数就成功完成了它的使命。
显示 Logo
前面我们已经设置了图形模式,也应该要展示一下了,检查一下工作成果。
我们来显示一下我们内核的 logo。其实在二级引导器中,我已经帮你写好了显示 logo 函数,而 logo 文件是个 24 位的位图文件,目前为了简单起见,我们只支持这种格式的图片文件。下面我们去调用这个函数。
在图格式的文件中,除了文件头的数据就是图形像素点的数据,只不过 24 位的位图每个像素占用 3 字节,并且位置是倒排的,即第一个像素的数据是在文件的最后,依次类推。我们只要依次将位图文件的数据,按照倒排次序写入显存中,这样就可以显示了。
我们需要把二级引导器的文件和 logo 文件打包成映像文件,然后放在虚拟硬盘中。
复制文件到虚拟硬盘中得先 mount,然后复制,最后转换成 VDI 格式的虚拟硬盘,再挂载到虚拟机上启动就行了。这也是为什么要手动建立硬盘的原因,打包命令如下。
如果手动打命令对你来说还是比较难,也别担心,我已经帮你写好了 make 脚本,你只需要进入代码目录中 make vboxtest 就行了,运行结果如下 。

代码运行结果示意图
啊哈!终于显示了 logo。是不是挺有成就感的?这至少证明我们辛苦写的代码是正确的。
但是目前我们的代码执行流还在二级引导器中,我们的目的是开发自己的操作系统,不,我们是要开发 Cosmos。
后面,我们正式用 Cosmos 命名我们的操作系统。Cosmos 可以翻译成宇宙,尽管它刚刚诞生,但我对它充满期待,所以用了这样一个能够“包括万物,包罗万象”的名字。
进入 Cosmos
我们在调用 Cosmos 第一个 C 函数之前,我们依然要写一小段汇编代码,切换 CPU 到长模式,初始化 CPU 寄存器和 C 语言要用的栈。因为目前代码执行流在二级引导器中,进入到 Cosmos 中这样在二级引导器中初始过的东西都不能用了。
因为 CPU 进入了长模式,寄存器的位宽都变了,所以需要重新初始化。让我们一起来写这段汇编代码吧,我们先在 Cosmos/hal/x86/ 下建立一个 init_entry.asm 文件,写上后面这段代码。
上述代码中,1~11 行表示加载 70~75 行的 GDT,13~17 行是设置 MMU 并加载在二级引导器中准备好的 MMU 页表,19~30 行是开启长模式并打开 Cache,34~54 行则是初始化长模式下的寄存器,55~61 行是读取二级引导器准备的机器信息结构中的栈地址,并用这个数据设置 RSP 寄存器。
最关键的是 63~66 行,它开始把 8 和 hal_start 函数的地址压入栈中。dw 0xcb48 是直接写一条指令的机器码——0xcb48,这是一条返回指令。这个返回指令有点特殊,它会把栈中的数据分别弹出到 RIP,CS 寄存器,这正是为了调用我们 Cosmos 的第一个 C 函数 hal_start。
重点回顾
这是我们设置工作模式与环境的最后一课,到此为止我们的二级引导器已经建立起来了,成功从 GRUB 手中接过了权柄,开始了它自己的一系列工作,二级引导器完成的工作不算少,我来帮你梳理一下,重点如下。
1. 二级引导器彻底摆脱了 GRUB 的控制之后,就开始检查 CPU,获取内存布局信息,确认是不是我们要求的 CPU 和内存大小,接着初始化内核栈、放置好内核文件和字库文件,建立 MMU 页表数据和设置好图形模式,为后面运行内核做好准备。
2. 当二级引导器完成了上述功能后,就会显示我们操作系统的 logo,这标志着二级引导器所有的工作一切正常。
3. 进入 Cosmos,我们的二级引导器通过跳转到 Cosmos 的入口,结束了自己光荣使命,Cosmos 的入口是一小段汇编代码,主要是开启 CPU 的长模式,最后调用了 Cosmos 的第一个 C 函数 hal_start。
你想过吗?我们的二级引导器还可以做更多的事情,其实还可以在二级引导器中获取 ACPI 表,进而获取 CPU 数量和其它设备信息,期待你的实现。
思考题
请你想一下,init_bstartparm() 函数中的 init_mem820() 函数,这个函数到底干了什么?
欢迎你在留言区跟我互动。如果你身边有朋友对手写操作系统有热情,也欢迎你把这节课转发给他。