3. 硬件描述语言(HDL)与网表:Verilog/VHDL基础,综合后的门级网表结构,以及如何从网表中提取功能信息
做逆向工程,说白了就是跟芯片的「骨架」和「灵魂」打交道。硬件描述语言(HDL)是设计师的思维蓝图,而网表就是这张蓝图被「编译」后的机器语言。我个人习惯把HDL比作菜谱,网表就是切好配好的食材——你得看懂菜谱,也得会从食材反推出菜谱。
3.1 Verilog/VHDL 基础:逆向工程师的「阅读能力」
你可能会问:我又不做设计,学HDL干嘛?嗯,这个问题我当年也问过自己。直到有一次,我拿到一个加密芯片的网表,里面全是标准单元,完全看不出功能。后来我试着把一小块网表反推回RTL代码,才恍然大悟——原来是个AES的S盒。
逆向工程中,我们不需要会写复杂的HDL,但必须能读懂。这里我挑几个最常用的点说说:
3.1.1 模块与端口
Verilog里,一个模块就是一个黑盒子。端口就是盒子的引脚。看下面这个例子:
module counter (
input clk,
input rst_n,
input en,
output reg [3:0] count
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 4'b0;
else if (en)
count <= count + 1;
end
endmodule
这段代码描述了一个4位计数器。注意看always块——这是时序逻辑的标志。在网表里,这种结构会变成一堆D触发器和加法器。我曾经在逆向一个老款MCU时,就是靠识别这种「计数器模式」定位了它的定时器模块。
3.1.2 组合逻辑 vs 时序逻辑
这是HDL里最核心的区分。组合逻辑用assign或always @(*),时序逻辑用always @(posedge clk)。在网表里,组合逻辑对应的是与门、或门、选择器这些;时序逻辑对应的是触发器。
重要: 逆向时,先找触发器(DFF),再找它们之间的组合逻辑。这是提取功能信息的「黄金法则」。
3.2 综合后的门级网表结构
综合工具(比如Synopsys Design Compiler、Yosys)会把HDL翻译成网表。网表长什么样?说白了就是一堆标准单元的实例化,加上它们之间的连线。
3.2.1 标准单元库
每个芯片厂都有自己的标准单元库。比如NAND2X1表示一个2输入与非门,驱动能力为1倍。常见的单元有:
| 单元名称 | 功能 | 输入 | 输出 |
|---|---|---|---|
| INVX1 | 反相器 | A | Y = !A |
| NAND2X1 | 2输入与非门 | A, B | Y = !(A & B) |
| DFFQX1 | D触发器(带Q输出) | D, CK | Q |
| MUX2X1 | 2选1多路选择器 | A, B, S | Y = S ? B : A |
我在项目中遇到过一种情况:网表里全是NAND2和INV,没有其他门。当时我就意识到,这可能是为了混淆故意做的「全与非门实现」。逆向这种网表,你得先把NAND2还原成更高级的逻辑。
3.2.2 网表的层次结构
好的网表是有层次的。顶层模块调用子模块,子模块再调用标准单元。看这个例子:
module top (input a, b, c, output y);
wire w1, w2;
NAND2X1 u1 (.A(a), .B(b), .Y(w1));
INVX1 u2 (.A(c), .Y(w2));
NAND2X1 u3 (.A(w1), .B(w2), .Y(y));
endmodule
这段网表描述了一个简单的逻辑:y = !(!(a & b) & !c)。如果你熟悉布尔代数,一眼就能看出这是 y = (a & b) | c。嗯,这就是逆向的核心——从门级还原出功能。
3.3 如何从网表中提取功能信息
这部分是我最想分享的。提取功能信息,说白了就是「模式识别」。我总结了三个步骤:
3.3.1 第一步:找触发器,画时序边界
先扫描网表里所有的DFF。每个DFF的D端输入是组合逻辑的输出,Q端输出是下一级组合逻辑的输入。把DFF之间的组合逻辑圈出来,这就是一个「逻辑锥」。我习惯用脚本自动提取这些逻辑锥,然后逐个分析。
小技巧: 用Python写个脚本,解析Verilog网表,提取所有DFF的D端表达式。然后把这些表达式化简,就能得到每个寄存器的功能。
3.3.2 第二步:识别常见结构
有些结构在网表里反复出现。比如:
- 加法器: 一堆全加器级联,输入是两个总线,输出是和与进位
- 多路选择器: 一堆MUX2,共享选择信号
- 状态机: 一组DFF,它们的D端逻辑相互关联,通常有反馈
- 存储器: 大量DFF排列成阵列,地址译码器控制读写
我曾经逆向过一个通信芯片的网表,里面有一大片结构完全相同的逻辑块。我数了数,一共8个,每个处理8位数据。再一看,每个块里都有个乘法器结构。嗯,这八成是个FIR滤波器。后来验证,果然如此。
3.3.3 第三步:布尔化简与功能还原
对于复杂的组合逻辑,直接看网表会头晕。我的做法是:把逻辑表达式提取出来,用卡诺图或Quine-McCluskey算法化简。化简后的表达式往往能揭示真实功能。
// 原始网表片段
wire n1, n2, n3;
NAND2X1 g1 (.A(a), .B(b), .Y(n1));
NAND2X1 g2 (.A(a), .B(c), .Y(n2));
NAND2X1 g3 (.A(b), .B(c), .Y(n3));
NAND3X1 g4 (.A(n1), .B(n2), .C(n3), .Y(y));
// 化简后:y = a & b | a & c | b & c
// 这是个3输入多数表决器!
注意: 有些网表会故意插入冗余逻辑或进行逻辑混淆。比如用一堆XOR门实现简单的AND功能。遇到这种情况,别急着化简,先看看有没有明显的「模式」——比如XOR链通常表示奇偶校验或加法器。
3.4 实战:从网表到功能——一个简单例子
假设你拿到下面这个网表:
module mystery (input [3:0] d, input clk, rst, output [3:0] q);
wire [3:0] n1, n2;
DFFQX1 ff0 (.D(n1[0]), .CK(clk), .Q(q[0]));
DFFQX1 ff1 (.D(n1[1]), .CK(clk), .Q(q[1]));
DFFQX1 ff2 (.D(n1[2]), .CK(clk), .Q(q[2]));
DFFQX1 ff3 (.D(n1[3]), .CK(clk), .Q(q[3]));
// 组合逻辑部分
XOR2X1 x0 (.A(q[0]), .B(d[0]), .Y(n1[0]));
XOR2X1 x1 (.A(q[1]), .B(d[1]), .Y(n1[1]));
XOR2X1 x2 (.A(q[2]), .B(d[2]), .Y(n1[2]));
XOR2X1 x3 (.A(q[3]), .B(d[3]), .Y(n1[3]));
endmodule
你看出来了吗?每个DFF的D端输入是Q和D的异或。这其实是个4位异或累加器——或者说,是个线性反馈移位寄存器(LFSR)的变种。我当年在逆向一个随机数生成器时,就遇到过完全相同的结构。
3.5 工具推荐
工欲善其事,必先利其器。我常用的工具有:
- Yosys: 开源综合工具,可以读入网表并输出各种格式。我最爱用它做网表到原理图的转换
- Netlist Explorer: 一个Python库,专门用来解析和操作网表
- ABC: 伯克利的逻辑综合与验证工具,布尔化简功能很强
说实话,逆向网表是个熟能生巧的活。刚开始你可能觉得满眼都是门,看久了就会发现——每个门都在「说话」。它们告诉你:这里是个加法器,那里是个状态机,那边是个计数器。你只需要学会听懂它们的语言。
嗯,这一章的内容就到这里。记住,网表逆向不是死记硬背,而是培养一种「直觉」。当你看到一堆门时,试着问自己:如果我是设计师,我会用这些门实现什么功能?