尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

gem5与SystemC联合仿真环境搭建:从零编译到最小Demo跑通

发布时间:2026/9/25 7:01:53

资讯中心
01
ARTICLE

gem5与SystemC联合仿真环境搭建:从零编译到最小Demo跑通

gem5与SystemC联合仿真环境搭建:从零编译到最小Demo跑通
先回答一个实际问题什么情况下你才需要把 gem5 和 SystemC 放进同一个仿真环境我在做 SoC 早期软件验证时这个问题卡了我很久。gem5 跑 CPU、跑 cache、跑完整 Linux 都很舒服可一旦要验证驱动和外设控制器的真实交互内置模型要么没有要么粗到没法用SystemC 这边 TLM 外设模型建起来非常顺手但你不能拿它去精确模拟乱序执行 CPU 的微架构。两边各有所长却互不搭台。所以 gem5 与 SystemC 联合仿真环境就成了绕不开的方案这也是本文的主题。这篇文章是我从一台接近空白的 Linux 机器开始到编译 SystemC、编译带联合仿真支持的 gem5、再跑通最小联仿 Demo 的完整过程记录。它适合正准备入坑联合仿真、但不想在环境搭建上白折腾一周的人阅读。我会把关键命令、版本组合、验证手段和踩过的坑都写出来尽量让每一步都可复现。1. 为什么偏偏是 gem5 和 SystemC这套组合到底解决什么问题1.1 gem5 真正的地盘和它的边界在哪里gem5 是计算机体系结构领域使用最广的全系统模拟器之一由 AMD、ARM、威斯康星大学等多个社区分支合并演化而来。它能以比较高精度模拟 CPU 流水线、Cache 层级、内存控制器甚至可以直接加载一个未修改的 Linux 内核跑起来这对架构性能评估、缓存策略研究和指令集行为分析来说是非常趁手的工具。但 gem5 的短板也很明显外部设备生态远不如 QEMU 或者 SystemC TLM 生态丰富。你要模拟一颗具体 SoC 的中断控制器、DMA 引擎、电源管理单元或者某条专用总线协议默认情况下都要自己从头写模型。而且 gem5 里写寄存器级外设模型的体验并不好——语法重、调试不方便、跑起来还慢。1.2 SystemC 最擅长补哪块短板SystemC 本质上是一个 C 库外加一个事件驱动仿真内核在 SoC 设计与验证领域是事实标准。芯片工程师喜欢用它搭 TLM 模型在架构探索、硬件验证和早期软件协同开发阶段做系统级仿真。它以模块、端口、进程、事件这些概念组织模型特别适合描述总线、外设、互连等系统组件之间的数据流行为。用 SystemC 建一个带时序、带中断行为的外设模型工作量通常比在 gem5 里写一个等价的模型小得多而且整套模型天然能和验证环境里的 testbench、覆盖率收集工具对接。反过来你让 SystemC 去模拟一个支持乱序执行的现代 CPU 内核那几乎是地狱级工程性能也完全不可接受。1.3 联合仿真的核心链路事务从 CPU 到外设模型联合仿真存在的唯一理由是把两边的长板拼在一起。在实际搭建中典型链路是这样的gem5 这边提供 CPU 和 Cache 模型跑真实固件或者驱动代码当 CPU 发出的访存操作落在外设地址区间时gem5 会把总线事务交给中间的适配层适配层再把事务转换成 SystemC 的 TLM 事务交给 SystemC 环境里的外设模型去处理响应路径再按原路返回。这条链路的价值在于驱动代码不必做任何修改CPU 侧看到的就是真实的设备寄存器访问。我在实际项目里就用这套方案验证过固件里一个 DMA 描述符处理模块前期不需要流片、也不需要 RTL 仿真环境就能把驱动逻辑里的地址错误和状态机 bug 暴露出来。1.4 先泼点冷水哪些场景不需要上联仿我见过不少团队看见“联合仿真”四个字就直接往上冲结果一周过去环境还没跑通。所以有必要把边界先讲清楚。如果你只是做纯 CPU 性能评估、跑 benchmark 量化架构改动的影响那单独的 gem5 就够了联合仿真带来的时序耦合只会拖慢仿真速度没有任何收益。反过来如果你要做的是完整 SoC 的功能验证而且并不需要 gem5 级别的 CPU 精确度那纯 SystemC 环境或者 QEMU 加 SystemC 的组合会更轻快。联合仿真的引入是有成本的编译时间变长、运行速度下降、调试复杂度上升。你必须先确认自己要解决的问题正好落在“需要精确 CPU 行为”和“需要快速自定义外设模型”的交集里。2. 环境准备与版本选型联仿环境的成败在编译之前就定了2.1 依赖清单一次性装齐别等编译报错再回头补我在新机器上搭建过多次这套环境每次遇到的最蠢问题都是编译到一半缺依赖。下面这份清单基于 Ubuntu 22.04用的是系统包管理工具其他发行版命令类似只是包名略有差别。GCC / G建议 9 到 12 的版本太老的编译器对 C17 支持不好太新的又容易和旧版 SystemC 源码产生兼容问题Python 3.8 以上scons 构建脚本依赖 Python太低版本在很多新版 gem5 上直接跑不起来SConsgem5 的构建工具建议 3.0 以上protobuf 相关libprotobuf-dev、protobuf-compilergem5 某些调试和序列化功能会用到pkg-config、zlib1g-dev、libboost-dev不装某些模块会在配置阶段报错安装命令可以直接贴这一条sudo apt update sudo apt install -y build-essential python3 python3-dev scons \ protobuf-compiler libprotobuf-dev pkg-config zlib1g-dev \ libboost-all-dev这里有个容易被忽略的点python3-dev一定要装。gem5 的 SCons 脚本在配置阶段会检测 Python.h缺了这个头文件即使 scons 本身能用后续代码生成也会莫名其妙失败。我第一次搭建时就是漏了这一项排查了很久才发现纯粹是环境问题而不是代码问题。2.2 SystemC 库的编译细节高版本 GCC 下的坑SystemC 官方发布包可以从 Accellera 官网下载我推荐直接拿 2.3.4 版本不要用 2.3.3。原因很现实2.3.3 的源码里有老式 C 写法在较新的 GCC 版本下编译时会报类似 “ISO C17 does not allow dynamic exception specifications” 的错误虽然可以通过加-fpermissive或CXXFLAGS-stdc11强行绕过去但没必要自讨苦吃。下载解压后编译安装步骤很简单mkdir build cd build ../configure --prefix/opt/systemc-2.3.4 make -j$(nproc) sudo make install装完以后重点检查/opt/systemc-2.3.4/lib-linux64/路径下是否生成了libsystemc.so和libsystemc.a。注意有的 32 位系统路径是lib-linux这个后面配置环境变量时很容易写错位。我个人的建议是自己编译 SystemC而不是使用发行版仓库里的包。原因有两个第一自己编译可以保证 SystemC 库和后续 gem5 用的是同一套 GCC 工具链省掉不少 ABI 兼容性问题第二发行版自带的 SystemC 版本往往偏旧如果是 Ubuntu 20.04 之类的系统自带的版本可能还是 2.3.1和较新的 gem5 在联合仿真初始化接口上存在不匹配。2.3 gem5 源码获取与版本为什么要看 systemc 目录的 READMEgem5 官方仓库地址是https://gem5.googlesource.com/public/gem5Github 上也有官方镜像。拉取代码时我建议直接切到稳定的 release 分支不要用最新开发主干。开发主干的 systemc 集成代码经常处于变动中今天能编译过明天换个 commit 可能就挂了。git clone https://gem5.googlesource.com/public/gem5 cd gem5 git checkout v23.0拿到源码后第一件事不是急着编译而是看gem5/systemc/目录下的 README 文件。不同版本的 gem5 对 SystemC 的集成方式有差别环境变量的命名也可能漂移。我在旧版本上用的变量到了新版本可能就已经改名或者换成新的机制了。以我实测过的 v21.2 和 v23.0 来说USE_SYSTEMC1和SYSTEMC_HOME这两个变量是稳定的但更老或更新的版本不好说。2.4 我的版本组合参考表直接把我在一套跑通的组合放出来方便你对照组件版本说明操作系统Ubuntu 22.04 x86_64内核版本无所谓GCC11.2系统自带的 gcc-11Python3.10系统自带SCons4.5pip 安装SystemC2.3.4自编译安装到 /opt/systemc-2.3.4gem5v23.0release 分支这套组合的优点是 gcc-11 对 C17 支持成熟SystemC 2.3.4 和 gem5 v23.0 都在这个编译器版本下验证过。如果你手上正好是 Ubuntu 20.04gcc-9 也可以用但 SystemC 建议坚持 2.3.4gem5 可以退回到 v21.2稳定性也不错。3. 编译流程与三步验证确保 SystemC 真正参与链接而不是编译了个寂寞3.1 打开编译开关USE_SYSTEMC 和 SYSTEMC_HOMEgem5 使用 scons 作为构建工具支持在命令行直接传自定义编译变量。编译带 SystemC 联合仿真支持的 gem5 目标核心命令如下export SYSTEMC_HOME/opt/systemc-2.3.4 scons build/ARM/gem5.opt USE_SYSTEMC1 -j$(nproc)USE_SYSTEMC1告诉 scons 构建脚本这次要启用 SystemC 支持。SYSTEMC_HOME指向 SystemC 的安装根目录scons 会从这个路径下的include/找头文件从lib-linux64/找库文件。这里有一个很多人都会犯的错误只加USE_SYSTEMC1但忘记设置SYSTEMC_HOME。此时 scons 会从默认路径找 SystemC大概率找不到然后报一个看起来和 SystemC 毫不相关的头文件缺失错误。所以设置环境变量这一步不要省略也不要偷懒写进 shell 配置后不 source。如果内存紧张把-j$(nproc)改成-j4或者更小。gem5 全量编译非常吃内存16GB 内存的机器上全并行编译很容易直接触发 OOM我在服务器上遇到过不止一次内核把编译器进程杀掉的情况。3.2 SC_CPP 与编译器的统一问题在较老的 gem5 版本里构建脚本还会读取SC_CPP变量用来指定 SystemC 库对应的 C 编译器前缀。它的实际作用是确保 gem5 编译时调用的编译器和 SystemC 库编译时用的编译器保持良好的 ABI 兼容性。我的建议是既然 SystemC 是自己编译的那就直接保证两边用同一个编译器。在命令行执行which g确认一下然后让 scons 和 SystemC 都走这个编译器。现代 Linux 上 GCC 的 ABI 兼容性已经相对稳定gcc-11 编译的 SystemC 库被 gcc-12 链接一般没问题但你没必要去赌这种兼容性。保持工具链统一是最省心的做法。在 gem5 v23.0 上实测SC_CPP不设置也能正常编译因为 scons 默认用的就是当前环境的 g。如果你使用的是自定义交叉编译工具链那么必须在 scons 命令里显式传SC_CPP你用的编译器名否则库和二进制会链接到不同的 C 运行时上运行阶段的崩溃非常难排查。3.3 编译过程中的三个确认点编译时间根据机器性能不同大约在四十分钟到一个半小时之间。别等到编译结束才去检查结果编译过程中可以分几个节点确认状态第一看编译日志最开始有没有输出Building with SystemC support之类的信息。不同版本提示文字不完全一样但只要出现 systemc 目录下的源文件进入编译列表就说明开关生效了。如果整个编译过程里所有文件名都集中在src/、base/、cpu/、mem/这些目录而systemc/目录完全没有出现那基本可以断定USE_SYSTEMC没有生效。第二看链接阶段。正常启用 SystemC 后最后一步链接 gem5.opt 可执行文件时链接器会从$SYSTEMC_HOME/lib-linux64/下找libsystemc.so。如果这里报错找不到库问题多半出在路径上回到 3.1 检查环境变量即可。第三也是最关键的一步——编译结束后验证产物。仅凭“scons 没报错”就能说明 SystemC 进去了吗不能。因为 scons 有可能构建了一个完全不含 SystemC 支持的目标文件只是恰好没报错而已。所以强烈建议做一下验证ls -lh build/ARM/gem5.opt ldd build/ARM/gem5.opt | grep -i systemc如果ldd输出能看到libsystemc.so的依赖项说明 SystemC 确实被链接进 gem5 了。如果链接的是静态库ldd不会显示这时可以改用nm build/ARM/gem5.opt | grep sc_main或者strings build/ARM/gem5.opt | grep sc_elab来确认符号存在。3.4 静态库和动态库的选择SystemC 编译安装时默认会同时生成动态库和静态库。gem5 默认链接的是动态库libsystemc.so这也意味着运行时环境里必须能找到它。最稳妥的方式是把$SYSTEMC_HOME/lib-linux64永久加入LD_LIBRARY_PATHexport LD_LIBRARY_PATH/opt/systemc-2.3.4/lib-linux64:$LD_LIBRARY_PATH如果你希望产物完全不依赖外部动态库比如要丢到 CI 环境或者容器里跑可以在编译 SystemC 时配置--disable-shared只生成静态库gem5 构建时会自动选择静态链接。缺点是最终可执行文件体积会大不少而且后续更新 SystemC 后必须重新链接。日常开发调试阶段我建议保持动态库方式切换版本更灵活。4. 最小联仿 Demo让 CPU 发起的读写落到 SystemC 外设上4.1 联仿的两种组织方式谁是主控方环境编译成功后下一步是跑通一个最小 Demo证明双方的通信链路真的通了。在动手之前需要先理解联合仿真的组织方式。网上资料看着乱其实本质只有两种一种是 SystemC 作为顶层主控。顶层是sc_main()SystemC 仿真内核驱动整个仿真时间gem5 作为一个特殊的 SystemC 模块被实例化到其中sc_start() 启动后gem5 的模拟逻辑在 SystemC 调度器的节拍里运行。这种方式的优点是外设模型的 integration 更自然时序控制器由 SystemC 统一管理。另一种是 gem5 作为主控方SystemC 只是其中一块外设加速器。这种模式在工程上也有应用但集成越深越复杂。对初学者来说我强烈建议先走第一种也就是 SystemC 顶层驱动的模式。这也是 gem5 官方 systemc 示例采用的方式参考资料最多踩坑概率最低。从代码层面看第一步就是找到 gem5 源码里systemc/目录下的示例工程通常会有examples/或tests/子目录。先把官方最小示例原封不动编译运行一遍确认环境本身没问题然后再加入自己的外设模型。4.2 最小 SystemC 外设模块怎么搭假设我们要实现一个最简单的寄存器设备它的功能是只要 CPU 写入一个地址就在 SystemC 侧打印一行日志然后返回一个固定状态。这个模块的骨架大致如下我写的是结构示意不同版本类的命名和接口会有差异但概念是通用的#include systemc.h SC_MODULE(MyDevice) { // 事务入口由联合仿真适配层回调 void handle_transaction(Transaction txn) { if (txn.addr BASE_ADDR) { std::cout [SystemC Device] write addr0x std::hex txn.addr data0x txn.data std::endl; txn.status TRANSACTION_OK; } else { txn.status TRANSACTION_ERROR; } } SC_CTOR(MyDevice) {} };真正让 gem5 和 SystemC 连起来的是“适配层”它负责把 gem5 总线上的一次读写请求包装成上面这个handle_transaction能够处理的Transaction对象然后再把处理结果返回给 gem5。这一步官方参考资料里有现成的基类或者接口要继承第一次做不需要自己发明照着示例改就行。我实际遇到的最大困惑是不知道该继承哪个类、注册哪个回调函数。这里分享一个高效的查询思路直接在 gem5 源码的systemc/目录下搜索带slave、port、device字样的文件重点看定义处附近的注释。不同版本里适配器有的走 TLM target socket有的走 gem5 原生端口接口差别不小但搜索定位的效率远比逐行读文档高。4.3 gem5 侧配置外设映射SystemC 外设模型写完还不够gem5 侧必须知道“这块外设挂在哪个地址区间”。这通常在 gem5 的配置文件里显式声明把 SystemC 外设模块实例化成 gem5 配置文件里的一个对象给它分配基地址和空间大小。比如# 示意配置实际类名和参数以版本为准 from systemc import SystemcDevice systemc_dev SystemcDevice(addr_base0x10000000, addr_size0x1000) system System() system.add_memory_controller(systemc_dev)这个配置的意思是当 CPU 访问 0x10000000 到 0x10000fff 这段地址时gem5 内部不再把它当作普通内存访问处理而是打包成本地事务转发给 SystemC 适配器最后由我们实现的MyDevice模块响应。注意这一步非常容易犯的错是地址配置错误导致永远不知道外设有没有被访问到。我跑第一个 Demo 时因为基地址写错CPU 发出的访问全部被内存子系统当成未命中处理SystemC 侧没有任何日志输出。排查了很久才发现是两个地址不一致一台电脑里写的是 0x40000000配置文件里写的是 0x10000000两边根本不在同一个空间。4.4 运行观察点如何确认事务真的落到了外设Demo 运行起来后不要急着看复杂指标先确认三件事第一SystemC 侧的设备日志有没有出现。如果 CPU 确实执行了对外设地址的读写而你又在事务入口打了打印那么模拟输出里必然包含对应日志。这一步可以证明 gem5 到 SystemC 的下行通路是通的。第二响应数据返回路径是否正常。可以写一个测试程序对设备地址做一次读然后在 gem5 侧把读到的值打印出来如果读到的值等于 SystemC 模块写进响应的值说明上行通路也是通的。第三SystemC 侧的时间推进是否和 gem5 侧一致。最简单的验证方式是观察设备日志里附带的时间戳看它是否随模拟时间自然增长而不是一次性全部打印在开头。如果出现时间不推进十有八九是联仿时钟配置出了问题这一块在下一章详细说。5. 联调中最常见的坑版本、链接与启动卡死的完整排查链路5.1 版本不匹配导致的崩溃症状与解决联合仿真环境最经典的问题是 SystemC 库和 gem5 集成代码版本不匹配。常见症状有两种第一种是运行阶段直接 segfault 或者抛出std::bad_alloc。这种崩溃往往和编译器 ABI 不兼容直接相关。如果你用系统包管理器装的 SystemC而 gem5 是用更新版本的 GCC 编译的两边对同一个复杂模板的定义可能出现在不同翻译单元里行为就不一致了。解决方案前面也讲过自己编译 SystemC保证两边工具链一致是最省事的。第二种是启动时提示某个 SystemC 内核版本校验失败。新版 SystemC 库内部有其实没有任何状态检查但联合仿真适配层的初始化代码往往假设了某个版本的具体接口布局。如果 SystemC 太老适配层调用某个函数时行为不符合预期就会在初始化阶段默默失败然后整体卡住。这类的排查思路是回归到官方示例如果官方示例也卡住就是版本问题。5.2 链接失败和运行时报错的逐个排除我把这段时间遇到过的问题整理成了表症状和解决方式一目了然症状根因解决方式编译时找不到systemc.hSYSTEMC_HOME未设置或路径不对检查SYSTEMC_HOME指向安装根目录确认include/systemc.h存在链接时报cannot find -lsystemc库路径未包含lib-linux64或lib-linux在LD_LIBRARY_PATH和 scons 配置中补充正确的库目录运行时提示libsystemc.so: cannot open shared object fileLD_LIBRARY_PATH没有包含 SystemC 库目录执行export LD_LIBRARY_PATH/opt/systemc-2.3.4/lib-linux64:$LD_LIBRARY_PATH编译成功但ldd看不到 SystemC 依赖链接的是静态库或开关未真正生效用nm/strings验证符号确认USE_SYSTEMC1已生效这里有个非常重要的排查思路把“编译成功”和“链接成功”分开看。许多新手一旦看到 scons 顺利完成就以为环境没问题然后在运行时碰到各种离奇错误。实际上编译只代表源码语法和头文件没问题链接才代表对象被真正放到最终产物里。每次改动环境变量或升级 SystemC 后都重新做一次 3.3 节里的三个验证点能省掉大量无效排查时间。5.3 启动阶段卡死时钟同步和初始化顺序这个坑我自己踩得最深也最值得展开说。表现为程序启动后SystemC 侧打印了一两行初始化日志然后整个进程就卡住了既不崩溃也不继续输出CPU 利用率也不高。根因通常是两个一个是时钟同步没有建立起来SystemC 仿真内核的时间精度和 gem5 侧的时间刻度没有对齐另一个是初始化顺序不对sc_start()先于 gem5 侧的初始化完成被调用gem5 模块还没准备好接事务SystemC 就试图驱动仿真循环结果双方在事件同步上互相等待。排查时不要上来就深挖内部实现先做两步。第一步把 gem5 的调试标志打开加入和你 SystemC 集成相关的 debug flag比如--debug-flagsSystemC看适配层的初始化流程走到哪一步了。理论上适配层会在日志里打印它和 SystemC 内核建立连接的状态如果日志停在建立连接之前说明 gem5 侧初始化没有完成。第二步在 SystemC 侧给sc_start()之前和之后各加一行打印确认 SystemC 内核确实收到了启动调用。把执行顺序对齐问题基本就浮出水面了。5.4 调试武器的使用顺序面对复杂联仿问题时我推荐的调试顺序是这样的先从最外层日志入手也就是 gem5 的--debug-flags和 SystemC 侧的std::cout定位问题在哪个大的阶段然后再考虑用测试程序缩小范围比如只做一次地址写不要一上来就加载完整固件最后才考虑用 GDB 设断点直接在事务回调函数上打断点确认有没有进入 SystemC 模块。GDB 调试联合仿真程序有一个值得记住的技巧既然 SystemC 模型本身是普通 C你完全可以在handle_transaction函数里设断点。如果断点能命中说明从 gem5 到 SystemC 的通信链路是通的问题大概率在响应路径如果断点根本不会命中则说明 gem5 侧的事务根本没有被路由到 SystemC问题出在地址映射或者适配器的端口连接上。这个二分法能帮你把排查范围缩小一半以上。另外SystemC 的 warning 默认可能不会阻止仿真继续但有些 warning 其实暴露了严重问题。建议在模型初始化阶段强制把 warning 升级为 errorsc_report_handler::set_actions(SC_WARNING, SC_DISPLAY | SC_ERROR);这样一旦出现时钟精度不匹配、槽函数未绑定之类的问题仿真会立刻停下来并报错而不是带着隐患继续跑避免把问题掩盖在后期的随机崩溃里。最后分享一个我自己的使用习惯每次动过环境变量、升级过 SystemC 或切换 gem5 版本之后我不会直接跑复杂仿真脚本而是先把systemc/目录下最小的官方示例重新编译运行一遍。这个动作只要几分钟却能过滤掉八成环境类问题。等官方示例确认正常再跑自己的模型心里就有底得多。下一章我打算写怎么在联合仿真环境里挂一个真实驱动模型把中断、DMA、地址映射串起来等流程跑稳了再回来更新。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。