Verilator 生成 C++ 后 make 阶段编译失败:verilate 通过但 g++ 缺头文件的两阶段定位
更新时间:2026-11-11。开源 EDA 排错系列。用 Verilator 做仿真的人多半见过这种分裂场面:
verilator --cc --exe安安静静退出 0,紧接着make却红着脸报Error 1。这不是工具自相矛盾,而是 Verilator 的构建本来就分两段,lint 通过只代表第一段过关。更多同类报错的入口见 开源 EDA 报错排查总览。
本文要回答的问题
- 为什么
verilator --cc --exe退出 0,紧接着的 make 却编译失败? Vsimple.mk:61: tb_bad.o Error 1这行到底在说谁出错?- verilate 段和 make 段各自检查什么、边界划在哪?
- testbench 引用了一个不存在的头文件,为什么 lint 阶段完全不拦?
- 修复后要重跑哪几条命令才算真正闭环?
下面这张分诊漏斗先给出全篇主线:从一条 make 错误出发,逐层收窄,最终落到 testbench 的坏头文件引用,而不是 RTL。
一、报错原文:verilate 退出 0,make 段 g++ 终止
复现工程只有一个 RTL 文件和两个 C++ testbench。先拿带坏引用的 tb_bad.cpp 跑完整流程,终端记录如下。
读这份记录先抓两个退出码。第 1 段 verilator --cc --exe simple.v tb_bad.cpp --top-module simple 的退出码是 0,说明 RTL lint、层次展开和 C++ 骨架生成都成功了;第 2 段 make 的退出码是 2,失败发生在编译 testbench 对象文件 tb_bad.o 时。make 段完整保留了 g++ 从命令行到终止的全部诊断。
--- 2. make stage: g++ cannot find the missing header ---
$ make -C obj_dir -f Vsimple.mk Vsimple
make: Entering directory '/work/ts-verilator-cxx-build-failure/obj_dir'
g++ -I. -MMD -I/usr/share/verilator/include -I/usr/share/verilator/include/vltstd -DVM_COVERAGE=0 -DVM_SC=0 -DVM_TRACE=0 -DVM_TRACE_FST=0 -DVM_TRACE_VCD=0 -faligned-new -fcf-protection=none -Wno-bool-operation -Wno-overloaded-virtual -Wno-shadow -Wno-sign-compare -Wno-uninitialized -Wno-unused-but-set-parameter -Wno-unused-but-set-variable -Wno-unused-parameter -Wno-unused-variable -Os -c -o tb_bad.o ../tb_bad.cpp
../tb_bad.cpp:2:10: fatal error: nonexistent_header_xyz.h: No such file or directory
2 | #include "nonexistent_header_xyz.h"
| ^~~~~~~~~~~~~~~~~~~~~~~~~~
compilation terminated.
make: *** [Vsimple.mk:61: tb_bad.o] Error 1
make: Leaving directory '/work/ts-verilator-cxx-build-failure/obj_dir'
exit code: 2诊断首行 ../tb_bad.cpp:2:10: fatal error: nonexistent_header_xyz.h: No such file or directory 已经把三件事一次说清:出错文件是 testbench 而非 RTL、位置在第 2 行第 10 列、性质是预处理期找不到头文件;下面的插入符行(^~~~~)正指向那处 #include,compilation terminated. 说明 g++ 在预处理阶段直接终止,还没进入语法分析与链接。对照 testbench 源码,第 2 行引用的正是这个工程里不存在的头文件。
#include "Vsimple.h"
#include "nonexistent_header_xyz.h"
int main(int argc, char** argv) {
Verilated::commandArgs(argc, argv);
Vsimple* top = new Vsimple;
top->eval();
delete top;
return 0;
}Vsimple.h 是 Verilator 生成在 obj_dir/ 下的真实头文件,Makefile 自带 -I obj_dir 能找到;nonexistent_header_xyz.h 在任何搜索路径下都不存在,g++ 预处理时找不到它,编译就此终止。make: *** [Vsimple.mk:61: tb_bad.o] Error 1 里的 tb_bad.o 就是直接线索——出错的是 C++ testbench 的编译单元,不是 RTL。
二、工具、版本与实验工程
本次复现完全在容器内完成,版本与目录结构如下。
| 项 | 实际值 |
|---|---|
| 容器镜像 | Docker geek-eda:0.2,Debian bookworm |
| Verilator | 5.020 |
| 编译器 | 镜像内 g++(由生成的 Vsimple.mk 调用) |
| RTL 文件 | simple.v,一个时钟沿触发的寄存器 |
| testbench | tb_bad.cpp(带坏引用)/ tb_fixed.cpp(修复版) |
| 驱动脚本 | run.sh,依次跑 verilate、make、修复版复跑、仿真 |
RTL 本身简单到没有任何 lint 争议,这正是本次实验想要的条件——把变量隔离在 C++ 一侧,避免 RTL 问题干扰判断。
module simple(input wire clk, input wire d, output reg q);
always @(posedge clk) q <= d;
endmodule三、根本原因:Verilator 的两阶段构建模型
Verilator 不是传统意义上的仿真器,它是一个「把 Verilog 翻译成 C++、再交给宿主编译器」的转译工具。理解本次报错,关键是把它的构建拆成两段看。
蓝色是本次顺利通过的 verilate 段,红色是实际失败点 make 段,绿色是修复后才能到达的仿真段。两段的职责边界可以用一张表对照。
| 对照维度 | verilate 段 | make 段 |
|---|---|---|
| 处理对象 | Verilog/SystemVerilog 设计与命令行 | 生成的 C++ 骨架加上用户 testbench |
| 主要动作 | lint、层次展开、转译生成 C++ 与 Makefile | g++ 预处理、编译、链接出可执行文件 |
| 查 C++ 头文件吗 | 不查,testbench 原样挂进构建列表 | 查,#include 在预处理期解析 |
| 失败时的信号 | %Error 开头诊断,退出码非 0 | g++/ld 诊断,make: *** [...] Error |
| 本次结果 | 退出码 0 | 退出码 2,停在 tb_bad.o |
--exe 这个参数容易给人错觉,好像加了它 Verilator 就会把 testbench 也检查一遍。实际上 --exe 只是告诉它:把后面的 C++ 文件登记进 Vsimple.mk 的源码列表,生成的 Makefile 里多几条 g++ 规则而已。testbench 里 #include 谁、调用哪个库,verilate 段既不预处理也不校验,全部留给 make 段的 g++ 揭晓。
所以本次的根因链条是:testbench 第 2 行引用了不存在的 nonexistent_header_xyz.h → verilate 段无视 C++ 内容、正常退出 0 → make 段 g++ 预处理找不到头文件,编译 tb_bad.o 失败 → make 把 g++ 的失败上卷为 Error 1、退出码 2。RTL 自始至终没有问题,回头怀疑 simple.v 只会白费时间。
四、排查清单:先分段,再定点
遇到 Verilator 构建失败,第一个动作不是盯报错文本,而是判断失败落在哪一段,两条支路的排查对象完全不同。
红色是失败入口与 verilate 段支路,蓝色是分段判定动作,橙色是两条支路各自的排查方向。落到 make 段失败后,按下表逐项过一遍。
| 序号 | 检查项 | 确认方法 | 本次命中 |
|---|---|---|---|
| 1 | 失败是否真在 make 段 | 回看 verilate 退出码是否为 0 | 是,退出码 0 |
| 2 | 读 g++ 第一条诊断而非末行 | 直接跑 make 不用 tail/管道,或 2>&1 | tee build.log | 坏在 #include 预处理 |
| 3 | 头文件是否真实存在 | ls/find 在工程与系统路径核对文件名 | 头文件不存在 |
| 4 | 搜索路径是否覆盖 | 检查 Vsimple.mk 的 -I 与自定义 CXXFLAGS | 路径再对也没有该文件 |
| 5 | 区分编译错误与链接错误 | fatal error/语法错停在 .o;undefined reference 才是链接期 | 编译期错误 |
| 6 | 可选依赖是否误开 | 如 VM_TRACE 需要 verilated_vcd_c.h 等配套头 | 未涉及 |
第 2 条值得单独强调:保留 make 的完整输出,不要用 tail 或管道截掉首条诊断。g++ 最有信息量的永远是第一条 error:/fatal error:,它同时给出文件、行列与性质;末行的 make Error 1 只负责告诉你「失败了」,不负责告诉你「为什么」。日志较长时用 2>&1 | tee build.log 落盘后再检索,比在终端里回看更可靠。
如果失败落在 verilate 段(%Error 且退出码非 0),那才是 RTL 与文件列表的战场,常见的模块找不到、搜索路径配错可对照同族的 Verilator %Error Cannot find file containing module:找不到模块文件排查,不要与本文的 make 段问题混用一套修法。
五、修复代码与修复后复跑确认
修复动作很小:删掉那行不存在的 #include。本工程里它本来就是多余引用,testbench 只需要 Vsimple.h 提供的生成类定义。
#include "Vsimple.h"
int main(int argc, char** argv) {
Verilated::commandArgs(argc, argv);
Vsimple* top = new Vsimple;
top->eval();
delete top;
return 0;
}如果那个头文件确实是工程需要的真实依赖,正确修法不是删除而是补齐:把头文件放进工程目录或 obj_dir,并通过 verilator -CFLAGS "-I<头文件目录>" 把搜索路径传进生成的 Makefile;双引号 include 先查当前文件目录与 -I 路径,尖括号 include 只查 -I 与系统路径,两类写法的查找顺序不要搞反。
修复后用同一个脚本复跑,verilate、make、仿真三段的退出码全部为 0。
--- 3. fixed testbench: verilate and make both pass ---
$ verilator --cc --exe simple.v tb_fixed.cpp --top-module simple
exit code: 0
make exit code: 0
--- 4. run simulation ---
$ ./obj_dir/Vsimple
exit code: 0闭环标准是三条同时满足:verilate 退出 0、make 退出 0、./obj_dir/Vsimple 正常退出。只跑到 make 通过就收工,可能漏掉运行期的参数与初始化问题;本例工程极简,仿真器实例化、eval() 与销毁一次通过,三段全绿才算修复完成。
代码获取
本文的最小复现工程(simple.v、tb_bad.cpp、tb_fixed.cpp 与一键 run.sh)已放在公开仓库:https://github.com/derekzhuo/geek-doc.cn/tree/main/experiments/ts-verilator-cxx-build-failure。拉取后用容器一条命令即可复现「原始报错 → 修复后复跑」全过程:
docker run --rm -v "$PWD":/work -w /work/ts-verilator-cxx-build-failure geek-eda:0.2 bash run.sh实验环境
- 机器/系统:Mac / macOS(Darwin 25.5.0),Docker 容器运行
- 工具版本:Verilator 5.020,镜像
geek-eda:0.2(Debian bookworm,内置 g++ 工具链) - 日期:2026-11-11
- 完整命令:
bash run.sh;脚本依次执行verilator --cc --exe simple.v tb_bad.cpp --top-module simple、make -C obj_dir -f Vsimple.mk Vsimple(失败)、清理obj_dir后换tb_fixed.cpp重跑 verilate 与 make、./obj_dir/Vsimple仿真
这一篇在知识体系里的位置
本篇是开源 EDA 排错簇中「Verilator 构建链路」节点的一页,专管 verilate 段通过之后的 make 段失败。同属 Verilator 的前置阶段问题——模块文件找不到、-I 搜索路径配错——见 Verilator %Error Cannot find file containing module:找不到模块文件排查;跨工具的报错分诊入口与其他开源 EDA 工具的同族案例,统一回到 开源 EDA 报错排查总览。读完本篇再看总览,能更清楚「lint 工具只对自己那段输入负责」这条边界在不同工具上的共同表现。
总结
Verilator 生成 C++ 后 make 阶段编译失败,本质是两阶段构建的后一段出了问题:verilate 段只做 RTL lint 与 C++ 骨架生成,不检查 testbench 的 #include,所以 verilator --cc --exe 退出 0 完全可能与 make 段 g++ fatal error 同时成立。排查先按退出码分段——verilate 非 0 回头查 RTL 与文件列表,verilate 为 0、make 非 0 则盯住 g++ 第一条诊断查 C++ 侧的头文件、库与编译选项;本次根因是 testbench 引用了不存在的 nonexistent_header_xyz.h,删除多余引用后 verilate、make、仿真三段退出码均为 0,修复闭环。