storage layout系列之VHD结构详解

2013年1月6日第二次修正
【概念】
    VHD(Microsoft Virtual Hard Disk)是一种虚拟磁盘的实现方式,即通过文件来模拟物理磁盘的方式来存储数据。如同正常的物理磁盘一样,可以分区,格式化等。
    VHD最早由Connectix公司定义,之后Connectix公司被Microsoft 收购。
    VHD格式用于Microoft Virtual PC 、Microsoft Windows Server 2008 R2和Microsoft Windows 7,包括hypervisor为基础的虚拟化技术- Hyper-V。
    VHD也用于xen虚拟化,目前也是xen上虚拟磁盘的默认标准。

【结构概要】
    VHD结构有2种实现方式:固定方式和动态方式。
    固定方式就是用真实大小的文件模拟同样大小的一个虚拟磁盘。当然,额外的,一定要加上一些对本文件的总的描述(在文件尾部),否则系统无法知道就是固定磁盘还是动态磁盘。
    动态磁盘是按稀疏方式,用文件做容器,来表述一个可能大得多的虚拟磁盘,如果某个区域没写数据,就可能不在文件中真正分配空间。另外,差异磁盘(快照)也基于动态磁盘的方式存储。本文用"$VHD文件"的说法表示VHD文件本身,而用"$虚拟磁盘"的说法表示 VHD文件描述的虚拟磁盘寻址空间。
    下面数据结构表示,如非特别提示,均为大尾(bigendian)方式。

【固定方式的VHD结构】
    结构如下图:clipboard[22]

    文件一开始就是和物理磁盘相同的raw image,即$虚拟磁盘的X扇区就是$VHD文件的X扇区,一对一映射。与物理磁盘不同的是,尾部增加了一个HD footer的结构。HD footer的结构如下图:

clipboard[23]

  磁盘上的HEX数据表现如下,用WINHEX截取

clipboard[24]

   xen中的源代码是这样表述的:

 

  
  
  
  
  1. struct hd_ftr { 
  2.   char   cookie[8];       /* Identifies original creator of the disk      */ 
  3.   u32    features;        /* Feature Support -- see below                 */ 
  4.   u32    ff_version;      /* (major,minor) version of disk file           */ 
  5.   u64    data_offset;     /* Abs. offset from SOF to next structure       */ 
  6.   u32    timestamp;       /* Creation time.  secs since 1/1/2000GMT       */ 
  7.   char   crtr_app[4];     /* Creator application                          */ 
  8.   u32    crtr_ver;        /* Creator version (major,minor)                */ 
  9.   u32    crtr_os;         /* Creator host OS                              */ 
  10.   u64    orig_size;       /* Size at creation (bytes)                     */ 
  11.   u64    curr_size;       /* Current size of disk (bytes)                 */ 
  12.   u32    geometry;        /* Disk geometry                                */ 
  13.   u32    type;            /* Disk type                                    */ 
  14.   u32    checksum;        /* 1's comp sum of this struct.                 */ 
  15.   vhd_uuid_t uuid;        /* Unique disk ID, used for naming parents      */ 
  16.   char   saved;           /* one-bit -- is this disk/VM in a saved state? */ 
  17.   char   hidden;          /* tapdisk-specific field: is this vdi hidden?  */ 
  18.   char   reserved[426];   /* padding                                      */ 
  19. }; 

结构解释:

cookie:
    识别标志,为"conectix",用于判断VHD是否有效

features:
    取值如下。

  
  
  
  
  1. #define HD_NO_FEATURES     0x00000000 
  2. #define HD_TEMPORARY       0x00000001 /* disk can be deleted on shutdown */ 
  3. #define HD_RESERVED        0x00000002 /* NOTE: must always be set        */ 

ff_version:
    VHD版本,用处不大,用于结构判断,似乎更多的会用到crtr_ver。

data_offset:
    描述中指下一个结构的起始绝对字节位置,如果是动态磁盘,这表明了dd_hdr(稍后会提到)的物理字节    位置。如果是固定磁盘,似乎总是0xFFFFFFFF。

