news 2026/10/12 3:13:33

读取硬盘MBR:从hexdump到Python解析器实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
读取硬盘MBR:从hexdump到Python解析器实战

简介:这份资源围绕硬盘MBR(主引导记录)的读取与解析展开,面向具备一定C++基础、希望深入理解磁盘底层结构与系统级I/O编程的开发者。内容涵盖文件操作、低级I/O调用、512字节扇区读取、内存映射、MBR分区表结构解析以及安全备份与错误处理等关键知识点,并配有可参考的代码示例,帮助读者掌握从打开设备到解析分区信息的完整流程。资源包共13个文件,以cpp源码、h头文件与txt说明文档为主,另含dsp、dsw、opt等工程配置文件,整体约9KB,结构紧凑,便于直接编译调试与二次修改。目前已有484人学习下载,适合用于系统开发、数据恢复等场景下的入门实践与思路参考。

1. 读取硬盘MBR:从一行 hexdump 到能写解析器的完整路径

很多人第一次听到「读取硬盘MBR」,脑子里浮现的是杀毒软件或者数据恢复工具的画面,觉得离自己很远。但真实情况是:你只要有一台 Linux 机器、一块能被系统识别的磁盘,用dd加hexdump两条命令,五分钟内就能把主引导记录的前 512 字节原封不动地打印出来。问题不在于「能不能读」,而在于读出来之后那一堆十六进制数字到底在说什么——分区表在哪、引导代码从哪开始、0x55AA 这个签名为什么必须存在、四个分区表项各自 16 字节怎么切。这篇笔记就是顺着这个路径走的:先让你把数据读出来,再教你把 512 字节拆成有意义的字段,最后给一个能跑的 Python 解析器,并说清楚哪些操作会翻车。

适合谁看?做数据恢复的、写底层工具的、搞系统安全的、以及单纯想弄明白「开机那一刻硬盘上到底发生了什么」的工程师。不需要你懂汇编,但需要你敢在终端里敲命令,并且愿意接受一个事实:MBR 这套东西是 1983 年定下来的,它的很多设计在今天看来是历史包袱,但你绕不过去。

2. MBR 的 512 字节到底装了什么:字段布局与读取原理

2.1 为什么是 512 字节,以及这 512 字节的分工

MBR(Master Boot Record)位于磁盘的第一个扇区,也就是 LBA 0。传统扇区大小是 512 字节,这个数字不是随便定的,它和早期硬盘的物理扇区规格绑定。虽然现在很多盘物理扇区是 4096 字节,但为了兼容,逻辑扇区仍然模拟成 512 字节,MBR 依然只占这 512 字节里的内容。

这 512 字节的分工非常明确:

偏移范围长度内容
0x000 - 0x1BD446 字节引导代码(Bootloader 第一阶段)
0x1BE - 0x1CD16 字节分区表项 1
0x1CE - 0x1DD16 字节分区表项 2
0x1DE - 0x1ED16 字节分区表项 3
0x1EE - 0x1FD16 字节分区表项 4
0x1FE - 0x1FF2 字节签名 0x55AA

446 + 16×4 + 2 = 512,刚好。这个布局从 IBM PC 时代延续至今,没有变过。你读到的任何一块使用 MBR 分区方案的磁盘,这 512 字节的结构都是一样的。

引导代码那 446 字节是最容易被忽略的部分。很多人以为 MBR 就是分区表,其实分区表只占最后 66 字节(4×16+2)。前 446 字节是真正在 BIOS 把控制权交出来之后第一批执行的机器码。Windows 的 MBR 引导代码、GRUB 的 stage1、syslinux 的引导扇区,都是写在这个位置的。如果你用dd把某块盘的 MBR 备份出来,前 446 字节往往能看到一些非零的指令字节,而不是全零。

2.2 用 dd 和 hexdump 把 MBR 读出来

在 Linux 下读取 MBR 最直接的方式就是用dd。假设你的目标磁盘是/dev/sda,那么:

# 从 /dev/sda 的第 0 字节开始,读取 512 字节,输出到文件 # bs=512 表示块大小为 512 字节,count=1 表示只读一块 # iflag=direct 绕过页缓存,确保读到的是磁盘上的真实数据 sudo dd if=/dev/sda of=mbr.bin bs=512 count=1 iflag=direct # 用 hexdump 以规范格式查看,-C 表示同时显示十六进制和 ASCII hexdump -C mbr.bin

执行完之后你会看到类似这样的输出(这里只展示关键部分):

