身边不少做嵌入式开发的朋友第一次拿到RISC-V开发板时,最头疼的往往不是写代码,而是怎么把riscv-gnu-toolchain这套交叉编译环境跑起来。这东西说简单也简单,无非是下载、配置、编译、安装几个步骤,但真操作起来,光是依赖缺失、configure报错、编译几个小时突然失败这些状况,就能劝退一大半人。
这篇文章是我自己从零搭建riscv-gnu-toolchain交叉编译环境的完整记录。我会把每一步为什么要这么做、参数怎么选、踩了哪些坑都讲清楚,而不是简单丢给你几条命令。无论你是刚接触RISC-V的新手,还是从ARM转向RISC-V的老手,这篇文章应该都能帮你少折腾几天。
1. 动手之前,先搞清楚riscv-gnu-toolchain到底是个什么东西
1.1 交叉编译:为什么电脑上的gcc编译不了RISC-V程序
先解决一个最基本的问题:为什么不能直接用Ubuntu自带的gcc?原因很简单——你的电脑是x86架构,而RISC-V开发板是RISC-V架构,两者指令集完全不同。
用生活里的事打个比方:你在国内写了一份中文文档,把它发给只会读英文的同事,对方肯定看不懂。交叉编译也一样,普通gcc生成的机器码是给x86 CPU“读”的,放到RISC-V CPU上,对方完全不认识。交叉编译工具链干的事情,就是在一台x86电脑上,生成RISC-V架构CPU能识别的机器码。
所以riscv-gnu-toolchain本质上是把整套GNU工具链(gcc编译器、binutils二进制工具、glibc或newlib运行库、gdb调试器)针对RISC-V目标平台做了一次完整移植和配置,让你能在x86主机上安心开发RISC-V程序。
1.2 riscv-gnu-toolchain项目里到底装了些什么
很多人误以为riscv-gnu-toolchain就是一个gcc,其实它是一个工具链全家桶。官方仓库里的核心组件包括:
- gcc:编译器前端和后端,负责把C/C++代码编译成RISC-V汇编和机器码
- binutils:包含汇编器as、链接器ld、反汇编器objdump等一系列二进制处理工具
- glibc:面向Linux系统的C标准库,提供printf、malloc这些基础函数
- newlib:面向裸机(bare-metal)和嵌入式RTOS的轻量级C库
- gdb:RISC-V架构的调试器,配合JTAG或QEMU使用
这一点特别重要,因为你后面编译时选哪个库,直接决定生成的是“裸机程序”还是“Linux应用程序”。我见过不少人在这一步搞混,结果折腾半天编出来的程序在目标板上跑不起来,原因就是工具链类型选错了。
1.3 先想清楚:你要的是裸机工具链还是Linux工具链
riscv-gnu-toolchain通过不同的make目标来区分两种工具链,这点非常关键:
- newlib工具链(默认):编译产物不带操作系统,适合跑在裸机环境、RTOS、裸机固件等场景。生成的编译器前缀是
riscv64-unknown-elf-gcc,这里的elf表示生成的是ELF格式的可执行文件,不依赖操作系统加载。 - Linux工具链:编译产物带glibc,依赖Linux内核,适合跑在完整的Linux系统上。生成的编译器前缀是
riscv64-unknown-linux-gnu-gcc,通过make linux来构建。
我在实际项目中一般是各编一套,两边场景都会用到。但如果你是纯新手,不确定该装哪个,先看你的目标板:板子上跑的是Linux系统,就选Linux工具链;板上是裸机、RTOS或者你只是写固件,就选newlib工具链。这个决定要在编译之前做,因为后面切换的成本很高。
2. 编译前的环境准备:依赖、源码和版本选择
2.1 宿主机依赖安装,一步都不能省
riscv-gnu-toolchain是从源码编译的,所以宿主机的软件环境和依赖必须齐全。这一步偷懒,后面configure阶段大概率会报错。我自己用的是Ubuntu 20.04/22.04系统,依赖安装命令如下:
sudo apt update sudo apt install -y autoconf automake autotools-dev curl python3 \ libmpc-dev libmpfr-dev libgmp-dev gawk build-essential \ bison flex texinfo gperf libtool patchutils bc \ zlib1g-dev libexpat-dev这里有三个依赖要特别说明,也是gcc编译绕不开的三兄弟:libgmp-dev(大整数运算库)、libmpfr-dev(多精度浮点运算库)、libmpc-dev(复数运算库)。gcc在编译过程中要做大量的数学运算和优化计算,这三者是硬依赖。如果你看到configure报错类似Building GCC requires GMP 4.2+, MPFR 2.4.0+ and MPC 0.8.0+,不用怀疑,就是这三兄弟没装好。
另外texinfo也很容易被忽略。它负责处理gcc文档的生成,版本太旧或者缺失,编译会在文档阶段直接卡死。flex和bison则是生成词法语法解析器用的,binutils和gcc的构建都离不开。
2.2 获取源码:几种方式的选择与对比
获取riscv-gnu-toolchain源码的主流方式有两种,我推荐优先使用官方仓库的归档包,省时省心。
第一种方式是用git克隆仓库,注意一定要带--recursive参数,因为项目依赖多个子模块(如gcc、binutils、glibc等都是以submodule形式引入的):
git clone --recursive https://github.com/riscv-collab/riscv-gnu-toolchain克隆完成后务必检查一下子模块是否完整:
git submodule update --init --recursive第二种方式是直接从GitHub Releases页面下载归档包。相比git方式,这种方式更适合网络不太稳定、或者不想等子模块慢慢拉取的场景。下载的是release归档包,不是某个开发中的中间状态,稳定性更有保障。
版本选择上,我的建议是:除非你要调试最新特性,否则别用master分支。master分支时刻在变,可能昨天还能编译通过,今天因为某个子模块更新就开始报错。我踩过一次这个大坑,后来就学聪明了,用release版本或者固定的tag版本(比如2023.10.21这类带日期的tag),至少保证整个团队用的版本是一致的,排查问题时也好对齐。
2.3 磁盘和内存要求,别等到编译到一半才后悔
编译这套工具链对硬件资源是有要求的,提前准备好能省去很多麻烦:
- 磁盘空间:源码解压后大约占2-3GB,编译产生的中间文件和安装产物加起来,建议预留20GB以上的空闲空间。我一开始以为10GB够用,结果编译到gcc阶段磁盘就爆了,白等了一个多小时。
- 内存和Swap:make -j高并发编译时,内存不够容易触发OOM(内存耗尽)被杀掉。我建议8GB内存的机器用-j4,16GB内存的用-j8,不要盲目拉满。如果物理内存偏小,提前配置好Swap空间,比如4GB的Swap,能救急。
提示:编译过程中如果系统突然卡死,优先检查是不是内存满了。Ctrl+Z暂停任务,用
free -h看一下内存和Swap状态,再决定要不要调整并发数。
3. 从源码编译到安装的完整实录
3.1 configure配置参数逐个说清楚
源码和环境都准备好后,进入源码目录,先创建一个构建目录(我习惯把编译产物和源码分开,方便清理):
cd riscv-gnu-toolchain mkdir build && cd build然后执行configure配置。这里有几个参数需要根据你的目标平台仔细选,我把最常用的几个讲清楚:
../configure --prefix=/opt/riscv --with-arch=rv64gc --with-abi=lp64d--prefix:指定工具的安装路径。我习惯装在/opt/riscv,这样所有RISC-V工具都在一个目录下,后续配PATH和查找都很方便。你也可以用$HOME/riscv,区别不大,但路径别带中文和空格。--with-arch:指定默认的目标架构。rv64gc表示64位RISC-V,其中g是通用扩展的缩写(包含IMAFD),c表示压缩指令扩展。如果你的芯片是RV32的(比如某些MCU),就需要改成--with-arch=rv32imac。--with-abi:指定二进制接口,即函数调用时参数怎么传、浮点数怎么处理。lp64d表示64位长整型指针,浮点数用硬件浮点寄存器传参。千万不要把arch和abi配得不匹配,比如rv32配lp64d,这种组合编译出来的工具链根本没法正常工作。
configure执行成功后,屏幕上会打印出配置摘要,告诉你将要构建哪些组件、默认架构是什么、sysroot路径在哪里。建议花十秒钟仔细看一下,确认没有配错。
3.2 make编译:两个不同的目标,两条不同的路
configure完成后,正式进入编译。这个环节有两个目标可选,理解它们再动手:
# 方式一:编译newlib裸机工具链 make -j$(nproc) # 方式二:编译Linux工具链(带glibc) make linux -j$(nproc)方式一的make不带参数,默认构建newlib工具链,生成的编译器是riscv64-unknown-elf-gcc。方式二则是make linux,构建带glibc的工具链,生成riscv64-unknown-linux-gnu-gcc。我刚才说过,这两个工具链用途完全不同:前者面向裸机,后者面向Linux系统。
这里提醒一下,make linux比make的编译时间更长,因为glibc的编译要分两轮:第一轮用临时编译器编出glibc库,第二轮再用完整编译器重新编一遍gcc的libgcc部分。我就是在这个环节踩过坑,第二轮编译时因为之前的configure参数没配对,整个报废重来。
编译时长方面,以我机器的实际经验(8核16线程,32GB内存),newlib工具链大约20分钟,Linux工具链需要40-50分钟。如果你的机器配置低一些,编译时间翻倍也很正常。这段时间我建议你该干嘛干嘛去,不要频繁打断编译进程,也尽量不要同时跑大负载的任务。
编译通过后,安装只需要一条命令:
sudo make install如果是Linux工具链,就执行sudo make install-linux,别搞混了。
3.3 安装完成后的目录结构解析
安装完成后,看一下/opt/riscv/bin目录,会发现一大批工具。我整理了一份对照表,方便你理解每个工具是干什么用的:
| 工具名 | 作用 | 类似x86下的对应工具 |
|---|---|---|
riscv64-unknown-elf-gcc | RISC-V编译器 | gcc |
riscv64-unknown-elf-as | RISC-V汇编器 | as |
riscv64-unknown-elf-ld | RISC-V链接器 | ld |
riscv64-unknown-elf-objdump | 反汇编、查看目标文件信息 | objdump |
riscv64-unknown-elf-objcopy | 格式转换(如ELF转bin/hex) | objcopy |
riscv64-unknown-elf-readelf | 查看ELF文件头信息 | readelf |
riscv64-unknown-elf-gdb | RISC-V调试器 | gdb |
riscv64-unknown-elf-run | 在主机上模拟运行RISC-V程序 | qemu-riscv64 |
如果安装的是Linux工具链,前缀会变成riscv64-unknown-linux-gnu-,工具功能一一对应,只是运行时依赖glibc。
注意:安装完成后务必运行
riscv64-unknown-elf-gcc -v确认版本。如果提示command not found,说明你的PATH没有配置好,看下面这一节。
4. 环境变量配置与工具链功能验证
4.1 PATH配置与常见小坑
安装完成不等于结束,还得让系统能找到这些工具。编辑~/.bashrc,在文件末尾加上一行:
export PATH=$PATH:/opt/riscv/bin然后执行source ~/.bashrc让配置立即生效。这里有个坑要提醒你:如果你用的是zsh,就要改~/.zshrc;如果用的是fish,配置方式又不一样。别机械照搬,先看看自己当前shell是什么。
还有个小技巧:配置完PATH后如果还是提示找不到命令,执行一下hash -r清空shell的命令哈希缓存。因为shell会把命令路径缓存起来,新装的工具可能识别不到,清一下缓存就正常了。
4.2 写一个C程序进行交叉编译验证
工具链能不能用,最直接的验证方式就是编一个hello world。新建一个hello.c:
#include <stdio.h> int main() { printf("Hello RISC-V!\n"); return 0; }然后执行编译(以newlib工具链为例):
riscv64-unknown-elf-gcc -o hello hello.c如果命令没有任何输出,说明编译成功了(Unix哲学:没有消息就是好消息)。这时用file命令检查生成的可执行文件:
file hello输出应该是类似这样的:
hello: ELF 64-bit LSB executable, UCB RISC-V, version 1 (SYSV), statically linked, not stripped注意这里的关键信息UCB RISC-V,说明这确实是一个RISC-V架构的ELF文件,不是x86的。用readelf -h hello还可以看到更详细的架构信息。
然后还可以用objdump反汇编看一下生成的机器码:
riscv64-unknown-elf-objdump -d hello | head -30你会看到RISC-V的汇编指令,比如addi、lui、jal这些,跟x86的汇编完全是两套风格。到这里,至少证明工具链本身工作正常。
4.3 为什么编译出来的程序不能直接跑
新手经常问:我把这个hello直接拷贝到开发板上能跑吗?如果是裸机工具链编译的,还真不能直接跑。因为裸机环境下没有操作系统加载ELF文件、初始化运行环境,你需要把它烧录到芯片里配合启动代码运行。
如果你想先在电脑上验证程序的功能,可以用QEMU的用户态模拟(user-mode):
sudo apt install qemu-user qemu-riscv64 ./hello输出Hello RISC-V!,就说明这个交叉编译产物在逻辑上是正确的。这个方法在做算法验证、单元测试时特别实用,不用每次都在开发板上跑。
如果你编的是Linux工具链(riscv64-unknown-linux-gnu-gcc),编译出的程序在qemu-riscv64里跑的话,还需要指定动态链接器的路径,命令会多几个参数。这里有个小经验:裸机工具链编译的程序默认是静态链接的,qemu直接跑就行;Linux工具链默认动态链接,需要把sysroot里的库路径指给qemu。为了避免麻烦,我经常在编译时加-static参数,临时验证时省心很多。
5. 常见问题与排查技巧实录
5.1 编译工具链自身时报错怎么办
报错1:configure时提示缺少GMP/MPFR/MPC
这个在前面依赖部分已经说过,本质是系统缺库。如果你确认已经安装了libmpc-dev等包,还报错,可以检查一下是否配置了不干净的CFLAGS环境变量,或者系统里有没有老旧版本的gcc残留。用echo $CFLAGS看一下,有内容就先清空再试。
报错2:编译到gcc阶段报错,日志显示incompatible pointer type或internal compiler error
这种情况大概率是宿主机的gcc版本太老或太新,和riscv-gnu-toolchain某个子模块版本不兼容。建议先检查宿主机gcc版本:gcc --version。如果是老旧的gcc 5.x,建议先升级宿主机gcc到7以上的版本。如果版本正常还报错,那就换个release tag再试。
报错3:编译过程中磁盘空间不足
前面我建议预留20GB,这是有教训的。如果已经发生了,先删掉build目录重新来过,同时用df -h检查一下你的/opt目录有没有独立分区。我见过有人把/分区根目录只分了15GB,结果/opt和/home挤在一起,编译到一半就爆掉。这种情况最稳妥的做法是把--prefix改到空间充足的路径,比如/home/yourname/riscv。
5.2 编译目标程序时报错的常见原因
报错1:fatal error: stdio.h: No such file or directory
这个报错太典型了,原因几乎都是你用了宿主机的gcc而不是交叉编译工具链来编译。检查一下命令开头是不是riscv64-unknown-elf-gcc,以及PATH顺序是不是被宿主机的某个脚本改乱了。用which riscv64-unknown-elf-gcc看看实际解析到哪个路径。
报错2:cannot find crt0.o: No such file or directory
出现这个报错,说明链接器找不到C运行时启动文件。crt0.o是程序入口的启动代码,newlib工具链会把它放在sysroot的lib目录下。如果你configure时自定义了--with-sysroot但指向了错误路径,就会这样。检查方法:riscv64-unknown-elf-gcc -print-sysroot,看看打印的路径是否真的存在,且里面有lib和include目录。
报错3:编译出来的程序在目标板上运行就段错误(Segmentation fault)
这种情况很大概率是ABI不匹配。比如你编译时用了-mabi=ilp32生成了32位代码,但目标板上的Linux内核或固件是64位的,两者不兼容。或者反过来,目标芯片不支持浮点单元,而你默认用了lp64d硬浮点。解决方法是确认目标板的架构和浮点单元情况,编译时显式指定-march和-mabi:
riscv64-unknown-elf-gcc -march=rv64gc -mabi=lp64d -o hello hello.c riscv64-unknown-elf-gcc -march=rv32imac -mabi=ilp32 -o hello hello.c5.3 多版本工具链共存与切换
实际开发中,你机器上很可能同时存在好几个RISC-V工具链:系统包管理器装的、自己编译的、IDE自带的。它们之间冲突起来很烦人,我这里的经验是:
经验一:装完后检查头文件路径
用riscv64-unknown-elf-gcc -E -x c - -v < /dev/null这个命令,可以打印出工具链的搜索路径,包括头文件路径和库路径。如果发现路径指向了别的版本工具链的目录,说明环境变量里混入了一些奇怪的配置。重点检查CPATH、LIBRARY_PATH、C_INCLUDE_PATH这几个环境变量,很多时候就是它们惹的祸。
经验二:用版本前缀区分工具链
不同工具链的前缀不同:riscv64-unknown-elf-、riscv64-unknown-linux-gnu-、riscv64-linux-gnu-、riscv-none-embed-这些都是不同的工具链。用命令ls /opt/riscv/bin/riscv*看一下你手上有哪些,心里有数。然后用绝对路径调用,永远不会错。
经验三:小心系统升级把工具链搞挂
Ubuntu的apt upgrade有时候会升级glibc或者其他系统库,导致你自己编译的交叉工具链在运行时出现version 'GLIBC_X.XX' not found之类的报错。这个问题的根源是工具链内部的某些工具依赖宿主机的库,升级后被破坏了。我的习惯是:重要项目锁定一个工具链版本,并且放在/opt下,不轻易动它。万一真的被破坏,重新make install一次通常能恢复。
5.4 编译很慢?先优化并发和磁盘IO
如果你编译工具链的时间特别长,除了换更好的机器,还有两个实操优化点:
一是合理设置make并发数。-j$(nproc)是全部核心参与编译,但实际上gcc编译阶段会同时启动多个cc1进程,每个进程都吃大量内存。8GB内存的机器用-j4比-j8更稳定,16GB内存以上再考虑拉满。
二是把build目录放到SSD上。工具链编译涉及大量小文件的创建和读写,在机械硬盘上编译时间可能比SSD多出将近一倍。如果你有条件,把整个构建放在SSD分区,体验会好很多。
6. 再补几句悄悄话:一些更省事的替代方案
自己从头编译riscv-gnu-toolchain这套流程,写出来洋洋洒洒几千字,实际操作起来也确实不是一件轻松的事。如果你只是想快速搭好开发环境开始写代码,其实还有更省事的路子可以走。
比如很多操作系统发行版的软件源里,直接就有RISC-V工具链的预编译包。以Ubuntu为例:
sudo apt install gcc-riscv64-unknown-elf一条命令装完,省去了源码编译的所有麻烦。这套预编译包虽然版本不一定是最新的,但它稳定、干净,用来学习刚好合适。如果你用的是Arch Linux,可以用riscv64-unknown-elf-gcc相关的AUR包;macOS下用Homebrew也有现成的配方。
另外,如果项目对工具链版本有特定要求,也可以去看看各大芯片原厂或厂商提供的SDK。很多RISC-V芯片厂商在SDK里会带一套完整可用、经过验证的工具链,比自己从头折腾要省心得多。不过需要注意版权和授权,特别是商用项目,提前确认好许可协议。
当然,自己编译这套工具链也并非没有价值。编译过程中你对整个工具链的构成、依赖关系、版本匹配逻辑都会理解得更深入。我身边不少资深工程师,搞了多年开发也不一定清楚gcc的编译还依赖GMP、MPFR这些库,而如果你完整地走完这一遍,以后遇到这类问题基本可以秒定位。
我个人在实际操作中的体会是:如果是初次接触RISC-V,先用现成的预编译工具链把整个开发流程跑通,建立信心,之后再找时间完整编译一遍源码,把原理搞清楚,比一上来就死磕编译要高效得多。两者结合,才是最快的学习路径。