《30天自制操作系统》笔记-day3

本文最后更新于 2026年7月9日 凌晨

Day3

Day3 是这本书的第一个分水岭。前两天的重点在”写一个能被 BIOS 识别并启动的 512 字节引导扇区”,而今天我们要让那个引导扇区去磁盘上把真正的操作系统加载进来。这意味着本章将首次覆盖:

  • 磁盘的 CHS(柱面/磁头/扇区)寻址模型,理解数据的物理组织方式;
  • 用 BIOS 的 int 0x13 中断逐扇区读盘,配合失败重试逻辑处理硬件不稳定性;
  • 段寄存器 ×16 + 偏移量 的 20 位地址计算,以及 Intel 在硬件层面专门设计加法通路来支持 1MB 寻址的巧思;
  • 在 16 位实模式下完成所有依赖 BIOS 的初始化(屏幕模式、键盘状态、显存地址),为切换到 32 位保护模式扫清障碍;
  • 首次将 C 语言引入项目——从 bootpack.charibote.sys 的完整编译链接工具链,以及用汇编封装 HLT 指令供 C 调用的跨语言衔接。

如果说 Day1 是擦出一颗火星,Day2 做出了一个打火机,那 Day3 就是把发动机点着了——虽然跑起来还只是个”黑屏”,但这已经是真正运行在裸机上、由我们自己编译链接而成的程序。

硬盘

[[磁盘]]基础知识

本项目的IPL启动区位于磁盘的C0-H0-S1(柱面0,磁头0,扇区1),下一个C0-H0-S2就是我们这次想要装载的扇区

缓冲区地址

是一个内存地址,要把从软盘上读取的数据装载到内存的哪个位置

设计初期BX寄存器最大表示0xffff,数值也就是0~65535,也就是64KB的内存地址,随着技术发展内存容量提升,就出现了EBX寄存器,也就是在BX基础上做了扩展,位宽32位,十六进制:0xffffffff4G的内存地址

BIOS设计阶段,是没有EBX寄存器的,就出现了段寄存器,这是一个方法

不得不佩服设计CPU的人的思路,为了让CPU能够支持1MB内存,在硬件层面专门设计了20位的加法通路,让其能够实现段寄存器

公式:$ES \times 16 + BX$
ES代表基地址,x16之后就是相当于左移了4位,让CPU支持的内存从64K扩大到了1M
BX为一个偏移量

例子:
ES=0x0820
BX=0
公式计算出来,基地址就是0x8200BX=0的话就是不做偏移,首地址就是0x8200
扇区一次读取512B,最后计算出的范围就是0x8200~0x83ff

段地址计算的结果(20bit的物理地址),直接发送给内存控制器,不会再存放在寄存器中

在代码上的体现

1
2
MOV CX,[1234] = MOV CX,[DS:1234]
MOV CX,[SI] = MOV CX,[DS:SI]

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模式写入软盘/硬盘

返回值

  1. FLASC.CF == 0
    • AH = 0 无错误
  2. FLAGS.CF == 1
    • 有错误,错误号存入AH

汇编语言

JC

进位标志,如果是1的就跳转

如果INT 0x13的返回值

  • 如果没有错,进位标志是0
  • 如果有错,进位标志是1
1
2
INT 0x13
JC error

中断函数执行后标志位状态,如果是0跳过,如果是1(错)跳转到error

JNC

JC相反

1
2
INT 0x13
JNC readload

中断函数执行后返回的值,如果是1跳过,如果是0(对)跳转到readload

JAE

大于或等于时跳转

1
2
    CMP SI,5
    JAE error

SI寄存器的数值与5对比,大于等于5就跳到error

JBE

小于等于则跳转

1
2
CMP CL,18
JBE readloop

CL寄存器的数值与18对比,小于等于就跳到readload

JB

小于则跳转

1
2
CMP DH,2
JB readloop

DH寄存器的数值与2对比,小于就跳到readload

EQU

定义了一个常量

1
2
CYLS EQU 10
; 相当于 #define CYLS 10

读取硬盘到内存程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
; hello-os
; TAB=4

CYLS EQU 10 ; 相当于#define CYLS 10

ORG 0x7c00 ; 指明程序装载地址

; FAT12格式软盘的专用代码

