4、网表解析入门:使用Python解析EDIF网表、提取基本单元(Cells)与连接(Nets)、构建内存中的图结构。

好,咱们进入正题。上一章我们聊了EDIF文件长什么样,这一章,我们就亲手把它拆了。

说白了,网表解析就是把那个文本文件,变成我们能在内存里操作的数据结构。你想想看,一个几百兆的EDIF文件,你总不能每次都打开文本编辑器去搜吧?那效率太低了。我们要的是——把它变成一张图,一张能让我们随意遍历、搜索、分析的图。

4.1 为什么选Python?

我个人习惯用Python做这类工作。原因很简单:快。不是运行快,是开发快。

我在项目中遇到过好几次,客户给了一个加密的网表,只给了EDIF格式。用C++写解析器?那得折腾好几天。Python呢?一个下午,连解析带可视化,全搞定了。

Python有几个天然优势:

  • 字符串处理强:EDIF本质就是文本,Python的字符串操作和正则表达式,处理起来得心应手。
  • 数据结构丰富:字典、列表、集合,直接拿来用,不用自己造轮子。
  • 生态好:NetworkX、PyGraphviz这些图处理库,直接帮我们把网表变成图。
小提示: 如果你处理的EDIF文件超过500MB,建议用Python的迭代解析方式,不要一次性读入内存。我吃过这个亏,有一次解析一个1.2GB的网表,直接把16GB内存干满了。

4.2 解析策略:先粗后细

EDIF文件是嵌套结构,一层套一层。我的解析策略很简单:先粗后细

第一步,先把整个文件读进来,按括号分组。EDIF的括号是成对出现的,我们可以写一个简单的括号匹配器,把顶层模块、库定义、单元定义这些大块先切出来。

第二步,再针对每个大块,递归解析内部细节。

嗯,这里要注意:EDIF的语法其实挺松散的,有些工具生成的EDIF文件,缩进乱七八糟,甚至一行写完。所以别依赖缩进来解析,老老实实按括号匹配。

4.3 核心数据结构设计

我们需要在内存里构建两个核心东西:Cells(单元)Nets(连接)

我一般用两个字典来存:

# 单元字典:key是单元名,value是单元对象
cells = {}

# 连接字典:key是网名,value是网对象
nets = {}

每个Cell对象,我至少存这些信息:

字段 说明 示例
name 单元实例名 U1, U2
cell_type 单元类型(对应库中的定义) AND2, DFF, INV
ports 端口列表,每个端口有名字和方向 {'A': 'input', 'Q': 'output'}
pin_connections 每个端口连接到的网名 {'A': 'net1', 'Q': 'net2'}

每个Net对象,我存这些:

字段 说明 示例
name 网名 N1, clk, rst
connected_pins 连接到此网的所有引脚列表 [(U1, 'Q'), (U2, 'A')]
核心思路: 网表解析的本质,就是把文本中的「U1 (A = N1, Q = N2)」这种描述,变成内存中「cells['U1'].pin_connections['A'] = 'N1'」这样的数据结构。

4.4 实战:写一个简易解析器

好,我们直接上代码。这个解析器不追求完美,但能处理90%的常见EDIF文件。

import re

class EdifParser:
    def __init__(self, filepath):
        self.filepath = filepath
        self.cells = {}
        self.nets = {}
        
    def parse(self):
        with open(self.filepath, 'r') as f:
            content = f.read()
        
        # 第一步:提取所有单元实例
        # 在EDIF中,单元实例通常以 (instance 开头
        instance_pattern = r'\(instance\s+(\S+)\s*\(viewref\s+(\S+)\)'
        instances = re.findall(instance_pattern, content)
        
        for inst_name, cell_type in instances:
            # 创建单元对象
            cell = {
                'name': inst_name,
                'cell_type': cell_type,
                'pin_connections': {}
            }
            self.cells[inst_name] = cell
        
        # 第二步:提取每个单元的引脚连接
        # 查找 (instance ... (port ...) ...) 结构
        # 这里用简化方法:找 (joined ...) 或 (net ...) 结构
        net_pattern = r'\(net\s+(\S+)\s*(.*?)\)'
        nets_found = re.findall(net_pattern, content, re.DOTALL)
        
        for net_name, net_body in nets_found:
            net = {
                'name': net_name,
                'connected_pins': []
            }
            self.nets[net_name] = net
            
            # 提取连接到这个网的所有引脚
            pin_pattern = r'\(portref\s+(\S+)\s*\(instanceref\s+(\S+)\)\)'
            pins = re.findall(pin_pattern, net_body)
            
            for port_name, inst_name in pins:
                net['connected_pins'].append((inst_name, port_name))
                # 同时更新单元的pin_connections
                if inst_name in self.cells:
                    self.cells[inst_name]['pin_connections'][port_name] = net_name
        
        return self.cells, self.nets
