
第一次打开gem5源码仓库的人大概率会和我当初一样愣住几万个源文件C和Python混在一起还有一堆以.slicc结尾的奇怪协议文件。别慌这东西没有看起来那么复杂。GEM5是计算机体系结构研究里最常见的模拟器没有“之一”可能也不算夸张它用来在真实芯片流片之前验证架构想法、评估性能功耗、跑各种benchmark。做体系结构的学生离不开它做软硬件协同的工程师也经常拿它做早期探索。这篇“gem5开始之旅”的第一篇我想从一个实际跑过不少实验的角度带你把这个模拟器的来龙去脉、安装编译、核心设计、第一个Hello World实验以及常见坑一次性说清楚。目标很简单你跟着这篇文章走完手里能真正跑起来一个gem5实验能看懂输出能改配置而不是止步于“装上但不知道下一句命令该敲什么”。1. 先理解GEM5是什么再决定要不要装1.1 GEM5的前世今生两个模拟器合并的产物GEM5不是从零写出来的它由两个模拟器合并而来一个是密歇根大学的M5一个是威斯康星大学麦迪逊分校的GEMS。M5擅长做处理器核心、系统级建模支持多种指令集架构整体架构比较干净GEMS则把重点放在存储系统上尤其是缓存一致性协议的详细建模。两者在2006年前后合并成了我们现在看到的GEM5。这件事非常重要因为它直接解释了GEM5内部一个“看起来有点诡异”的结构内存子系统有两套实现。一套叫Classic Memory用起来简单、灵活、跑得快适合做CPU微架构和片上互连的研究另一套叫Ruby是当年GEMS那边带过来的精细化缓存一致性协议模拟器你可以用它实现和验证MESI、MOESI等各种缓存一致性协议甚至自己写一个新的协议。如果你只是跑单核benchmark用Classic Memory就够了如果你研究多核缓存一致性甚至异构系统Ruby才是主战场。新人如果一上来就听说“GEM5能模拟缓存一致性”然后去啃Ruby多半会被前两周的学习曲线劝退。我的建议是先通过Classic Memory把整个工具链摸熟再按需进入Ruby。1.2 GEM5能做什么、不能做什么GEM5是一个事件驱动的、模块化的离散事件模拟器。它的定位是体系结构级Architecture Level模拟不是寄存器传输级RTL仿真。这意味着它关心的是“一个指令经过流水线需要多少个时钟周期”“某个缓存替换策略的缺失率是多少”“一个内存控制器请求队列里排队延时有多大”而不是某个逻辑门在300MHz下能不能满足建立时间。它能做的事情非常多支持多种指令集架构x86、ARM、RISC-V、MIPS、SPARC、POWER等提供多种CPU模型从最简单的AtomicSimpleCPU、TimingSimpleCPU到乱序执行的O3CPU再到流水线化的MinorCPU支持两种运行模式SESystem-call Emulation系统调用模拟和FSFull System全系统模拟支持多核、多芯片、NUMA拓扑、缓存层级、片上网络、DRAM控制器等高级建模集成了一部分GPU建模能力比如AMD的HSA模型它不能做什么GEM5不是RTL仿真器所以你不能用它来做逻辑综合、时序收敛验证也不能把它直接当成性能测试的代用品——模拟器的性能数据是用来做相对比较和趋势分析的不是用来当作spec sheet上的绝对性能数字用。这个认识决定了你怎么解读实验结果。1.3 适合谁来学、怎么学才高效如果你属于下面几类人GEM5学起来是花得着时间的体系结构方向的研究生需要做缓存、预取、调度、指令压缩等课题做编译器或系统软件的人想评估新指令扩展对性能的影响做芯片前期探索的工程师想在架构冻结前快速比较多个候选方案纯粹对CPU内部工作原理好奇的开发者想通过模拟器看到一条指令从取指到退休的全过程学习路径上我不建议一上来就通读源码。正确顺序是先跑通官方示例 → 学会改配置脚本参数 → 学会看统计输出 → 再针对自己实验涉及的模块去读对应源码。GEM5的代码量非常大但模块化做得还算清晰如果你知道自己要找什么读起来效率会高很多。2. 环境准备与编译安装把GEM5跑起来是第一关2.1 操作系统选择与依赖安装GEM5官方支持Linux和macOSWindows需要借助WSL。我自己的主力环境是Ubuntu 20.04和22.04长期支持版踩坑最少。在动手编译前先把依赖装齐。以Ubuntu/Debian系为例sudo apt update sudo apt install -y build-essential git scons swig python3-dev python3-pip \ libboost-all-dev libprotobuf-dev protobuf-compiler \ libgoogle-perftools-dev libpng-dev pkg-config几个关键依赖需要注意scons是GEM5的传统构建工具类似make但用Python写构建脚本swig用于生成Python和C之间的接口绑定版本不能太老否则会报一些奇怪的编译错误libboost是GEM5很多模块依赖的C库尤其是序列化、线程等部分protobuf用于存储模拟过程中的跟踪信息像--trace相关的功能会用到如果你是macOS可以用Homebrew装同样的依赖但有些包的名称略有不同比如swig、scons、boost都是直接有的。2.2 用scons编译GEM5依赖装好后先把代码克隆下来git clone https://github.com/gem5/gem5.git cd gem5如果你想稳定复现某个教程或论文的实验我更建议切到一个release tag而不是直接跑最新的master开发版。因为GEM5开发版接口变动很频繁今天能跑的配置脚本下个月某个SimObject参数名可能就改了。git checkout v21.2.0然后开始编译scons build/X86/gem5.opt -j$(nproc)这里的“X86”是目标指令集架构你可以换成ARM、RISCV等。“gem5.opt”是构建目标类型GEM5提供了几种常见构建模式构建类型优化断言调试符号适用场景gem5.debug无有有使用gdb调试模拟器内部逻辑gem5.opt有有部分日常实验、功能验证的首选gem5.fast有无无长时间仿真追求运行速度我第一次编译的时候机器配置是8核16G内存大概跑了40分钟。如果你用的是老笔记本编译时间超过一个小时也正常。中间如果报内存不足减少并行度比如-j4。编译完成后build/X86目录下会出现gem5.opt这个可执行文件。你拿到模拟器的方式就是运行这个二进制文件后面跟上配置脚本或配置文件路径。2.3 版本选择的一个额外提醒GEM5从23.0版本开始逐步迁移构建系统到CMake到24.x版本官方推荐用CMake构建scons路线逐步退出。但市面上绝大多数教程、课程作业、论文实验都基于21.x或20.x的scons流程。所以如果你跟着某些开源课程做实验用21.2版本会顺畅很多如果你要学最新功能再看新版本的构建文档。这不是什么大问题但一定要有版本意识。我见过太多人拿着新版代码去跑老教程里的配置脚本结果报错一堆以为是自己的环境问题其实是版本API变了。3. GEM5的骨架SimObject、事件驱动与端口3.1 SimObject模拟器里一切都是对象GEM5的设计哲学可以总结为一句话一切硬件组件都是SimObject。CPU是SimObjectCache是SimObject总线是SimObject内存控制器是SimObject时钟域也是SimObject。每个SimObject在C里实现核心逻辑同时通过一份Python配置文件向用户暴露参数。GEM5启动时读入你的Python配置脚本实例化这些SimObject建立它们之间的连接关系然后开始模拟。这样做的好处非常明显你不用修改一行C代码就能通过Python脚本验证不同配置。比如我想把L1缓存大小从32KB改成64KB只需要在Python配置脚本里调整一下参数。对于做大量参数扫描的实验来说这简直是救命的功能。如果你想给GEM5添加一个全新的硬件模块一般需要三件事写一个C类继承SimObject写一个Python类定义它的参数然后在SConscript里注册。这个流程说起来简单实际上涉及时钟、事件、端口等一堆概念我们后面再展开。3.2 事件驱动模拟本质是一本“未来事件台账”很多第一次接触GEM5的人会困惑模拟器怎么知道“下一秒”该干什么它不是实时运行的而是事件驱动的。GEM5内核维护一个全局事件队列EventQueue。每个SimObject可以往这个队列里插入一个事件并指定该事件在未来多少个时钟周期后触发。内核不断取出队列里最近要触发的事件、执行它、执行过程中又可能插入新事件以此循环。模拟时间simTick是离散前进的而且是跳跃式的——不是每纳秒都扫一遍而是“下一个事件在哪就把当前时间推进到哪里”。这个机制你可以理解成手机上的日程表现在是早上8点你安排了8点30分开会、9点45分回邮件。内核不会一直盯着时钟看而是直接睡到8点30分处理完开会这件事再睡到9点45分。日程表上如果没有新事件手机就空转不日程表空了模拟就该结束了。理解事件驱动是看懂GEM5运行逻辑的基础。你以后如果自己写新的SimObject核心工作往往就是“根据当前事件决定几十个周期后触发另一个事件”。这也是为什么GEM5的C代码里到处是scheduleEvent、rescheduleEvent之类的调用。3.3 端口和内存层级组件之间怎么说话既然所有组件都是SimObject那它们之间怎么通信靠端口Port。端口是SimObject对外暴露的连接点。处理器核心有指令端口和数据端口缓存也有连接下一级缓存或总线的端口。在较新的GEM5版本中端口分成了request端口和response端口两类对应传输方向。传输的数据结构叫Packet里面带着请求地址、命令类型、数据大小、时间戳等信息。CPU和内存不是直接用一根线连起来的中间通过交叉开关XBar类似总线互联逐级连接。典型的内存访问路径是这样的CPU发出访存请求 → 经过端口到达L1 Cache → L1未命中则请求发往后级Cache或总线 → 经过L2/L3 → 到达内存控制器 → 访问DRAM → 响应沿原路返回。你自己写配置脚本时最重要的任务就是把这条路径上的每个组件创建出来然后用端口把它们连好。入门阶段最容易出的错就是忘了把某个缓存端口接到总线上或者把CPU的指令端口和数据端口接错了位置导致模拟一运行就崩溃。4. 第一个实验用GEM5运行Hello World4.1 准备一个可执行程序GEM5的SE模式可以直接运行一个用户态可执行文件而不需要启动整个操作系统。入门实验自然是Hello World。写一个最简单的C程序#include stdio.h int main() { printf(Hello, gem5!\n); return 0; }然后用gcc静态编译gcc -static hello.c -o hello为什么要静态编译因为SE模式本质上是在模拟Linux的系统调用接口动态链接的场景需要额外处理动态链接器的加载和共享库映射虽然新版本的支持逐渐变好但入门阶段静态编译是稳定性最高的方式。静态编译后模拟器只需要加载这一个可执行文件把入口地址设置好就能开始执行。如果你只是想快速跑通GEM5仓库自带了一些测试用的可执行文件比如ls tests/test-progs/hello/bin/x86/linux/里面通常有一个编译好的hello程序你可以直接用。如果要为ARM目标平台编译程序就需要用交叉编译器比如arm-linux-gnueabihf-gcc -static hello.c -o hello_arm然后在构建模拟器时选择ARM架构。4.2 最小的GEM5配置脚本官方推荐的做法是直接用configs/example/se.py它已经封装好了大部分参数./build/X86/gem5.opt configs/example/se.py -c ./hello --cpu-typeTimingSimpleCPU --caches这条命令的含义是用gem5模拟器运行hello程序CPU类型选择TimingSimpleCPU开启缓存层级。但如果你只看结果不看配置过几天再接触GEM5还是一头雾水。我更建议从头写一个最简单的配置脚本哪怕代码长得像官方示例的缩水版。下面这个脚本我用在21.x版本上是能直接跑的import m5 from m5.objects import * system System() system.clk_domain SrcClockDomain(clock1GHz, voltage_domainVoltageDomain()) system.mem_mode timing system.mem_ranges [AddrRange(512MB)] system.cpu X86TimingSimpleCPU() system.membus SystemXBar() system.cpu.icache_port system.membus.cpu_side_ports system.cpu.dcache_port system.membus.cpu_side_ports system.cpu.createInterruptController() system.cpu.interrupts[0].pio system.membus.mem_side_ports system.cpu.interrupts[0].int_master system.membus.mem_side_ports system.cpu.interrupts[0].int_slave system.membus.mem_side_ports system.mem_ctrl MemCtrl() system.mem_ctrl.dram DDR3_1600_8x8() system.mem_ctrl.port system.membus.mem_side_ports system.system_port system.membus.cpu_side_ports process Process() process.cmd [hello] system.cpu.workload process system.cpu.createThreads() root Root(full_systemFalse, systemsystem) m5.instantiate() print(Beginning simulation!) exit_event m5.simulate() print(Exiting tick %i because %s % (m5.curTick(), exit_event.getCause()))逐段解释几个关键点system.clk_domain定义了系统的时钟频率这里用1GHz。GEM5里时钟是独立于被模拟程序的它决定的是模拟器里那些硬件组件的节拍快慢。system.mem_ranges告诉模拟器物理内存范围SE模式下这里只是地址空间的声明实际内存由模拟器内部管理。system.cpu X86TimingSimpleCPU()创建了一个单核CPU实例。TimingSimpleCPU对每条指令都建立详细时序模型会发出带时序的访存请求适合入门理解执行流程。如果你只关心功能验证可以换成AtomicSimpleCPU跑得飞快但不对内存访问的时序细节建模。system.membus SystemXBar()是系统的中央交叉开关相当于一个总线。CPU的指令端口和数据端口都接到这个总线的cpu_side_ports上。createInterruptController()是x86处理器的必要步骤创建中断控制器并连接到总线。system.mem_ctrl创建了内存控制器并使用DDR3_1600_8x8作为DRAM模型。system.system_port用来让模拟器内部对特殊地址的访问也走这条总线。process对象定义要运行的程序process.cmd [hello]是命令行。m5.instantiate()把所有SimObject实例化m5.simulate()开始模拟直到某个退出条件触发。把这段保存为hello_gem5.py放在gem5根目录下然后运行./build/X86/gem5.opt hello_gem5.py正常情况下你会看到Beginning simulation! Hello, gem5! Exiting tick 13717500 because exiting with last active thread context这说明模拟器成功加载了hello程序通过系统调用模拟执行了printf把内容打印到标准输出最后模拟在某个tick数下正常退出。4.3 运行结果都在哪里模拟结束后gem5会在当前目录下生成一个m5out/文件夹这个路径可以用--outdir参数修改里面有如下关键文件config.ini/config.json完整的系统配置转储你可以在里面看到每个SimObject的参数值用于实验记录和复查非常方便stats.txt核心性能统计文件记录每条指令数、周期数、缓存命中率、带宽使用等simout.txt被模拟程序的标准输出如果你的程序打印了“Hello, gem5!”它会同时出现在终端和这个文件里stats.txt是后期分析的重头戏。第一次跑我建议你至少看一眼下面这几项simSeconds 0.000014 simTicks 13717... hostSeconds 0.000001再往下翻会看到CPU相关的统计system.cpu.numCycles 13717500 system.cpu.committedInsts 2这里committedInsts为2说明hello程序只提交了很少的指令不对其实GEM5统计的“提交指令”和你理解的用户程序指令数会有差异因为这个极简配置没有打开详细流水线统计。等你用O3CPU跑一个更复杂的程序stats.txt里的统计项会丰富得多。5. 进阶一步定制一个双核系统5.1 把单核配置改成双核跑通单核之后可以试着把系统扩展成双核。改动其实很小核心就是把system.cpu从一个CPU对象变成一个CPU对象列表system.cpu [X86TimingSimpleCPU() for _ in range(2)] for i in range(2): system.cpu[i].icache_port system.membus.cpu_side_ports system.cpu[i].dcache_port system.membus.cpu_side_ports system.cpu[i].createInterruptController() system.cpu[i].interrupts[0].pio system.membus.mem_side_ports system.cpu[i].interrupts[0].int_master system.membus.mem_side_ports system.cpu[i].interrupts[0].int_slave system.membus.mem_side_ports注意每个CPU都要单独创建中断控制器并连接总线。process.cmd如果只有一个程序GEM5默认会让这个程序只在一个CPU线程上跑其他核可能在跑idle任务。要真的让两个核都干活得用多线程程序或者在SE模式下用process.cmd启动多个工作负载分别绑定到不同核心。入门阶段可以先不管这个能成功建出双核系统并启动模拟就已经理解了配置脚本里“列表”这个核心范式。多核模拟对性能统计的影响马上就能看出来stats.txt里会分别出现system.cpu0.xxx和system.cpu1.xxx两组统计。通过对比单核和双核的统计数据你就能直观感受到并行度和性能数据之间的关系。5.2 先用Atomic模式验证功能再用Timing模式评估性能这是我自己做实验时养成的一个习惯也建议新人沿用在配置脚本开发阶段先把CPU类型设为AtomicSimpleCPU让模拟器以最快速度把功能跑通。Atomic模式不会对访存建立细粒度时序模型所以运行速度非常快特别适合验证“我的脚本逻辑对不对”。功能没问题之后再切到TimingSimpleCPU或O3CPU做正式的性能实验。这样做省时省力避免在配置脚本有歧义时还在那里等O3模拟器慢慢跑几十秒甚至几分钟。5.3 从stats.txt里读出门道模拟实验最终比拼的是谁对数据敏感。我见过不少人跑完实验只把stats.txt保存下来连看都不看就扔给导师。这样会丢掉很多信息。入门阶段建议先关注以下统计分组执行概况simSeconds、simTicks、hostSeconds反映模拟速度和规模CPU流水线system.cpu.numCycles、system.cpu.committedInsts、system.cpu.ipc算IPC就用committedInsts除以numCycles缓存部分system.cpu.iCache.overallHits::total、system.cpu.iCache.overallMisses::total、system.cpu.dCache.overallMissRate::total做缓存策略研究重点关注这些内存控制器system.mem_ctrl.readBandwidth、system.mem_ctrl.writeBandwidth、平均访存延迟等举个例子如果你想比较不同L1缓存容量对性能的影响可以只改配置脚本里缓存容量参数然后对比MissRate和IPC两列数据。GEM5为你做的就是把每种配置的硬件模型都放到同一个程序面前跑一遍剩下的解读和对照就是你的工作了。6. 常见问题排查与避坑速查表6.1 我自己踩过的坑汇总GEM5的坑很多不是功能复杂而是环境和版本问题。我把自己和身边人踩过的整理成一张表你可以直接收藏备查现象可能原因解决方法提示scons: command not found没有安装sconspip install scons或apt install scons编译时大量Python相关错误swig版本过旧升级swig到3.x以上编译到一半报internal compiler error并行任务过多、内存溢出降低-j并发数比如-j4切换版本后配置脚本报参数不存在gem5版本API变化先确认使用的release版本再匹配对应教程模拟运行后simout.txt为空程序路径不对或静态编译失败用绝对路径指定process.cmd重新静态编译stats.txt里simSeconds为0模拟未实际执行目标程序就退出了检查process.cmd里的程序是否存在检查SE模式入口是否正确FS模式下看不到终端输出FS终端设备配置不完整入门阶段建议先用SE模式FS模式等到需要跑操作系统级实验再研究m5out目录没有生成模拟器在初始化阶段就崩溃在命令行加--debug-flagsConfig等调试开关定位具体错误位置第6行的“模拟器在初始化阶段就崩溃”其实是新手遇到最多的一类问题。GEM5报错信息看起来很长但关键行一般是最后的fatal:或panic:信息。比如端口没接好它会明确告诉你哪个端口没有被连接。遇到这类错误先不要全文翻译报错从最后几行往前读。6.2 调试GEM5的几个实用技能学会定位问题是GEM5学习路上必须过关的一环。我常用的调试手段有三个。第一个是使用GEM5的debug flags。在运行命令后面加./build/X86/gem5.opt --debug-flagsExec hello_gem5.py这会输出每条被模拟指令的详细信息包括PC、汇编指令内容、寄存器变化等对理解程序在模拟器中的执行流程帮助很大。注意输出量极大跑大程序时记得重定向到文件。第二个是使用--debug-start参数指定从某个tick开始打印调试信息避免从开始就刷屏。比如./build/X86/gem5.opt --debug-flagsExec --debug-start100000 hello_gem5.py第三个是在自己的Python配置脚本里加打印函数把关键对象的状态打出来。GEM5的Python接口支持直接访问SimObject的属性你可以在m5.instantiate()前后打印出system.cpu.clock、system.mem_ranges等内容确认配置是否符合预期。6.3 一个关于学习路线的诚意建议网上关于GEM5的教程不少但质量参差不齐而且版本差异大。我的建议是核心只看两个来源一个是官方文档一个是gem5的源码示例。官方文档更新得还算及时虽然有些地方写得简略但至少准确。源码示例在configs/example/目录下像se.py和fs.py是两个最顶层的入口脚本顺着它们往下读你能了解到GEM5官方设计和推荐的配置方式。读源码的时候不要试图一下读懂每个类的每个方法。先画出“脚本是怎么一步步构建system对象的”这条主线再在实验需要时深入某一个分支。这样做两周基本能入门。最后再分享一个小经验每次跑实验前养成把配置文件、gem5版本、运行命令一起记录下来的习惯。GEM5实验有一个尴尬的特点——它的可复现性非常强因为所有随机性都可以通过种子控制但如果你自己都不记得当时怎么跑的回头想复现时就会很痛苦。我是后来才养成了每个实验目录里放一个README的习惯把命令和参数全部写进去。这个习惯比你去啃任何高级调试技巧都管用。