分享
  1. 首页
  2. 文章

从零开发操作系统:从加电自检到内核引导

xiao_wenwen123 · · 110 次点击 · · 开始浏览

从零开发操作系统:从加电自检到内核引导

从零开发操作系统:从加电自检到内核引导的技术拆解

“下仔客”: itxt.top/4753/

操作系统作为硬件与应用软件的 “桥梁”,其启动过程是计算机系统从 “硬件断电” 到 “软件运行” 的关键过渡。对开发者而言,理解 “从加电自检到内核引导” 的完整流程,是从零开发操作系统的第一步,也是掌握系统底层逻辑的核心。本文将以 x86 架构为例,按技术时序拆解操作系统启动的四大核心阶段 —— 加电自检(POST)、BIOS 初始化与引导扇区加载、实模式到保护模式切换、内核加载与启动,用通俗的语言解析每个环节的原理、实现难点与关键代码逻辑,为入门操作系统开发的读者提供清晰的实践路径。

一、预备知识:操作系统启动的 “技术坐标系”

在深入具体阶段前,需先建立两个核心认知,避免陷入技术细节的混乱:

1.1 硬件基础:x86 架构的 “启动约定”

x86 架构(如 Intel/AMD 处理器)为操作系统启动制定了明确的硬件规则,核心包括:

  • CPU 工作模式:启动初期 CPU 处于 “实模式”(16 位地址空间,仅能访问 1MB 内存,无内存保护机制),后续需切换到 “保护模式”(32 位 / 64 位地址空间,支持内存分段 / 分页、权限控制),最终才能加载 32 位 / 64 位内核;
  • 内存地址分配:实模式下内存前 640KB 为 “常规内存”(DOS 时代遗留,供 BIOS 与引导程序使用),0xFFFF0~0xFFFFFF 为 “BIOS ROM 地址”(存放 BIOS 固件,加电后 CPU 首先执行此处代码);
  • 引导设备优先级:BIOS 通过 “引导顺序”(如 U 盘→硬盘→光驱)确定从哪个设备加载引导程序,引导设备的第一个扇区(512 字节)被称为 “引导扇区”,是启动流程的关键载体。

1.2 工具准备:从零开发的 “最小环境”

入门阶段无需复杂设备,只需搭建以下基础开发环境:

  • 模拟器:使用 Bochs 或 QEMU(轻量级 x86 模拟器,支持调试与断点,可模拟完整的计算机硬件,避免直接操作真实硬件的风险);
  • 汇编编译器:NASM(跨平台汇编器,语法简洁,支持生成二进制文件,适合编写引导扇区与实模式代码);
  • 调试工具:GDB(配合 QEMU 使用,可单步调试 CPU 指令,查看寄存器与内存数据,定位启动过程中的错误);
  • 二进制编辑工具:HexEdit(查看引导扇区、内核文件的二进制结构,验证代码是否正确写入指定地址)。

二、阶段一:加电自检(POST)—— 硬件的 “自我体检”

当按下计算机电源键,操作系统启动的 “第一缕代码” 并非由开发者编写,而是硬件自带的 “加电自检程序”(Power-On Self-Test,POST),其核心作用是确保硬件能正常工作。

2.1 POST 的核心任务:硬件可用性验证

POST 由 BIOS(Basic Input/Output System,基本输入输出系统)固件(存放在主板 ROM 芯片中)执行,流程约持续 1-3 秒,关键步骤包括:

  1. CPU 初始化:检测 CPU 型号、核心数,初始化 CPU 寄存器(如将指令指针寄存器 IP 设为 0xFFFF,段寄存器 CS 设为 0xF000,因此 CPU 第一条执行地址为 CS:IP=0xF000:0xFFFF,即 0xFFFF0,对应 BIOS ROM 的起始地址);
  1. 关键硬件检测:依次检测内存(通过写入 / 读取数据验证内存芯片是否正常)、显卡(初始化显示控制器,输出 POST 进度到屏幕)、硬盘 / U 盘(检测设备是否存在,读取设备参数)、键盘 / 鼠标(验证输入设备响应);
  1. 错误处理:若检测到硬件故障(如内存损坏、无引导设备),通过 “蜂鸣器报警”(如连续短鸣表示内存错误)或屏幕提示告知用户,检测通过则进入下一阶段。

