简介:Extract-xiso 是一套面向 Xbox 游戏光盘映像备份与制作需求的开源工具源码包,适合希望研究 ISO 提取/生成流程、跨平台 C 程序构建以及 FTP 传输实现的开发者与爱好者。压缩包共 66 个文件,以 44 个 .c 源文件和 8 个 .h 头文件为主体,辅以 Makefile 构建脚本、README/LICENSE 说明文档、man 手册、Win32 兼容代码及 Visual C++ 工程文件,整体约 203KB,体量轻便却覆盖核心实现与多平台适配线索。其代码围绕 Xbox ISO 的创建与提取展开,并包含修改版 libftp 模块,便于读者理解镜像处理、网络传输与条件编译的组织方式。目前已有 504 人学习下载,适合作为研读开源备份工具、复现编译流程和二次开发的基础材料。
1. 一个只有 4KB 的“空镜像”
某开发者接手一批老旧归档盘,把扩展名改成 .iso 后双击挂载,里面只有一个几 KB 的说明文件;换几个解压软件,要么报“不支持的卷描述符”,要么干脆把目录树读成乱码。文件没坏,坏在预期——XISO 用的是 XDVDFS,一种以 ISO9660 为骨架、但目录结构和时间戳都改过的私有文件系统。extract-xiso 就是为它写的开源软件:一个 C 实现的小型命令行工具,能列目录、提取、从散装目录反向打包,也能把已有镜像重写一遍去掉冗余填充。它不负责模拟器、不负责转换格式,只把 XISO 这层壳拆开或装上。手上有一批镜像要归档、要核对内容、要压体积的人,基本绕不开它。
2. XDVDFS 拆解:XISO 与普通 ISO9660 差在哪三个地方
动手之前先把格式看明白,后面所有“为什么报错”“为什么提取出来少了文件”才有地方对号入座。XISO 常被当成“改了个后缀的 ISO”,实际上它只借了 ISO9660 的 2048 字节扇区和整体分层思路,卷描述符、目录记录、时间戳三处全是自己的一套。搞不清这三处,用通用工具去读就只能碰到运气好的镜像。
2.1 卷描述符不在扇区 16
标准 ISO9660 的主卷描述符固定在扇区 16,偏移 32768 字节,开头是CD001这个魔数。XDVDFS 把自己的卷描述符放在更靠后的位置,并且用一套独立的结构描述根目录的起始扇区和目录表大小。这意味着任何按“跳到 0x8000 找 CD001”这条硬规则实现的解析器,在 XISO 上第一步就会失败——不是它写得烂,是它压根没打算认这个格式。
卷描述符里最关键的两个字段是根目录记录的起始扇区和长度。拿到这两个值,才能定位到目录表;目录表本身又是一棵按名字排序的平衡二叉树,不是一个平铺的数组。很多人第一次用十六进制编辑器翻 XISO,看到一堆不像文件名的字节就放弃了,问题就出在这里:目录项之间不是等距排列的,得顺着指针走。
提示:判断一个镜像到底是标准 ISO9660 还是 XDVDFS,别靠扩展名,先看工具能不能列出
default.xbe。Xbox 可执行入口文件名是固定的,能列出来基本就是对的格式。
2.2 目录记录是变长的,靠左右子树指针串联
XDVDFS 的目录记录头部是 14 字节:2 字节左子树偏移、2 字节右子树偏移、4 字节起始扇区、4 字节文件长度、1 字节属性、1 字节文件名长度,后面紧跟变长的文件名字节,再按 4 字节对齐补零。两个子树偏移是相对整个目录表起点的相对量,不是绝对扇区号。名字比较按字节序做,插入时维持平衡,所以一个目录下的项在物理上可能是乱序的,逻辑顺序靠指针还原。
属性字节里,目录位置位表示这是个子目录,其余位描述只读等属性。文件名长度是单字节,意味着单段名字有上限;文件名本身保留原始大小写,不像 ISO9660 Level 1 那样强制转大写。这一条看着不起眼,实际影响很大:同一个文件在镜像里可能叫Default.xbe,也可能叫default.xbe,提取到大小写敏感的文件系统上就是两个不同的东西。
时间戳也不是 ISO9660 那套 7 字节的年月日时分秒加时区偏移,而是 64 位的 Windows FILETIME,从 1601 年起算、单位 100 纳秒。用通用工具提取出来的文件修改时间经常是 1970 或者 1980,就是因为解析器读错了字段宽度,把 8 字节当成了别的长度,后面整条记录全错位。
2.3 为什么通用工具读不全:三条路线的取舍
遇到 XISO,能选的路其实只有三条,各有各的边界。
| 路线 | 典型做法 | 能拿到什么 | 边界 |
|---|---|---|---|
| 通用解压/挂载 | 直接挂载或解压 .iso | 格式凑巧兼容时能读出部分文件 | 不认 XDVDFS 卷描述符,失败率高;即使能读,时间戳和大小写也常丢 |
| 虚拟光驱类工具 | 挂成盘符再拷贝 | 目录树完整,可交互浏览 | 依赖驱动,批量和脚本化困难;镜像本身有问题时报错信息很模糊 |
| extract-xiso | 命令行-l/-x/-c/-r | 目录列表、完整提取、反向打包、重写 | 只处理 game partition,不含光盘的视频分区;不做格式转换 |
选 extract-xiso 的核心理由不是“它最强”,而是“它把读写两头都做了,而且能进脚本”。要归档几百个镜像、要每次提取后做哈希比对、要在 CI 里跑一遍完整性检查,图形工具挨个点是不现实的。命令行工具的价值在于它的输出是可解析的、退出码是可判断的,这两点决定了它能不能被塞进流水线。
还要说清一个容易误解的点:Xbox 光盘物理上是双层 DVD,包含视频分区和游戏分区,而常见的 XISO 镜像通常只保留游戏分区,也就是 XDVDFS 那一段。所以一个“完整光盘”和一个“能用的 XISO”在体积上本来就对不上,拿两者比大小没有意义。搞清这一点,后面看到重建后的镜像小了一截才不会慌。
3. 从源码到第一条命令:编译 extract-xiso 并跑通列表与提取
这类小工具最大的好处是依赖少,一个 C 编译器加标准库基本就够,不需要装一堆运行时。但它年代偏早,构建脚本有两套并存的情况,直接make之前最好先看一眼目录里到底有哪个构建文件,能省掉一轮莫名其妙的报错。
3.1 依赖检查与源码目录结构
拿到源码包后先解压,看根目录有什么。常见做法是先确认三件事:有没有 Makefile、有没有 CMakeLists.txt、有没有 README 里写的额外依赖。绝大多数情况下它只依赖 libc,个别平台上的构建脚本会用到 zlib 之类的东西,但核心的读写逻辑是自包含的。
# 解压并进入源码目录(文件名以实际下载到的包为准) tar -xzf extract-xiso-*.tar.gz cd extract-xiso-* # 先看清有哪些构建入口,别上来就 make ls -1 Makefile CMakeLists.txt README* 2>/dev/null # 编译器在不在 cc --version || gcc --version这段的意义在于把“构建方式不确定”这个变量提前消掉。ls那条把三种可能的文件一次列出来,有哪个走哪条路;cc --version是确认工具链就位,很多编译失败其实是环境里根本没有编译器,报错信息却指向源码,白查半小时。
3.2 两条编译路径:Makefile 与 CMake
如果目录里有 Makefile,最省事:
# 路径 A:传统 Makefile make # 默认目标一般就是 release 构建 sudo make install # 装到 /usr/local/bin,不加 sudo 就装到当前目录 extract-xiso -h # 确认命令可用,顺便看一眼这版支持哪些参数make不带参数会走默认目标,多数项目的默认目标就是优化过的构建;make install的前缀可以用PREFIX=/usr/local覆盖,不想污染系统目录就省略这一步,直接在当前目录用./extract-xiso调。装完先跑-h,因为不同分支的参数集合略有差异,后面写脚本要按实际输出对齐。
没有 Makefile 或者要在 Windows 上构建,走 CMake:
# 路径 B:CMake,适合 Windows / macOS / 需要指定生成器的场景 cmake -S . -B build -DCMAKE_BUILD_TYPE=Release cmake --build build --config Release -j4 # 产物一般在 build/ 下,Windows 是 build/Release/extract-xiso.exe ./build/extract-xiso -h-S . -B build是源码目录与构建目录分离,好处是编译产物不弄脏源码树,删掉 build 目录就等于清理干净。-DCMAKE_BUILD_TYPE=Release打开优化并去掉调试断言,处理几百 MB 的镜像时差别很明显;多配置生成器(比如 Visual Studio)下这个变量不生效,要在--build时用--config Release指定,两条都写上是为了跨平台不折腾。-j4是并行编译,小项目收益有限,但习惯留着没坏处。
Windows 上如果用的是 MSVC,编译出来的 exe 直接双击会一闪而过,因为它是控制台程序且没有交互输入。正确做法是开一个终端窗口,cd 到镜像所在目录再调,或者把路径写成绝对路径。这个坑几乎每个第一次在 Windows 上用命令行工具的人都会踩一次。
3.3 列表与提取:先看再动
拿到工具后第一条命令不要急着提取,先列表。列表是只读操作,不会往磁盘写任何东西,用它确认三件事:镜像能不能被正确解析、根目录下有没有default.xbe、目录规模大概多大。
# 只读:列出镜像内的完整目录树 extract-xiso -l ./backup-01.iso # 输出比较长时接分页或过滤 extract-xiso -l ./backup-01.iso | less extract-xiso -l ./backup-01.iso | grep -i '\.xbe$'-l走的是只读路径,解析卷描述符、遍历目录树、按层级打印。如果这一步就报错,后面提取必然失败,问题出在镜像本身或者格式判断上,跟提取参数无关。接grep过滤.xbe是个实用习惯:Xbox 可执行文件不止default.xbe一个,很多镜像里还有若干附加模块,列出来能快速判断这个镜像是不是完整归档。
确认无误后再提取:
# 提取到当前目录下与镜像同名的文件夹 extract-xiso -x ./backup-01.iso # 指定输出目录,适合批量归档时统一收纳 extract-xiso -x -d /data/archive/backup-01 ./backup-01.iso # 静默模式,脚本里用,只保留错误输出 extract-xiso -x -q -d /data/archive/backup-01 ./backup-01.iso-x是提取动作,-d指定落盘目录,目录不存在时工具一般会自己建。-q关掉进度类输出,批量跑的时候日志会干净很多,出问题只看 stderr 就够。要注意-d给的是“父目录”还是“目标目录”在不同版本上偶有差异,第一次用先拿一个小镜像试,确认落盘层级符合预期,再放进批处理。
提取完成后立刻做一件事:核对根目录。default.xbe在不在、目录层级是不是多套了一层、文件名大小写和-l的输出是否一致。这三项里任何一项不对,说明要么镜像本身有问题,要么提取参数给错了目录。
3.4 批量提取脚本:把几十个镜像压成一条命令
单个镜像手工敲没问题,一旦上到几十个就必须脚本化。写批量脚本最容易翻车的地方是文件名里的空格和特殊字符,用for f in $(ls)这种写法,遇到带空格的路径直接把一个文件拆成两个参数,工具报“文件不存在”,你还以为是镜像坏了。
#!/usr/bin/env bash # batch-extract.sh:批量提取目录下的 XISO 镜像 set -euo pipefail SRC_DIR="${1:?用法: $0 <镜像目录> <输出目录>}" OUT_DIR="${2:?用法: $0 <镜像目录> <输出目录>}" mkdir -p "$OUT_DIR" ok=0; fail=0 # -print0 + read -d '' 组合,正确处理带空格的路径 find "$SRC_DIR" -maxdepth 1 -type f -iname '*.iso' -print0 | while IFS= read -r -d '' img; do name="$(basename "$img" .iso)" target="$OUT_DIR/$name" printf '[*] %s -> %s\n' "$name" "$target" # 失败不中断整批,只记录并继续 if extract-xiso -x -q -d "$target" "$img"; then ok=$((ok + 1)) else printf '[!] 提取失败: %s\n' "$img" >&2 fail=$((fail + 1)) fi doneset -euo pipefail让脚本在未定义变量、管道中途失败时立刻停下,比默认的“错了也继续跑”安全。find的-print0用 NUL 分隔结果,配合read -r -d ''就读不出错,这是处理任意文件名的标准姿势;-maxdepth 1限制只扫当前层,避免把子目录里的镜像重复处理。循环里的if包住调用是关键——set -e会让命令失败直接退出脚本,这里显式接管退出码,一个镜像坏了不影响剩下几十个。最后那个ok/fail计数在子 shell 里其实传不出来(管道会开子 shell),要严格统计得换成进程替换,日常用看日志就够了。
跑完拿find "$OUT_DIR" -maxdepth 2 -name default.xbe扫一遍,能列出多少个就说明有多少个镜像至少结构是完整的。
4. 反向打包:从散装目录重建 XISO 并控制体积
提取是拆,创建是装。这一节解决的问题是:手上一堆散装文件想变成能被识别器认的镜像,或者一个旧镜像填充太多想重写一遍压小。两条路,一条从目录创建,一条从已有镜像重写,参数和行为不完全一样。
4.1 目录准备:入口文件与层级
从目录创建镜像,工具对这个目录是有要求的,不是随便扔一堆文件进去就能成。
第一,根目录下必须有default.xbe,而且大小写要和目标环境约定的一致。XDVDFS 保留大小写,创建时不做什么规范化,你给的是Default.xbe它就写Default.xbe,回头在大小写敏感的文件系统上解出来就是另一个名字。
第二,目录层级不能多套一层。常见的错误是解压出来是game_dir/game_dir/...,直接拿外层去创建,结果镜像里所有路径都多了一级,能创建成功但加载端找不到入口文件。创建之前先ls一眼,确认入口文件就在你准备传进去的那一层。
第三,符号链接和特殊文件不要混进去。这个工具是照普通文件写的,遇到链接可能按目标大小写记录,也可能直接跳过,行为取决于版本。归档场景下稳妥做法是先find -type l扫一遍,把链接替换成实体文件再打包。
# 打包前的三项检查 ls -1 ./game_dir/default.xbe # 入口存在 find ./game_dir -type l -print # 有无符号链接 find ./game_dir -type f | wc -l # 文件总数,留个基准4.2 创建与重写:命令和参数怎么定
从目录创建:
# 从目录创建一个 XISO,产物一般是 <目录名>.iso extract-xiso -c ./game_dir # 指定输出位置(依据版本,输出目录参数含义以 -h 为准) extract-xiso -c -d /data/out ./game_dir # 详细模式,看清每个文件被写到哪个扇区 extract-xiso -c -v ./game_dir-c是创建动作,输入是目录、输出是镜像。-v打详细日志,第一次打包建议开着,能看出文件写入顺序和大小;后面稳定了再换-q。
从已有镜像重写:
# 重写一个已有镜像,去掉冗余填充 extract-xiso -r ./backup-01.iso # 重写到指定目录,避免覆盖原件 extract-xiso -r -d /data/rewritten ./backup-01.iso-r的语义是“读进来再按自己的规则写出去”,所以它对原镜像做了一遍重新布局。好处是填充被裁掉、目录结构被规整,体积能小一截;代价是原始镜像里的非标准内容(尾部保留区、厂商写入的额外扇区)不会被保留。这一点必须提前想清楚:原件是唯一副本的时候,不要直接对它跑-r,先复制一份。
参数的具体含义不同分支可能有出入,下面这张表是我常用的几个,实际以-h输出为准:
| 参数 | 作用 | 使用场景 |
|---|---|---|
-l | 只读列出目录树 | 提取前确认镜像可解析、入口文件在不在 |
-x | 提取 | 拆开镜像落盘 |
-c | 从目录创建镜像 | 散装文件归档成 XISO |
-r | 重写已有镜像 | 压体积、规整目录结构 |
-d | 指定输出目录 | 批量归档统一收纳,避免污染当前目录 |
-q | 静默 | 脚本里跑,只看错误 |
-v | 详细 | 首次打包、排查扇区级问题时开 |
注意:
-r会覆盖同名产物,且不保证逐字节还原原镜像。归档场景下把它当成“生成一个可用副本”,不要当成“无损压缩”。
4.3 校验:哈希对不上时先看哪里
重建之后一定要校验,不然等到加载端报错再回头查,成本高得多。最直接的办法是往返比对:把原镜像提取到 A,重建得到新镜像,再把新镜像提取到 B,最后比 A 和 B。
# 1) 原镜像提取到 A extract-xiso -x -q -d /tmp/A ./backup-01.iso # 2) 重写原镜像 extract-xiso -r -d /tmp/rewritten ./backup-01.iso # 3) 新镜像提取到 B extract-xiso -x -q -d /tmp/B /tmp/rewritten/backup-01.iso # 4) 比对两份内容(只比文件内容和相对路径,不比时间戳) diff -r --no-dereference /tmp/A /tmp/B && echo "内容一致"diff -r递归比对,输出为空即一致。这里不要加比对时间戳的选项,因为 XDVDFS 的 FILETIME 在往返过程中可能被规整,时间差个几秒是正常的,拿它当失败依据会误判。真正该关心的是文件集合和每个文件的内容。
如果diff报某几个文件不一致,排查顺序是:先看这几个文件是不是大于某个阈值(大文件在扇区对齐上更容易出问题),再看它们的名字里有没有大小写混排或特殊字符,最后看它们在原镜像里的扇区偏移是不是连续。经验上,内容不一致多半出在名字解析而不是数据搬运——数据是按扇区整块拷的,出错概率低;名字是变长解析的,边界条件多。
镜像级别的哈希也要留一份:
sha256sum ./backup-01.iso /tmp/rewritten/backup-01.iso两个值必然不同(重写去掉了填充),所以这个哈希不是用来判等的,是用来做归档指纹的。把它和文件清单一起存进归档记录,下次有人问“这个镜像哪来的、什么时候处理的”,能直接对上。
5. 避坑与排查:五个让提取结果对不上的坑
这一节全是踩过的。每一条都按“看到什么现象 → 为什么 → 怎么解”写,遇到同类问题直接对号入座,不用从头推。
5.1 解压软件只能读出一个几 KB 的文件
现象:把镜像挂载或解压,目录里只有一个很小的文本类文件,或者工具直接报“不支持的卷描述符”,但文件大小明明有好几个 G。
原因:通用工具按 ISO9660 的规则去扇区 16 找主卷描述符,XDVDFS 的卷描述符不在那儿,找不到就退化成“只有一个文件”或者直接失败。这不是镜像损坏,是格式不匹配。
解决:先用extract-xiso -l验证能不能列出目录树。能列出来就说明镜像完好、只是工具选错了;列不出来再怀疑镜像本身,比如传输过程中被截断。顺手用ls -lh看一眼文件大小是否与预期量级相符,被截断的镜像通常大小是个不整的数值。
5.2 提取出来的文件名大小写和预期不一致
现象:在 Linux 上提取,发现入口文件叫Default.xbe而不是default.xbe,某些加载端直接报找不到入口。
原因:XDVDFS 原样保留写入时的大小写,不做好坏统一。如果这个镜像是被第三方工具重新打包过的,名字可能在那一轮就被改过。提取端只是忠实还原,不做纠正。
解决:以-l的原始输出为唯一事实来源,别凭记忆硬编码文件名。需要固定名字的场景,提取后统一做一次重命名,并且把重命名规则写进脚本,而不是手工改——手工改的下一步就是漏改。
5.3 重建后体积小了一两个 G,但加载端行为变了
现象:-r重写之后镜像小了一大截,模拟器能跑,但某个流程走到一半失败,报读取越界或校验失败。
原因:原镜像尾部有填充区或者厂商写入的保留扇区,-r按自己的布局规则重写时把这些裁掉了。对被裁剪的内容有依赖的加载路径就会出问题。这不是 bug,是重写的定义如此。
解决:重写只用于“生成归档副本”,原件永远留一份只读备份。真要交付,先拿重写版跑一遍完整流程再决定用哪个。归档记录里把“原件哈希”和“重写版哈希”都存上,别只留一个。
5.4 从目录创建时失败,提示找不到入口或卷信息
现象:-c直接报错退出,提示找不到default.xbe或者无法确定目录结构。
原因:九成是层级多套了一层。解压工具默认会建一个和压缩包同名的目录,你再把它解到目标目录,就变成game_dir/game_dir/default.xbe。工具在根目录找不到入口文件,自然拒绝创建。
解决:创建前ls ./game_dir/default.xbe确认路径。多套一层就把内层目录当作输入,或者先mv拍平。顺手find ./game_dir -maxdepth 1看一眼根层都有什么,比猜快。
5.5 批量脚本跑一半停了,日志里只有一个文件报错
现象:几十个镜像的批处理,跑到中间退出,剩下的全没处理。
原因:set -e加上没有接管退出码,单个镜像失败就把整个脚本带走了。这类失败往往还是偶发的——某个镜像扩展名对但内容不是 XISO,或者传输时截断。
解决:像第 3 章那个脚本一样,把工具调用包进if里显式判断,失败就记一笔继续。批处理的目标是“把能处理的都处理完”,不是“一个失败全体停摆”。跑完之后单独 grep 一遍失败日志,再人工过一遍那几个。
6. 进阶:把提取接进校验流水线
到这一步工具已经能用了,剩下的是让它变成例行流程的一部分,而不是每次手工敲。
第一件事是给每次提取留指纹。提取完成后,对输出目录生成一份“相对路径 + 字节数”的清单,排序后存成文件,和镜像的 sha256 放在一起。下次怀疑某个归档被动过,不用重新提取,直接比清单就能定位到具体是哪个文件变了。
# 生成可复现的文件清单:路径 + 大小,排序后落盘 find ./extracted -type f -printf '%P\t%s\n' | LC_ALL=C sort > manifest.tsv # 与历史清单比对,只关心差异行 diff -u manifest.old.tsv manifest.tsv || true-printf '%P\t%s\n'输出相对路径和大小,LC_ALL=C sort固定排序规则,避免不同环境下 locale 差异导致同一份内容排出不同顺序——这个细节不加,跨机器比对会出现大量假差异。diff后面接|| true是因为差异本身就是我们要的信息,不该让它中断脚本。
第二件事是把“列表”当成廉价的前置检查。批量场景里,-l的开销远小于完整提取,可以在提取前先跑一遍列表,把解析失败的镜像提前筛出来单独处理。我的习惯是列表输出重定向到一个日志文件并按镜像名分文件存,出问题时能直接翻到那一次的输出,不用重跑。
还有一个我常用的技巧:对不确定的镜像,先-r重写到临时目录,再对新镜像-x提取,然后和原镜像的提取结果做diff。这一步相当于让工具自己把镜像“读一遍写一遍再读一遍”,如果两轮提取结果一致,说明这个镜像的结构在工具的理解范围内是自洽的;如果不一致,问题八成出在原镜像有非标准布局,这时候原件必须原样保留,别再对它做任何写操作。
血泪经验是:所有会写盘的命令,输出目录都不要和输入目录相同。-r覆盖同名产物、-c的输出名由目录名推导,这两条加在一起,一个手滑就能把唯一的副本盖掉。从那以后我每次跑批量之前都强制走一遍“输入目录只读挂载 + 输出目录单独建”的流程,宁可多一步mkdir,也不给后悔药留空子。希望帮到你。
本文还有配套的精品资源,点击获取