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 *SuperBlock*,其佈局如下

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 的第一個位元組,根據串流目錄的指示,區塊 N+1 可能在檔案中晚數萬個位元組(甚至更早!)。