磁盤的GPT分區表(解決xFsRedir目錄重定向程序中一個擴展虛擬磁盤子功能的兼容問題)

                                          by fanxiushu  2020-05-18 轉載或引用請註明原始作者。
平時也不大關注磁盤的分區參數等信息,只是最近在更新xFsRedir程序的時候,
想要使用底層的storport框架的虛擬磁盤驅動代替基於直接IO的簡單模型的虛擬磁盤驅動,
這樣做的目的是爲了讓虛擬出來的磁盤更接近操作系統需求,更能欺騙windows把它當成真正的磁盤。
xFsRedir的使用請去GITHUB下載:
https://github.com/fanxiushu/xFsRedir
使用storport框架的虛擬磁盤驅動,會被windows當成一塊真正的磁盤,會在設備管理器和磁盤管理中找到這樣的一個“設備”,
但是它需要的鏡像文件源,則相當於一塊真正的物理磁盤上的數據存儲佈局。
也就是裏邊包含分區表結構信息,可能還包括引導數據,分區的實際數據內容等。
xFsRedir的xfs_redir.sys驅動集成了一種叫直接IO的簡單的磁盤模型,其實就是 filedisk 這種框架。
這些內容可以去CSDN上查閱我之前的文章,都有詳細介紹。
這種驅動的好處就是簡單,容易理解,也容易開發,安裝起來也很方便。
缺點也很明顯,不會被windows當成真正的磁盤,少數奇奇怪怪的程序不能識別。
還有就是鏡像文件源,數據存儲的佈局只是單個分區的數據內容。

爲了讓這兩種虛擬磁盤驅動的鏡像文件兼容,
主要是爲了讓基於storport的虛擬磁盤能正確讀寫基於filedisk模式虛擬磁盤驅動生成的只有單一分區的鏡像文件。
我們就不得不想辦法解決這個問題。
能想到最好的辦法,就是給這個只有單一分區數據內容的鏡像文件,“添加“額外的分區表信息,
這個分區表只包含一個分區信息,也就是這個單一分區的鏡像文件。
這種“添加”不是要我們修改這個只存儲單一分區數據的鏡像文件,
而是在storport驅動發起讀寫扇區請求的時候,根據讀寫情況,給他額外回覆一些扇區,這些扇區就是分區表等磁盤的初始化信息。

回覆額外扇區主要包括兩個做法:
一,驅動不做修改,在請求鏡像文件的服務端程序上添加這種額外扇區回覆。
       這種做法一般針對自定義的私有協議的服務端程序比較好處理,因爲都是自己定義的,
       而xFsRedir包含了大量的公共通訊協議,因此這種辦法在xFsRedir來說不可取。
二,在storport驅動中做修改,添加額外扇區回覆。這是xFsRedir採用的辦法。

其實在windows系統中,我們把只要把小於2T的單一分區的鏡像文件源輸入給storport驅動,也能正確識別並且自動彈出對應磁盤盤符。
這也許看起來比較神奇,因爲這塊只有分區,而沒有分區表的”磁盤“,居然能被windows正確識別出來了。
但是當我們把一個分區超過 2T 大小的單一分區鏡像輸入給storport驅動,情況就很混亂了。
也許會被當成未被初始化的磁盤,或者這塊磁盤出現很多未格式化的分區。簡單的說,就是不能正確識別。
因此我們可以這樣總結:
對於小於2T的分區的單一分區鏡像,windows會把他當成MBR分區的磁盤,也就是會“自做主張”的添加MBR分區表頭等信息。
windows這樣做的目的主要是爲了兼容的考慮,但是針對大於2T的分區,windows也就無能爲力了只能亂認。

爲了兼容,我們都得給storport虛擬磁盤驅動 ”添加“ 額外分區表等初始化扇區,但是很顯然如果是添加MBR分區表意義不大。
因此得添加GPT分區表頭。win7以上系統都支持GPT分區的磁盤作爲數據盤,而storport驅動框架其實也只支持win7以上的系統。
而在驅動中添加GPT分區表頭,我們就必須熟悉GPT分區表的格式。
而這就是下面具體闡述的內容。
(這裏不再闡述如何開發storport驅動,如何在驅動中增加這種額外的請求,有興趣可去查閱我CSDN上虛擬磁盤驅動開發相關文章)。

現代的磁盤的尋址都是使用線性的邏輯存儲塊地址方式尋址(Logical Block  Address)簡稱 LBA,
而早期磁盤採用複雜的柱面,磁頭,扇區等尋址。
一個邏輯塊的大小也不再是固定的 512字節(雖然通常都是512字節)。邏輯塊,我們爲了習慣,也可以簡單稱呼爲扇區。
比如我正在使用的電腦,邏輯塊的大小是 4096字節(也就是 4K-Sector), 還有些是 2048 字節。

