第四章:网表解析基础——用Python解析EDIF网表

EDIF网表,说白了就是FPGA设计的“电路图纸”。你想想看,一个复杂的FPGA工程,综合工具会把它翻译成一种标准格式——EDIF。这个文件里记录了所有的模块、信号、实例化关系。我刚开始做逆向时,面对动辄几万行的EDIF文件,头都是大的。后来慢慢摸索出一套用Python解析的方法,今天分享给你。

4.1 EDIF文件长什么样?

先看一眼EDIF的骨架。它其实是一种Lisp风格的文本格式,括号嵌套括号。我截取一段真实的EDIF片段给你看:

(edif top
  (cell TOP (cellType GENERIC)
    (view netlist (viewType NETLIST)
      (interface
        (port clk (direction INPUT))
        (port rst (direction INPUT))
        (port data_out (direction OUTPUT))
      )
      (contents
        (instance u_pll (cellRef PLL))
        (instance u_reg (cellRef REG))
        (net clk_net
          (joined
            (portRef clk (instanceRef u_pll))
            (portRef clk_out (instanceRef u_reg))
          )
        )
      )
    )
  )
)

嗯,这里要注意:EDIF的层级结构非常清晰。最外层是edif,里面包着cellcell里又有viewview里分interfacecontents。我个人习惯把这种结构想象成一棵树——根节点是edif,叶子节点是port、instance、net这些具体元素。

4.2 为什么选Python?

说实话,用Perl或者Tcl也能解析EDIF。但我坚持用Python,原因有三:

  • 库多:Python有现成的解析库,比如pyparsing,能省不少事
  • 好调试:我在项目中遇到过EDIF文件格式不规范的情况,Python的异常信息比Perl友好太多
  • 生态好:解析完网表后,后续做分析、画图、生成报告,Python一条龙搞定
我的小技巧:如果你只是临时解析一次,用正则表达式硬撸也行。但如果是做工具链,建议用pyparsing或者lxml(EDIF可以转XML)。我早期用正则踩过坑——EDIF里字符串可能包含括号,正则匹配会出错。

4.3 核心数据结构设计

解析之前,先想清楚我们要提取什么。我一般会设计三个核心类:

类名 属性 说明
Module name, ports, instances, nets 对应EDIF里的cell
Signal name, width, direction, connected_pins 对应EDIF里的net/port
Instance name, cell_ref, pin_connections 对应EDIF里的instance

你想想看,有了这三个类,整个网表就能被完整描述了。模块包含实例和信号,实例之间通过信号连接。这不就是电路的本质吗?

4.4 解析实战:从EDIF到Python对象

下面我写一个简化版的解析器。为了让你看得清楚,我不用第三方库,纯手写递归下降解析:

import re

class EdifParser:
    def __init__(self, content):
        self.tokens = self._tokenize(content)
        self.pos = 0
    
    def _tokenize(self, content):
        # 把EDIF文本拆成token列表
        # 处理括号、字符串、标识符
        tokens = []
        i = 0
        while i < len(content):
            if content[i] in '()':
                tokens.append(content[i])
                i += 1
            elif content[i] == '"':
                j = i + 1
                while content[j] != '"':
                    j += 1
                tokens.append(content[i:j+1])
                i = j + 1
            elif content[i] in ' \t\n\r':
                i += 1
            else:
                j = i
                while j < len(content) and content[j] not in '() \t\n\r"':
                    j += 1
                tokens.append(content[i:j])
                i = j
        return tokens
    
    def parse(self):
        """解析整个EDIF文件"""
        self._expect('(')
        self._expect('edif')
        name = self._consume()
        modules = []
        while self.pos < len(self.tokens) and self.tokens[self.pos] == '(':
            module = self._parse_cell()
            if module:
                modules.append(module)
        return modules
    
    def _parse_cell(self):
        """解析一个cell(模块)"""
        self._expect('(')
        self._expect('cell')
        cell_name = self._consume()
        # ... 解析interface和contents
        # 这里省略具体实现,避免代码太长
        self._expect(')')
        return {'name': cell_name, 'ports': [], 'instances': [], 'nets': []}
我曾经踩过的坑:EDIF文件里有些属性是可选参数,比如(cellType GENERIC)。如果你写死了顺序,遇到没有这个属性的cell就会崩。我的建议是:解析时用“遇到什么处理什么”的模式,不要假设顺序。

4.5 提取模块信息

模块信息是网表的骨架。每个模块包含:

  • 端口列表:输入、输出、双向口
  • 子实例列表:调用了哪些其他模块
  • 信号列表:内部连线

我一般会这样提取:

def extract_module_info(edif_data):
    modules = []
    for cell in edif_data:
        module = {
            'name': cell['name'],
            'ports': [],
            'instances': [],
            'signals': []
        }
        # 提取端口
        for port in cell.get('interface', []):
            module['ports'].append({
                'name': port['name'],
                'direction': port['direction']
            })
        # 提取实例
        for inst in cell.get('instances', []):
            module['instances'].append({
                'name': inst['name'],
                'cell_ref': inst['cell_ref']
            })
        # 提取信号
        for net in cell.get('nets', []):
            module['signals'].append({
                'name': net['name'],
                'connections': net['joined']
            })
        modules.append(module)
    return modules

4.6 提取信号连接关系

信号连接是网表的核心价值。说白了,你要知道哪个实例的哪个引脚连到了哪里。EDIF里用joined关键字表示连接:

def extract_connections(edif_data):
    connections = []
    for cell in edif_data:
        for net in cell.get('nets', []):
            for join in net.get('joined', []):
                conn = {
                    'net_name': net['name'],
                    'instance': join.get('instanceRef', 'TOP'),
                    'port': join.get('portRef', '')
                }
                connections.append(conn)
    return connections

嗯,这里要注意:有些EDIF文件里,instanceRef可能为空,表示这个端口是顶层端口。我在项目中遇到过这种情况,处理时一定要加判空。

4.7 知识体系总览

下面这张图,是我自己总结的EDIF解析知识体系。你可以把它当作一个检查清单:

EDIF网表解析知识体系 EDIF文件输入 递归下降解析器 模块信息提取 信号连接提取 实例化信息提取 Python对象:Module / Signal / Instance 网表分析 / 逆向还原 / 对比验证

4.8 避坑指南与实用技巧

最后,分享几个我实战中总结的经验:

1. 处理大文件:EDIF文件动辄几十MB,一次性读入内存可能爆掉。我建议用流式解析,或者分块读取。Python的mmap模块可以帮你做内存映射。
2. 处理非标准EDIF:有些综合工具会生成“方言”EDIF。比如Xilinx的EDIF里会带一些私有属性。我的做法是:先解析标准部分,遇到不认识的关键字就跳过,不要直接报错。
3. 调试技巧:解析出错时,打印出当前token位置和前后的上下文。我曾经花了一整天找bug,最后发现是EDIF文件里有个字符串没闭合——从那以后我写了解析器都会加一个“括号匹配检查”的预处理步骤。
我的个人习惯:解析完EDIF后,我会先打印一份“网表摘要”——包含模块数量、实例数量、信号数量。如果这些数字和综合报告对不上,说明解析有问题,赶紧排查。

好了,EDIF解析的基础就讲到这里。你只要掌握了递归下降解析的思路,再加上对EDIF语法的理解,写一个解析器其实不难。记住:先搭骨架(模块),再填血肉(信号和实例),最后连筋脉(连接关系)。


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