timestamp:
    VHD的创建时间,指2000年1月1日00:00:00起始的秒值。和HFS对时间的描述方式一致,也就是说此处数值加上0xB492F400 (即2000/01/01 00:00:00),即是标准的HFS时间方法对本值的解释。

crtr_app:
    见代码注释

crtr_ver:
    创建版本。根据版本号可实施对应的方法。似乎目前只有当创建版本号为0x00000001时,对于bitmap的操作会有不同(具体细节请参考xen源码)。

crtr_os:
    见代码注释

orig_size :
    创建时$虚拟磁盘大小,再强调一下,这个大小指虚拟出来的磁盘的可用寻址空间。如果是固定格式的VHD,这个大小等于$VHD文件的大小减去1扇区(尾部 hd_ftr)。

curr_size:
    或许是用于vhd在线扩容后的最后大小表述,没仔细研究过。同样指$虚拟磁盘的大小,即虚拟出来的磁盘的可用寻址空间,如果没有扩容,和orig_size相同。

geometry:
    VHD的C/H/S结构参数,兼容一些老的应用(其实估计不会用到了)。如下表示

  
  
  
  
  1. #define GEOM_GET_CYLS(_g)  (((_g) >> 16) & 0xffff) 
  2. #define GEOM_GET_HEADS(_g) (((_g) >> 8)  & 0xff) 
  3. #define GEOM_GET_SPT(_g)   ((_g) & 0xff) 

type:
    非常重要的,表示这个VHD的类型。如下表示

  
  
  
  
  1. #define HD_TYPE_NONE       0 
  2. #define HD_TYPE_FIXED      2  /* fixed-allocation disk */ 
  3. #define HD_TYPE_DYNAMIC    3  /* dynamic disk */ 
  4. #define HD_TYPE_DIFF       4  /* differencing disk */ 

    如果是差异磁盘,在dd_hdr中会描述父设备的设备号,但结构与动态磁盘相同

checksum:
    整个扇区所有字节(当然一开始不包括checksum本身)相加得到32位数,再按位取反。

uuid:
    用于VHD识别号,如果是有差异磁盘,这个ID非常重要,决定了VHD间的主从关系。

saved:
    动态中使用,见代码注释

hidden:
    见代码注释

reserved:
    保留空间,总是为0

上述结构中最重要的变量为VHD类型、大小、checksum。

 

【动态方式的VHD结构】

clipboard[25]

    动态VHD和固定VHD相同的是,尾部也是重要的hd_ftr格式,不同的是hd_ftr会表明本VHD是动态方式的。
    上图中,位置描述部分黄色区域为数据区,其余区域,均为元数据区(结构管理区)。
    0扇区的hd_ftr mirror是对尾部hd_ftr的备份。hd_ftr在固定格式的VHD中已经详细解释,hd_ftr中data_offset会描述dd_hdr的位置,不过,这个位置目前总是1扇区。
    dd_hdr结构用于表述整个动态vhd的概况,分配块大小等的变量。
    BAT指Block allocation table,非常重要的,表示$虚拟磁盘地址到$VHD文件地址的块映射表。
    tdbatmap指BAT的分配位图,其实就是所有分配块的是否用满的位描述表。在某些版本的vhd中,可能不存在此结构,xen源码中是这样判断是否存在batmap的:

  
  
  
  
  1. int vhd_has_batmap(vhd_context_t *ctx) 
  2.     if (!vhd_type_dynamic(ctx)) 
  3.         return 0; 
  4.     if (!vhd_creator_tapdisk(ctx)) 
  5.         return 0; 
  6.     if (ctx->footer.crtr_ver <= VHD_VERSION(0, 1)) 
  7.         return 0; 
  8.     if (ctx->footer.crtr_ver >= VHD_VERSION(1, 2)) 
  9.         return 1; 
  10.     /* 
  11.      * VHDs of version 1.1 probably have a batmap, but may not  
  12.      * if they were updated from version 0.1 via vhd-update. 
  13.      */ 
  14.     if (!vhd_validate_batmap_header(&ctx->batmap)) 
  15.         return 1; 
  16.     if (vhd_read_batmap_header(ctx, &ctx->batmap)) 
  17.         return 0; 
  18.     return (!vhd_validate_batmap_header(&ctx->batmap)); 
  19. }


    每个数据块都由块bitmap和块本身组成,块bitmap用于描述块中每个扇区是否占用的位图表,块本身就是数据区。

