【参考】 实现X86_64架构下的BootLoader(二)文件系统

https://www.cycycd.com/blog/?p=352

上一章中通过使用Boot程序在屏幕上显示出了“start boot”字符串,如果在这个现有程序上启动Loader原理也不难:要么将Loader直接写在这512B中,统一引导启动;要么单独存放Loader,在Boot中读取Loader程序并写入内存,使用跳转指令去执行Loader程序。但是这样有一些弊端,第一种方法限制了Loader程序的大小(因为整个Boot只能占用1个扇区);第二种方法中,Boot程序需要知道Loader程序存放的起始位置以及Loader程序的大小,这样才能保证每次读取都准确无误,若Loader程序频繁被修改,则需要修改对应的Boot读取程序,这样是非常不利于后续代码的编写的,所以我们需要一个简单的文件系统。

对于容量限制为512B的Boot程序,可以使用简单的FAT12文件系统,该文件系统提供简单的文件搜索,按文件读写等功能,满足我们管理和加载Loader的需要。

FAT12文件系统结构由引导程序、FAT表、根目录区和数据区构成。引导程序即之前的Boot程序,固定占用1个扇区,FAT表分为FAT表1和FAT表2,存储完全相同的内容,冗余的一份作为文件恢复用,一个FAT表占用9个扇区;接下来是根目录系统,根目录系统存储文件的基本信息,占用扇区数由引导程序定义的参数计算得出:(根目录最大文件数*单个目录项大小)/每个扇区字节数;再下来是数据区,存储文件的数据内容,长度不定长。下面分别分析每个结构的功能和作用。

引导程序比较简单,除了Boot中需要的一些标志位之外,还需要在数据开始时标记一些Flag,用来标记硬件和文件系统的一些基本信息,固定格式如下:

名称 偏移 长度 内容 软盘参考值
BS_jmpBoot 0 3   jmp LABEL_STARTnop
BS_OEMName 3 8 厂商名 ‘ForrestY’
BPB_BytsPerSec 11 2 每扇区字节数 0x200(即十进制512)
BPB_SecPerClus 13 1 每簇扇区数 0x01
BPB_RsvdSecCnt 14 2 Boot记录占用多少扇区 0x01
BPB_NumFATs 16 1 共有多少FAT表 0x02
BPB_RootEntCnt 17 2 根目录文件数最大值 0xE0 (224)
BPB_TotSec16 19 2 扇区总数 0xB40(2880)
BPB_Media 21 1 介质描述符 0xF0
BPB_FATSz16 22 2 每FAT扇区数 0x09
BPB_SecPerTrk 24 2 每磁道扇区数 0x12
BPB_NumHeads 26 2 磁头数 0x02
BPB_HiddSec 28 4 隐藏扇区数 0
BPB_TotSec32 32 4 如果BPB_TotSec16是0,由这个值记录扇区数 0xB40(2880)
BS_DrvNum 36 1 中断13的驱动器号 0
BS_Reserved1 37 1 未使用 0
BS_BootSig 38 1 扩展引导标记 0x29
BS_VolD 39 4 卷序列号 0
BS_VolLab 43 11 卷标 ‘OrangeS0.02’
BS_FileSysType 54 8 文件系统类型 ‘FAT12’
引导代码 62 448 引导代码、数据及其他填充字符等  
结束标志 510 2   0xAA55

FAT表、根目录区、数据区之间的关系比较复杂,这里先说FAT表和数据区的关系。FAT表固定占用9个扇区,也就是9*512B=4608B, 其中存储一定数量的FAT表项,每个FAT表项为12bit,则最多有4608/1.5=3072个表项,表项1和表项2保留,所以最多可用表项为3070个。 数据区由许多个簇构成,簇是一个逻辑上的概念,是由引导程序定义,根据偏移量来获取的一片数据区域,在引导程序中可以看到一个簇大小为一个扇区(512B)。FAT表的表项和数据区的簇是一一对应关系,不过由于FAT[0]和FAT[1]另作他用,所以簇的编号直接从2开始。

