• 【参考】 实现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程序完成后测试。

  • 相关阅读:
    几种php加速器比较
    细说firewalld和iptables
    Linux上iptables防火墙的基本应用教程
    mysql 字符串按照数字类型排序
    《设计模式之禅》之六大设计原则下篇
    《设计模式之禅》之六大设计原则中篇
    《设计模式之禅》之六大设计原则上篇
    git bash 乱码问题之解决方案
    nexus没有授权导致的错误
    Java之微信公众号开发
  • 原文地址:https://www.cnblogs.com/davytitan/p/13358881.html
Copyright © 2020-2023  润新知