开发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;
}