一步步完整地讲清楚整个自举过程:从 实模式启动 → 保护模式切换 → 加载 ELF → 构建页表启用分页 → 跳转 e_entry
,全部解释清楚,绝无黑箱。
🚀 从零实现 OS:完整启动流程(x86)
我们按时间线分 6 个阶段:
✅ 1. BIOS 启动阶段(实模式)
CPU 上电后默认进入 实模式(Real Mode),从
0xFFFF0
开始执行 BIOS 引导代码
BIOS 会:
- 初始化硬件
- 读取主引导扇区(MBR,512 字节)到内存
0x7C00
- 跳转到
0x7C00
执行
👉 你的工作:
- 写一个 bootloader(512 字节)
- 用 BIOS
int 13h
从磁盘加载 kernel ELF 文件(比如从扇区 1 开始加载 200 扇区)到内存(如0x100000
)
✅ 2. 切换到保护模式(Protected Mode)
实模式只能寻址 1MB,不能用分页,不支持 32 位寄存器。我们要进入保护模式来访问大内存并启用分页。
步骤:
- 关闭中断(
cli
) - 构建 GDT(全局描述符表),设置代码段/数据段为 flat 模式(基址 0,长度 4GB)
- 加载 GDT(
lgdt
) - 设置 CR0 的 PE(bit 0)位,开启保护模式
jmp
到 32 位段选择子刷新流水线
cli
lgdt [gdt_ptr]
mov eax, cr0
or eax, 1
mov cr0, eax
jmp CODE_SELECTOR:flush ; 长跳转刷新流水线,进入保护模式
✅ 3. 加载并解析 ELF 文件(你自己写)
现在你在保护模式,已经可以用 32 位寻址了(可以用 mov eax, [0x100000]
这种访问方式)。
你从之前加载的地址(比如 0x100000
)开始:
- 读取
Elf32_Ehdr
头,校验0x7F 45 4C 46
- 遍历
Program Header Table
- 对每个
p_type == PT_LOAD
的段:- 把
p_filesz
字节从p_offset
拷贝到p_vaddr
- 再对
.bss
做零填充:p_memsz > p_filesz
- 把
你现在就把 ELF 的段映射到了“它们想要的地址”。
⚠️ 这时候它们的地址可能是虚拟地址,要不要启用分页,取决于 e_entry 是不是一个“高地址”
✅ 4. 启用分页(Paging)
如果你想支持虚拟地址(比如内核用 0xC0000000
以上的地址):
步骤:
- 分配页目录(Page Directory)和页表(Page Table)结构
- 设置页表映射,比如:
0x00000000 → 0x00000000
(identity 映射,保留 bootloader 可继续运行)0xC0000000 → 0x00100000
(高地址映射你的内核段)
- 将页目录地址写入 CR3
- 设置 CR0 的 PG 位(bit 31)
mov eax, page_directory
mov cr3, eax
mov eax, cr0
or eax, 0x80000000
mov cr0, eax
✅ 5. 跳转到 ELF 的 e_entry
(虚拟地址)
现在内核已经加载,页表已经映射,你只需要:
jmp e_entry ; e_entry 是 ELF Header 中指定的入口点
因为你已经设置了分页,虚拟地址到物理地址的映射是生效的,CPU 能直接跳转过去执行!
你此时就完成了整个“自举”,从 BIOS 到自己的内核启动
✅ 6. 你在内核 C 的 void kernel_main()
中运行啦!
此时你处于:
- CR0.PG = 1(分页已开启)
- CR0.PE = 1(保护模式)
- 页表已经启用
- 栈也应该初始化了(你要在跳转 e_entry 前设置好栈)
现在就可以开始写你的 printf
、内核态 malloc、虚拟内存管理器、进程管理器了!
🧠 整体流程图(简化)
[BIOS]
↓ (读取 MBR 到 0x7C00)
[你写的 bootloader (实模式)]
↓
BIOS int13h → 读取 kernel.elf 到内存
↓
切换到保护模式(设置 GDT、CR0.PE)
↓
你写的 ELF loader 解析 kernel.elf
↓
如果用虚拟地址 → 建立页表 → 设置 CR3、开启分页
↓
跳转到 e_entry(虚拟地址)
↓
[C语言内核入口:kernel_main()]
📦 附加建议:分页地址推荐布局
区域 | 虚拟地址 | 映射到物理地址 | 用途 |
---|---|---|---|
bootloader | 0x00000000 | 0x00000000 | identity 映射 |
kernel ELF | 0xC0000000 | 0x00100000 | 高半区映射 |
页目录/页表 | 0x00102000+ | 自己分配 | 内核管理页表 |
栈 | 0xC03FFFFF 向下 | 高内核区 | 内核栈 |
✅ 总结一句话:
你作为 bootloader,需要负责从磁盘加载 ELF 文件 → 构造保护模式与页表环境 → 跳转到
e_entry
虚拟地址运行内核,整个“从无到有”的链条你都要手动构建!
🛠️ 想继续搞实战?
我可以给你提供:
- Bootloader 示例(汇编)
- ELF Loader 代码(C语言)
- 页表构建模板(4KB页、页目录+页表)
- 启用分页+跳转 e_entry 示例
- 最后一跳的
jmp e_entry
+ 设置内核栈的写法
要哪个部分,我直接贴你代码 ✅