第3章 数据路径提取:从比特流中提取组合逻辑路径与寄存器路径的方法

说实话,比特流逆向里最磨人的一步,就是数据路径提取。

你拿到了比特流,也知道了配置位大概对应什么资源。但怎么把这些孤立的位,拼成一条完整的逻辑路径?

这就像你手里有一堆乐高零件,图纸却丢了。你得自己琢磨出,哪个零件该插在哪。

我个人习惯,把路径提取分成两大部分:组合逻辑路径寄存器路径。两条路走通了,整个设计就活了。

3.1 组合逻辑路径:从LUT到LUT的连线

组合逻辑路径,说白了就是信号从一组LUT出来,经过布线,再进到下一组LUT。中间没有寄存器打拍。

我在项目中遇到过最头疼的情况:明明LUT配置位都读对了,但输出就是不对。后来发现,是路径上的一个MUX配置位被我忽略了。

3.1.1 LUT配置位的提取

每个LUT本质上是一个小RAM。它的真值表就写在比特流里。

举个例子,一个4输入LUT,有16个配置位。你找到这16个位,就能还原出它的逻辑函数。

// 伪代码:提取LUT配置
for (int i = 0; i < 16; i++) {
    lut_init[i] = bitstream[ lut_base_addr + i ];
}
// 然后根据输入顺序,重建真值表
我的小技巧: 提取LUT时,别只看配置位。还要看它的输入选择器。有些LUT前面有MUX,用来选择是走进位链还是走普通布线。这个MUX的配置位,往往藏在LUT配置位的附近。

3.1.2 布线资源的追踪

组合逻辑路径的难点,在于追踪布线。

比特流里,每个可编程互联点(PIP)对应一个位。你要做的,就是把这些PIP串起来。

我建议你这样做:

  1. 找到源端:比如一个LUT的输出引脚。
  2. 找到目标端:比如下一个LUT的输入引脚。
  3. 遍历中间的所有PIP:从源端出发,沿着布线通道,一路查下去。

嗯,这里要注意。布线通道里有很多“死胡同”。有些PIP虽然配置了,但连到的是空脚。我曾经因为这个,浪费了整整两天。

避坑指南: 我曾经在追踪一条长路径时,发现中间断开了。后来才意识到,那个路径经过了一个“跨区域”的缓冲器。缓冲器的配置位不在常规的PIP表里,而是在另一个专门的配置块里。所以,一定要先搞清楚芯片的全局布线架构

3.2 寄存器路径:时钟与数据的握手

寄存器路径,比组合逻辑路径多了一个维度:时钟

你不仅要看数据从哪来,还要看时钟从哪来。时钟错了,数据全白搭。

3.2.1 寄存器的配置位

每个寄存器(FF)在比特流里,通常有这几个关键位:

配置项 说明 常见陷阱
时钟选择 选择全局时钟还是局部时钟 局部时钟的配置位可能被复用
复位/置位 同步还是异步,高有效还是低有效 有些芯片复位极性是固定的
时钟使能 CE信号的选择 CE信号可能来自LUT输出
初始值 上电后寄存器的初始状态 这个位很容易被忽略

你看,光一个寄存器,就有这么多门道。我刚开始做的时候,只关注了数据输入,结果复位极性搞反了,仿真结果全是反的。

3.2.2 时钟树的追踪

时钟路径的提取,和普通数据路径不太一样。

时钟信号通常走专门的全局布线资源。这些资源在比特流里,有自己独立的配置区域。

我个人习惯,先找到寄存器的时钟输入引脚,然后反向追踪:

  • 这个引脚连到了哪个时钟缓冲器(BUFG)?
  • 这个BUFG的输入来自哪个时钟输入引脚(PAD)?
  • 中间有没有经过MMCM或PLL?

你想想看,如果时钟路径上有一个MMCM,它的分频倍频配置位,往往藏在比特流的另一个角落。不找到它,你永远不知道时钟频率是多少。

3.3 路径提取的自动化脚本

手工提取太慢了。我建议你写脚本。

下面是我常用的一个提取流程:

# 伪代码:自动提取数据路径
def extract_path(bitstream, start_pin, end_pin):
    path = []
    current_pin = start_pin
    
    while current_pin != end_pin:
        # 1. 查找当前引脚的所有扇出
        fanouts = find_fanouts(bitstream, current_pin)
        
        # 2. 根据布线资源表,找到有效的连接
        next_pin = find_valid_connection(fanouts, end_pin)
        
        if next_pin is None:
            # 路径中断,需要回溯
            print("路径中断在:", current_pin)
            break
        
        path.append((current_pin, next_pin))
        current_pin = next_pin
    
    return path
核心要点: 自动提取的关键,是建立一个完整的“布线资源图”。这个图里,每个节点是一个引脚,每条边是一个PIP。有了这个图,路径提取就变成了图搜索问题。我一般用深度优先搜索(DFS),配合一些启发式规则(比如优先走直线),效率很高。

3.4 实战:提取一条完整的寄存器到寄存器路径

我们来走一遍完整的流程。

假设我们要提取从寄存器FF_A到寄存器FF_B的路径。

  1. 提取FF_A的输出:找到FF_A的Q引脚,以及它对应的输出MUX配置。
  2. 追踪布线:从Q引脚出发,沿着PIP一路走到FF_B的D引脚。
  3. 提取FF_B的配置:读取FF_B的时钟选择、复位极性等配置位。
  4. 提取时钟路径:找到FF_B的时钟源,追踪回全局时钟输入。
  5. 验证:用提取到的配置,重建一个小的仿真模型,看看数据能不能正确传递。

最后一步验证,特别重要。我遇到过好几次,路径提取看起来没问题,但仿真就是不对。后来发现,是路径上有一个“透明锁存器”被当成了普通寄存器。锁存器的行为,和寄存器完全不同。

我的经验: 提取路径时,多留个心眼。看看路径上有没有“隐藏”的元件,比如锁存器、三态门、ODDR等。这些元件在比特流里的配置方式,往往和普通LUT/FF不一样。

3.5 本章小结

数据路径提取,是比特流逆向的核心技能。

说白了,就是两件事:找到元件找到连线

组合逻辑路径,关注LUT和布线。寄存器路径,多了一个时钟维度。

我给你的建议是:别急着自动化。先手工提取一两条路径,把芯片的“脾气”摸清楚。等你熟悉了,再写脚本批量处理。

嗯,路径提取这块,水很深。但只要你把基础打牢了,后面分析复杂设计,就会轻松很多。