第二章:比特流格式解析中的字节序与对齐陷阱

比特流解析,是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 知识体系图

下面这张图展示了比特流解析中字节序与对齐的核心逻辑:

比特流解析核心逻辑 比特流文件 判断厂商 & 芯片系列 Xilinx:混合字节序 Intel:小端为主 Lattice:非对齐数据 解析策略:分字段处理 + 手动对齐 + 已知数据验证

说白了,比特流解析就是一场和厂商斗智斗勇的过程。你永远不知道下一个字节会以什么顺序出现。但只要你掌握了字节序和对齐的基本规则,再加上一点耐心,总能找到正确的解析方式。

嗯,这一章的内容就到这里。记住,解析之前先验证,验证之后再解析。这个习惯能帮你省下大量时间。


专注资料整理