从零写OS(四):汇编交棒给 C,最关键的一跳

前三章全是汇编。到这里,CPU 已经跑在 64 位长模式下了,但还是什么都做不了。 这一章要完成最关键的一步:把控制权从汇编交给 C。之后的内核全部用 C 写,汇编只在最必要的地方出现。 磁盘要怎么布局 512 字节的 Bootloader 放不下 C 内核,所以内核单独编译成一个二进制文件,放在磁盘的后续扇区(扇区,Sector,磁盘读写的最小单位,传统上每个 512 字节): 扇区 0(512B):Bootloader 扇区 1~8 :内核二进制 Bootloader 启动后,用 BIOS 的磁盘读取中断把扇区 1 开始的内容读到内存地址 0x10000,然后跳过去执行。 一个容易踩的坑:内核入口不在 0x10000 把内核编译好、加载到 0x10000,然后 call 0x10000——看起来没问题,实际上很可能直接崩。 用 objdump(反汇编工具,可以查看二进制文件的结构和函数地址)一查才发现:kernel_main 的实际地址是 0x10109,不是 0x10000。0x10000 开头是另一个函数 outb,跳过去执行完全是错的。 解决方案:加一个 entry.asm 作为内核最开头,内容只有一行:跳转到 kernel_main。然后在链接脚本(Linker Script,告诉链接器如何拼装各个目标文件、每段放在哪个地址)里把 entry.o 排在最前面,保证 _start 恰好落在 0x10000。 这样 Bootloader 跳到 0x10000,第一条指令就是 jmp kernel_main,稳了。 还有一个坑:栈 进入 64 位模式之后,rsp(栈顶指针寄存器)是未初始化的。C 函数一用栈就崩。 在跳入内核之前,要先设好栈: mov rsp, 0x90000 选 0x90000 是因为这块内存没有被占用,往下增长也不会覆盖内核代码。 内核编译参数 内核不能用普通方式编译,要加几个关键选项: ...

May 6, 2026 · 1 min · 大飞

从零写OS(三):进入 64 位,CPU 真正醒了

上一章切到了保护模式,32 位,最多 4GB 内存。 听起来挺多,但现代系统动不动就几十 GB 内存,4GB 根本不够用。所以还得再往前走一步:进入长模式(Long Mode,x86_64 的 64 位工作模式,支持 128TB 虚拟地址空间)。 现在跑在你电脑上的 Linux、macOS、Windows,全都在长模式里。 长模式比保护模式多了什么 最直接的:寻址空间从 4GB 扩展到理论上的 16EB(16 × 10¹⁸ 字节),实际当前 CPU 支持 128TB,够用很久了。 但更重要的变化是强制引入了分页(Paging,把物理内存切成固定大小的页,通过页表做虚拟地址到物理地址的映射)。 保护模式下分页是可选的,长模式下分页是必须开启的。没有页表,CPU 根本不让你进长模式。 先建页表 切换之前,必须在内存里建好页表(Page Table,记录虚拟地址到物理地址映射关系的数据结构)。 x86_64 用四级页表,一个虚拟地址的翻译路径是: 虚拟地址 → PML4 → PDPT → PD → PT → 物理地址 每级都是一张表,每张表 512 项,每项 8 字节。 启动阶段不需要映射所有内存,先映射前 2MB 就够了——把四张表放在物理地址 0x1000 开始的位置,让虚拟地址和物理地址一一对应(恒等映射,虚拟地址和物理地址相同,是启动阶段最简单的映射策略)。 切换步骤 在 32 位保护模式下,按顺序操作: 建好页表,把 PML4 地址写入 CR3(页表根寄存器) 开启 PAE(Physical Address Extension,物理地址扩展,让 32 位系统支持超过 4GB 的物理地址,长模式必须开启):CR4.PAE=1 设置 EFER.LME=1(通过 MSR 寄存器(Model Specific Register,CPU 内部的特殊寄存器,用 rdmsr/wrmsr 读写)开启长模式使能位) 开启分页:CR0.PG=1,CPU 正式进入长模式兼容态 far jump 跳入 64 位代码段,彻底进入 64 位模式 顺序不能乱。尤其是第 3 步必须在开启分页之前,不然 CPU 会拒绝。 ...

May 6, 2026 · 1 min · 大飞

从零写OS(二):跨过这道门,CPU 才算醒了

