既然我们能够通过根目录条目找到DIR_FstClus了,这个字段告诉了我们文件开始的簇号,它告诉我们文件存放在磁盘的什么位置,从而让我们可以找到它。其实准确来说,这里应该是它告诉了我们文件存放在磁盘数据区的什么位置。需要注意的是,数据区的第一个簇的簇号是2,而不是0或者1.也就是说,如果我们在根目录条目中发现了一个文件,该文件的开始簇号是2,那么就是说,该文件的数据开始于数据区的第一个簇。
那么既然我们可以通过根目录区找到改文件它在磁盘的位置,那么我们还要FAT做什么,原因是对于小于512字节的文件来说,FAT表用处并不大,但如果大于512字节,那么我们就需要依靠FAT表来找到所有的簇,即找到该文件分布在磁盘中的扇区。
FAT表是什么?它有点像一个位图,其中,每12位称为一个FAT项(FATEntry),代表一个簇。第0个和第1个FAT项始终不使用,从第2个FAT项开始表示数据区的每一个簇,也就是说,第2个FAT项表示数据区的第一个簇,这与数据区的第一个簇的簇号为2相呼应的。
由于每个FAT项占12位,包含一个字节和另一个字节的一半,所以显得有点别扭,假设连续3个字节分别如下,那么灰色框表示的是前一个FAT项(FATEntry1),BYTE1是FATEntry的低8位,BYTE2是FATEntry的高4位,白色框表示的是后一个FAT项(FATEntry2),BYTE2的高4位是FATEntry2的低4位,BYTE3是FATEntry2的高8位。
那么FAT项的值代表的是什么呢?因为我们上面说过,我们要依靠FAT表来找到这个文件分布的位置,那么很容易想到FAT项的值就是代表文件的一个簇号,有点类似链表的感觉,如果FAT项的值大于0xFF8,则表示当前簇号已经是文件的最后一个簇了,如果值是0xFF7,表示它是一个坏簇。
那么我们把之前的boot.asm的LABEL_FILENAME_FOUND:jmp做下面的修改
;; 既然发现有LOADER.BIN了,那我们就需要将它加载到内存来了
LABEL_FILENAME_FOUND:
mov ax, RootDirSectors ;ax=14个扇区
and di, 0FFE0h ;di -> 当前条目的开始
add di, 01Ah ;di -> 首Sector根目录条目的前26个字节放着开始的簇号
mov cx, word [es:di]
push cx ;保存此Sector在FAT中的序号
add cx, ax
add cx, DeltaSectorNo ;这句完成时cl里面编程LOADER.BIN的其实扇区号(从0开始数的序号)
mov ax, BaseOfLoader ;es -> BaseOfLoader
mov es, ax
mov bx, OffsetOfLoader ;bx -> OffsetOfLoader 于是,es:bx=BaseOfLoader:OffsetOfLoader=BaseOfLoader*10h+OffsetOfLoader
mov ax, cx ;ax <- Sector号
;; 将LOADER.BIN的内容从磁盘拷贝到内存BaseOfLoader:OffsetOfLoader的位置
LABEL_GOON_LOADING_FILE:
push ax
push bx
mov ah, 0Eh
mov bl, 0Fh
int 10h
pop bx
pop ax
mov cl, 1
call ReadSector
pop ax ;取出此Sector在FAT中的序号,这里存放的是cx的值
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] ;往后加上512个字节
jmp LABEL_GOON_LOADING_FILE
我们的任务就是取出每个在FAT表中的簇号,然后转换为实际的扇区号,然后把实际扇区号中的内容load到内存进来。
GetFATEntry的实现:
;----------------------------------------------------------------------------
;----------------------------------------------------------------------------
; 函数名: GetFATEntry
;----------------------------------------------------------------------------
; 作用:
; 找到序号为 ax 的 Sector 在 FAT 中的条目, 结果放在 ax 中
; 需要注意的是, 中间需要读 FAT 的扇区到 es:bx 处, 所以函数一开始保存了 es 和 bx
; 以上部分是整个函数的难点所在,用户计算簇号在FAT表中所对应的FATENTRY相对于FAT首地址的偏移。
; 从书上可以得知,FAT12中每个FATENTRY是12位的。所以如下:
; 7654 | 3210(byte1) 7654|3210(byte2) 7654|3210(byte3)
; byte1和byte2的低4位表示一个Entry;根据Big-Endian,Entry内容为:3210(byte2)76543210(byte1)
; byte3和byte2的高4位表示一个Entry;根据Big-Endian,Entry内容为:76543210(byte3)7654(byte2)
; 所以这里存在一个奇偶数的问题,以字节为单位。以上为例,Entry0偏移为0,Entry1偏移为1,Entry2偏移为3。
; 以INT[“簇号”*1.5]的方式增加。这也就是为什么上面先乘3再除2来计算。
; 根据DIV指令规定,商保存在ax中,余数在dx中。所以此时ax就是FATENTRY在FAT中以字节为边界的偏移量。
GetFATEntry:
push es ;BaseOfLoader
push bx ;OffsetOfLoader
push ax ;存进此Sector在FAT中的序号
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 * 2
mov bx, 2
div bx ;dx:ax / 2 ==> ax <- 商,dx <- 余数
cmp dx, 0
jz LABEL_EVEN
mov byte [bOdd], 1
;; 偶数处理
LABEL_EVEN:
xor dx, dx ;现在ax中是FATEntry在FAT中的偏移量,下面来计算FATEntry在哪个扇区中(FAT占用不止一个扇区)
mov bx, [BPB_BytsPerSec] ; dx:ax / BPB_BytsPerSec==>ax<-商(FATEntry 所在的扇区相对于 FAT 来说的扇区号)
div bx
push dx
mov bx, 0 ;bx <- 0,于是es:bx=(BaseOfloader-100):00=(BaseOfloader-100)*10h
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 ;ax的值在此被更新
;7654 | 3210(byte1) 7654|3210(byte2) 7654|3210(byte3)
;根据上面这个例子,奇数的簇号,1,偏移为1,所以读要ax中的时候,因为ax是16位,所以根据Big-Endian,ax的内容为7654|3210(byte3)|7654|3210(byte2),所以右移4位就可以了。
;偶数簇号,0,偏移为0,读入ax后,ax的内容为7654|3210(byte2)7654 | 3210(byte1),我们只需要低12位即可,
;所以and ax,0FFFh
LABEL_EVEN_2:
and ax, 0FFFh
LABEL_GET_FAT_ENRY_OK:
pop bx
pop es
ret
最后再来一个跳转:
; *****************************************************************************************************
jmp BaseOfLoader:OffsetOfLoader ; 这一句正式跳转到已加载到内存中的 LOADER.BIN 的开始处
; 开始执行 LOADER.BIN 的代码
; Boot Sector 的使命到此结束
; *****************************************************************************************************
loader.asm内容如下:
org 0100h
mov ax, 0B800h
mov gs, ax
mov ah, 0Fh ; 0000: 黑底 1111: 白字
mov al, 'L'
mov [gs:((80 * 0 + 39) * 2)], ax ; 屏幕第 0 行, 第 39 列
jmp $ ; 到此停住
运行效果: