从磁盘映像中挂载或提取指定分区

2009-10-22

最近在做虚拟机相关的事,需要处理一些磁盘和分区的映像文件。如何从一个磁盘映像中挂载指定的分区到本地 Linux 文件系统呢?理论上说,可以用 dd 把该分区从磁盘映像中提取出来再挂载,不过 mount 提供了针对 loop 设备的偏移量参数,方便直接从磁盘映像中挂载指定分区。笔记如下:

演示用的磁盘映像使用 qemu-img 制作。我们使用原生的 raw 格式,等价于磁盘上的原始数据流,保证它在任何 Linux 系统上都可以直接挂载。使用 Windows PE 工具盘启动该 qemu 虚拟机,创建一系列不同格式的分区并在其中建立几个文件。

root@lj-laptop:/opt/vm# qemu-img create -f raw vmtest.img 5G
root@lj-laptop:/opt/vm# qemu -hda ./vmtest.img -cdrom /dev/cdrom -boot d -m 256M -localtime

磁盘分区结构如截图:

磁盘分区结构示例

回到宿主系统,使用 fdisk 的 -l、-u 参数查看磁盘映像。其中 -u 表示以扇区为单位显示分区起止位置,方便后续计算。

root@lj-laptop:/opt/vm# fdisk -l -u vmtest.img 
You must set cylinders.
You can do this from the extra functions menu.

Disk vmtest.img: 0 MB, 0 bytes
255 heads, 63 sectors/track, 0 cylinders, total 0 sectors
Units = sectors of 1 * 512 = 512 bytes
Disk identifier: 0xbd86bd86

Device Boot      Start         End      Blocks   Id  System
vmtest.img1              63     8193149     4096543+  83  Linux
vmtest.img2         8193150    10249469     1028160    5  Extended
vmtest.img3        10249470    10474379      112455    6  FAT16
vmtest.img5         8193213     8610839      208813+  83  Linux
vmtest.img6         8610903     9028529      208813+  82  Linux swap / Solaris
vmtest.img7         9028593    10249469      610438+   b  W95 FAT32

这时我们只需要为 mount 添加 offset 参数,指定分区在磁盘映像中的逻辑地址,即可挂载这一分区。注意 offset 的单位是字节,通常一个扇区是 512 字节,因此需要用 fdisk 输出的 Start 乘以 512。以 vmtest.img3 分区为例。

root@lj-laptop:/opt/vm# mount -o loop,offset=$((10249470*512)) vmtest.img ./part/
root@lj-laptop:/opt/vm# ls ./part/
InFAT16.txt
root@lj-laptop:/opt/vm# umount ./part/

如果有必要,我们可以将特定的分区从硬盘映像中提取出来,方便单独使用。通过 dd 可以完成提取,简便起见使用扇区大小 512 字节作为 bs 参数,这样 skip、count 参数很容易从 fdisk 的输出中计算出来。下面提取 vmtest.img5。

root@lj-laptop:/opt/vm# dd if=vmtest.img of=linux2.img bs=512 skip=8193213 count=$((8610839-8193213+1))
417627+0 records in
417627+0 records out
213825024 bytes (214 MB) copied, 4.67434 s, 45.7 MB/s
root@lj-laptop:/opt/vm# mount -o loop linux2.img ./part/
root@lj-laptop:/opt/vm# ls ./part/
InLinux2.txt  lost+found
root@lj-laptop:/opt/vm# umount ./part/

到这一步,我们已经实现了从磁盘映像中挂载或提取指定分区。其中 fdisk 帮我们完成了磁盘映像的分析,直接将分区的逻辑地址显示给了我们。现在我们再借机复习一下磁盘分区表的格式,试试手工计算相关地址。这里推荐大家阅读网上这篇《解读 Windows 操作系统分区表的秘密》,其中详解过的概念这里不再赘述,下面的内容权当对这篇文章在 Linux 平台下的补充。

首先查看磁盘映像的主引导扇区,其中分区表位于 0x1be 开始的 64 字节。

