第三章 网表解析与预处理:从原始数据到可分析结构
说实话,网表解析这一步,看着简单,做起来全是坑。
我刚开始做硬件安全分析那会儿,拿到一个几十万门的网表文件,直接往工具里一丢,结果解析出来全是乱码。后来才明白——网表这东西,不同工具、不同工艺库,格式差异大得离谱。你想想看,一个综合工具吐出来的网表,跟另一个工具吐出来的,连端口命名规则都可能不一样。
所以这一章,咱们就聊聊怎么把那些「原始」的网表,变成我们能用的、标准化的数据结构。
3.1 网表解析工具的选择
做网表解析,工具选对了,事半功倍。我个人习惯用两套工具搭配着来。
3.1.1 Yosys:开源界的利器
Yosys 是我最常用的开源综合工具,它不仅能做综合,还能做网表解析和转换。它的优势在于:
- 支持多种输入格式:Verilog、EDIF、BLIF 都能吃
- 内部表示统一:解析后都转成 RTLIL 格式,方便后续处理
- 脚本化操作:用 Tcl 或 Python 接口,自动化程度高
举个例子,用 Yosys 读入一个 Verilog 网表:
# 读入设计文件
read_verilog top_module.v
# 层次化展开
flatten
# 输出统计信息
stat
# 写出处理后的网表
write_verilog -noattr processed_netlist.v
我在项目中遇到过一个问题:某个第三方 IP 的网表里,用了很多 Synopsys 特有的 `$sdf_*` 系统函数。Yosys 默认不认识这些,直接报错。后来我加了个 `-lib` 参数,把标准单元库的 Verilog 模型也读进去,才搞定。
3.1.2 Synopsys 工具链:工业级的选择
如果你在芯片公司工作,大概率会用到 Synopsys 的工具链。DC(Design Compiler)和 Formality 都能导出网表,而且格式更规范。
Synopsys 工具生成的网表,通常有这些特点:
- 命名规则统一:`U_*` 表示标准单元,`I_*` 表示实例
- 包含时序信息:`.lib` 库文件里有时序弧数据
- 支持 SDC 约束:时序约束文件可以直接关联
不过,Synopsys 的网表也有个麻烦——它喜欢用 `\` 转义特殊字符。比如 `\reg[0] ` 这种写法,解析的时候要特别处理。
3.2 网表标准化处理
不同工具吐出来的网表,就像不同方言——意思一样,说法不同。标准化处理,就是把它们翻译成「普通话」。
3.2.1 端口映射标准化
我见过最离谱的端口命名:同一个时钟信号,在顶层叫 `clk`,在子模块叫 `clock`,在综合后的网表里叫 `CK`。这要是直接分析,肯定乱套。
标准化的做法是:
- 建立端口映射表:把不同命名统一成标准名称
- 信号类型标注:区分时钟、复位、数据、控制信号
- 方向性确认:明确每个端口的输入/输出/双向属性
# 端口映射示例
port_mapping = {
'clk': 'CLOCK',
'clock': 'CLOCK',
'CK': 'CLOCK',
'rst_n': 'RESET_N',
'reset': 'RESET_N',
'RST': 'RESET_N'
}
3.2.2 层次化展开
网表通常有层次结构——顶层模块调用子模块,子模块再调用标准单元。做安全分析时,我建议把层次全部展开,变成扁平网表。
为什么?因为安全漏洞往往藏在跨层次的连接里。你想想看,一个 Trojan 可能藏在某个子模块里,通过顶层信号触发。不展开,根本发现不了。
Yosys 的 flatten 命令就是干这个的:
# 展开所有层次
flatten
# 查看展开后的模块
hierarchy -check
# 重命名所有内部信号
rename -hide path
3.2.3 标准单元库对齐
不同工艺库的标准单元,名字和功能可能不同。比如:
| 功能 | 台积电 28nm | 中芯国际 55nm | 华虹 130nm |
|---|---|---|---|
| D触发器 | DFFQ_X1 | DFFR_1 | DFF_1 |
| 二输入与非门 | NAND2_X1 | ND2_1 | NAND2_1 |
| 反相器 | INV_X1 | INV_1 | INV_1 |
标准化时,需要建立一个「功能到单元名」的映射表。我一般用 JSON 文件维护:
{
"DFF": ["DFFQ_X1", "DFFR_1", "DFF_1"],
"NAND2": ["NAND2_X1", "ND2_1", "NAND2_1"],
"INV": ["INV_X1", "INV_1", "INV_1"]
}
3.3 网表数据结构的构建
解析和标准化都做完了,接下来就是构建数据结构。这一步决定了后续分析的效率和准确性。
3.3.1 图结构:网表的本质
网表本质上就是个有向图——标准单元是节点,连线是边。我习惯用邻接表来存储:
class NetlistGraph:
def __init__(self):
self.nodes = {} # 节点:标准单元
self.edges = [] # 边:连线
self.inputs = [] # 输入端口
self.outputs = [] # 输出端口
self.registers = [] # 寄存器列表
def add_cell(self, name, cell_type, ports):
self.nodes[name] = {
'type': cell_type,
'ports': ports,
'fanin': [],
'fanout': []
}
为什么要用图结构?因为安全分析本质上就是图分析。比如:
- 路径分析:从输入到输出的所有路径
- 扇入扇出分析:某个信号影响了多少后续逻辑
- 环路检测:组合逻辑环路往往是漏洞的温床
3.3.2 信号追踪表
除了图结构,我还会建一个信号追踪表。这个表记录了每个信号的「前世今生」:
| 信号名 | 源单元 | 源端口 | 目标单元 | 目标端口 | 信号类型 |
|---|---|---|---|---|---|
| net_123 | U_AND2_1 | Z | U_DFF_1 | D | data |
| clk_tree | BUF_1 | Z | U_DFF_1 | CK | clock |
有了这个表,追踪一个信号从产生到消费的完整路径,就变得非常直观。
3.3.3 时序弧信息
做安全分析,光有结构信息不够,还得有时序信息。比如:
- 建立时间:数据必须在时钟沿前稳定
- 保持时间:数据必须在时钟沿后保持
- 传播延迟:信号从输入到输出的延迟
这些信息在 .lib 文件里都有。我一般会解析 .lib 文件,提取出每个标准单元的时序弧:
# 时序弧示例
cell DFFQ_X1 {
pin D {
timing() {
related_pin : CK;
timing_type : setup_rising;
rise_constraint("0.1:0.2:0.3");
}
}
}
有了时序弧信息,就能做更深入的分析。比如检查是否存在「建立时间违规」——这往往是硬件木马利用的漏洞点。
3.4 知识体系总览
说了这么多,我画个图帮你理清思路:
这张图把整个流程串起来了。从原始输入开始,经过工具解析、标准化处理,最后构建成可分析的数据结构。每一步都有坑,但每一步也都有对应的解决方案。
嗯,网表解析和预处理这块,说白了就是「磨刀不误砍柴工」。前期把数据结构搭好了,后面的安全分析才能又快又准。我见过太多人急着做分析,结果网表都没解析对,分析出来的结果全是错的——那才是真正的浪费时间。
公众号:蓝海资料掘金营,微信deep3321