磁盘的GPT分区表(解决xFsRedir目录重定向程序中一个扩展虚拟磁盘子功能的兼容问题)

                                          by fanxiushu  2020-05-18 转载或引用请注明原始作者。
平时也不大关注磁盘的分区参数等信息,只是最近在更新xFsRedir程序的时候,
想要使用底层的storport框架的虚拟磁盘驱动代替基于直接IO的简单模型的虚拟磁盘驱动,
这样做的目的是为了让虚拟出来的磁盘更接近操作系统需求,更能欺骗windows把它当成真正的磁盘。
xFsRedir的使用请去GITHUB下载:
https://github.com/fanxiushu/xFsRedir
使用storport框架的虚拟磁盘驱动,会被windows当成一块真正的磁盘,会在设备管理器和磁盘管理中找到这样的一个“设备”,
但是它需要的镜像文件源,则相当于一块真正的物理磁盘上的数据存储布局。
也就是里边包含分区表结构信息,可能还包括引导数据,分区的实际数据内容等。
xFsRedir的xfs_redir.sys驱动集成了一种叫直接IO的简单的磁盘模型,其实就是 filedisk 这种框架。
这些内容可以去CSDN上查阅我之前的文章,都有详细介绍。
这种驱动的好处就是简单,容易理解,也容易开发,安装起来也很方便。
缺点也很明显,不会被windows当成真正的磁盘,少数奇奇怪怪的程序不能识别。
还有就是镜像文件源,数据存储的布局只是单个分区的数据内容。

为了让这两种虚拟磁盘驱动的镜像文件兼容,
主要是为了让基于storport的虚拟磁盘能正确读写基于filedisk模式虚拟磁盘驱动生成的只有单一分区的镜像文件。
我们就不得不想办法解决这个问题。
能想到最好的办法,就是给这个只有单一分区数据内容的镜像文件,“添加“额外的分区表信息,
这个分区表只包含一个分区信息,也就是这个单一分区的镜像文件。
这种“添加”不是要我们修改这个只存储单一分区数据的镜像文件,
而是在storport驱动发起读写扇区请求的时候,根据读写情况,给他额外回复一些扇区,这些扇区就是分区表等磁盘的初始化信息。

回复额外扇区主要包括两个做法:
一,驱动不做修改,在请求镜像文件的服务端程序上添加这种额外扇区回复。
       这种做法一般针对自定义的私有协议的服务端程序比较好处理,因为都是自己定义的,
       而xFsRedir包含了大量的公共通讯协议,因此这种办法在xFsRedir来说不可取。
二,在storport驱动中做修改,添加额外扇区回复。这是xFsRedir采用的办法。

其实在windows系统中,我们把只要把小于2T的单一分区的镜像文件源输入给storport驱动,也能正确识别并且自动弹出对应磁盘盘符。
这也许看起来比较神奇,因为这块只有分区,而没有分区表的”磁盘“,居然能被windows正确识别出来了。
但是当我们把一个分区超过 2T 大小的单一分区镜像输入给storport驱动,情况就很混乱了。
也许会被当成未被初始化的磁盘,或者这块磁盘出现很多未格式化的分区。简单的说,就是不能正确识别。
因此我们可以这样总结:
对于小于2T的分区的单一分区镜像,windows会把他当成MBR分区的磁盘,也就是会“自做主张”的添加MBR分区表头等信息。
windows这样做的目的主要是为了兼容的考虑,但是针对大于2T的分区,windows也就无能为力了只能乱认。

为了兼容,我们都得给storport虚拟磁盘驱动 ”添加“ 额外分区表等初始化扇区,但是很显然如果是添加MBR分区表意义不大。
因此得添加GPT分区表头。win7以上系统都支持GPT分区的磁盘作为数据盘,而storport驱动框架其实也只支持win7以上的系统。
而在驱动中添加GPT分区表头,我们就必须熟悉GPT分区表的格式。
而这就是下面具体阐述的内容。
(这里不再阐述如何开发storport驱动,如何在驱动中增加这种额外的请求,有兴趣可去查阅我CSDN上虚拟磁盘驱动开发相关文章)。

现代的磁盘的寻址都是使用线性的逻辑存储块地址方式寻址(Logical Block  Address)简称 LBA,
而早期磁盘采用复杂的柱面,磁头,扇区等寻址。
一个逻辑块的大小也不再是固定的 512字节(虽然通常都是512字节)。逻辑块,我们为了习惯,也可以简单称呼为扇区。
比如我正在使用的电脑,逻辑块的大小是 4096字节(也就是 4K-Sector), 还有些是 2048 字节。