2.2 开发者视角:POST 的 “不可编程性”

POST 由硬件厂商固化在 BIOS 中,开发者无法修改其逻辑,但需了解两个关键影响:

  • POST 后的硬件状态:检测通过后,BIOS 会将硬件参数(如内存大小、硬盘数量、显卡型号)存入 “BIOS 数据区”(内存 0x400~0x4FF 地址段,如 0x413 单元存放常规内存大小,单位为 KB),后续引导程序可读取这些数据适配硬件;
  • 引导设备的 “激活”:POST 完成后,BIOS 会按 “引导顺序” 扫描引导设备,检查设备第一个扇区(512 字节)是否符合 “引导扇区标识”—— 即最后两个字节为 0x55AA(这是 BIOS 识别引导扇区的唯一标志,若不满足则跳过该设备)。

三、阶段二:BIOS 引导扇区加载 —— 从 “硬件” 到 “软件” 的第一步

POST 完成后,BIOS 的核心任务转为 “加载引导扇区”,这是操作系统启动中 “开发者可干预” 的第一个环节,也是从零开发的起点。

3.1 引导扇区的 “硬件约定”

引导扇区是引导设备的第一个扇区(512 字节),需满足两个硬性条件才能被 BIOS 加载:

  • 大小固定:必须为 512 字节(若代码不足 512 字节,需用 0 填充剩余空间);
  • 标识正确:最后两个字节必须是 0x55AA(BIOS 通过检测这两个字节判断是否为有效引导扇区)。

BIOS 加载引导扇区的流程如下:

  1. 按引导顺序选中引导设备(如 U 盘),读取设备第一个扇区的 512 字节数据;
  1. 检查最后两个字节是否为 0x55AA,若是则将这 512 字节数据加载到内存 0x7C00~0x7DFF 地址段(这是 BIOS 约定的引导扇区加载地址,开发者需确保引导程序在此地址执行);
  1. 将 CPU 控制权交给引导扇区代码 —— 即设置 CS:IP=0x0000:0x7C00,CPU 开始执行引导扇区中的汇编代码。

3.2 实战:编写第一个引导扇区(NASM 实现)

引导扇区代码需用 16 位汇编编写(因 CPU 此时处于实模式),核心功能是 “验证引导成功” 并给出可视化反馈(如在屏幕显示字符)。以下是最小化引导扇区代码示例:

; 引导扇区代码:boot.asm

org 0x7C00 ; 告诉编译器,代码将被加载到0x7C00地址,确保地址引用正确

; 实模式下,屏幕输出字符需通过BIOS中断0x10实现

; AH=0x0E:BIOS中断0x10的功能号,表示“Teletype模式输出字符”

; AL:要输出的ASCII字符

; BH:页号(默认0)

; BL:字符颜色(实模式下仅文本模式有效,如0x07表示黑底白字)

start:

mov ah, 0x0E ; 设置BIOS中断功能为“输出字符”

mov al, 'H' ; 第一个字符'H'

mov bh, 0x00 ; 页号0

mov bl, 0x07 ; 黑底白字

int 0x10 ; 调用BIOS中断,输出'H'

mov al, 'i' ; 第二个字符'i'

int 0x10 ; 输出'i'

mov al, '!' ; 第三个字符'!'

int 0x10 ; 输出'!'

jmp $ ; 无限循环,防止程序执行完后跑飞($表示当前指令地址)

; 填充剩余空间到512字节,确保引导扇区大小正确

times 510 - ($ - $$) db 0 ; $$表示当前段起始地址,$-$计算已编写代码长度,用0填充到510字节

dw 0xAA55 ; 引导扇区标识(小端序,实际存储为0x55AA)

3.3 编译与运行:验证引导扇区

将上述代码编译为二进制文件并在模拟器中运行,验证引导效果:

  1. 编译代码:使用 NASM 生成引导扇区二进制文件,命令为nasm boot.asm -o boot.bin(-o 指定输出文件为 boot.bin);
  1. 制作引导镜像:创建一个空的硬盘镜像(如 10MB),并将 boot.bin 写入第一个扇区,命令为dd if=boot.bin of=os.img bs=512 count=1(if 为输入文件,of 为输出镜像,bs=512 表示按 512 字节块写入,count=1 表示仅写入 1 块);
  1. 模拟器运行:使用 QEMU 启动镜像,命令为qemu-system-i386 -hda os.img,若屏幕显示 “Hi!”,则说明引导扇区加载成功 —— 这是你从零开发的第一个 “可运行的操作系统片段”。

三、阶段三:实模式到保护模式切换 —— 突破 1MB 内存限制

引导扇区仅能处理 512 字节代码,且实模式下 CPU 仅能访问 1MB 内存,无法满足 32 位 / 64 位内核的运行需求。因此,必须将 CPU 从 “实模式” 切换到 “保护模式”,这是操作系统启动的 “关键技术分水岭”。

3.1 实模式的 “瓶颈” 与保护模式的 “优势”

实模式是 x86 CPU 的 “兼容模式”,为兼容 DOS 时代的 16 位程序设计,核心限制包括:

  • 地址空间有限:16 位地址总线仅支持 2^16=64KB 段地址,配合 16 位段寄存器(CS/DS/ES/SS),实际地址为 “段地址 <<4 + 偏移地址”,最大仅能访问 1MB 内存(0x00000~0xFFFFF);
  • 无内存保护:任何程序都可访问任意内存地址,可能导致内核代码被破坏;
  • 无多任务支持:无法实现进程调度与权限隔离。

保护模式则解决了这些问题,核心优势包括:

  • 32 位地址空间:支持 2^32=4GB 内存访问,满足现代内核与应用的内存需求;
  • 内存分段 / 分页:通过 “段描述符表”(GDT)实现内存段的权限控制(如内核段仅内核可访问,用户段不可修改),通过 “页表” 实现内存地址映射与物理内存管理;
  • 多任务支持:支持进程上下文切换与 CPU 权限分级(如内核态 Ring 0、用户态 Ring 3),确保系统稳定性。

3.2 切换保护模式的 “三步骤”(核心实现)

从实模式切换到保护模式需严格遵循 x86 硬件规则,核心分为三步,需在引导扇区后续代码中实现:

步骤 1:初始化全局描述符表(GDT)

保护模式下,CPU 通过 “全局描述符表(GDT)” 管理内存段,GDT 需开发者手动定义,核心作用是 “描述内存段的基地址、大小、权限”。以下是简化的 GDT 定义(汇编代码):

; 全局描述符表定义(GDT)

gdt_start:

; 空描述符(GDT第一个条目必须为空,硬件要求)

dd 0x00000000 ; 低32位:基地址0,段大小0

dd 0x00000000 ; 高32位:权限0

; 代码段描述符(供内核代码使用,权限Ring 0)

; 基地址=0x00000000,段大小=4GB(32位模式最大),权限=可执行、读、Ring 0

dw 0xFFFF ; 段大小低16位(0xFFFF表示4GB,配合粒度位)

dw 0x0000 ; 基地址低16位

db 0x00 ; 基地址中8位

db 0x9A ; 权限字节:0x9A表示“代码段、可执行、读、Ring 0”

db 0xCF ; 段大小高4位 + 粒度位:0xCF中,粒度位=1(表示段大小单位为4KB),段大小高4位=0xF(配合低16位0xFFFF,总大小=0xFFFFF*4KB=4GB)

db 0x00 ; 基地址高8位

; 数据段描述符(供内核数据使用,权限Ring 0)

; 基地址=0x00000000,段大小=4GB,权限=可写、读、Ring 0

dw 0xFFFF ; 段大小低16位

dw 0x0000 ; 基地址低16位

db 0x00 ; 基地址中8位

