我正在为Linux系统准备一个镜像文件。我需要能够运行创建图像的脚本,并使输出每次都逐位相同。
我执行正常的过程,创建一个大的二进制文件,对其进行分区,使用该分区创建一个循环设备,然后创建 I 文件系统。然后我mount
将文件系统复制到系统Linux和初始化程序东西结束,卸载分区,删除循环设备,我就有了我的图像文件。我可以dd
将它复制到磁盘上,并且 linux 系统可以正确启动。所以我正在正确地制作文件系统。
我运行执行上述步骤的脚本,但每次输出都不同。其中一些是时间戳外部2数据结构。我写了一个程序,读取外部2结构并可以清除时间戳,并且tune2fs
可以清除更多内容,但某些位图数据甚至不同,并且文件数据似乎每次都不在同一位置。
那么我该如何创建相同的文件系统呢?
这是我用来创建文件系统、在其上放置文件并卸载它的命令。保存输出并再次运行它,然后比较输出,文件a.txt
被放置在不同的位置。
dd if=/dev/zero bs=1024 count=46112 of=cf.bin
parted cf.bin <<EOF
unit
s
mklabel
msdos
mkpart
p
ext2
63s
45119s
set
1
boot
on
q
EOF
losetup -o $(expr 63 \* 512) /dev/loop0 cf.bin
mke2fs -b 1024 -t ext2 /dev/loop0 22528
#clear some parameters
tune2fs -i 0 /dev/loop0 # interval between check
tune2fs -L LABEL /dev/loop0
tune2fs -U 00000000-0000-0000-0000-000000000000 /dev/loop0 #uuid
tune2fs -c 0 /dev/loop0 #mount count
mount /dev/loop0 mnt
# make a dummy file
echo HELLO > mnt/a.txt
umount mnt
losetup -d /dev/loop0
更新
如果我将上述命令放入脚本中,复制并粘贴它们以第二次运行(但保存其间的输出),甚至在第二次运行命令之前更改日期(使用命令date
),则a.txt被放置在相同的磁盘位置。但是,如果您运行该脚本,保存输出,然后从命令行再次运行它,请比较输出和a.txt位于不同的地点。非常好奇的行为。使用哪些数据来生成文件位置?显然现在还不是时候。我唯一能想到的是通过调用脚本两次来调用命令两次与在同一脚本中运行命令两次之间的区别就像调用进程的进程 ID 一样。有人有想法吗?
更新 #2
我放弃了使用 ext2 的尝试。因此,我无法回答关于 ext2 的最初问题,但我将描述我为获得基本 Linux 系统的完全可重现构建所做的工作。
- 使用 FAT 变体或 ISO9660 代替 ext2。如果您需要小于32MB的分区,则Linux系统分区使用FAT16,否则使用FAT32。 FAT16 或 FAT32 都会将文件重复放置在相同位置。但它的目录条目中确实有一些时间戳。
- 添加启动所需的linux系统文件。
- 编写一个程序来遍历 FAT16/32 文件系统目录结构并将所有时间戳设置为 0。
- 清除mbr 中的磁盘签名。要么在清除时间戳的程序中执行此操作,要么使用 dd。
- 由于它是 FAT 文件系统,因此我使用 syslinux 作为引导加载程序。 cpio 将在每次运行时生成相同的 initrd,因此不会出现任何问题。这就是一个基本的逐位相同的 Linux 系统所需要的全部。
FAT 文件系统的问题
对于只是启动 Linux 系统,FAT 应该不会造成任何问题。但对于较大的数据分区,FAT32 可能会出现一些问题。
- 有可能会遇到目录中文件的最大数量。这不太可能成为问题。 (但当然,就我而言是这样)
- FAT32 将为每个文件存储一个 8.3 文件名。长文件名被缩短为带有波形符和附加数字的词干。但是,如果您有超过 9 个文件映射到同一个短茎,FAT32 会使用未记录的过程来生成某种哈希值以附加到文件名。我深入研究了 FAT32 的 Linux 内核代码,它使用时间作为哈希种子(文件名 i_vfat.c 中的函数
vfat_create_shortname()
)。所以这个字段是不可重现的。我不知道微软的实现是如何实现的。您可能只需清除此字段即可逃脱惩罚,因为我认为 8.3 名称不会用于 DOS 以外的任何用途。或者您可以生成自己可以复制的唯一数字,数字是什么并不重要,只要它们是唯一的即可。
使用 ISO9660 作为附加分区
使用 genisoimage 创建 iso。除了时间戳之外,它将在每次运行中生成相同的输出。使用 -l 选项可以让文件名最多包含 31 个字符。如果您需要比这更长的文件名,请使用 rock ridge 扩展名。命令是
genisoimage -o gfx.iso -R -l -f assets/files/
编写一个程序,遍历 iso9660 文件系统,清除所有时间戳,包括 Rock Ridge 条目的 TF 字段。
- 使用 fdisk 或 parted 在磁盘映像中创建一个分区。 96h 是 ISO9660 的 MBR ID 号。
- 如有必要,请修补分区表。 Parted 不支持创建 iso9660 类型的分区。不幸的是,我一直使用旧版本的parted 和fdisk,而且parted 更容易使用。所以我使用parted将我的第二个分区设置为fat32。然后使用fdisk将类型更改为96。
使用 dd 将 iso 嵌入磁盘映像中,使用的编号与创建分区时使用的编号相同。我用了
dd bs=512 seek=$part2_start_lba conv=notrunc if=gfx.iso of=cf.bin
其中 cf.bin 是我的磁盘映像文件。 6. Linux启动后挂载iso分区。如果 iso 是第二个分区,则为 /dev/sda2。您可能必须首先使用 mknod 在 /dev 中创建正确的设备文件。
答案1
恕我直言,这一切似乎都变得过于复杂。什么时候柏油单独看来是显而易见的解决方案。 tar 可以创建几乎任何文件系统,包括 cdfs(--options cd9660:*)。它还允许您将输出文件的时间戳记为最近的-m || --modification-time
、、、、...中的任何一个--gid id || --gname name
。 --acls || --no-acls
--same-owner || --no-same-owner
或者你可以创建你的文件系统。chown -Rh someone:somegroup .
在您的文件树中执行 a ,chmod
根据您的喜好并使用tar
, 或同步将文件树放入您准备好的文件系统中。然后一切都会保持一致——相同的日期、相同的所有者/组和权限。
嗯,这就是我处理类似事情的方式。 :)
华泰
答案2
您原来问题的答案是一个名为genext2fs。如果您提供 -f 开关,那么在给定相同的输入的情况下,它将创建逐位相同的输出。这可以通过它自己的测试套件来证明,该测试套件将创建的图像与正确输出的预先计算的 md5sum 进行比较,或者通过此测试(在源目录内执行)来证明:
$ ./genext2fs -f -B 1024 -b 40 -d m4 rootfs.img
$ md5sum rootfs.img
322053a8962acc599eaabb2dfde28783 rootfs.img
$ rm rootfs.img
$ ./genext2fs -f -B 1024 -b 40 -d m4 rootfs.img
$ md5sum rootfs.img
322053a8962acc599eaabb2dfde28783 rootfs.img
您可以挂载生成的映像并检查其内容是否确实与打包目录相同:
$ sudo mount rootfs.img /mnt
$ ls -lha /mnt
total 27K
drwxr-xr-x 3 root root 1.0K Jan 1 1970 .
drwxr-xr-x 25 root root 4.0K Mar 22 14:56 ..
-rw-r--r-- 1 josch josch 1.5K Mar 22 23:05 ac_func_scanf_can_malloc.m4
-rw-r--r-- 1 josch josch 2.4K Mar 22 14:24 ac_func_snprintf.m4
drwx------ 2 root root 16K Jan 1 1970 lost+found
$ rmdir /mnt/lost+found
$ diff -rq m4 /mnt
$ echo $?
0
玩得开心!
答案3
笔记:这不是完整的答案;只是部分,或者至少是一个提示
我需要能够运行创建图像的脚本,并使输出每次都逐位相同。
您必须实现的第一个问题是分区表disk signatures
中的msdos
(分区表中的偏移量 440)膜生物反应器,4字节长)。如果您的 MBR 不同,则您仅在第一个扇区就未能实现目标。每次mklabel
在里面执行时parted
,都会生成一个新的disk signature
.您可以克服这个问题,用相同的随机签名覆盖这四个字节,如下所示:
printf RAMDOM_SIGNATURE | xxd -p -r | dd bs=1 count=4 seek=440 of=YOUR_DOT_BIN conv=notrunc 2> /dev/null
RANDOM_SIGNATURE
可能是这样的'73396992'
我对你的脚本做了一些修改,修复了以下问题:
dd if=/dev/zero bs=1024 count=46112 of="$1"
parted "$1" <<EOF
unit
s
mklabel
msdos
mkpart
p
ext2
63s
45119s
set
1
boot
on
q
EOF
printf "$2" | xxd -p -r | dd bs=1 count=4 seek=440 of="$1" conv=notrunc 2> /dev/null
losetup -o $(expr 63 \* 512) /dev/loop0 "$1"
mke2fs -b 1024 -t ext2 /dev/loop0 22528
#clear some parameters
tune2fs -i 0 /dev/loop0 # interval between check
tune2fs -L LABEL /dev/loop0
tune2fs -U 00000000-0000-0000-0000-000000000000 /dev/loop0 #uuid
tune2fs -c 0 /dev/loop0 #mount count
#mount /dev/loop0 mnt
## make a dummy file
#echo HELLO > mnt/a.txt
#umount mnt
losetup -d /dev/loop0
现在,您可以像这样调用脚本
./script_name BIN_FILE_NAME RANDOM_SIGNATURE
现在,如果你这样做:
./test.sh cf00.bin '73396992'
./test.sh cf01.bin '73396992'
./test.sh cf02.bin '73396992'
./test.sh cf03.bin '73396992'
然后是这个:
dd if=cf00.bin count=63 2>/dev/null | sha1sum
dd if=cf01.bin count=63 2>/dev/null | sha1sum
dd if=cf02.bin count=63 2>/dev/null | sha1sum
dd if=cf03.bin count=63 2>/dev/null | sha1sum
您将看到所有这些文件在第一个分区中的文件系统之前都是相同的(尝试与原始脚本相同,并且总和将彼此不同)。
您可能会注意到,在我的脚本版本中,我注释掉了写入文件的行a.txt
。我这样做是因为当你无法使文件系统相同时,即使文件系统上没有文件,尝试修复这个问题也是没有意义的。情况就是这样:即使没有文件,文件系统也不同,所以,首先,我们需要修复这个问题。
如果您dumpe2fs
针对每个映像上的文件系统分区运行,将其转储到文件,然后diff
针对任何一对转储使用,您将看到如下内容:
25c25
< Filesystem created: Sat Jun 15 07:37:32 2019
---
> Filesystem created: Sat Jun 15 07:37:40 2019
27c27
< Last write time: Sat Jun 15 07:37:33 2019
---
> Last write time: Sat Jun 15 07:37:40 2019
30c30
< Last checked: Sat Jun 15 07:37:32 2019
---
> Last checked: Sat Jun 15 07:37:40 2019
37c37
< Directory Hash Seed: 603130ae-82de-4530-9772-f68ae3d6df5f
---
> Directory Hash Seed: 1d9c5af8-a48e-4221-9e70-8fa2ccc6936f
所以,至少,在一个非常高的水平(在此之后,您需要更深入地了解最低级别,即:实际的逐字节比较)文件系统的不同之处在于上面显示的细节。首先解决这个问题。
即使您更改机器中的日期,您也无法成功篡改时间戳并使它们相等,因为在程序执行中存在您可以控制的时间间隙。在这种情况下,您需要冻结至少从创建文件系统的程序的角度来看是你的时钟。你可以深入研究这一点,但我认为这不是正确的方法,因为你说他们需要在他们的机器上执行你的脚本:你不想弄乱他们的时钟。所以,恕我直言,要走的路可能是篡改文件系统上的正确字节,就像我对disk signature
.周围搜索一下。
另外,别忘了Superblock backups
……追踪他们。如果它们在每个文件系统上包含不同的数据,它们将传播它们驻留的字节范围的差异。
最后,请记住,当您复制文件时,您无法直接控制文件系统内文件字节的“分布”...如果您无法克隆,则需要找到一种方法来控制它也。
答案4
你也可以尝试e2image
:
e2image 程序会将设备上的关键 ext2、ext3 或 ext4 文件系统元数据保存到文件中。
尽管默认情况下e2image
仅保存元数据,但它也可以通过使用-a
选项来保存数据。 (请参阅链接的“包括数据”部分)
可以指定 -a 选项以包含所有数据。这将提供一个适合用于克隆整个 FS 或用于备份目的的映像。请注意,此选项仅适用于原始或 QCOW2 格式。
QCOW2 格式比原始格式更适合备份。 QCOW2 映像是一个普通文件,其大小接近要备份的分区的已用空间。原始图像是一个具有所有含义的稀疏文件 - 不专门处理稀疏文件的工具不仅会处理已用空间,还会处理可用空间。
因此,用法示例是:
- 备份
sda1
分区,包括文件和元数据到文件boot-part.qcow2
:
sudo mount -o remount,ro /dev/sda1
sudo e2image -a -Q /dev/sda1 boot-part.qcow2
请注意,需要重新挂载为只读,以确保在备份过程中不会有人写入该分区。毕竟,sudo mount -o remount,rw /dev/sda1
如果需要,您可以以读写方式重新安装。
/dev/sda1
从 QCOW2 映像文件恢复分区boot-part.qcow2
:
sudo umount /dev/sda1
sudo e2image -r boot-part.qcow2 /dev/sda1
请注意,这umount
是必须的,因为您无法重写已安装分区的超级块。如果/dev/sda1
当时没有挂载,可以跳过这一步。 :)
还值得注意的是,在恢复场景中,大小/dev/sda1
应等于或大于映像文件内的分区,否则您将收到错误:e2image: Invalid argument while trying to convert qcow2 image (boot-part.qcow2) into raw image (/dev/sda1)
。