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 個位元組。請注意,最後兩個陣列中的每一個陣列的長度都是可變的,特別是第二個陣列是參差不齊的。

範例: 假設一個區塊大小為 4KiB 的 PDB 檔案,其中包含 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 的第一個位元組,這可能會在檔案中的數萬個位元組之後(甚至之前!),具體取決於資料流目錄的內容。