db 0x92 ; 权限字节:0x92表示“数据段、可写、读、Ring 0”

db 0xCF ; 段大小高4位 + 粒度位

db 0x00 ; 基地址高8位

gdt_end:

; GDT描述符(供LGDT指令加载GDT时使用,需包含GDT的大小与基地址)

gdt_descriptor:

dw gdt_end - gdt_start - 1 ; GDT大小(总字节数-1,硬件要求)

dd gdt_start ; GDT基地址(实模式下为物理地址)

步骤 2:打开 A20 地址线

实模式下,CPU 为兼容 DOS,将第 20 位地址线(A20)强制置 0,导致内存地址超过 1MB 时会 “回卷”(如 0x100000 会被映射到 0x00000)。切换到保护模式前必须打开 A20 地址线,否则无法访问 1MB 以上内存。打开 A20 的常用方法是通过键盘控制器(8042 芯片):

; 打开A20地址线

enable_a20:

in al, 0x64 ; 读取键盘控制器状态寄存器(0x64为端口地址)

test al, 0x02 ; 检查状态寄存器第2位(输入缓冲区是否满),若满则等待

jnz enable_a20

mov al, 0xD1 ; 发送“写入键盘控制器命令”(0xD1),准备修改A20位

out 0x64, al

wait_for_a20:

in al, 0x64 ; 等待输入缓冲区为空

test al, 0x02

jnz wait_for_a20

mov al, 0xDF ; 发送“打开A20”的数据(0xDF,第1位为1表示打开A20)

out 0x60, al ; 写入键盘控制器数据端口(0x60)

ret

步骤 3:切换到保护模式(设置 CR0 寄存器)

CPU 的 “控制寄存器 CR0” 的第 0 位(PE 位,Protection Enable)是保护模式的 “开关”——PE=1 表示进入保护模式,PE=0 表示回到实模式。切换代码如下:

; 切换到保护模式

switch_to_protected_mode:

call enable_a20 ; 第一步:打开A20地址线

lgdt [gdt_descriptor] ; 第二步:加载GDT(LGDT指令将gdt_descriptor中的大小与基地址写入CPU内部寄存器)

mov eax, cr0

or eax, 0x00000001 ; 第三步:将CR0寄存器的PE位置1(第0位)

mov cr0, eax

; 关键:远跳转(Far Jump),刷新CPU段缓存,确保段寄存器加载GDT中的描述符

; 此处跳转到代码段(GDT中代码段描述符的索引为1,因第一个条目为空),偏移地址为protected_mode_start

jmp dword 0x08:protected_mode_start ; 0x08表示“段选择子”:高13位为GDT索引(1),低3位为权限(0)

; 保护模式下的第一个函数(32位汇编)

[BITS 32] ; 告诉编译器,后续代码为32位指令

protected_mode_start:

; 初始化保护模式下的段寄存器(DS/ES/FS/GS/SS),全部指向数据段(GDT索引2,段选择子0x10)

mov ax, 0x10

mov ds, ax

mov es, ax

mov fs, ax

mov gs, ax

mov ss, ax

; 保护模式下输出字符(需直接操作显卡显存,BIOS中断在保护模式下不可用)

; 显卡文本模式下,显存地址为0xB8000~0xBFFFF,每个字符占2字节:低字节为ASCII,高字节为颜色

mov byte [0xB8000], 'P' ; 第一个字符'P'(表示Protected Mode)

mov byte [0xB8001], 0x0A ; 颜色:0x0A表示绿底黑字

mov byte [0xB8002], 'M' ; 第二个字符'M'

mov byte [0xB8003], 0x0A

jmp $ ; 无限循环

3.3 验证保护模式切换

将上述代码整合到引导扇区(需注意引导扇区 512 字节限制,实际开发中可将保护模式代码放在引导扇区后续扇区,通过引导扇区加载),编译运行后,若屏幕显示 “PM”,则说明 CPU 已成功进入保护模式 —— 此时 CPU 可访问 4GB 内存,具备加载 32 位内核的条件。