MBR分區的磁盤第一個Block(也就是0號扇區, 可以簡單寫作 LBA0 ) 存儲的是引導程序和分區信息,
引導程序佔用446字節,接下來64個字節是硬盤分區表,最後兩個字節固定爲 55AA,
每個分區信息佔用 16 個字節,64個字節也就是隻能分4個分區,這就是MBR分區的限制,
同時 在16個字節中,只使用了4個字節表示扇區的個數,每個扇區固定512字節的話,
這樣每個分區 不會超過 4G*512字節 = 2T 大小,這也是MBR分區最大的限制。

GPT分區的出現主要是爲了解決MBR分區的各種限制,適應現在的越來越大的磁盤容量。
比如我基本上都是把買來的4T,8T機械硬盤分成一個分區,這種做法除了使用GPT分區沒其他選擇。

GPT分區的磁盤 LBA0 (也就是磁盤的第一個Block, 0號扇區),存儲MBR,這個叫保護性MBR,
其實就是爲了兼容老舊只認MBR分區的軟件,防止這些老舊軟件在認不出GPT分區之後亂操作。
這個保護性MBR前446字節全寫0,接下來64個字節只需填寫前16字節,其餘全0,
這16字節設置一個不識別的分區屬性,並且扇區結束地址全寫0xFF,當然最後兩個字節必須是 55AA。
GPT分區的磁盤 LBA1 (磁盤的第2個block,1號扇區),存儲的是GPT頭,GPT頭雖然只需要佔用92個字節,
但是整個 LBA1 都給了 GPT頭,除了前面92字節,其餘填 0。
接下來從 LBA2 開始存儲分區表結構信息,分區表存儲不再使用扇區爲單位,而是採用字節爲單位,
也就是不再是一個扇區存儲一個或幾個分區表結構,
而是所有這些扇區組成一塊大的地址,分區表結構按次序一個挨着一個的存儲,下面會舉例說明。
分區表結構結構存儲之後,就是分區數據的實際內容了,
至於每個分區起始LBA地址和結束LBA地址,屬性等各種信息,自然是存儲在分區表結構裏。

最後在磁盤的尾部,會存儲GPT分區的備份,一般是把GPT頭存儲到磁盤的最後一個扇區中,然後倒數的其他扇區存儲分區表結構。

GPT頭的描述,可以簡化成如下的C語言數據結構來描述:
存儲方式按照小尾序的方式存儲。(uint32_t 4字節大小無符號整數, uuint64_t 8字節大小的無符號整數)
struct gpt_header
{
     char         magic[8];  //GPT頭的標誌,固定爲 “EFI PART”
     uint32_t   version;   // 版本,固定爲 0x100 00
     uint32_t   header_size;  // GPT大小,固定爲0x5C,也就是92
     uint32_t   header_crc32; //GPT頭的CRC32校驗和,計算的時候先把此值設置爲0,
     uint32_t   reserved; //保留,必須爲0
     uint64_t   header_LBA; ///GPT頭的LBA地址,通常是 LBA1,
     uint64_t   header_back_LBA; ///GPT頭備份地址,通常是磁盤的最後一個扇區。
     uint64_t   part_area_start_LBA; //分區區域的起始LBA,通常是分區表結束之後的第一個扇區。
     uint64_t   part_area_end_LBA; //分區區域的結束LBA,通常是分區表備份之前的一個扇區。
     GUID      disk_id;// 磁盤的GUID值,16個字節。
     uint64_t  part_table_start_LBA;  ///分區表的起始LBA
     uint32_t  part_table_count;  //分區表的個數,在windows系統中,通常固定爲 128個GPT分區表。
     uint32_t  part_table_size;    //每個分區表大小,固定爲128字節。
     uint32_t  part_table_crc32;  //所有分區表的CRC32校驗和。
};
一共92個字節描述GPT頭。

下面是每個分區表結構的C語言描述:
struct gpt_table
{
      GUID      part_type;    //GUID表示的這個分區類型,比如 C12A7328-F81F-11D2-BA4B-00A0C93EC93B 表示 EFI 系統分區等
      GUID      part_id;      //這個分區的唯一標誌
      uint64_t   start_LBA;   //這個分區的起始LBA
      uint64_t   end_LBA;    //這個分區的結束LBA
      uint64_t   attr;              //這個分區的屬性
      WCHAR  name[36];    //這個分區的名字。
};
一共128個字節描述GPT分區表結構。

正如上面所述,LBA0存儲保護性MBR,LBA1存儲 gpt_header結構。然後從LBA2開始存儲 gpt_table數組也就是分區表數組。
我們以windows系統爲例,windows系統基本都是固定128個分區表,也就是 128個gpt_table結構。
每個分區表佔用128個字節,因此存儲所有分區表佔用 128*128 = 16384 字節,也就是 16KB,
上面說過分區表是挨着存儲的,我們做個簡單計算。

如果磁盤扇區大小是512字節,則需要 16KB/512 = 32個扇區存儲所有分區表,再加一個保護性MBR,一個GPT頭,
   因此磁盤前面需要 1 + 1 + 32 = 34個扇區。同時GPT還需要備份扇區,因此在磁盤最後需要 1 + 32 = 33個扇區來備份GPT頭和GPT分區表。

