第1章:网表解析实战——用Python/Tcl脚本解析Verilog网表
各位同行,欢迎来到网表逆向工程的第一站。说实话,很多人一听到「解析网表」就觉得头大,觉得不就是读文件嘛。但我在项目里踩过太多坑了——一个端口连错,后面所有分析全白做。今天咱们就踏踏实实把这件事聊透。
1.1 为什么要自己写脚本解析?
你可能会问:EDA工具不是自带网表浏览器吗?嗯,工具确实能看,但有几个痛点:
- 效率问题:几千个模块的网表,工具打开都卡半天
- 定制需求:我想提取特定类型的连线,工具做不到
- 批量处理:几十个网表要对比差异,手工点鼠标会疯掉
我自己习惯的做法是:先用脚本把网表「结构化」存起来,后面想怎么查就怎么查。说白了,就是给网表建个数据库。
3.2 Verilog网表的结构特点
先看一个典型的综合后网表片段:
module top (clk, rst_n, data_in, data_out);
input clk, rst_n;
input [7:0] data_in;
output [7:0] data_out;
wire [7:0] net_a, net_b;
wire ctrl_sig;
u_adder inst_adder (
.a (net_a),
.b (net_b),
.sum (data_out),
.carry (ctrl_sig)
);
u_reg inst_reg (
.clk (clk),
.rst_n (rst_n),
.d (data_in),
.q (net_a)
);
endmodule
注意几个关键点:
- 端口声明(input/output)定义了模块的对外接口
- wire/net声明了内部连线
- 实例化语句(u_adder、u_reg)描述了子模块的连接关系
我在项目中遇到过最坑的情况:有人把端口名写错了大小写,但Verilog不区分大小写,工具能过,可脚本解析时全乱了。所以解析前一定要先统一大小写。
3.3 Python脚本实战:逐层解析
我个人偏爱Python做原型验证,因为它的正则表达式和字典操作太方便了。下面是我常用的解析框架:
import re
class NetlistParser:
def __init__(self, filename):
self.filename = filename
self.modules = {} # 存储所有模块
self.top_module = None
def parse(self):
with open(self.filename, 'r') as f:
content = f.read()
# 提取所有模块定义
module_pattern = r'module\s+(\w+)\s*\((.*?)\);\s*(.*?)\s*endmodule'
matches = re.findall(module_pattern, content, re.DOTALL)
for mod_name, ports_str, body in matches:
ports = self._parse_ports(ports_str)
instances = self._parse_instances(body)
nets = self._parse_nets(body)
self.modules[mod_name] = {
'ports': ports,
'instances': instances,
'nets': nets
}
# 识别顶层模块(没有在其他模块中被实例化的)
all_instanced = set()
for mod in self.modules.values():
for inst in mod['instances']:
all_instanced.add(inst['type'])
for mod_name in self.modules:
if mod_name not in all_instanced:
self.top_module = mod_name
break
def _parse_ports(self, ports_str):
"""解析端口列表"""
ports = {}
# 处理 input/output/inout 声明
port_decls = re.findall(
r'(input|output|inout)\s*(?:wire|reg)?\s*(?:\[(\d+):(\d+)\])?\s*(\w+)',
ports_str
)
for direction, msb, lsb, name in port_decls:
ports[name] = {
'direction': direction,
'width': int(msb) - int(lsb) + 1 if msb else 1
}
return ports
def _parse_instances(self, body):
"""解析实例化语句"""
instances = []
# 匹配 u_xxx inst_name ( .port(net), ... );
inst_pattern = r'(\w+)\s+(\w+)\s*\((.*?)\)\s*;'
matches = re.findall(inst_pattern, body, re.DOTALL)
for cell_type, inst_name, conn_str in matches:
connections = re.findall(r'\.(\w+)\s*\(\s*(\w+)\s*\)', conn_str)
instances.append({
'type': cell_type,
'name': inst_name,
'connections': dict(connections)
})
return instances
def _parse_nets(self, body):
"""提取wire/net声明"""
nets = {}
net_pattern = r'wire\s*(?:\[(\d+):(\d+)\])?\s*(\w+)'
matches = re.findall(net_pattern, body)
for msb, lsb, name in matches:
nets[name] = {
'width': int(msb) - int(lsb) + 1 if msb else 1
}
return nets
我的小技巧:解析时先做一次「语法归一化」——把所有的换行、多余空格去掉,正则匹配会稳定很多。我曾经因为网表里混了tab和空格,debug了整整一下午。
3.4 Tcl脚本:快速验证与调试
Python适合做深度分析,但如果你在EDA环境里工作,Tcl更顺手。很多工具(比如Synopsys的PrimeTime)原生支持Tcl,可以直接操作网表对象。
下面是一个Tcl脚本示例,用于提取模块层次:
# 读取网表文件
set netlist_file "top.v"
set fp [open $netlist_file r]
set content [read $fp]
close $fp
# 提取所有模块名
set module_pattern {module\s+(\w+)}
set module_names [regexp -all -inline $module_pattern $content]
# 构建层次树
array set hierarchy {}
foreach {full_match mod_name} $module_names {
# 查找该模块中实例化了哪些子模块
set inst_pattern "${mod_name}\\s+\\(.*?\\)\\s*;"
# ... 具体解析逻辑
}
# 打印层次结构
proc print_hierarchy {mod_name {depth 0}} {
global hierarchy
set indent [string repeat " " $depth]
puts "${indent}${mod_name}"
if [info exists hierarchy($mod_name)] {
foreach sub_mod $hierarchy($mod_name) {
print_hierarchy $sub_mod [expr {$depth + 1}]
}
}
}
print_hierarchy "top"
注意:Tcl的正则表达式语法和Python略有不同。特别是转义字符和分组捕获,写的时候要小心。我建议先在命令行里用regexp测试一下再集成到脚本里。
3.5 端口与连线的识别策略
解析端口和连线时,有几个容易踩的坑:
- 总线处理:像
data_in[7:0]这种,要拆成8根单线还是保留为总线?我的建议是:保留总线结构,但内部用位索引访问。 - 悬空端口:有些端口没接任何线,比如
.unused()。解析时要跳过,否则会报错。 - 同名异线:不同模块里可能有同名的wire,但它们是不同的物理连线。一定要带上模块路径来区分。
我常用的端口识别流程:
def identify_connections(module):
"""识别模块内部所有连线的完整路径"""
connections = {}
for inst in module['instances']:
inst_path = f"{module['name']}.{inst['name']}"
for port_name, net_name in inst['connections'].items():
# 构建完整路径
full_net_path = f"{module['name']}.{net_name}"
if full_net_path not in connections:
connections[full_net_path] = []
connections[full_net_path].append({
'instance': inst_path,
'port': port_name
})
return connections
3.6 实战案例:从网表到层次图
咱们用个实际例子走一遍。假设有个简单的网表,包含顶层模块top,下面挂了reg_file和alu两个子模块。解析后应该得到:
| 模块名 | 端口数 | 子模块数 | 内部连线数 |
|---|---|---|---|
| top | 8 | 2 | 12 |
| reg_file | 16 | 0 | 32 |
| alu | 10 | 0 | 8 |
有了这个结构,你就可以做很多事情了:
- 检查某个信号从哪来到哪去
- 统计模块的使用次数
- 对比两个网表的差异
核心思路:解析网表不是目的,目的是把网表变成可查询、可分析的数据结构。后面的所有逆向工作都建立在这个基础上。
3.7 本章知识体系总览
下面这张图概括了网表解析的核心流程和关键节点:
这张图把整个流程串起来了:从原始网表文件开始,经过语法解析,变成结构化的数据,最后输出到各种应用场景。你想想看,有了这个框架,后面做任何逆向分析都有据可依了。
避坑指南:我曾经在解析一个10万行的大网表时,直接用正则匹配整个文件,结果内存爆了。后来改成逐行读取+状态机的方式,问题就解决了。大文件一定要考虑内存效率。
好了,这一章的内容就到这儿。脚本写好了,数据也解析出来了,下一章咱们就可以拿着这些数据做更深度的分析——比如信号追踪和逻辑恢复。记住,基础打牢了,后面才能走得远。
公众号:蓝海资料掘金营,微信deep3321