前言
简介
Snailix内核由Lettle在无vibe coding环境下编写,旨在锻炼C语言编程及处理常规操作系统问题的能力。
在开始阅读本文前,我希望你对汇编有一点点了解,并且在计算机组成原理学科有一定基础,起码知道x86架构是什么,都有哪些寄存器。(不知道的话快点去问AI)
本文是对Snailix内核开发的一个极简概述,只包含最基础最重要的部分,帮助读者快速了解一款操作系统内核是怎样工作的,应该从何开始写起。
汇编代码
我还是在这里简单说一下怎样阅读汇编程序吧。
汇编程序的执行是从最上面往下无脑顺序执行,除非执行到了跳转指令,那就按照跳转指令说的那样跳转到相应位置继续顺序执行。
此外,你可能会看到一些长得像函数头的一行东西,那是标签,用于标记一个代码位置,跟函数头有点像吧。比如:
_loader:
; Print a string to the screen
mov si, loading
call print
; Check memory size
mov ebx, 0
mov di, memCheckBuf
.memCheckLoop:
; Skip the implementation...
jmp ready_to_protected_mode这里面的 _loader 和 .memCheckLoop 都是一个标签,在阅读代码的时候无视它们,无脑从上到下逐行运行就行了。
还有一件事,下面这条代码是死循环代码,即程序执行到这里就会陷入循环,不再执行其他指令:
jmp $内核编写思路
本文将简化整个内核编写的过程,只将最基础最重要的部分展示出来,忽略掉一些功能明确、又不必纠结如何实现的功能函数。
目前,文章中包含如下程序:
boot.asm这里整个内核最初始的入口,在硬件进入这个程序时,x86架构的CPU会处于实模式,此时执行的是16位程序,最大寻址空间是1MB。
我们需要把
boot程序编写成刚好512字节大小并写入引导扇区,因为**引导扇区(boot sector)**的大小固定为 512 字节 ,这是由 BIOS 的引导机制决定的。boot程序在最后的两个字节上写入db 0x55, 0xAA作为结尾,这样就会被识别为一个正确的引导扇区。loader.asm在这个程序中,我们突破了512字节的限制,你可以编写任意长的程序了。在这里,内核将要从16位的实模式跳转到32位的保护模式,这样就可以利用4GB的内存了(至于为什么,这就要去学习什么是实模式和保护模式了)
start.asm内核即将进入C语言的世界了,在此之前先跳入一个汇编编写的入口,做一些只有汇编方便做的工作,然后跳转到C语言编写的内核主程序。
main.c欢迎来到C语言的世界,在这里你可以想编写什么就编写什么了!
内核的启动过程如下:
boot(16位, 1MB内存) --> loader(32位,4GB内存) --> start(汇编) --> main(C语言)快速开始
环境搭建
Linux 操作系统环境(可使用 WSL)
x86_64-elf 编译套装
x86_64-elf-gcc
x86_64-elf-ld
x86_64-elf-objcopy
x86_64-elf-nm
nasm
qemu-system-i386
运行
仅需执行一条命令
make run内核详解
0 硬盘
首先,我们需要一个介质来存储整个内核的数据和代码。
使用Linux中的dd命令即可创建出一个磁盘文件,这可以作为我们内核的镜像来使用。
dd if=/dev/zero of=Snailix.img bs=1M count=16这样就可以创建出一个名为Snailix.img的文件,以后就用它来装载所有文件了!
1 Boot
1.1 编写程序
想要创建一个直接运行在硬件上的操作系统,第一步一定是要编写一个离硬件最近的程序。
什么样的程序离硬件最近呢?答案是:汇编程序。
在本文我们将要使用Intel语法的汇编开始编写Snailix的引导程序。
; Filename: boot.asm
[ORG 0x7C00]
_boot:
; Set the screen to TEXT MODE and clear the screen
mov ax, 3
int 0x10
; Initialize the segment register
mov ax, 0
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x7c00
; Read the loader program to 0x1000
mov edi, 0x1000
mov ecx, 2
mov bl , 4
call read_disk
; Check if the loader program is valid
cmp word [0x1000], 0x2233
jnz error
; Jump to the loader program,
; Don't forget the 2 byte magic number at 0x1000
jmp 0:0x1002
; Block machine here, but that's never going to happen
jmp $
; Skip the 'read_disk' function implementation...
; End of the boot sector
times 510 - ($ - $$) db 0 ; Padding to 510 bytes
db 0x55, 0xAA ; Boot sector flags1.2 功能详解
在该汇编程序中,我们做了如下几件事:
- 清屏,设置屏幕为文本模式
- 初始化段寄存器
- 将
loader程序从硬盘读入内存,放在0x1000内存地址起始的位置 - 使用预先设定的魔数
0x2233检查读取的loader程序是否正确(非必要步骤) - 跳转到
loader程序的第一条指令的位置
看吧,短短几十行汇编指令做了这么多事情,这是因为我忽略了read_disk函数的实现部分。
我们目前不需要知道其中每一句话的全部细节,只需要知道大致做了什么就好,实在想知道细节的话我也会在后面加以补充。
看到这里,你可能会疑惑:为什么我要加载一个Loader程序并跳转过去呢?为什么我不是直接在这里开始编写我的操作系统呢?
1.3 编译程序
接下来将其保存为boot.asm,使用
nasm -f bin boot.asm -o boot.bin命令进行编译,就可以得到这段汇编程序相对应的二进制指令了,这就是我们为Snailix编写的引导程序。
这样的指令可以直接被CPU执行,在本文中我们使用qemu这款软件执行这个引导程序。
别忘了将它写入到我们之前准备好的镜像文件中,使用如下命令:
dd if=boot.bin of=Snailix.img bs=512 count=1 conv=notrunc这样就可以在Snailix.img中写入我们的引导程序了。
1.4 执行结果

