程序员

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

2018-08-27  本文已影响11人  KernelThread

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

上一篇下一篇

猜你喜欢

热点阅读