内核开发

 

NASM汇编初探(入门教程) 搭建i386交叉编译环境

开发32位内核

基本开发环境设置

首先需要一个编译工具链(如 GCC 或 Clang), 以及一个虚拟化环境(如 QEMU)用于测试内核

使用 QEMU 运行操作系统, 并使用 nasm 来编写启动引导程序

创建启动引导程序(Bootloader)

启动引导程序负责加载内核到内存并启动它

使用汇编语言编写一个简单启动引导程序, 它会执行

  • 初始化 x86 实模式

  • 加载内核到内存

  • 将控制权转交给内核

编写 boot.asm

编写一个简单16 位汇编启动引导程序, 它会加载内核并将控制权交给内核:

复制代码
; boot.asm

; 16位代码模式
bits 16      

; 代码起始地址为0x7C00(BIOS加载位置)
org 0x7C00          

start:
    ; 清除中断标志, 禁用硬件中断
    cli
    ; 清除方向标志, 字符串操作时自动递增           
    cld               

    ; 将字符串地址加载到SI寄存器中
    mov si, msg_loading
    call print_string

    ; 加载内核到0x1000地址
    mov ax, 0x1000
    ; 设置ES段寄存器为0x1000, 表示内核加载段地址为0x1000
    mov es, ax        
    call load_kernel

    jmp 0x1000:0x0000

load_kernel:
    ; BIOS中断0x13功能读扇区
    mov ah, 0x02
    ; 每次读取1个扇区
    mov al, 1
    ; 磁道号0
    mov ch, 0x00
    ; 从第二个扇区开始读取(第一个是引导扇区)
    mov cl, 0x02
    ; 磁头号:0
    mov dh, 0x00
    ; 硬盘驱动器号(0x80表示第一块硬盘)
    mov dl, 0x80

    ; 调用BIOS中断读取扇区
    int 0x13

    ; 若CF标志位设置, 则跳转到disk_error处理
    jc load_failed

load_failed:
    mov si, msg_error
    call print_string
    jmp $

print_string:
    ; BIOS中断0x10功能号, 显示字符
    mov ah, 0x0E

.repeat:
    ; 从[SI]加载下一个字节到AL, 并递增SI
    lodsb
    ; 检查AL中字符是否为0(字符串结束符)
    cmp al, 0
    ; 若为0, 则跳转到done
    jz .done
    ; 调用BIOS中断显示字符
    int 0x10
    ; 重复, 直到字符串结束
    jmp .repeat

.done:
    ret

; BIOS显示字符串必须以0作为结束符
msg_loading db 'Loading Kernel...', 0
msg_error   db 'Disk read error!', 0

; 填充引导扇区, 确保引导程序正好是512字节
; 510-($-$$)计算了当前代码位置与起始位置偏移量, 然后填充相应空字节0x00, 直到字节数达到510
times 510 - ($ - $$) db 0

; 引导扇区签名(magic number)
; BIOS启动时会检查扇区最后两个字节,若不是0xAA55则BIOS不会将其作为有效引导扇区进行加载
dw 0xAA55          
  • 编译启动引导程序
nasm -f bin boot.asm -o boot.bin

编写内核(kernel.c)

我们将用 C 语言编写一个最简单内核, 它在屏幕上显示一个字符串

这是一个”hello world”内核, 负责将控制权从引导程序接收并输出文本

// kernel.c
void kernel_main() {
    // Video memory starts at 0xB8000 for text mode
    char *video_memory = (char *)0xB8000;
    // Display a simple message
    const char *message = "Hello, MyOS!";
    int i = 0;
   
    // Each character is 2 bytes in video memory: 1 byte character, 1 byte attribute
    while (message[i] != '\0') {
        video_memory[i * 2] = message[i];        // Character
        video_memory[i * 2 + 1] = 0x07;          // Attribute byte: light grey on black
        i++;
    }
}
  • 编译内核

编译内核为 32 位代码, 因为要切换到保护模式后执行 C 代码

i386-elf-gcc -ffreestanding -m32 -c kernel.c -o kernel.o
i386-elf-ld -o kernel.bin -Ttext 0x1000 --oformat binary kernel.o
  • 链接内核与启动引导程序

将编译好 boot.bin 和 kernel.bin 合并为一个镜像文件:

cat boot.bin kernel.bin > 操作系统-image.bin

运行

使用 QEMU 运行操作系统

qemu-system-i386 -fda 操作系统-image.bin

这时你应该能在屏幕上看到”Hello, MyOS!”字样

上述步骤实现一个简单从启动引导程序加载内核并输出文本功能

接下来可以进一步扩展内核能, 例如:

进入保护模式:x86 实模式寻址限制为 1MB 内存空间, 进入 32 位保护模式可以解除这个限制, 并让我们使用现代 CPU 功能

中断处理:实现中断描述符表(IDT)并处理硬件中断

内存管理:实现简单内存管理器, 分配和释放内存块

多任务处理:实现进程调度器, 使操作系统能够同时运行多个任务

文件系统:实现简单文件系统, 如 FAT12, 读取磁盘文件

设备驱动:实现键盘和显示器等硬件设备驱动程序

上下文切换

  • jmp_buf

C 语言中一种数据类型, 用于存储程序执行上下文

  • setjmp

setjmp 函数会保存当前上下文信息到 jmp_buf 中, 并返回 0

  • longjmp

longjmp 来恢复这个上下文, 实现从一个函数跳转到另一个函数

单任务

#include <stdio.h>
#include <setjmp.h>

jmp_buf buffer;

void function() {
    printf("Inside function, jumping back...\n");
    // 跳回主函数
    longjmp(buffer, 1);
}

int main() {
    if (setjmp(buffer) == 0) {
        printf("Calling function...\n");
        // 调用函数
        function(); 
    } else {
        printf("Returned to main after longjmp.\n");
    }
    return 0;
}

jmp_buf 是实现非局部跳转关键, 允许程序在需要时恢复到先前状态, 这在处理异常、状态机等场景中非常有用

多任务

#include <stdio.h>
#include <setjmp.h>
#include <unistd.h>

#define STACK_SIZE 1024

typedef struct {
    jmp_buf context;
    char stack[STACK_SIZE];
} Task;

Task task1, task2;

// 0: task1, 1: task2
int current_task = 0; 

void task1_function() {
    while (1) {
        printf("Task 1 running\n");
        sleep(1);
        longjmp(task2.context, 1);  // 切换到任务2
    }
}

void task2_function() {
    while (1) {
        printf("Task 2 running\n");
        sleep(1);
        longjmp(task1.context, 1);  // 切换到任务1
    }
}

void scheduler() {
    if (setjmp(task1.context) == 0) {
        task1_function();
    }
    if (setjmp(task2.context) == 0) {
        task2_function();
    }
}

int main() {
    scheduler();
    return 0;
}