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