在磁盘的使用过程中经常需要写操作和删操作,如果直接线性连续的存储整个文件数据,在使用一定时间后,会出现很多碎片化空间,这些碎片化空间不足以存储一个较大的文件,所以引入了簇的概念,这样就使得数据不必在磁盘中连续的存储,而是分割成若干个簇,可以在磁盘中分段存储,例如一个文件大小为600B,那么就会占用两个簇,这两个簇在磁盘中不一定是连续的,那么文件系统怎么标记哪些簇和哪些簇是属于一个文件的呢?这里就要用到簇和FAT表的配合了,FAT表项的存储内容有相关规则:000h表示对应的簇可用,002h~FEFh表示下一个簇的簇号,FF0h~FF6h为保留簇,FF7h为坏簇,FF8h~FFFh表示为文件的最后一个簇。可以看出,簇和FAT表共同实现了一个单向链表的功能,下面用图解来更直观的解析:

以上图为例,上面的表为FAT表,下面的为数据区的簇,现在从3号簇开始,取出“H”,然后查看3号簇对应的FAT表的FAT[3]中为004h,得知下一个簇是4号簇,从4号簇中取出“E”,然后查看4号簇对应的FAT[4]中为006h,得知下一个簇为6号簇…以此类推,最后从10号簇中读出“O”,然后查看10号簇中对应的FAT[10]中为FF8h为结束符(该簇为最后一个簇),整个读取过程结束,读出“HELLO”数据,这就是FAT表和簇实现单向链表来读取数据的过程,同理也可以使用该原理来存储数据。

问题似乎已经解决了,但是在上述过程中还有一个重要的未解决的问题,那就是我们是怎么知道数据是从3号簇也就是“H”开始的?也就是什么在充当链表的表头?答案就是最后一个结构,根目录区。根目录区也是由一条条记录构成,每条数据大小为32Byte,结构如下所示:

可以看到,里面记录了文件开始的簇号,也就是说,根目录区的记录在承担表头的功能;除了簇号之外,还存储了文件的一些基本信息。至此,FAT12文件系统下文件的读写已经很明了了,下面是代码实现:

org 0x7c00 

BaseOfStack equ 0x7c00

BaseOfLoader equ 0x1000
OffsetOfLoader equ 0x00

RootDirSectors equ 14
SectorNumOfRootDirStart equ 19
SectorNumOfFAT1Start equ 1
SectorBalance equ 17 
;FAT12文件系统标记
 jmp short Label_Start
 nop
 BS_OEMName db 'testboot'
 BPB_BytesPerSec dw 512
 BPB_SecPerClus db 1
 BPB_RsvdSecCnt dw 1
 BPB_NumFATs db 2
 BPB_RootEntCnt dw 224
 BPB_TotSec16 dw 2880
 BPB_Media db 0xf0
 BPB_FATSz16 dw 9
 BPB_SecPerTrk dw 18
 BPB_NumHeads dw 2
 BPB_HiddSec dd 0
 BPB_TotSec32 dd 0
 BS_DrvNum db 0
 BS_Reserved1 db 0
 BS_BootSig db 0x29
 BS_VolID dd 0
 BS_VolLab db 'boot loader'
 BS_FileSysType db 'FAT12   '

Label_Start:

 mov ax, cs
 mov ds, ax
 mov es, ax
 mov ss, ax
 mov sp, BaseOfStack

;清屏

 mov ax, 0600h
 mov bx, 0700h
 mov cx, 0
 mov dx, 0184fh
 int 10h

;显示startboot

 mov ax, 0200h
 mov bx, 0000h
 mov dx, 0000h
 int 10h

 mov ax, 1301h
 mov bx, 000fh
 mov dx, 0000h
 mov cx, 10
 push ax
 mov ax, ds
 mov es, ax
 pop ax
 mov bp, StartBootMessage
 int 10h

;搜索Loader文件
 mov word [SectorNo], SectorNumOfRootDirStart

