唐凌 发表于 2022-12-23 04:43:20

【光盘】解析ISO文件格式

本帖最后由 tangptr@126.com 于 2022-12-23 04:45 编辑


# 前言
先说结论:ISO文件(光驱映像)本身没有格式。有格式的东西是光盘。
本质上和FLP文件(软驱映像)格式一样,ISO文件的格式可以认为是Flat Binary。

# 光盘定义
通常说硬盘的时候,一个扇区是512字节。但是在光盘上,一个扇区是2048字节。
光盘有虚拟扇区的定义,意义在于模拟硬盘,软盘等。一个虚拟扇区大小是512字节。

ISO-9660标准没有特别规定只使用何种字节序,但是会明确指出某个字段会使用何种字节序。某些字段甚至会给出小端序和大端序两份数值。

## ISO-9660标准
在ISO-9660标准(即CDFS文件系统)下,光盘的前16个扇区不使用,第0x10个扇区开始,每个扇区存放一个卷描述符(Volume Descriptor)。
卷描述符的前7个字节的定义是一致的。
- +0x000处的一个字节是一个整数,用于描述该卷描述符的类型。
- +0x001处的五个字节是一个字符串,是一个签名标识符,应当是`CD001`。
- +0x006处的一个字节用于描述版本,目前只有一个版本,就是`1`。

### 卷描述符类型
卷描述符有以下类型:

| 值 | 描述 |
|---|---|
| 0 | 启动记录 |
| 1 | 主要卷描述符 |
| 2 | 补充卷描述符 |
| 3 | 卷分区描述符 |
| 4-254 | 保留未用 |
| 255 | 终止描述符 |

一般而言,启动记录必须在第0x11个扇区上,主要卷描述符必须在第0x10个扇区上。终止描述符用于表示光盘里的最后一个卷描述符。

### 启动记录
目前而言,光盘启动记录至今就只有一种规范,即`El Torito`规范,即便是UEFI也遵守该规范来启动程序。
如无说明,则整数以小端序表示。
遵守`El Torito`规范的启动记录所在扇区内容不多。
从第0x07至第0x26个字节,是一个`EL TORITO SPECIFICATION`字符串。第0x27至第0x46个字节保留不用。
第0x47至第0x4A个字节是一个32位的整数,用于描述启动目录(Booting Catalog)所在扇区。

#### 启动目录
启动目录本质是“启动目录项”的数组,每个项均为32字节。
第一项为“验证项”(Validation Entry):

| 偏移量 | 类型 | 描述 |
|---|---|---|
| 0x00 | 字节 | 项标识符,验证项的标识符必须为0x01 |
| 0x01 | 字节 | 平台标识符,0表示80x86,1表示PowerPC,2表示Mac |
| 0x02 | 16位字 | 保留不用 |
| 0x04 | 字符串 | 该光盘的制造商/开发者 |
| 0x1C | 16位整数 | 校验和 |
| 0x1E | 16位整数 | 0xAA55,注意校验和包含了0xAA55 |

随后是初始项/默认项(Initial/Default Entry):

| 偏移量 | 类型 | 描述 |
|---|---|---|
| 0x00 | 字节 | 启动指示量:0x88表示可启动,0x00表示不可启动 |
| 0x01 | 字节 | 启动媒介类型。高四位保留不用,低四位表示类型:0为光盘模式(无模拟),1为模拟1.2M软盘,2为模拟1.44M软盘,3为模拟2.88M软盘,4为模拟硬盘 |
| 0x02 | 16位整数 | 启动段选择子。对于平坦内存模型的处理器上,表示启动地址/16。为0时,则段选择子为0x7C0 |
| 0x04 | 字节 | 系统类型 |
| 0x05 | 字节 | 保留 |
| 0x06 | 16位整数 | 启动时固件加载到内存里的**模拟**扇区数(一个扇区512字节) |
| 0x08 | 32位整数 | 启动扇区号 |
| 0x0C | 字节数组 | 保留不用 |

在初始项之后是节(Section),每个节带一个节头(Section Header)。但需要注意的是启动记录里未必有节。

| 偏移量 | 类型 | 描述 |
|---|---|---|
| 0x00 | 字节 | 节头指示量: 0x90表示这是个节,0x91表示这是最后一节 |
| 0x01 | 字节 | 平台标识符 |
| 0x02 | 16位字 | 该节中有多少个项 |
| 0x04 | 字符串 | 节ID |

节里放的是节项(Section Entry),内容和初始项相似,重复内容不在赘述,可参考初始项。