我们依次详细分析上述所有结构:

1、hd_ftr:
    固定格式vhd中已详细分析,在动态磁盘中,不同的是data_offset总为0x200,类型为3或4。

2、dd_hdr:
    结构如下图:

clipboard[26]

    WINHEX中的磁盘表现如下:

clipboard[27]

    xen中的源代码是这样表述的:

  
  
  
  
  1. struct dd_hdr { 
  2.   char   cookie[8];       /* Should contain "cxsparse"                    */ 
  3.   u64    data_offset;     /* Byte offset of next record. (Unused) 0xffs   */ 
  4.   u64    table_offset;    /* Absolute offset to the BAT.                  */ 
  5.   u32    hdr_ver;         /* Version of the dd_hdr (major,minor)          */ 
  6.   u32    max_bat_size;    /* Maximum number of entries in the BAT         */ 
  7.   u32    block_size;      /* Block size in bytes. Must be power of 2.     */ 
  8.   u32    checksum;        /* Header checksum.  1's comp of all fields.    */ 
  9.   vhd_uuid_t prt_uuid;    /* ID of the parent disk.                       */ 
  10.   u32    prt_ts;          /* Modification time of the parent disk         */ 
  11.   u32    res1;            /* Reserved.                                    */ 
  12.   char   prt_name[512];   /* Parent unicode name.                         */ 
  13.   struct prt_loc loc[8];  /* Parent locator entries.                      */ 
  14.   char   res2[256];       /* Reserved.                                    */ 
  15. }; 

结构解释:
cookie:
    识别标识,为"cxsparse",用于是否dd_hdr的校验。

data_offset:
    未使用,总设置为0xFFFFFFFF。

table_offset:
    很重要,BAT结构在$VHD文件中的绝对字节位置。几乎总是0x600

hdr_ver:
    dd_hdr的版本

max_bat_size:
    BAT条目的最大数量,实际上每个bat条目,就相当于一个块。

block_size:
    块大小,几乎总是2MB。

checksum:
    同hd_ftr中 checksum的计算方式相同。计算范围为从dd_hdr开始的1024字节。

ptr_uuid:
    差异磁盘中非常重要,表示其父vhd的uuid。这样才可以实现快照穿透。

prt_ts:
    父磁盘的修改时间,时间表示方法参考hd_ftr中timestamp的表示方式。

res1:
    保留

ptr_name:
    父磁盘的unicode名称。可以更快地找到父磁盘,但找到后,还需通过uuid校验。

loc:
    用来记录在不同平台上的父磁盘的名称,并不很重要。可见dd_hdr结构图解释,本结构的每个条目会指向一个存储文件名称的$vhd文件的绝对字节位置。

res1:
    保留。

上述结构最重要的变量为:块大小,BAT位置,BAT数量,父磁盘的uuid(对于差异磁盘)

3、BAT:
    结构如下图:
clipboard[28]

    WINHEX中的磁盘表现如下:clipboard[29]

结构解释:
    BAT表中,每4个字节表示一个bat entry,从BAT的0字节开始,以4字节为单位,第x个条目(条目从0开始编号),表示$虚拟磁盘中第x块在$vhd中的扇区位置。如果第x个条目的值为0xFFFFFFFF,表示$虚拟磁盘的第x块为稀疏,返回一整块0。
    假定块大小为2MB,对应上面winhex的磁盘表现图,从0x600位置起(按左上角offset定位),前4行全部为0xFFFFFFFF,表明整个$虚拟磁盘的前16个2MB块是全0,并未在$vhd文件中分配空间。位于0x640处的第16个bat entry(从0开始编号的序号)的值为0x00D6CC27,表示$虚拟磁盘的第16块(块大小为2M)的数据流存储于$vhd文件的第0x00D6CC27扇区。

