上周给一块 RISC-V 32 位内核的板子做裸机固件,在 Ubuntu 24.04 LTS 上竟然找不到一个现成的 riscv32 交叉编译器。官方源里躺着的是 riscv64-linux-gnu 那套,riscv32 的目标只能自己去第三方发布页翻。一开始我图省事直接拉源码编译 riscv-gnu-toolchain,binutils、GCC、newlib、GDB 四件套从 configure 到 make 折腾了两个多小时,中间还踩了版本不匹配的坑。后来换成预编译工具链,十分钟解决问题,干净利落。
这篇文章就把我这次的完整安装过程、选型思路和排错经验整理出来,给需要在 Ubuntu 24.04 上快速拿到 riscv32 预编译工具链的同学一条可复现的路径。你会看到为什么我不建议自己编译、如何区分裸机工具链和 Linux 用户态工具链、下载之后怎么验证包没问题、PATH 与第一次编译怎么配置,以及最后怎么用 QEMU 把程序真正跑起来验证整条链路。
1. 为什么我劝你优先用预编译包:源码编译的时间账与Ubuntu 24.04的现状
1.1 源码编译的时间账:一小时起步,还不一定成功
很多人一听说“工具链”,第一反应是拉 riscv-gnu-toolchain 源码自己编。这个仓库本身没毛病,问题在于它默认会同时构建 binutils、GCC、newlib、GDB 四件套,构建顺序和版本组合非常讲究。叉乘一下,交叉编译工具链的构建时间和宿主机器性能直接挂钩。八核十六线程、CPU 频率正常的机器上,全量构建大概三四十分钟到一小时;如果你是在虚拟机里做,四核的配置下等上两三个小时也不奇怪。
我这里不是故意劝退。真实情况是,源码编译还存在一个隐形成本:失败后的定位。GCC 构建中常见的“configure: error: cannot compute suffix of object files”这类错误,往往需要你回头查依赖版本、看 config.log,对新手来说非常打击积极性。我第一回自己编的时候,就是 GDB 和系统自带的 libgmp 版本冲突,编译到一半直接中断,找问题又花了半小时。等你把环境理顺、把版本对齐,三四个小时就没了。
预编译包把这些都帮你处理完了。发布者会用固定的 GCC 版本、固定的 binutils 版本、固定的 newlib 版本,在 CI 流水线上跑完一整套构建,然后打成一个压缩包。你拿过来解压就能用,第一次就能成功。对绝大多数做嵌入式开发、学 RISC-V 指令集、写裸机 demo 的人来说,预编译包是明显更理性的选择。
1.2 Ubuntu 24.04 官方仓库里到底有什么、缺什么
我在 Ubuntu 24.04 上敲apt search riscv翻了一遍,能看到的主要是这几类:
- riscv64-linux-gnu 系列的交叉编译器,比如
gcc-riscv64-linux-gnu、binutils-riscv64-linux-gnu - riscv64-unknown-elf 裸机工具链相关包
- QEMU 模拟器相关包,比如
qemu-system-misc、qemu-user
关键问题是:这些包里几乎都是 riscv64 目标,riscv32 的交叉编译器在官方仓库里基本是空白。有人会觉得,那我把 riscv64 工具链装上,加个 multilib 说不定就能编 32 位。实际操作中,riscv64 的 multilib 默认往往只覆盖 riscv64 自身的 ABI 组合,你很难用它编出干净的 riscv32 裸机镜像。与其折腾这种方式,不如直接找 riscv32 的专用预编译包。
还有一个容易混淆的点:Ubuntu 的包管理里也有gcc-riscv64-unknown-elf这种裸机工具链,它编译出来的 ELF 文件目标架构确实是 RISC-V,但默认模型是 64 位。你想要 riscv32,就得自己显式传-march=rv32...和-mabi=ilp32,而且它的库文件(newlib 变体)也不一定是按 32 位配置的。我实测下来,还是单独装一套 riscv32 工具链省心,路径清晰、不会被系统包干扰。
1.3 预编译包的风险点与应对策略
预编译包当然不是完全没有风险,尤其是“到底从哪下载”和“这个包是在哪个系统上编出来的”这两件事。前者关系到包是否完整,后者关系到动态库依赖是否能在你的发行版上跑起来。
我的应对思路很固定。下载时只从有长期维护的 Release 页面拿,优先看最近一年内还有更新的版本,避免下载到那种三五年没动过的老包。解压之前一定要执行一次sha256sum校验,用发布页上给出的哈希值比对,不要只图快按下回车。
动态库兼容性方面,Ubuntu 24.04 比较新,系统自带的 glibc 版本和 libstdc++ 版本都比较高。如果你下载的预编译包是很久以前打的,GDB 或者 GDB 的 Python 扩展可能因为依赖旧的 libncurses 或旧版 Python 而起不来。这个我会在第 6 章专门讲排查链路,这里先记住一个原则:尽量选近期发布的、明确注明支持“Ubuntu 24.04 / glibc 2.39”或类似字样的版本。
2. 动手前必须分清楚:unknown-elf与linux-gnu两种riscv32工具链
2.1 unknown-elf与linux-gnu的区别一句话讲清楚
在看任何预编译工具链下载页面时,你首先会碰到两种前缀完全不同的包:riscv32-unknown-elf-和riscv32-linux-gnu-。用一句话概括这两者的区别:前者是为“没有操作系统的裸机”准备的,链接的是 newlib 这种轻量 C 库,产物是烧录到 Flash 或者加载进模拟器的 ELF 固件;后者是为“运行在 Linux 用户态里的程序”准备的,链接的是 glibc,产物是依赖 Linux syscall 的普通可执行文件。
我习惯打个比方。裸机工具链像是教一个在荒岛上独自生活的人,所有东西都要自己准备:自己写启动代码、自己设置栈指针、自己初始化串口,甚至printf背后都要你自己实现输出函数。Linux 用户态工具链则像是教一个在现代化城市里的人,你只管调用别人已经建好的基础设施,开个文件、写个终端、请求系统时间都有现成的接口。
初次接触 RISC-V 的朋友往往会选错,因为看到“预编译工具链”就下意识觉得是那个带 Linux 的。实际上,嵌入式裸机、RTOS、学习指令集、写 bootloader,这些场景用的都是unknown-elf版本。只有在你想交叉编译一个能在 RISC-V Linux 系统上跑的用户态程序时,才需要linux-gnu版本。
2.2 按你的用途选型:嵌入式裸机/RTOS/模拟教学
结合我自己的开发场景,大多数情况下 riscv32 的目标代码都是用riscv32-unknown-elf-gcc来编的。原因很现实:目前 riscv32 的 Linux 用户态生态比较稀薄,32 位用户态程序能跑的 Linux 发行版镜像也不好找;而裸机生态非常成熟,QEMU 里有现成的 virt 模拟器,各种 RISC-V MCU 的开发板也都倾向于用裸机工具链烧写固件。
文章后面我用 QEMU 做验证,也是基于这套裸机工具链。你只需要下载riscv32-unknown-elf前缀的预编译包,然后编译、链接、加载进模拟器,整个过程不需要 Linux 内核参与,非常适合把工具链的可用性验证一遍。
如果你确实是奔着交叉编译 Linux 用户态程序去的,那么请确认下载的是riscv32-linux-gnu前缀的包,并且要准备好对应的 sysroot 和 QEMU 用户态模拟器。那套流程会更复杂,不是本文重点,但趁着选型阶段说清楚,能帮你少走弯路。
2.3 版本与镜像选择:rv32imac还是rv32gc、怎么辨别新包
另一个选型问题是架构扩展组合。RISC-V 的指令集是可裁剪的,同一个 rv32 可以有不同的扩展集合:rv32i是基础整数指令,rv32imac在基础之上加了乘除、原子指令和压缩指令,rv32imafdc又加了单精度浮点、双精度浮点和压缩指令。预编译工具链通常会默认支持一整组扩展,但关键是你编译时用-march告诉编译器目标芯片支持哪些。
从预编译包的下载选择来说,我会优先选默认配置为rv32imac的版本。这个组合在 RISC-V 32 位的小型控制器里覆盖度最高,也最符合“低成本、低功耗”的设计定位。如果你手里有带浮点单元的内核,再选支持对应浮点扩展的版本也不迟。
辨别一个包是不是足够“新”,除了看发布日期,还可以看它附带的 GCC 版本号。GCC 13 系列已经非常稳定,GCC 14 也逐步铺开。选择发布日期在一年内、GCC 版本在 13 或者 14 的包,在 Ubuntu 24.04 上踩到旧库依赖问题的概率会小很多。
3. 下载、校验与解压:把工具链安放在系统目录里的完整操作
3.1 目录规划与权限方案
预编译工具链解压之后体积不小,加上库文件和调试文件,轻松超过 1GB。我不建议直接解压到家目录下随便扔着,也不建议在/bin或者/usr/bin里逐个复制可执行文件。最常用的做法是统一放进一个专属目录,方便后续升级和回滚。
个人开发机,我推荐~/riscv32-toolchain;如果是多人共用或者需要配合 CI 构建,放在/opt/riscv32更合适。放在/opt下时要注意权限,直接sudo mkdir /opt/riscv32然后sudo tar -xJf解压,最后sudo chown -R $USER:$USER /opt/riscv32把这个目录归自己管理。这样做的好处是,之后编译工程、写脚本调用工具链时都不需要反复输入 sudo。
我在目录命名上会特意带上工具的版本语义,比如~/riscv32-toolchain-13.2。这样如果某天下了新版本,我可以直接并行放一个新目录,在 PATH 里切换测试,而不是覆盖原有环境。多版本工具链共存的这个话题,第 6 章还会再提到。
3.2 获取预编译包的命令行操作
下载预编译包的入口通常是发布页面里的 Release 附件,文件名的格式类似riscv32-unknown-elf-gcc-13.2.0-xxx.tar.xz。浏览器下载当然可以,但我更喜欢命令行,因为后面要立刻做校验,命令行可以一气呵成:
cd ~/Downloads wget https://example.org/path/riscv32-unknown-elf-gcc-13.2.0-xxx.tar.xz wget https://example.org/path/riscv32-unknown-elf-gcc-13.2.0-xxx.tar.xz.sha256 sha256sum -c riscv32-unknown-elf-gcc-13.2.0-xxx.tar.xz.sha256看到OK字样说明文件的哈希对得上。如果提示FAILED,要立刻放弃这个包,大概率是下载过程中文件损坏,或者内容与发布方声明不一致。哈希验证是很多新手容易跳过的步骤,我在这里多说一句:预编译工具链是要写进固件的,原始完整性直接影响后面所有工作的可信度,几十秒的校验开销无论如何都不该省。
3.3 解压后的目录体检:bin、lib、riscv32-unknown-elf每个文件夹是干嘛的
校验通过之后开始解压。这里有个容易出错的细节:很多发布包解压后会带着一层顶层目录,比如riscv32-13.2.0/。如果直接解压到安装目录,就会形成/opt/riscv32/riscv32-13.2.0/...这种多层嵌套,后续访问时路径又臭又长。用--strip-components=1可以去掉这一层:
sudo mkdir -p /opt/riscv32 sudo tar -xJf riscv32-unknown-elf-gcc-13.2.0-xxx.tar.xz -C /opt/riscv32 --strip-components=1 sudo chown -R $USER:$USER /opt/riscv32解压完先别急着配置,看一遍目录结构,确认装的是不是你想要的:
ls /opt/riscv32/bin正常情况下你会看到一堆riscv32-unknown-elf-开头的工具:gcc、g++、as、ld、objcopy、objdump、readelf、gdb。另外两个目录也值得了解。riscv32-unknown-elf/里面放的是目标系统相关的头文件、库文件和链接脚本,包括 newlib 的各类库;lib/gcc/riscv32-unknown-elf/13.2.0/里面则是 GCC 内部的 crt 文件和 multilib 信息。知道这些目录的作用,后面排查链接错误时会快很多。
4. PATH配置与第一次编译:让riscv32交叉编译器正常“开口说话”
4.1 PATH配置的两种写法和生效方式
安装只是把文件放到磁盘上,要让系统“认得”这些工具,必须把它的 bin 目录加进 PATH。我见过两种比较主流的做法。
第一种是写在用户的 shell 配置里:
echo 'export PATH=/opt/riscv32/bin:$PATH' >> ~/.bashrc source ~/.bashrc第二种是放在系统全局配置里,适合多用户共享的情况:
echo 'export PATH=/opt/riscv32/bin:$PATH' | sudo tee /etc/profile.d/riscv32.sh新开一个终端或者重新登录后即可生效。配置完成后用riscv32-unknown-elf-gcc -v验证一下。正常会输出一长串信息,重点看Target: riscv32-unknown-elf这一行,只要确认 Target 是 riscv32,基本就没问题。
提示:如果
which riscv32-unknown-elf-gcc找到的路径不是你刚配置的那个,说明 PATH 顺序被其他目录干扰了。这个问题第 6 章有详细排查,这里先把“指向哪”记在心里。
4.2 用GCC -v确认版本、C库和搜索路径
-v参数不只是看版本,它还会告诉你编译器的内部配置。我每次拿到新工具链都会执行:
riscv32-unknown-elf-gcc -v riscv32-unknown-elf-gcc -print-sysroot-print-sysroot会输出工具链内部 sysroot 的路径,也就是它默认搜索头文件和库文件的根目录。如果这个路径指向一个不存在的位置,后面编译任何程序都会报找不到头文件。预编译包一般已经配置好相对路径,输出结果指向安装目录下的某一层级就说明结构正常。
-v输出里还能看到 GCC 内置的配置参数,比如--with-arch=rv32imac这类信息。如果你的劳动工具链默认架构和你的板子不一致,就得靠编译时的-march参数兜底。所以这一步的价值是“确认工具链是怎样被构建出来的”,和安装第 2 章的选型预期对得上号。
4.3 编译最小C程序并阅读ELF信息
工具链能跑起来还只是第一步,真正让它“开口说话”的是第一次编译。我用一个最简单的无依赖程序做冒烟测试:
/* smoke.c */ int add(int a, int b) { return a + b; } int main(void) { return add(1, 2); }编译命令加上了 RISC-V 架构参数:
riscv32-unknown-elf-gcc -march=rv32imac -mabi=ilp32 -O2 -o smoke.elf smoke.c riscv32-unknown-elf-readelf -h smoke.elf注意看readelf输出里的Machine: RISC-V和Flags。如果 Flags 里能看到 RISC-V 相关标识、Class 是 ELF32,说明你拿到的是真正的 32 位产物。很多人在这一步会发现,自己之前用 riscv64 工具链编出来的 ELF 其实是 ELF64,Clsss 那一栏直接就暴露了问题。
那-march=rvi32imac -mabi=ilp32是什么意思?-march是告诉编译器目标芯片支持哪些指令扩展,-mabi是告诉它函数调用时怎么传参、怎么用寄存器。ilp32表示整数、长指针、指针都是 32 位,这是 32 位目标最常用的 ABI。给不同板子编译时,这两个参数要保持和芯片设计一致,贸然混用会导致链接失败或者运行崩溃。
5. 别停在编译这一步:用QEMU把裸机Hello真正跑起来
5.1 安装QEMU virt模拟器组件
编译出 ELF 只说明工具链“语法正确”,还不代表它能产出真正可执行的固件。要验证整条链路,我最喜欢的方式是用 QEMU 模拟一个 RISC-V 32 位虚拟机,把刚才的编译产物加载进去跑。QEMU 是开源虚拟化软件,安装非常方便:
sudo apt update sudo apt install qemu-system-misc sudo apt install qemu-userqemu-system-misc提供的是完整系统模拟器,里面包含qemu-system-riscv32和qemu-system-riscv64。qemu-user提供用户态模拟器,在交叉开发中偶尔也要用到。安装完成后执行:
qemu-system-riscv32 --version能输出版本号就说明模拟器已经就位。QEMU 在 Ubuntu 24.04 的版本足够新,完全可以跑最新的 RISC-V 32 位模拟。
5.2 裸机Hello的内核:启动汇编、UART输出、链接脚本
这里我们要写一个最简单的裸机最小程序。它不依赖 Linux,不依赖任何库,直接把字符串写到模拟器的串口外设上。整个过程涉及三个文件:启动汇编、主 C 文件、链接脚本。
启动汇编负责设置栈指针然后跳到 main:
/* startup.S */ .section .text.start .globl _start _start: li sp, 0x80200000 call main 1: wfi j 1b主 C 文件直接操作 UART 外设寄存器。QEMU virt 机器的串口基地址固定在0x10000000。
/* main.c */ #define UART0_BASE 0x10000000UL void uart_putc(char c) { volatile unsigned char *uart_thr = (volatile unsigned char *)UART0_BASE; *uart_thr = c; } void uart_puts(const char *s) { while (*s) uart_putc(*s++); } int main(void) { uart_puts("Hello, riscv32 toolchain on qemu!\n"); return 0; }链接脚本把代码段加载地址定在 0x80000000,这是 RISC-V virt 机器物理内存的起始地址。
/* link.ld */ OUTPUT_ARCH(riscv) ENTRY(_start) SECTIONS { . = 0x80000000; .text : { *(.text.start) *(.text*) } .rodata : { *(.rodata*) } .data : { *(.data*) } .bss : { *(.bss*) } }编译的命令需要注意几个重要参数:
riscv32-unknown-elf-gcc \ -march=rv32imac \ -mabi=ilp32 \ -Os \ -ffreestanding \ -nostdlib \ -nostartfiles \ -T link.ld \ -o hello.elf \ startup.S main.c-ffreestanding告诉编译器这是一个不依赖标准启动过程的裸机程序,-nostdlib禁止自动链接标准库,-nostartfiles禁止链接默认的启动文件,-T link.ld指定链接脚本。这几项是裸机开发的标配,缺一不可。
5.3 运行与调试输出解读
编译成功会生成hello.elf。运行它:
qemu-system-riscv32 \ -machine virt \ -m 128M \ -nographic \ -kernel hello.elf-machine virt选择 RISC-V virt 模拟机器,-m 128M给虚拟机分配 128MB 内存,-nographic让串口直接映射到当前终端。如果你看到终端里打印出:
Hello, riscv32 toolchain on qemu!那么恭喜你,工具链从汇编、编译、链接到加载运行,整条链路全都通了。退出 QEMU 的方法是先按Ctrl+A,再按X。
这个测试的意义远比一个 Hello 大。它证明了你下载的预编译工具链中的 GCC、汇编器、链接器、C 库和启动过程组合在一起是自洽的,而不是孤立的“编译器能编译个空函数”而已。以后你换了新版本工具链,或者换了新板子,这套最小工程就是你最可靠的冒烟测试工具。
6. 安装排错实战:五个高频报错与完整排查链路
6.1 GCC能跑但gdb起不来:动态库依赖问题
这是我遇到次数最多的问题。riscv32-unknown-elf-gcc能正常输出版本号,但一执行riscv32-unknown-elf-gdb,终端直接报:
riscv32-unknown-elf-gdb: error while loading shared libraries: libncurses.so.5: cannot open shared object file: No such file or directory问题根源是预编译包的 GDB 是在旧系统上构建的,依赖了旧版 curses 库,而 Ubuntu 24.04 默认只提供 libncurses6。排查思路分两步:先用ldd看它到底缺哪些库:
ldd /opt/riscv32/bin/riscv32-unknown-elf-gdb确定缺的库名之后,优先考虑换一个更新的、明确支持 24.04 的预编译包。如果必须用这个版本,才考虑安装兼容包。我的建议是不要在一棵树上吊死:GDB 只是工具链的一小部分,你完全可以先用 GCC、binutils、newlib 完成编译,调试功能等找到合适的包版本再补上。实际工作中,很多裸机调试场景通过串口日志就能覆盖大部分需求,GDB 配合 OpenOCD 属于后话。
6.2 cannot execute binary file:下载成了riscv64版本
另一个高频错误是执行riscv32-unknown-elf-gcc时直接报:
bash: /opt/riscv32/bin/riscv32-unknown-elf-gcc: cannot execute binary file: Exec format error这个错够让人困惑的,因为文件明明存在。用file命令一查就明白了:
file /opt/riscv32/bin/riscv32-unknown-elf-gcc如果输出显示ELF 64-bit LSB executable, ARM aarch64或者RISC-V 64-bit,说明你下载的包和宿主机的 CPU 架构不匹配。尤其是从某些镜像站下载时,容易手快拿成 ARM 版或者 riscv64 版。我上周就犯过一次这个错误,看到包名里带 riscv32 就以为万事大吉,却没注意到那是“给 riscv32 机器跑的宿主机工具”,而不是“运行在 x86_64 上、目标是 riscv32 的交叉工具链”。
所以下载前先确认两件事:宿主架构是不是x86_64(可以用uname -m看),以及发布页上是否写着x86_64等字样。文件级验证做这一步,能省下整整一个排查周期。
6.3 -march=rv32ic报错:架构扩展组合怎么选
编译时如果随意拼写架构扩展,会遇到:
cc1: error: invalid argument to option '-march=rv32ic'原因是 GCC 对-march参数非常严格,扩展字母必须符合指令集版本和顺序规则,不是所有组合都能排列组合。RISC-V 32 位最常见的合法组合是rv32i、rv32im、rv32imac、rv32imafdc。其中a代表原子指令、c代表压缩指令、m代表乘除、f代表单精度浮点、d代表双精度浮点。
我给新手的经验是:不知道扩展该写什么,就用rv32imac。这是绝大多数 MCU 级 RISC-V 内核的基线配置,兼容性最好。确认板子的 ISA 扩展可以看芯片手册或者readelf -A查看当前 ELF 的架构属性。每个 GCC 版本能识别的合法组合都可能不同,必要时用:
riscv32-unknown-elf-gcc --print-multi-lib查看当前工具链内部支持的多库配置,这比猜靠谱得多。
6.4 QEMU启动后完全没有输出:链接地址与加载方式
这是第五章 QEMU 验证中最容易翻车的一步。现象是 QEMU 启动了,但终端一片空白,没有任何输出。可能的原因很多,我按实际排查频率排在前三位的:
第一,链接地址错了。link.ld里. = 0x80000000改成别的地址,或忘记.text.start段放在最前面,PC 起点落到错误位置,CPU 直接跑飞。用readelf -h hello.elf看 Entry point 是否为 0x80000000,再用riscv32-unknown-elf-objdump -d hello.elf看 0x80000000 处是不是_start的代码。
第二,你用了-bios而不是-kernel加载固件。-bios需要的是一个完整的 BIOS 镜像,而裸机 ELF 用-kernel更合适,QEMU 会根据 ELF 头把代码加载到正确的物理地址,并把启动 PC 设置到入口点。
第三,UART 地址写错。在 QEMU virt 机器里,16550 串口的基址是0x10000000。如果写成了0x10001000或者别的偏移,程序跑是跑了,但输出发不出去。裸机调试没有 printf,出现这种情况只能靠逐项排除。
我习惯把readelf -h、objdump -d、qemu启动命令三者的信息放在一起比对,五分钟内基本能找到问题。
6.5 多版本工具链共存时的PATH污染
如果你机器上同时装过 riscv64 工具链、riscv32 工具链或者别的嵌入式工具链,容易发生“我明明配置了/opt/riscv32/bin,但编译器还是老的”这类情况。这属于 PATH 顺序污染。
排查先看:
which riscv32-unknown-elf-gcc riscv32-unknown-elf-gcc --version如果which指向的不是你预期的路径,说明在 PATH 里有其他目录优先挡在了前面。解决办法是把你当前工具链的 bin 目录放在最前面:
export PATH=/opt/riscv32/bin:$PATH注意不能写成$PATH:/opt/riscv32/bin,那个顺序会让系统先找到旧的。写到 shell 配置里时,也要确保追加的这一行在 PATH 相关配置的最后。
多版本共存的另一个坑是riscv32-unknown-elf-gcc和riscv64-unknown-elf-gcc都装了之后,编译脚本里写死了riscv64-unknown-elf-gcc,而你以为用的是 32 位。顺手检查一下 Makefile 里的交叉编译前缀,这是很多工程“莫名写错固件”的真正原因。
最后分享一个我个人的小习惯。装完工具链的第一时间,就把第 5 章那个最小工程原样跑一遍,并把gcc --version、QEMU 版本、下载包的 SHA256 记录下来放在项目目录的一个说明文件里。工具链这种基础设施,不冒烟测试直接投入项目,等于把隐患埋在起点。先花十分钟验证,比之后在几百个源文件里怀疑编译器要划算得多。