起因

自制操作系统系列的第一课:加电之后 CPU 到底做了什么、怎么让它跑起我们自己的程序。这条链路的第一棒就是引导程序(boot sector)。

本系列学习有前面的汇编学习基础最好,如果没有影响也不大本系列学习主要资源来自《[30天自制操作系统].(川合秀实)》,《自己动手写操作系统》两本书

需求

  • 引导程序在 FAT12 软盘镜像里找到 NotOneOS.sys,把它所在的磁盘块载入内存
  • 跳转执行它,并用 bochs 调试器验证“确实跳过去了”

技术实现原理

为什么是 0x7c00 和 512 字节

x86 上电后进入实模式,BIOS 做完自检,把启动盘的第一个扇区(512 字节,结尾要有 0x55AA 标记)整块读到内存 0x7c00,然后跳过去执行——这就是引导程序的身世:被 BIOS 固定搬到 0x7c00 的 512 字节。512 字节装不下任何“操作系统”,所以它的宿命就是搬运工:把盘上更多内容读进内存,再把控制权交出去。

FAT12 的盘面布局:0x2600 和 0x4200 是算出来的

引导程序要找文件,就得按文件系统的规矩来。FAT12 软盘的扇区分配是固定的:1 个引导扇区 + 2 份 FAT 表(各 9 扇区)+ 根目录区(224 项,占 14 扇区)+ 之后全是数据区。换算一下:根目录从第 19 扇区开始,19×512 = 0x2600——文件名在这里;数据区从第 33 扇区开始,33×512 = 0x4200——文件内容从这里起放。前一节已经把整块盘搬进内存 0x8000 起,于是“找文件”变成“查内存”:磁盘偏移 + 0x8000 就是内存地址,0x4200 处的内容落在 0xc200。跳到 0xc200 执行,就是运行 NotOneOS.sys。

读盘循环:CHS 与重试

INT 13h 按“柱面-磁头-扇区”三元组读盘,一次一个扇区。读完一个扇区 ES 加 0x20(512 字节换算成段单位),扇区数满 18 就进位到磁头,磁头满 2 就进位到柱面,直到读满 10 个柱面。读失败不是异常而是常态(软盘的特性),所以每个扇区都带“失败重试 5 次 + 复位驱动器再来”的逻辑——这段是整个加载过程的可靠性来源。

为什么调试用 bochs

bochs 自带调试器,能反汇编、下断点、看寄存器:u 0x7c00 反汇编引导区找到关键跳转,b 0x7cca 下断点,单步后看寄存器与跳转目标——确认程序真的跳到了 0xc200 的 NotOneOS.sys。

一、准备工作

bochs
virtualbox,windows虚拟机
nasm

二、操作流程

回顾前文的程序,我们的引导程序已经可以加载软盘的10个柱面的数据到内存的 0x0820 处,引导扇区本身的512字节由BIOS加载到 0x7c00 处(所以代码里 org 07c00h)。
这一节让程序从这些数据中找到软盘上的程序,并运行其中的程序。

下面程序的重点是加载文件后,程序所在位置。通过前一节可以知道:

文件名在磁盘的是 0x2600处
文件内容在磁盘的0x4200处
磁盘的内容加载到内存的0x8000处
所以磁盘的0x4200处的内容在 内存的 0x8000+0x4200=0xc200处

  1. 修改程序
 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