如果磁盤扇區是4K-Sector(也就是4096字節),則需要 16KB/4K = 4個扇區存儲分區表,加一個保護性MBR,一個GPT頭,
   因此磁盤前面需要 1 + 1 + 4 = 6個扇區, 同時GPT需要備份扇區,因此磁盤最後需要 1 + 4 = 5個扇區來備份。

以下僞代碼演示如何模擬出只有一個分區的GPT分區表:
這裏先假設幾個參數:
block_size代表扇區大小(比如 512,4096等)
disk_size 代表整個磁盤的大小。
LBA0 代表磁盤的起始地址,(也就是0號扇區)

     LONG part_sector_count = ( 128 * 128 + block_size - 1) / block_size; ///計算所有分區表佔用多少個扇區。
     LONG pre_len = ( 1 + 1 + part_sector_count ) * block_size; // 1 PMBR + 1 GPT header + part tables,磁盤前面佔用扇區數
     LONG suf_len = ( 1 + part_sector_count ) *block_size; /// backup, 1 GPT header + part tables. 磁盤後面佔用扇區數

     PCHAR  LBA1 =  LBA0 + block_size; //LBA1地址,
     PCHAR  LBA2 =  LBA1 + block_size; //LBA2地址

     uint64_t end_sector = disk_size.QuadPart / block_size - 1; // 整個磁盤的結束扇區
     ////保護性MBR
     memset(LBA0, 0,  block_size); ///全部設置0,
     LBA0[0x1BE] = LBA0[0x1BF] = 0x00;//填寫第一個分區表,
     LBA0[0x1C0] = 2; LBA0[0x1C1] = 0; LBA0[0x1C2] = 0xEE; LBA0[0x1C3] = LBA0[0x1C4] = LBA0[0x1C5] = 0xFF;
     LBA0[0x1C6] = 0x01; LBA0[0x1CA] = LBA0[0x1CB] = LBA0[0x1CC] = LBA0[0x1CD] = 0xFF;
     LBA0[510] = 0x55; LBA0[511] = 0xAA; // MBR 結束

    //填寫GPT頭
    gpt_header* hdr =(gpt_header*)LBA1;
    memset(LBA1, 0, block_size);
    strcpy(hdr->magic, "EFI PART"); // 8bytes
    hdr->version = 0x010000; ///  version 1.0
    hdr->header_size = 0x5C; // GPT header size 92 字節
    hdr->header_crc32 = 0; //gpt header CRC
    hdr->reserved = 0; // reserved 4 bytes
    hdr->header_LBA = 0x01; // LBA1 ,GPT頭起始位置
    hdr->header_back_LBA = end_sector; //GPT header 備份扇區,
    hdr->part_area_start_LBA = 1 + 1 + part_sector_count ; ///分區起始,通常從 34 sector
   hdr->part_area_end_LBA = end_sector - (1 + part_sector_count );
   hdr->disk_id = diskId;
    hdr->part_table_start_LBA = 0x02; /// 分區表起始扇區
   hdr->part_table_count = 0x80; ///分區表總項數 128
   hdr->part_table_size = 0x80; ///每個分區表字節數 128
    hdr->part_table_crc32 = 0;   //分區表CRC

    //填寫GPT分區表,這裏只需要第一個分區表,其他全部設置爲0
    memset(LBA2, 0, part_sector_count*block_size); 
    gpt_table* tbl = (gpt_table*)LBA2;
   tbl->part_type = PARTITION_BASIC_DATA_GUID; /// basic partition
    tbl->part_id = partId;
    tbl->start_LBA = 1 + 1 + part_sector_count ; //分區開始
    tbl->end_LBA = end_sector - (1 + part_sector_count ); // end partition sector
   tbl->attr = 0; /// attribute
    wcscpy(tbl->name, L"xFsRedirVDisk"); /// name
    
    //計算crc32
    hdr->header_crc32=crc32(LBA1, block_size);
    hdr->part_table_crc32=crc32(LBA2, 128*128);  ///
   
    ///計算備份 LBA地址
    bak_LBA1 = LBA0 + end_sector;
    bak_part_LBA = LBA0 + end_sector - part_sector_count;

    memcpy(bak_part_LBA, LBA2, part_sector_count*block_size); //直接複製分區表
    memcpy(bak_LBA1, LBA1, block_size); ////直接複製GPT頭,但是有些參數需要修改
    ///修改某些參數
    gpt_header* bak_hdr = (gpt_header*)bak_LBA1;
    bak_hdr->header_LBA = end_sector; //這個與LBA1中設置相反
    bak_hdr->header_back_LBA = 1; ///
    bak_hdr->part_table_start_LBA = bak_part_LBA; //
    bak_hdr->header_crc32 = 0;  bak_hdr->header_crc32 = crc32(bak_LBA1, block_size); ///重新計算CRC32

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章