网表解析基础:从EDIF到Verilog的逆向之旅

说实话,网表解析这件事,是FPGA逆向工程里最枯燥但也最关键的环节。我刚开始接触逆向时,拿到一个EDIF文件直接懵了——满屏的括号和数字,根本不知道从哪下手。后来踩了无数坑,才慢慢摸清门道。

今天咱们就聊聊怎么用Python解析网表文件。我会从EDIF和Verilog两种主流格式入手,带你走一遍完整的解析流程。

EDIF文件解析:括号里的秘密

EDIF(Electronic Design Interchange Format)是一种老牌网表格式。它的结构其实很简单——就是一堆嵌套的括号。你想想看,这不就是天然的树形结构吗?

我个人习惯用递归下降解析器来处理EDIF。核心思路是:遇到左括号就压栈,遇到右括号就弹栈。嗯,就这么简单。

import re

class EDIFParser:
    def __init__(self, content):
        self.content = content
        self.pos = 0
        
    def parse(self):
        """解析EDIF文件,返回抽象语法树"""
        tokens = self.tokenize()
        return self.build_ast(tokens)
    
    def tokenize(self):
        """词法分析:把文本拆成token流"""
        # 跳过空白和注释
        pattern = r'\(|\)|[^\s()]+'
        return re.findall(pattern, self.content)
    
    def build_ast(self, tokens):
        """构建抽象语法树"""
        ast = []
        i = 0
        while i < len(tokens):
            if tokens[i] == '(':
                # 递归解析子结构
                sub_ast, i = self.parse_list(tokens, i + 1)
                ast.append(sub_ast)
            else:
                ast.append(tokens[i])
                i += 1
        return ast
    
    def parse_list(self, tokens, start):
        """解析一个括号内的列表"""
        result = []
        i = start
        while i < len(tokens):
            if tokens[i] == ')':
                return result, i + 1
            elif tokens[i] == '(':
                sub_list, i = self.parse_list(tokens, i + 1)
                result.append(sub_list)
            else:
                result.append(tokens[i])
                i += 1
        return result, i

避坑指南:我曾经在解析大型EDIF文件时遇到内存爆炸的问题。后来发现是递归深度太大导致的。建议用迭代方式替代递归,或者设置最大递归深度限制。

Verilog网表解析:从文本到语法树

Verilog网表比EDIF复杂得多。它有关键字、运算符、模块实例化等等。我建议分两步走:先做词法分析,再做语法分析。

词法分析:把代码切成单词

词法分析说白了就是把Verilog代码切成一个个token。我常用的token类型包括:

Token类型示例说明
关键字module, input, wireVerilog保留字
标识符my_module, clk用户定义的名称
数字32'h1234, 8'b1010各种进制的数字
运算符+, -, &, |逻辑和算术运算符
分隔符;, (, ), .语法分隔符
import re

class VerilogLexer:
    def __init__(self):
        self.token_patterns = [
            ('KEYWORD', r'\b(module|endmodule|input|output|wire|reg|assign|always)\b'),
            ('IDENTIFIER', r'[a-zA-Z_][a-zA-Z0-9_]*'),
            ('NUMBER', r'\d+\'[bBodOhH][0-9a-fA-F_]+|\d+'),
            ('OPERATOR', r'[+\-*/&|~^!]'),
            ('SEPARATOR', r'[;,.()\[\]{}]'),
            ('WHITESPACE', r'\s+'),
            ('COMMENT', r'//.*|/\*.*?\*/'),
        ]
        self.token_re = re.compile('|'.join(
            f'(?P<{name}>{pattern})' 
            for name, pattern in self.token_patterns
        ), re.DOTALL)
    
    def tokenize(self, code):
        tokens = []
        for match in self.token_re.finditer(code):
            kind = match.lastgroup
            value = match.group()
            if kind not in ('WHITESPACE', 'COMMENT'):
                tokens.append((kind, value))
        return tokens

关键点:词法分析时要注意Verilog的数字格式。比如 32'hFF00_0000 这种带下划线的写法,很多新手会漏掉。我当年就因为这个bug排查了整整一天。

语法分析:构建抽象语法树

有了token流,接下来就是构建语法树。我通常用递归下降法,为每个语法规则写一个解析函数。

