从0手写自己的Linux x86操作系统(视频教程)

概述

  • 手写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

  1. 编译MBR :使用NASM将汇编代码编译为二进制文件:

    bash 复制代码
    nasm -f bin mbr.asm -o mbr.bin
  2. 生成磁盘镜像 :创建一个10MB的空镜像,将MBR写入镜像的第一个扇区:

    bash 复制代码
    dd 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(不截断镜像)
  3. 运行MBR :使用QEMU启动镜像,验证引导信息是否正常显示:

    bash 复制代码
    qemu-system-i386 -drive format=raw,file=myos.img

    若正常,QEMU窗口会显示"Booting My Linux OS...";若读取失败(如内核未写入),会显示错误信息。

2.3 实现第二阶段引导(可选,加载内核)

MBR仅512字节,无法容纳复杂逻辑(如读取大内核、解析ELF格式),因此通常需要第二阶段引导程序(Loader)

  1. 在MBR中添加代码,将Loader从硬盘扇区(如2~9扇区)加载到内存0x9000处。
  2. Loader使用32位汇编(保护模式),实现ELF格式解析(Linux内核通常是ELF文件),将内核的代码段、数据段加载到指定内存地址(如代码段到0xC0000000,内核虚拟地址起始)。
  3. 最终跳转到内核入口地址(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) << 
相关推荐
铭哥的编程日记2 小时前
《Linux 基础 IO 完全指南:从文件描述符到缓冲区》
android·linux·运维
lang201509282 小时前
MySQL Online DDL:高性能表结构变更指南
数据库·mysql
阿沁QWQ3 小时前
MySQL程序简介
数据库·mysql
一 乐3 小时前
社区互助养老系统|基于java和小程序的社区互助养老系统小程序设计与实现(源码+数据库+文档)
java·数据库·spring boot·小程序·论文·毕设·社区互助养老系统小程序
xiatianit3 小时前
【centos生产环境搭建(二)redis安装】
运维
微步_ym3 小时前
RabbitMQ:在Linux上安装RabbitMQ
linux·rabbitmq·erlang
CC.GG4 小时前
【Linux】倒计时和进度条实现
linux
从零开始学习人工智能4 小时前
Apache Airflow:让复杂工作流自动化变得简单优雅
运维·自动化·apache
Code Warrior4 小时前
【Linux】库的制作与原理(1)
linux