《30天自制操作系统》笔记-day3
本文最后更新于 2026年7月13日 晚上
Day3
Day3 是这本书的第一个分水岭。前两天的重点在”写一个能被 BIOS 识别并启动的 512 字节引导扇区”,而今天我们要让那个引导扇区去磁盘上把真正的操作系统加载进来。这意味着本章将首次覆盖:
- 磁盘的 CHS(柱面/磁头/扇区)寻址模型,理解数据的物理组织方式;
- 用 BIOS 的
int 0x13中断逐扇区读盘,配合失败重试逻辑处理硬件不稳定性; - 段寄存器 ×16 + 偏移量 的 20 位地址计算,以及 Intel 在硬件层面专门设计加法通路来支持 1MB 寻址的巧思;
- 在 16 位实模式下完成所有依赖 BIOS 的初始化(屏幕模式、键盘状态、显存地址),为切换到 32 位保护模式扫清障碍;
- 首次将 C 语言引入项目——从
bootpack.c到haribote.sys的完整编译链接工具链,以及用汇编封装HLT指令供 C 调用的跨语言衔接。
如果说 Day1 是擦出一颗火星,Day2 做出了一个打火机,那 Day3 就是把发动机点着了——虽然跑起来还只是个”黑屏”,但这已经是真正运行在裸机上、由我们自己编译链接而成的程序。
硬盘
[[磁盘]]基础知识
本项目的IPL启动区位于磁盘的C0-H0-S1(柱面0,磁头0,扇区1),下一个C0-H0-S2就是我们这次想要装载的扇区
缓冲区地址
是一个内存地址,要把从软盘上读取的数据装载到内存的哪个位置
设计初期BX寄存器最大表示0xffff,数值也就是0~65535,也就是64KB的内存地址,随着技术发展内存容量提升,就出现了EBX寄存器,也就是在BX基础上做了扩展,位宽32位,十六进制:0xffffffff,4G的内存地址
在BIOS设计阶段,是没有EBX寄存器的,就出现了段寄存器,这是一个方法
不得不佩服设计CPU的人的思路,为了让CPU能够支持1MB内存,在硬件层面专门设计了20位的加法通路,让其能够实现段寄存器
公式:$ES \times 16 + BX$ES代表基地址,x16之后就是相当于左移了4位,让CPU支持的内存从64K扩大到了1MBX为一个偏移量
例子:
ES=0x0820
BX=0
公式计算出来,基地址就是0x8200,BX=0的话就是不做偏移,首地址就是0x8200
扇区一次读取512B,最后计算出的范围就是0x8200~0x83ff
段地址计算的结果(20bit的物理地址),直接发送给内存控制器,不会再存放在寄存器中
在代码上的体现
1 | |
BIOS中断函数
INT0x13
是实模式下 BIOS 提供的磁盘服务中断,用于读写软盘、硬盘、光驱等存储设备,仅在 16 位实模式(DOS、Bootloader、裸机汇编)可用,保护模式下失效。
| Function | Description |
|---|---|
| AH = 0x0 | 重置软盘/硬盘 |
| AH = 0x2 | 以CHS模式读取软盘/硬盘 |
| AH = 0x3 | 以CHS模式写入软盘/硬盘 |
| AH = 0x15 | 检测第二个磁盘 |
| AH = 0x41 | 测试BIOS强化磁盘驱动服务(EDD)的存在情况 |
| AH = 0x42 | 以LBA模式读取软盘/硬盘 |
| AH = 0x43 | 以LBA模式写入软盘/硬盘 |
返回值
- FLASC.CF == 0
- AH = 0 无错误
- FLAGS.CF == 1
- 有错误,错误号存入
AH内
- 有错误,错误号存入
汇编语言
JC
进位标志,如果是1的就跳转
如果INT 0x13的返回值
- 如果没有错,进位标志是
0 - 如果有错,进位标志是
1
1 | |
中断函数执行后标志位状态,如果是0跳过,如果是1(错)跳转到error
JNC
与JC相反
1 | |
中断函数执行后返回的值,如果是1跳过,如果是0(对)跳转到readload
JAE
大于或等于时跳转
1 | |
SI寄存器的数值与5对比,大于等于5就跳到error
JBE
小于等于则跳转
1 | |
CL寄存器的数值与18对比,小于等于就跳到readload
JB
小于则跳转
1 | |
DH寄存器的数值与2对比,小于就跳到readload
EQU
定义了一个常量
1 | |
读取硬盘到内存程序
1 | |
当前程序从[[Day2]]的基础上引入了[[磁盘]]的概念
涉及到磁盘的原理
程序流程:
整体前置初始化
ORG 0x7c00标记程序被 BIOS 加载到内存 0x7C00;写入 FAT12 软盘头部规范字段,满足启动盘格式。entry寄存器初始化:AX 清零,设置栈段 SS 与栈指针 SP、数据段 DS,保证内存访问、堆栈可用。- 配置磁盘加载参数:
- ES=0x0820:磁盘扇区加载到内存物理地址
0x0820 << 4 = 0x8200; - CH=0、DH=0、CL=2:从 0 柱面、0 磁头、2 号扇区开始读取(1 号扇区是自身引导程序,无需读取)。
- ES=0x0820:磁盘扇区加载到内存物理地址
循环读取磁盘主体(readloop)
4. 每次读取前清空 SI,SI 用作读盘失败计数器。
5. retry 分支调用 BIOS 中断 int 0x13 ah=02 读取 1 个扇区到 ES:0:
- 读取成功(CF=0):跳 readload,处理下一扇区地址与寻址参数;
- 读取失败(CF=1):
失败计数 SI 自增 1;
判断失败次数是否≥5:
- 达到 5 次:跳 error 打印错误信息;
- 不足 5 次:调用 int 0x13 ah=00 重置软盘驱动器,回到 retry 重新尝试读盘。
readload:更新内存地址 + 磁盘寻址,循环判断是否读完
- 内存地址偏移:ES 增加 0x20,物理内存向后偏移 512 字节(1 扇区),用于存放下一扇区数据。
- 扇区号 CL 自增 1;若 CL ≤ 18(当前磁头扇区未读完),直接跳回 readloop 读取下一扇区。
- 当前磁头 18 个扇区全部读完:
- 重置 CL=1,准备从第 1 扇区重新开始;
- DH 磁头 + 1,切换到软盘反面磁头;
- 判断 DH 是否小于 2:
- DH<2:跳回 readloop,读取同一柱面反面所有扇区;
- DH≥2:正反面磁头全部读完,DH 重置为 0,柱面 CH 自增 1。
- 判断当前柱面 CH 是否小于 CYLS (10):
- CH<10:跳回 readloop,读取下一个柱面;
- CH≥10:0~9 共 10 个柱面全部读取完成,跳转打印成功提示。
打印分支
- error 分支:SI 指向错误字符串
ermsg,进入 putloop 循环打印; - 全部扇区读取完成:SI 指向成功字符串
scmsg,进入 putloop 循环打印; - putloop 通用打印子程序:逐个读取 SI 指向的字符输出到屏幕,读到 0 终止符后跳 fin 停机。
到此启动区已经完成
操作系统部分
最简单的操作系统
1 | |
保存为haribote.nas,nask编译成haribyte.sys,并且保存到之前的img文件里面
这里需要使用makefile工具来进行编译
如何编译
基于Day2的Makefile文件做出一定的修改
1 | |
命令执行
1 | |
可以使用十六进制编辑器查看新生成的img
在0x2600可以看到sys文件的文件名,说明已经把haribyte.sys写入到了img文件中
再看0x4200这个是sys的文件内容,可以通过十六进制编辑器来查看haribyte.sys的内容进行验证
验证:haribyte.sys
总结以上内容
- 文件名会写在0x002600以后的地方
- 文件的内容会写在0x004200以后的地方
从启动区执行操作系统
要把磁盘的内容加载到内存当中,也就是把0x4200位置的内容装载到内存中
程序从启动区开始,把磁盘上的内容装载到0x8000地址上,所以最终内存上计算出来就是0x8000 + 0x4200 = 0xC200
计算完毕后就可以写操作系统的内容
1 | |
保存之后运行就会直接出现一个黑色界面
说明运行时没问题的
32位模式前期准备
32位模式支持大于1MB的内存,32位拥有自我保护功能,能隔离非法的机器语言,以免破坏系统
但是如果使用32位模式又会有另一个问题,BIOS是16位的机器语言,一旦进去32位模式就不能调用BIOS中断函数(或者说很麻烦调用),所以我们要在进入32位模式之前完成所有依赖BIOS的操作
BIOS操作程序
1 | |
[VMODE]:八位色彩模式[SCRNX/Y]:X/Y方向的像素值[VRAM]:显存地址的起始位置
程序流程:
- 定义了各种启动信息的内存地址的别名
- 显示器的各种数据保存在对应的内存地址
- 获取键盘的LED指示灯状态
导入C语言
关于haribote.nas后续改名为asmhead.nas并且还有一些内容,但是作者表示需要先学习后面内容再回头学习这部分内容,所以暂时把harib00i中的文件复制过来,现在先来看C语言部分
新建一个bootpack.c文件
1 | |
C语言到机器语言
ccl.exe编译器把bootpack.c文件转为bootpack.gas文件gas2nask.exe把bootpack.gas转为bootpack.nas文件nask.exe把bootpack.nas转为bootpack.obj文件obi2bim.exe把bootpack.obj转为bootpack.bim文件bim2hrb.exe把bootpack.bim转为bootpack.hrb文件
把bootpack.hrb与asmhead.bin结合在一起就是haribote.sys
为了完成上面的整个流程,所以需要继续修改Makefile文件
1 | |
1 | |
运行,出现黑色界面即代表成功
HLT
C语言中不能直接使用HLT指令,所以需要自己写一个类HLT函数
这里要用汇编语言写一个函数,文件naskfunc.nas
1 | |
函数名:_io_hlt
因为是用汇编写的函数,要与bootpack.obj链接,所以也需要编译成目标文件,因此输出格式设定为WCOFF模式,同时设定为32位模式
nask目标文件模式下,必须设定文件名信息
下面再写明程序的函数名,函数名前加上_否则不能很好的链接
需要链接的函数名,需要用GLOBAL指令声明
现在可以在bootpack.c中调用这个函数
1 | |
源文件编辑完毕后在Makefile中添加一段
1 | |
同样,运行成功!
总结
回顾 Day3,我们其实走完了一个微型操作系统的完整启动链路:
1 | |
具体来说:
- IPL 不再只是一个”能被启动的 512 字节”,而是学会了按 CHS 地址逐扇区从软盘读取数据,把控制权交给磁盘上的真正操作系统。引入失败重试机制也让程序从”玩具”向”工程”迈了一小步。
- 段寄存器与 20 位地址通路——Intel 在 8086 时代的硬件设计决策,40 年后依然以某种形式影响着 x86 体系结构。站在 2026 年的角度看这个”为了省几根引脚而发明段寄存器”的故事,仍然觉得精巧。
- C 语言的引入是架构上的转折点:从此以后,核心逻辑用 C 写,只有必须触碰硬件指令的地方(HLT、端口 I/O 等)才用汇编封装。这个分工方式一直延续到操作系统内核开发的工业实践中。
- 编译工具链从
nask单步汇编,升级为cc1 → gas2nask → nask → obj2bim → bim2hrb的多级流水线——每一步做什么、输入输出是什么,搞清楚了以后就不会对着 Makefile 发怵。
Day3 结束时,系统虽然只显示一个黑屏,但它已经跑通了从冷启动到执行我们自己的 C 代码的完整路径。下一章(Day4)将在这个基础上引入画面模式切换和图形绘制,黑屏马上就要亮起来了。
总结
Day3 是全书第一个真正的分水岭——从前两天的”写一个能被 BIOS 启动的 512 字节引导扇区”,跨越到”让引导扇区从磁盘加载完整的操作系统”。核心收获如下:
- CHS 磁盘寻址:理解磁盘的物理三维坐标——柱面(Cylinder)、磁头(Head)、扇区(Sector),是实模式下 BIOS 读盘的唯一坐标体系。扇区从 1 开始编号,盘片正反两面由磁头号切换,柱面则是最慢的维度变化。
- BIOS
int 0x13磁盘服务:AH=0x02读盘、AH=0x00重置驱动器。每次读盘后检查 CF(进位标志),非 0 表示失败 → 重置 → 重试,超过 5 次才报错——这是嵌入式/系统编程中处理不可靠硬件的经典模式。 - 段寄存器 ×16 + 偏移量:Intel 8086 为用 16 位寄存器寻址 1MB 空间,在硬件层面设计了 20 位加法通路。
ES × 16 + BX等价于 ES 左移 4 位后与 BX 相加,这是理解实模式内存模型的核心公式。 - 条件跳转指令族:
JC/JNC(进位标志)、JAE/JBE/JB(无符号比较)——这些是汇编级流程控制的基础,配合CMP构建所有分支逻辑。EQU则是汇编级的宏常量,相当于 C 的#define。 - IPL 完整流程:从 C0-H0-S2 开始逐扇区读盘 → 每读完一扇区缓冲区地址 +0x200 → 扇区号递增 → 磁头翻转 → 柱面递增 → 直到读完 CYLS 个柱面,控制权移交操作系统。失败重试、地址偏移、寻址参数切换一应俱全。
- Makefile 编译流水线:
cc1(.c→.gas) → gas2nask(.gas→.nas) → nask(.nas→.obj) → obj2bim(.obj→.bim) → bim2hrb(.bim→.hrb),最后copy /B asmhead.bin + bootpack.hrb = haribote.sys。每一步的输入输出搞清楚,整个工具链就不会发怵。 - C 与汇编的跨语言衔接:C 不能直接执行
HLT,需要在汇编中封装为函数_io_hlt,声明为GLOBAL,按WCOFF目标文件格式编译后与bootpack.obj链接——这是内核开发中”核心逻辑用 C、硬件指令用汇编封装”这一分工模式的起点。 - 32 位模式前置准备:BIOS 是 16 位代码,进入 32 位保护模式后无法调用。因此在切入之前,必须做完所有 BIOS 依赖的初始化——设置 VGA 模式(
int 0x10, AX=0x0013)、获取键盘 LED 状态(int 0x16, AH=0x02)、记录屏幕参数和显存地址到约定内存位置,供后续 C 代码使用。
Day3 结束时,系统虽然只显示一块黑屏,但冷启动 → BIOS → IPL → 加载 OS → 实模式初始化 → 执行 C 代码的完整链路已经打通。