class VerilogParser:
    def __init__(self, tokens):
        self.tokens = tokens
        self.pos = 0
    
    def parse_module(self):
        """解析module定义"""
        self.expect('KEYWORD', 'module')
        name = self.expect('IDENTIFIER')
        self.expect('SEPARATOR', '(')
        
        ports = []
        while self.peek() != ('SEPARATOR', ')'):
            port = self.parse_port()
            ports.append(port)
            if self.peek() == ('SEPARATOR', ','):
                self.advance()
        
        self.expect('SEPARATOR', ')')
        self.expect('SEPARATOR', ';')
        
        body = self.parse_module_body()
        self.expect('KEYWORD', 'endmodule')
        
        return {
            'type': 'module',
            'name': name[1],
            'ports': ports,
            'body': body
        }
    
    def parse_port(self):
        """解析端口定义"""
        direction = self.expect('KEYWORD')  # input/output/inout
        width = None
        if self.peek() == ('SEPARATOR', '['):
            width = self.parse_range()
        name = self.expect('IDENTIFIER')
        return {
            'direction': direction[1],
            'width': width,
            'name': name[1]
        }

数据存储结构设计

解析完网表后,怎么存数据是个大学问。我试过好几种方案,最后总结出一套比较实用的结构。

核心思路是:用字典存模块,用列表存连接关系。这样既方便查找,又便于遍历。

class NetlistDatabase:
    def __init__(self):
        self.modules = {}      # 模块字典
        self.nets = {}         # 信号网络
        self.instances = {}    # 实例化对象
        self.connections = []  # 连接关系列表
    
    def add_module(self, name, ports, body):
        """添加模块定义"""
        self.modules[name] = {
            'name': name,
            'ports': ports,
            'body': body,
            'instances': []
        }
    
    def add_instance(self, module_name, inst_name, port_map):
        """添加实例化"""
        inst_id = f"{module_name}.{inst_name}"
        self.instances[inst_id] = {
            'module': module_name,
            'name': inst_name,
            'port_map': port_map
        }
        self.modules[module_name]['instances'].append(inst_id)
    
    def add_connection(self, source, target):
        """添加连接关系"""
        self.connections.append({
            'source': source,
            'target': target
        })
    
    def find_path(self, start, end):
        """查找两个节点之间的路径"""
        # BFS搜索
        visited = set()
        queue = [(start, [start])]
        while queue:
            node, path = queue.pop(0)
            if node == end:
                return path
            if node not in visited:
                visited.add(node)
                for conn in self.connections:
                    if conn['source'] == node:
                        queue.append((conn['target'], path + [conn['target']]))
        return None

注意:网表数据量通常很大,一个中等规模的FPGA设计可能有上万个节点。如果用纯Python的字典和列表,内存占用会非常恐怖。我建议在解析时就做数据压缩,比如用整数ID代替字符串名称。

实战经验总结

做了这么多年逆向,我总结了几条血泪教训:

  • 先看文件头:EDIF和Verilog的文件头里通常有版本信息和工具信息,能帮你快速判断文件格式
  • 分步调试:别想着一次写完所有解析代码。先做词法分析,验证token正确后再做语法分析
  • 善用断言:在解析函数里加assert,能帮你快速定位语法错误
  • 考虑容错:实际网表文件可能有各种不规范写法,解析器要能优雅地处理异常

我记得有一次解析一个客户给的EDIF文件,里面居然混了Verilog的注释。当时我那个解析器直接崩溃了。后来我加了个预处理步骤,先把文件标准化再解析,问题就解决了。

核心要点:网表解析的本质是把文本结构转化为数据结构。EDIF用括号嵌套,Verilog用关键字和分号。理解了这一点,解析器怎么写都行。

网表解析核心流程 网表文件输入 解析引擎 EDIF解析器 | Verilog词法分析 | 语法树构建 抽象语法树 (AST) 模块定义 | 端口列表 | 实例化关系 Token流 关键字 | 标识符 | 数字 数据存储结构 模块字典 | 信号网络 | 实例化列表 | 连接关系 输入层 解析层 中间表示 存储层

这张图展示了我常用的网表解析流程。从输入文件开始,经过解析引擎生成中间表示,最后存入结构化的数据库。每一步都有对应的数据结构和算法支撑。

说实话,网表解析这件事,入门容易精通难。但只要掌握了EDIF和Verilog这两种主流格式的解析方法,其他格式(如BLIF、AIGER)都是类似的套路。多练练,你也能写出健壮的解析器。


专注资料整理