本文完整阅读约需 16 分钟,如时间较长请考虑收藏后慢慢阅读~

笔者最近使用 DosBox 安装了 Windows 98,以便运行一些只兼容 Win9x 环境的软件,但在尝试拷贝文件到 Windows 98 时,却遇到了一个难题:如何才能挂载虚拟磁盘文件呢?按照 Windows 的使用习惯,自然是需要寻找一款专用的磁盘挂载软件,或者使用 DiskGenius 等磁盘分区软件读取磁盘内容。但好在我们有 Unix 哲学指引下的 Linux,无需任何第三方软件就可以挂载磁盘!

0x01 发现问题

DosBox 提供了 IMGMAKE 命令用于新建虚拟磁盘,该文件被称作 Raw disk image,即将一个硬盘的所有扇区使用文件形式存储。不像 VHD、VMDK 等商业虚拟磁盘格式存在复杂的数据结构,Raw disk image 十分灵活,因此可以用来描述所有存储媒介(硬盘、光盘、软盘等)。下图是一个 4GiB 大小 .img 文件的信息:

查看 Wikipedia 关于 .img 格式的描述 IMG (file format) – Wikipedia,提到了各种用于读写此文件的工具,但大部分的工具都是用于读写软盘格式或其他专有磁盘格式:

能找到支持 .img 格式硬盘的软件,大部分都是数据恢复类软件,价格同样不菲:https://getdataforensics.com/product/mount-image-pro/

300 美元的天价足够买两份 Windows10 正版拷贝,这钱实在花得不值,那么有没有免费的办法来实现这个需求呢?

0x02 分析问题

Windows 98 安装时会对磁盘进行初始化操作,初始化或格式化的本质是建立分区表,标记硬盘中某块扇区范围属于哪种类型的分区,以供操作系统读写。

和 Windows 下不同的是,Linux 的磁盘本身就是一种文件而非一种特殊的系统资源,以挂载磁盘为例:

mount /dev/sdb1 /mnt/data

这里 /dev/sdb1 可以理解为一个文件形式的分区,既然 mount 能接受一个文件作为设备,那能不能接受 .img 磁盘文件作为设备呢?

sudo mkdir /mnt/win98
sudo mount /mnt/data/Win98.img /mnt/win98

报错提示文件系统类型损坏,即无法识别该 .img 文件对应的文件系统,实际上使用任意一个非磁盘文件如 .mp3 文件挂载,也会出现类似的错误。这是为什么呢?

这里我们需要区分磁盘和分区,/dev/sdb1 可以理解为 /dev/sdb 下被识别出的第一个分区,因此我们上面的操作就等同于直接挂载 /dev/sdb,挂载的是磁盘而非分区,自然会失败。

Windows 98 会使用经典的 MBR 分区表对磁盘进行初始化,并默认建立唯一的一个 FAT32 分区,老狼的这篇文章解释了其具体结构:MBR与GPT – 知乎。如图所示:

如果我们能找到这个分区开始的位置,就能读取到这个分区的所有信息,包括结束位置。但分区开始的位置在哪里呢?

查询维基百科,我们可以发现,Windows 使用 MBR 分区表初始化磁盘的第一个分区位置为第 63 扇区,而每个扇区的大小默认为 512 Byte,即从磁盘最开始,需要跳过 512 * 63 = 32256 Byte,即可读取到第一个分区的信息。

0x03 解决问题

问题来了,我们如何才能挂载这个磁盘文件的一部分呢?

最简单的办法当然是使用 dd

dd if=Win98.img of=Win98_C.img bs=1 skip=32256

但由于大部分磁盘读写的最小单元都是 512 字节,而这里是一个一个字节写入,该方法会导致磁盘写入速度极慢:

既然我们已知虚拟磁盘一个扇区大小为 512 Byte,也知道第一个分区开始位置是 63 扇区,其实可以直接修改参数为:

dd if=Win98.img of=Win98_C.img bs=512 skip=63

速度快了许多。

通过上面的命令,我们从磁盘文件中获取到了分区文件,接下来再使用 mount 命令,就能成功挂载文件!

sudo mount /mnt/data/Win98_C.img /mnt/win98

对分区中的文件做完修改后,我们可以将磁盘的 0~62 扇区与修改结束后的文件进行组合:

dd if=Win98.img of=Win98_header.img bs=512 count=63
cat ./Win98_C.img >> Win98_header.img
mv Win98_header.img Win98.img

对分割后合并的文件进行 MD5 验证,可以证明该方法不会破坏分区表:

0x04 举一反三

1. 获取磁盘分区表信息

上文所述适用于大部分使用 MBR 分区表的磁盘 + Linux 支持的文件系统,但如果我们要挂载的分区起始扇区不是 63,或者扇区大小根本不是 512 Byte,又或是磁盘采用新的 GPT 分区表呢?

此时,我们可以使用更高级的工具来对磁盘进行分析,笔者采用 fdisk (GPT fdisk 同理) 进行操作:

fdisk -l /mnt/data/Win98.img

可以发现 fdisk 显示出了这个磁盘的所有信息:

GPT fdisk 则会输出如下所示的信息(笔者暂时没有 GPT 格式的 .img 文件,使用物理磁盘代替):

以 fdisk 为例,我们可以看到这个磁盘的分区信息,其第一个分区的起始扇区为 63,而扇区大小为 512,这和我们从 Wikipedia 中了解到的知识一致。

2. 就地挂载,无需拷贝

上面介绍的方法虽然直观,且符合 Unix 哲学(使用已知的工具,在不了解操作系统细节实现情况下完成工作),但还存在一个问题:我们把一个分区剥离了出来,目的是方便从外部拷贝文件进去,但拷贝结束后,我们还得把这个分区和 0~62 扇区组合,这个过程依旧繁杂。

解决这个问题的线索,其实在上文已有体现。如果读者没有发觉,请容许笔者再复制一段文本:

mount: /mnt/win98: 文件系统类型错误、选项错误、/dev/loop0 上有坏超级块、缺少代码页或帮助程序或其他错误.
dmesg(1) may have more information after failed mount system call.

注意这里提到的 /dev/loop0 设备,我们指定的磁盘明明是个文件,为什么不显示文件名,而显示一个我们没有指定的设备呢?这就不得不提到 Linux 的 loop 设备了。

根据 loop(4) 的文档 loop(4) – Linux manual page,其中提到:

The loop device is a block device that maps its data blocks not
to a physical device such as a hard disk or optical disk drive,
but to the blocks of a regular file in a filesystem or to another
block device. This can be useful for example to provide a block
device for a filesystem image stored in a file, so that it can be
mounted with the mount(8) command.

也就是说,在 Linux 中,我们理解的 Unix 哲学,本质上是这些接受真正文件作为磁盘设备的命令封装了一层。以 mount 命令为例,既然 mount 命令封装了 loop 设备的细节,就应该会考虑到挂载设备中的文件系统需要 offset 这一事实。

果不其然,查询 mount 命令的文档 mount(8) – Linux manual page,我们发现了最简单挂载虚拟磁盘分区的方法:

This type of mount knows about three options, namely loop, offset
and sizelimit, that are really options to losetup(8). (These
options can be used in addition to those specific to the
filesystem type.)

mount 命令对于 loop 设备,接受三个参数:loopoffsetsizelimit,这里我们需要的是 offset 参数:

sudo mount -o offset=32256 /mnt/data/Win98.img /mnt/win98

可以看到,通过这样的办法就可以实现就地挂载,无需分割与合并文件~