这节首先介绍了突破引导扇区只有512字节的原理,然后介绍了FAT12文件系统,最后通过实验加载loader并将控制权交给loader来实现突破512字节的束缚。
突破512字节的限制
前面所用的引导扇区只有512字节。然而实际上操作系统在启动过程需要做的事情是很多的。所以需要通过某种方法突破512字节的限制。
那么如何突破512字节的限制呢?一种方法是再建立一个文件,通过引导扇区把它加载到内存,然后把控制权教给它。这样,512字节的束缚就没有了。
这里被引导扇区加载进内存的并不是操作系统的内核。因为从开机到开始运行,操作系统经历了“引导→加载内核入内存→跳入保护模式→开始执行内核”这样一个过程。也就是说,才内核开始执行之前不但要加载内核,而且还有准备保护模式等一系列工作,如果全都交给引导扇区来做,512字节很可能是不够用的。因此,这里加载进内存的并不是内核,而是另外一个模块叫Loader。引导扇区把Loader加载进内存并把控制权交给它。上面所说的其他工作都交给Loader来做。Loader没有512字节的限制。所以会灵活很多。
接下来最主要的是如何找到Loader文件并加载进入内存。首先介绍FAT12文件系统
FAT12
FAT的全称是File Allocation Table。它是DOS时代就开始使用的文件系统(File System),现在的软盘上面仍旧使用此文件系统。FAT把磁盘划分成若干层次以方便组织和管理,这些层次如下:
- 扇区(Sector):磁盘上的最小数据单元。
- 簇(Cluster):一个或多个扇区。
- 分区(Partition):通常指整个文件系统。
下面是FAT12格式的软盘的结构:
引导扇区
首先是引导扇区,它位于第0个扇区。它的结构如下图
引导扇区有一个很重要的数据结构叫做BPB(BIOS ParameterBlock),它以BPB_
开头。以BS_
开头的域不属于BPB,只是引导扇区(Boot
Sector)的一部分。
FAT
可以看到有两个FAT表,FAT2可看作是FAT1的备份,他们通常是一样的。FAT有点像是一个位图。每12位称为一个FAT项(FATEntry),代表一个簇。
通常FAT项的值代表的是文件下一个簇号。从这里可以计算出FAT12中数据区的最大簇号是2^12=4K
,如果每簇512字节,那么最大数据量是4K×512B=2MB
当FAT表项的值大于或等于0xFF8
时,表示当前簇已经是文件的最后一个簇。如果值为0xFF7
,表示它是一个坏簇。
其中第0个和第1个FAT项始终不使用,从第2个FAT项开始表示数据区的每一个簇。也就是说,第二个FAT项表示数据区的第一个簇,所以数据区的第一个簇号是2。
根目录区
根目录区位于第二个FAT表之后,开始的扇区号是19,它由若干个目录条目(Directory Entry)组成,条目最多有BPB_RootEntCnt
个。由于根目录区的大小是依赖于BPB_RootEntCnt的,所以长度不固定。
根目录区的每一个条目占用32字节,格式如下:
根目录区主要定义了名称
、属性
、时间
、开始簇号
、大小
。
数据区
数据区的簇号从2开始。这是因为上面所说的FAT表项从第二个开始。因为根目录区长度不是固定的。所以需要计算数据区的第一个簇号的位置。
如何读取某一文件
首先是进入根目录区根据文件名和属性来寻找文件。找到文件目录项后根据目录向中的开始簇号读取文件第一簇的信息,接下来查看FAT表项,找到文件的下一簇号是啥?如果小于0xFF7,则数据没读取完,如果大于或等于0xFF8则说明文件读取结束
接下来,实现一个最简单的loader并实现加载过程。主要有如下几步:
制作一个DOS可以识别的引导盘
引导扇区需要有BPB等头信息才能被微软识别,我们首先加上它,代码大致如下:
30 ; 下面是 FAT12 磁盘的头
31 BS_OEMName DB ‘ForrestY‘ ; OEM String, 必须 8 个字节
32 BPB_BytsPerSec DW 512 ; 每扇区字节数
33 BPB_SecPerClus DB 1 ; 每簇多少扇区
34 BPB_RsvdSecCnt DW 1 ; Boot 记录占用多少扇区
35 BPB_NumFATs DB 2 ; 共有多少 FAT 表
36 BPB_RootEntCnt DW 224 ; 根目录文件数最大值
37 BPB_TotSec16 DW 2880 ; 逻辑扇区总数
38 BPB_Media DB 0xF0 ; 媒体描述符
39 BPB_FATSz16 DW 9 ; 每FAT扇区数
40 BPB_SecPerTrk DW 18 ; 每磁道扇区数
41 BPB_NumHeads DW 2 ; 磁头数(面数)
42 BPB_HiddSec DD 0 ; 隐藏扇区数
43 BPB_TotSec32 DD 0 ; 如果 wTotalSectorCount 是 0 由这个值记录扇区数
44 BS_DrvNum DB 0 ; 中断 13 的驱动器号
45 BS_Reserved1 DB 0 ; 未使用
46 BS_BootSig DB 29h ; 扩展引导标记 (29h)
47 BS_VolID DD 0 ; 卷序列号
48 BS_VolLab DB ‘OrangeS0.02‘; 卷标, 必须 11 个字节
49 BS_FileSysType DB ‘FAT12 ‘ ; 文件系统类型, 必须 8个字节
现在的软盘已经能够被DOS和Linux识别了,我们已经可以方便地往上添加或删除文件了。
编写一个简单的loader程序
要将Loader
加载到内存中,首先需要有一个Loader
。所以接下来就是写一个最简单的loader,代码如下:
2 org 0100h
3
4 mov ax, 0B800h
5 mov gs, ax
6 mov ah, 0Fh ; 0000: 黑底 1111: 白字
7 mov al, ‘L‘
8 mov [gs:((80 * 0 + 39) * 2)], ax ; 屏幕第 0 行, 第 39 列。
9
10 jmp $ ; Start
将此代码大保存在loader.asm
文件中。这段代码被编译成.COM
文件直接在DOS下执行,效果是在屏幕中央输出字符L
,然后进入死循环。在这里,我们用下面的命令行来编译:
$ nasm loader.asm -o loader.bin
这里面编译出的二进制代码加载到内存的任意位置都可以正确执行,但是我们要扩展它,为了将来的执行不会出现问题,要保证把它放入某个段内偏移0x100的位置。
加载loader进入内存
int 13h
加载软盘上的一个文件进入内存,使用的是BIOS中断int 13h
。它的用法如下图:
从上图可以看出,中断需要的参数不是从第0扇区开始的扇区号,而是柱面号、磁头号以及在当前柱面上的扇区号三个分量。所以要通过下图方法来转换:
软盘相对扇区号的转换
转换的原理如下:
首先,1.44M的软盘结构:一个软盘包括2个盘面(0和1),每个盘面有80条磁道(磁柱),每个磁道有18个扇区,每个扇区大小位512Byte。所以总容量:2×80×18×512Byte=1474569Byte=1.44MB
然后,从第0扇区开始一次编号叫做相对扇区,它与物理位置的关系如下:
0面,0道,1扇区 0 0面,0道,2扇区 1 0面,0道,3扇区 2 ... 0面,0道,18扇区 17 1面,0道,1扇区 18 ... 1面,0道,18扇区 35 0面,1道,1扇区 36 ... 0面,1道,18扇区 53 1面,1道,1扇区 54
读软盘扇区
因为loader可能包含多个扇区,所以接下来写一个读软盘扇区的函数:
215 ;----------------------------------------------------------------------------
216 ; 函数名: ReadSector
217 ;----------------------------------------------------------------------------
218 ; 作用:
219 ; 从第 ax 个 Sector 开始, 将 cl 个 Sector 读入 es:bx 中
220 ReadSector:
221 ; -----------------------------------------------------------------------
222 ; 怎样由扇区号求扇区在磁盘中的位置 (扇区号 -> 柱面号, 起始扇区, 磁头号)
223 ; -----------------------------------------------------------------------
224 ; 设扇区号为 x
225 ; ┌ 柱面号 = y >> 1
226 ; x ┌ 商 y ┤
227 ; -------------- => ┤ └ 磁头号 = y & 1
228 ; 每磁道扇区数 │
229 ; └ 余 z => 起始扇区号 = z + 1
230 push bp
231 mov bp, sp
232 sub esp, 2 ; 辟出两个字节的堆栈区域保存要读的扇区数: byte [bp-2]
233
234 mov byte [bp-2], cl
235 push bx ; 保存 bx
236 mov bl, [BPB_SecPerTrk] ; bl: 除数
237 div bl ; y 在 al 中, z 在 ah 中
238 inc ah ; z ++
239 mov cl, ah ; cl <- 起始扇区号
240 mov dh, al ; dh <- y
241 shr al, 1 ; y >> 1 (其实是 y/BPB_NumHeads, 这里BPB_NumHeads=2)
242 mov ch, al ; ch <- 柱面号
243 and dh, 1 ; dh & 1 = 磁头号
244 pop bx ; 恢复 bx
245 ; 至此, "柱面号, 起始扇区, 磁头号" 全部得到 ^^^^^^^^^^^^^^^^^^^^^^^^
246 mov dl, [BS_DrvNum] ; 驱动器号 (0 表示 A 盘)
247 .GoOnReading:
248 mov ah, 2 ; 读
249 mov al, byte [bp-2] ; 读 al 个扇区
250 int 13h
251 jc .GoOnReading ; 如果读取错误 CF 会被置为 1, 这时就不停地读, 直到正确为止
252
253 add esp, 2
254 pop bp
255
256 ret
上面的代码用到了堆栈,所以程序开头要初始化ss
和esp
:
14 BaseOfStack equ 07c00h ; Boot状态下堆栈基地址(栈底, 从这个位置向低地址生长)
52 mov ax, cs
53 mov ds, ax
54 mov es, ax
55 mov ss, ax
56 mov sp, BaseOfStack
读扇区的函数写好了,接下来就开始在软盘中寻找Loader.bin
寻找loader
主要包括两个寻找:
- 在根目录区寻找
Loader
的第一个扇区 - 在FAT表中寻找
Loader
的其余扇区
根目录区寻找loader.bin
72 ; 下面在 A 盘的根目录寻找 LOADER.BIN
73 mov word [wSectorNo], SectorNoOfRootDirectory
74 LABEL_SEARCH_IN_ROOT_DIR_BEGIN:
75 cmp word [wRootDirSizeForLoop], 0 ; ┓
76 jz LABEL_NO_LOADERBIN ; ┣ 判断根目录区是不是已经读完
77 dec word [wRootDirSizeForLoop] ; ┛ 如果读完表示没有找到 LOADER.BIN
78 mov ax, BaseOfLoader
79 mov es, ax ; es<-BaseOfLoader
80 mov bx, OffsetOfLoader; bx<-OffsetOfLoader于是,es:bx = BaseOfLoader:OffsetOfLoader
81 mov ax, [wSectorNo] ; ax <- Root Directory 中的某 Sector 号
82 mov cl, 1
83 call ReadSector
84
85 mov si, LoaderFileName ; ds:si -> "LOADER BIN"
86 mov di, OffsetOfLoader ; es:di -> BaseOfLoader:0100 = BaseOfLoader*10h+100
87 cld
88 mov dx, 10h
89 LABEL_SEARCH_FOR_LOADERBIN:
90 cmp dx, 0 ; ┓循环次数控制,
91 jz LABEL_GOTO_NEXT_SECTOR_IN_ROOT_DIR ; ┣如果已经读完了一个 Sector,
92 dec dx ; ┛就跳到下一个 Sector
93 mov cx, 11
94 LABEL_CMP_FILENAME:
95 cmp cx, 0
96 jz LABEL_FILENAME_FOUND ; 如果比较了 11 个字符都相等, 表示找到
97 dec cx
98 lodsb ; ds:si -> al
99 cmp al, byte [es:di]
100 jz LABEL_GO_ON
101 jmp LABEL_DIFFERENT ; 只要发现不一样的字符就表明本 DirectoryEntry 不是
102 ; 我们要找的 LOADER.BIN
103 LABEL_GO_ON:
104 inc di
105 jmp LABEL_CMP_FILENAME ; 继续循环
106
107 LABEL_DIFFERENT:
108 and di, 0FFE0h ; else ┓ di &= E0 为了让它指向本条目开头
109 add di, 20h ; ┃
110 mov si, LoaderFileName ; ┣ di += 20h 下一个目录条目
111 jmp LABEL_SEARCH_FOR_LOADERBIN; ┛
112
113 LABEL_GOTO_NEXT_SECTOR_IN_ROOT_DIR:
114 add word [wSectorNo], 1
115 jmp LABEL_SEARCH_IN_ROOT_DIR_BEGIN
116
117 LABEL_NO_LOADERBIN:
118 mov dh, 2 ; "No LOADER."
119 call DispStr ; 显示字符串
120 %ifdef _BOOT_DEBUG_
121 mov ax, 4c00h ; ┓
122 int 21h ; ┛没有找到 LOADER.BIN, 回到 DOS
123 %else
124 jmp $ ; 没有找到 LOADER.BIN, 死循环在这里
125 %endif
126
127 LABEL_FILENAME_FOUND: ; 找到 LOADER.BIN 后便来到这里继续
128 mov ax, RootDirSectors
129 and di, 0FFE0h ; di -> 当前条目的开始
130 add di, 01Ah ; di -> 首 Sector
131 mov cx, word [es:di]
132 push cx ; 保存此 Sector 在 FAT 中的序号
133 add cx, ax
134 add cx, DeltaSectorNo ; cl <- LOADER.BIN的起始扇区号(0-based)
135 mov ax, BaseOfLoader
136 mov es, ax ; es <- BaseOfLoader
137 mov bx, OffsetOfLoader ; bx <- OffsetOfLoader
138 mov ax, cx ; ax <- Sector 号
上面的代码的逻辑过程是:遍历根目录区所有的扇区,将每一个扇区加载入内存,然后从中寻找文件名为loader.bin
的条目,指导找到为止。找到的那一刻,es:di
是指向条目中字母N后面的哪个字符。其中有一些宏定义如下:
17 BaseOfLoader equ 09000h ; LOADER.BIN 被加载到的位置 ---- 段地址
18 OffsetOfLoader equ 0100h ; LOADER.BIN 被加载到的位置 ---- 偏移地址
19
20 RootDirSectors equ 14 ; 根目录占用空间
21 SectorNoOfRootDirectory equ 19 ; Root Directory 的第一个扇区号
还有一些变量和字符串的值定义如下:
176 ;============================================================================
177 ;变量
178 ;----------------------------------------------------------------------------
179 wRootDirSizeForLoop dw RootDirSectors ; Root Directory 占用的扇区数, 在循环中会递减至零.
180 wSectorNo dw 0 ; 要读取的扇区号
181 bOdd db 0 ; 奇数还是偶数
182
183 ;============================================================================
184 ;字符串
185 ;----------------------------------------------------------------------------
186 LoaderFileName db "LOADER BIN", 0 ; LOADER.BIN 之文件名
187 ; 为简化代码, 下面每个字符串的长度均为 MessageLength
188 MessageLength equ 9
189 BootMessage: db "Booting "; 9字节, 不够则用空格补齐. 序号 0
190 Message1 db "Ready. "; 9字节, 不够则用空格补齐. 序号 1
191 Message2 db "No LOADER"; 9字节, 不够则用空格补齐. 序号 2
192 ;============================================================================
读取过程中会打印一些字符,打印字符串的函数如下:
195 ;----------------------------------------------------------------------------
196 ; 函数名: DispStr
197 ;----------------------------------------------------------------------------
198 ; 作用:
199 ; 显示一个字符串, 函数开始时 dh 中应该是字符串序号(0-based)
200 DispStr:
201 mov ax, MessageLength
202 mul dh
203 add ax, BootMessage
204 mov bp, ax ; ┓
205 mov ax, ds ; ┣ ES:BP = 串地址
206 mov es, ax ; ┛
207 mov cx, MessageLength ; CX = 串长度
208 mov ax, 01301h ; AH = 13, AL = 01h
209 mov bx, 0007h ; 页号为0(BH = 0) 黑底白字(BL = 07h)
210 mov dl, 0
211 int 10h ; int 10h
212 ret
loader的第一个扇区找到了,接下来寻找loader的剩下扇区,在FAT表项中寻找下一个扇区号。
由扇区号寻找FAT项的值
22 SectorNoOfFAT1 equ 1 ; FAT1 的第一个扇区号 = BPB_RsvdSecCnt
...
258 ;----------------------------------------------------------------------------
259 ; 函数名: GetFATEntry
260 ;----------------------------------------------------------------------------
261 ; 作用:
262 ; 找到序号为 ax 的 Sector 在 FAT 中的条目, 结果放在 ax 中
263 ; 需要注意的是, 中间需要读 FAT 的扇区到 es:bx 处, 所以函数一开始保存了 es 和 bx
264 GetFATEntry:
265 push es
266 push bx
267 push ax
268 mov ax, BaseOfLoader; `.
269 sub ax, 0100h ; | 在 BaseOfLoader 后面留出 4K 空间用于存放 FAT
270 mov es, ax ; /
271 pop ax
272 mov byte [bOdd], 0
273 mov bx, 3
274 mul bx ; dx:ax = ax * 3
275 mov bx, 2
276 div bx ; dx:ax / 2 ==> ax <- 商, dx <- 余数
277 cmp dx, 0
278 jz LABEL_EVEN
279 mov byte [bOdd], 1
280 LABEL_EVEN:;偶数
281 ; 现在 ax 中是 FATEntry 在 FAT 中的偏移量,下面来
282 ; 计算 FATEntry 在哪个扇区中(FAT占用不止一个扇区)
283 xor dx, dx
284 mov bx, [BPB_BytsPerSec]
285 div bx ; dx:ax / BPB_BytsPerSec
286 ; ax <- 商 (FATEntry 所在的扇区相对于 FAT 的扇区号)
287 ; dx <- 余数 (FATEntry 在扇区内的偏移)。
288 push dx
289 mov bx, 0 ; bx <- 0 于是, es:bx = (BaseOfLoader - 100):00
290 add ax, SectorNoOfFAT1 ; 此句之后的 ax 就是 FATEntry 所在的扇区号
291 mov cl, 2
292 call ReadSector ; 读取 FATEntry 所在的扇区, 一次读两个, 避免在边界
293 ; 发生错误, 因为一个 FATEntry 可能跨越两个扇区
294 pop dx
295 add bx, dx
296 mov ax, [es:bx]
297 cmp byte [bOdd], 1
298 jnz LABEL_EVEN_2
299 shr ax, 4
300 LABEL_EVEN_2:
301 and ax, 0FFFh
302
303 LABEL_GET_FAT_ENRY_OK:
上面寻找loader的工作已经做完了,接下来加载loader:
127 LABEL_FILENAME_FOUND: ; 找到 LOADER.BIN 后便来到这里继续
128 mov ax, RootDirSectors
129 and di, 0FFE0h ; di -> 当前条目的开始
130 add di, 01Ah ; di -> 首 Sector
131 mov cx, word [es:di]
132 push cx ; 保存此 Sector 在 FAT 中的序号
133 add cx, ax
134 add cx, DeltaSectorNo ; cl <- LOADER.BIN的起始扇区号(0-based)
135 mov ax, BaseOfLoader
136 mov es, ax ; es <- BaseOfLoader
137 mov bx, OffsetOfLoader ; bx <- OffsetOfLoader
138 mov ax, cx ; ax <- Sector 号
139
140 LABEL_GOON_LOADING_FILE:
141 push ax ; `.
142 push bx ; |
143 mov ah, 0Eh ; | 每读一个扇区就在 "Booting " 后面
144 mov al, ‘.‘ ; | 打一个点, 形成这样的效果:
145 mov bl, 0Fh ; | Booting ......
146 int 10h ; |
147 pop bx ; |
148 pop ax ; /
149
150 mov cl, 1
151 call ReadSector
152 pop ax ; 取出此 Sector 在 FAT 中的序号
153 call GetFATEntry
154 cmp ax, 0FFFh
155 jz LABEL_FILE_LOADED
156 push ax ; 保存 Sector 在 FAT 中的序号
157 mov dx, RootDirSectors
158 add ax, dx
159 add ax, DeltaSectorNo
160 add bx, [BPB_BytsPerSec]
161 jmp LABEL_GOON_LOADING_FILE
162 LABEL_FILE_LOADED:
163
164 mov dh, 1 ; "Ready."
165 call DispStr ; 显示字符串
向loader交出控制权
万事具备,只差最后一步,向loader交出控制权,可以理解为直接跳转到loader所在的代码执行:
167 ; ****************************************************************************
168 jmp BaseOfLoader:OffsetOfLoader ; 这一句正式跳转到已加载到内
169 ; 存中的 LOADER.BIN 的开始处,
170 ; 开始执行 LOADER.BIN 的代码。
171 ; Boot Sector 的使命到此结束。
172 ; ****************************************************************************
接下来看成果
bochs调试与运行
$ nasm boot.asm -o boot.bin
$ nasm loader.asm -o loader.bin
$ dd if=boot.bin of=a.img bs=512 count=1 conv=notrunc
$ sudo mount -o loop a.img /mnt/floppy
$ sudo cp loader.bin /mnt/floppy/ -v
$ sudo umount /mnt/floppy
运行结果如下:
源代码
;%define _BOOT_DEBUG_
; 做 Boot Sector 时一定将此行注释掉!将此行打开后用 nasm Boot.asm -o Boot.com 做成一个.COM文件易于调试
%ifdef _BOOT_DEBUG_
org 0100h ; 调试状态, 做成 .COM 文件, 可调试
%else
org 07c00h ; Boot 状态, Bios 将把 Boot Sector 加载到 0:7C00 处并开始执行
%endif
;=========================================================
%ifdef _BOOT_DEBUG_
BaseOfStack equ 0100h ; 调试状态下堆栈基地址(栈底, 从这个位置向低地址生长)
%else
BaseOfStack equ 07c00h ; Boot状态下堆栈基地址(栈底, 从这个位置向低地址生长)
%endif
BaseOfLoader equ 09000h ; LOADER.BIN 被加载到的位置 ---- 段地址
OffsetOfLoader equ 0100h ; LOADER.BIN 被加载到的位置 ---- 偏移地址
RootDirSectors equ 14 ; 根目录占用空间
SectorNoOfRootDirectory equ 19 ; Root Directory 的第一个扇区号
SectorNoOfFAT1 equ 1 ; FAT1 的第一个扇区号 = BPB_RsvdSecCnt
DeltaSectorNo equ 17;DeltaSectorNo = BPB_RsvdSecCnt + (BPB_NumFATs * FATSz) - 2
; 文件的开始Sector号 = DirEntry中的开始Sector号 + 根目录占用Sector数目 + DeltaSectorNo
;=========================================================
jmp short LABEL_START ; Start to boot.
nop ; 这个 nop 不可少
; 下面是 FAT12 磁盘的头
BS_OEMName DB ‘ForrestY‘ ; OEM String, 必须 8 个字节
BPB_BytsPerSec DW 512 ; 每扇区字节数
BPB_SecPerClus DB 1 ; 每簇多少扇区
BPB_RsvdSecCnt DW 1 ; Boot 记录占用多少扇区
BPB_NumFATs DB 2 ; 共有多少 FAT 表
BPB_RootEntCnt DW 224 ; 根目录文件数最大值
BPB_TotSec16 DW 2880 ; 逻辑扇区总数
BPB_Media DB 0xF0 ; 媒体描述符
BPB_FATSz16 DW 9 ; 每FAT扇区数
BPB_SecPerTrk DW 18 ; 每磁道扇区数
BPB_NumHeads DW 2 ; 磁头数(面数)
BPB_HiddSec DD 0 ; 隐藏扇区数
BPB_TotSec32 DD 0 ; 如果 wTotalSectorCount 是 0 由这个值记录扇区数
BS_DrvNum DB 0 ; 中断 13 的驱动器号
BS_Reserved1 DB 0 ; 未使用
BS_BootSig DB 29h ; 扩展引导标记 (29h)
BS_VolID DD 0 ; 卷序列号
BS_VolLab DB ‘OrangeS0.02‘; 卷标, 必须 11 个字节
BS_FileSysType DB ‘FAT12 ‘ ; 文件系统类型, 必须 8个字节
LABEL_START:
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, BaseOfStack
; 清屏
mov ax, 0600h ; AH = 6, AL = 0h
mov bx, 0700h ; 黑底白字(BL = 07h)
mov cx, 0 ; 左上角: (0, 0)
mov dx, 0184fh ; 右下角: (80, 50)
int 10h ; int 10h
mov dh, 0 ; "Booting "
call DispStr ; 显示字符串
xor ah, ah ; ┓
xor dl, dl ; ┣ 软驱复位
int 13h ; ┛
; 下面在 A 盘的根目录寻找 LOADER.BIN
mov word [wSectorNo], SectorNoOfRootDirectory
LABEL_SEARCH_IN_ROOT_DIR_BEGIN:
cmp word [wRootDirSizeForLoop], 0 ; ┓
jz LABEL_NO_LOADERBIN ; ┣ 判断根目录区是不是已经读完
dec word [wRootDirSizeForLoop] ; ┛ 如果读完表示没有找到 LOADER.BIN
mov ax, BaseOfLoader
mov es, ax ; es <- BaseOfLoader
mov bx, OffsetOfLoader ; bx <- OffsetOfLoader 于是, es:bx = BaseOfLoader:OffsetOfLoader
mov ax, [wSectorNo] ; ax <- Root Directory 中的某 Sector 号
mov cl, 1
call ReadSector
mov si, LoaderFileName ; ds:si -> "LOADER BIN"
mov di, OffsetOfLoader ; es:di -> BaseOfLoader:0100 = BaseOfLoader*10h+100
cld
mov dx, 10h
LABEL_SEARCH_FOR_LOADERBIN:
cmp dx, 0 ; ┓循环次数控制,
jz LABEL_GOTO_NEXT_SECTOR_IN_ROOT_DIR ; ┣如果已经读完了一个 Sector,
dec dx ; ┛就跳到下一个 Sector
mov cx, 11
LABEL_CMP_FILENAME:
cmp cx, 0
jz LABEL_FILENAME_FOUND ; 如果比较了 11 个字符都相等, 表示找到
dec cx
lodsb ; ds:si -> al
cmp al, byte [es:di]
jz LABEL_GO_ON
jmp LABEL_DIFFERENT ; 只要发现不一样的字符就表明本 DirectoryEntry 不是
; 我们要找的 LOADER.BIN
LABEL_GO_ON:
inc di
jmp LABEL_CMP_FILENAME ; 继续循环
LABEL_DIFFERENT:
and di, 0FFE0h ; else ┓ di &= E0 为了让它指向本条目开头
add di, 20h ; ┃
mov si, LoaderFileName ; ┣ di += 20h 下一个目录条目
jmp LABEL_SEARCH_FOR_LOADERBIN; ┛
LABEL_GOTO_NEXT_SECTOR_IN_ROOT_DIR:
add word [wSectorNo], 1
jmp LABEL_SEARCH_IN_ROOT_DIR_BEGIN
LABEL_NO_LOADERBIN:
mov dh, 2 ; "No LOADER."
call DispStr ; 显示字符串
%ifdef _BOOT_DEBUG_
mov ax, 4c00h ; ┓
int 21h ; ┛没有找到 LOADER.BIN, 回到 DOS
%else
jmp $ ; 没有找到 LOADER.BIN, 死循环在这里
%endif
LABEL_FILENAME_FOUND: ; 找到 LOADER.BIN 后便来到这里继续
mov ax, RootDirSectors
and di, 0FFE0h ; di -> 当前条目的开始
add di, 01Ah ; di -> 首 Sector
mov cx, word [es:di]
push cx ; 保存此 Sector 在 FAT 中的序号
add cx, ax
add cx, DeltaSectorNo ; cl <- LOADER.BIN的起始扇区号(0-based)
mov ax, BaseOfLoader
mov es, ax ; es <- BaseOfLoader
mov bx, OffsetOfLoader ; bx <- OffsetOfLoader
mov ax, cx ; ax <- Sector 号
LABEL_GOON_LOADING_FILE:
push ax ; `.
push bx ; |
mov ah, 0Eh ; | 每读一个扇区就在 "Booting " 后面
mov al, ‘.‘ ; | 打一个点, 形成这样的效果:
mov bl, 0Fh ; | Booting ......
int 10h ; |
pop bx ; |
pop ax ; /
mov cl, 1
call ReadSector
pop ax ; 取出此 Sector 在 FAT 中的序号
call GetFATEntry
cmp ax, 0FFFh
jz LABEL_FILE_LOADED
push ax ; 保存 Sector 在 FAT 中的序号
mov dx, RootDirSectors
add ax, dx
add ax, DeltaSectorNo
add bx, [BPB_BytsPerSec]
jmp LABEL_GOON_LOADING_FILE
LABEL_FILE_LOADED:
mov dh, 1 ; "Ready."
call DispStr ; 显示字符串
; ***************************************************************************
jmp BaseOfLoader:OffsetOfLoader ; 这一句正式跳转到已加载到内
; 存中的 LOADER.BIN 的开始处,
; 开始执行 LOADER.BIN 的代码。
; Boot Sector 的使命到此结束。
; ***************************************************************************
;============================================================================
;变量
;----------------------------------------------------------------------------
wRootDirSizeForLoop dw RootDirSectors ; Root Directory 占用的扇区数, 在循环中会递减至零.
wSectorNo dw 0 ; 要读取的扇区号
bOdd db 0 ; 奇数还是偶数
;============================================================================
;字符串
;----------------------------------------------------------------------------
LoaderFileName db "LOADER BIN", 0 ; LOADER.BIN 之文件名
; 为简化代码, 下面每个字符串的长度均为 MessageLength
MessageLength equ 9
BootMessage: db "Booting "; 9字节, 不够则用空格补齐. 序号 0
Message1 db "Ready. "; 9字节, 不够则用空格补齐. 序号 1
Message2 db "No LOADER"; 9字节, 不够则用空格补齐. 序号 2
;============================================================================
;----------------------------------------------------------------------------
; 函数名: DispStr
;----------------------------------------------------------------------------
; 作用:
; 显示一个字符串, 函数开始时 dh 中应该是字符串序号(0-based)
DispStr:
mov ax, MessageLength
mul dh
add ax, BootMessage
mov bp, ax ; ┓
mov ax, ds ; ┣ ES:BP = 串地址
mov es, ax ; ┛
mov cx, MessageLength ; CX = 串长度
mov ax, 01301h ; AH = 13, AL = 01h
mov bx, 0007h ; 页号为0(BH = 0) 黑底白字(BL = 07h)
mov dl, 0
int 10h ; int 10h
ret
;----------------------------------------------------------------------------
; 函数名: ReadSector
;----------------------------------------------------------------------------
; 作用:
; 从第 ax 个 Sector 开始, 将 cl 个 Sector 读入 es:bx 中
ReadSector:
; -----------------------------------------------------------------------
; 怎样由扇区号求扇区在磁盘中的位置 (扇区号 -> 柱面号, 起始扇区, 磁头号)
; -----------------------------------------------------------------------
; 设扇区号为 x
; ┌ 柱面号 = y >> 1
; x ┌ 商 y ┤
; -------------- => ┤ └ 磁头号 = y & 1
; 每磁道扇区数 │
; └ 余 z => 起始扇区号 = z + 1
push bp
mov bp, sp
sub esp, 2 ; 辟出两个字节的堆栈区域保存要读的扇区数: byte [bp-2]
mov byte [bp-2], cl
push bx ; 保存 bx
mov bl, [BPB_SecPerTrk] ; bl: 除数
div bl ; y 在 al 中, z 在 ah 中
inc ah ; z ++
mov cl, ah ; cl <- 起始扇区号
mov dh, al ; dh <- y
shr al, 1 ; y >> 1 (其实是 y/BPB_NumHeads, 这里BPB_NumHeads=2)
mov ch, al ; ch <- 柱面号
and dh, 1 ; dh & 1 = 磁头号
pop bx ; 恢复 bx
; 至此, "柱面号, 起始扇区, 磁头号" 全部得到 ^^^^^^^^^^^^^^^^^^^^^^^^
mov dl, [BS_DrvNum] ; 驱动器号 (0 表示 A 盘)
.GoOnReading:
mov ah, 2 ; 读
mov al, byte [bp-2] ; 读 al 个扇区
int 13h
jc .GoOnReading ; 如果读取错误 CF 会被置为 1, 这时就不停地读, 直到正确为止
add esp, 2
pop bp
ret
;----------------------------------------------------------------------------
; 函数名: GetFATEntry
;----------------------------------------------------------------------------
; 作用:
; 找到序号为 ax 的 Sector 在 FAT 中的条目, 结果放在 ax 中
; 需要注意的是, 中间需要读 FAT 的扇区到 es:bx 处, 所以函数一开始保存了 es 和 bx
GetFATEntry:
push es
push bx
push ax
mov ax, BaseOfLoader; `.
sub ax, 0100h ; | 在 BaseOfLoader 后面留出 4K 空间用于存放 FAT
mov es, ax ; /
pop ax
mov byte [bOdd], 0
mov bx, 3
mul bx ; dx:ax = ax * 3
mov bx, 2
div bx ; dx:ax / 2 ==> ax <- 商, dx <- 余数
cmp dx, 0
jz LABEL_EVEN
mov byte [bOdd], 1
LABEL_EVEN:;偶数
; 现在 ax 中是 FATEntry 在 FAT 中的偏移量,下面来
; 计算 FATEntry 在哪个扇区中(FAT占用不止一个扇区)
xor dx, dx
mov bx, [BPB_BytsPerSec]
div bx ; dx:ax / BPB_BytsPerSec
; ax <- 商 (FATEntry 所在的扇区相对于 FAT 的扇区号)
; dx <- 余数 (FATEntry 在扇区内的偏移)。
push dx
mov bx, 0 ; bx <- 0 于是, es:bx = (BaseOfLoader - 100):00
add ax, SectorNoOfFAT1 ; 此句之后的 ax 就是 FATEntry 所在的扇区号
mov cl, 2
call ReadSector ; 读取 FATEntry 所在的扇区, 一次读两个, 避免在边界
; 发生错误, 因为一个 FATEntry 可能跨越两个扇区
pop dx
add bx, dx
mov ax, [es:bx]
cmp byte [bOdd], 1
jnz LABEL_EVEN_2
shr ax, 4
LABEL_EVEN_2:
and ax, 0FFFh
LABEL_GET_FAT_ENRY_OK:
pop bx
pop es
ret
;----------------------------------------------------------------------------
times 510-($-$$) db 0 ; 填充剩下的空间,使生成的二进制代码恰好为512字节
dw 0xaa55 ; 结束标志