root@lj-laptop:/opt/vm# cat vmtest.img | xxd | head -32
...
00001b0: 0000 0000 0000 0000 86bd 86bd 0000 0001  ................
00001c0: 0100 83fe 7ffd 3f00 0000 3f04 7d00 0000  ......?...?.}...
00001d0: 41fe 05fe bf7d 7e04 7d00 8060 1f00 0000  A....}~.}..`....
00001e0: 817e 06fe bf8b fe64 9c00 8e6e 0300 0000  .~.....d...n....
00001f0: 0000 0000 0000 0000 0000 0000 0000 55aa  ..............U.

对于 vmtest.img1 和 vmtest.img3 这两个主分区,我们读出它们的起始逻辑地址分别为 0x3f 和 0x9c64fe(注意在分区表中以 Little-endian 存储),换算成十进制与 fdisk 输出的一致。bash 的算术表达式支持十六进制字面值,挂载方式同上。

root@lj-laptop:/opt/vm# mount -o loop,offset=$((0x3f*512)) vmtest.img ./part/
root@lj-laptop:/opt/vm# ls ./part/
InLinux.txt  lost+found
root@lj-laptop:/opt/vm# umount ./part/
root@lj-laptop:/opt/vm# mount -o loop,offset=$((0x9c64fe*512)) vmtest.img ./part/
root@lj-laptop:/opt/vm# ls ./part/
InFAT16.txt
root@lj-laptop:/opt/vm# umount ./part/

对于扩展分区 vmtest.img2,我们通过 dd 定位到它所指向的第一个逻辑分区 vmtest.img5 前面的卷引导记录,查看逻辑分区的链式分区表。

root@lj-laptop:/opt/vm# dd if=vmtest.img bs=512 skip=$((0x7d047e)) | xxd | head -32
...
00001b0: 0000 0000 0000 0000 0000 0000 0000 0001  ................
00001c0: 41fe 83fe bf17 3f00 0000 5b5f 0600 0000  A.....?...[_....
00001d0: 8118 05fe bf31 9a5f 0600 9a5f 0600 0000  .....1._..._....
00001e0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00001f0: 0000 0000 0000 0000 0000 0000 0000 55aa  ..............U.

vmtest.img5 相对于 vmtest.img2 的偏移量为 0x3f,按此地址挂载。

root@lj-laptop:/opt/vm# mount -o loop,offset=$(((0x7d047e+0x3f)*512)) vmtest.img ./part/
root@lj-laptop:/opt/vm# ls ./part/
InLinux2.txt  lost+found
root@lj-laptop:/opt/vm# umount ./part/

沿着链式分区表的第二条记录(即链表指针)指示的偏移量,我们可以找到第二、第三个逻辑分区(vmtest.img6、vmtest.img7)前面的卷引导记录以及这两个分区的逻辑地址。

root@lj-laptop:/opt/vm# dd if=vmtest.img bs=512 skip=$((0x7d047e+0x65f9a)) | xxd | head -32
...
00001b0: 0000 0000 0000 0000 0000 0000 0000 0001  ................
00001c0: 8118 82fe bf31 3f00 0000 5b5f 0600 0000  .....1?...[_....
00001d0: 8132 05fe bf7d 34bf 0c00 4ca1 1200 0000  .2...}4...L.....
00001e0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00001f0: 0000 0000 0000 0000 0000 0000 0000 55aa  ..............U.
root@lj-laptop:/opt/vm# dd if=vmtest.img bs=512 skip=$((0x7d047e+0xcbf34)) | xxd | head -32
...
00001b0: 0000 0000 0000 0000 0000 0000 0000 0001  ................
00001c0: 8132 0bfe bf7d 3f00 0000 0da1 1200 0000  .2...}?.........
00001d0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00001e0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00001f0: 0000 0000 0000 0000 0000 0000 0000 55aa  ..............U.

取得了相应的地址,挂载或提取分区当然是轻而易举的。下面的命令挂载了 vmtest.img7、提取了 vmtest.img5。

root@lj-laptop:/opt/vm# mount -o loop,offset=$(((0x7d047e+0xcbf34+0x3f)*512)) vmtest.img ./part/
root@lj-laptop:/opt/vm# ls ./part/
InFAT32.txt
root@lj-laptop:/opt/vm# umount ./part/
root@lj-laptop:/opt/vm# dd if=vmtest.img of=linux2.img bs=512 skip=$((0x7d047e+0x3f)) count=$((0x65f5b))
417627+0 records in
417627+0 records out
213825024 bytes (214 MB) copied, 4.65149 s, 46.0 MB/s
root@lj-laptop:/opt/vm# mount -o loop ./linux2.img ./part/
root@lj-laptop:/opt/vm# ls ./part/
InLinux2.txt  lost+found
root@lj-laptop:/opt/vm# umount ./part/