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
個位元組。請注意,最後兩個陣列中的每一個陣列的長度都是可變的,特別是第二個陣列是參差不齊的。
範例: 假設一個區塊大小為 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 的第一個位元組,這可能會在檔案中的數萬個位元組之後(甚至之前!),具體取決於資料流目錄的內容。