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