
CPIO 文件格式全面详解
CPIO 文件格式全面详解
本文参考 POSIX 规范、GNU cpio / libarchive 官方文档、cpio(5) 手册页
编写而成。文中所有字段布局、magic 值、对齐规则均以公开规范为准,
数据结构以通用 C 语言定义给出。
1. 概述
cpio 是 Unix 上最早期的通用归档程序(archiver)之一,其名称来自 "copy in and out"(复制进/复制出),因为它本质上是一个面向标准输入/标准输出的流式拷贝工具:find | cpio -o 把文件列表打包成归档流,cpio -i 再从归档流中解出文件。
一个 cpio 归档(archive)就是一组 文件条目(entry)的简单串联:
[条目 1][条目 2][条目 3]...[TRAILER!!! 条目]
每个条目由三部分组成:
- 文件头(header):固定长度的元数据块,记录文件名长度、大小、
inode、权限(mode)、属主(uid/gid)、链接数、时间戳、设备号等; - 文件名(name):以
\0(NUL)结尾的字节串; - 文件数据(data):文件的实际内容,可为空(目录、设备节点、
符号链接、硬链接引用等)。
归档以名为 TRAILER!!! 的特殊条目收尾,之后的字节是补零填充(padding),用于对齐到磁带/磁盘的块边界。
cpio 格式最大的特点是:极简、流式、可管道化。它没有 tar 那样的 512 字节固定块结构,头很小(26~110 字节),不要求随机访问,天然适合磁带顺序写入和 管道 | gzip 链式处理。
2. 历史沿革
2.1 起源:1977,AT&T PWB/UNIX
cpio 由 AT&T 贝尔实验室 Unix 支持组的 Dick Haight 于 1977 年编写,随 PWB/UNIX 1.0(Programmer's Workbench,程序员工作台,一个基于第六版 UNIX 的内部发行版)首次发布。它与 find 配套设计:find 负责枚举文件名,cpio 负责把列出的文件复制到别处或打包进归档。
为什么发明 cpio? 当时的核心诉求是把文件备份到 9 轨磁带上:
- 磁带只能顺序读写,无法像磁盘那样随机寻址,因此需要一种
顺序写入、边读边写的流式格式; - 磁带介质昂贵,记录头要尽量小,避免浪费;
- 备份要能保留 Unix 文件系统的特殊文件(设备节点、FIFO、符号链接、
inode、硬链接),这是早期磁带归档格式做不到的。
cpio 于 1981 年随 System III UNIX 首次对外发布。值得注意的是:cpio 比 tar 更早诞生——tar 直到 1979 年(第七版 UNIX)才出现,只是 cpio 早期仅限 AT&T 内部使用,知名度反而不如 tar。
2.2 分裂:二进制格式与字符格式并行
- PWB 二进制格式(bin,magic 070707):最初的格式,所有数字用
2 字节/4 字节原生二进制表示,依赖机器字节序; - 旧字符格式(odc,magic 070707):到 1980 年左右(System III
时期)已出现,数字改用 ASCII 八进制表示,解决了跨机器可移植性问题。
2.3 标准化:POSIX.1-1988
IEEE 1003.1-1988(POSIX.1)把 cpio 的格式正式标准化,即 "portable octet-oriented cpio format",俗称 odc / old character / cpiop。这是历史上唯一进入 POSIX 的 cpio 变体。
2.4 SVR4 改革:newc 与 crc
1989 年,System V Release 4(SVR4) 引入两个新格式:
- newc("new character",magic 070701):字段改为 8 位十六进制,
字段宽度从 18 位跃升到 32 位,支持超过 65536 个 inode 的文件系统,
并拆分 devmajor/devminor; - crc(magic 070702):在 newc 基础上增加 32 位校验和字段,
用于检测介质(磁带)读写错误。
之所以需要改革,是因为旧格式的硬上限(详见第 4、5 节)在磁盘容量快速增长后成为瓶颈:inode 编号超过 262143、单文件超过 2GB 的机器越来越常见。
2.5 2001:被 pax 取代
POSIX.1-2001 中,cpio 与 tar 命令双双被移出标准,取而代之的是能同时读写两者格式的 pax 工具。但 cpio/ustar/pax 三种格式仍由 POSIX.1-2001 明确定义(作为 pax 的交换格式)。今天 pax -w -x cpio 仍可生成 odc 格式归档。
2.6 现代实现
- GNU cpio:主流 cpio 实现。出于历史兼容,
cpio -o的默认输出
格式仍是旧二进制bin(这带来大 inode 编号无法存储的问题,见
Red Hat bug #952313),官方文档明确建议用-H newc/-H crc。
读取时自动探测所有格式,并能自动处理异种字节序的二进制归档。 - libarchive / bsdcpio:BSD 系的现代重实现,默认输出格式为 odc。
- Linux 内核 initramfs:内核初始化根文件系统就是一个
gzip 压缩的 newc 归档——这是 cpio 在当代最重要的应用场景之一。
2.7 演进动因小结
| 时期 | 事件 | 要解决的问题 |
|---|---|---|
| 1977 | cpio 诞生(PWB/UNIX) | 磁带顺序备份;保留特殊文件与 inode |
| ~1980 | odc 出现 | 二进制格式依赖机器字节序,无法跨平台交换 |
| 1988 | POSIX.1-1988 采纳 odc | 命令与格式标准化 |
| 1989 | SVR4 推出 newc | inode/文件大小超过旧格式 18/33 位上限 |
| 1989 | SVR4 推出 crc | 无任何校验,磁带坏块会静默损坏数据 |
| 2001 | POSIX 用 pax 取代 cpio/tar 命令 | 两大格式之争;统一交换格式 |
| 至今 | GNU cpio / libarchive / 内核 initramfs | 仍在固件、系统启动场景广泛使用 |
3. 通用结构(所有格式的公共部分)
3.1 条目模型
+----------------+---------------------------+--------------------+
| header | name (NUL 结尾) | data(可为空) |
| 固定长度 | 长度由 namesize 字段给出 | 长度由 filesize 字段 |
+----------------+---------------------------+--------------------+
- 文件名与文件数据之间、文件数据之后,按格式不同可能有对齐填充(见 3.3);
- 目录(directory)条目的 data 为空;设备节点、FIFO、socket 的 data 也为空;
- 符号链接的 data 存放链接目标路径字符串;
- 硬链接引用条目(newc/crc 下)data 为空,靠 (dev, ino) 关联数据载体。
3.2 mode 字段:文件类型位 + 权限位
mode 是 16 位/32 位字段,高 4 位(掩码 0xF000)表示文件类型,低 12 位(掩码 0x0FFF)是 Unix 权限位。类型位的标准定义如下:
/* 文件类型位(与 <sys/stat.h> 的 S_IF* 同值,此处以十六进制书写) */
#define CPIO_IFMT 0xF000 /* 文件类型掩码 */
#define CPIO_IFREG 0x8000 /* 普通文件 */
#define CPIO_IFDIR 0x4000 /* 目录 */
#define CPIO_IFLNK 0xA000 /* 符号链接 */
#define CPIO_IFCHR 0x2000 /* 字符设备 */
#define CPIO_IFBLK 0x6000 /* 块设备 */
#define CPIO_IFIFO 0x1000 /* 命名管道(FIFO) */
#define CPIO_IFSOCK 0xC000 /* 套接字 */
#define CPIO_PERM 0x0FFF /* 权限位掩码 */
/* 文件类型 = mode & CPIO_IFMT */
| 类型位 | 含义 | 数据内容 |
|---|---|---|
0x8000 |
普通文件 | 文件内容 |
0x4000 |
目录 | 空 |
0xA000 |
符号链接 | 链接目标路径 |
0x2000 |
字符设备节点 | 空(设备号在 rdev 字段) |
0x6000 |
块设备节点 | 空(设备号在 rdev 字段) |
0x1000 |
FIFO 命名管道 | 空 |
0xC000 |
套接字 | 空 |
这是 cpio 相对 tar 的核心优势之一:tar 的传统格式几乎无法表达设备节点、socket 等特殊文件,而 cpio 从一开始就为备份整机(含 /dev)设计,完整保留设备号。
3.3 对齐(padding)规则
| 格式 | 对齐基数 | 说明 |
|---|---|---|
| newc / crc | 4 字节 | 文件名(含 NUL)后、文件数据后各对齐到 4 字节边界 |
| bin(旧二进制) | 2 字节 | 文件名后、文件数据后各对齐到 2 字节边界 |
| odc(旧 ASCII) | 无对齐 | 文件名后紧跟数据,不填充 |
通用的实现方式:
/* 跳过填充:使偏移 (offset + pad) 成为 align 的整数倍 */
static void skip_padding(FILE *fp, long offset, int align)
{
long pad = (align - (offset % align)) % align;
while (pad-- > 0)
fgetc(fp);
}
newc 采用 4 字节对齐是为了让头/数据在主流 32 位机器上自然对齐,加快磁带/磁盘 I/O;odc 不填充则是因为纯文本格式本就逐字节解析,省掉填充能减小归档体积。
3.4 结束标记:TRAILER
归档最后一个条目名为 TRAILER!!!,filesize 为 0。读到它即视为归档结束;其后剩余字节是补零,用于把归档长度填充到块边界(GNU cpio 默认 512 字节块,可通过 --block-size / -B 调整)。很多实现(如 libarchive)还会校验末尾是否为全零。
#define CPIO_TRAILER "TRAILER!!!"
3.5 符号链接
符号链接条目的 data 字段直接存放目标路径(字符串),不存链接自身的 inode 目标。读取时把 data 按字节解码为字符串即为链接目标;写入时把目标路径原样填入 data 即可。
注意:解包符号链接时若不加以控制,可能被用于路径逃逸(详见第 11 节)。
3.6 硬链接
cpio 用 (devmajor, devminor, ino) 三元组标识一组硬链接:同一组的多个条目共享 inode/设备号与 nlink(链接计数),但只有其中一个条目携带文件数据,其余条目 filesize 为 0。
写入时把同一组的非载体条目改写为无数据条目(filesize = 0);读取时按 (dev, ino) 分组,把无数据条目关联到携带数据的载体条目,即可还原硬链接关系,避免重复存储同一份文件内容。
3.7 数字编码的三种形态
| 形态 | 使用格式 | 特点 |
|---|---|---|
| 二进制整数 | bin | 体积最小,但依赖机器字节序 |
| 八进制 ASCII | odc | 可移植,但定长字段位数少(18/33 位) |
| 十六进制 ASCII | newc / crc | 可移植且字段宽度大(32 位) |
字符格式(odc/newc/crc)可统一用 strtol / sscanf 按相应进制解析;二进制格式(bin)读取时先按大端读 magic,若不匹配则整体交换字节序,从而兼容大端/小端机器产出的归档。
4. 旧二进制格式:bin
4.1 历史与动机
bin 是 cpio 的原始格式,随 1977 年 PWB/UNIX 诞生。它把元数据全部编码为原生 2 字节/4 字节整数,体积最小、解析最快,非常适合磁带顺序备份。其 magic 为八进制 070707(十六进制 0x71C7,即 070707 的二进制展开)。
4.2 数据结构(26 字节头)
| 偏移 | 长度 | 字段 | 说明 |
|---|---|---|---|
| 0 | 2 | magic | 0x71C7,按机器字节序存储 |
| 2 | 2 | dev | 设备号(16 位) |
| 4 | 2 | ino | inode 编号(16 位) |
| 6 | 2 | mode | 文件类型 + 权限 |
| 8 | 2 | uid | 属主(16 位) |
| 10 | 2 | gid | 属组(16 位) |
| 12 | 2 | nlink | 链接数(16 位) |
| 14 | 2 | rdev | 特殊文件的设备号 |
| 16 | 4 | mtime | 修改时间(32 位,秒) |
| 20 | 2 | namesize | 文件名长度(含 NUL) |
| 22 | 4 | filesize | 文件数据长度(32 位) |
共 26 字节,之后是文件名,文件名与数据均按 2 字节对齐。
通用的 C 结构体定义(紧凑布局,与磁盘格式完全一致,共 26 字节):
#pragma pack(push, 1) /* 禁止编译器插入对齐填充 */
struct cpio_bin_header {
uint16_t c_magic; /* 070707 */
uint16_t c_dev;
uint16_t c_ino;
uint16_t c_mode;
uint16_t c_uid;
uint16_t c_gid;
uint16_t c_nlink;
uint16_t c_rdev;
uint32_t c_mtime; /* 4 字节 */
uint16_t c_namesize;
uint32_t c_filesize; /* 4 字节 */
}; /* sizeof == 26 */
#pragma pack(pop)
为保证跨机器一致,推荐统一按大端写出:
struct cpio_bin_header hdr = {0};
hdr.c_magic = htons(070707);
hdr.c_dev = htons((uint16_t)(dev & 0xFFFF));
hdr.c_ino = htons((uint16_t)(ino & 0xFFFF));
hdr.c_mode = htons((uint16_t)(mode & 0xFFFF));
hdr.c_uid = htons((uint16_t)(uid & 0xFFFF));
hdr.c_gid = htons((uint16_t)(gid & 0xFFFF));
hdr.c_nlink = htons((uint16_t)(nlink & 0xFFFF));
hdr.c_rdev = htons((uint16_t)(rdev & 0xFFFF));
hdr.c_mtime = htonl((uint32_t)(mtime & 0xFFFFFFFF));
hdr.c_namesize = htons((uint16_t)(strlen(name) + 1));
hdr.c_filesize = htonl((uint32_t)size);
fwrite(&hdr, 1, sizeof(hdr), fp);
fwrite(name, 1, strlen(name) + 1, fp);
/* 随后按 2 字节对齐(见 3.3),再写文件数据 */
读取时自动识别字节序——先按本机字节序读 magic,不匹配则整体交换:
uint16_t magic;
fread(&magic, 2, 1, fp);
int byte_swap = 0;
if (magic != 070707) {
magic = (uint16_t)((magic >> 8) | (magic << 8)); /* 交换字节序 */
byte_swap = 1;
}
if (magic != 070707)
error("not a bin cpio archive");
/* 后续 16 位字段按 byte_swap 决定是否交换 */
4.3 问题与局限性
- 字节序依赖:小端机器(如 VAX、x86)写出的归档与大端机器
(如 68000、SPARC)不兼容。虽然读取端可以通过 magic 试探补偿,
但跨平台交换仍不可靠; - 字段过窄:inode/uid/gid/dev/nlink 都是 16 位,最大 65535;
大磁盘文件系统的 inode 编号很容易超过; - 文件大小上限:32 位 filesize,按有符号解释最大约 2GB
(GNU cpio 标注 bin 单文件上限 2147483647 字节); - 时间戳 32 位:2038 年问题(与所有 32 位 time_t 相同)。
这些限制促使 1980 年代出现 ASCII 字符格式(odc),1990 年代又被 newc 全面取代。如今 bin 只用于读取历史归档,GNU cpio 默认仍输出 bin(出于向后兼容),但官方明确建议改用 newc/crc。
5. 旧 ASCII 格式:odc(POSIX cpiop)
5.1 历史与动机
odc(old character,也称 portable old ASCII)在 System III 时期(约 1980 年)出现,是 cpio 对可移植性问题的第一次回应:把数字全部改为 ASCII 八进制文本,彻底摆脱机器字节序。它是 IEEE 1003.1-1988(POSIX.1)唯一标准化的 cpio 格式(即 cpiop),也是 GNU cpio 的 -H odc、pax 的 -x cpio 所生成的格式。
5.2 数据结构(76 字节头)
| 偏移 | 长度 | 字段 | 说明 |
|---|---|---|---|
| 0 | 6 | magic | 070707 |
| 6 | 6 | dev | 设备号(八进制,18 位) |
| 12 | 6 | ino | inode 编号(18 位) |
| 18 | 6 | mode | 文件类型 + 权限 |
| 24 | 6 | uid | 属主(18 位) |
| 30 | 6 | gid | 属组(18 位) |
| 36 | 6 | nlink | 链接数(18 位) |
| 42 | 6 | rdev | 特殊文件设备号(18 位) |
| 48 | 11 | mtime | 修改时间(八进制,33 位) |
| 59 | 6 | namesize | 文件名长度(含 NUL,18 位) |
| 65 | 11 | filesize | 文件数据长度(八进制,33 位) |
共 76 字节,文件名后无对齐填充,数据紧跟文件名。
各 6 位字段不足时高位截断(dev/rdev 只保留低 18 位),mtime/filesize 为 11 位八进制(33 位)。通用的构造方式:
char hdr[76];
snprintf(hdr, sizeof(hdr),
"070707"
"%06o%06o%06o%06o%06o%06o%06o" /* dev ino mode uid gid nlink rdev */
"%011lo%06o%011lo", /* mtime namesize filesize */
(unsigned)(dev & 0x3FFFF),
(unsigned)(ino & 0x3FFFF),
(unsigned)(mode & 0x3FFFF),
(unsigned)(uid & 0x3FFFF),
(unsigned)(gid & 0x3FFFF),
(unsigned)(nlink & 0x3FFFF),
(unsigned)(rdev & 0x3FFFF),
(unsigned long)(mtime & 0x1FFFFFFFFL),
(unsigned)(strlen(name) + 1),
(unsigned long)(size & 0x1FFFFFFFFL));
fwrite(hdr, 1, sizeof(hdr), fp);
fwrite(name, 1, strlen(name) + 1, fp);
fwrite(data, 1, size, fp); /* odc 无对齐,数据紧跟文件名 */
解析端按同样偏移读取定长字段,用 strtol(..., 8) 或 sscanf(..., "%6o", ...) 逐字段还原。
5.3 问题与局限性(为什么被 newc 取代)
- 18 位字段上限:6 位八进制 = 18 位,dev/ino/uid/gid/nlink/rdev
最大 262143。90 年代磁盘容量暴增后,inode 编号轻松突破; - 33 位文件大小上限:11 位八进制 = 33 位,单文件最大约
8GB(8589934591 字节),当年足够,如今偏小; - 解析成本:每个字段都要做文本→整数的转换,且长度不定
(要 trim 前导零); - 无校验:任何位翻转都会静默产生错误文件(与 bin 相同)。
odc 的优势是完全可移植、纯文本可读、兼容所有 POSIX 实现,因此在需要最大互操作性的场合(如 POSIX pax、跨平台分发)至今仍被使用,GNU cpio 手册将其列为推荐的互操作格式之一。
6. 新 ASCII 格式:newc(SVR4,magic 070701)
6.1 历史与动机
newc(new character,SVR4 便携格式)随 System V Release 4 (1989 年)引入,是 odc 的全面升级,专门解决旧格式的三大硬伤:inode 上限 65535/262143、单文件上限 2/8GB、设备号无法拆分。
改进要点:
- 字段全部改为 8 位十六进制(32 位整数),inode 上限从 26 万
提升到 40 亿(2^32 - 1),足以覆盖现代文件系统; - dev 拆分为 devmajor / devminor,rdev 同理,彻底解决设备号
编码问题; - 4 字节对齐,让头与数据在 32 位机器上自然对齐,加快读写;
- namesize 显式化:文件名长度单独存储,且规定含结尾 NUL,
解析更简单、无歧义; - 数据字段按大端十六进制书写,与平台无关。
6.2 数据结构(110 字节头)
| 偏移 | 长度 | 字段 | 说明 |
|---|---|---|---|
| 0 | 6 | magic | 070701(newc)/ 070702(crc) |
| 6 | 8 | ino | inode 编号(十六进制,32 位) |
| 14 | 8 | mode | 文件类型 + 权限 |
| 22 | 8 | uid | 属主 |
| 30 | 8 | gid | 属组 |
| 38 | 8 | nlink | 链接数 |
| 46 | 8 | mtime | 修改时间(秒) |
| 54 | 8 | filesize | 文件数据长度 |
| 62 | 8 | devmajor | 设备号主部 |
| 70 | 8 | devminor | 设备号次部 |
| 78 | 8 | rdevmajor | 特殊文件设备号主部 |
| 86 | 8 | rdevminor | 特殊文件设备号次部 |
| 94 | 8 | namesize | 文件名长度(含结尾 NUL) |
| 102 | 8 | check | 校验和(crc 格式用,newc 为 0) |
共 110 字节。文件名(含 NUL)后与文件数据后均按 4 字节对齐。
通用的构造方式(逐字段拼接 8 位十六进制字符串,不足补零):
char hdr[110];
snprintf(hdr, sizeof(hdr),
"070701" /* 或 "070702"(crc) */
"%08x%08x%08x%08x%08x" /* ino mode uid gid nlink */
"%08x%08x" /* mtime filesize */
"%08x%08x%08x%08x" /* devmaj devmin rdevmaj rdevmin */
"%08x%08x", /* namesize check */
ino, mode, uid, gid, nlink,
(unsigned)(mtime & 0xFFFFFFFF),
(unsigned)(size & 0xFFFFFFFF),
devmajor, devminor, rdevmajor, rdevminor,
(unsigned)(strlen(name) + 1),
checksum); /* newc 填 0,crc 填校验和 */
fwrite(hdr, 1, sizeof(hdr), fp);
fwrite(name, 1, strlen(name) + 1, fp);
skip_padding(fp, 110 + strlen(name) + 1, 4); /* 4 字节对齐(见 3.3) */
fwrite(data, 1, size, fp);
skip_padding(fp, size, 4);
解析端按同样偏移读取定长字段,用 sscanf(..., "%8x", &v) 逐字段还原。
6.3 限制
- 单文件最大 4GB(filesize 为 32 位,4294967295 字节);
- 无任何完整性校验(这是 crc 格式存在的理由);
- 文件名字段 namesize 为 32 位,实际够用。
6.4 为什么 newc 是当代事实标准
Linux 内核的 initramfs(初始根文件系统)规定使用 newc 格式:内核解压 cpio 归档后直接作为 tmpfs 挂载,而内核的 gen_init_cpio 工具、dracut / mkinitcpio / busybox cpio 均以 newc 为默认。由于固件、嵌入式系统的 rootfs/ramdisk 普遍走 initramfs 链路,newc 已成为嵌入式与固件场景的事实标准格式。
7. 带校验的 newc:crc(magic 070702)
7.1 历史与动机
crc 格式与 newc 同时由 SVR4 引入,唯一区别是把 110 字节头中最后的 check 字段(偏移 102,8 位十六进制)从 0 改为文件数据的校验和,用于检测磁带坏块、网络传输错误等导致的静默数据损坏。GNU cpio 称之为 Sum32。
注意:这个"校验和"不是真正的 CRC(循环冗余校验),而是把文件数据的每个字节直接相加后取 32 位低位的和。它实现极简(一个累加循环),能捕获大多数随机位翻转与整块丢失,但对抗结构性错误的能力弱于 CRC32;名称沿用了"crc"的叫法,纯属历史命名。
7.2 校验算法
写入端对文件数据逐字节求和(32 位溢出自然取模 2^32),写入 check 字段:
uint32_t checksum = 0;
for (size_t i = 0; i < size; i++)
checksum += data[i];
/* crc 格式:check = checksum;newc 格式:check = 0 */
读取端重新求和比对,不一致即判定归档损坏:
uint32_t sum = 0;
for (size_t i = 0; i < size; i++)
sum += data[i];
if (sum != hdr_checksum)
error("crc mismatch: %s", name);
/* 注意:GNU cpio 对符号链接与硬链接条目不做求和校验 */
与 GNU cpio 保持一致的细节:符号链接与硬链接条目跳过校验(它们的 data 是路径字符串而非文件内容,GNU 实现不做求和)。
7.3 何时选 crc vs newc
| 场景 | 推荐 | 理由 |
|---|---|---|
| 内核 initramfs | newc | 内核要求/约定,校验无意义(有 gzip 完整性兜底) |
| 磁带/慢速介质备份 | crc | 能发现介质错误 |
| 网络传输(ftp/管道) | crc | 能发现传输损坏 |
| 最小体积、最大兼容 | newc | 体积更小、实现更广 |
8. HP-UX 变体:hpbin 与 hpodc
8.1 由来
HP-UX 的 cpio 在存储字符/块设备节点时与众不同:它把设备号(major/minor)放在 filesize 字段中,而不是 rdev 字段,且数据为空。GNU cpio 因此单独提供两个格式名:
- hpbin:HP-UX 旧二进制格式(设备文件存储方式不同);
- hpodc:HP-UX 旧 ASCII 格式(设备文件存储方式不同)。
GNU cpio 与 libarchive 等主流实现均以这两个格式名识别并读写。
8.2 读取逻辑
hpodc 读取时,若条目是字符/块设备,从 filesize 字段拆出设备号(高 16 位为 major、低 16 位为 minor),且不读取数据:
if ((mode & 0xF000) == 0x2000 || (mode & 0xF000) == 0x6000) {
/* HP 格式把设备号编码在 filesize 字段中 */
uint16_t dev_major = (uint16_t)((filesize >> 16) & 0xFFFF);
uint16_t dev_minor = (uint16_t)( filesize & 0xFFFF);
data = NULL; /* 无文件数据 */
}
hpbin 的处理方式相同(把设备号编码进 32 位 filesize 字段)。写入端在 HP 格式下反向操作:把设备号编码进 filesize,rdev 字段置零。
这类变体提醒我们:"cpio" 是一个格式家族而非单一格式,实现者必须逐格式处理这些 vendor 差异。
9. cpio 与 tar 的对比
cpio 与 tar 是 Unix 历史上两大归档格式,命运交织:cpio(1977)比 tar(1979)更早诞生,但 tar 因随 7th Edition 一同发布而更广为人知。POSIX 曾长期为"标准里该留谁"争论,最终 2001 年两者都被 pax 取代。核心差异:
| 维度 | cpio | tar |
|---|---|---|
| 诞生 | 1977(PWB/UNIX,Dick Haight) | 1979(7th Edition Unix) |
| 头大小 | 26~110 字节 | 固定 512 字节/块 |
| 块结构 | 无块概念,条目紧凑串联 | 512 字节块,末尾两块全零 |
| 流式/管道 | 天生为管道设计(find | cpio -o) |
可流式,但块结构更重 |
| 特殊文件 | 完整支持设备节点、FIFO、socket | 传统 tar 无法表达设备节点 |
| inode/链接数 | 保留 inode、nlink、硬链接组 | 不保存 inode |
| 设备号 | dev/rdev 字段显式存储 | 无标准字段 |
| 字节序问题 | 仅 bin 格式有(已废弃) | 无(数字为 ASCII) |
| 单文件上限 | bin 2GB / odc 8GB / newc 4GB | v7 8GB / ustar 8GB(扩展可更大) |
| 文件名长度 | newc 支持 32 位长度 | v7 仅 100 字符(ustar 前缀+255) |
| 长文件名/大文件扩展 | 无标准扩展(换格式解决) | ustar 前缀字段、pax 扩展头 |
| POSIX 命运 | 命令在 2001 年移除,格式留作 pax 交换 | 同左 |
| 现状 | 内核 initramfs、固件、嵌入式 | 软件分发、系统备份最普遍 |
cpio 的优势:头小省磁带空间;天然管道化;完整保留设备文件与 inode,适合整机/系统盘备份(/dev、/proc 下的节点);无需预知总大小,可无限追加。
tar 的优势:512 字节块可独立定位、损坏容忍度更高;后来通过 ustar/pax 获得长文件名、大文件(8GB+)、扩展属性(xattr/ACL)等能力,生态与工具链远大于 cpio;POSIX 的 pax 也以 ustar 为默认。
实用经验:
- 打包普通软件、源码分发 → 首选 tar/ustar/pax;
- 打包 initramfs、设备树、固件 rootfs → 用 newc(内核约定);
- 需要跨平台最小依赖 → odc(POSIX 兼容性最好)。
10. 现代应用场景
10.1 Linux 内核 initramfs(最核心)
Linux 内核把初始根文件系统做成 gzip 压缩的 newc cpio 归档:gen_init_cpio 脚本从文件列表生成 newc 归档,usr/ 目录下的 initramfs 在编译期被打进内核镜像;启动时内核解压后直接挂为 tmpfs。dracut、mkinitcpio、busybox initramfs 工具全部围绕 newc 工作。任何需要"引导用最小文件系统"的固件/嵌入式场景都离不开 cpio。
10.2 固件与嵌入式
- Android boot.img:内核 + 压缩的 ramdisk(cpio 归档);
- U-Boot:
bootm加载的 ramdisk 常为 newc/odc cpio; - 路由器/交换机固件:常见
squashfs + cpio组合或纯 cpio rootfs; - UEFI 平台:部分厂商用 cpio 打包诊断镜像与驱动目录。
10.3 系统工具链
- GNU cpio:备份脚本(
find | cpio -o -H crc \| gzip)仍在广泛使用; - libarchive / bsdcpio:Python
tarfile之外的另一互操作选择; - 内核/initramfs 调试:
cpio -itv < initramfs.gz直接列出内容; - POSIX pax:
pax -w -x cpio生成 odc 互操作归档。
10.4 谁在生产中还在"写"cpio?
| 工具/场景 | 默认或常用格式 |
|---|---|
| Linux gen_init_cpio | newc |
| dracut / mkinitcpio / busybox | newc |
GNU cpio -o(默认) |
bin(建议 -H crc) |
bsdcpio -o(默认) |
odc |
pax -x cpio |
odc |
11. 安全注意点
cpio 格式本身极简,但解包时存在与 tar 类似的经典风险,实现库必须处理:
- 路径穿越(Path Traversal):恶意归档的条目名可含
../或绝对路径(/etc/passwd),若直接拼接到目标目录,会覆盖归档外文件。安全实现应拒绝绝对路径与含..段的条目名,或按策略剥离后解包; - 符号链接逃逸:若先解出指向目录外的符号链接,后续条目再通过该链接写入,可绕过路径检查。安全实现要么拒绝链接目标含
..,要么在每次写入前重新校验真实路径; - 设备节点与权限:归档可含设备节点和 setuid 文件,解包时若以特权身份执行,等于给攻击者"制造设备/提升权限"的能力。生产工具默认应跳过设备节点或以非特权身份解包;
- zip-bomb 式资源耗尽:恶意
filesize巨大或条目数量极多,读取端要防止一次性分配超大缓冲,采用流式/分块读取并限制缓冲上限; - 头部畸形输入:
namesize为 0、filesize与对齐填充不符、TRAILER!!!缺失等,健壮实现必须显式校验,非法即停止解析并报错; - 校验和:crc 格式的求和校验能发现传输损坏,但不能防篡改(求和极易被构造为相同值),完整性校验仍需配套 gzip 或签名。
12. 结语
从 1977 年贝尔实验室的磁带备份工具,到 2026 年仍是 Linux 启动链路与固件生态的基石,cpio 用五十年初步显示了一个道理:极简的流式格式拥有最强的生命力。它被 POSIX 移出命令标准,却因内核 initramfs 的约定而"曲线复活";它被 tar 在通用领域超越,却在设备文件、inode 保留、管道化这些 tar 的短板上无可替代。cpio文件格式的演进史,就是"可移植性"与"容量上限"这两个归档格式永恒主题的一部浓缩史。
13. 参考资料
- GNU cpio 手册(
info cpio/man cpio):格式选项与上限说明 cpio(5)手册页:cpio 归档格式规范(字段布局权威参考)- IEEE Std 1003.1-1988 / 1003.1-2001(POSIX):odc 与 pax 交换格式定义
- libarchive Wiki:
FormatCpio(格式历史与变体综述) - libarchive
cpio.5(FreeBSD/Ubuntu man 仓库版:PWB 历史考证) - Linux 内核文档:
Documentation/driver-api/early-userspace/(initramfs 规范)