| 偏移量 | 类型 | 描述 |
|---|---|---|
| 0x00 | 字节 | 启动指示量:0x88表示可启动,0x00表示不可启动 |
| 0x01 | 字节 | 启动媒介类型。低四位表示类型;第4位保留不用;第5位表示该项有节项拓展(Section Entry Extension);第6位表示映像包含ATAPI驱动;第7位表示映像包含SCSI驱动 |
| 0x02 | 16位整数 | 启动段选择子 |
| 0x04 | 字节 | 系统类型 |
| 0x05 | 字节 | 保留 |
| 0x06 | 16位整数 | 启动时固件加载到内存里的**模拟**扇区数 |
| 0x08 | 32位整数 | 启动扇区号 |
| 0x0C | 字节 | 选择标准:0表示无标准,1表示IBM的语言与版本信息,其他值保留不用 |
| 0x0D | 字节数组 | 发行者的选择标准信息,如果0x13个字节不够,可以使用节项拓展 |

节项拓展定义如下:

| 偏移量 | 类型 | 描述 |
|---|---|---|
| 0x00 | 字节 | 节项拓展指示量:必须是0x44 |
| 0x01 | 字节 | 第五位置位时表示后面还有节项拓展,复位表示无节项拓展。其余位保留不用 |
| 0x02 | 字节数组 | 发行者的选择标准信息,如果0x1E个字节不够,可以继续使用节项拓展 |

简单总结一下,`El Torito`标准下,一个启动目录里,每个项均为32个字节,以验证项(Validation Entry)开头,紧跟一个初始项/默认项(Initial/Default Entry)。启动目录里可能会有一个或多个节,节头的指示量会表示这是否是最后一个节,以及该节之内有多少个节项。每个节项可能会有一个或多个扩展项,见第1个字节的第5位,节项扩展的第1节第5位会指示是否位最后一个扩展项。
最后,由于默认光盘启动的入口是`0x7C0:0`,不是`0:0x7C00`,因此如果你的实模式启动代码要兼容光盘的话,必须用远跳把`cs`段选择子切到0x0上。EFI启动无需考虑这一条。

### 主要卷描述符
如果需要枚举光盘里的文件,就有必要解析这个卷描述符了。

| 偏移量 | 类型 | 描述 |
|---|---|---|
| 0x000 | 字节 | 卷描述符类型标识符,此处为0x00 |
| 0x001 | 字符串 | 卷描述符标识符,必须为`CD001` |
| 0x006 | 字节 | 版本,一直为0x01 |
| 0x007 | 字节 | 保留不用,一直为0 |
| 0x008 | 字符串 | 系统标识符 |
| 0x028 | 字符串 | 卷标识符 |
| 0x048 || 保留不用,一直为0 |
| 0x050 | 32位整数-小大端 | 卷内的扇区数量 |
| 0x058 || 保留不用,一直为0 |
| 0x078 | 16位整数-小大端 | 卷内磁盘数量 |
| 0x07C | 16位整数-小大端 | 卷内磁盘序号 |
| 0x080 | 16位整数-小大端 | 扇区大小 |
| 0x084 | 32位整数-小大端 | 路径表大小 |
| 0x08C | 32位小端整数 | 小端路径表扇区号 |
| 0x090 | 32位小端整数 | 小端可选路径表扇区号 |
| 0x094 | 32位大端整数 | 大端路径表扇区号 |
| 0x098 | 32位大端整数 | 大端可选路径表扇区号 |
| 0x09C | 目录结构 | 根目录 |
| 0x0BE | 字符串 | 卷集 |
| 0x13E | 字符串 | 出版社 |
| 0x1BE | 字符串 | 数据准备者 |
| 0x23E | 字符串 | 应用 |
| 0x2BE | 字符串 | 版权所有者 |
| 0x2E3 | 字符串 | 摘要 |
| 0x308 | 字符串 | 引用 |
| 0x32D | 字符串 | 创建时间 |
| 0x33E | 字符串 | 修改时间 |
| 0x34F | 字符串 | 过期时间 |
| 0x360 | 字符串 | 有效时间 |
| 0x371 | 字节 | 文件结构版本 |
| 0x372 | 字节 | 保留不用,一直为0 |
| 0x373 || 随意使用,ISO不使用 |
| 0x573 || 由ISO标准保留 |

#### 目录
我们具体看`+0x09C`偏移的根目录项,其类型为目录结构,定义如下:

