4、网表解析入门:使用Python解析EDIF网表、提取基本单元(Cells)与连接(Nets)、构建内存中的图结构。
好,咱们进入正题。上一章我们聊了EDIF文件长什么样,这一章,我们就亲手把它拆了。
说白了,网表解析就是把那个文本文件,变成我们能在内存里操作的数据结构。你想想看,一个几百兆的EDIF文件,你总不能每次都打开文本编辑器去搜吧?那效率太低了。我们要的是——把它变成一张图,一张能让我们随意遍历、搜索、分析的图。
4.1 为什么选Python?
我个人习惯用Python做这类工作。原因很简单:快。不是运行快,是开发快。
我在项目中遇到过好几次,客户给了一个加密的网表,只给了EDIF格式。用C++写解析器?那得折腾好几天。Python呢?一个下午,连解析带可视化,全搞定了。
Python有几个天然优势:
- 字符串处理强:EDIF本质就是文本,Python的字符串操作和正则表达式,处理起来得心应手。
- 数据结构丰富:字典、列表、集合,直接拿来用,不用自己造轮子。
- 生态好:NetworkX、PyGraphviz这些图处理库,直接帮我们把网表变成图。
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')] |
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
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 验证解析结果
解析完了,怎么知道对不对?我一般做三件事:
- 数量核对:解析出的Cell数量,应该和EDIF文件里 (instance 的数量一致。Net数量同理。
- 连通性检查:随机挑几个Net,手动在EDIF文件里搜一下,看看连接的引脚对不对。
- 图结构可视化:用小规模的网表(比如几百个Cell),画成图,肉眼看一下有没有孤立的节点。
4.7 知识体系总览
下面这张图,概括了本章的核心逻辑。从EDIF文本文件开始,经过解析、提取、建图,最终得到内存中的图结构。
4.8 小结
这一章我们走完了从EDIF文本到内存图结构的全过程。说白了,就是三步:读文件、拆结构、建图。
我个人觉得,解析器写得好不好,直接决定了后续逆向分析的效率。一个健壮的解析器,能帮你省下大量排查问题的时间。我建议你拿到一个新的EDIF文件后,先用这个解析器跑一遍,看看Cell数量和Net数量是否合理,再往下做分析。
嗯,代码已经给你了,剩下的就是动手试试。找个真实的EDIF文件,跑一遍,看看你能不能解析出那些隐藏的电路结构。