4、tdbatmap
    bat entry的bitmap,也可理解为块的分配位图。每一位表示一个block,如果位为1,表示block已经用满。如果为0,表示未使用,或未用满。
    tdbatmap由头结构和batmap内容部分组成。
    头结构如下图:clipboard[30]

      在WINHEX中的磁盘表现(连同其后的bitmap区域)如下:clipboard[31]

xen中的源代码是这样表述头结构的:

  
  
  
  
  1. struct dd_batmap_hdr { 
  2.   char   cookie[8];       /* should contain "tdbatmap"                    */ 
  3.   u64    batmap_offset;   /* byte offset to batmap                        */ 
  4.   u32    batmap_size;     /* batmap size in sectors                       */ 
  5.   u32    batmap_version;  /* version of batmap                            */ 
  6.   u32    checksum;        /* batmap checksum -- 1's complement of batmap  */ 
  7. }; 

头结构说明:
cookie:
    识别标识,为" tdbatmap ",用于是否dd_batmap_hdr的校验。

batmap_offset:
    batmap的起始物理位置,如上面winhex磁盘图中表示的起始位置为0x400800。

batmap_size:
    batmap的大小,以扇区为单位 。其实等于bat entry数量除以8,再对齐扇区大小的扇区数。

batmap_version:
    batmap结构的版本号。

checksum:
    参考hd_ftr中checksum的计算方法。计算范围为包括batmap头和内容区的整个batmap区域

batmap内容区结构说明:
    如上面winhex磁盘图中偏移为0x400800起始的数据,每一位表示一个块的分配情况,开始的0x004000....表示整个$虚拟磁盘的第9块(块从0开始编号)是用满了的。而第0、1、2、3等块是未用满或未使用的块。

5、数据块区:
    如果块大小为2M,其实每个bat entry所指向的空间大小为512byte+2MB。最前面的512字节是本块内的扇区位图,如果位为1,表示代指的扇区已使用,如果为0,表示代指的扇区为全0,更多的意义用于差异磁盘。举个例子,如果bitmap的开始2个字节为0x80和0x12,表示第0、11、14扇区已分配。
    紧跟在块内512字节bitmap后的就是块数据本身,可按bitmap的表述,直接映射到$虚拟磁盘中。

 

VHD动态磁盘格式总结:
    对一个动态vhd磁盘的寻址过程大致为:
    1、通过读取hd_ftr结构,确定是否动态磁盘,以及dd_hdr的位置。
    2、读取dd_hdr,确定块大小,bat的位置,bat的数量,以及是否差异磁盘(差异磁盘的处理方式后面讲到)
    3、定位bat区域,可随机确定任何一个 block的位置,如果bat的值为0xFFFFFFFF,则返回全0。
    4、确定block位置后,先读取1扇区的bitmap区域,本block的某个扇区是否已使用,可以通过此bitmap确定。

【差异磁盘的读取方式】
    差异磁盘是建立在一个固定或动态磁盘上的快照。其本意是差异磁盘中仅存储自创建备份点以来的所有改动。本质上,差异磁盘就是个动态磁盘。
    如何在数据层面合并差异磁盘和其父磁盘呢?
    如果要读取某个数据块x,系统会首先读取差异磁盘x块的bat entry。如果其值为 0xFFFFFFFF,表明差异磁盘中未记录数据,接下来就应该读取本差异磁盘的父磁盘的第x块;如果bat entry其值不为0xFFFFFFFF,则可以通过batmap中x块是否用满(或分析块数据区中的bitmap),来确定是否需要穿透进父磁盘进行补差,如果已用满,则不需再处理父磁盘;如果未用满,再看本块数据区中bitmap哪些位为0,为0的穿透进父磁盘进行补差,为1则直接读取。

【参考资料】
1、http://en.wikipedia.org/wiki/VHD_(file_format)
2、http://xen.org/

[作者及后记]
    作者:张宇,北亚数据恢复中心创始人
    本文也做为目前北亚招聘数据恢复工程师和C++开发工程师的背景材料。
    本文仅首发于51CTO,如需转载,请保留完整信息。
 

本文出自 “张宇(数据恢复)” 博客,转载请与作者联系!

你可能感兴趣的:(layout,xen,VHD)