标题：VHD结构
概念
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结构
结构如下图：
文件一开始就是和物理磁盘相同的raw image,即$虚拟磁盘的X扇区就是$VHD文件的X扇区，一对一映射。与物理磁盘不同的是，尾部增加了一个HD footer的结构。HD footer的结构如下图：
磁盘上的HEX数据表现如下,用WINHEX截取
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结构
动态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. { 
3. if (!vhd_type_dynamic(ctx)) 
4. return 0; 
5. if (!vhd_creator_tapdisk(ctx)) 
6. return 0; 
7. if (ctx->footer.crtr_ver <= VHD_VERSION(0, 1)) 
8. return 0; 
9. if (ctx->footer.crtr_ver >= VHD_VERSION(1, 2)) 
10. return 1; 
11. /* 
12. * VHDs of version 1.1 probably have a batmap, but may not 
13. * if they were updated from version 0.1 via vhd-update. 
14. */ 
15. if (!vhd_validate_batmap_header(&ctx->batmap)) 
16. return 1; 
17. if (vhd_read_batmap_header(ctx, &ctx->batmap)) 
18. return 0; 
19. return (!vhd_validate_batmap_header(&ctx->batmap)); 
20.}
每个数据块都由块bitmap和块本身组成，块bitmap用于描述块中每个扇区是否占用的位图表，块本身就是数据区。
我们依次详细分析上述所有结构：
1、hd_ftr：固定格式vhd中已详细分析，在动态磁盘中，不同的是data_offset总为0x200,类型为3或4。
2、dd_hdr：结构如下图：
WINHEX中的磁盘表现如下：
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：结构如下图：
WINHEX中的磁盘表现如下：
结构解释：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、tdbatmapbat entry的bitmap，也可理解为块的分配位图。每一位表示一个block，如果位为1，表示block已经用满。如果为0，表示未使用，或未用满。tdbatmap由头结构和batmap内容部分组成。头结构如下图：
在WINHEX中的磁盘表现(连同其后的bitmap区域)如下：
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则直接读取。
[作者及后记]
作者：张宇，北亚数据恢复中心创始人本文也做为目前北亚招聘数据恢复工程师和C++开发工程师的背景材料。本文仅首发于51CTO，如需转载，请保留完整信息。[1]