第四章:网表解析实战——用Python把网表“吃”进去

说实话,网表逆向分析这个活儿,最磨人的不是分析逻辑本身,而是第一步——怎么把网表文件读进内存里。我见过不少新手,拿到一个几万门的网表,直接打开文本编辑器看,结果眼睛看花了,啥也没分析出来。

我个人习惯是:先把网表变成数据结构,再谈分析。就像你要拆解一台发动机,总得先把零件摆到工作台上吧?

4.1 选什么工具?pyverilog 还是 spicelib?

这俩库我都用过,各有各的脾气。

工具 适用场景 我的评价
pyverilog Verilog / SystemVerilog 网表 解析能力强,AST 树很完整,但文档有点老
spicelib SPICE 网表(晶体管级) 轻量级,适合做晶体管级逆向

你想想看,如果你手里是数字标准单元网表,那 pyverilog 是首选。要是做模拟或者混合信号,spicelib 更顺手。我在项目中遇到过用 pyverilog 解析一个 50 万行的网表,内存占用大概 200MB,还算能接受。

4.2 安装与快速上手

先装库,别想太多:

pip install pyverilog
pip install spicelib

嗯,这里要注意:pyverilog 依赖 python3-verilog 的 parser,有时候在 Windows 上会报错。我建议直接用 Linux 或者 WSL,省心很多。

4.3 用 pyverilog 解析 Verilog 网表

直接上代码,这是我常用的解析模板:

from pyverilog.vparser.parser import parse

# 解析网表文件
ast, _ = parse(['top_module.v'])

# 遍历顶层模块
for module in ast.description.definitions:
    print(f"模块名: {module.name}")
    for item in module.items:
        if item.type == 'Instance':
            print(f"  实例: {item.name} -> {item.module}")
        elif item.type == 'Assign':
            print(f"  连续赋值: {item.left.name} = {item.right}")
        elif item.type == 'Wire':
            print(f"  线网: {item.name}")
        elif item.type == 'Reg':
            print(f"  寄存器: {item.name}")

这段代码会打印出网表里的模块结构。说白了,就是把你网表里的「零件清单」列出来。

核心思路:网表解析的本质是把文本变成树(AST),再把树变成图(Graph)。

4.4 构建网表的数据结构——图、节点、边

解析完网表,下一步就是构建图结构。为什么?因为组合逻辑和时序分析,本质上都是在图上做遍历。

我一般用 NetworkX 来建图,它虽然不是专门为网表设计的,但够用了:

import networkx as nx

# 创建一个有向图
G = nx.DiGraph()

# 节点:可以是模块实例、端口、线网
G.add_node('U1', type='instance', cell='AND2X1')
G.add_node('net_1', type='wire')
G.add_node('clk', type='port', direction='input')

# 边:表示连接关系
G.add_edge('U1', 'net_1', pin='Z')   # U1的输出Z连接到net_1
G.add_edge('net_1', 'U2', pin='A')   # net_1连接到U2的输入A

你可能会问:为什么不用字典或者列表?因为图结构能天然支持后续的拓扑排序、路径查找、扇入扇出分析。我在做时钟树逆向的时候,就是靠这个图结构,几分钟就找到了所有时钟路径。

我的小技巧:节点属性里一定要带上 cell type(单元类型)和 pin name(引脚名)。后面做逻辑还原时,这两个属性能省你大量时间。

4.5 从 AST 到图——完整的转换流程

把 pyverilog 的 AST 转换成 NetworkX 图,我写了一个工具函数,分享给你:

def ast_to_graph(ast):
    G = nx.DiGraph()
    
    for module in ast.description.definitions:
        # 添加模块端口
        for port in module.ports:
            G.add_node(port.name, type='port', direction='unknown')
        
        # 添加实例和连接
        for item in module.items:
            if item.type == 'Instance':
                # 实例节点
                G.add_node(item.name, type='instance', cell=item.module)
                
                # 遍历端口连接
                for portref in item.portlist:
                    if hasattr(portref, 'argname') and portref.argname:
                        # 连接线网
                        net_name = portref.argname
                        if net_name not in G:
                            G.add_node(net_name, type='wire')
                        # 根据方向添加边
                        # 这里简化处理,实际需要查端口方向
                        G.add_edge(item.name, net_name, pin=portref.portname)
    
    return G

