实现一个最简单的内核

编写一个引导程序

首先我们要先要编写一个汇编程序,使用C语言作为高级语言不能直接控制硬件,而且 C 语言的函数调用、函数传参,都需要用栈。我们需要先要为C语言提供一个工作环境。

MBT_HDR_FLAGS EQU 0x00010003;flag字段,指出OS映像需要引导程序提供或支持的特性
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

我们来对上面的汇编做一些详细的解释:

  1. 我们先来看代码的第1行到第39行

    首先是MBT_HDR_FLAGS,其定义的是flags字段,用来指出OS映像需要引导程序提供或支持的特性。其中0-15位指出需求:如果引导程序发现某些值杯设置,但出于某种原因无法满足其需求,则需要告知用户,并拒绝加载操作系统映像。其中16-31位为可选特性,与低16位不同,如果无法满足要求可以忽视并照常进行。自然,所有flags尚未定义的位都必须设置为0。flags字段的作用是用于版本控制以及简单的功能选择。

    其中第0位若为1,那么所有与操作系统一起加载的引导模块必须在页面(4KB)边界上对齐。有些操作系统能够在启动时将包含引导模块的页直接映射到一个分页的地址空间,因此需要引导模块是页对齐的。

    如果第1位为1则必须通过Multiboot信息结构的**mem_***域包括可用内存的信息。

    如果引导程序能够传递内存分布(**mmap_***域)并且它确实存在,则也包括它。

    如果第2位为1,有关视频模式表(参见引导信息格式)的信息必须对内核有效

    如果flags字段中的第16位被设置,那么Multiboot头部中偏移量为12-28的字段是有效的,引导加载器应该使用它们而不是实际可执行头部中的字段来计算加载操作系统镜像的位置。如果内核镜像是ELF格式的,那么这个信息不需要被提供,但是如果镜像是a.out格式或者其他格式的,那么它必须被提供。符合规范的引导加载器必须能够加载那些要么是ELF格式的,要么包含了嵌入在Multiboot头部中的加载地址信息的镜像;它们也可以直接支持其他可执行格式,比如特定的a.out变体,但是不是必须的。

    接下来MBT_HDR_MAGIC则代表着多引导协议头魔数,MBT_HDR2_MAGIC第二版多引导协议头魔数,可以告诉计算机使用的是何种协议。

    接下来从第10行到第19行代表GRUB所需要的头,21到39行为GRUB2所需要的头。包含这两个头是为了兼容GRUB与GRUB2。

  2. 代码43~5行,关掉中断,设定 CPU 的工作模式。

  3. 代码53~72行,初始化 CPU 的寄存器和 C 语言的运行环境。

  4. 代码77~86行,GDT_START 开始的,是 CPU 工作模式所需要的数据。

编写C代码

//main.c
#include "vgastr.h"
void main()
{
printf("Hello OS!");
return;
}

主函数实现了一个打印”Hello OS!”字符串的功能,但是我们的操作系统中没有库函数,所以需要我们自定义一个printf函数打印到屏幕上。

//vgastr.c
void _strwrite(char * str)
{
char* p_str = (char*)(0xb8000);
while (*str)
{
*p_str = *str;
p_str+=2;
str++;
}
return;
}

void printf(char *str,...)
{
_strwrite(str);
return;
}

_strwrite函数将我们要打印的字符打印到屏幕之上从0xb8000开始每两个字符代表一个字母,第一个字节代表着字符,第二个字节代表着字体的颜色,其字符编码通常是 utf8,而 utf8 编码对 ASCII 字符是兼容的。

//vgastr.h
void _strwrite(char * str);
void printf(char *str,...);

编译和部署

通过makefile将代码编译,得到HelloOS.bin

编译的结果

接下来就是部署了

我们进入etc\default\grub文件

首先将GRUB_TIMEOUT_STYLE的值从hidden改为menu,并将GRUB_TIMEOUT的值从0改为30

修改grub文件

然后更新grub配置

sudo update-grub

然后我们修改/boot/grub/grub.cfg文件,增加HelloOS项

menuentry 'HelloOS' {
insmod part_gpt #GRUB加载分区模块识别分区
insmod ext2 #GRUB加载ext文件系统模块识别ext文件系统
set root='hd0,gpt3' #注意boot目录挂载的分区,这是我机器上的情况
multiboot2 /boot/HelloOS.bin #GRUB以multiboot2协议加载HelloOS.bin
boot #GRUB启动HelloOS.bin
}

根据df /root/的命令来查看set root应该设置为什么

df /root/命令

其中的“sda3”就是硬盘的第四个分区(硬件分区选择 MBR),但是 GRUB 的 menuentry 中不能写 sda3,而是要写“hd0,gpt3”,这是 GRUB 的命名方式,hd0 表示第一块硬盘,我的虚拟机是使用gpt分区表所以是gpt3。

可以根据grub.cfg其他的menuentry来确定。

grub.cfg

最后将HelloOS.bin文件复制到/boot/目录下,重启虚拟机。

选择HelloOS

选择HelloOS,可以看到HelloOS的字样。

完成!

这样我们就实现了一个最简单的OS了!

Author: Kr0emer
Link: http://kr0emer.com/2023/11/12/实现一个最简单的内核/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.