运行后,硬件发生的变化如下:
- 屏幕是黑色的,没有任何文字。
ds、es、ss寄存器被初始化为0,sp寄存器被初始化为0x7c00- CPU试图跳转到
0x1002内存地址处继续执行指令
2 Loader
2.1 编写程序
接下来,我们新建一个文件,编写如下代码:
; Filename: loader.asm
; The code will be loaded at 0x1000
[ORG 0x1000]
dw 0x2233 ; Magic number, used to determine whether an error has occurred
_loader:
.memCheckLoop:
; Check the memory...
jmp ready_to_protected_mode
.memCheckFail:
mov si, error ; Skip the 'error' implementation...
call print ; Skip the 'print' implementation...
hlt
jmp $
; Prepare to jump to 32-bit protected mode.
ready_to_protected_mode:
cli ; Disable interrupts
in al, 0x92 ; Read CMOS byte 0x92
or al, 0b10 ; Set bit 1 and bit 2
out 0x92, al ; Write back to CMOS byte 0x92
lgdt [gdt_ptr] ; Load the GDT
mov eax, cr0 ; Move the CR0 register to EAX
or eax, 1 ; Set the PE bit
mov cr0, eax ; Move the value back to CR0
jmp dword code_selector:protected_mode ; Far jump to the protected mode
[bits 32]
protected_mode:
; Set up the segment registers
mov ax, data_selector
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
; Print '32-bit' to the left of the first line.
mov [0xb8094], byte '3'
mov [0xb8096], byte '2'
mov [0xb8098], byte '-'
mov [0xb809a], byte 'b'
mov [0xb809c], byte 'i'
mov [0xb809e], byte 't'
jmp $
2.2 功能详解
程序开始的2字节装载了0x2233这个魔数(magic number),用于在boot.asm中进行验证。
从第2个字节之后正式进入代码部分,即从_loader标签处开始整个程序的执行。
_loader做了两件事:
- 检查内存(省略实现方式)
- 跳转到
ready_to_protected_mode标签
ready_to_protected_mode做了如下事情:
- 关中断
- 向
0x92端口输出数据(重要步骤,但不必在意) - 加载
GDT - 为
cr0寄存器设置PE位为1(重要步骤,但不必在意) - 跳转到
protected_mode32位代码段
2.3 编译程序
将其保存为loader.asm,使用
nasm -f bin loader.asm -o loader.bin命令进行编译,即可得到我们的 Loader 程序了!
2.4 执行结果

运行后,硬件发生的变化如下:
gdtr寄存器指向了一个用户编写的GDTcr0寄存器被正确设置了,CPU可以进入32位保护模式- 跳转到了32位的代码段进行执行
- 在32位代码段中设置了显存,屏幕在右上角显示了
32-bit的字样(这里在右上角显示是为了将该字样作为一个标志,而不是一个提示信息,真正的提示信息将会从屏幕左边逐行输出)