| 偏移量 | 类型 | 描述 |
|---|---|---|
| 0x00 | 字节 | 记录长度 |
| 0x01 | 字节 | 扩展属性长度 |
| 0x02 | 32位整数-小大端 | 扇区号 |
| 0x0A | 32位整数-小大端 | 数据大小 |
| 0x12 | 字节数组 | 日期 |
| 0x19 | 字节 | 文件标志位 |
| 0x1A | 字节 | Interleaved模式下,文件大小单位。非Interleaved模式下保留不用 |
| 0x1B | 字节 | Interleaved模式下,文件间隙大小。非Interleaved模式下保留不用 |
| 0x1C | 16位整数-小大端 | 卷序列号 |
| 0x20 | 字节 | 文件名长度 |
| 0x21 | 字符串 | 文件名 |
| 可变 | 字节 | 2字节对齐:如果字符串长度为奇数,则该字节不用 |
| 可变 || 系统使用:ISO-9660标准的扩展定义,如SUSP协议,RRIP协议等 |

每个目录项最小34字节,最大254个字节,以2字节对齐紧凑排列。ISO-9660*不允许目录结构跨扇区*,故扇区结尾不足以存放一个目录结构时,需要把目录结构延后到下一个扇区存储。
前两个目录结构均为34字节:前者的文件名长度为0,表示当前目录,即`.`;后者的文件名长度为1,表示上一个目录,即`..`。
后续目录结构存放的目录之下的所有文件。

文件标志位定义如下:

| 位 | 描述 |
|---|---|
| 0 | 文件是否隐藏 |
| 1 | 文件是否为目录 |
| 2 | 文件是为“关联文件” |
| 3 | 文件是否包含扩展属性 |
| 4 | 文件是否包含所有者权限信息 |
| 5,6 | 保留不用 |
| 7 | 该目录项未完成描述一个文件,比如超过4G的文件 |

文件日期的结构如下:

| 字节 | 描述 |
|---|---|
| 0 | 年-1900 |
| 1 | 月 |
| 2 | 日 |
| 3 | 时 |
| 4 | 分 |
| 5 | 秒 |
| 6 | 相对于格林威治时区的偏移量,以15分钟为单位 |

#### 路径表
ISO-9660还定义了个路径表,这张表里包含的是目录关联表,不包含文件。一式两份,大小端各一份,定义如下:

| 偏移量 | 类型 | 描述 |
|---|---|---|
| 0x0 | 字节 | 目录名长度 |
| 0x1 | 字节 | 扩展属性长度 |
| 0x2 | 32位整数 | 扇区号 |
| 0x6 | 16位整数 | 上级目录索引 |
| 0x8 | 字符串 | 目录名 |
| 可变 | 字节 | 2字节对齐:如果字符串长度为偶数,则该字节不用 |

每个路径表项最小10字节,以2字节对齐紧凑排列。ISO-9660*允许路径表结构跨扇区*。
注意:目录索引以1为底。
由于索引号是16位数,以1为底,故标准的ISO-9660光盘不得存放多于65536个目录。
扇区存储的内容为目录结构表。

#### 查找文件
由于存在着两套结构,因此查找文件的时候也有两个套路。
一种方式是递归式地照着目录结构顺路径序查找。
另一种方式是根据路径表逆路径序查找。
两者的理论时间复杂度均为准矩形复杂度,但前者的时间复杂度是$O(x\cdot\log(y+z))$,而后者是$O(x\cdot\log(y)+\log(z))$,其中$x$是路径深度,$y$为目录数量,$z$为文件数量。(注意这里已经按名称排过序了,因此可以结合`strncmp`函数来二分查找)
在理论层面上,显然路径表要有优势。在实践层面上,由于路径表的体积比目录结构要小得多,走路径表来查找文件的方式可以更好地利用缓存来加速搜索。

## ISO-13346标准
ISO-13346即UDF光盘文件系统,但篇幅所限,本文并不想讲这个标准。
ISO-13346通常可以和ISO-9660兼容,但仅支持ISO-9660的光盘读取器在枚举文件的时候往往只会枚举到一个README.TXT,里面写着:
```
This disc contains a "UDF" file system and requires an operating system
that supports the ISO-13346 "UDF" file system specification.
```
ISO-13346标准与`El Torito`兼容,因此固件仍能以老套路加载ISO-13346光盘里的OS启动器,但这个光盘里的OS启动器必须要能正确解析ISO-13346格式。

# 总结
光盘有ISO-9660和ISO-13346两种文件格式,前者为CDFS文件系统,后者为UDF文件系统。
以光盘启动时,入口地址一般为`0x7C0:0`,而非`0:0x7C00`。因此如果你的启动器需要兼容光盘启动,需要用远跳指令修正`cs`段选择子。
本文未讲述UEFI启动时`El Torito`的工作原理,以后和ISO-13346一并解析。

Golden Blonde 发表于 2022-12-25 14:32:28

在REACTOS的boot\freeldr\freeldr\lib\fs目录下有解析iso文件的C语言代码。

除此之外,还有解析FAT、NTFS和EXT2的代码。
页: [1]
查看完整版本: 【光盘】解析ISO文件格式