四、阶段四:内核加载与启动 —— 操作系统的 “真正开始”

保护模式切换完成后,引导程序的最终任务是 “加载内核”,将 CPU 控制权交给内核代码,这是操作系统启动的 “最后一公里”。

4.1 内核的 “形态约定”

从零开发的内核通常为 32 位 ELF 格式(Executable and Linkable Format,可执行链接格式),需满足两个条件:

  • 入口地址明确:内核编译时需指定入口函数(如kernel_main),链接器会将入口地址写入 ELF 文件头;
  • 内存布局适配:内核需被加载到保护模式下的指定内存地址(如 0x100000,即 1MB 处,避开引导程序与 BIOS 占用的内存区域)。

4.2 引导程序加载内核的 “三步流程”

引导程序(需扩展为 “多扇区引导程序”,因 512 字节无法容纳内核加载代码)加载内核的核心流程如下:

步骤 1:读取内核文件到内存

假设内核文件(kernel.elf)存放在引导设备的第 2 个扇区开始的区域(引导扇区为第 1 个扇区),引导程序需通过 BIOS 中断(实模式下)或直接操作硬盘控制器(保护模式下)读取内核到指定内存地址(如 0x100000)。以下是实模式下通过 BIOS 中断读取硬盘的代码示例:

; 实模式下读取硬盘扇区(BIOS中断0x13)

; 参数:

; cl:要读取的扇区数

; ch:磁道号(低8位)

; dh:磁头号

; dl:驱动器号(0x80表示第一块硬盘,0x00表示软盘)

; es:bx:读取到的内存地址

read_sectors:

push ax

push bx

push cx

push dx

mov ah, 0x02 ; BIOS中断0x13功能号:读取扇区

mov al, cl ; 读取的扇区数(cl为传入参数)

int 0x13 ; 调用BIOS中断

jc read_error ; 若CF标志位为1,表示读取错误,跳转到错误处理

pop dx

pop cx

pop bx

pop ax

ret

read_error:

; 读取错误时输出提示

mov ah, 0x0E

mov al, 'E'

int 0x10

jmp $

步骤 2:解析 ELF 格式内核

ELF 文件包含 “文件头”“程序头表”“代码段”“数据段” 等结构,引导程序需解析 ELF 文件头,找到内核代码段与数据段的基地址、大小,将其加载到正确的内存地址。关键解析步骤包括:

  1. 读取 ELF 文件头(前 52 字节),验证 ELF 标识(前 4 字节为 0x7F454C46,表示 ELF 格式);
  1. 找到程序头表的偏移地址与条目数(ELF 文件头中 e_phoff 字段表示程序头表偏移,e_phnum 字段表示条目数);
  1. 遍历程序头表,对每个条目(描述一个代码段 / 数据段),将 ELF 文件中对应的数据复制到条目指定的内存地址(p_vaddr 字段),复制长度为 p_filesz 字段。

步骤 3:跳转到内核入口函数

解析完成后,引导程序通过 “远跳转” 到内核入口地址(ELF 文件头中 e_entry 字段指定),将 CPU 控制权交给内核。代码示例如下:

; 解析ELF内核后,跳转到内核入口

jmp dword 0x08:kernel_entry ; 0x08为代码段选择子,kernel_entry为ELF文件头中的e_entry值

4.3 内核启动的 “第一行代码”

内核入口函数(如kernel_main)通常用 C 语言编写(需配合汇编启动代码初始化栈),核心任务是 “完成系统初始化”,如:

  • 内存管理初始化:建立页表,开启分页机制(保护模式下默认仅开启分段,分页需手动初始化);
  • 中断控制器初始化:初始化 8259 PIC 芯片,使能硬件中断(如时钟中断、键盘中断);
  • 驱动初始化:编写键盘驱动、显卡驱动,实现基本的输入输出功能;
  • 进程管理初始化:创建第一个进程(如 shell 进程),实现进程调度。

以下是简化的内核入口函数示例(C 语言):

// kernel.c

#include "types.h" // 自定义数据类型,如uint32_t

// 内核入口函数