Label_Search_In_Root_Dir_Begin:

 cmp word [RootDirSizeForLoop], 0
 jz Label_No_LoaderBin
 dec word [RootDirSizeForLoop] 
 mov ax, 00h
 mov es, ax
 mov bx, 8000h
 mov ax, [SectorNo]
 mov cl, 1
 call Func_ReadOneSector
 mov si, LoaderFileName
 mov di, 8000h
 cld
 mov dx, 10h
 
Label_Search_For_LoaderBin:

 cmp dx, 0
 jz Label_Goto_Next_Sector_In_Root_Dir
 dec dx
 mov cx, 11

Label_Cmp_FileName:

 cmp cx, 0
 jz Label_FileName_Found
 dec cx
 lodsb 
 cmp al, byte [es:di]
 jz Label_Go_On
 jmp Label_Different

Label_Go_On:
 
 inc di
 jmp Label_Cmp_FileName

Label_Different:

 and di, 0ffe0h
 add di, 20h
 mov si, LoaderFileName
 jmp Label_Search_For_LoaderBin

Label_Goto_Next_Sector_In_Root_Dir:
 
 add word [SectorNo], 1
 jmp Label_Search_In_Root_Dir_Begin
 
;未搜索到Loader,显示错误信息

Label_No_LoaderBin:

 mov ax, 1301h
 mov bx, 008ch
 mov dx, 0100h
 mov cx, 21
 push ax
 mov ax, ds
 mov es, ax
 pop ax
 mov bp, NoLoaderMessage
 int 10h
 jmp $

;搜索到Loader,进行读取

Label_FileName_Found:

 mov ax, RootDirSectors
 and di, 0ffe0h
 add di, 01ah
 mov cx, word [es:di]
 push cx
 add cx, ax
 add cx, SectorBalance
 mov ax, BaseOfLoader
 mov es, ax
 mov bx, OffsetOfLoader
 mov ax, cx

Label_Go_On_Loading_File:
 push ax
 push bx
 mov ah, 0eh
 mov al, '.'
 mov bl, 0fh
 int 10h
 pop bx
 pop ax

 mov cl, 1
 call Func_ReadOneSector
 pop ax
 call Func_GetFATEntry
 cmp ax, 0fffh
 jz Label_File_Loaded
 push ax
 mov dx, RootDirSectors
 add ax, dx
 add ax, SectorBalance
 add bx, [BPB_BytesPerSec]
 jmp Label_Go_On_Loading_File

Label_File_Loaded:
;此时跳转Loader,未完成

;扇区读取模块

Func_ReadOneSector:
 
 push bp
 mov bp, sp
 sub esp, 2
 mov byte [bp - 2], cl
 push bx
 mov bl, [BPB_SecPerTrk]
 div bl
 inc ah
 mov cl, ah
 mov dh, al
 shr al, 1
 mov ch, al
 and dh, 1
 pop bx
 mov dl, [BS_DrvNum]
Label_Go_On_Reading:
 mov ah, 2
 mov al, byte [bp - 2]
 int 13h
 jc Label_Go_On_Reading
 add esp, 2
 pop bp
 ret

;FAT表的索引

Func_GetFATEntry:

 push es
 push bx
 push ax
 mov ax, 00
 mov es, ax
 pop ax
 mov byte [Odd], 0
 mov bx, 3
 mul bx
 mov bx, 2
 div bx
 cmp dx, 0
 jz Label_Even
 mov byte [Odd], 1

Label_Even:

 xor dx, dx
 mov bx, [BPB_BytesPerSec]
 div bx
 push dx
 mov bx, 8000h
 add ax, SectorNumOfFAT1Start
 mov cl, 2
 call Func_ReadOneSector
 
 pop dx
 add bx, dx
 mov ax, [es:bx]
 cmp byte [Odd], 1
 jnz Label_Even_2
 shr ax, 4

Label_Even_2:
 and ax, 0fffh
 pop bx
 pop es
 ret

;临时变量

RootDirSizeForLoop dw RootDirSectors
SectorNo dw 0
Odd db 0

;常量字符串

StartBootMessage: db "Start Boot"
NoLoaderMessage: db "ERROR:No LOADER Found"
LoaderFileName: db "LOADER  BIN",0

