看到标题你可能以为我要聊乐高那个 Spike 魔方机器人,其实不是。这个 Spike 是 RISC-V 指令集模拟器,来自riscv-isa-sim项目,编译出来那个可执行文件就叫 spike。它干的事情很朴素:在没有真实开发板的情况下,用软件把一颗 RISC-V CPU 跑起来,加载你的程序、执行指令、把结果返回给宿主机。我这次装它,是为了验证一条自定义 RISC-V 指令在模拟器里的行为,结果光搭环境就耗了大半天,前前后后踩了十来个坑。
这篇文章不是官网 README 的翻译,是我实打实撞墙之后的记录。从依赖安装、环境变量、编译选项,到最终spike pk hello跑通,每一段都标了问题现象和解决路径。适合两类人看:一是刚接触 RISC-V、想在本机搭一套纯软件模拟环境的学生或工程师;二是已经把 Spike 装到一半、卡在某个莫名其妙的报错上的人。内容按我实际操作的顺序展开,建议从头顺着读,也可以直接跳到对应报错段查。
1. 动手前先理清这组"套件":Spike、pk 和工具链各自干什么
1.1 Spike 只是 CPU 模拟器,不是操作系统,也不是编译器
很多人的第一个误区就是把 Spike 当成一个"能跑 C 程序的软件"。实际上 spike 只模拟 RISC-V 核心、内存模型、中断和部分外设,它本身不提供 C 库,也不认识 Linux 的系统调用。你往里面塞一个普通 ELF 可执行文件,它不知道该怎么处理,因为没人帮它做程序加载、栈初始化、系统调用转发这些事。
这个"帮它做杂活"的角色就是 pk,也就是 Proxy Kernel,代理内核。pk 是一个极小的裸机运行时,负责把目标程序从 ELF 文件加载进模拟内存,然后把程序里用到的printf、exit这类系统调用转发给宿主机。没有 pk,spike 只适合跑纯汇编或者非常底层的裸机代码;有了 pk,你才能舒服地跑 C 程序。
再看编译这一环。要让 spike + pk 跑起来,你得有 RISC-V 的交叉编译器。最常见的两个目标是riscv64-unknown-elf-和riscv64-linux-gnu-,前者是 newlib 裸机工具链,pk 用的就是这套;后者面向 Linux ABI,想跑 Linux 用户态程序需要走更复杂的启动流程,不建议第一次就碰。
一句话记住三者关系:工具链负责把 C 代码编译成 RISC-V 指令,spike 是那块"虚拟 CPU 插槽",pk 是插槽上的启动引导和系统调用代理。三者缺一个,spike pk hello就跑不起来。
1.2 版本搭配:不是越新越好,但最好别跨太久
riscv-isa-sim 和 riscv-pk 虽然属于同一个 RISC-V 生态,但它们是独立仓库,tag 并不总是对齐。我这次直接拉了两个仓库的 master 分支,编译没问题,运行也很稳定。如果追求稳妥,可以用 riscv-isa-sim 官方 release tag,再配上同时期附近的 riscv-pk tag,基本不会出兼容问题。
工具链方面,riscv-gnu-toolchain 默认的 newlib 目标是riscv64-unknown-elf,和 pk 的默认 host 正好对上。如果你机器上已经装了别人编译好的工具链,先确认riscv64-unknown-elf-gcc在不在 PATH 里,再决定要不要重新编。工具链编译非常耗时,非必要不重复。
1.3 我实际用的版本组合
我这边实测的组合是:riscv-isa-sim 的 master 分支(2024 年后拉取)、riscv-pk 的 master 分支、riscv-gnu-toolchain 的 master 分支。操作系统是 Ubuntu 22.04,GCC 版本 11.4。这个组合没有出现明显的不兼容,后面所有命令都基于这套环境。
| 组件 | 仓库 | 构建方式 | 安装前缀 |
|---|---|---|---|
| Spike 模拟器 | riscv/riscv-isa-sim | autotools | --prefix=$RISCV |
| 代理内核 pk | riscv/riscv-pk | autotools | --prefix=$RISCV --host=riscv64-unknown-elf |
| 交叉工具链 | riscv/riscv-gnu-toolchain | gnu make | ./configure --prefix=$RISCV+make |
这里有个容易忽略的点:pk 构建时的--host=riscv64-unknown-elf不是随便填的参数,它会让 configure 去找riscv64-unknown-elf-gcc来编译 pk。如果你没先装 newlib 工具链,或者工具链没进 PATH,pk 的 configure 会在这一步步死。反过来,如果你用本机 gcc 把 pk 编出来了,那是个 x86 程序,spike 根本没法把它当作 RISC-V 负载加载,后面报错更难看。
2. 环境准备阶段最常见的三个坑:依赖、目录和 PATH
2.1 dtc 和 libfdt-dev:configure 直接挂掉的一个隐性组合
第一次跑../configure,我收到的报错是:
checking for dtc... no configure: error: "dtc not found"这个好办,装device-tree-compiler就行。我装上之后再跑 configure,结果 configure 倒是过了,make 到一半开始报:
fatal error: libfdt.h: No such file or directory这才是坑。Spike 的 fesvr(前端服务器)在解析 device tree 时需要操作 libfdt 库,而 configure 只检查了dtc这个二进制有没有,其实编译时还需要libfdt.h头文件和libfdt.so链接库。在 Debian/Ubuntu 上,这个库在单独的libfdt-dev包里,只装device-tree-compiler是不够的。
所以直接一步到位:
sudo apt update sudo apt install -y autoconf automake autotools-dev libtool \ pkg-config device-tree-compiler libfdt-dev \ libboost-regex-dev libmpc-dev libmpfr-dev libgmp-dev不同发行版包名不太一样,我做了一个临时对照表:
| 系统 | 需要安装的包 |
|---|---|
| Ubuntu / Debian | device-tree-compiler libfdt-dev libboost-regex-dev |
| Fedora / RHEL | dtc libfdt-devel boost-devel |
| Arch Linux | dtc boost |
| macOS (Homebrew) | dtc boost |
如果你用新版 riscv-isa-sim,boost.regex 的依赖可能已经去掉了,但装上也无害。我在旧分支上确实遇到过Could not link to boost::regex的 configure 报错,当时就是靠补libboost-regex-dev解决的。
2.2 RISCV 环境变量:不是锦上添花,而是 configure 的默认前缀
很多人装软件习惯不设环境变量,直接./configure --prefix=/usr/local。Spike 这里有个隐藏行为:如果不显式传--prefix,configure 会去看$RISCV环境变量,有就用它当前缀,没有才落到/usr/local。也就是说,$RISCV不是一个可选的"建议变量",它直接影响安装路径。
我在第一次安装时忽略了这一点,导致 spike 装到了/usr/local/bin,pk 装到了/usr/local/riscv64-unknown-elf/bin,工具链散落各处,后面排查路径问题花了不少时间。强烈建议一开始就统一规划:
export RISCV=$HOME/riscv export PATH=$RISCV/bin:$PATH export LD_LIBRARY_PATH=$RISCV/lib:$LD_LIBRARY_PATH把这三行写进~/.bashrc,然后source ~/.bashrc。目录不一定要在/opt,放自己家目录反而省得跟 root 权限纠缠。如果你确实想放/opt/riscv,记得先sudo chown -R $USER /opt/riscv,不然make install会卡在 permission denied 上。
2.3 CXX 环境变量残留导致 configure 找不到编译器
这个坑很隐蔽,容易发生在你之前编译过其他交叉工具链的机器上。如果你在 shell 里export CXX=riscv64-unknown-elf-g++,再去跑 spike 的 configure,它会把本机的模拟器代码当成 RISC-V 目标来编译,报错通常长这样:
checking for C++ compiler default output file name... configure: error: in `/home/user/riscv-isa-sim/build': configure: error: C++ compiler cannot create executables第一反应往往是"依赖没装全",其实只要echo $CXX看看,如果是交叉编译器的路径,直接unset CC CXX清掉再跑 configure 就行。我的教训是:编译不同项目前,先确认当前环境的 CC/CXX/CFLAGS 是不是残留了别的项目的配置,尤其在同一台机器上切换不同交叉编译任务时。
3. 编译期间踩过的坑:autogen、configure 与 make 的连锁反应
3.1 没有 configure 脚本:autoreconf 报错
从 GitHub 拉下来的 riscv-isa-sim 源码目录里,不一定带已经生成好的 configure 脚本。我第一次直接执行:
./configure --prefix=$RISCV结果告诉我没有这个文件。当时有点懵,后来才发现仓库用的是 autotools 标准流程,需要先跑仓库里的autogen.sh生成 configure。
git clone https://github.com/riscv/riscv-isa-sim.git cd riscv-isa-sim git submodule update --init --recursive ./autogen.sh这里会调autoreconf生成一系列构建文件。如果报autoreconf: not found,说明第一节里那几个 autoconf/automake/libtool 包没装齐。装齐后重新跑./autogen.sh即可。
git submodule update --init --recursive也要做,riscv-isa-sim 依赖了一些外部代码,比如 riscv-opcodes 相关的数据文件。漏掉这一步,编译到后面可能出现encoding.h缺失或者No rule to make target之类的错误,表面上看跟 submodule 毫无关系,排查起来很痛苦。
3.2 为什么我坚持用独立 build 目录
riscv-isa-sim 支持 out-of-source 构建,也就是源码目录之外建一个 build 目录,在 build 里跑../configure。我强烈建议不要图省事直接在源码根目录./configure,因为这样会把很多 generated 文件混进源码树,之后你想在多个配置之间切换(比如一个 rv64 构建、一个带 commitlog 的构建)就得反复清理,非常容易出问题。
我实际跑的是:
mkdir build && cd build ../configure --prefix=$RISCV make -j$(nproc) make installnproc是 Linux 下的 CPU 核数命令,macOS 没有,可以用sysctl -n hw.ncpu替代。注意这里make默认编的是模拟器本体,不包含 pk,pk 要单独下载 riscv-pk 仓库单独编,别等模拟器装完才发现没有 pk。
3.3 make 期间链接不到 libfdt / boost,先别急着重装
如果你 configure 已经过了,make 到一半报链接错误,比如找不到-lfdt或者 boost 库相关符号,先检查两件事。
第一,libfdt-dev是否真的装上了,头文件在不在/usr/include/libfdt.h。第二,configure 缓存。autotools 会把检测结果缓存到config.cache或 build 目录的config.status相关文件里,你后补装了依赖之后,直接再次 make 不一定重新检测,最稳妥的做法是删掉 build 目录重新建一个,再跑一遍 configure 和 make。我吃过这个亏:补装 libfdt-dev 后反复 make 都是同一个报错,差点怀疑人生,后来发现是 configure 缓存作祟,删 build 重来一次就好了。
3.4 make install 之后还要检查安装产物
make install顺利结束不代表万事大吉。建议检查一下$RISCV下面是否有这几个东西:
ls $RISCV/bin/spike ls $RISCV/lib/libfesvr.solibfesvr.so是 fesvr 前端服务器的核心库,spike 运行时需要动态加载它。如果这个库没装上,后面跑 spike 会直接报共享库错误,这个我放在下一节详细说。
4. 运行阶段:不是编译完就能跑起来
4.1 找不到 libfesvr.so:动态链接器的经典问题
编译完 spike,兴奋地敲下:
spike --isa=rv64gc结果是:
spike: error while loading shared libraries: libfesvr.so: cannot open shared object file: No such file or directory原因很简单:spike 可执行文件在链接时记录了它需要libfesvr.so,但动态链接器的搜索路径里没有$RISCV/lib。手动安装到非系统前缀时,这个目录不会自动进 ldconfig 配置,必须靠LD_LIBRARY_PATH补上。
export LD_LIBRARY_PATH=$RISCV/lib:$LD_LIBRARY_PATH我建议把这行也写进~/.bashrc,否则每次新开终端都会遇到。排查时可以先用:
ldd $(which spike)看看哪些库显示not found,这比瞎猜高效得多。另外注意区分两个变量:编译时链接库找的是LIBRARY_PATH,运行时加载库找的是LD_LIBRARY_PATH。有人编译时配好LIBRARY_PATH就以为运行没问题,其实两边各管各的。
4.2 spike 不会去 PATH 里找 pk,必须给完整路径
这是我这次踩得最莫名其妙的一个坑。我明明已经把 pk 装到了$RISCV/riscv64-unknown-elf/bin/pk,PATH 也包含了$RISCV/bin,但执行:
spike pk hello一直报类似could not open pk的错误。后来翻了源码才确认:spike 把第一个非选项参数当成目标文件路径,直接在当前工作目录打开,它不会像 shell 一样去 PATH 里搜索。也就是说,pk就是一个字面路径,当前目录没有叫pk的文件就失败。
正确的做法是写完整路径:
spike --isa=rv64gc $RISCV/riscv64-unknown-elf/bin/pk hello或者干脆进到 pk 所在目录再跑。为了省事,我习惯在环境变量里加一个:
export PK=$RISCV/riscv64-unknown-elf/bin/pk后续直接spike --isa=rv64gc $PK hello,清晰很多。
4.3 ELF 位宽和 --isa 不匹配
模拟器本身的 ISA 支持范围通过--isa指定,比如rv64gc代表 64 位、通用整数/浮点/原子操作加压缩指令扩展。如果程序和 pk 是 32 位的,却用--isa=rv64gc,就会遇到 ELF class 不匹配的报错,大意是目标 ELF 是 ELFCLASS32,而当前运行环境是 64 位。
这类问题最常见的根源是工具链选错了。riscv-gnu-toolchain 默认编出来的是 64 位 newlib 工具链,对应riscv64-unknown-elf-gcc。如果你机器上同时有 32 位工具链,编译时一定确认用的是哪个:
riscv64-unknown-elf-gcc -march=rv64gc -mabi=lp64d -O2 -static -o hello hello.c-march=rv64gc和-mabi=lp64d必须保持配对,同理 32 位要用-march=rv32gc -mabi=ilp32d。spike 端的--isa=rv64gc要和程序编译参数一致。不一致时不一定在加载阶段报错,有时候会等到执行浮点指令才触发 illegal instruction,那种错更让人头大。
4.4 用 pk 跑程序,目标文件要用 newlib 工具链编
还有一个概念性坑:不要用riscv64-linux-gnu-gcc编程序然后丢给 pk。pk 是裸机代理内核,它处理的 ELF 是面向 newlib 环境、按裸机 ABI 组织的;Linux 工具链编出来的程序默认链接 glibc,依赖 Linux 的系统调用接口和动态链接机制,pk 根本伺候不了。
判断方法很简单:
file hello如果是 newlib 工具链编的,通常会显示RISC-V ELF executable, statically linked之类的描述。如果用 Linux 工具链,很可能是动态链接,spike + pk 跑起来就各种诡异段错误。第一阶段的建议就是:所有的测试程序都统一用riscv64-unknown-elf-gcc编译,并加-static,等以后需要跑 Linux 用户态程序时再研究 bbl + vmlinux 那条更重的链路。
5. 从能跑到跑通的实战:一条完整命令怎么组合
5.1 最小可复现的完整流程
前面把坑拆开了,这里给一条从头到尾的完整命令链,我照着这套重新搭过一次,从零到跑通大概半小时,前提是网络和基础编译环境没问题。
# 1. 设定环境变量 export RISCV=$HOME/riscv export PATH=$RISCV/bin:$PATH export LD_LIBRARY_PATH=$RISCV/lib:$LD_LIBRARY_PATH # 2. 编译 newlib 工具链(第一次比较久) git clone https://github.com/riscv/riscv-gnu-toolchain.git cd riscv-gnu-toolchain ./configure --prefix=$RISCV make -j$(nproc) cd .. # 3. 编译 Spike 模拟器 git clone https://github.com/riscv/riscv-isa-sim.git cd riscv-isa-sim git submodule update --init --recursive ./autogen.sh mkdir build && cd build ../configure --prefix=$RISCV make -j$(nproc) make install cd ../.. # 4. 编译代理内核 pk git clone https://github.com/riscv/riscv-pk.git cd riscv-pk mkdir build && cd build ../configure --prefix=$RISCV --host=riscv64-unknown-elf make -j$(nproc) make install cd ../.. # 5. 写一个测试程序并运行 cat > hello.c <<'EOF' #include <stdio.h> int main() { printf("hello spike\n"); return 0; } EOF riscv64-unknown-elf-gcc -march=rv64gc -mabi=lp64d -O2 -static -o hello hello.c spike --isa=rv64gc $RISCV/riscv64-unknown-elf/bin/pk hello看到hello spike输出,说明整套环境通了。这里每个步骤都有前文提到的坑:工具链没进 PATH 时,pk 的 configure 会找不到riscv64-unknown-elf-gcc;spike 运行时不配 LD_LIBRARY_PATH 会报 libfesvr 缺失;不给 pk 完整路径又会出现 couldn't open。整套流程连起来跑,任何一个环节断了都能在对应步骤定位到原因。
5.2 常见报错速查表
我把这一路遇到的现象、原因、解法整理成一张表,方便你遇到问题时直接对照:
| 报错或现象 | 根因 | 处理办法 |
|---|---|---|
| configure: error: dtc not found | 缺 device-tree-compiler | 安装对应发行版 dtc 包 |
| make 报 libfdt.h 或 -lfdt 找不到 | 缺 libfdt-dev | 安装 libfdt-dev |
| configure 报 boost::regex 链接失败 | 旧版依赖 Boost.Regex | 安装 libboost-regex-dev |
| autoreconf: not found | autotools 工具缺失 | 安装 autoconf automake libtool |
| C++ compiler cannot create executables | CC/CXX 残留交叉编译配置 | unset CC CXX后重新 configure |
| make install 权限拒绝 | 前缀目录属主不是当前用户 | 调整目录属主或用 sudo |
| libfesvr.so cannot open | LD_LIBRARY_PATH 缺$RISCV/lib | 补充 LD_LIBRARY_PATH |
| could not open pk | spike 不走 PATH 找目标文件 | 使用 pk 的完整路径 |
| ELF class / ISA 不匹配 | 程序位宽和 --isa 不一致 | 统一 march/mabi/--isa |
| illegal instruction | 编译用到的扩展在 --isa 中未开启 | 确保 --isa 包含程序用到的扩展 |
| pk 无法运行 Linux 工具链产物 | pk 面向 newlib 裸机 ABI | 改用 riscv64-unknown-elf-gcc 编译 |
这张表基本覆盖了安装阶段的 90% 问题。如果你遇到的报错不在表里,优先用ldd、file和 configure 日志定位,比盲目重装有效得多。
5.3 进阶方向:自定义指令、commitlog 和 GDB 调试
环境跑通之后,Spike 真正的价值才开始显现。我最初装它就是为了验证自定义 RISC-V 指令,所以特意说下这个方向。
Spike 支持扩展指令集的模拟,如果你在工具链里加了自定义指令,并且通过--isa指定了包含该扩展的字符串,spike 可以把未知指令派发给自定义的processor_t处理逻辑。具体做法是在源码的decode.h或execute.cc中注册新的指令解码条目,然后重新编译模拟器。这个流程比在真实 FPGA 上验证快很多,适合指令集设计的前期验证。
如果你做的是性能分析或指令流跟踪,建议重新编译一个带 commitlog 的版本:
cd build ../configure --prefix=$RISCV --enable-commitlog make -j$(nproc) make install带 commitlog 的 spike 可以把每条指令的 PC、指令字、寄存器写回等信息按行打印,对调试乱序执行、异常处理这类问题非常有用。代价是模拟速度下降,所以平时用普通版本,需要追踪时再切到 commitlog 版本。
调试裸机程序时,Spike 还支持 GDB 远程调试。先让 spike 停在入口处:
spike --isa=rv64gc --gdb-port 9824 --halted $PK hello再另开终端:
riscv64-unknown-elf-gdb hello (gdb) target remote localhost:9824就可以像调试本地程序一样打断点、单步、看寄存器。对于没有真实开发板的 RISC-V 学习场景,这套组合基本能满足大部分调试需求。
我个人在实际踩过这些坑之后最大的体会是:Spike 的安装难度并不高,真正考验人的是"以为装好了但其实没装对"的中间状态。环境变量、依赖库、路径、工具链类型,任何一个环节错位,报错都会以最不直观的方式呈现。如果你在装的过程中卡住了,别急着重装整个工具链,先按速查表一项项排除,大概率比暴力重来省时间。