第三章:LUT配置提取中的逻辑混淆与冗余

LUT(查找表)是FPGA最基础的逻辑单元。说白了,它就是一个小型存储器,用输入信号作为地址,输出预先存好的值。提取LUT配置,是逆向工程中最核心、也最头疼的一步。

我刚开始做逆向时,以为只要把比特流里的LUT内容读出来就完事了。结果呢?被各种混淆和冗余折腾得够呛。这一章,我就把那些坑一个个给你指出来。

3.1 逻辑混淆:厂商的“小把戏”

FPGA厂商不是傻子。他们知道有人会逆向,所以在LUT配置里埋了不少雷。最常见的,就是比特位重映射

什么意思?你从比特流里读到的0和1,并不是直接对应LUT的真值表。厂商会按照某种规则重新排列这些位。比如,一个4输入LUT有16个配置位,但实际存储顺序可能是{bit15, bit0, bit14, bit1, ...}这种乱序。

关键点:不要相信比特流中的位顺序。必须先通过已知逻辑(比如一个简单的AND门)来校准映射关系。

我在项目中遇到过一种更狠的混淆——多LUT共享配置位。厂商把多个LUT的配置位混在一起存储,读取时再通过地址译码器分开。你如果不知道译码规则,读出来的数据就是一团浆糊。

3.2 冗余配置:看着多,其实没用

冗余,是另一个大坑。FPGA里很多LUT配置位,其实是冗余的。它们的存在,要么是为了物理对称性,要么是为了时序平衡,跟逻辑功能半毛钱关系没有。

举个例子。一个6输入LUT,理论上需要64个配置位。但实际芯片里,可能用了128个存储单元。多出来的64个,就是冗余。它们的内容可能是固定的0或1,也可能是前64位的镜像。

LUT类型 理论配置位数 实际存储位数 冗余比例
4输入LUT 16 16-32 0%-100%
5输入LUT 32 32-64 0%-100%
6输入LUT 64 64-128 0%-100%

你想想看,如果你把冗余位也当成有效逻辑来解析,那结果肯定错得离谱。我建议,提取LUT配置后,先做一步冗余检测。方法很简单:看配置位中是否有重复模式,或者是否有大量连续的0或1。

小技巧:用统计方法。如果某个LUT的配置位中,0和1的比例严重失衡(比如90%都是0),那很可能包含了冗余位。真正的逻辑功能,通常0和1的比例不会这么极端。

3.3 提取流程:从比特流到真值表

好了,理论说完了。咱们来点实际的。下面是我个人习惯的LUT配置提取流程,你可以参考一下。

  1. 定位LUT配置块:在比特流中找到LUT配置数据的起始位置。这通常需要参考芯片的位流格式文档,或者通过逆向分析得出。
  2. 读取原始位:按字节或按字读取配置数据。注意字节序(大端/小端)。
  3. 位重映射:根据已知的映射规则,将原始位重新排列成标准真值表顺序。如果没有现成规则,就用已知逻辑来校准。
  4. 冗余过滤:去除冗余位。这一步需要你判断哪些位是有效的。我一般用穷举法:假设所有可能的冗余模式,然后看哪种模式能生成合理的逻辑。
  5. 生成真值表:将过滤后的配置位,按输入顺序排列,生成LUT的真值表。

嗯,这里要注意。第3步和第4步的顺序,有时候可以互换。取决于具体的芯片架构。我个人习惯是先做位重映射,再做冗余过滤。因为重映射后,数据更规整,冗余模式也更容易识别。

3.4 实战案例:一个4输入LUT的提取

光说不练假把式。咱们看一个具体的例子。

假设我们从比特流中读到了以下16个位(原始顺序):

原始位: 1 0 1 0 0 1 0 1 1 0 1 0 0 1 0 1

看起来挺整齐的,对吧?但别高兴太早。经过分析,我们发现这个芯片的位重映射规则是:将原始位按4位一组反转。也就是说,每4位一组,组内顺序颠倒。

应用重映射后:

重映射后: 0 1 0 1 1 0 1 0 0 1 0 1 1 0 1 0

现在看起来有点不一样了。接下来做冗余检测。我们发现,这个LUT实际只有8个有效配置位,后8位是前8位的镜像。去掉冗余后:

有效位: 0 1 0 1 1 0 1 0

这个真值表对应什么逻辑?我们列一下:

输入 (A,B,C) 输出
000 0
001 1
010 0
011 1
100 1
101 0
110 1
111 0

看出来了吗?这是一个三输入异或门(奇数个1输出1)。如果当初没做重映射和冗余过滤,直接拿原始位去解析,你可能会得到一个完全不同的逻辑。

警告:千万不要跳过重映射和冗余过滤步骤。我曾经因为偷懒,直接拿原始位去综合,结果花了两天时间才发现是配置位顺序搞错了。那两天,我一直在怀疑自己的智商。

3.5 自动化工具:能省就省

手动提取LUT配置,偶尔干一次还行。要是批量处理几百个LUT,手都能废掉。我建议你写个脚本,或者用现成的工具。

我自己写过一个Python脚本,流程大概是:

def extract_lut(raw_bits, mapping_table, redundancy_pattern):
    # 1. 位重映射
    remapped = [raw_bits[i] for i in mapping_table]
    # 2. 冗余过滤
    valid_bits = remove_redundancy(remapped, redundancy_pattern)
    # 3. 生成真值表
    truth_table = bits_to_truth_table(valid_bits)
    return truth_table

这个脚本的核心,就是mapping_table和redundancy_pattern。这两个参数,需要你根据具体的芯片型号来设定。不同厂商、不同系列的芯片,规则都不一样。

我建议你建立一个芯片规则库。每逆向一款新芯片,就把它的映射表和冗余模式记录下来。时间长了,这就是你的宝贵资产。

3.6 避坑指南:我的血泪史

最后,分享几个我踩过的坑。希望能帮你少走弯路。

  • 坑1:以为所有LUT的配置格式都一样。 同一款芯片里,不同位置的LUT,配置格式可能不同。比如,靠近I/O的LUT和内部的LUT,冗余模式就不一样。我一开始没注意,结果提取出来的逻辑全是错的。
  • 坑2:忽略全局配置位。 有些LUT的配置,会受到全局配置位的影响。比如,一个全局的“复位极性”位,可能会改变LUT的输出极性。如果你没考虑这个,提取出来的真值表就是反的。
  • 坑3:过度依赖自动化工具。 工具能帮你省时间,但不能替你思考。每次提取完,我都会手动抽查几个LUT,验证一下结果是否合理。别问我为什么,问就是吃过亏。

核心原则:永远保持怀疑。你从比特流里读到的每一个位,都可能是假的。只有经过验证的逻辑,才是真的。

好了,这一章就到这里。LUT配置提取,说难不难,说简单也不简单。关键是要有耐心,还要有方法。下一章,咱们聊聊更刺激的——布线资源提取。到时候你会发现,跟布线比起来,LUT这点事根本不算什么。


公众号:蓝海资料掘金营,微信deep3321