第四章:比特流文件格式解析
说实话,搞FPGA逆向这么久,我觉得最基础也最关键的一步,就是搞懂比特流文件的结构。你想想看,如果你连文件里装的是什么都不知道,那后面的分析根本无从下手。今天我就带你把Xilinx、Intel、Lattice这三家的比特流格式挨个捋一遍。
4.1 Xilinx .bit文件结构
Xilinx的.bit文件,我个人觉得是最好解析的。它的结构非常规整,说白了就是一个文件头加上一堆配置数据。我在做第一个逆向项目时,就是拿.bit文件练的手。
4.1.1 文件头结构
每个.bit文件开头都有个固定魔数——0xFF 0xFF 0xFF 0xFF,然后是0xAA 0x99 0x55 0x66。嗯,这里要注意,不同版本的ISE/Vivado生成的魔数可能略有差异,但前四个0xFF基本不变。
// Xilinx .bit文件头结构
typedef struct {
uint8_t sync_word[4]; // 0xFF 0xFF 0xFF 0xFF
uint8_t magic[4]; // 0xAA 0x99 0x55 0x66
uint16_t header_len; // 文件头长度(大端)
char design_name[216]; // 设计名称
char part_name[216]; // 器件型号
char date[216]; // 编译日期
char time[216]; // 编译时间
uint32_t data_len; // 配置数据长度
} xilinx_bit_header;
我曾经遇到过一个坑:某个客户给的.bit文件死活解析不出来。后来发现是Vivado版本太新,文件头里多了几个字段。所以我的建议是,写解析器时一定要做版本兼容。
4.1.2 同步字与配置寄存器
文件头之后就是真正的配置数据了。这里有个关键概念——同步字。Xilinx FPGA在接收配置数据前,必须先收到一个32位的同步字:0xAA 0x99 0x55 0x66。为什么是这个值?其实它和文件头的魔数是一样的,用来告诉FPGA:「嘿,配置数据要来了,准备好!」
配置寄存器这块,我建议重点关注这几个:
| 寄存器名称 | 地址 | 功能说明 |
|---|---|---|
| CRC | 0x00 | CRC校验寄存器 |
| FAR | 0x01 | 帧地址寄存器 |
| FDRI | 0x02 | 帧数据输入寄存器 |
| CMD | 0x04 | 命令寄存器 |
| CTL0 | 0x05 | 控制寄存器0 |
| MASK | 0x06 | 掩码寄存器 |
| STAT | 0x07 | 状态寄存器 |
| LOUT | 0x08 | 菊花链输出寄存器 |
| COR0 | 0x09 | 配置选项寄存器0 |
4.1.3 帧数据解析
帧数据是比特流的核心。每个帧包含了一列逻辑资源(CLB、DSP、BRAM等)的配置信息。Xilinx 7系列FPGA的帧结构大概是这样的:
// 帧数据包格式
typedef struct {
uint8_t type; // 包类型(0x01=读, 0x02=写)
uint8_t opcode; // 操作码
uint16_t word_count; // 数据字数
uint32_t data[]; // 实际数据
} xilinx_packet;
我记得有一次逆向一个加密的比特流,发现帧数据被重新排列了。当时花了整整一周才找到规律——原来是用了一个自定义的地址映射表。所以如果你遇到帧数据对不上,别急着怀疑自己,先看看是不是有地址重映射。
4.2 Intel .pof/.sof文件格式
Intel(原Altera)的比特流格式和Xilinx差别很大。.sof是SRAM对象文件,用于JTAG直接下载;.pof是编程对象文件,用于配置器件存储。
4.2.1 .sof文件结构
.sof文件的结构相对简单,我直接上代码:
// .sof文件头
typedef struct {
char magic[4]; // "SOF\x00"
uint32_t version; // 版本号
uint32_t part_id; // 器件ID
uint32_t data_len; // 配置数据长度
uint32_t crc32; // CRC32校验
} sof_header;
这里有个有意思的地方:Intel的.sof文件用的是小端序,而Xilinx的.bit文件是大端序。我第一次切换平台时就被这个坑过,读出来的数据全是反的。
4.2.2 .pof文件结构
.pof文件比.sof复杂得多,因为它要支持压缩和加密。基本结构如下:
// .pof文件结构
struct pof_header {
uint32_t magic; // 0x12345678
uint32_t version;
uint32_t num_blocks; // 数据块数量
uint32_t flags; // 标志位(压缩/加密)
};
struct pof_block {
uint32_t block_type; // 0x01=配置数据, 0x02=加密数据
uint32_t block_len;
uint32_t block_crc;
uint8_t data[];
};
我曾经在分析一个加密的.pof文件时,发现它的CRC校验值怎么也算不对。后来才意识到,Intel在计算CRC时会把文件头排除在外。这个细节在官方文档里写得很隐晦,我建议你直接看源码验证。
4.3 Lattice .bit文件格式
Lattice的.bit文件格式,说实话,是三家里最「随性」的。它的文件头没有固定长度,完全靠关键字来定位。
4.3.1 文件头解析
// Lattice .bit文件头(示例)
"LATTICE" // 厂商标识
"BITSTREAM" // 比特流标识
"DEVICE: LCMXO2-1200HC" // 器件型号
"PACKAGE: TQFP144" // 封装
"DATE: 2024/01/15" // 日期
"TIME: 14:30:00" // 时间
"LENGTH: 0x00012345" // 数据长度
嗯,这里要注意,Lattice的.bit文件里,关键字和值之间是用冒号分隔的,而且每行结尾没有分号。我刚开始写解析器时,按Xilinx的习惯去解析,结果全乱了。
3.3.2 配置数据格式
Lattice的配置数据是按页组织的,每页包含多个帧。帧的大小取决于器件型号。以LCMXO2系列为例:
// Lattice配置数据包
struct lattice_page {
uint16_t page_addr; // 页地址
uint16_t frame_count; // 帧数量
uint32_t frame_data[]; // 帧数据(每帧固定32位)
};
我个人觉得Lattice的格式最灵活,但也最不统一。不同系列的器件,帧大小、页数量都可能不同。我的建议是,解析时先读器件型号,再查对应的参数表。
4.4 CRC校验机制
CRC校验是比特流文件里最重要的安全机制之一。三家厂商用的CRC算法各不相同:
| 厂商 | CRC算法 | 多项式 | 初始值 |
|---|---|---|---|
| Xilinx | CRC-32 | 0x04C11DB7 | 0xFFFFFFFF |
| Intel | CRC-32 | 0x04C11DB7 | 0x00000000 |
| Lattice | CRC-16 | 0x8005 | 0x0000 |
我曾经在逆向一个Xilinx的比特流时,发现CRC校验总是失败。排查了半天,原来是配置数据里插入了几个空帧,导致CRC计算范围不对。所以我的经验是:CRC校验时,一定要搞清楚计算范围——是只算配置数据,还是连文件头也算进去。
核心要点:
- Xilinx .bit:固定文件头 + 同步字 + 帧数据包,大端序
- Intel .sof/.pof:小端序,.pof支持压缩加密
- Lattice .bit:关键字驱动文件头,按页组织数据
- CRC校验:三家算法不同,计算范围要注意
实用技巧:写解析器时,建议先用已知的比特流文件做测试。我一般会准备三个厂商的官方例程生成的比特流,确保解析器能正确读取。
警告:不要假设所有比特流文件都遵循标准格式。有些厂商会更新格式,或者加入自定义字段。解析时一定要做边界检查,防止缓冲区溢出。
好了,以上就是三家主流FPGA厂商的比特流文件格式解析。说实话,这些格式看起来复杂,但只要你掌握了文件头、配置数据和CRC校验这三个核心要素,剩下的就是细节问题了。我在实际项目中,一般会先写一个通用的解析框架,再针对不同厂商做适配。
公众号:蓝海资料掘金营,微信deep3321