
上周给一块 RISC-V 32 位内核的板子做裸机固件在 Ubuntu 24.04 LTS 上竟然找不到一个现成的 riscv32 交叉编译器。官方源里躺着的是 riscv64-linux-gnu 那套riscv32 的目标只能自己去第三方发布页翻。一开始我图省事直接拉源码编译 riscv-gnu-toolchainbinutils、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-gnuriscv64-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就得自己显式传-marchrv32...和-mabiilp32而且它的库文件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-components1可以去掉这一层sudo mkdir -p /opt/riscv32 sudo tar -xJf riscv32-unknown-elf-gcc-13.2.0-xxx.tar.xz -C /opt/riscv32 --strip-components1 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-archrv32imac这类信息。如果你的劳动工具链默认架构和你的板子不一致就得靠编译时的-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 -marchrv32imac -mabiilp32 -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 其实是 ELF64Clsss 那一栏直接就暴露了问题。那-marchrvi32imac -mabiilp32是什么意思-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 \ -marchrv32imac \ -mabiilp32 \ -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 的方法是先按CtrlA再按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 -marchrv32ic报错架构扩展组合怎么选编译时如果随意拼写架构扩展会遇到cc1: error: invalid argument to option -marchrv32ic原因是 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 记录下来放在项目目录的一个说明文件里。工具链这种基础设施不冒烟测试直接投入项目等于把隐患埋在起点。先花十分钟验证比之后在几百个源文件里怀疑编译器要划算得多。