MBR分区的磁盘第一个Block(也就是0号扇区, 可以简单写作 LBA0 ) 存储的是引导程序和分区信息,
引导程序占用446字节,接下来64个字节是硬盘分区表,最后两个字节固定为 55AA,
每个分区信息占用 16 个字节,64个字节也就是只能分4个分区,这就是MBR分区的限制,
同时 在16个字节中,只使用了4个字节表示扇区的个数,每个扇区固定512字节的话,
这样每个分区 不会超过 4G*512字节 = 2T 大小,这也是MBR分区最大的限制。

GPT分区的出现主要是为了解决MBR分区的各种限制,适应现在的越来越大的磁盘容量。
比如我基本上都是把买来的4T,8T机械硬盘分成一个分区,这种做法除了使用GPT分区没其他选择。

GPT分区的磁盘 LBA0 (也就是磁盘的第一个Block, 0号扇区),存储MBR,这个叫保护性MBR,
其实就是为了兼容老旧只认MBR分区的软件,防止这些老旧软件在认不出GPT分区之后乱操作。
这个保护性MBR前446字节全写0,接下来64个字节只需填写前16字节,其余全0,
这16字节设置一个不识别的分区属性,并且扇区结束地址全写0xFF,当然最后两个字节必须是 55AA。
GPT分区的磁盘 LBA1 (磁盘的第2个block,1号扇区),存储的是GPT头,GPT头虽然只需要占用92个字节,
但是整个 LBA1 都给了 GPT头,除了前面92字节,其余填 0。
接下来从 LBA2 开始存储分区表结构信息,分区表存储不再使用扇区为单位,而是采用字节为单位,
也就是不再是一个扇区存储一个或几个分区表结构,
而是所有这些扇区组成一块大的地址,分区表结构按次序一个挨着一个的存储,下面会举例说明。
分区表结构结构存储之后,就是分区数据的实际内容了,
至于每个分区起始LBA地址和结束LBA地址,属性等各种信息,自然是存储在分区表结构里。

最后在磁盘的尾部,会存储GPT分区的备份,一般是把GPT头存储到磁盘的最后一个扇区中,然后倒数的其他扇区存储分区表结构。

GPT头的描述,可以简化成如下的C语言数据结构来描述:
存储方式按照小尾序的方式存储。(uint32_t 4字节大小无符号整数, uuint64_t 8字节大小的无符号整数)
struct gpt_header
{
     char         magic[8];  //GPT头的标志,固定为 “EFI PART”
     uint32_t   version;   // 版本,固定为 0x100 00
     uint32_t   header_size;  // GPT大小,固定为0x5C,也就是92
     uint32_t   header_crc32; //GPT头的CRC32校验和,计算的时候先把此值设置为0,
     uint32_t   reserved; //保留,必须为0
     uint64_t   header_LBA; ///GPT头的LBA地址,通常是 LBA1,
     uint64_t   header_back_LBA; ///GPT头备份地址,通常是磁盘的最后一个扇区。
     uint64_t   part_area_start_LBA; //分区区域的起始LBA,通常是分区表结束之后的第一个扇区。
     uint64_t   part_area_end_LBA; //分区区域的结束LBA,通常是分区表备份之前的一个扇区。
     GUID      disk_id;// 磁盘的GUID值,16个字节。
     uint64_t  part_table_start_LBA;  ///分区表的起始LBA
     uint32_t  part_table_count;  //分区表的个数,在windows系统中,通常固定为 128个GPT分区表。
     uint32_t  part_table_size;    //每个分区表大小,固定为128字节。
     uint32_t  part_table_crc32;  //所有分区表的CRC32校验和。
};
一共92个字节描述GPT头。

下面是每个分区表结构的C语言描述:
struct gpt_table
{
      GUID      part_type;    //GUID表示的这个分区类型,比如 C12A7328-F81F-11D2-BA4B-00A0C93EC93B 表示 EFI 系统分区等
      GUID      part_id;      //这个分区的唯一标志
      uint64_t   start_LBA;   //这个分区的起始LBA
      uint64_t   end_LBA;    //这个分区的结束LBA
      uint64_t   attr;              //这个分区的属性
      WCHAR  name[36];    //这个分区的名字。
};
一共128个字节描述GPT分区表结构。

正如上面所述,LBA0存储保护性MBR,LBA1存储 gpt_header结构。然后从LBA2开始存储 gpt_table数组也就是分区表数组。
我们以windows系统为例,windows系统基本都是固定128个分区表,也就是 128个gpt_table结构。
每个分区表占用128个字节,因此存储所有分区表占用 128*128 = 16384 字节,也就是 16KB,
上面说过分区表是挨着存储的,我们做个简单计算。

如果磁盘扇区大小是512字节,则需要 16KB/512 = 32个扇区存储所有分区表,再加一个保护性MBR,一个GPT头,
   因此磁盘前面需要 1 + 1 + 32 = 34个扇区。同时GPT还需要备份扇区,因此在磁盘最后需要 1 + 32 = 33个扇区来备份GPT头和GPT分区表。

