
追根溯源xv6启动之旅从第一条指令到第一个Shell的5个阶段【免费下载链接】xv6-publicxv6 OS项目地址: https://gitcode.com/gh_mirrors/xv/xv6-public打开一台计算机按下电源键屏幕黑屏片刻后跳出熟悉的提示符——这背后xv6 操作系统经历了从一条 16 位指令到用户 Shell 的完整觉醒。xv6 是 MIT 6.828 课程使用的教学操作系统复刻了 Unix V6 的结构却运行在现代 x86 多核 CPU 上。本文将带你分 5 个阶段追根溯源看懂 xv6 的启动之旅BIOS 加载第一条指令、切换保护模式、加载内核、开启虚拟内存直到第一个 Shell 进程被创建。阶段一BIOS 与第一条指令实模式关键词BIOS 引导、boot block、实模式按下电源后CPU 处于实模式Real Mode所有程序都是 16 位的。BIOS 会执行一件约定俗成的事把硬盘第一个扇区512 字节读入内存地址0x7C00并跳转到那里执行。这第一个扇区在 xv6 中就是boot block它由两部分组成组成部分作用bootasm.S汇编代码负责 CPU 模式切换bootmain.cC 代码负责从磁盘加载内核boot block 的第一条指令bootasm.S 中的start:就是cli # 关闭中断——BIOS 开启过中断必须先关掉随后 CPU 把数据段寄存器 DS、ES、SS 清零并通过端口 I/O 打开被早期 PC 硬件屏蔽的A20 地址线这是为了兼容只有 1MB 内存的老机器留下的历史包袱。 记住这个起点实模式、16 位、CPU 还看不见 1MB 以上的内存。阶段二进入 32 位保护模式关键词保护模式、GDT、分段描述符要运行现代内核必须进入保护模式Protected Mode。这一步的难点在于CPU 不能原地变身需要一个平滑过渡方案。xv6 的做法分三步bootasm.S加载引导 GDT全局描述符表——这是 CPU 认识内存分段方式的说明书设置 CR0 寄存器的 PE 位——正式切入保护模式执行一条ljmp长跳转——强制 CPU 重新装载段寄存器完成到 32 位的最后一跃巧妙之处在于 GDT 的设计代码段和数据段都映射到0x00000000开始的 4GB 空间即虚拟地址 物理地址恒等映射。切换前后内存地址看起来完全没变程序不会出错。完成切换后boot block 设置好栈指针ESP然后调用 C 函数bootmain()——从这一刻起xv6 的启动正式进入 C 语言世界。阶段三bootmain 加载 ELF 内核关键词bootmain、ELF、内核加载现在 CPU 是 32 位了但内存里只有 boot block 这一小段代码。真正的内核还在硬盘上。bootmain()bootmain.c的任务很纯粹把内核从磁盘搬进内存。流程如下从磁盘第 1 扇区读入内核 ELF 头放到 0x10000 空间 ↓ 校验 ELF 魔数ELF_MAGIC确认这不是垃圾数据 ↓ 遍历程序头proghdr逐段把内核代码搬入指定物理地址 ↓ 跳到 ELF 头中记录的入口地址 entry() —— 从此一去不返其中读取磁盘用的是最原始的 IDE 端口 I/O向端口0x1F0~0x1F7写命令、从0x1F0读数据readsect。此时还没有驱动框架一切靠裸操作。 小知识boot block 本身也遵循小步快跑原则——它不直接内联全部代码而是通过readseg()按需把内核分扇区搬入内存bootmain.c。阶段四开启分页跳入内核 C 入口关键词分页、虚拟内存、entry、main 函数内核被搬进内存后CPU 跳到了entry——这是 xv6 内核真正的入口点entry.S。此时虽然已处于保护模式但分页Paging还没开启CPU 仍然使用物理地址。entry完成三件关键事步骤说明开启4MB 大页CR4 的 PSE 位减少页表数量简化启动期地址翻译加载页目录到CR3开启分页CR0 的 PG 位虚拟地址 → 物理地址的转换生效设置栈指针跳转到main()正式进入 C 代码这里有一个精妙的内存布局设计见 memlayout.h内核链接在虚拟地址0xC0000000KERNLINK附近但启动时运行在低物理地址。启动页表entrypgdirmain.c同时把虚拟地址[0, 4MB)和[0xC0000000, 0xC0400000)都映射到物理地址[0, 4MB)——所以同一段代码既能从低地址执行又能被认成高地址切换毫无感觉。entry的最后一行是一个间接跳转jmp *%eax目标是main()——这就是 xv6 内核的第一条 C 指令。阶段五main() 初始化万物唤醒第一个 Shell关键词userinit、init 进程、Shell 创建main()main.c是整个启动流程的总指挥按严格顺序初始化内核子系统kinit1 → 物理页分配器4MB 之后的空闲内存 kvmalloc → 内核页表 mpinit → 检测多核 CPU lapicinit → 中断控制器 consoleinit → 控制台硬件 uartinit → 串口你看到的启动日志就从这里来 pinit → 进程表 binit → 磁盘缓冲区缓存 ideinit → 磁盘驱动 startothers → 唤醒其他 CPU 核心 kinit2 → 回收 AP 核占用的启动内存 userinit → 创建第一个用户进程 ✨其中startothers()main.c会把一小段启动代码写到0x7000地址再通过 LAPIC 的 INIT/SIPI 信号唤醒其他核心它们从 entryother.S 进入内核加入进程调度。userinit第一个用户进程的诞生userinit()proc.c做了件看似简单实则精妙的事它不 exec 磁盘上的文件而是直接把一小段机器码initcode放进新进程。这段引导代码initcode.S只有寥寥几条指令作用是设置好寄存器后执行一条exec(init)系统调用让内核加载真正的 init 程序init.c。init 进程一切 Shell 的祖先init 是 xv6 系统中PID 为 1的进程它的逻辑简单得令人感动init.cfor(;;){ // 无限循环 printf(1, init: starting sh\n); pid fork(); if(pid 0){ exec(sh, argv); // 子进程替换为 shell } wait(); // 父进程等 shell 退出后重来 }打开console设备把标准输出/错误都指向屏幕fork()出子进程子进程exec(sh)变成Shell用户退出 Shell 后init 会重新fork一个——所以 xv6 的 Shell永远杀不死全景回顾5 个阶段一张表阶段关键文件发生了什么CPU 状态1bootasm.SBIOS 加载 boot block 到 0x7C00实模式 / 16 位2bootasm.S加载 GDT、开 PE 位、A20保护模式 / 32 位3bootmain.c端口 I/O 读盘搬入 ELF 内核32 位恒等映射4entry.S开启分页跳入 main()虚拟内存生效5main.c → init.c初始化子系统userinit → init → sh用户态 Shell 就绪为什么值得读 xv6 的启动流程xv6 的全部内核代码只有 1 万多行启动路径bootasm.S → bootmain.c → entry.S → main.c → init.c完整覆盖了操作系统启动的核心概念实模式转保护模式、ELF 加载、分段与分页、多核唤醒、进程创建与 exec。理解了这条链路你就拿到了打开现代操作系统Linux 的 boot、init、systemd 体系的第一把钥匙。想亲手编译运行只需安装交叉编译器和 QEMU执行make qemu即可构建规则见 Makefile。欢迎追根溯源从第一条cli指令读起。【免费下载链接】xv6-publicxv6 OS项目地址: https://gitcode.com/gh_mirrors/xv/xv6-public创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考