MSF 文件格式

文件布局

MSF 文件格式由以下组件组成

  1. 超级块

  2. 空闲块映射(也称为空闲页面映射或 FPM)

  3. 数据

每个组件都存储为一个索引块,其长度在SuperBlock::BlockSize中指定。文件由 1 个或多个以下模式的迭代组成(有时称为“间隔”)

  1. 1 个数据块

  2. 空闲块映射 1(对应于SuperBlock::FreeBlockMapBlock 1)

  3. 空闲块映射 2(对应于SuperBlock::FreeBlockMapBlock 2)

  4. 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只能是12

  • 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 的第一个字节,这可能在文件中的后面数万字节(甚至更早!),具体取决于流目录中说明的内容。