MSF 文件格式¶
文件布局¶
MSF 文件格式由以下组件组成
每个组件都存储为一个索引块,其长度在SuperBlock::BlockSize
中指定。文件由 1 个或多个以下模式的迭代组成(有时称为“间隔”)
1 个数据块
空闲块映射 1(对应于
SuperBlock::FreeBlockMapBlock
1)空闲块映射 2(对应于
SuperBlock::FreeBlockMapBlock
2)SuperBlock::BlockSize - 3
个数据块
在第一个间隔中,第一个数据块用于存储超级块。
下图演示了文件的总体布局(| 表示间隔的结束,仅用于可视化目的)
块索引 |
0 |
1 |
2 |
3 - 4095 |
| |
4096 |
4097 |
4098 |
4099 - 8191 |
| |
… |
---|---|---|---|---|---|---|---|---|---|---|---|
含义 |
空闲块映射 1 |
空闲块映射 2 |
数据 |
| |
数据 |
FPM1 |
FPM2 |
数据 |
| |
… |
文件可以在任何块之后结束,包括紧接在 FPM1 之后。
注意
LLVM 仅支持 4096 字节块(有时称为“BigMsf”变体),因此本文档的其余部分将假设块大小为 4096。
超级块¶
在 MSF 文件的偏移量 0 处是 MSF 超级块,其布局如下
struct SuperBlock {
char FileMagic[sizeof(Magic)];
ulittle32_t BlockSize;
ulittle32_t FreeBlockMapBlock;
ulittle32_t NumBlocks;
ulittle32_t NumDirectoryBytes;
ulittle32_t Unknown;
ulittle32_t BlockMapAddr;
};
FileMagic - 必须等于
"Microsoft C / C++ MSF 7.00\\r\\n"
,后跟字节1A 44 53 00 00 00
。BlockSize - 内部文件系统的块大小。有效值为 512、1024、2048 和 4096 字节。MSF 文件布局的某些方面会根据块大小而有所不同。出于 LLVM 的目的,我们只处理 4KiB 的块大小,并且所有进一步的讨论都假设块大小为 4KiB。
FreeBlockMapBlock - 文件中一个块的索引,该块开始表示文件中所有“空闲”块的集合(即该块中的数据未被使用)的位字段。有关更多信息,请参阅空闲块映射。重要:
FreeBlockMapBlock
只能是1
或2
!NumBlocks - 文件中的块总数。
NumBlocks * BlockSize
应等于磁盘上文件的大小。NumDirectoryBytes - 流目录的大小(以字节为单位)。流目录包含有关每个流的大小以及它所占用的块集的信息。稍后将对其进行更详细的描述。
BlockMapAddr - MSF 文件中一个块的索引。在此块中,是一个
ulittle32_t
数组,列出了流目录所在的块。对于大型 MSF 文件,流目录(描述每个流的块布局)可能无法完全放在单个块上。因此,引入了这种额外的间接层,其中此块包含流目录所占用的块列表,并且流目录本身可以相应地拼接在一起。此数组中的ulittle32_t
数量由ceil(NumDirectoryBytes / BlockSize)
给出。
空闲块映射¶
空闲块映射(有时称为空闲页面映射或 FPM)是一系列块,其中包含文件中每个块的位标志。如果块正在使用,则标志将设置为 0;如果块未被使用,则标志将设置为 1。
每个文件包含两个 FPM,其中一个在任何给定时间都处于活动状态。此功能旨在支持对底层 MSF 文件的增量和原子更新。在写入 MSF 文件时,如果活动 FPM 为 FPM1,则可以将新的修改后的位字段写入 FPM2,反之亦然。只有在将文件提交到磁盘时,才需要交换 SuperBlock 中的值以指向新的FreeBlockMapBlock
。
空闲块映射作为一系列单个块存储在整个文件中,间隔为 BlockSize。因为每个 FPM 块的大小为BlockSize
字节,所以它包含的位数是间隔块数的 8 倍。这意味着每个 FPM 的第一个块指的是文件的前 8 个间隔(前 32768 个块),每个 FPM 的第二个块指的是接下来的 8 个块,依此类推。这导致存在的 FPM 块远远多于所需的块,但为了保持向后兼容性,格式必须保持这种方式。
流目录¶
流目录是访问 MSF 文件中其他所有流的根目录。从流目录的字节 0 开始,是以下结构
struct StreamDirectory {
ulittle32_t NumStreams;
ulittle32_t StreamSizes[NumStreams];
ulittle32_t StreamBlocks[NumStreams][];
};
并且此结构恰好占用SuperBlock->NumDirectoryBytes
字节。请注意,最后两个数组中的每一个都是可变长度的,特别是第二个数组是不规则的。
示例:假设一个虚拟 PDB 文件,其块大小为 4KiB,并且有 4 个长度分别为 {1000 字节、8000 字节、16000 字节、9000 字节} 的流。
流 0:ceil(1000 / 4096) = 1 个块
流 1:ceil(8000 / 4096) = 2 个块
流 2:ceil(16000 / 4096) = 4 个块
流 3:ceil(9000 / 4096) = 3 个块
总共使用了 10 个块。让我们看看流目录可能是什么样子
struct StreamDirectory {
ulittle32_t NumStreams = 4;
ulittle32_t StreamSizes[] = {1000, 8000, 16000, 9000};
ulittle32_t StreamBlocks[][] = {
{4},
{5, 6},
{11, 9, 7, 8},
{10, 15, 12}
};
};
总共占用15 * 4 = 60
字节,因此SuperBlock->NumDirectoryBytes
将等于60
,并且SuperBlock->BlockMapAddr
将是一个ulittle32_t
数组,因为60 <= SuperBlock->BlockSize
。
还要注意,流是不连续的,并且流 3 的一部分位于流 2 的一部分的中间。您不能假设块的布局!
对齐和块边界¶
如您现在可能已经清楚的那样,单个字段(无论是高级记录、长字符串字段,甚至单个uint16
)都可能在不同的块中开始和结束。例如,如果块大小为 4096 字节,并且uint16
字段从当前块的最后一个字节开始,则它需要在下一个块的第一个字节结束。由于块在文件中不一定是连续布局的,这意味着 MSF 文件的使用者和生产者都必须准备好相应地拆分数据。在上述示例中,uint16
的高字节将写入块 N 的最后一个字节,低字节将写入块 N+1 的第一个字节,这可能在文件中的后面数万字节(甚至更早!),具体取决于流目录中说明的内容。