如果磁盘扇区是4K-Sector(也就是4096字节),则需要 16KB/4K = 4个扇区存储分区表,加一个保护性MBR,一个GPT头,
   因此磁盘前面需要 1 + 1 + 4 = 6个扇区, 同时GPT需要备份扇区,因此磁盘最后需要 1 + 4 = 5个扇区来备份。

以下伪代码演示如何模拟出只有一个分区的GPT分区表:
这里先假设几个参数:
block_size代表扇区大小(比如 512,4096等)
disk_size 代表整个磁盘的大小。
LBA0 代表磁盘的起始地址,(也就是0号扇区)

     LONG part_sector_count = ( 128 * 128 + block_size - 1) / block_size; ///计算所有分区表占用多少个扇区。
     LONG pre_len = ( 1 + 1 + part_sector_count ) * block_size; // 1 PMBR + 1 GPT header + part tables,磁盘前面占用扇区数
     LONG suf_len = ( 1 + part_sector_count ) *block_size; /// backup, 1 GPT header + part tables. 磁盘后面占用扇区数

     PCHAR  LBA1 =  LBA0 + block_size; //LBA1地址,
     PCHAR  LBA2 =  LBA1 + block_size; //LBA2地址

     uint64_t end_sector = disk_size.QuadPart / block_size - 1; // 整个磁盘的结束扇区
     ////保护性MBR
     memset(LBA0, 0,  block_size); ///全部设置0,
     LBA0[0x1BE] = LBA0[0x1BF] = 0x00;//填写第一个分区表,
     LBA0[0x1C0] = 2; LBA0[0x1C1] = 0; LBA0[0x1C2] = 0xEE; LBA0[0x1C3] = LBA0[0x1C4] = LBA0[0x1C5] = 0xFF;
     LBA0[0x1C6] = 0x01; LBA0[0x1CA] = LBA0[0x1CB] = LBA0[0x1CC] = LBA0[0x1CD] = 0xFF;
     LBA0[510] = 0x55; LBA0[511] = 0xAA; // MBR 结束

    //填写GPT头
    gpt_header* hdr =(gpt_header*)LBA1;
    memset(LBA1, 0, block_size);
    strcpy(hdr->magic, "EFI PART"); // 8bytes
    hdr->version = 0x010000; ///  version 1.0
    hdr->header_size = 0x5C; // GPT header size 92 字节
    hdr->header_crc32 = 0; //gpt header CRC
    hdr->reserved = 0; // reserved 4 bytes
    hdr->header_LBA = 0x01; // LBA1 ,GPT头起始位置
    hdr->header_back_LBA = end_sector; //GPT header 备份扇区,
    hdr->part_area_start_LBA = 1 + 1 + part_sector_count ; ///分区起始,通常从 34 sector
   hdr->part_area_end_LBA = end_sector - (1 + part_sector_count );
   hdr->disk_id = diskId;
    hdr->part_table_start_LBA = 0x02; /// 分区表起始扇区
   hdr->part_table_count = 0x80; ///分区表总项数 128
   hdr->part_table_size = 0x80; ///每个分区表字节数 128
    hdr->part_table_crc32 = 0;   //分区表CRC

    //填写GPT分区表,这里只需要第一个分区表,其他全部设置为0
    memset(LBA2, 0, part_sector_count*block_size); 
    gpt_table* tbl = (gpt_table*)LBA2;
   tbl->part_type = PARTITION_BASIC_DATA_GUID; /// basic partition
    tbl->part_id = partId;
    tbl->start_LBA = 1 + 1 + part_sector_count ; //分区开始
    tbl->end_LBA = end_sector - (1 + part_sector_count ); // end partition sector
   tbl->attr = 0; /// attribute
    wcscpy(tbl->name, L"xFsRedirVDisk"); /// name
    
    //计算crc32
    hdr->header_crc32=crc32(LBA1, block_size);
    hdr->part_table_crc32=crc32(LBA2, 128*128);  ///
   
    ///计算备份 LBA地址
    bak_LBA1 = LBA0 + end_sector;
    bak_part_LBA = LBA0 + end_sector - part_sector_count;

    memcpy(bak_part_LBA, LBA2, part_sector_count*block_size); //直接复制分区表
    memcpy(bak_LBA1, LBA1, block_size); ////直接复制GPT头,但是有些参数需要修改
    ///修改某些参数
    gpt_header* bak_hdr = (gpt_header*)bak_LBA1;
    bak_hdr->header_LBA = end_sector; //这个与LBA1中设置相反
    bak_hdr->header_back_LBA = 1; ///
    bak_hdr->part_table_start_LBA = bak_part_LBA; //
    bak_hdr->header_crc32 = 0;  bak_hdr->header_crc32 = crc32(bak_LBA1, block_size); ///重新计算CRC32

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章