概述
- 手写Linux x86操作系统是一项涉及计算机底层原理、汇编语言、C语言和操作系统内核设计的复杂工程,需分阶段逐步实现。
- 视频教程:
https://pan.quark.cn/s/ba54cd9d2ebc
一、前置知识与环境准备
在开始编写代码前,需掌握底层知识并搭建开发环境,确保能够编译、调试和运行操作系统代码。
1.1 必备前置知识
手写操作系统需要深入理解计算机底层逻辑,核心知识包括:
- x86架构基础:实模式(16位)、保护模式(32位)/长模式(64位)的内存寻址方式、段寄存器(CS/DS/ES等)、页表机制、中断控制器(8259 PIC)、CPU特权级(Ring 0~3)。
- 汇编语言:x86 16位汇编(用于引导程序)和32位汇编(用于内核底层操作,如中断处理、上下文切换),需熟悉NASM语法(常用的操作系统开发汇编语法)。
- C语言底层特性:指针操作(直接访问内存地址)、结构体(描述进程、页表等数据结构)、函数指针(中断处理表、系统调用表)、无标准库编程(内核无法依赖glibc,需自己实现内存分配、字符串操作等基础函数)。
- 操作系统核心概念:进程/线程管理(PCB、调度算法)、内存管理(分页/分段、物理内存分配)、中断与异常处理、系统调用、文件系统(inode、目录结构)、设备驱动(字符设备/块设备)。
1.2 开发环境搭建
需搭建支持x86交叉编译、镜像生成和虚拟机调试的环境,推荐使用Linux主机(Ubuntu 20.04+) ,核心工具如下:
工具名称 | 作用 | 安装命令(Ubuntu) |
---|---|---|
NASM | 汇编器,编译16位/32位x86汇编代码 | sudo apt install nasm |
GCC x86交叉编译器 | 编译32位内核C代码(避免主机编译器依赖64位特性) | sudo apt install gcc-multilib |
LD | 链接器,将汇编目标文件和C目标文件链接为内核镜像 | 随GCC安装,无需额外操作 |
QEMU | 虚拟机,运行操作系统镜像并支持调试 | sudo apt install qemu-system-x86 |
GDB | 调试工具,配合QEMU调试内核代码 | sudo apt install gdb |
dd | 生成磁盘镜像文件 | 系统自带 |
parted | 分区工具(可选,用于复杂磁盘布局) | sudo apt install parted |
二、阶段1:实现引导程序(Bootloader)
计算机启动时,CPU首先执行BIOS(基本输入输出系统),BIOS会检测硬件并将硬盘第一个扇区(MBR,主引导记录,512字节)加载到内存0x7C00
处,然后跳转到该地址执行。引导程序的核心任务是:将内核加载到内存,并从实模式切换到保护模式,最终跳转到内核入口。
2.1 编写MBR引导程序(16位汇编)
MBR是引导的第一阶段,需实现硬件检测、读取内核到内存、切换保护模式的基础工作。以下是简化的MBR代码(mbr.asm
):
nasm
org 0x7C00 ; 告诉汇编器,代码加载到0x7C00处
bits 16 ; 16位实模式
start:
; 初始化段寄存器(实模式下,段地址+偏移地址=物理地址)
mov ax, 0x00
mov ds, ax ; 数据段寄存器=0x00
mov es, ax ; 附加段寄存器=0x00
mov ss, ax ; 栈段寄存器=0x00
mov sp, 0x7C00 ; 栈指针指向0x7C00(栈向下生长,避免覆盖代码)
; 清屏(BIOS中断0x10,功能号0x06:滚动清屏)
mov ah, 0x06
mov al, 0x00 ; 滚动行数=0(清屏)
mov ch, 0x00 ; 左上角行号
mov cl, 0x00 ; 左上角列号
mov dh, 0x18 ; 右下角行号(24行)
mov dl, 0x4F ; 右下角列号(80列)
mov bh, 0x07 ; 字符属性(黑底白字)
int 0x10
; 显示引导信息(BIOS中断0x10,功能号0x13:显示字符串)
mov ah, 0x13
mov al, 0x01 ; 字符串模式:显示后光标移动
mov bh, 0x00 ; 页号
mov bl, 0x02 ; 字符属性(绿底黑字)
mov cx, msg_len ; 字符串长度
mov dh, 0x00 ; 行号
mov dl, 0x00 ; 列号
push ax
mov ax, ds
mov es, ax ; ES=DS(字符串在DS段)
pop ax
mov bp, boot_msg ; BP=字符串偏移地址
int 0x10
; 读取内核到内存0x10000处(BIOS中断0x13,功能号0x02:读扇区)
mov ah, 0x02 ; 功能号:读扇区
mov al, 0x08 ; 读取扇区数(假设内核占8个扇区,可根据实际调整)
mov ch, 0x00 ; 磁道号(0号磁道)
mov cl, 0x02 ; 扇区号(从2号扇区开始,1号扇区是MBR)
mov dh, 0x00 ; 磁头号(0号磁头)
mov dl, 0x80 ; 驱动器号(0x80=第一块硬盘)
mov bx, 0x1000 ; ES:BX = 目标地址(0x0000:0x1000 = 0x10000)
mov es, bx
mov bx, 0x0000
int 0x13 ; 调用BIOS中断读扇区
jc read_error ; 若CF=1,读取失败,跳转报错
; 准备切换到保护模式:开启A20地址线(实模式下A20被屏蔽,只能访问1MB内存)
in al, 0x92 ; 读取端口0x92(系统控制端口)
or al, 0x02 ; 置位第1位(开启A20)
out 0x92, al ; 写回端口
; 加载全局描述符表(GDT),保护模式必备(定义段的基地址、限长、权限)
lgdt [gdt_descriptor]
; 切换到保护模式:将CR0寄存器的PE位(第0位)置1
mov eax, cr0
or eax, 0x01 ; PE=1(保护模式使能)
mov cr0, eax
; 远跳转到代码段(清空流水线,确保保护模式生效)
jmp CODE_SEG:init_protected
; 读取扇区失败处理
read_error:
mov ah, 0x13
mov al, 0x01
mov bh, 0x00
mov bl, 0x04 ; 红底黑字(错误提示)
mov cx, err_len
mov dh, 0x01
mov dl, 0x00
push ax
mov ax, ds
mov es, ax
pop ax
mov bp, err_msg
int 0x10
jmp $ ; 死循环
; 全局描述符表(GDT):保护模式下,内存访问需通过段描述符
; GDT格式:8字节/描述符,包含基地址(32位)、限长(20位)、权限位
gdt_start:
; 空描述符(GDT第一个描述符必须为空)
gdt_null:
dd 0x00000000
dd 0x00000000
; 代码段描述符:基地址=0x00000000,限长=0xFFFFF(4GB,页粒度),权限=Ring 0(内核级)
gdt_code:
dw 0xFFFF ; 限长(低16位)
dw 0x0000 ; 基地址(低16位)
db 0x00 ; 基地址(中8位)
db 0x9A ; 权限位:Present=1, DPL=0, Code=1, Non-conforming=1, Readable=1
db 0xCF ; 限长(高4位)+ 粒度位:Granularity=1(4KB页), 32位模式=1
db 0x00 ; 基地址(高8位)
; 数据段描述符:基地址=0x00000000,限长=0xFFFFF,权限=Ring 0
gdt_data:
dw 0xFFFF ; 限长(低16位)
dw 0x0000 ; 基地址(低16位)
db 0x00 ; 基地址(中8位)
db 0x92 ; 权限位:Present=1, DPL=0, Data=1, Expand-up=1, Writable=1
db 0xCF ; 限长(高4位)+ 粒度位
db 0x00 ; 基地址(高8位)
gdt_end:
; GDT描述符:告诉CPU GDT的基地址和长度(长度=GDT结束地址-GDT开始地址-1)
gdt_descriptor:
dw gdt_end - gdt_start - 1 ; GDT长度(16位)
dd gdt_start ; GDT基地址(32位)
; 定义段选择子(GDT中描述符的索引,左移3位+权限位)
CODE_SEG equ gdt_code - gdt_start ; 代码段选择子(索引=1,DPL=0)
DATA_SEG equ gdt_data - gdt_start ; 数据段选择子(索引=2,DPL=0)
; 字符串定义
boot_msg db "Booting My Linux OS..."
msg_len equ $ - boot_msg
err_msg db "Error: Failed to load kernel!"
err_len equ $ - err_msg
; 填充MBR到512字节,末尾添加启动标志0xAA55(BIOS识别MBR的标志)
times 510 - ($ - $$) db 0x00
dw 0xAA55
2.2 编译与验证MBR
-
编译MBR :使用NASM将汇编代码编译为二进制文件:
bashnasm -f bin mbr.asm -o mbr.bin
-
生成磁盘镜像 :创建一个10MB的空镜像,将MBR写入镜像的第一个扇区:
bashdd if=/dev/zero of=myos.img bs=1M count=10 # 创建10MB空镜像 dd if=mbr.bin of=myos.img bs=512 count=1 conv=notrunc # 写入MBR(不截断镜像)
-
运行MBR :使用QEMU启动镜像,验证引导信息是否正常显示:
bashqemu-system-i386 -drive format=raw,file=myos.img
若正常,QEMU窗口会显示"Booting My Linux OS...";若读取失败(如内核未写入),会显示错误信息。
2.3 实现第二阶段引导(可选,加载内核)
MBR仅512字节,无法容纳复杂逻辑(如读取大内核、解析ELF格式),因此通常需要第二阶段引导程序(Loader) :
- 在MBR中添加代码,将Loader从硬盘扇区(如2~9扇区)加载到内存
0x9000
处。 - Loader使用32位汇编(保护模式),实现ELF格式解析(Linux内核通常是ELF文件),将内核的代码段、数据段加载到指定内存地址(如代码段到
0xC0000000
,内核虚拟地址起始)。 - 最终跳转到内核入口地址(ELF文件头中定义的
e_entry
)。
三、阶段2:实现内核核心模块(32位C+汇编)
内核是操作系统的核心,需实现内存管理、中断处理、进程调度等基础功能。以下从内核入口开始,逐步实现关键模块。
3.1 内核入口:从引导程序跳转到内核
引导程序(Loader)完成保护模式切换和ELF解析后,会跳转到内核入口函数(通常是kernel_main
)。内核入口需先初始化硬件(如中断控制器)、设置页表,再进入主循环。
3.1.1 内核入口汇编(kernel_entry.asm
)
用于初始化栈和段寄存器,调用C语言写的kernel_main
:
nasm
bits 32 ; 32位保护模式
global kernel_entry ; 导出入口函数,供Loader调用
extern kernel_main ; 声明C语言实现的kernel_main
; 内核栈:在内存0x80000~0x90000处分配16KB栈空间
KERNEL_STACK equ 0x90000
kernel_entry:
; 初始化数据段寄存器(使用GDT中的数据段选择子)
mov ax, DATA_SEG
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax ; 栈段使用数据段
; 初始化栈指针
mov esp, KERNEL_STACK
; 调用C语言内核主函数
call kernel_main
; 内核返回后死循环(防止CPU跑飞)
jmp $
3.1.2 内核主函数(kernel.c
)
内核初始化的入口,先实现简单的屏幕输出(VGA文本模式),再逐步添加其他模块:
c
#include "vga.h" // 自定义VGA输出头文件
// 内核主函数
void kernel_main() {
// 初始化VGA文本模式(清屏,设置光标位置)
vga_init();
// 在屏幕上显示内核启动信息
vga_print("Welcome to My Linux OS Kernel!\n");
vga_print("Kernel is running in 32-bit Protected Mode.\n");
// 内核主循环(后续添加进程调度、中断处理等)
while (1) {
// 暂时空循环,后续替换为调度逻辑
}
}
3.2 实现VGA文本模式输出(内核调试必备)
内核无法依赖BIOS中断(保护模式下BIOS不可用),需直接操作VGA硬件寄存器实现屏幕输出。VGA文本模式的显存地址为0xB8000
(物理地址),每个字符占2字节:1字节ASCII码 + 1字节属性(前景色+背景色)。
3.2.1 VGA工具函数(vga.h
+ vga.c
)
c
// vga.h
#ifndef VGA_H
#define VGA_H
#include <stdint.h>
#include <stddef.h>
// VGA文本模式属性:高4位背景色,低4位前景色(0=黑,1=蓝,2=绿,3=青,4=红,5=紫,6=棕,7=灰)
#define VGA_COLOR_BLACK 0x0
#define VGA_COLOR_BLUE 0x1
#define VGA_COLOR_GREEN 0x2
#define VGA_COLOR_CYAN 0x3
#define VGA_COLOR_RED 0x4
#define VGA_COLOR_MAGENTA 0x5
#define VGA_COLOR_BROWN 0x6
#define VGA_COLOR_LIGHT_GREY 0x7
// 组合前景色和背景色(默认黑底白字)
#define VGA_ENTRY_COLOR(fg, bg) ((bg << 4) | fg)
#define VGA_DEFAULT_COLOR VGA_ENTRY_COLOR(VGA_COLOR_LIGHT_GREY, VGA_COLOR_BLACK)
// 组合字符和属性
#define VGA_ENTRY(uc, color) ((uint16_t)(uc) | (uint16_t)(color) <<