头部
原创:弄潮小狮
从前文谈及的两份来之OpenOffice的文档看,《compdocfileformat》(复合文档文件格式)只有区区200来KB,《excelfileformat》(Excel文件格式)却超过1MB,看起来,Excel文件的结构似乎比复合文档复杂5倍?实际上,Excel文件格式相当简单,复合文档的文件格式却要复杂的多。
本文很多说法来源或翻译于《compdocfileformat》一文,若无特别说明,本文所指“英文原文”即指该文。
下文提及的“复合文档”均指“微软复合文档”。
摆弄文件格式,于小狮来讲,历史已经不短了,如果也可以说是资历的,那可谓“老”了。
最早应该在15年前,为了在Dos下用TC显示一幅BMP图片,连猜带蒙,搞定了BMP的文件格式,如果那时候知道这样做就能够成为黑客的话,那……
遐想中……
接下来就是几年前学习了一下PE,后来又搞了反汇编,顺带把com文件的格式也摸索了,熟悉我的朋友应该知道,做这些事为了一个“PB神盾”,也只是应用在自己的一些产品上,有朋友跟我说这个东东商机大大的……
又遐想……
摆弄文件结构,需要一些知识准备:字节、字、整数、浮点数、结构体等等,还有数制转换、ANIS、Unicode等等……
我相信很多人对这些知识已经滚瓜烂熟了,嗤之以鼻、不屑一谈。
曾经有牛人由于太精通PB,以至于把硬盘文件分为“二进制文件”(或者流文件)及“文本文件”,有鉴于此,为了准备让基础薄弱如小狮者能够理解本文,是有必要就有关知识啰嗦啰嗦的。
但本文又不是要讲这些,只能有机会另外撰文了,本文就先假设读者已经对上述知识滚瓜烂熟了……
鉴于有人称微软未公开复合文档等的文件结构,手头资料又并非微软官方文档,那么就意味官方术语可能并不存在,本文中有关术语、定义或者说法,或小狮自己的理解,或人云亦云、以讹传讹,或来源于对有关文档的翻译,小狮的翻译水平又确实有限,难免谬误,深表歉意之余,欢迎指正。
但谢绝引为学术依据,否则后果自负,这算是免责声明。
为了形象的表示复合文档的结构,以便下一步工作的校验,小狮专门为此用Delphi编写了一个名叫《复合文档查看器》的小程序,若无特别说明,本文插图均为该程序的运行结果。
复合文档的头部存放于文件开头的512(200h)个字节,这是固定的,头部存放的信息,基本上反映了整个文件的总体概貌。
小狮注:作为头部,大多数文件都用固定的字节数存放在文件的开头。但这并非固定的标准,有些头部是可变长度的,有些文件的头部则存放于文件的末尾,可见,“头部”仅仅是个称谓,并不等于“开头部分”,这不是翻译的问题,英文原文写做“Header”,大约是有来由的。如果“Header”确实又有“尾部”的意思,那就是小狮翻译水平的问题了。但复合文档的“头部”确实位于文件开头的的512个字节,这就使得中华儿女们对此的理解不会有歧义,这很好!
还有一句废话得讲讲,本文中涉及数字后面带个“h”的,说明是个十六进制数,如前文的200h,换算为十进制就是512。有些地方直接用十六进制而没有相应的换算为十进制,其一是小狮偷懒了,其二有些数值用十进制表示,于业内人士而言,反而不好表达。当然,如果读者认为我不懂换算,那我也管不了……
废话讲多了,该入正题了,先看图1:
图1是一个示例文件的头部,为了更能说明问题,我选择了一个16.4 MB那么大的文件来作为示例,为什么这么做?后面会讲到。
再啰嗦一下,在计算机科学中,计数一般从0开始,本文也这么做,其一是为了更像那么回事,其二在Delphi编程方便。
如图1所示:
头部从0字节开始的8个字节,是复合文档的标识,8个字节的值依次是D0h、CFh、11h、E0h、A1h、B1h、1Ah、E1h。如果这8个字节不是这些值,那么这个文件就不是复合文件,当然,刚好是这些值,那也不能说就是……
8至23一共16个字节,英文原文称“Unique identifier (UID) of this file (not of interest in the following, may be all 0)”,大意是“文件唯一标识,可能全部是0”,老实说,我没搞明白这个标识有什么用,手头用来做实验的文件也没找到一个不是全0的,自己生成的文件里面添上全0也没发生过什么事故,因此个人认为,这个当它全0就行了,不用管它,因此《复合文档查看器》也没有把它显示出来。
24(18h)至25两个字节,是一个16位整数,固定值为003Eh,文件格式的修订号。
26(1Ah)至27两个字节,是一个16位整数,固定值为0003h,文件格式的版本号。
28(1Ch)至29两个字节,是一个16位整数,值为FFFEh,字节排序方式。解释这个必须依赖WinHex,图2是WinHex打开示例文件的截图:
图2(WinHex截图)
图中:28(1Ch)字节的值是FEh,29(1Dh)字节的值是FFh,合起来,作为一个16位(2个字节)整数,它的值是FFFEh,这就是说,低字节在前,高字节在后。其实,这个在英文原文中有举例说明(第9页,4.2 Byte Order),其中说明很详细,我就不照译了,但这对于初涉其中的同学很容易混淆,所以专门说一下。
另外就是,按照原文的说法,FFFEh表示“Little-Endian”(这个词我不知道怎么翻译,意即低字节在前),FEFFh表示“Big-Endian”(这个词我不知道怎么翻译,意即高字节在前),那么,对于本字段而言,不管采用何种方法进行储存,其储存结果都如图2所示,这就很麻烦(其实小狮也完全搞不懂),所幸,英文原文又称“在现实应用中,只采用前者”,这就没什么问题了,我们直接把这个值固定为FFFEh,而且“Little-Endian”符合内存读写规律,省去很多转换。幸甚!幸甚!
30(1Eh)至31两个字节,是一个16位整数,值通常为9,表示一个扇区的大小,是2的幂,值为9表示2^9,即512个字节,这个值不是固定的,但通常是9。
扇区:英文“sector”,我不知道原文是否有“扇形区域”的意思,但“扇区”的叫法本身有点意思,这个有机会再写,在本文中,只需要把它理解为一个“存储块”就行了。
32(20h)至33两个字节,是一个16位整数,值通常为6,表示一个短扇区的大小,是2的幂,值为6表示2^6,即64个字节,这个值不是固定的,但通常是6。
短扇区:英文“short-sector”,这个概念我也是第一次接触,英文原文有解释(第12页,6.1 Short-Stream Container Stream),就不翻译了,我的理解是它“集装”(原文用了“Container”这个词)于扇区中。
由此,对这两个字段用2的幂来表示就很好理解,也只有这样,只要后者不大于前者,一个扇区就刚好能够“装下”整数个短扇区。
接下来从34(22h)至43共10个字节,英文原文称“Not used”,也就是不使用,这与其他文档的表述有点区别(别的文档称“保留”),我不知道是否是一样的意思,也不知道这10个字节是否真的不被使用,实际上这10个字节的值总是0。《复合文档查看器》也没有把它显示出来。
44(2Ch)至47四个字节,是一个32位整数,表示分区表扇区的总数,这个值跟文件大小有关。
分区表:英文原文“sector allocation table”(缩写SAT),意为“扇区分配表”,我把它称为“分区表”,其一是我记得磁盘系统有这种叫法,其二是为了下本提及时少打几个字。
在复合文档中:除了头部,其余存储空间按扇区大小划分成若干个扇区(你可以发现一个复合文档的字节总数总是512的整数倍),用分区表进行管理,而分区表本身也存储于扇区,这个字段表示用来存储分区表的扇区的总数。事实上,这种空间分配机制跟微软赖以起家的磁盘系统的分配机制极其类似。
大家都知道格式化磁盘的时候有一种“快速格式化”,其实就是重建一个磁盘分区表,仅此而已。
48(30h)至51四个字节,是一个32位整数,表示目录流第一个扇区的ID,ID这个词,用途太广泛了,在这里是编号的意思,小狮习惯称之为“编码”。
扇区编码:前面讲过,在复合文档中,除了头部,其余存储空间按扇区大小划分成若干个扇区,紧跟着头部的那个扇区就是第一个扇区,编号为0(从0开始编码),这样,从扇区编码就可以很容易的计算出扇区相对于文件开始位置的偏移量,示例中:目录流第一个扇区的ID为1,则偏移量=头部大小(512)+扇区编码(1)×扇区大小(512)=1024(400h)。
接下来52(34h)至55四个字节又是“Not used”,不使用。《复合文档查看器》也没有把它显示出来。
56(38h)至59四个字节,是一个32位整数,表示最小标准流尺寸,值通常为4096(1000h),当一个流的大小不小于这个值时,采用扇区(标准流)储存,否则采用短扇区(短流)储存,我不知道这种机制的好处在哪里,但人家这样做,我想自有道理吧?在文件处理的时候,时空开销上应该能够得到优化,但我不确定。
流:先贤们从西方文献中直译过来的,原文“stream”我不知道是否有其他译法,但“流”已经成为计算机科学中的一个基本术语,意思大约是“连续的数据信息”,其实我很难表述这个概念,也解释不好。便于理解起见,你可以将它当作文件系统中的一个文件来看待。
60(3Ch)至63四个字节,是一个32位整数,表示短分区表(缩写SSAT)的第一个扇区的ID,类似分区表,短分区表是用来管理短扇区的。值为FFFFFFFFh(-1)时,表示短分区表不存在。如前所述,如果复合文档的所有“流”的大小均不小于“最小标准流尺寸”,则没有采用短扇区来储存的“流”,当然短扇区就不存在,也不需要所谓的短扇区分区表来管理。
64(40h)至67四个字节,是一个32位整数,表示短分区表扇区总数,类似分区表,短分区表也需要扇区来储存。
68(44h)至71四个字节,是一个32位整数,表示主分区表(缩写MSAT)的第一个扇区的ID。前面讲到“分区表(SAT)”、“短分区表(SSAT)”,现在又冒出了一个“主分区表(MSAT)”,后面会有一篇专门来认识它们。要理解这个字段,还得借助接下来的两个字段,先接着说……
72(48h)至75四个字节,是一个32位整数,表示主分区表的扇区总数。
从76(4C)开始到头部结束,一共436个字节,为109个32位整数(可以看作数组),为主分区表的开头109个记录,每个记录为分区表扇区的编码(即ID)。当分区表扇区的个数(44(2Ch)至47)大于109个时,就需要另外的扇区来存储,前一个字段“主分区表的扇区总数”表示的就是“另外的扇区”的个数,而再前一个字段“主分区表的第一个扇区的ID”就表示这“第一个另外的扇区”的编码。当分区表扇区的个数不大于109个时,整个主分区表就存储于头部,不再需要“另外的扇区”。那么,“主分区表的第一个扇区的ID”的值为FFFFFFFFh(-1),“主分区表的扇区总数”的值为0。
当需要两个以上的“另外的扇区”来储存主分区表时,接下来的扇区如何寻址,下一篇再讲……
拗口!
早就说了,小狮是个极为啰嗦的人,一个头部就写了这么多,后面还有不少内容,不知什么时候才是个头。
请登录后查看评论内容