2164 字
11 分钟
Snailix 内核
2026-04-09
OS
/
C
Python-Lettle
/
Snailix
Waiting for api.github.com...
00K
0K
0K
Waiting...

前言#

简介#

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 flags

1.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 执行结果#

boot

运行后,硬件发生的变化如下:

  • 屏幕是黑色的,没有任何文字。
  • ds、es、ss寄存器被初始化为0sp寄存器被初始化为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 执行结果#

loader

运行后,硬件发生的变化如下:

  • gdtr寄存器指向了一个用户编写的GDT
  • cr0寄存器被正确设置了,CPU可以进入32位保护模式
  • 跳转到了32位的代码段进行执行
  • 在32位代码段中设置了显存,屏幕在右上角显示了32-bit的字样(这里在右上角显示是为了将该字样作为一个标志,而不是一个提示信息,真正的提示信息将会从屏幕左边逐行输出)
Snailix 内核
https://lettle.cn/posts/snailix/
作者
Lettle
发布于
2026-04-09
许可协议
CC BY-NC-SA 4.0