00000000 fa 31 c0 8e d0 bc 00 7c 8e c0 8e d8 be 00 7c |.1.....|......|.| 00000010 bf 00 06 b9 00 02 fc f3 a4 50 68 1c 06 cb fb b9 |.........Ph.....| ... 000001b0 ................ ................ |................| 000001c0 02 00 83 fe ff ff 00 08 00 00 00 00 08 00 |................| 000001d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| ... 000001f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 aa |..............U.|

这里有几个关键点。第一,iflag=direct这个参数很重要,不加的话dd可能从页缓存里读到旧数据,尤其是你刚改过分区表但内核还没刷新的时候。第二,bs=512 count=1是最小读取单位,不要用bs=1 count=512,那样效率极低而且容易出错。第三,如果你要读的不是整盘而是某个分区,比如/dev/sda1,那读出来的不是 MBR,而是该分区的引导扇区(VBR),结构完全不同,别搞混。

注意:对磁盘设备执行 dd 读取通常需要 root 权限。如果你只是想做实验,可以先在一个镜像文件上操作,比如dd if=/dev/zero of=test.img bs=1M count=10造一个 10MB 的空镜像,然后用fdisk给它写一个 MBR 分区表,再拿这个镜像来练手。

2.3 分区表项的 16 字节怎么拆

四个分区表项,每个 16 字节,结构是固定的。以第一个分区表项(偏移 0x1BE)为例,16 字节的分配如下:

相对偏移长度含义
01 字节引导标志(0x80 表示可引导,0x00 表示不可引导)
1-33 字节起始 CHS 地址(柱面/磁头/扇区,已过时)
41 字节分区类型(如 0x83 表示 Linux,0x07 表示 NTFS)
5-73 字节结束 CHS 地址
8-114 字节起始 LBA 扇区号(小端序)
12-154 字节分区扇区总数(小端序)

CHS 那 3 个字节在今天基本可以忽略,因为 LBA 寻址已经全面取代了它。但你在解析的时候不能跳过,因为偏移量是固定的,跳过了后面全错。分区类型那个字节值得记几个常见值:0x83 是 Linux,0x07 是 NTFS/exFAT,0x0B 和 0x0C 是 FAT32,0x82 是 Linux swap,0xEF 是 EFI 系统分区。如果你看到 0xEE,那说明这块盘用的是 GPT 保护性 MBR,真正的分区表在 GPT 里,不在 MBR 里。

起始 LBA 和扇区总数都是小端序的 4 字节整数。比如你在 hexdump 里看到00 08 00 00,那起始 LBA 就是 0x00000800 = 2048。这是很常见的值,因为很多分区工具默认从 2048 扇区开始分区,为的是对齐到 1MB 边界。

3. 用 Python 写一个能跑通的分区表解析器

3.1 读取与校验签名的代码实现

光看 hexdump 不够,你需要一个能自动解析字段的工具。下面这个 Python 脚本读取 MBR 二进制文件,校验签名,然后逐个解析四个分区表项:

import struct import sys def parse_mbr(filepath): with open(filepath, 'rb') as f: mbr = f.read(512) if len(mbr) != 512: print(f"错误:读取到 {len(mbr)} 字节,预期 512 字节") return # 校验签名,偏移 0x1FE 处的两个字节必须是 0x55 0xAA signature = struct.unpack_from('<H', mbr, 0x1FE)[0] if signature != 0xAA55: print(f"警告:签名不匹配,读到 0x{signature:04X},预期 0xAA55") print("这可能不是 MBR,或者磁盘使用了 GPT 保护性 MBR") return print("签名校验通过:0x55AA") print(f"引导代码前 16 字节:{mbr[:16].hex(' ')}") print() # 四个分区表项,起始偏移分别是 0x1BE, 0x1CE, 0x1DE, 0x1EE for i in range(4): offset = 0x1BE + i * 16 entry = mbr[offset:offset + 16] boot_flag = entry[0] part_type = entry[4] start_lba = struct.unpack_from('<I', entry, 8)[0] sector_count = struct.unpack_from('<I', entry, 12)[0] # 全零的分区表项表示未使用 if part_type == 0 and start_lba == 0 and sector_count == 0: print(f"分区 {i+1}: 未使用") continue boot_str = "可引导" if boot_flag == 0x80 else "不可引导" print(f"分区 {i+1}:") print(f" 引导标志: 0x{boot_flag:02X} ({boot_str})") print(f" 分区类型: 0x{part_type:02X}") print(f" 起始 LBA: {start_lba}") print(f" 扇区总数: {sector_count}") print(f" 容量: {sector_count * 512 / 1024 / 1024:.2f} MB") print() if __name__ == '__main__': if len(sys.argv) != 2: print(f"用法: {sys.argv[0]} <mbr.bin>") sys.exit(1) parse_mbr(sys.argv[1])

