从 实模式启动 → 保护模式切换 → 加载 ELF → 构建页表启用分页 → 跳转 e_entry

从零实现 OS 的完整启动流程

一步步完整地讲清楚整个自举过程:从 实模式启动保护模式切换加载 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 位寄存器。我们要进入保护模式来访问大内存并启用分页。

步骤:

  1. 关闭中断(cli
  2. 构建 GDT(全局描述符表),设置代码段/数据段为 flat 模式(基址 0,长度 4GB)
  3. 加载 GDT(lgdt
  4. 设置 CR0 的 PE(bit 0)位,开启保护模式
  5. 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)开始:

  1. 读取 Elf32_Ehdr 头,校验 0x7F 45 4C 46
  2. 遍历 Program Header Table
  3. 对每个 p_type == PT_LOAD 的段:
    • p_filesz 字节从 p_offset 拷贝到 p_vaddr
    • 再对 .bss 做零填充:p_memsz > p_filesz

你现在就把 ELF 的段映射到了“它们想要的地址”。

⚠️ 这时候它们的地址可能是虚拟地址,要不要启用分页,取决于 e_entry 是不是一个“高地址”


✅ 4. 启用分页(Paging)

如果你想支持虚拟地址(比如内核用 0xC0000000 以上的地址):

步骤:

  1. 分配页目录(Page Directory)和页表(Page Table)结构
  2. 设置页表映射,比如:
    • 0x00000000 → 0x00000000(identity 映射,保留 bootloader 可继续运行)
    • 0xC0000000 → 0x00100000(高地址映射你的内核段)
  3. 将页目录地址写入 CR3
  4. 设置 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()]

📦 附加建议:分页地址推荐布局

区域虚拟地址映射到物理地址用途
bootloader0x000000000x00000000identity 映射
kernel ELF0xC00000000x00100000高半区映射
页目录/页表0x00102000+自己分配内核管理页表
0xC03FFFFF 向下高内核区内核栈

✅ 总结一句话:

你作为 bootloader,需要负责从磁盘加载 ELF 文件构造保护模式与页表环境跳转到 e_entry 虚拟地址运行内核,整个“从无到有”的链条你都要手动构建!


🛠️ 想继续搞实战?

我可以给你提供:

  1. Bootloader 示例(汇编)
  2. ELF Loader 代码(C语言)
  3. 页表构建模板(4KB页、页目录+页表)
  4. 启用分页+跳转 e_entry 示例
  5. 最后一跳的 jmp e_entry + 设置内核栈的写法

要哪个部分,我直接贴你代码 ✅

