在STM32上實現NTFS之5:GPT分區表的C語言實現(2)GPT實現以及統一方式讀取磁盤分區
上一節實現了主GPT頭的信息提取,這一節繼續提取整個的GPT數據,並且將GPT分區表和MBR分區表兩種格式融合成一個模塊,使主調函數(也可以說是使用者)不需要關心磁盤的分區表類型:它太底層了,確實不需要過多的關心。
繼續看上一節的圖1,這裏就不貼圖了,LBA1的主GPT頭給出了分區信息的總數,還有每一個分區信息所占用的字節數,分區信息的結構如表1:
表1 分區信息結構(GPT Entry)
字節偏移量 |
數據長度(字節) |
範例數值 |
數據項說明 |
0x00 |
16 |
28 73 2A C1 1F F8 D2 11 BA 4B 00 A0 C9 3E C9 3B |
用GUID表示的分區類型 |
0x10 |
16 |
82 63 7A C8 13 0E F8 46 95 29 E6 31 E9 16 B5 42 |
用GUID表示的分區唯一標示符 |
0x20 |
8 |
00 08 00 00 00 00 00 00 |
該分區的起始扇區,用LBA值表示 |
0x28 |
8 |
FF 27 E6 31 E9 16 B5 42 |
該分區的結束扇區(包含),用LBA值表示,通常是奇數 |
0x30 |
8 |
00 00 00 00 00 00 00 80 |
該分區的屬性標誌 |
0x38 |
72 |
|
UTF-16LE編碼的人類可讀的分區名稱,最大32個字符 |
可以看到,這個結構非常之簡潔,只標註了起始扇區,結束扇區,分區類型,GUID,屬性,分區名,而不關心磁頭、磁道等等信息。
需要特殊說明的是,整個分區信息的結構,也就是從LBA2到LBA34(MS總是給劃分128個分區信息),是完全連續的,不依靠任何扇區等信息去定位,也就是說,第n個分區信息的結構起始地址,僅僅根據LBA2的起始地址+分區字節數*n來確定,最常見的是微軟定義的128個分區信息,512字節扇區,128字節的分區信息字節數,所以最常見的是每一個扇區4個分區信息,密集的排列32個扇區。然而正確的計數方法是每128個字節一個分區信息,密集排列128個。當扇區不是512字節時(註意這裏不要跟簇混淆,目前我還沒見過不是512字節扇區的存儲器),每個扇區存儲的分區信息有可能不是4個,下面的程序規避了這個問題,采用了一個新的磁盤讀寫函數——ReadDiskData而不是ReadSectorData——來防止讀分區信息出現錯誤。
1 /******************************************************************************* 2 3 函 數 名:GetVolumeNumberOfGPT 4 5 函數功能:獲取一個GPT格式的磁盤中有效分卷數 6 7 輸入參數: 8 9 hDisk:磁盤句柄 10 11 DPT: 磁盤DPT 12 13 返回參數:int型,返回DPT中包含的有效分區數量(包含未格式化的分卷),如果該DPT不是GPT 14 15 分區形式,將會返回-1 16 17 *******************************************************************************/ 18 19 int GetVolumeNumberOfGPT(HANDLE hDisk, DPT_Info* DPT) 20 21 { 22 23 if (DPTDetermination(DPT) == DPT_MBR) 24 25 return -1; 26 27 GPTEntry_Byte GPTEbuffer; 28 29 GPTEntry_Info GPTEinfo; 30 31 PGPTH_Info PGPTH; 32 33 GetPGPTH(hDisk, &PGPTH); 34 35 int ValidPartitions = 0; 36 37 for (int i = 0; i < PGPTH.PartitionTables; i++) 38 39 { 40 41 ReadDiskData( 42 43 hDisk, //讀取磁盤句柄 44 45 SECTOR_SIZE * PGPTH.PartitionStart + i * PGPTH.BytesPerPartitionTable, //計算讀取的GPTE位置 46 47 (uint8_t*)(void*)&GPTEbuffer, //緩沖區地址 48 49 sizeof(GPTEntry_Byte)); //字節數 50 51 GetGPTEInfo(&GPTEbuffer, &GPTEinfo); 52 53 if (GUIDcmp(&(GPTEinfo.TypeGUID), (GUID_Info*)&GUID_ptUnuse)) 54 55 break; 56 57 else 58 59 ValidPartitions++; 60 61 } 62 63 return ValidPartitions; 64 65 }
分區類型是有規定的,一般有表2的幾種GUID;屬性標誌也是有規定的,見表3。
表2:分區類型的16Byte GUID
數值 |
類型說明 |
00000000-0000-0000-0000-000000000000 |
未使用 |
024DEE41-33E7-11D3-9D69-0008C781F39F |
MBR分區表 |
C12A7328-F81F-11D2-BA4B-00A0C93EC93B |
EFI系統分區[EFI System partition (ESP)],必須是VFAT格式 |
BC13C2FF-59E6-4262-A352-B275FD6F7172 |
擴展boot分區,必須是VFAT格式 |
21686148-6449-6E6F-744E-656564454649 |
BIOS引導分區,其對應的ASCII字符串是"Hah!IdontNeedEFI"。 |
D3BFE2DE-3DAF-11DF-BA40-E3A556D89593 |
Intel Fast Flash (iFFS) partition (for Intel Rapid Start technology) |
E3C9E316-0B5C-4DB8-817D-F92DF00215AE |
微軟保留分區 |
EBD0A0A2-B9E5-4433-87C0-68B6B72699C7 |
基本數據分區 |
DE94BBA4-06D1-4D40-A16A-BFD50179D6AC |
Windows恢復環境 |
0FC63DAF-8483-4772-8E79-3D69D8477DE4 |
數據分區。Linux曾經使用和Windows基本數據分區相同的GUID。 |
44479540-F297-41B2-9AF7-D131D5F0458A |
x86根分區 (/) 這是systemd的發明,可用於無fstab時的自動掛載 |
4F68BCE3-E8CD-4DB1-96E7-FBCAF984B709 |
x86-64根分區 (/) 這是systemd的發明,可用於無fstab時的自動掛載 |
69DAD710-2CE4-4E3C-B16C-21A1D49ABED3 |
ARM32根分區 (/) 這是systemd的發明,可用於無fstab時的自動掛載 |
B921B045-1DF0-41C3-AF44-4C6F280D3FAE |
AArch64根分區 (/) 這是systemd的發明,可用於無fstab時的自動掛載 |
3B8F8425-20E0-4F3B-907F-1A25A76F98E8 |
服務器數據分區(/srv) 這是systemd的發明,可用於無fstab時的自動掛載 |
933AC7E1-2EB4-4F13-B844-0E14E2AEF915 |
HOME分區 (/home) 這是systemd的發明,可用於無fstab時的自動掛載 |
0657FD6D-A4AB-43C4-84E5-0933C84B4F4F |
交換分區(swap) 不是systemd的發明,但同樣可用於無fstab時的自動掛載 |
A19D880F-05FC-4D3B-A006-743F0F84911E |
RAID分區 |
E6D6D379-F507-44C2-A23C-238F2A3DF928 |
邏輯卷管理器(LVM)分區 |
8DA63339-0007-60C0-C436-083AC8230908 |
保留 |
上面的GUID數值比較有意思,根據winHex得到的扇區數據,它將16字節的GUID分成了5部分,在很多編譯器中已經有GUID結構體的定義不過是4部分的,在這裏無法使用,所以我們另外定義一個結構
1 typedef struct 2 3 { 4 5 uint32_t Part1; //GUID第1部分 6
7 uint16_t Part2; //GUID第2部分 8 9 uint16_t Part3; //GUID第3部分 10 11 uint16_t Part4; //GUID第4部分 12 13 uint48_t Part5; //GUID第5部分 14 15 }GUID_Info;
來表示GUID。而有意思的地方在於,這5部分並不是單純的完全大端或者小端模式,它是混合著來的,前三個部分是小端,後兩個部分是大端,我們在讀取結構的時候一定要註意這點。
表3:分區屬性
數值 |
類型說明 |
0 |
系統分區 |
1 |
EFI隱藏分區(EFI不可見分區) |
2 |
傳統的BIOS的可引導分區標誌 |
60 |
只讀 |
62 |
隱藏 |
63 |
不自動掛載,也就是不自動分配盤符 |
以上的屬性列表並不完全,至少我的硬盤分區之後,類型就是0x80。因為只有一個字節有用的信息,一個枚舉類型就足以解決問題。
現在我們實際看一個擁有4個分區信息的LBA,如圖1:
圖1 LBA分區
如圖所示,紅框框起來的部分就是一個分區信息結構,比較精髓的部分是粉色框裏的72字節,它是以UTF16的小端模式編碼的,因為涉及到了中文問題,UTF16LE解碼比較復雜,在這裏我們直接用wchar_t來獲取這個結構:
1 typedef struct 2 3 { 4 5 uint8_t TypeGUID[16]; //用GUID表示的分區類型 6 7 uint8_t UniqueGUID[16]; //用GUID表示的分區唯一標示符 8 9 uint8_t SectorStart[8]; //該分區的起始扇區,用LBA值表示 10 11 uint8_t SectorEnd[8]; //該分區的結束扇區(包含),用LBA值表示,通常是奇數 12 13 uint8_t PartitionAttrib[8]; //該分區的屬性標誌 14 15 WCHAR PartitionName[36]; //UTF-16LE編碼的人類可讀的分區名稱,最大32個字符。 16 17 }GPTEntry_Byte;
現在GPT結構已經可以完整的讀取,由於博文實在實驗階段性完成後做的,所以沒有截圖(=_=尷尬)。
接下來的步驟是整合GPT和MBR。由於DPT引導到GPT和MBR的部分可以直接整合,所以原DPT部分代碼可以不動,但不同的是GPT是通過讀取LBA直接得到每個分區的起始扇區、終止扇區、分區類型等等,而MBR方式是先通過DPT得到小於等於4個引導扇區的位置,然後對這幾個引導扇區進行解析,每個引導扇區對應一個分區信息。
在使用其他的一些庫的時候,筆者受到困擾的地方就是,API函數太多,有很多的庫有不同的API功能是接近的,同時又有很多API名字相近但功能截然不同,所以為了避免這種情況,在這裏盡量縮減API。根據這個思想,可以構建大致的流程如下:首先由用戶提取分區總數,然後用戶來進行malloc操作來根據分區數建立分區列表,然後根據分區列表可以找到每個分區的引導扇區位置。在本節中,所謂的“分區信息”是針對於分區表的分區信息,事實上根據前幾節的介紹,分卷容量並不一定等於分區容量,所以目前的代碼中都是以Partition代表分區,而以Volume代表分卷。
詳細的代碼結構這裏不進行更多的截取,有關代碼的調用方式在main.cpp中很容易的看到。在壓縮包裏有基於VS2015community環境的完整工程。
最後貼上一張完整的分區列表運行結果(磁盤3是我的移動硬盤,磁盤4是我的64GB SD卡)。
圖2 代碼運行結果
工程下載地址:https://files.cnblogs.com/files/Coder-Ku/NTFS5.rar
在STM32上實現NTFS之5:GPT分區表的C語言實現(2)GPT實現以及統一方式讀取磁盤分區