返回资讯中心
外部精选
软件工程
#开发者实时流

ZIP 自启动归档的制作方法

来源标题:Creating a self booting ZIP archive

文章解释如何创建一个能自启动的 ZIP 归档。ZIP 从尾部读取、文件头支持 64k 额外数据、尾部支持 64k 注释,这些特性让在归档前附加其他内容成为可能,理论上归档器不应关心文件之间的间隙。文中还指出尾部结构存在一处设计缺陷。

Hacker News 最新gdgghhhhh2026/11/14 · 今天27 分钟阅读

以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。

最后这一点在ZIP归档还要分装到多张软盘上的年代尤为重要。要访问某个文件,只需扫描最后一张软盘上的目录,然后告诉用户该插入哪张软盘来取文件。

相比之下,访问tarball中的文件需要线性扫描整个归档(反过来,这对磁带机反而更友好,TAR正是源自磁带机)。

ZIP归档本应从后往前读:footer指向中央目录,中央目录指向各个文件。理论上,归档器不该在意两个文件之间是否恰好存在空隙、里面装着别的东西,也不该在意归档前面是否有别的内容,比如另一种文件格式的文件头。 本地文件头还支持在头部与文件数据之间塞入最多64k的“额外”内容,footer末尾则有“注释”字段,同样最多64k。

如果你仔细看一眼footer,可能会发现一个有趣的设计缺陷:

typedef struct { uint32_t magic; uint16_t disknum; uint16_t diskstart; uint16_t num_dir_entries; uint16_t total_dir_entries; uint32_t dir_size; uint32_t dir_start; uint16_t comment_length; uint8_t comment[comment_length]; } attribute((packed)) zip_end_t;

它是变长的,归档器需要从后往前找到它,但magic和长度都存储在变长注释字段之前。不妨想一想:你会怎么在归档器里实现这个?要求注释为空?读取最后64k再加一点并扫描?从哪个方向扫?如果footer的注释字段里又藏着另一个合法的footer呢?取决于实现如何处理这种情况,一个精心构造的归档在不同归档器下可能看起来包含不同的文件。

试图让格式具备可扩展性,结果却埋下了一大堆隐患。而这还没算上多年来附加到官方规范上的大量扩展。

许多现代实现为了省去一堆麻烦,干脆把ZIP归档当作tarball来对待,改从前往后线性扫描。它们期望归档以本地文件头开头,按大小跳过之后必定到达下一个条目,或者最终到达中央目录。凡是不满足这一点的归档一律拒绝。这确实有效阻止了大量多格式混装的把戏,但代价是可能合法的归档也会变得无法读取。

因此,对于自启动ZIP归档,挑战就变成了:归档必须以本地文件头开头,不能有任何空隙,所有内容都必须妥善地包含在文件内部。

任何看过DIY玩具操作系统教程的人,大概都碰到过Multiboot规范——这可能是让程序在裸机x86上运行的最简单、对初学者最友好的方式。

Multiboot规范源自GNU/FSF生态,由GRUB或Qemu直接内核启动等引导加载程序实现。SYSLINUX通过插件支持它。还有一个设计过度复杂的第二版草案,同时支持MIPS,但这与我们此处无关。

符合Multiboot 1的内核只需在前8k内某处包含魔数0x1BADB002。其后是flags和一个校验和(三者相加必须为零)。根据flags的不同,后面可能还有额外字段。

内核本身可以是ELF文件,加载器从中提取加载地址和入口点。内核以32位保护模式运行,使用平坦段(base=0,limit=4G),并启用A20门。

除了普通的老式ELF文件,内核还可以手动指定内存布局(通过一个flag指示),从而允许加载flat binary或其他文件格式。扩展头如下所示:

typedef struct { uint32_t magic; uint32_t flags; uint32_t checksum; uint32_t hdr_addr; uint32_t load_addr; uint32_t load_end_addr; uint32_t bss_end_addr; uint32_t entry_point; } mb_header_t;

hdr_addr指定文件加载后头部在内存中的地址,利用它以及找到头部时的文件偏移,加载器可以把其他地址换算回文件偏移。

这样,load_addr和load_end_addr就用来确定文件的哪些部分要加载到内存的什么位置。如果bss_end_addr在其之后,加载器会将其清零,最后跳转到entry_point运行内核。

符合Multiboot的加载器还会建立一个包含系统信息(如内存映射、APM表)、内核命令行等的结构体,并把指针传给内核(通过寄存器EBX,EAX中为魔数0x2BADB002)。

制作一个可Multiboot启动的ZIP归档很简单。只需把一个带Multiboot头的flat binary作为未压缩文件打包进去,确保它位于开头且地址对齐。然后外部工具可以修正头部,使整个归档被加载。

对于任何实际应用来说,到此为止才是合理的,但我们在这里追求的不是“实际”或“理智”。

在IBM兼容机上,BIOS会遍历已安装的驱动器,从每个驱动器加载第一个扇区(到地址0x7c00),直到找到一个第一个扇区以0xaa55结尾的驱动器,然后跳转到第一个字节。

用文件系统格式化的磁盘,通常开头有一个头部,以一条跳过头部进入后续引导代码的跳转指令开始。DOS MBR格式则选择把分区表放在第一个扇区的末尾,所有代码放在前面。

如果我们把BIOS启动签名塞进ZIP归档,至少ZIP本地文件头的部分内容会被执行,而开头几个字节是规定的魔数,我们不能直接在那里放跳转指令。

(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)

正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。

打开原文
去社区讨论 →