;%define _BOOT_DEBUG_   ; 做Boot Sector时把这行注释掉
; 启用这行就用nasm Boot.asm -o Boot.com生成.com文件用于调试
%ifdef _BOOT_DEBUG_   
org  0100h%
else   
org 07c00h
%endif
CYLS EQU 10                ; 一共读取10个柱面, 共 10柱面*2面*18扇区*512字节 = 184320 byte = 180K; 把软盘按Fat12格式填充
start:
JMP init                ; 跳转指令
DB       0x90            ; 空 DB,DD用来写单字节   
DB       "NotOneOS"      ; 厂商名,8字节,DB用来写双字节   
DW       512             ; 每个扇区大小512字节,DW用来写4字节   
DB       1               ; 每个簇的扇区数   
DW       1               ; Boot占的扇区   
DB       2               ; 有2个FAT表   
DW       224             ; 根目录大小224   
DW       2880            ; 磁盘扇区总数 2880   
DB       0xf0            ; 介质描述符,磁盘种类必须为0xf0   
DW       9               ; 每个FAT扇区数   
DW       18              ; 每个磁道18个扇区   
DW       2               ; 2个磁头
DD       0               ; 隐藏扇区数
DD       2880            ; 同上,磁盘大小
DB       0, 0, 0x29      ; 0x29 扩展引用标记
DD       0xffffffff      ; 无意义,固定这么写
DB       "NotOneOS   "   ; 磁盘名(卷标),11字节
DB       "FAT12   "      ; 磁盘格式名,8字节
RESB     18              ; 空18个字节,填充0x00
init:
MOV      AX,0
MOV      SS,AX
MOV      SP,0x7c00      ; 堆栈空间,从0x7c00向前
MOV      DS,AX; 读取磁盘
MOV      AX,0x0820      ; 把磁盘数据加载到内存0x0820处。 0x8000~0x81ff的512字节给启动区用的,所以从0x8200开始
MOV      ES,AX          ; 注意 ES:BX 是指向的地址,后面还需要对BX赋值0
; 初始化磁盘接口
MOV      CH,0           ; 柱面 0
MOV      DH,0           ; 磁头 0
MOV      CL,2           ; 扇区 2
readloop:
MOV      SI,0           ; 记录失败次数
retry:
MOV      AH,0x02        ; 0x02 读磁盘
MOV      AL,1           ; 读1个扇区
MOV      BX,0
MOV      DL,0x00        ; A驱动器
INT      0x13           ; BIOS 读磁盘功能
JNC      next           ; 成功跳转
ADD      SI,1           ; 失败加一次
CMP      SI,5           ; 到5次就跳到error
JAE      error
MOV      AH,0x00        ; 复位磁盘功能
MOV      DL,0x00        ; A 驱动器
INT      0x13           ; 重置磁盘驱动器
JMP      retry          ; 重试
next:
MOV      AX,ES          ; 内存地址向后移动0x0020
ADD      AX,0x0020
MOV      ES,AX          ; 通过AX给ES加0x0020
ADD      CL,1           ; 扇区+1
CMP      CL,18          ; 有没有到18个扇区
JBE      readloop       ; CL<=18,就跳到 readloop
MOV      CL,1
ADD      DH,1
CMP      DH,2
JB       readloop       ; 如果 DH < 2 ,则跳到readloop
MOV      DH,0
ADD      CH,1
CMP      CH,CYLS
JB       readloop       ; 如果CH
  1. 编译,生成镜像 ```sh make img
 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


  3. Win7虚拟机中打开A盘,装载镜像  
    
    
    
    
      
        
        
      
    
    
![](/uploads/images/c871989eba2e1ea6b2f1cb3bffdbda5c.png)

NotOneOs.sys拷到A盘



![](/uploads/images/6cb8d5696753d049f4fe96b6b5de6f74.png)  
再移除虚拟盘:

![](/uploads/images/8468a552bcb6d6e1b3acf5ed9397363d.png) 

# 三、启动镜像

这时候boot.img里包含了启动的NotOneOs.sys程序,使用bochs加载运行

    这里进入调试状态,以确定是否运行了NotOneOs.sys程序make debug b 0x7c00cs



![](/uploads/images/9a9b3ef90fa95c482c45d43af35809fb.png)  
通过反汇编,可以找到我们的重点代码位置:

    u 0x7c00 0x7cff

![](/uploads/images/77aff9f66e8318f1f5ae1ef9663998a8.png) 

0x7cca处设置断点:

    b 0x7cca

![](/uploads/images/11865b74cf936ed8b0b949756e694d2d.png) 

查看现在寄存器的值:

rdx的值里看到 DH确实为1,再次单步运行,程序跳转到了0xc200

![](/uploads/images/11865b74cf936ed8b0b949756e694d2d.png) 

0xc200处正是NotOneOs.sys的程序

![](/uploads/images/bf55d521b686db87e9460b272654d45f.png) 

![](/uploads/images/de2fd71316fcdbbf846ebd4857b112de.png) 

后面的重心终于可以跳到C语言了

# 踩坑记录与注意事项

- **拷完文件先移除虚拟盘**NotOneOS.sys 拷进 A 盘后不移除就接着跑 bochs,镜像里可能还是旧内容——装载工具的写入要等卸载落盘;
- **引导扇区必须恰好 512 字节、结尾 0x55AA**:超长或没有结束标记,BIOS 直接认为“没有可启动设备”,跟代码逻辑对不对没关系;
- **org 07c00h 不能省**nasm 不设加载基址,代码里的绝对地址全部错位,跳转飞到不知哪里;
- **FAT12 头字段填错,Windows 挂载镜像就报未格式化**:每扇区 512、每簇 1 扇区、2  FAT、根目录 224 项这些参数决定布局,错一个文件就拷不进去;
- **跳转地址算错一位就是满屏乱指令**0x8000+0x4200=0xc200 这笔账在 bochs 里用反汇编一核对就现形;
- **INT 13h 偶发读失败是常态**:重试与复位逻辑别当冗余代码删掉,删了就随机加载失败。