void kernel_main() {

// 初始化显卡,在屏幕显示内核启动信息

char *video_mem = (char *)0xB8000;

const char *msg = "Kernel Started!";

int i = 0;

while (msg[i] != '0円') {

video_mem[2*i] = msg[i]; // ASCII字符

video_mem[2*i + 1] = 0x0B; // 颜色:青底黑字

i++;

}

// 后续初始化:内存分页、中断控制器、驱动...

while (1); // 无限循环,防止内核执行完后退出

}

五、开发难点与避坑指南:从零启动的 “常见问题”

在 “从加电自检到内核引导” 的开发过程中,新手常遇到以下问题,需重点关注:

5.1 地址引用错误:实模式下的 “org 指令” 与保护模式下的 “段选择子”

  • 实模式:若引导程序未加org 0x7C00指令,编译器会默认代码加载到 0x0000 地址,导致内存访问错误(如mov byte [0x00], 'A'会写入 0x0000,而非 0x7C00+0x00);
  • 保护模式:跳转时若段选择子错误(如用 0x00 代替 0x08),会导致 CPU 触发 “段不存在异常”,需确保段选择子对应 GDT 中的正确条目。

5.2 引导扇区大小错误:512 字节与 0x55AA 标识

  • 若引导扇区代码编译后不足 512 字节,且未用times 510 - ($ - $$) db 0填充,会导致 BIOS 无法识别为有效引导扇区;
  • 0x55AA 标识需用dw 0xAA55(小端序),若误写为dw 0x55AA,则最后两个字节实际为 0xAA55,BIOS 会判定为无效。

5.3 保护模式下 BIOS 中断不可用

实模式下依赖 BIOS 中断(如 0x10 输出字符、0x13 读取硬盘),但进入保护模式后,BIOS 中断向量表(IVT)失效,需手动实现硬件驱动(如直接操作显卡显存、硬盘控制器端口),否则会导致程序崩溃。

六、总结:从零开发的 “技术闭环” 与后续方向

“从加电自检到内核引导” 的过程,是操作系统从 “硬件唤醒” 到 “软件运行” 的完整技术闭环 ——POST 确保硬件可用,BIOS 加载引导扇区实现 “硬件到软件的过渡”,实模式到保护模式切换突破内存限制,内核加载则开启操作系统的真正功能。对开发者而言,这一流程不仅是 “编写代码”,更是理解 “硬件规则与软件逻辑如何协同” 的核心训练。

完成内核引导后,从零开发操作系统的后续方向包括:

  • 内存管理:实现分页机制、物理内存分配器(如伙伴系统)、虚拟内存管理;
  • 中断与异常处理:编写中断服务程序(ISR),处理时钟中断、键盘中断、页面错误等;
  • 进程管理:实现进程控制块(PCB)、进程调度算法(如时间片轮转)、进程间通信;
  • 文件系统:设计简单的文件系统(如 FAT32 简化版),支持文件的创建、读取、写入。

从零开发操作系统的难度较高,但每完成一个阶段(如引导扇区加载、保护模式切换、内核启动),都会对计算机系统的底层逻辑产生更深刻的理解 —— 这正是操作系统开发的独特魅力。


有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

关注微信
110 次点击
添加一条新回复 (您需要 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传

用户登录

没有账号?注册
(追記) (追記ここまで)

今日阅读排行

    加载中
(追記) (追記ここまで)

一周阅读排行

    加载中

关注我

  • 扫码关注领全套学习资料 关注微信公众号
  • 加入 QQ 群:
    • 192706294(已满)
    • 731990104(已满)
    • 798786647(已满)
    • 729884609(已满)
    • 977810755(已满)
    • 815126783(已满)
    • 812540095(已满)
    • 1006366459(已满)
    • 692541889

  • 关注微信公众号
  • 加入微信群:liuxiaoyan-s,备注入群
  • 也欢迎加入知识星球 Go粉丝们(免费)

给该专栏投稿 写篇新文章

每篇文章有总共有 5 次投稿机会

收入到我管理的专栏 新建专栏