JMP entry
DB 0x90
DB "HELLOIPL" ; 启动区名称(任意字符串)
DW 512 ; 每个扇区大小(必须是512byte)
DB 1 ; 簇的大小(必须是一个扇区)
DW 1 ; FAT的起始位置
DB 2 ; FAT的个数
DW 224 ; 根目录的大小(一般为224项)
DW 2880 ; 磁盘大小(必须是2880扇区)
DB 0xF0 ; 磁盘种类(必须是F0)
DW 9 ; FAT长度(必须是9扇区)
DW 18 ; 1个磁道有多少个扇区(必须是18)
DW 2 ; 磁头数(必须是2)
DD 0 ; 不使用分区,必须是0
DD 2880 ; 重写一次磁盘大小
DB 0,0,0x29 ; 意义不明,固定
DD 0xFFFFFFFF ; (可能是)卷标号码
DB "HELLO-OS "; 磁盘名称(11byte)
DB "FAT12 " ; 磁盘格式名称(8byte)
RESB 18 ; 空出18个字节

; 程序主体

entry:
MOV AX,0 ;初始化寄存器
MOV SS,AX
MOV SP,0x7c00
MOV DS,AX

; 设置内存基地址
MOV AX,0x0820
MOV ES,AX

; 设置硬盘读取位置参数
MOV CH,0 ; 柱面0
MOV DH,0 ; 磁头0
MOV CL,2 ; 扇区2

readloop:

MOV SI,0 ; 记录失败次数寄存器

retry:
; 开始读盘
MOV AH,0x02 ; 读盘标志
MOV AL,1 ; 1个扇区
MOV BX,0 ; 偏移量为0
MOV DL,0x00 ; A驱动器
INT 0x13 ; 调用BIOS
JNC readload ; 进位标志为0,也就是没出错的话跳转readload

; 失败次数判断
ADD SI,1 ; 往SI+1
CMP SI,5 ; 比较SI与5
JAE error ; SI >= 5时,跳转error

; 如果失败之后重置再读
MOV AH,0x00 ; 重置标志
MOV DL,0x00 ; A驱动器
INT 0x13 ; 重置驱动器
JMP retry


readload:

; 把内存地址后移0x200 512字节
; ES没有ADD指令,需要转换一下
MOV AX,ES
ADD AX,0x0020
MOV ES,AX

; CL是扇区号,ES指定读入地址。

; CL从2递增至18,逐个扇区读取
ADD CL,1 ; CL+1
CMP CL,18 ; 比较CL与18
JBE readloop ; CL <= 18, 跳转readloop

;C0-H0-S18下一个扇区是盘片的反面C0-H1-S1
MOV CL,1 ; 扇区重置为1
ADD DH,1 ; 切换磁头为1
CMP DH,2 ; DH与2做比较
JB readloop ; 比较结果<2就跳转到readloop
MOV DH,0 ; 如果>=2就切换磁头为0
ADD CH,1 ; 切换到下一个柱面
CMP CH,CYLS ; CH与CYLS做比较
JB readloop ; CH < CYLS 就跳转readloop

MOV SI,scmsg ; 打印加载成功信息
JMP putloop


fin:
HLT ;让CPU停止,等待指令
JMP fin ;无限循环

error:
MOV SI,ermsg
JMP putloop


putloop:
MOV AL,[SI]
ADD SI,1 ;给SI+1
CMP AL,0
JE fin
MOV AH,0x0e ;显示一个文字
MOV BX,15 ;指定字符颜色
INT 0x10 ;调用显卡BIOS
JMP putloop

ermsg:
DB 0x0a, 0x0a
DB "load error"
DB 0x0a
DB 0x00

scmsg:
DB 0x0a, 0x0a
DB "load success"
DB 0x0a
DB 0x00

RESB 0x7DFE-$ ; 补0

DB 0x55, 0xaa ;启动标签

当前程序从[[Day2]]的基础上引入了[[磁盘]]的概念
涉及到磁盘的原理

程序流程:

整体前置初始化

  1. ORG 0x7c00 标记程序被 BIOS 加载到内存 0x7C00;写入 FAT12 软盘头部规范字段,满足启动盘格式。
  2. entry 寄存器初始化:AX 清零,设置栈段 SS 与栈指针 SP、数据段 DS,保证内存访问、堆栈可用。
  3. 配置磁盘加载参数:
    • ES=0x0820:磁盘扇区加载到内存物理地址 0x0820 << 4 = 0x8200
    • CH=0、DH=0、CL=2:从 0 柱面、0 磁头、2 号扇区开始读取(1 号扇区是自身引导程序,无需读取)。