;填充至512B

 times 510 - ($ - $$) db 0
 dw 0xaa55

程序可以分为几个大部分:FAT12文件系统定义、搜索模块、扇区读取模块、文件读取模块,其中扇区读取模块封装BIOS系统中断提供的基本的扇区读取功能,供文件读取模块调用,文件读取模块将Loader文件数据从磁盘读取到内存地址0x10000处后,执行跳转将CPU控制权交给Loader。整个程序主要有以下几个问题需要注意:

扇区读取模块涉及逻辑扇区(LBA)转物理扇区(CHS),CHS模式是一个历史遗留问题,CHS模式的硬盘如下:

CHS模式由柱面(磁道)、磁头、扇区组成,定位一片扇区位置的描述是第a柱面第b磁头第c扇区,需要三个参数,而LBA模式将磁盘视为逻辑上的一片线性地址,直接通过第n扇区获取指定扇区,上述参数中,CHS中的柱面、磁头和LBA中的扇区编号均从0开始,CHS中的扇区编号从1开始。LBA模式转CHS模式公式如下:

从商Q采用的处理方式可以看出,最后LBA线性地址映射到磁盘上,顺序必然是第0磁道第0磁头,第0磁道第1磁头,第1磁道第0磁头,第1磁道第1磁头,第2磁道第0磁头…至于为什么这样?统一标准咯,余数R+1是因为CHS模式下扇区编号从1开始。

FAT表项的索引模块需要通过当前的FAT表项找到下一个簇和它的FAT表项,簇比较简单,FAT表项中记录的正是簇号,可以轻易的算出偏移地址,进而取到簇的内容,而FAT表项比较复杂,因为一个FAT表项占12bit,即1.5字节,下面借助Excel表格加以说明:

这个表格的上面一个格子表示8bit(1Byte),而下面一个格子表示1个FAT表项,如果要取到一个FAT表项,则需要读2B的数据到一个16位寄存器中,并且寄存器的数据的低4位或高4位无效。用簇号乘1.5,如果有小数,说明存储单元的高4位数据无效(注:这里的存储单元是对内存和寄存器而言,因为寄存器和内存最小读写粒度为Byte,所以尽管一个表项只占1.5Byte,但是仍需要读取2Byte的数据),如果是整数,说明存储单元的低4位数据无效 。这里我们优化一下策略,不乘1.5,而是乘3除以2,商即为开始读取的位置,固定读取两个字节,余数为1则高4位无效,余数为0则低4位无效。

这里还有一个问题,上面的读取过程是从内存到CPU的,那需要在之前将哪些数据读入内存呢?有个比较简单的方法,就是将整个FAT表读入内存,然后直接根据上面计算得出的偏移量进行进一步的读取,为了节省内存空间,我们可以只读取部分FAT表。上面乘3除以2得到商是字节偏移量,为了方便区分我们记为A,用A再除以每个扇区的字节数得到商B,余数C,可想而知,商B即为扇区偏移量,余数C是新的字节偏移量。用停车场举例子,我们可以描述一辆车的位置在“从第一层开始按顺序往上数第352个车位上”,假设每层固定有100个车位,我们就可以将描述简化为“第4层的第52个车位”。FAT表区域前还有一个大小为1扇区的引导程序,所以扇区偏移量为B+1,然后需要通过扇区读取模块连续读入两个扇区的内容到内存(因为一个扇区大小为512B,而一个表项为1.5B,必然会出现某一个表项横跨两个扇区的情况,所以需要读取两个扇区确保数据完整),再进一步根据C来读取2Byte数据,即可拿到目标表项。当然,还需要根据上一节中乘3除以2的余数来判断高四位无效还是低4位无效。

在实现文件系统后,光盘镜像文件就可以被 UltraISO等程序识别了,可以直接用 UltraISO打开文件并添加Loader程序,文件系统将在Loader程序完成后测试。

posted @ 2020-07-22 09:43  姜大伟  阅读(622)  评论(0编辑  收藏  举报