这段代码的核心逻辑很直白:先读 512 字节,校验最后两字节是不是 0x55AA,然后从 0x1BE 开始每 16 字节切一刀,用struct.unpack_from按小端序取出起始 LBA 和扇区总数。<I里的<表示小端序,I表示 4 字节无符号整数。如果你在大端序平台上跑,这个格式字符串保证了字节序不会出错。

参数方面,唯一需要你改的就是传入的文件路径。如果你直接从磁盘设备读,可以把open(filepath, 'rb')换成open('/dev/sda', 'rb'),但要注意权限和读取位置——open默认从文件头开始读,对磁盘设备来说就是 LBA 0,正好是 MBR。

3.2 分区类型映射与输出可读性优化

上面的脚本输出的是十六进制类型码,实际用的时候你肯定想知道 0x83 到底是什么意思。加一个映射表:

PARTITION_TYPES = { 0x00: "空", 0x07: "NTFS / exFAT", 0x0B: "FAT32 (CHS)", 0x0C: "FAT32 (LBA)", 0x0E: "FAT16 (LBA)", 0x82: "Linux swap", 0x83: "Linux", 0x8E: "Linux LVM", 0xEE: "GPT 保护性 MBR", 0xEF: "EFI 系统分区", } # 在打印分区类型时替换为: type_name = PARTITION_TYPES.get(part_type, f"未知 (0x{part_type:02X})") print(f" 分区类型: 0x{part_type:02X} ({type_name})")

这个映射表不需要背,用的时候查就行。但有一个值你必须记住:0xEE。如果你解析出来的分区类型是 0xEE,说明这块盘实际用的是 GPT 分区方案,MBR 里那个 0xEE 分区只是一个保护性占位,真正的分区信息在 LBA 1 开始的 GPT 头里。这时候你继续按 MBR 的逻辑去解析后面的分区表项,得到的全是垃圾数据。

3.3 在镜像文件上完整跑一遍验证流程

不要一上来就拿真实磁盘练手,先用镜像文件走一遍完整流程:

# 创建一个 64MB 的空镜像 dd if=/dev/zero of=test.img bs=1M count=64 # 用 fdisk 给它写一个 MBR 分区表 # 注意:这里用 printf 管道自动输入命令,避免交互 printf 'n\np\n1\n\n+16M\nn\np\n2\n\n+16M\nw\n' | fdisk test.img # 读取这个镜像的 MBR dd if=test.img of=mbr_test.bin bs=512 count=1 # 用 Python 脚本解析 python3 parse_mbr.py mbr_test.bin

跑完之后你应该能看到两个分区,类型是 0x83,起始 LBA 和扇区总数都能对上。如果输出里签名校验失败,先检查dd的count和bs是不是写反了。如果分区显示为「未使用」,检查fdisk那一步是不是真的写入了分区表——有时候printf的换行符数量不对,fdisk会卡在交互界面里没执行完。

4. 读取 MBR 时最容易翻车的五个地方

4.1 把 GPT 盘当成 MBR 盘解析

现象:脚本输出了四个分区,但起始 LBA 和扇区总数看起来完全不合理,比如起始 LBA 是 1、扇区总数是 0xFFFFFFFF。

原因:这块盘用的是 GPT 分区方案,MBR 里只有一个类型为 0xEE 的保护性分区,覆盖整块盘。你看到的「四个分区」里,第一个是 0xEE,后面三个是全零或者残留数据。

解决:在解析之前先检查第一个分区表项的类型字节。如果是 0xEE,直接停止 MBR 解析,改用 GPT 解析逻辑去读 LBA 1 的 GPT 头。不要试图从保护性 MBR 里提取分区信息,它本来就不包含。

4.2 dd 读到了缓存里的旧数据

现象:你刚用分区工具调整了分区表,然后用dd读 MBR,解析出来的还是旧的分区布局。

原因:Linux 内核会缓存块设备的数据,dd默认从页缓存读取,不一定反映磁盘上的最新状态。

解决:加iflag=direct绕过缓存。如果还是旧数据,用sudo blockdev --flushbufs /dev/sda强制刷新块设备缓冲区,或者sudo partprobe /dev/sda让内核重新读取分区表。

