网表解析基础:从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, wire | Verilog保留字 |
| 标识符 | 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这两种主流格式的解析方法,其他格式(如BLIF、AIGER)都是类似的套路。多练练,你也能写出健壮的解析器。