回归构建模块的构建模块

Lobsters Hottest 新闻

摘要

本文类比了C/C++中的安全漏洞与Verilog中的安全漏洞,指出硬件描述语言的设计导致了缺陷,并认为行业应投资于更安全的替代方案,类似于软件领域对内存安全编程语言的推动。

<p><a href="https://lobste.rs/s/kgcvtj/back_building_blocks_building_blocks">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/05/27 03:27

# 回归基础之基础 来源:https://www.cs.cornell.edu/~asampson/blog/buildingblocks.html 如果有硬件工程师真心喜欢 Verilog,那我还没遇到过。几乎所有人都对 Verilog 抱有这样一种态度:它令人沮丧、荒谬、容易出错,却又是在实际中唯一务实的选择。 Verilog 之所以不可避免,是因为它本质上是所有 EDA 工具的输入格式。它的核心地位意味着它实际上成为了所有其他 HDL 的中间表示:即使你偏爱 Bluespec (https://github.com/B-Lang-org/bsc)、Chisel (https://www.chisel-lang.org/)、Amaranth (https://amaranth-lang.org/) 或 Spade (https://spade-lang.org/),它们都必须编译成 Verilog 才能与硬件世界的其他部分交互。 我担心 Verilog 的缺陷会成为新一轮硬件 Bug 的根源。这与 C 和 C++ 的问题有相似之处:随着定制硬件设计变得越来越流行,我们有可能让一种危险的 HDL 像内存不安全的编程语言在软件领域那样扩散和恶化。 我目前还不知道硬件 Bug 的“内存安全”类比是什么,甚至不知道是否会有一个占主导地位的缺陷类别。不过,Verilog 中确实存在大量地雷,候选目标很多。这篇文章旨在论证,我们应当投入更多精力去理解 Verilog 的问题,以便未来的 HDL 能够避免它们。 ## 关于“基础” [](https://bidenwhitehouse.archives.gov/wp-content/uploads/2024/02/Final-ONCD-Technical-Report.pdf)作为一个美国的编程语言爱好者,我认为“回归基础”(https://bidenwhitehouse.archives.gov/wp-content/uploads/2024/02/Final-ONCD-Technical-Report.pdf) 是过去十年中最令人兴奋的发展之一。这是一份 2024 年白宫国家网络总监办公室的报告,倡导内存安全。它呼吁关键基础设施从 C 和 C++ 这类内存不安全语言迁移,甚至点名提到了 Rust (https://rust-lang.org/) 作为一种有前景的替代方案。 “回归基础”并非开创性观点:到 2024 年,所有理性的人都已经清楚内存安全是一个巨大问题。它令人兴奋是因为*乔·拜登*说了我们多年来一直在说的话。¹ (https://www.cs.cornell.edu/~asampson/blog/buildingblocks.html#fn:biden) 我不是一个非常爱国的人,但当我看到政府报告中这样的内容时,我的心像一只雄伟的秃鹰一样翱翔: > 尽管有严格的代码审查以及其他预防性和检测性控制,但在内存不安全语言中修补并被分配 CVE 标识的安全漏洞中,高达 70% 是由于内存安全问题造成的。 而当报告提到以下内容时,我仿佛听到了国歌奏响: > 对于新产品,选择使用内存安全编程语言构建是一个早期的架构决策,可以带来显著的安全优势。即使对于现有代码库,完全重写代码更具挑战性,仍可通过采用混合方法走上采用内存安全编程语言的道路。 上帝保佑美国。“回归基础”概括了一个难以反驳的三段论,大致如下: 1. 正确性很重要。 2. 存在具有相似根本原因的大类 Bug。 3. 某些语言比另一些语言更容易导致这些 Bug 类别的出现。 4. 因此,我们应该将这些 Bug 归咎于语言本身,而非程序员。 5. 那些使用起来更难但能显著减少这些 Bug 频率的语言可能是值得的。 在原始报告中,这个“基础”论证是关于 C 语言的。但我相信同样的推理也适用于 Verilog。 ## 主张(弱形式和强形式) 我将以弱形式和强形式陈述我的论点;你可以选择你认为合适的层面。弱形式是: > Verilog 导致大量 Bug。 而强形式是: > 下一场“基础”危机将发生在硬件领域,并且责任在于 Verilog。 换句话说:就在我们开始掌控内存安全的同时,我们正在产生更多的 Verilog 代码。因此,下一波可避免的 Bug 浪潮可能出现在硬件中。更好的 HDL 可以显著减少这些 Bug 的频率。 Verilog 的问题并不新鲜,但直到现在,硬件设计方法学缓解了它们的危害。传统的硬件开发方式——大型 CPU 厂商所用的那种流程——部分地通过投入大量资源进行验证来减轻 Verilog 的问题。这一观察很难用具体证据支持,但可以看看这份来自某个行业联盟的略微可疑的报告 (https://resources.sw.siemens.com/en-US/white-paper-2022-wilson-research-group-functional-verification-study-ic-asic-functional-verification-trend-report/),其中声称在 CPU 设计项目中,验证工程师与设计工程师的比例为 5:1。当有那样的安全网时,一个糟糕的 HDL 就没那么重要了。 但如今,更多人想要设计定制化的、特定应用的硬件。专门化、小批量的硬件项目不会(也不应该)使用与苹果下一代 iPhone SoC 相同的工程流程。新兴的长尾更廉价、更轻量级的硬件设计项目将更容易受到 Verilog 问题的影响。 ## 对 Verilog 的一些廉价批评 这篇文章的主要目的是呼吁系统性地研究 Verilog 对硬件正确性的影响,而不是揪着我个人讨厌的特定缺陷不放。但我还是忍不住要在这个桶里随便打几条鱼。 Verilog 问题的根源在于它并非为实现硬件而设计。它最初 (https://dl.acm.org/doi/10.1145/3386337) 是作为一种用于编写数字逻辑事件驱动*仿真器*的 DSL 而开发的。后来,逻辑综合工具将 Verilog 改用于生成真实的网表。Verilog 的许多问题源于仿真与实现之间界限的模糊: - **存在定义不清的“可综合”子集。** 工具无法就该子集达成一致,但我们都同意并非所有 Verilog 都能合理地被翻译成硬件。 - **Verilog 需要承载过重的 lint 工具。** 严谨的硬件设计公司会购买极其昂贵的商业工具,将工程师限制在其工具链能够处理的 Verilog 子集内。 具体来说,让我们看看 Verilog 中的三个特定地雷:推断锁存器、“无关”值的语义,以及缺少周期级时序信息。(警告:后者是一个自利性的抱怨,它激发了我共同作者的一些研究 (https://filamenthdl.com/)。) ### 推断锁存器 人们说,Verilog 使用寄存器传输级(RTL)抽象。如果这是我了解 Verilog 的第一件事,我会认为它的工作方式是这样的:语言具有特殊的、内置的用于*状态元素*(如锁存器、寄存器和存储器)的构造。你通过描述如何根据第 *n* 周期的值计算所有状态元素在第 *n+1* 周期的值来编写 Verilog 程序。在 RTL 语言中,所有寄存器似乎都应该是显式的,并与无状态组合逻辑清晰分离。 由于 Verilog 源于事件驱动仿真,实际情况并非如此。相反,变量*可能*是有状态的,*可能*是无状态的,具体取决于它们的使用方式。例如,以下是一种在 Verilog 中编写 32 位锁存器的方法: `` module latch ( input wire en, input wire [31:0] data_in, output reg [31:0] data_out ); always @(*) begin if (en) data_out = data_in; end endmodule `` 有一个输入数据端口、一个输出数据端口和一个 1 位*使能*信号,决定锁存器是否应获得新值。Verilog 语义规定`data_out`必须是*有状态的,因为它是条件赋值*。也就是说,因为在`en == 0`的周期内`data_out`没有被赋值,它会保持旧值——这意味着其实需要一个有状态电路。Verilog 工程师称之为“推断锁存器”。² (https://www.cs.cornell.edu/~asampson/blog/buildingblocks.html#fn:reg) 这里的地雷是:意外创建一个锁存器非常容易。考虑这个人为的 XOR 门实现: `` module funny_xor ( input wire [1:0] in_bits, output reg out ); always @(*) begin if (in_bits == 2'b00) out = 1'b0; else if (in_bits == 2'b01) out = 1'b1; else if (in_bits == 2'b10) out = 1'b1; else if (in_bits == 2'b11) out = 1'b0; end endmodule `` 该模块通过单个 2 位端口`in_bits`接收两个输入。这个电路是无状态的,因为我们的`else if`级联覆盖了所有情况:`out`在每个情况下都被赋值。 但是如果你忘记了其中一个情况呢?例如,删除这两行: `` else if (in_bits == 2'b10) out = 1'b1; `` 该模块现在变成有状态,硬件实现需要一个锁存电路。诡异吧。 ### 无意义的 X 语义 HDL 通常需要一种表示*无关*值的方法,通常写为`X`。有点像未定义行为(从好的方面看 (https://www.ralfj.de/blog/2021/11/18/ub-good-idea.html)),`X`让你可以向优化器传达你的规范只覆盖某些情况,在其他情况下它可以自由决定。 虽然`X`是一个好且有用的想法,但 Verilog 对其语义的定义并不合理。下面说明它*应该*如何工作:`X`表示一个*可能*为 0 或 1 的位,我们不知道它究竟是哪个。³ (https://www.cs.cornell.edu/~asampson/blog/buildingblocks.html#fn:top) 根据这个定义,`X`应该通过 Verilog 的数学运算符传播,就像这样: `` module optimism1; reg [31:0] in; reg [31:0] out; initial begin in = 32'bx; $display("in = %d", in); out = in * 2 + 4; $display("out = %d", out); end endmodule `` 确实,仿真这个模块显示`out`也是一个*无关*值,和`in`一样: `` $ iverilog optimism1.v && ./a.out in = x out = x `` 世界一切正常。Verilog 还有三元运算符,我们可以尝试在其条件中使用`X`: `` out = (in * 2 + 4) > 42 ? 32'b1 : 32'b0; `` 令人欣慰的是,未定义条件的结果本身也是`X`: `` $ iverilog optimism2.v && ./a.out in = x out = X `` 因为`out`可能为 1 也可能为 0,仿真器报告其值为`X`是合理的。让我们更进一步,将该三元运算符重写为长格式的`if`等效形式: `` if ((in * 2 + 4) > 42) out = 32'b1; else out = 32'b0; `` 再次尝试仿真: `` $ iverilog optimism3.v && ./a.out in = x out = 0 `` 什么鬼。 (https://www.destroyallsoftware.com/talks/wat) 这个地雷有一个名字:*X 乐观主义*。这是 Verilog 的标准行为:`if`将`X`视为假,这*错误地*使条件赋值看起来是确定的,而实际上它们是未知的。可悲的是,这种行为显然是故意为之 (https://dl.acm.org/doi/10.1145/3386337): > if-else 的这种乐观行为是 Moorby 有意为之。他意识到在这种过程式上下文中,计算 if-else 表达式两边的值可能极其复杂。减少乐观主义需要仿真器中的执行时间。 请随意称我为编程语言书呆子,但我认为仿真性能不是破坏语义的合理理由。 不出所料,研究人员已经探索了 X 乐观主义如何导致隐晦的安全问题 (https://dl.acm.org/doi/10.1145/3061639.3062328),因为仿真器会就最终电路的行为向你撒谎。 ### 时序问题 最后一个地雷有些不同,因为这是我们在近期关于更安全 HDL 的研究 (https://dl.acm.org/doi/10.1145/3591234) 中试图解决的问题(并且它借用了那篇论文)。这不仅仅是 Verilog 的问题;这是我所知的每个主要 HDL 中都存在的危险。这也是我目前对于与软件中内存安全问题类似的 Bug 类别的最佳猜测。 假设你已经有一个乘法器和一个加法器: `` module Mul32 ( input wire [31:0] in_a, input wire [31:0] in_b, output reg [31:0] out, ); // ... endmodule module Add32 ( input wire [31:0] in_a, input wire [31:0] in_b, output reg [31:0] out ); // ... endmodule `` 假设你想将这两个功能单元组合成一个既能加法又能乘法的 ALU。我们将添加一个 1 位信号`op`来选择使用哪个功能单元: `` module alu ( input wire [31:0] in_a, input wire [31:0] in_b, input wire op, output reg [31:0] out ); wire [31:0] add_out; wire [31:0] mul_out; Add32 add (.in_a(in_a), .in_b(in_b), .out(add_out)); Mul32 mul (.in_a(in_a), .in_b(in_b), .out(mul_out)); always @(*) begin out = (op == 1'b0) ? add_out : mul_out; end endmodule `` 该模块实例化了一个加法器`add`和一个乘法器`mul`,并使用 Verilog 的三元运算符根据`op`选择输出。 如果`add`和`mul`都是组合逻辑,这个实现可以正常工作。但更现实的情况是,一个 32 位乘法器可能是时序的并是流水线化的。如果是这样,这段代码很可能是不正确的:它隐含地假设了`Add32`和`Mul32`具有相同的时序行为。 问题在于:*功能单元的模块签名中没有任何内容告诉我们这一点*。`Add32`和`Mul32`的接口是相同的,因为它们只能描述端口的物理形状,而不能描述时序。即使`Add32`是组合逻辑而`Mul32`需要 3 个周期,它们的 Verilog 签名仍然相同。唯一知道如何正确使用 Verilog 模块的方法是阅读注释(或者直接查看源代码)。 如果你对这个特定问题感兴趣,请阅读我们关于 Filament 的论文 (https://dl.acm.org/doi/10.1145/3591234),这是一种具有强制时序安全类型系统的 HDL。⁴ (https://www.cs.cornell.edu/~asampson/blog/buildingblocks.html#fn:rachit) ## 基础之基础 如果说编译器之类的底层系统是软件行业的基础,那么硬件设计工具就是这些基础的基础。系统抽象的宝塔需要一个坚固的地基,而 Verilog 就是一块开裂破碎的混凝土板。 我们必须努力迈向 Verilog 之后的世界。我们需要在现代化 HDL 上投入更多,我们需要能够支持这些更优秀语言而无需通过 Verilog 这个最小公分母的工具链。

相似文章

迈向可理解的软件

Lobsters Hottest

本文批判了当前的编程实践和对大语言模型的依赖,反而主张通过更好的抽象、文档和软件栈来使代码更易于理解和维护。

内存安全绝对主义者

Lobsters Hottest

本文批评了编程语言辩论中的内存安全绝对主义,认为像 Fil-C 这样的新方法也有权衡,而将 Rust 视为不安全忽略了实际好处。