循环读取磁盘主体(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:更新内存地址 + 磁盘寻址,循环判断是否读完

  1. 内存地址偏移:ES 增加 0x20,物理内存向后偏移 512 字节(1 扇区),用于存放下一扇区数据。
  2. 扇区号 CL 自增 1;若 CL ≤ 18(当前磁头扇区未读完),直接跳回 readloop 读取下一扇区。
  3. 当前磁头 18 个扇区全部读完:
    • 重置 CL=1,准备从第 1 扇区重新开始;
    • DH 磁头 + 1,切换到软盘反面磁头;
    • 判断 DH 是否小于 2:
      • DH<2:跳回 readloop,读取同一柱面反面所有扇区;
      • DH≥2:正反面磁头全部读完,DH 重置为 0,柱面 CH 自增 1。
  4. 判断当前柱面 CH 是否小于 CYLS (10):
    • CH<10:跳回 readloop,读取下一个柱面;
    • CH≥10:0~9 共 10 个柱面全部读取完成,跳转打印成功提示。

打印分支

  1. error 分支:SI 指向错误字符串ermsg,进入 putloop 循环打印;
  2. 全部扇区读取完成:SI 指向成功字符串scmsg,进入 putloop 循环打印;
  3. putloop 通用打印子程序:逐个读取 SI 指向的字符输出到屏幕,读到 0 终止符后跳 fin 停机。

到此启动区已经完成

操作系统部分

最简单的操作系统

1
2
3
fin:
HLT
JMP fin

保存为haribote.nas,nask编译成haribyte.sys,并且保存到之前的img文件里面
这里需要使用makefile工具来进行编译

如何编译

基于Day2的Makefile文件做出一定的修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# 相对路径
TOOLPATH = ../z_tools/
MAKE = $(TOOLPATH)make.exe -r
NASK = $(TOOLPATH)nask.exe
EDIMG = $(TOOLPATH)edimg.exe
IMGTOL = $(TOOLPATH)imgtol.com

COPY = copy
DEL = del

default:
$(MAKE) img

# 文件生成规则

ipl.bin : ipl.nas Makefile
$(NASK) ipl.nas ipl.bin ipl.list

haribote.sys: haribote.nas Makefile
$(NASK) haribote.nas haribote.sys haribote.lst

haribote.img: ipl.bin haribote.sys Makefile
$(EDIMG) imgin:../z_tools/fdimg0at.tek \
wbinimg src:ipl.bin len:512 from:0 to:0\
copy from:haribote.sys to:@: \
imgout:haribote.img


# 命令

img:
$(MAKE) haribote.img


run:
$(MAKE) img
$(COPY) haribote.img ..\z_tools\qemu\fdimage0.bin
$(MAKE) -C ../z_tools/qemu

clean :
-$(DEL) ipl.bin
-$(DEL) ipl.lst
-$(DEL) haribote.sys
-$(DEL) haribote.lst

src_only :
$(MAKE) clean
-$(DEL) haribote.img

命令执行

1
2
make img //生成img文件
make run //运行img文件

可以使用十六进制编辑器查看新生成的img
0x2600可以看到sys文件的文件名,说明已经把haribyte.sys写入到了img文件中

再看0x4200这个是sys的文件内容,可以通过十六进制编辑器来查看haribyte.sys的内容进行验证

验证:haribyte.sys

总结以上内容

  1. 文件名会写在0x002600以后的地方
  2. 文件的内容会写在0x004200以后的地方

从启动区执行操作系统

要把磁盘的内容加载到内存当中,也就是把0x4200位置的内容装载到内存中

程序从启动区开始,把磁盘上的内容装载到0x8000地址上,所以最终内存上计算出来就是0x8000 + 0x4200 = 0xC200

计算完毕后就可以写操作系统的内容

1
2
3
4
5
6
7
8
9
10
11
12
; haribote.nas
; TAB = 4

ORG 0xc200 ; 程序装载位置

MOV AL,0x13 ; VGA显卡 320x200x8位彩色
MOV AH,0x00
INT 0x10

fin:
HLT
JMP fin

保存之后运行就会直接出现一个黑色界面

说明运行时没问题的

32位模式前期准备

32位模式支持大于1MB的内存,32位拥有自我保护功能,能隔离非法的机器语言,以免破坏系统

但是如果使用32位模式又会有另一个问题,BIOS是16位的机器语言,一旦进去32位模式就不能调用BIOS中断函数(或者说很麻烦调用),所以我们要在进入32位模式之前完成所有依赖BIOS的操作

BIOS操作程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
; haribote.nas
; TAB = 4

; BOOT_INFO

CYLS EQU 0x0ff0 ; 设定启动区
LEDS EQU 0x0ff1
VMODE EQU 0x0ff2 ; 关于颜色数目的信息。颜色位数
SCRNX EQU 0x0ff4 ; 屏幕分辨率x
SCRNY EQU 0x0ff6 ; 屏幕分辨率y
VRAM EQU 0x0ff8 ; 图像缓冲区的开始地址


ORG 0xc200 ; 程序装载位置

MOV AL,0x13 ; VGA显卡 320x200x8位彩色
MOV AH,0x00
INT 0x10

MOV BYTE [VMODE],8 ; 记录画面模式
MOV WORD [SCRNX],320 ; 设置X的值
MOV WORD [SCRNY],200 ; 设置Y的值
MOV DWORD [VRAM],0x000a0000 ; 显存的初始地址

; BIOS取的键盘上各种LED指示灯的状态
MOV AH,0x02 ; 获取修饰键状态
INT 0x16 ; keyboard BIOS
MOV [LEDS],AL


fin:
HLT
JMP fin

[VMODE]:八位色彩模式
[SCRNX/Y]:X/Y方向的像素值
[VRAM]:显存地址的起始位置

程序流程:

  1. 定义了各种启动信息的内存地址的别名
  2. 显示器的各种数据保存在对应的内存地址
  3. 获取键盘的LED指示灯状态

导入C语言

关于haribote.nas后续改名为asmhead.nas并且还有一些内容,但是作者表示需要先学习后面内容再回头学习这部分内容,所以暂时把harib00i中的文件复制过来,现在先来看C语言部分

新建一个bootpack.c文件

1
2
3
4
void HariMain(void){
    fin:
        goto fin;
}

C语言到机器语言

  1. ccl.exe编译器把bootpack.c文件转为bootpack.gas文件
  2. gas2nask.exebootpack.gas转为bootpack.nas文件
  3. nask.exebootpack.nas转为bootpack.obj文件
  4. obi2bim.exebootpack.obj转为bootpack.bim文件
  5. bim2hrb.exebootpack.bim转为bootpack.hrb文件

bootpack.hrbasmhead.bin结合在一起就是haribote.sys

为了完成上面的整个流程,所以需要继续修改Makefile文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# 相对路径
TOOLPATH = ../z_tools/
INCPATH = ../z_tools/haribote/

MAKE = $(TOOLPATH)make.exe -r
NASK = $(TOOLPATH)nask.exe
EDIMG = $(TOOLPATH)edimg.exe
IMGTOL = $(TOOLPATH)imgtol.com
CC1 = $(TOOLPATH)cc1.exe -I$(INCPATH) -Os -Wall -quiet
GAS2NASK = $(TOOLPATH)gas2nask.exe -a
BIM2HRB = $(TOOLPATH)bim2hrb.exe
OBJ2BIM = $(TOOLPATH)obj2bim.exe
RULEFILE = $(TOOLPATH)haribote/haribote.rul


COPY = copy
DEL = del

default:
$(MAKE) img

# 文件生成规则

ipl.bin : ipl.nas Makefile
$(NASK) ipl.nas ipl.bin ipl.list

asmhead.bin: asmhead.nas Makefile
$(NASK) asmhead.nas asmhead.bin asmhead.list

bootpack.gas: bootpack.c Makefile
$(CC1) -o bootpack.gas bootpack.c

bootpack.nas: bootpack.gas Makefile
$(GAS2NASK) bootpack.gas bootpack.nas

bootpack.obj: bootpack.nas Makefile
$(NASK) bootpack.nas bootpack.obj bootpack.list

bootpack.bim: bootpack.obj Makefile
$(OBJ2BIM) @$(RULEFILE) out:bootpack.bim stack:3136k map:bootpack.map \
bootpack.obj

bootpack.hrb: bootpack.bim Makefile
$(BIM2HRB) bootpack.bim bootpack.hrb 0

haribote.sys: asmhead.bin bootpack.hrb Makefile
copy /B asmhead.bin+bootpack.hrb haribote.sys

haribote.img: ipl.bin haribote.sys Makefile
$(EDIMG) imgin:../z_tools/fdimg0at.tek \
wbinimg src:ipl.bin len:512 from:0 to:0\
copy from:haribote.sys to:@: \
imgout:haribote.img


# 命令

img:
$(MAKE) haribote.img


run:
$(MAKE) img
$(COPY) haribote.img ..\z_tools\qemu\fdimage0.bin
$(MAKE) -C ../z_tools/qemu

clean :
-$(DEL) ipl.bin
-$(DEL) ipl.lst
-$(DEL) haribote.sys
-$(DEL) haribote.lst

src_only :
$(MAKE) clean
-$(DEL) haribote.img
1
./make run # 运行

运行,出现黑色界面即代表成功

HLT

C语言中不能直接使用HLT指令,所以需要自己写一个类HLT函数

这里要用汇编语言写一个函数,文件naskfunc.nas

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
; naskfunc.nas
; TAB = 4

[FORMAT "WCOFF"] ; 制作目标文件的模式
[BITS 32] ; 制作32位模式用的机器语言

; 制作目标文件的信息

[FILE "naskfunc.nas"] ; 源文件名信息

GLOBAL _io_hlt ; 程序中包含的函数名

[SECTION .text] ; 目标文件中写了这些之后再写程序

_io_hlt: ;void io_hlt(void);
HLT
RET ; 类似c的return

函数名:_io_hlt
因为是用汇编写的函数,要与bootpack.obj链接,所以也需要编译成目标文件,因此输出格式设定为WCOFF模式,同时设定为32位模式

nask目标文件模式下,必须设定文件名信息
下面再写明程序的函数名,函数名前加上_否则不能很好的链接
需要链接的函数名,需要用GLOBAL指令声明

现在可以在bootpack.c中调用这个函数

1
2
3
4
5
6
7
8
void io_halt(void); //声明函数

void HariMain(void){

fin:
io_hlt(); // 执行naskfunc.nas中的io_hlt
goto fin;
}

源文件编辑完毕后在Makefile中添加一段

1
2
3
4
5
6
7
naskfunc.obj : naskfunc.nas Makefile
    $(NASK) naskfunc.nas naskfunc.obj naskfunc.lst
   
# 下面段落做相应位置覆盖
bootpack.bim: bootpack.obj naskfunc.obj Makefile
$(OBJ2BIM) @$(RULEFILE) out:bootpack.bim stack:3136k map:bootpack.map \
bootpack.obj naskfunc.obj

同样,运行成功!

总结

回顾 Day3,我们其实走完了一个微型操作系统的完整启动链路

1
2
BIOS → IPL(引导扇区) → 从磁盘加载 haribote.sys → 实模式初始化 →
汇编/C 混合链接 → 进入 32 位保护模式 → 调用 HLT 停机

具体来说:

  1. IPL 不再只是一个”能被启动的 512 字节”,而是学会了按 CHS 地址逐扇区从软盘读取数据,把控制权交给磁盘上的真正操作系统。引入失败重试机制也让程序从”玩具”向”工程”迈了一小步。
  2. 段寄存器与 20 位地址通路——Intel 在 8086 时代的硬件设计决策,40 年后依然以某种形式影响着 x86 体系结构。站在 2026 年的角度看这个”为了省几根引脚而发明段寄存器”的故事,仍然觉得精巧。
  3. C 语言的引入是架构上的转折点:从此以后,核心逻辑用 C 写,只有必须触碰硬件指令的地方(HLT、端口 I/O 等)才用汇编封装。这个分工方式一直延续到操作系统内核开发的工业实践中。
  4. 编译工具链nask 单步汇编,升级为 cc1 → gas2nask → nask → obj2bim → bim2hrb 的多级流水线——每一步做什么、输入输出是什么,搞清楚了以后就不会对着 Makefile 发怵。

Day3 结束时,系统虽然只显示一个黑屏,但它已经跑通了从冷启动到执行我们自己的 C 代码的完整路径。下一章(Day4)将在这个基础上引入画面模式切换和图形绘制,黑屏马上就要亮起来了。


《30天自制操作系统》笔记-day3
https://www.ming-ice-tea.top/2026/07/09/《30天自制操作系统》笔记-day3/
作者
Ming
发布于
2026年7月9日
许可协议