4.3 扇区大小不是 512 字节

现象:在某种存储设备上读取 MBR,读出来的数据偏移全部错位,签名不在 0x1FE 位置。

原因:少数设备逻辑扇区大小是 4096 字节,MBR 虽然只占前 512 字节,但如果你用bs=4096 count=1去读,拿到的 4096 字节里前 512 字节确实是 MBR,但你的解析代码如果按 512 字节切片就会出错。

解决:读取时始终用bs=512 count=1,不要假设扇区大小。如果你需要确认设备的逻辑扇区大小,用sudo blockdev --getss /dev/sda,输出 512 就是传统扇区,输出 4096 就是 4K 扇区。

4.4 权限不足导致读到全零

现象:dd命令没有报错,但读出来的 512 字节全是 0x00,签名校验自然也不通过。

原因:没有用sudo,普通用户对/dev/sda没有读权限,某些系统上dd会静默失败或者读到空数据。

解决:加sudo。如果加了sudo还是全零,检查设备路径是否正确,用lsblk确认磁盘设备名。另外,某些容器环境里块设备不可见,需要在宿主机上操作。

4.5 混淆 MBR 和 VBR

现象:你想读某块盘的 MBR,但不小心读了/dev/sda1而不是/dev/sda,解析出来的「分区表」完全是乱码。

原因:/dev/sda1是分区设备,它的第一个扇区是 VBR(Volume Boot Record),不是 MBR。VBR 的结构取决于文件系统类型,跟 MBR 完全不是一回事。

解决:读 MBR 永远用整盘设备(/dev/sda、/dev/nvme0n1),不要用分区设备(/dev/sda1、/dev/nvme0n1p1)。如果你确实想读 VBR,那是另一个话题,需要按文件系统类型去解析。

5. 从能读到能用:把 MBR 解析嵌进自动化检查流程

5.1 批量检查多块盘的 MBR 签名与分区布局

单块盘手动读一读没问题,但如果你手上有几十块盘要做巡检,一个个敲命令就不现实了。我一般的做法是把解析逻辑包成一个函数,然后批量跑:

import subprocess import struct import os def read_mbr_from_device(device): """从块设备直接读取 512 字节 MBR""" try: result = subprocess.run( ['sudo', 'dd', f'if={device}', 'bs=512', 'count=1', 'iflag=direct'], capture_output=True, timeout=10 ) if result.returncode != 0: return None, f"dd 失败: {result.stderr.decode().strip()}" data = result.stdout if len(data) != 512: return None, f"读取长度异常: {len(data)}" return data, None except subprocess.TimeoutExpired: return None, "读取超时" def check_mbr_health(device): """检查单块盘的 MBR 状态,返回摘要信息""" data, err = read_mbr_from_device(device) if err: return f"{device}: 读取失败 - {err}" sig = struct.unpack_from('<H', data, 0x1FE)[0] if sig != 0xAA55: return f"{device}: 签名异常 (0x{sig:04X})" # 检查第一个分区表项类型 first_type = data[0x1BE + 4] if first_type == 0xEE: return f"{device}: GPT 保护性 MBR,需用 GPT 解析" # 统计有效分区数 valid_parts = 0 for i in range(4): off = 0x1BE + i * 16 ptype = data[off + 4] start_lba = struct.unpack_from('<I', data, off + 8)[0] if ptype != 0 and start_lba != 0: valid_parts += 1 return f"{device}: MBR 正常,{valid_parts} 个有效分区" # 批量检查 devices = ['/dev/sda', '/dev/sdb', '/dev/sdc'] for dev in devices: if os.path.exists(dev): print(check_mbr_health(dev)) else: print(f"{dev}: 设备不存在")

这段代码的关键改进是用了subprocess.run加capture_output=True,把dd的输出直接拿进 Python 里处理,不需要落盘中转。timeout=10是防止某块盘响应慢导致整个脚本卡死。iflag=direct依然保留,确保读到的是磁盘真实数据。

5.2 把解析结果输出成结构化 JSON 供后续消费

如果你要把 MBR 检查结果接入监控系统或者配置管理数据库,输出 JSON 比纯文本好用得多:

import json def mbr_to_dict(device): data, err = read_mbr_from_device(device) if err: return {"device": device, "error": err} sig = struct.unpack_from('<H', data, 0x1FE)[0] result = { "device": device, "signature": f"0x{sig:04X}", "signature_valid": sig == 0xAA55, "is_gpt_protective": data[0x1BE + 4] == 0xEE, "partitions": [] } for i in range(4): off = 0x1BE + i * 16 ptype = data[off + 4] start_lba = struct.unpack_from('<I', data, off + 8)[0] sector_count = struct.unpack_from('<I', data, off + 12)[0] if ptype == 0 and start_lba == 0: continue result["partitions"].append({ "index": i + 1, "bootable": data[off] == 0x80, "type": f"0x{ptype:02X}", "start_lba": start_lba, "sector_count": sector_count, "size_mb": round(sector_count * 512 / 1024 / 1024, 2) }) return result # 输出 JSON print(json.dumps(mbr_to_dict('/dev/sda'), indent=2, ensure_ascii=False))

这样输出的结果可以直接喂给日志系统或者 CMDB。ensure_ascii=False保证中文不会被转义成\uXXXX,可读性更好。

5.3 一个我踩过的坑:别在挂载中的系统盘上反复 dd

最后说一个血泪经验。早期我做批量巡检的时候,图省事直接在运行中的系统盘上反复dd读 MBR,频率高了之后发现某些盘会出现短暂的 I/O 错误。原因不复杂:iflag=direct绕过了缓存,每次读都直接打到磁盘上,而系统盘本身还在承受正常的读写压力,高频直接读会加剧 I/O 竞争。后来我改成先dd到内存文件系统(/dev/shm)再解析,或者干脆用hdparm之类的工具做只读查询,问题就没再出现过。如果你只是偶尔读一次,无所谓;但要是做成定时任务每分钟跑一次,建议加个间隔或者改用更轻量的方式。

另外一个习惯:每次解析 MBR 之前,先确认目标设备没有被挂载为可写。用mount | grep sda看一眼,如果是rw挂载的,读之前心里要有数——你读到的数据可能正在被文件系统层修改,虽然 MBR 区域通常不会被文件系统动,但谨慎一点没坏处。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/12 3:11:51

光线追踪渲染器从零实现:核心代码、调参与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/12 3:11:38

Linux 下用 nvm 安装 Node v23 与 npm 10,实现多版本隔离管理

1. 项目概述1.1 为什么需要 nvm&#xff0c;而不是直接改系统的 Node先说个我踩过的坑。早些年我在一台服务器上把 Node 从 v16 升到 v18&#xff0c;直接拿了官方 tar 包覆盖&#xff0c;结果系统里某老运维脚本里硬编码的 npm 路径全部炸掉&#xff0c;找问题花了整整半天。后…

作者头像 李华
网站建设 2026/10/12 3:10:57

OpenCV 4.8.0 DNN模块集成ONNX Runtime,推理加速与部署实践指南

简介&#xff1a;OpenCV 4.8.0 是一套跨平台计算机视觉与机器学习库&#xff0c;面向 C、Python、Java 开发者&#xff0c;覆盖图像处理、特征检测、对象识别、深度学习模型部署等常见任务。该版本整合 core、imgproc、dnn、calib3d 等核心模块&#xff0c;并针对硬件加速和运行…

作者头像 李华
网站建设 2026/10/12 3:10:50

从InfluxDB到Doris:DolphinScheduler离线同步实践

最近在调数据中台的离线链路时&#xff0c;我把一条一直很“绕”的路真正打通了&#xff1a;在AllData数据中台的离线开发平台&#xff08;集成DolphinScheduler&#xff09;上&#xff0c;把InfluxDB里的监控指标同步到Doris进行分析。其实InfluxDB和Doris我都用了很长时间&am…

作者头像 李华
网站建设 2026/10/12 3:10:46

ArcGIS新手Day1:理解坐标系、属性表与专题图

我第一次打开ArcGIS的时候&#xff0c;整个人是懵的&#xff1a;窗口密密麻麻全是按钮&#xff0c;工具栏层层叠叠&#xff0c;地图区域一片空白&#xff0c;完全不知道从哪里下手。后面用得久了才想明白&#xff0c;ArcGIS本质上并不是一个“画图软件”&#xff0c;而是一套围…

作者头像 李华
网站建设 2026/10/12 3:08:19

Tesseract 5.0编译后完整版本实战:从安装配置到中文识别避坑

简介&#xff1a;一份Tesseract 5.0编译后的完整版资源包&#xff0c;面向OCR应用开发者与图像文本识别场景。包内已集成核心引擎、依赖库及常用工具&#xff0c;可开箱即用或作为二次开发基础&#xff0c;降低自行编译源码的门槛。压缩包共496个文件&#xff0c;涵盖动态库、静…

作者头像 李华