bootloader.asm: ; ============================== ; bootloader.asm - 完整可运行的最小引导程序 ; 编译: nasm -f bin bootloader.asm -o bootloader.bin ; ============================== section .text global _start bits 16 _start: ; 初始化寄存器 cli xor ax, ax mov ds, ax mov es, ax mov ss, ax mov sp, 0xFFFE ; 打印 'S' 表示开始 mov ah, 0x0E mov al, 'S' int 0x10 ; 加载内核到 0x1000:0x0000 (物理地址 0x10000) mov ah, 0x02 ; BIOS 磁盘读取功能 mov al, 0x10 ; 读取1个扇区 mov ch, 0x00 ; 柱面0 mov cl, 0x02 ; 扇区2(LBA 1) mov dh, 0x00 ; 磁头0 mov dl, 0x80 ; 第一块硬盘 mov bx, 0x1000 ; ES:BX = 0x1000:0000 mov es, bx xor bx, bx int 0x13 jc .disk_error ; 显示 'O' 表示成功 mov al, 'O' mov ah, 0x0E int 0x10 jmp enter_protected_mode .disk_error: mov al, 'X' mov ah, 0x0E int 0x10 hlt .hang: hlt jmp .hang ; ======================================== ; 进入保护模式部分 ; ======================================== enter_protected_mode: cli lgdt [gdt_descriptor] ; ✅ 加载 GDT mov eax, cr0 or eax, 1 ; ✅ 设置 PE 位 mov cr0, eax mov dword [0xB8000], 0x0F52 ; 白字黑底的 'R' jmp CODE_SEG:flush ; ✅ 远跳转刷新流水线(far jump) mov dword [0xB8000], 0x0F52 ; 白字黑底的 'R' mov dword [0xB8000], 0x0F52 ; 白字黑底的 'R' mov dword [0xB8000], 0x0F52 ; 白字黑底的 'R' bits 32 flush: ; 设置段寄存器为数据段选择子(DATA_SEG) mov ax, DATA_SEG mov ds, ax mov ss, ax mov es, ax mov fs, ax mov gs, ax ; 设置栈指针(可选) mov ebp, 0x90000 mov esp, ebp ; ✅ 写显存显示 'R' mov dword [0xB8000], 0x0F52 ; 白字黑底的 'R' ; ❌ 不要再使用 int 0x10,BIOS 中断只在模式有效! ; ✅ 跳转到内核入口地址(物理地址 0x10000) jmp CODE_SEG:0x10000 ; ======================================== ; GDT 定义 ; ======================================== gdt_start: dq 0x0 ; Null段 gdt_code: dw 0xFFFF ; Limit (low) dw 0x0000 ; Base (low) db 0x00 ; Base (middle) db 0x9A ; Present, DPL=0, S=1, Executable, Readable db 0xCF ; Granularity, 32-bit db 0x00 ; Base (high) gdt_data: dw 0xFFFF ; Limit (low) dw 0x0000 ; Base (low) db 0x00 ; Base (middle) db 0x92 ; Present, DPL=0, S=1, Writable db 0xCF ; Granularity, 32-bit db 0x00 ; Base (high) gdt_end: gdt_descriptor: dw gdt_end - gdt_start - 1 dd gdt_start CODE_SEG equ gdt_code - gdt_start DATA_SEG equ gdt_data - gdt_start ; ======================================== ; 引导扇区结束 ; ======================================== times 510 - ($ - $$) db 0 dw 0xAA55 kernel_entry.axm: ; ============================== ; kernel_entry.asm ; 功能: 32位内核入口,设置保护模式环境并调用kernel_main ; 编译: nasm -f elf32 kernel_entry.asm -o kernel_entry.o ; ============================== bits 32 section .text global start_pm extern kernel_main ; 声明外部C函数 start_pm: ; 打印 'R' 到屏幕左上角 mov dword [0xB8000], 0x0F52 ; 绿色背景白字的 'R' mov dword [0xB8000], 0x0F52 ; 绿色背景白字的 'R' mov dword [0xB8000], 0x0F52 ; 绿色背景白字的 'R' mov dword [0xB8000], 0x0F52 ; 绿色背景白字的 'R' mov dword [0xB8000], 0x0F52 ; 绿色背景白字的 'R' mov dword [0xB8000], 0x0F52 ; 绿色背景白字的 'R' mov dword [0xB8000], 0x0F52 ; 绿色背景白字的 'R' mov dword [0xB8000], 0x0F52 ; 绿色背景白字的 'R' mov dword [0xB8000], 0x0F52 ; 绿色背景白字的 'R' mov dword [0xB8000], 0x0F52 ; 绿色背景白字的 'R' mov dword [0xB8000], 0x0F52 ; 绿色背景白字的 'R' mov dword [0xB8000], 0x0F52 ; 绿色背景白字的 'R' mov dword [0xB8000], 0x0F52 ; 绿色背景白字的 'R' mov dword [0xB8000], 0x0F52 ; 绿色背景白字的 'R' ; 设置段寄存器 mov ax, DATA_SEG mov ds, ax mov es, ax mov fs, ax mov gs, ax mov ss, ax ; 设置栈指针 mov ebp, 0x90000 mov esp, ebp ; 调用C内核入口 call kernel_main ; 如果返回,则挂起 hang: cli hlt jmp hang ; GDT符号(需与bootloader.asm一致) CODE_SEG equ 0x08 DATA_SEG equ 0x10 linker.ld: /* 内核链接器脚本 */ OUTPUT_FORMAT("elf32-i386") ENTRY(start_pm) SECTIONS { /* 内核加载地址为0x10000 (与bootloader中的0x1000:0x0000对应) */ . = 0x10000; .text : { *(.text) } .data : { *(.data) *(.rodata*) } .bss : { *(COMMON) *(.bss) } /* 确保内核大小不超过8KB (16个扇区) */ /DISCARD/ : { *(.note*) *(.comment*) } } Makefile: # Makefile for MyOS (兼容 Windows, WSL 和 Ubuntu 18.04.6 LTS) # 工具定义 ifeq ($(OS),Windows_NT) # Windows 设置 NASM := nasm GCC := gcc LD := ld RM := del /Q MKDIR := mkdir QEMU := qemu-system-x86_64 OBJCOPY := objcopy else # Linux/WSL 设置 NASM := nasm GCC := gcc LD := ld RM := rm -f MKDIR := mkdir -p QEMU := qemu-system-x86_64 OBJCOPY := objcopy endif # 编译选项 NASMFLAGS := -f elf32 GCCFLAGS := -m32 -nostdlib -nostdinc -fno-builtin -fno-stack-protector \ -nostartfiles -nodefaultlibs -Wall -Wextra -c \ -Ikernel/include LDFLAGS := -m elf_i386 -T linker.ld # 目标文件 BOOTLOADER := boot/bootloader.bin KERNEL_ENTRY_OBJ := boot/kernel_entry.o # 新增:添加 process_switch.o KERNEL_OBJS := kernel/main.o kernel/memory_manager.o kernel/process.o kernel/vga.o kernel/process_switch.o KERNEL_BIN := kernel/kernel.bin KERNEL_ELF := kernel/kernel.elf OS_IMAGE := myos.img # 默认目标 all: $(OS_IMAGE) # 构建引导扇区 $(BOOTLOADER): boot/bootloader.asm $(NASM) -f bin $< -o $@ # 构建内核入口 $(KERNEL_ENTRY_OBJ): boot/kernel_entry.asm $(NASM) $(NASMFLAGS) $< -o $@ # 构建C内核对象文件 kernel/%.o: kernel/%.c $(GCC) $(GCCFLAGS) $< -o $@ # 特别处理 .S 汇编文件(注意使用 $(CC) 来启用预处理器) kernel/%.o: kernel/%.S $(GCC) $(GCCFLAGS) $< -o $@ # 链接内核 $(KERNEL_ELF): $(KERNEL_ENTRY_OBJ) $(KERNEL_OBJS) $(LD) $(LDFLAGS) $^ -o $@ # 生成纯二进制内核 $(KERNEL_BIN): $(KERNEL_ELF) $(OBJCOPY) -O binary $< $@ # 创建OS镜像 $(OS_IMAGE): $(BOOTLOADER) $(KERNEL_BIN) @echo "Creating OS image..." @dd if=/dev/zero of=$@ bs=512 count=2880 2>/dev/null @dd if=$(BOOTLOADER) of=$@ conv=notrunc 2>/dev/null @dd if=$(KERNEL_BIN) of=$@ seek=1 conv=notrunc 2>/dev/null # 清理 clean: $(RM) $(BOOTLOADER) $(KERNEL_ENTRY_OBJ) $(KERNEL_OBJS) $(KERNEL_ELF) $(KERNEL_BIN) $(OS_IMAGE) # 运行QEMU run: $(OS_IMAGE) $(QEMU) -drive format=raw,file=$< 这些程序在QEMU中运行时,为什么会导致不断地运行到设置PE位附近时重新开始运行
最新发布
06-15
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值