上一章我们让机器打出了第一行字。但那时候 CPU 还处于一种"出厂状态"——实模式,最多只能用 1MB 内存,没有任何权限隔离,任何代码想写哪块内存就写哪块。 这对一个操作系统来说是不可接受的。 所以这一章要做一件事:把 CPU 从实模式切换到保护模式(Protected Mode,32 位工作模式,支持内存保护和权限隔离)。 为什么叫"保护"模式 名字来源很直接——这个模式下,内存是被保护的。 实模式里,程序可以随意读写任何内存地址,一个 bug 就能把整个系统搞崩。保护模式引入了两个机制: 内存段的权限描述:每块内存都有自己的访问权限,越界访问直接触发异常 特权级别(Ring):内核跑在 Ring 0,用户程序跑在 Ring 3,用户程序没法直接碰内核内存 现代操作系统全都建立在保护模式之上。 切换的关键:GDT 切到保护模式之前,必须先准备好 GDT(Global Descriptor Table,全局描述符表,内存里的一张表,描述每个内存段的地址、大小和权限)。 可以把 GDT 理解成一个"内存段登记簿"。CPU 进入保护模式后,所有内存访问都要查这张表,看看你有没有权限。 GDT 最少需要三项: 索引 用途 0 空描述符(CPU 规定必须全零,不能用) 1 代码段(可执行,Ring 0) 2 数据段(可读写,Ring 0) GDT 写好之后,用 lgdt 指令告诉 CPU 表在哪里,CPU 才知道去哪查。 切换步骤 整个切换过程就四步,缺一不可: 关中断(cli)—— 切换过程不能被打断,否则 CPU 状态会乱 加载 GDT(lgdt)—— 告诉 CPU 描述符表在哪 设置 CR0 寄存器(CR0.PE=1)—— CR0(控制寄存器0,控制CPU工作模式的关键寄存器)第0位置1,CPU 正式进入保护模式 far jump 刷新流水线 —— 跳转的同时加载新的代码段选择子,清空 CPU 的指令预取缓存 最后这一步很容易漏掉。CPU 有指令流水线,切换模式后如果不强制刷新,可能还在用旧模式的指令译码方式,导致莫名其妙的错误。 ...

May 6, 2026 · 1 min · 大飞

从零写OS(一):让机器听你的第一句话

电脑开机之后,CPU 做的第一件事是什么? 不是运行 Linux,不是加载驱动,甚至不是执行你写的任何代码——而是执行一段固化在主板上的程序:BIOS(Basic Input/Output System,主板上的固件,负责硬件自检和引导启动)。 BIOS 做完硬件自检之后,会去找一个可以启动的磁盘。找到之后,它做了一件很朴素的事:把磁盘最开头的 512 字节复制到内存地址 0x7C00,然后跳过去执行。 就这样。没有操作系统,没有文件系统,没有任何框架。你写的代码,就从这 512 字节开始。 512 字节能干什么 说实话,不多。但够用来在屏幕上打一行字。 这一章的目标很简单:从磁盘启动,打印 Hello, OS!。 这段代码叫 Bootloader(引导程序,操作系统启动前最先运行的代码),是你第一次真正掌控机器的代码。 有两个硬性要求必须满足,BIOS 才会认你: 代码必须放在内存 0x7C00 开始的位置 这 512 字节的最后两个字节必须是 0x55 0xAA(引导魔数,BIOS 用来识别可启动扇区的标志) 少一个,BIOS 直接忽略你。 实模式:CPU 刚醒来的状态 启动的时候,CPU 处于实模式(Real Mode,x86 最原始的工作模式,16 位寻址,最多访问 1MB 内存)。 实模式有一个好处:可以直接调用 BIOS 提供的中断服务(INT,软中断,通过中断号调用 BIOS 内置功能)。打印字符用的是 int 0x10,往 ah 里放功能号 0x0E,往 al 里放字符,中断一触发,字符就出现在屏幕上。 这是整个计算机世界里最"低级"的打印方式,但也是最直接的。 两个坑 第一次写这段代码,我卡在两个地方: 段寄存器没初始化。 实模式下内存寻址是通过"段:偏移"来算的,ds、es、ss(数据段、附加段、栈段寄存器)这些上来没初始化,地址算出来是错的。在代码开头手动清零就好。 栈没设置。 汇编代码里调用任何东西之前,栈(Stack,后进先出的内存区域,用于保存临时数据和返回地址)要先指到一个安全的地方。把 sp(栈顶指针)设成 0x7C00 就够了——往下增长,不会覆盖代码。 跑起来 写好汇编,用 NASM(汇编器,把汇编代码编译成机器码)编译成二进制,扔给 QEMU(开源的硬件模拟器,可以模拟一台完整的 x86 计算机)模拟器: ...

May 6, 2026 · 1 min · 大飞

你好,大飞的博客

这是第一篇文章。 这个博客记录我用 AI 学技术的过程——从零写 Linux 内核、理解操作系统原理、各种踩坑实录。 内容不追求完美,追求真实。

May 6, 2026 · 1 min · 大飞
京ICP备14031575号-3