第二章:比特流格式解析中的字节序与对齐陷阱
比特流解析,是FPGA逆向的第一步,也是最容易踩坑的一步。
我刚开始做逆向的时候,拿到一个Xilinx的比特流文件,直接按大端方式解析,结果读出来的配置数据全是乱的。折腾了两天,才发现是字节序搞反了。嗯,这种错误,犯过一次就再也不会忘了。
2.1 字节序:大端与小端的混战
FPGA厂商对字节序的处理,说实话,挺随性的。
- Xilinx:比特流头部使用大端序,但配置数据部分使用小端序
- Intel (Altera):全程小端序,但某些控制字是大端
- Lattice:大部分是小端,但配置寄存器地址是大端
为什么会这样?我个人的理解是,这些厂商的硬件设计团队和软件团队,可能用了不同的参考手册。你想想看,一个芯片内部总线可能是小端,但JTAG协议规范又要求大端,最后就变成了这种混搭风格。
关键点:解析任何比特流之前,先确认字节序。不要假设。
2.2 对齐陷阱:不是所有数据都按字节对齐
比特流里经常出现非对齐的数据结构。比如,一个配置包可能是32位,但有效数据只有18位。剩下的14位是填充位,但填充位的位置,不同厂商不一样。
我在项目中遇到过一件事:解析一个Lattice的比特流,按照文档说的偏移量去读配置数据,结果读出来的值总是差几位。后来发现,文档里说的"字节对齐"是指地址对齐,不是数据对齐。数据本身是bit级别的对齐,需要手动移位。
| 厂商 | 对齐方式 | 常见陷阱 |
|---|---|---|
| Xilinx | 32位对齐 | 配置包内的字段可能是非对齐的 |
| Intel | 8位对齐 | 控制字可能跨字节边界 |
| Lattice | 16位对齐 | 填充位位置不固定 |
2.3 实战:解析一个Xilinx配置包
我们来看一个具体的例子。Xilinx的配置包结构是这样的:
typedef struct {
uint32_t header; // 大端
uint32_t data[16]; // 小端
uint32_t footer; // 大端
} xilinx_packet;
注意看,header和footer是大端,但中间的data数组是小端。如果你统一用大端解析,data部分全错。统一用小端,header和footer又不对。
正确的做法是:
// 解析header时用大端
uint32_t header = read_be32(stream);
// 解析data时用小端
for (int i = 0; i < 16; i++) {
data[i] = read_le32(stream);
}
// 解析footer时用大端
uint32_t footer = read_be32(stream);
小技巧:我习惯在解析前先打印前几个字节的原始值,对照厂商文档确认字节序。这一步花不了几分钟,但能省下后面几天的调试时间。
2.4 避坑指南
我曾经因为字节序问题,浪费了整整一周。后来总结了几条经验:
- 先确认厂商:不同厂商的字节序规则不同,甚至同一厂商不同系列的芯片也可能不同
- 不要相信文档:厂商文档也可能有错。我遇到过文档写的是小端,实际是大端的情况
- 用已知数据验证:找一个已知的配置数据,比如全0或全1,先验证解析逻辑是否正确
- 注意填充位:非对齐数据中的填充位,可能是0,也可能是随机值,不要假设
警告:有些比特流文件会在头部包含字节序标识,但有些不会。不要依赖这个标识,自己验证最可靠。
2.5 知识体系图
下面这张图展示了比特流解析中字节序与对齐的核心逻辑:
说白了,比特流解析就是一场和厂商斗智斗勇的过程。你永远不知道下一个字节会以什么顺序出现。但只要你掌握了字节序和对齐的基本规则,再加上一点耐心,总能找到正确的解析方式。
嗯,这一章的内容就到这里。记住,解析之前先验证,验证之后再解析。这个习惯能帮你省下大量时间。