注意: 上面的代码是简化版。真实的EDIF文件可能有嵌套的 (design) 和 (library) 结构,需要递归解析。我曾经在一个项目中,因为没处理好嵌套的 (cell) 定义,导致解析出来的单元类型全是错的,排查了整整两天。

4.5 构建图结构

解析出Cells和Nets之后,下一步就是构建图。我推荐用NetworkX库,它把图论里那些复杂概念都封装好了。

import networkx as nx

def build_graph(cells, nets):
    G = nx.DiGraph()  # 有向图
    
    # 添加节点(每个Cell是一个节点)
    for cell_name, cell_info in cells.items():
        G.add_node(cell_name, 
                   cell_type=cell_info['cell_type'],
                   ports=cell_info['pin_connections'])
    
    # 添加边(通过Net连接的两个Cell之间加边)
    for net_name, net_info in nets.items():
        pins = net_info['connected_pins']
        # 如果有两个以上的引脚连接,说明这是一个信号传播路径
        for i in range(len(pins)):
            for j in range(i+1, len(pins)):
                src_cell, src_port = pins[i]
                dst_cell, dst_port = pins[j]
                # 判断方向:输出到输入
                # 这里需要查库定义,简化处理:假设端口名含'Q'或'O'的是输出
                if 'Q' in src_port or 'O' in src_port:
                    G.add_edge(src_cell, dst_cell, net=net_name)
                else:
                    G.add_edge(dst_cell, src_cell, net=net_name)
    
    return G

你可能会问:「为什么要用有向图?」

因为信号是有方向的。从触发器的Q端输出,到下一个门的A端输入,这个方向不能搞反。否则你后面做时序分析、扇出分析,全都会错。

4.6 验证解析结果

解析完了,怎么知道对不对?我一般做三件事:

  1. 数量核对:解析出的Cell数量,应该和EDIF文件里 (instance 的数量一致。Net数量同理。
  2. 连通性检查:随机挑几个Net,手动在EDIF文件里搜一下,看看连接的引脚对不对。
  3. 图结构可视化:用小规模的网表(比如几百个Cell),画成图,肉眼看一下有没有孤立的节点。
避坑指南: 我曾经遇到过一个坑——EDIF文件里有些Net是「未连接的」,也就是只有一个引脚连在上面。这种Net在构建图的时候要小心,不要因为它而创建自环边。我的做法是:只处理连接了2个及以上引脚的Net。

4.7 知识体系总览

下面这张图,概括了本章的核心逻辑。从EDIF文本文件开始,经过解析、提取、建图,最终得到内存中的图结构。

网表解析流程 EDIF 文本文件 (.edf / .edn) 括号匹配 解析器 提取 Cells & Nets 数据结构化 内存字典 cells{} & nets{} 建图 NetworkX 关键步骤详解: 1. 读取EDIF文件,按括号匹配切分顶层结构 2. 提取 (instance ...) 得到所有Cell实例 3. 提取 (net ...) 得到所有Net连接关系 4. 建立 Cell → Pin → Net 的映射关系 5. 使用 NetworkX 构建有向图,Cell为节点,Net为边 注意:解析时需处理嵌套结构、转义字符、注释等特殊情况

4.8 小结

这一章我们走完了从EDIF文本到内存图结构的全过程。说白了,就是三步:读文件、拆结构、建图

我个人觉得,解析器写得好不好,直接决定了后续逆向分析的效率。一个健壮的解析器,能帮你省下大量排查问题的时间。我建议你拿到一个新的EDIF文件后,先用这个解析器跑一遍,看看Cell数量和Net数量是否合理,再往下做分析。

嗯,代码已经给你了,剩下的就是动手试试。找个真实的EDIF文件,跑一遍,看看你能不能解析出那些隐藏的电路结构。


专注资料整理