美文网首页程序员
实现一个简单的64位操作系统 (0x03)熟悉FAT12文件系统

实现一个简单的64位操作系统 (0x03)熟悉FAT12文件系统

作者: KernelThread | 来源:发表于2018-08-27 16:04 被阅读11次

    0x01 概述

    之前已经实现了一个简单的boot程序,但是它最大只能占一个Sector,也就是512 Bytes,局限性太大,能够完成的工作不多。接下来就要想办法加载一个更大的程序,用这个更大的程序来加载内核。这个程序就是Loader。
    为了能够避免地址硬编码的问题,需要实现一个简单的文件系统来加载Loader与内核。当有了一个文件系统之后,就能灵活地将数据写入软盘以及加载到内存中了。
    这里选择的是FAT12文件系统,这个文件系统广泛地用于微软早期的各个系统中。关于FAT12文件系统的介绍可以看FAT12文件系统之引导扇区结构来了解FAT12文件系统的引导扇区结构,以及FAT12文件系统之数据存储方式详解来了解FAT12文件系统中文件的存储方式。
    强烈推荐看这篇文章:FAT Filesystem 这篇文章对FAT文件系统讲得非常详细,甚至细到了每一个字段能够取哪些值,以及取值的意义。
    这一章计划使用C语言实现一个对FAT12文件系统镜像的解析程序。选择先用C语言实现的原因是能够从镜像外的角度来观察镜像,能够对整个镜像有一个直观的了解。
    通过使用C语言对FAT12的镜像文件进行解析,能够熟悉FAT12文件系统,同时,之后的boot实现只需要将C实现人工“翻译”成汇编即可。

    0x02 设计

    实现一个程序,能够解析FAT12文件系统的镜像,输出其引导扇区结构,遍历根目录,以及对根目录特定文件数据进行读取。

    0x03 准备镜像

    准备镜像使用到了WinImage。使用WinImage创建了一个新镜像,然后往镜像内写入了几个文件,以及创建了一个目录。如下图。


    新镜像文件

    将其保存为sample_image.ima(注意不要保存为压缩模式,也不要加密,不然无法正确解析)。

    0x04 实现

    (1) main函数的实现

    main函数负责将镜像文件读到内存中,并调用函数解析内存中的镜像文件。由于镜像文件不大,整个才1.44MB,所以索性将它整个读到内存中了。
    先从命令行参数中拿到镜像文件名,并打开镜像。

        if(argc != 2)
        {
            printf("Usage: %s ImageFile\n", argv[0]);
            return 1;
        }
    
        // open image file
        FILE *pImageFile = fopen(argv[1], "rb");
    
        if(pImageFile == NULL)
        {
            puts("Read image file failed!");
            return 1;
        }
    

    然后获取文件大小并申请一个Buffer来存储文件数据。

        // get file size
        fseek(pImageFile,0,SEEK_END);
        long lFileSize = ftell(pImageFile);
    
        printf("Image size: %ld\n",lFileSize);
    
        // alloc buffer
        unsigned char *pImageBuffer = (unsigned char *)malloc(lFileSize);
    
        if(pImageBuffer == NULL)
        {
            puts("Memmory alloc failed!");
            return 1;
        }
    

    接着将文件读到Buffer中,并关闭文件。

        // set file pointer to the beginning
        fseek(pImageFile,0,SEEK_SET);
    
        // read the whole image file into memmory
        long lReadResult = fread(pImageBuffer,1,lFileSize,pImageFile);
    
        printf("Read size: %ld\n",lReadResult);
    
        if(lReadResult != lFileSize)
        {
            puts("Read file error!");
            free(pImageBuffer);
            fclose(pImageFile);
            return 1;
        }
    
        // finish reading, close file
        fclose(pImageFile);
    

    最后,调用PrintImage函数打印FAT12的引导扇区结构,调用SeekRootDir来遍历根目录,调用ReadFile来读入指定文件,然后讲读到的内容输出到屏幕上。

        // finish reading, close file
        fclose(pImageFile);
    
        // print FAT12 structure
        PrintImage(pImageBuffer);
    
        // seek files of root directory
        SeekRootDir(pImageBuffer);
    
        // file read buffer
        unsigned char outBuffer[2048];
    
        // read file 0
        DWORD fileSize = ReadFile(pImageBuffer, &FileHeaders[0], outBuffer);
    
        printf("File size: %u, file content: \n%s",fileSize, outBuffer);
    

    至此,main函数就实现完成了。接下来就要开始PrintImage、SeekRootDir、ReadFile这三个函数的实现了。

    (2) 准备结构体

    由于FAT12的引导扇区结构以及目录项结构的每个字段都是固定长度的,所以可以通过使用结构体方便地解析它们。
    在定义它们的结构体之前,需要先给出几个宏,让每个字段的大小有一个直观的了解(原谅我的Windows混搭风格,后续再慢慢改~)。

    #define BYTE    unsigned char
    #define WORD    unsigned short
    #define DWORD   unsigned int
    
    #define BOOT_START_ADDR 0x7c00
    

    使用unsigned的原因是它们在参与计算时不会使用补码(不会带上符号)。
    BYTE表示1个字节;WORD表示单字,也就是2个字节;DWORD表示双字,也就是四个字节。
    BOOT_START_ADDR表示Boot扇区在内存中的加载起始地址位0x7c00。
    接下来就能根据FAT12的规范给出FAT12引导扇区的结构体了:

    typedef struct _FAT12_HEADER FAT12_HEADER;
    typedef struct _FAT12_HEADER *PFAT12_HEADER;
    
    struct _FAT12_HEADER {
        BYTE    JmpCode[3];
        BYTE    BS_OEMName[8];
        WORD    BPB_BytesPerSec;
        BYTE    BPB_SecPerClus;
        WORD    BPB_RsvdSecCnt;
        BYTE    BPB_NumFATs;
        WORD    BPB_RootEntCnt;
        WORD    BPB_TotSec16;
        BYTE    BPB_Media;
        WORD    BPB_FATSz16;
        WORD    BPB_SecPerTrk;
        WORD    BPB_NumHeads;
        DWORD   BPB_HiddSec;
        DWORD   BPB_TotSec32;
        BYTE    BS_DrvNum;
        BYTE    BS_Reserved1;
        BYTE    BS_BootSig;
        DWORD   BS_VolID;
        BYTE    BS_VolLab[11];
        BYTE    BS_FileSysType[8];
    }__attribute__((packed)) _FAT12_HEADER;
    

    具体每一个成员的意义可以查阅文章FAT12文件系统之引导扇区结构
    其中,要格外注意的是:

    __attribute__((packed)) _FAT12_HEADER
    

    __attribute__((packed)) 告诉编译器,这个结构体是不需要对齐的(GNU GCC有效),如果不指定这个关键字,编译器在编译这个结构体时,会将其对齐,这样解析起Boot扇区就不正确了。对于这个结构体来说,会对齐6 Bytes。
    然后是目录项结构的结构体:

    typedef struct _FILE_HEADER FILE_HEADER;
    typedef struct _FILE_HEADER *PFILE_HEADER;
    
    struct _FILE_HEADER {
        BYTE    DIR_Name[11];
        BYTE    DIR_Attr;
        BYTE    Reserved[10];
        WORD    DIR_WrtTime;
        WORD    DIR_WrtDate;
        WORD    DIR_FstClus;
        DWORD   DIR_FileSize;
    }__attribute__((packed)) _FILE_HEADER;
    

    (3) PrintImage实现

    PrintImage函数负责打印被解析的_FAT12_HEADER结构体。其实现如下。

    void PrintImage(unsigned char *pImageBuffer)
    {
        puts("\nStart to print image:\n");
    
        PFAT12_HEADER pFAT12Header = (PFAT12_HEADER)pImageBuffer;
    
        // calculate start address of boot program
        WORD wBootStart = BOOT_START_ADDR + pFAT12Header->JmpCode[1] + 2;
        printf("Boot start address: 0x%04x\n",wBootStart);
    
        char buffer[20];
    
        memcpy(buffer,pFAT12Header->BS_OEMName,8);
        buffer[8] = 0;
    
        printf("BS_OEMName:         %s\n",buffer);
        printf("BPB_BytesPerSec:    %u\n",pFAT12Header->BPB_BytesPerSec);
        printf("BPB_SecPerClus:     %u\n",pFAT12Header->BPB_SecPerClus);
        printf("BPB_RsvdSecCnt:     %u\n",pFAT12Header->BPB_RsvdSecCnt);
        printf("BPB_NumFATs:        %u\n",pFAT12Header->BPB_NumFATs);
        printf("BPB_RootEntCnt:     %u\n",pFAT12Header->BPB_RootEntCnt);
        printf("BPB_TotSec16:       %u\n",pFAT12Header->BPB_TotSec16);
        printf("BPB_Media:          0x%02x\n",pFAT12Header->BPB_Media);
        printf("BPB_FATSz16:        %u\n",pFAT12Header->BPB_FATSz16);
        printf("BPB_SecPerTrk:      %u\n",pFAT12Header->BPB_SecPerTrk);
        printf("BPB_NumHeads:       %u\n",pFAT12Header->BPB_NumHeads);
        printf("BPB_HiddSec:        %u\n",pFAT12Header->BPB_HiddSec);
        printf("BPB_TotSec32:       %u\n",pFAT12Header->BPB_TotSec32);
        printf("BS_DrvNum:          %u\n",pFAT12Header->BS_DrvNum);
        printf("BS_Reserved1:       %u\n",pFAT12Header->BS_Reserved1);
        printf("BS_BootSig:         %u\n",pFAT12Header->BS_BootSig);
        printf("BS_VolID:           %u\n",pFAT12Header->BS_VolID);
    
        memcpy(buffer,pFAT12Header->BS_VolLab,11);
        buffer[11] = 0;
        printf("BS_VolLab:          %s\n",buffer);
    
        memcpy(buffer,pFAT12Header->BS_FileSysType,8);
        buffer[11] = 0;
        printf("BS_FileSysType:     %s\n",buffer);
    }
    

    其中,由于能够相信pImageBuffer的首地址开始就是_FAT12_HEADER结构体,PFAT12_HEADER pFAT12Header = (PFAT12_HEADER)pImageBuffer;直接将读到内存中的镜像文件首地址传给FAT12_HEADER结构体指针,进行强制转化,就能对各字段进行读取了。
    WORD wBootStart = BOOT_START_ADDR + pFAT12Header->JmpCode[1] + 2;将BOOT_START_ADDR(Boot扇区读到内存中的首地址)加上跳转Offset再加上2就能得到引导程序的收地了。引导扇区一开始是一个JMP Offset和一个NOP。在实模式下,JMP Offset占两个Bytes,NOP占一个Byte。其中,JMP Offset的操作码为0xEB,操作数(Offset)占一个Byte,NOP为0x90,占一个Byte。所以,这整个就是 0xEB Offset 0x90。如果想要得到跳转地址,就需要将当前地址(BOOT_START_ADDR)加上跳转偏移(Offset, 也就是JmpCode[1]),再加上2(JMP Offset的指令长度,因为Offset是针对当前指令的下一条指令地址来的)。也就是BOOT_START_ADDR + pFAT12Header->JmpCode[1] + 2
    接着,分别对每个字段进行打印。PrintImage的使命就完成了。

    (4) SeekRootDir的实现

    SeekRootDir用来遍历Root Directory,将文件名、文件属性和文件首簇号打印出来,并将其目录结构作为_FILE_HEADER结构体存储在一个数组中。由于这里很清除具体有多少个文件,为了省事就不动态申请内存存放了,而是使用了一个固定大小的数组。

    FILE_HEADER FileHeaders[30];
    
    void SeekRootDir(unsigned char *pImageBuffer)
    {
        PFAT12_HEADER pFAT12Header = (PFAT12_HEADER)pImageBuffer;
    
        puts("\nStart seek files of root dir:");
    
        // sectors number of start of root directory
        DWORD wRootDirStartSec = pFAT12Header->BPB_HiddSec + pFAT12Header->BPB_RsvdSecCnt + pFAT12Header->BPB_NumFATs * pFAT12Header->BPB_FATSz16;
    
        printf("Start sector of root directory:    %u\n", wRootDirStartSec);
    
        // bytes num of start of root directory
        DWORD dwRootDirStartBytes = wRootDirStartSec * pFAT12Header->BPB_BytesPerSec;
        printf("Start bytes of root directory:      %u\n",dwRootDirStartBytes);
    
        PFILE_HEADER pFileHeader = (PFILE_HEADER)(pImageBuffer + dwRootDirStartBytes);
    
        int fileNum = 1;
        while(*(BYTE *)pFileHeader)
        {
            // copy file header to the array
            FileHeaders[fileNum - 1] = *pFileHeader;
            
            char buffer[20];
            memcpy(buffer,pFileHeader->DIR_Name,11);
            buffer[11] = 0;
    
            printf("File no.            %d\n", fileNum);
            printf("File name:          %s\n", buffer);
            printf("File attributes:    0x%02x\n", pFileHeader->DIR_Attr);
            printf("First clus num:     %u\n\n", pFileHeader->DIR_FstClus);
    
            ++pFileHeader;
            ++fileNum;
        }
    }
    

    DWORD wRootDirStartSec = pFAT12Header->BPB_HiddSec + pFAT12Header->BPB_RsvdSecCnt + pFAT12Header->BPB_NumFATs * pFAT12Header->BPB_FATSz16;计算出了根目录的起始扇区。计算方法为:隐藏扇区数 + 保留扇区数(Boot Sector) + FAT表数量 × FAT表大小(Sectors)。也就是将根目录前面所有的扇区数加起来。
    得到起始扇区数后,将其乘上每扇区的字节数就能得到根目录的起始字节偏移了:DWORD dwRootDirStartBytes = wRootDirStartSec * pFAT12Header->BPB_BytesPerSec;
    接着,讲pImageBuffer地址加上计算出的根目录字节偏移就能得到根目录第一个文件的_FILE_HEADER结构体:PFILE_HEADER pFileHeader = (PFILE_HEADER)(pImageBuffer + dwRootDirStartBytes);。之后就能够对这个结构体进行操作,然后使用++pFileHeader;来遍历根目录。
    根据pFileHeader的第一个Byte是否为0x00来判断是否到达最后一个文件(这个判断是不对的,中间有文件可能被删除,而且可能隔着0x00后面还有有效文件,所以这里需要后续再改。但是仅仅针对这一个构造的Image是有效的,就暂时用着了)。最终得到的文件都放入FileHeaders中。

    (5) ReadFile的实现

    ReadFile函数能够根据传入的_FILE_HEADER结构体从传入的ImageBuffer中读出数据,并写到传入的outBuffer中。实现如下:

    DWORD ReadFile(unsigned char *pImageBuffer, PFILE_HEADER pFileHeader, unsigned char *outBuffer)
    {
        PFAT12_HEADER pFAT12Header = (PFAT12_HEADER)pImageBuffer;
    
        char nameBuffer[20];
        memcpy(nameBuffer, pFileHeader->DIR_Name, 11);
        nameBuffer[11] = 0;
    
        printf("The FAT chain of file %s:\n", nameBuffer);
    
        // calculate the pointer of FAT Table
        BYTE *pbStartOfFATTab = pImageBuffer + (pFAT12Header->BPB_HiddSec + pFAT12Header->BPB_RsvdSecCnt) * pFAT12Header->BPB_BytesPerSec;
    
        WORD next = pFileHeader->DIR_FstClus;
        
        DWORD readBytes = 0;
        do
        {
            printf(", 0x%03x", next);
    
            // get the LSB of clus num
            DWORD dwCurLSB = GetLSB(next, pFAT12Header);
    
            // read data
            readBytes += ReadData(pImageBuffer, dwCurLSB, outBuffer + readBytes);
    
            // get next clus num according to current clus num
            next = GetFATNext(pbStartOfFATTab, next);
    
        }while(next <= 0xfef);
    
        puts("");
    
        return readBytes;
    }
    

    首先要得到FAT表的指针:BYTE *pbStartOfFATTab = pImageBuffer + (pFAT12Header->BPB_HiddSec + pFAT12Header->BPB_RsvdSecCnt) * pFAT12Header->BPB_BytesPerSec;,也就是用指向ImageBuffer的指针加上FAT表的偏移,就能得到这个指针。FAT表的偏移用FAT表之前的所有GetFATNext来计算下一个簇的簇号。GetFatNext的实现会在后面提到。
    最后,将读到的字节数返回。

    (6) GetLSB的实现

    GetLSB用来计算出给出的FAT表项对应在数据区的扇区号。下面是它的实现。

    DWORD GetLSB(DWORD ClusOfTable, PFAT12_HEADER pFAT12Header)
    {
        DWORD dwDataStartClus =  pFAT12Header->BPB_HiddSec + pFAT12Header->BPB_RsvdSecCnt + pFAT12Header->BPB_NumFATs * pFAT12Header->BPB_FATSz16 + \
                                pFAT12Header->BPB_RootEntCnt * 32 / pFAT12Header->BPB_BytesPerSec;
    
        return dwDataStartClus + (ClusOfTable - 2) * pFAT12Header->BPB_SecPerClus;
    }
    

    也比较简单。就是将数据区前面所有的扇区号都加起来,得到数据区的起始扇区,然后将给出的FAT项减2,再乘上每簇的扇区数,加上数据区的起始扇区号,最后就得到了当前FAT项的LSB。

    (7) GetFATNext的实现

    GetFATNext根据当前给出的FAT表项,得到它在FAT表里的下一项。其实现如下。

    WORD GetFATNext(BYTE *FATTable, WORD CurOffset)
    {
        WORD tabOff = CurOffset * 1.5;
    
        WORD nextOff = *(WORD *)(FATTable + tabOff);
    
        nextOff = CurOffset % 2 == 0 ?  nextOff & 0x0fff : nextOff >> 4;
    
        return nextOff;
    }
    

    由于在FAT12文件系统中,FAT表中的每一项是1.5 Bytes(6 Bits),而又没有任何一种数据类型能够表示1.5 Bytes,所以需要用一个Word,也就是2 Bytes来存储它。但是每一项在FAT表中又是紧紧相连的,所以在读的时候需要用一点小技巧。
    先用传来的FAT表项×1.5得到实际需要读取的值在FAT表中的Bytes偏移,然后用一个WORD来存储它。
    接着,判断这个偏移是奇数还是偶数。如果是奇数,则将前4位清0(与上0x0fff),如果是偶数,则将其右移4位,最终得到下一项的FAT表偏移。
    至于这个具体是怎么来的,可以通过观察一个具体的FAT12的FAT表得出。例如下面的FAT表:


    示例FAT表

    由于第0项和第1项都是保留的,所以跨过前两项,直接看第2项。
    第2项的起始Bytes是2×1.5=3,在第3 Bytes处读出一个Word,根据小端序,读出来是0x4003。然后,由于当前偏移3是奇数,所以将其前4位清零(&0x0FFF),得到0x0003,取后1.5 bytes,得到0x003,也就是它的下一项是0x003。
    第3项的起始Bytes是3×1.5=4,在第4 Bytes处读出一个Word,得到0x0040。由于4是偶数,将其右移4位,得到0x0004。取后1.5 Bytes,得到0x004,这就是第3项的下一项。
    以此类推。由于这里写文件的可用Cluster都是连续的,所以这里的表项也是连续的。其实FAT表像是一个单项链表,文件存储的地址是可以不连续的。

    (8) ReadData的实现

    ReadData的实现比较简单,计算出传入的LSB在镜像Buffer中的位置(Bytes),然后写到传入的outBuffer中。

    DWORD ReadData(unsigned char *pImageBuffer, DWORD LSB, unsigned char *outBuffer)
    {
        PFAT12_HEADER pFAT12Header = (PFAT12_HEADER)pImageBuffer;
    
        DWORD dwReadPosBytes = LSB * pFAT12Header->BPB_BytesPerSec;
    
        memcpy(outBuffer, pImageBuffer + dwReadPosBytes, pFAT12Header->BPB_SecPerClus * pFAT12Header->BPB_BytesPerSec);
    
        return pFAT12Header->BPB_SecPerClus * pFAT12Header->BPB_BytesPerSec;
    }
    

    首先,使用LSB×每扇区的字节数,得到要读的扇区的字节起始值DWORD dwReadPosBytes = LSB * pFAT12Header->BPB_BytesPerSec;,然后用memcpy将ImageBuffer的ReadPosBytes偏移处的数据写到outBuffer中,写入长度是每簇的扇区数与每扇区的字节数的积,也就是每簇的字节数。

    至此,整个实现就完成了。

    0x05 测试

    将程序编译后使用上面生成的镜像进行测试。
    运行后,首先输出了镜像的引导扇区结构:


    引导扇区结构

    与镜像实际值对照,发现是正确的。说明程序能够正确解析改镜像的引导扇区结构。
    然后,程序开始遍历镜像的根目录,并将根目录输出。如下图。


    根目录遍历
    能看到,FAT12文件系统中,文件名一共11个字节的长度,后三个字节用来存储扩展名,剩余的字节用来存储文件名,不够的字节用0x20(空格)来填充。
    接下来,是对文件README TXT的读取。
    读取README TXT

    先输出了文件在FAT表中的所有项,然后将其读到了Buffer中。下面的内容是在main函数中对读取的buffer的打印。拉倒末尾,发现打印完整,说明整个文件都读出来了。

    0x06 总结

    这一章使用C实现了一个从FAT12文件系统的镜像文件中读出引导扇区结构、根目录及特定文件的程序。接下来要做的就是将其在Boot Sector中用汇编实现出来,并加载Loader了。

    附件是这章用到的WinImage生成的镜像文件。

    附件:
    sample_image.ima

    相关文章

      网友评论

        本文标题:实现一个简单的64位操作系统 (0x03)熟悉FAT12文件系统

        本文链接:https://www.haomeiwen.com/subject/wryziftx.html