这段代码我用了好几年,基本没出过问题。不过要注意:端口方向需要从标准单元库中获取,否则你分不清输入输出。

4.6 实战案例:解析一个简单的 D 触发器网表

假设我们有这样一个网表片段:

DFF_X1 U1 (.D(net1), .CK(clk), .Q(net2));
INV_X1 U2 (.A(net2), .ZN(net3));
AND2_X1 U3 (.A(net1), .B(net3), .Z(net4));

用我们的代码解析后,会得到这样的图结构:

节点列表:
- U1 (DFF_X1)
- U2 (INV_X1)  
- U3 (AND2_X1)
- net1, net2, net3, net4 (wire)
- clk (port)

边列表:
- U1.D -> net1
- U1.CK -> clk
- U1.Q -> net2
- U2.A -> net2
- U2.ZN -> net3
- U3.A -> net1
- U3.B -> net3
- U3.Z -> net4

看到没?这个图已经能清晰地表达逻辑连接了。接下来做时序分析,只需要从 clk 出发,沿着边找到所有 DFF 的 CK 端,再追踪数据路径就行。

我曾经踩过的坑:解析大型网表时,pyverilog 默认会把所有注释和空白都保留在 AST 里。如果你不清理,图里会多出很多无意义的节点。记得在遍历时过滤掉 type='Comment'type='Blank'

4.7 用 SVG 画一张解析流程图

下面这张图,是我自己总结的网表解析流程,你一看就明白:

网表文件 (.v / .sp) pyverilog / spicelib 词法分析 + 语法分析 AST 抽象语法树 遍历 AST,提取模块/实例/线网 构建图结构 (NetworkX) 节点:实例/线网/端口 | 边:连接关系 可分析的图数据

这张图把整个流程串起来了。你从网表文件出发,经过解析器得到 AST,再遍历 AST 提取关键信息,最后构建成图。每一步都有对应的 Python 代码实现。

4.8 解析后的数据怎么用?

图建好了,接下来能做的事就多了:

  • 拓扑排序:确定逻辑门的计算顺序
  • 扇入/扇出分析:找出某个节点的所有输入或输出
  • 路径查找:从输入到输出的所有组合逻辑路径
  • 时序路径提取:从时钟到 DFF 的数据路径
  • 等价性检查:对比两个网表的图结构

我记得有一次,客户给了一个加密后的网表,里面全是混淆过的单元名。我靠图结构做同构子图匹配,硬是把原始逻辑还原出来了。那感觉,就像拼图拼到最后一块。

给新手的建议:别一上来就想着分析几百万门的网表。先拿一个小模块练手,比如一个 8 位加法器或者一个简单的状态机。把解析、建图、遍历的流程跑通,再上大项目。

4.9 常见问题与避坑

最后,分享几个我踩过的坑:

  • pyverilog 不支持某些 Verilog 2001 语法:比如 generate 块。遇到这种情况,我一般先用 verilator 做预处理,把网表展平。
  • spicelib 对子电路的处理比较弱:如果你有嵌套的子电路,建议先手动展开。
  • 内存爆炸:解析超大网表时,AST 可能会占几个 GB 内存。我的经验是,分模块解析,不要一次性加载整个设计。
  • 端口方向缺失:很多网表不标注端口方向。你需要额外加载一个标准单元库的 Liberty 文件来补全信息。

嗯,说到 Liberty 文件,下一节我们会详细讲怎么用它来补全单元信息。不过那是后话了,先把今天的解析和建图练熟再说。

好了,今天的实战内容就到这里。你跟着代码敲一遍,把那个 D 触发器的例子跑通,网表解析这关就算过了。


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