扇區(qū)如何決定系統(tǒng)啟動與分區(qū)管理)
簡介這份資源圍繞硬盤MBR主引導(dǎo)記錄的讀取展開面向具備一定C基礎(chǔ)、希望深入理解磁盤底層結(jié)構(gòu)與系統(tǒng)級I/O編程的開發(fā)者可用于系統(tǒng)開發(fā)、數(shù)據(jù)恢復(fù)等場景的學(xué)習(xí)與參考。包內(nèi)共13個文件以cpp源碼、h頭文件與txt說明文檔為主另含dsp、dsw等工程配置文件和plg等輔助文件壓縮包約9KB工程結(jié)構(gòu)完整可直接在開發(fā)環(huán)境中打開研讀。資源從文件操作、低級I/O、扇區(qū)大小、內(nèi)存映射、MBR結(jié)構(gòu)解析、安全處理與錯誤處理等角度切入配合示例代碼演示如何將硬盤首扇區(qū)512字節(jié)讀入緩沖區(qū)并解析分區(qū)表項幫助讀者理解引導(dǎo)程序與分區(qū)信息的組織方式。目前已有485人學(xué)習(xí)下載適合想打通底層磁盤讀寫與C工程實踐的學(xué)習(xí)者對照源碼逐步消化。1. 讀取硬盤MBR為什么一個512字節(jié)的扇區(qū)能決定整臺機器怎么啟動把一塊硬盤接到機器上BIOS 或 UEFI 固件做完自檢后第一件事往往就是去讀這塊盤的第一個扇區(qū)——512 字節(jié)不多不少。這 512 字節(jié)里藏著分區(qū)表、引導(dǎo)代碼和兩個魔數(shù)機器能不能找到操作系統(tǒng)、分區(qū)邊界畫在哪里全看它。很多人寫磁盤工具時覺得「讀個扇區(qū)」是最簡單的活結(jié)果一上手就翻車讀出來全是 0x00或者分區(qū)表解析出一堆重疊區(qū)間甚至把 GPT 盤當(dāng)成 MBR 盤硬解。問題不在讀而在沒搞懂 MBR 這個結(jié)構(gòu)的邊界條件和歷史包袱。搞懂它你才能理解為什么分區(qū)表只有 4 項、為什么主分區(qū)最多 4 個、為什么擴展分區(qū)要套一層鏈表、為什么 2TB 是道坎。這些不是玄學(xué)是 1980 年代定下的字節(jié)布局在今天的直接后果。2. MBR 的字節(jié)布局446 64 2 是怎么切出來的2.1 一個扇區(qū)的三段式結(jié)構(gòu)MBRMaster Boot Record固定占一個邏輯扇區(qū)傳統(tǒng)上按 512 字節(jié)算。這 512 字節(jié)不是隨便分的它被切成三段每段承擔(dān)完全不同的職責(zé)偏移長度內(nèi)容職責(zé)0x000446 字節(jié)引導(dǎo)代碼bootloader stage 1固件跳轉(zhuǎn)后執(zhí)行的第一段機器碼0x1BE64 字節(jié)分區(qū)表4 × 16 字節(jié)描述磁盤上分區(qū)的起止與類型0x1FE2 字節(jié)結(jié)束標(biāo)志 0x55 0xAA告訴固件「這是一個可引導(dǎo)扇區(qū)」446 這個數(shù)字不是設(shè)計出來的是減出來的512 減去 64 字節(jié)分區(qū)表再減去 2 字節(jié)簽名剩下 446 給引導(dǎo)代碼。這意味著引導(dǎo)代碼的空間極其緊張早期 stage 1 干的事非常有限——通常只是把后續(xù)扇區(qū)加載到內(nèi)存再跳過去。分區(qū)表每項 16 字節(jié)4 項正好 64 字節(jié)這個「4」是硬編碼在結(jié)構(gòu)里的不是可配置的。2.2 分區(qū)表項 16 字節(jié)里裝了什么每個分區(qū)表項Partition Entry的 16 字節(jié)布局是理解 MBR 的關(guān)鍵字段順序和字節(jié)序都有講究/* MBR 分區(qū)表項共 16 字節(jié)小端序 */ struct mbr_partition_entry { uint8_t status; /* 0x00非活動, 0x80活動(可引導(dǎo)) */ uint8_t chs_start[3]; /* 起始扇區(qū)的 CHS 地址歷史遺留 */ uint8_t type; /* 分區(qū)類型如 0x83Linux, 0x07NTFS */ uint8_t chs_end[3]; /* 結(jié)束扇區(qū)的 CHS 地址 */ uint32_t lba_start; /* 起始扇區(qū)的 LBA 地址小端序 */ uint32_t sector_count; /* 分區(qū)占用的扇區(qū)總數(shù)小端序 */ };這里有兩個必須注意的點。第一lba_start和sector_count都是 32 位無符號整數(shù)按小端序存儲。32 位扇區(qū)號乘以 512 字節(jié)最大可尋址約 2TB這就是 MBR 的容量天花板。第二CHS 字段是歷史遺留現(xiàn)代磁盤上基本不用但很多老工具仍然會讀它如果 CHS 和 LBA 不一致某些固件會犯迷糊。常見的實現(xiàn)思路是解析時以 LBA 為準(zhǔn)CHS 只做兼容性參考不要拿它做實際尋址。2.3 為什么分區(qū)表只有 4 項4 項的限制直接來自 64 字節(jié)除以 16 字節(jié)。當(dāng)年設(shè)計時沒預(yù)料到一塊盤會需要超過 4 個分區(qū)于是用「主分區(qū) 擴展分區(qū)」的方案打補丁把其中一個主分區(qū)標(biāo)記為擴展分區(qū)類型 0x05 或 0x0F擴展分區(qū)本身不直接存文件系統(tǒng)而是指向一個擴展引導(dǎo)記錄EBR鏈每個 EBR 里再放一個邏輯分區(qū)的描述并用鏈表串下去。這就是為什么你最多只能有 4 個主分區(qū)但邏輯分區(qū)可以有很多——代價是解析時要順著 EBR 鏈走鏈斷了后面的分區(qū)就全丟。3. 讀取 MBR 的實現(xiàn)從打開設(shè)備到解析出分區(qū)3.1 用 Python 讀原始扇區(qū)讀 MBR 的本質(zhì)是繞過文件系統(tǒng)直接對塊設(shè)備做偏移讀取。Linux 下是/dev/sdX或/dev/nvme0n1Windows 下是\\.\PhysicalDriveN。下面這段代碼演示了最小可用的讀取與解析流程import struct SECTOR_SIZE 512 MBR_SIGNATURE b\x55\xAA def read_mbr(device_path): # 以二進制只讀方式打開塊設(shè)備不做任何緩沖假設(shè) with open(device_path, rb) as f: sector f.read(SECTOR_SIZE) if len(sector) ! SECTOR_SIZE: raise IOError(讀取不足一個扇區(qū)設(shè)備可能不是塊設(shè)備) # 校驗結(jié)束標(biāo)志位置固定在 0x1FE if sector[0x1FE:0x200] ! MBR_SIGNATURE: raise ValueError(0x55AA 簽名缺失可能不是 MBR 盤) return sector def parse_partitions(sector): parts [] for i in range(4): # 每項 16 字節(jié)從 0x1BE 開始 off 0x1BE i * 16 entry sector[off:off 16] status entry[0] ptype entry[4] lba_start, count struct.unpack(II, entry[8:16]) if ptype 0 or count 0: continue # 空項跳過 parts.append({ index: i 1, bootable: status 0x80, type: ptype, start_lba: lba_start, sectors: count, size_bytes: count * SECTOR_SIZE, }) return partsstruct.unpack(II, ...)里的表示小端序這是 MBR 的硬性約定用大端解出來會得到完全錯誤的扇區(qū)號。entry[8:16]正好覆蓋lba_start和sector_count兩個字段。跳過ptype 0的項是因為空分區(qū)表項的類型字節(jié)為 0不跳過會解析出一堆起始為 0 的假分區(qū)。3.2 擴展分區(qū)的鏈表遍歷主分區(qū)解析完后如果發(fā)現(xiàn)類型是 0x05 或 0x0F就要順著 EBR 鏈繼續(xù)讀。EBR 的結(jié)構(gòu)和 MBR 類似但只有前兩項有意義第一項描述當(dāng)前邏輯分區(qū)第二項指向下一個 EBR。遍歷時要注意EBR 里的lba_start是相對于擴展分區(qū)起點的偏移不是絕對扇區(qū)號直接拿來用會算錯位置。def walk_extended(f, ext_start_lba): logical [] current ext_start_lba while current: f.seek(current * SECTOR_SIZE) ebr f.read(SECTOR_SIZE) if ebr[0x1FE:0x200] ! MBR_SIGNATURE: break # 第一項當(dāng)前邏輯分區(qū)起始是相對擴展分區(qū)起點的偏移 first ebr[0x1BE:0x1CE] rel_start, count struct.unpack(II, first[8:16]) if count: logical.append({ start_lba: ext_start_lba rel_start, sectors: count, }) # 第二項下一個 EBR 的絕對位置 second ebr[0x1CE:0x1DE] next_lba, _ struct.unpack(II, second[8:16]) current next_lba if next_lba else None return logical這里最容易踩的坑是ext_start_lba rel_start這個加法。很多實現(xiàn)忘了加基址導(dǎo)致邏輯分區(qū)的起始扇區(qū)全部偏小讀出來的文件系統(tǒng)全是亂的。另外循環(huán)要有終止條件鏈損壞時next_lba可能指回自己不加防護會死循環(huán)。3.3 校驗與容錯讀之前先確認盤的類型現(xiàn)代磁盤很多是 GPT 分區(qū)表但 GPT 盤為了保護老工具會在第一個扇區(qū)放一個「保護性 MBR」類型字節(jié)是 0xEE覆蓋整個磁盤。如果你只檢查 0x55AA 簽名就往下解析會把保護性 MBR 當(dāng)成真 MBR解析出一個橫跨全盤的分區(qū)。正確的做法是先看分區(qū)表項類型如果第一項類型是 0xEE說明這是 GPT 盤應(yīng)該轉(zhuǎn)去讀 LBA 1 的 GPT 頭而不是繼續(xù)按 MBR 解。這個判斷順序是讀取 MBR 時最該養(yǎng)成的習(xí)慣。4. 避坑與排查讀 MBR 時最常見的四類翻車4.1 讀出來全是 0x00現(xiàn)象打開設(shè)備成功read(512)也返回了 512 字節(jié)但內(nèi)容全是零簽名校驗直接失敗。原因通常有兩個一是打開的是分區(qū)設(shè)備而不是整盤設(shè)備比如讀了/dev/sda1而不是/dev/sda分區(qū)起點不是 MBR二是權(quán)限不足某些系統(tǒng)上非 root 讀塊設(shè)備會返回零填充而不是報錯。解決方法是確認設(shè)備路徑指向整盤并用lsblk或類似工具核對設(shè)備層級同時確保有讀原始設(shè)備的權(quán)限。4.2 簽名對但分區(qū)全空現(xiàn)象0x55AA 校驗通過但四個分區(qū)表項的類型字節(jié)全是 0。原因可能是這塊盤確實沒有分區(qū)裸盤直接建文件系統(tǒng)也可能是讀到了某個被清零的扇區(qū)。還有一種隱蔽情況某些虛擬化環(huán)境或鏡像文件的前 512 字節(jié)是引導(dǎo)代碼但沒有分區(qū)表。排查時先打印 0x1BE 開始的 64 字節(jié)原始十六進制人工看一眼是不是全零再決定是盤本身如此還是讀取偏移錯了。4.3 分區(qū)區(qū)間重疊或越界現(xiàn)象解析出的分區(qū)起始扇區(qū)加扇區(qū)數(shù)超過了磁盤總?cè)萘炕蛘邇蓚€分區(qū)區(qū)間互相重疊。原因多半是字節(jié)序搞反了把lba_start按大端解出來會得到一個巨大的數(shù)。另一個常見原因是把 CHS 字段誤當(dāng) LBA 用。解決方法是固定用struct.unpack(II, ...)解 LBA 字段并在解析后加一道校驗start_lba sector_count 磁盤總扇區(qū)數(shù)越界就報警而不是靜默接受。4.4 擴展分區(qū)鏈死循環(huán)現(xiàn)象遍歷 EBR 鏈時程序卡住或內(nèi)存暴漲。原因是鏈中某個 EBR 的next_lba指向了已經(jīng)訪問過的扇區(qū)形成環(huán)。血淚經(jīng)驗是遍歷時必須記錄已訪問的 LBA 集合發(fā)現(xiàn)重復(fù)立即終止并報錯。同時給鏈長度設(shè)一個上限比如 128超過就認為鏈損壞。這兩個防護不加遇到一塊分區(qū)表被寫壞的盤工具直接掛死。5. 設(shè)計取舍為什么今天還要親手讀一遍 MBRMBR 是一套帶著明顯時代烙印的結(jié)構(gòu)4 個分區(qū)項、32 位扇區(qū)號、CHS 與 LBA 并存、擴展分區(qū)用鏈表打補丁。從工程角度看它幾乎每個設(shè)計都是妥協(xié)的產(chǎn)物。但恰恰因為它是「事實標(biāo)準(zhǔn)」大量存量磁盤、鏡像文件、嵌入式設(shè)備仍然以它為準(zhǔn)很多底層工具的第一道解析入口還是這 512 字節(jié)。親手讀一遍 MBR價值不在于學(xué)會一個讀取函數(shù)而在于理解「固定偏移 硬編碼長度 魔數(shù)校驗」這套底層二進制格式的通用解析范式——GPT 頭、文件系統(tǒng)超級塊、可執(zhí)行文件頭本質(zhì)上都是同一套思路的變體。這里有一個值得想清楚的設(shè)計取舍解析 MBR 時是嚴格校驗還是寬松容錯嚴格校驗簽名不對就拒絕能避免誤判但遇到保護性 MBR 或非標(biāo)準(zhǔn)盤就會失效寬松容錯盡量解析能兼容更多情況但可能把垃圾數(shù)據(jù)當(dāng)成分區(qū)表。我一般會這么理解先做類型判別0xEE 轉(zhuǎn) GPT再做簽名校驗最后對每個分區(qū)項做區(qū)間合法性檢查三層過濾下來既不誤傷也不放過明顯損壞的盤。這個順序比單純「讀 512 字節(jié)然后解 64 字節(jié)」要可靠得多。驗證方法也很直接拿一塊已知分區(qū)的盤用系統(tǒng)工具如fdisk -l或分區(qū)管理界面導(dǎo)出分區(qū)起止再和你解析出的start_lba、sectors逐項比對數(shù)字對得上才算解析正確。別只看「沒報錯」就以為成了靜默的錯誤比崩潰更難查。從那以后我每次寫涉及原始扇區(qū)讀取的代碼都強制先打印前 512 字節(jié)的十六進制確認簽名和類型字節(jié)再往下走解析邏輯——這個習(xí)慣幫我擋掉了好幾次「讀錯設(shè)備」的低級錯誤。希望幫到你。本文還有配套的精品資源點擊獲取