复合文件CFB的存储结构及格式解析
OLE 2.0(CFB)
CFB(Microsoft Compound File Binary)复合文件二进制的文件格式实现,也称OLE(Object Linking and Embedding)即对象链接和嵌入或COM(Component Object Model)组件对象模型结构化存储复合文件实现二进制文件格式,这样的格式简称Compound File。简单的说,OLE 结构化存储的标准Windows COM 实现称为CF 复合文件。
传统文件系统难以将多种对象放入一个文档文件中,CF 提供了一个在单个文件中实现一个简单文件系统的解决方案。结构化存储定义了如何将一个文件当作是存储对象和流对象两种类型对象的分层集合。类似于文件系统,存储对象对应目录,而流对象对应文件。
- storage object,存储对象:
- 类似于文件系统目录,提供一个容器。storage 可以包含其他storage 和stream,并维护其下的子对象的位置、大小等信息。
- 存储对象可以包含一个被叫做CLSID的 object class GUID,可以用来识别一个application 去读写存储对象下的流对象。
- stream object,流对象:类似于文件系统文件,stream 包含连续顺序存储的用户数据。
- directory Entry,目录项提供了对存储对象的逻辑描述和元数据信息,它定义了storage object的属性和关联关系。
- sector, 扇区,配合扇区链,实现storage、stream 的物理存储。
所以,storage、stream 是逻辑数据结构,sector 是物理存储结构。
一个最小的CFB 文件包含一个Header sector、一个FAT sector、一个Directory sector,所以一个CFB 至少包含三个sector。
数据结构
一个Compound File 分为多个长度相同的扇区(sector),扇区是磁盘的最小寻址单位,一个扇区 512字节。一部分扇区 元素可以组成一个链表,扇区号用于索引。如下,#0、#2、#4 构成一个链表。一个扇区链构成一个流,一个扇区中只能存放一种类型的数据。
最小的复合文件CF 至少包含三个扇区:Header(512 bytes),一个FAT扇区(512 bytes),以及一个directory 扇区(512 bytes)。
具体的各扇区类型如下,
扇区类型 | 单个元素长度 | 大小 | 用途 |
---|---|---|---|
Header | 非固定 | 512 字节(实际有效76 字节,剩余436 字节用于DIFAT) | 包含这个复合文件的初始元数据,由offset 0 处开始 |
DIFAT | 4字节(共128 个元素) | 4109 + 419 | 用于FAT 寻址 |
FAT | 4字节(共128 个元素) | 4 * 128 = 512 字节 | OLE 中的主要Allocator,用于mini FAT、directory、mini stream 等扇区寻址 |
Mini FAT | 4字节(共128 个元素) | 512 字节 | mini stream 用户数据的Allocator |
Directory | 128字节 | 128 * 4 = 512 字节 | 包含storage 对象或stream 对象的元数据 |
用户自定义数据(即mini stream) | 非固定 | stream 对象的用户自定义数据 | |
Rang Lock | 非固定 | 用于管理对复合文件的并发访问的单个扇区。此扇区 必须包含文件偏移量 0x7FFFFFFF。 | |
Unallocated Free |
非固定 | OLE 的未分配空间 |
通常,每个扇区 512 字节
对于各扇区 值的含义,
Header
一下是offvis 工具解析出的.doc 文件的ole header:
其中,ByteOrder 指定了复合文件中所有的整型区都是以小端little-endian 存储,包含UTF16编码的signature,不过用户自定义数据流不受该限制。
最后剩余4 * 109 = 436 bytes 空间用于存储DIFAT 扇区地址。
DIFAT --> FAT-->|-mini FAT
|-directory
|-mini stream
DIFAT 扇区
Double-indirect file allocation table (DIFAT) 数组用于存储FAT 扇区,用于FAT 寻址
。每个数组元素也是一个4 字节的FAT 扇区号。
1个DIFAT 扇区分为512 字节(CFB_3),前4*127 字节用于FAT 寻址,最后4字节用于寻址下个DIFAT 扇区。DIFAT 起始地址在Header 中保存。
FAT 扇区
简而言之,FAT 用于分配扇区
。
扇区 链表和扇区 分配主要由File Allocation Table(FAT) 来管理,如FAT[N] 包含扇区 #N下个扇区。每个数组元素是一个32 位的扇区 号。FAT 数组的扇区s
FAT 扇区 的详细结构如下:
每个FAT 扇区的偏移地址为:
offset = (sector_number + 1) x sector_size
所以#0 FAT 扇区 的offset 不是0,而是sector_size。
Mini FAT 扇区
mini stream:为了解决扇区空间浪费的问题,将内部stream object 的用户定义数据
部分均分为多个mini stream。
与FAT 数组类似,只是mini FAT 数组存的是mini stream 的mini 扇区号
,而不是Compound file 的扇区号。mini 扇区的地址存储在FAT 标准链中,而此mini 扇区链的起始地址存在Header 中(first mini 扇区 num)
Directory entry 扇区
- directory: users, groups, computers, printers, and the directory service 等信息的存储实体,以被应用或用户访问。
- directory entry: 包含一个 storage 或stream 的元数据。
- directory stream: directory entries 数组打包放在扇区中。
directory entry(目录项)用于维护storage 对象和stream 对象的元数据。directory entry 数组包含多个directory entry,该数组存储在一个directory扇区中。
一个storage 对象或一个stream 对象对应一个directory entry,第一个directory entry对应root storage 对象。Directory entry扇区维护的数组空间由FAT分配
。directory entry 固定128 字节。
starting sector location:stream 对象的首扇区地址。
* stream object:first 扇区 地址。
* root storage object:mini stream 的first 扇区 地址。
* storage object:全0 填充。
stream size:stream 对象中用户自定义数据大小。
* stream object:用户自定义数据大小。
* root storage object:mini stream 的大小。
* storage object:全0 填充。
Directory Entry Name (64 bytes): 此字段必须包含以UTF-16编码的storage 或stream 名的Unicode字符串,且名称必须以UTF-16终止空字符结束。
Directory Entry Name Length (2 bytes): directory entry name 的unicode 字符串长度,限制最大不超过64,单位字节。
Object type:如下
Type | Value |
---|---|
Unknown or unallocated | 0x00 |
Storage Object | 0x01 |
Stream Object | 0x02 |
Root Storage Object | 0x05 |
Left Sibling ID (4 bytes): 左兄弟stream id
Right Sibling ID (4 bytes): 右兄弟stream id
Child ID (4 bytes): 子对象stream id
Root Directory entry
作为第一个directory entry,对应root storage object,它是所有对象的root,它存储了mini stream 的大小和第一个mini 扇区。
其他 Directory entry
对应普通storage object 、stream object,或者未分配对象。
cuteoff size:在mini stream 中存在的文件的截止大小(通常为4,096字节)。
红黑树
控制层次结构的一层中的每一组兄弟对象(存储对象下的所有子对象)都表示为红黑树。这一组兄弟对象的父对象有一个指向树顶的指针。红黑树是一种特殊的二叉搜索树,其中每个节点都有一个红色或黑色的颜色属性。红黑树允许在存储对象下的子对象列表中进行高效搜索。红黑树上的约束允许二叉树大致平衡,因此插入、删除和搜索操作是高效的。
用户自定义数据扇区
FAT 或mini FAT 中 user-defined data 扇区s 构成一条链,每条链有一个单独的directory entry 关联,并维护其stream 对象的元数据,如大小、名字等。
Range Lock 扇区
位于文件的 0x7FFFFF00-0x7FFFFFFF 位置,用于支持并发、事务等CFB特性。Range lock 扇区 不允许存储用户自定义数据。Header、FAT、DIFAT、mini FAT、directory 链不允许存储range lock 扇区 的地址。
复合文档CF的大小限制
因为兼容性原因,CFB-3 (512 字节的扇区)的复合文档的大小不允许超过2GB。
常规最大sector 地址 MAXREGSECT = 0xFFFFFFFA,但是range lock 的起始地址是0x7FFFFF00,所以一个CFB_3 复合文件一般最大2GB。