3. 固件与启动流程
交叉编译工具链,说白了就是一套能在你的x86笔记本上生成RISC-V可执行文件的工具集。你想想看,QEMU模拟的是RISC-V处理器,但你的开发机是x86架构,没有这套工具链,你写的C代码根本跑不起来。
我刚开始接触RISC-V时,第一件事就是折腾工具链。当时踩了不少坑,今天我把两种主流方式——从源码编译和使用预编译包——都给你讲透。
3.1 工具链的核心组件
一套完整的RISC-V交叉编译工具链包含以下组件:
| 组件 | 作用 | 典型名称 |
|---|---|---|
| 编译器 | 将C/C++源码编译为RISC-V汇编 | riscv64-unknown-elf-gcc |
| 汇编器 | 将汇编代码转为机器码 | riscv64-unknown-elf-as |
| 链接器 | 将目标文件链接为可执行文件 | riscv64-unknown-elf-ld |
| 库 | 提供标准C库支持 | newlib / glibc |
| 调试器 | 用于GDB远程调试 | riscv64-unknown-elf-gdb |
关键区分:裸机工具链(riscv64-unknown-elf-*)和Linux工具链(riscv64-linux-gnu-*)的区别在于是否依赖操作系统。做固件开发、裸机程序时,用前者;跑Linux内核时,用后者。
3.2 方式一:从源码编译GCC工具链
这种方式最灵活,但耗时最长。我记得第一次编译时,整整跑了两个多小时。不过,自己编译能精确控制版本和优化选项,适合深度定制场景。
3.2.1 安装依赖
在Ubuntu 20.04/22.04上,先装好基础工具:
sudo apt-get update
sudo apt-get install -y autoconf automake autotools-dev curl \
python3 python3-pip libmpc-dev libmpfr-dev libgmp-dev \
gawk build-essential bison flex texinfo gperf \
libtool patchutils bc zlib1g-dev libexpat-dev
我的经验:libmpc、libmpfr、libgmp这三个数学库经常被忽略,少了它们编译到一半会报错。建议提前装好,省得回头补。
3.2.2 下载源码
我习惯用riscv-gnu-toolchain这个官方仓库,它把GCC、binutils、newlib整合在一起:
git clone https://github.com/riscv-collab/riscv-gnu-toolchain.git
cd riscv-gnu-toolchain
git checkout 2023.12.20 # 选一个稳定版本
仓库里包含子模块,需要一并拉取:
git submodule update --init --recursive
这一步会下载GCC、binutils、newlib等子项目。网络不好的话,可能会卡住。我曾经在公司内网拉了一下午没成功,后来换了代理才搞定。
3.2.3 配置与编译
配置时指定目标架构和ABI。对于裸机开发,我通常这样配:
mkdir build && cd build
../configure --prefix=/opt/riscv64-elf \
--target=riscv64-unknown-elf \
--enable-multilib \
--with-arch=rv64gc \
--with-abi=lp64d
参数说明:
--prefix:安装路径,我放在/opt/riscv64-elf--enable-multilib:支持多种ISA组合(rv32i、rv64gc等)--with-arch=rv64gc:64位通用指令集,包含整数、乘除、原子、浮点--with-abi=lp64d:LP64数据模型,双精度浮点ABI
然后开始编译:
make -j$(nproc)
-j$(nproc)用满所有CPU核心。我建议第一次编译时先用-j4,因为并行太多容易把内存吃满。我的机器32GB内存,开16线程编译没问题,但8GB的机器建议用-j2。
注意:编译过程大约需要1-2小时。如果中途失败,先检查config.log,多半是依赖缺失或网络问题。不要盲目重跑,先看日志。
3.2.4 设置环境变量
编译完成后,把工具链加入PATH:
export PATH=/opt/riscv64-elf/bin:$PATH
为了永久生效,我习惯把它加到~/.bashrc里:
echo 'export PATH=/opt/riscv64-elf/bin:$PATH' >> ~/.bashrc
source ~/.bashrc
3.3 方式二:使用预编译工具链
如果你不想等两个小时,直接用预编译包。我个人更推荐这种方式,除非你有特殊定制需求。
3.3.1 Bootlin工具链
Bootlin(以前叫Linaro)提供了各种架构的预编译工具链,下载即用:
wget https://toolchains.bootlin.com/downloads/releases/toolchains/riscv64-lp64d/releases/riscv64-lp64d--glibc--bleeding-edge-2023.08-1.tar.bz2
tar -xjf riscv64-lp64d--glibc--bleeding-edge-2023.08-1.tar.bz2
export PATH=$PWD/riscv64-lp64d--glibc--bleeding-edge-2023.08-1/bin:$PATH
Bootlin的优点是版本新、支持glibc和musl两种C库。我做过一个项目需要musl库的静态链接,Bootlin直接提供了,省了我自己编译的功夫。
3.3.2 SiFive工具链
SiFive作为RISC-V的创始公司,官方提供了Freedom Studio开发套件,里面包含完整的工具链:
wget https://static.dev.sifive.com/dev-tools/riscv64-unknown-elf-gcc-8.3.0-2020.04.1-x86_64-linux-ubuntu14.tar.gz
tar -xzf riscv64-unknown-elf-gcc-8.3.0-2020.04.1-x86_64-linux-ubuntu14.tar.gz
export PATH=$PWD/riscv64-unknown-elf-gcc-8.3.0-2020.04.1-x86_64-linux-ubuntu14/bin:$PATH
SiFive的工具链非常稳定,我最早做RISC-V裸机开发时用的就是它。不过版本偏老,如果你需要新特性(比如向量扩展v1.0),建议用Bootlin或自己编译。
3.4 验证工具链安装
不管用哪种方式,装完后都要验证。我一般写一个最小的C程序来测试:
cat > hello.c << 'EOF'
#include <stdio.h>
int main() {
printf("Hello, RISC-V!\n");
return 0;
}
EOF
编译:
riscv64-unknown-elf-gcc -o hello hello.c
检查生成的文件:
file hello
输出应该类似:
hello: ELF 64-bit LSB executable, UCB RISC-V, version 1 (SYSV), statically linked, not stripped
看到UCB RISC-V字样,说明工具链工作正常。
一个小技巧:用riscv64-unknown-elf-readelf -h hello可以查看ELF头信息,确认机器类型是RISC-V,入口地址是否正确。
3.5 三种方式对比
| 方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 源码编译 | 完全可控,可定制优化选项 | 编译时间长,依赖多 | 深度定制、生产环境 |
| Bootlin | 版本新,支持多种C库 | 需要联网下载 | 快速原型、日常开发 |
| SiFive | 官方出品,稳定可靠 | 版本偏老 | 入门学习、兼容性要求高 |
3.6 知识体系结构图
下面这张图帮你理清工具链搭建的整体脉络:
3.7 避坑指南
最后,分享几个我实际踩过的坑:
- 路径不要有中文或空格:GCC编译脚本对路径很敏感,我曾经把工具链装到
/home/张三/tools,结果编译到一半报路径解析错误。 - 注意ABI匹配:编译QEMU固件时,如果工具链用
lp64d(双精度浮点),但QEMU模拟器只支持lp64(软浮点),链接时会报undefined reference to __adddf3。我遇到过两次,都是ABI没对齐。 - 多版本管理:如果你同时做裸机和Linux开发,建议用
update-alternatives或模块文件管理不同版本的工具链。我习惯在/opt下按riscv64-elf-2023.12、riscv64-linux-2023.12这样命名,切换时改PATH就行。
重要提醒:不要用系统自带的gcc来编译RISC-V程序!系统gcc生成的是x86指令,放到QEMU里跑会直接崩溃。交叉编译工具链的前缀riscv64-unknown-elf-就是用来区分这个的。
嗯,工具链搭好了,下一步就可以用它来编译固件,然后在QEMU上跑了。这部分内容,我们后面会详细展开。