第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 端口与连线的识别策略

解析端口和连线时,有几个容易踩的坑:

  1. 总线处理:像 data_in[7:0] 这种,要拆成8根单线还是保留为总线?我的建议是:保留总线结构,但内部用位索引访问。
  2. 悬空端口:有些端口没接任何线,比如 .unused()。解析时要跳过,否则会报错。
  3. 同名异线:不同模块里可能有同名的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 本章知识体系总览

下面这张图概括了网表解析的核心流程和关键节点:

网表解析核心流程 1. 读取网表文件 2. 语法解析 3. 结构化存储 支持Verilog/VHDL 正则匹配 + 语法树 字典/列表/对象 关键输出 模块层次树 | 端口列表 | 连线关系 | 实例化映射 层次分析 信号追踪 网表对比

这张图把整个流程串起来了:从原始网表文件开始,经过语法解析,变成结构化的数据,最后输出到各种应用场景。你想想看,有了这个框架,后面做任何逆向分析都有据可依了。

避坑指南:我曾经在解析一个10万行的大网表时,直接用正则匹配整个文件,结果内存爆了。后来改成逐行读取+状态机的方式,问题就解决了。大文件一定要考虑内存效率。

好了,这一章的内容就到这儿。脚本写好了,数据也解析出来了,下一章咱们就可以拿着这些数据做更深度的分析——比如信号追踪和逻辑恢复。记住,基础打牢了,后面才能走得远。


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