24、RISC-V交叉编译实战:以RISC-V 64位为例,编写工具链文件并编译

好,咱们今天来点真格的。前面聊了那么多工具链文件的原理和语法,说白了都是为了这一刻——动手写一个能用的工具链文件,然后真正编译出RISC-V 64位的程序。

我个人习惯是,学任何交叉编译,先别急着看文档,先让一个Hello World跑起来。跑通了,信心就有了,后面再深入细节也不迟。

24.1 准备工作:你需要什么?

在动手之前,先把家伙事儿备齐。我当年第一次搞RISC-V交叉编译时,光装工具链就折腾了大半天,后来发现是版本没选对。嗯,这里要注意。

你需要准备以下东西:

  • RISC-V 64位交叉编译器:推荐使用官方的GCC工具链,或者SiFive提供的预编译包。我一般用riscv64-unknown-linux-gnu-gcc这个前缀。
  • CMake 3.10+:版本别太老,否则有些变量不支持。
  • 一个干净的测试目录:别在系统目录里乱搞,咱们新建一个文件夹。
小提示:如果你不想自己编译GCC工具链,可以直接从SiFive官网下载预编译的二进制包。解压后把bin目录加到PATH里就行。省时省力。

24.2 编写工具链文件:riscv64-toolchain.cmake

好,重头戏来了。工具链文件其实就是告诉CMake三件事:用哪个编译器、目标平台是什么、系统环境怎么配置

下面是我在实际项目中用过的模板,我稍微简化了一下,方便你理解核心逻辑:

# riscv64-toolchain.cmake
# 用于RISC-V 64位 Linux系统的交叉编译

# 目标系统名称
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR riscv64)

# 指定交叉编译器
set(CMAKE_C_COMPILER riscv64-unknown-linux-gnu-gcc)
set(CMAKE_CXX_COMPILER riscv64-unknown-linux-gnu-g++)

# 指定编译器前缀(方便查找工具链中的其他工具)
set(CMAKE_FIND_ROOT_PATH /usr/riscv64-linux-gnu)

# 搜索策略:只在目标系统路径中查找库和头文件
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)

# 设置编译器标志(可选,但建议加上)
set(CMAKE_C_FLAGS "-march=rv64gc -mabi=lp64d")
set(CMAKE_CXX_FLAGS "-march=rv64gc -mabi=lp64d")

这段代码不长,但每行都有讲究。我解释一下:

  • CMAKE_SYSTEM_NAME设为Linux,告诉CMake目标系统是Linux。如果是裸机(bare-metal),就设成Generic。
  • CMAKE_C_COMPILERCMAKE_CXX_COMPILER直接指定了编译器路径。你也可以用绝对路径,但我习惯用前缀名,前提是PATH里能找到。
  • CMAKE_FIND_ROOT_PATH指向目标系统的根目录。这样CMake在找头文件和库时,会优先去这个目录下找,而不是去宿主机的/usr/include
  • 后面四个MODE变量,说白了就是控制搜索范围。我建议把PROGRAM设为NEVER,因为程序(比如编译器本身)应该在宿主机上找;而库和头文件只在目标系统路径里找,避免污染。
注意:如果你用的是不同的工具链前缀(比如riscv64-linux-gnu-),记得把上面的名字改掉。我曾经因为前缀写错,编译出来的程序在目标板上跑不起来,查了半天才发现是ABI不匹配。

24.3 编写测试程序:一个简单的Hello World

工具链文件写好了,咱们写个简单的C程序来验证一下。别整太复杂,能打印一行字就行。

// hello_riscv.c
#include <stdio.h>

int main() {
    printf("Hello from RISC-V 64-bit!\n");
    printf("Architecture: %s\n", 
        #ifdef __riscv
            "RISC-V"
        #else
            "Unknown"
        #endif
    );
    return 0;
}

这里我加了个小技巧:用预定义宏__riscv来判断当前编译目标是不是RISC-V。如果编译成功,程序会打印出"RISC-V"字样。这招我在调试时经常用,能快速确认编译器是否工作正常。

24.4 编写CMakeLists.txt并编译

现在写CMakeLists.txt。这个文件很简单,就是调用工具链文件,然后添加一个可执行目标。

cmake_minimum_required(VERSION 3.10)
project(RISCV_Hello C)

add_executable(hello_riscv hello_riscv.c)

然后执行编译命令。我个人习惯在项目根目录下建一个build文件夹,把所有编译产物都放进去,保持源码目录干净。

mkdir build
cd build
cmake .. -DCMAKE_TOOLCHAIN_FILE=../riscv64-toolchain.cmake
make

如果一切顺利,你会看到hello_riscv这个可执行文件生成。用file命令检查一下:

$ file hello_riscv
hello_riscv: ELF 64-bit LSB executable, UCB RISC-V, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-riscv64-lp64d.so.1, for GNU/Linux 4.15.0, not stripped

看到"UCB RISC-V"这几个字了吗?说明编译成功了。这一刻,你手里的程序已经可以在RISC-V 64位的Linux系统上运行了。

24.5 核心逻辑流程图

为了让你更直观地理解整个流程,我画了一张图。从源码到目标可执行文件,中间经历了哪些步骤,一目了然。

RISC-V 64位交叉编译核心流程 C/C++ 源码 hello_riscv.c 工具链文件 riscv64-toolchain.cmake CMake 配置阶段 cmake -DCMAKE_TOOLCHAIN_FILE=... 编译阶段 make / ninja RISC-V 64位可执行文件 hello_riscv (ELF 64-bit RISC-V) 关键检查点 1. 编译器前缀是否正确 2. ABI/march是否匹配 3. 搜索路径是否隔离

24.6 常见问题与避坑指南

编译过程中难免会遇到问题。我把自己踩过的坑整理了一下,你遇到了可以直接对照着查。

问题现象 可能原因 解决方法
riscv64-unknown-linux-gnu-gcc: command not found 编译器未安装或未加入PATH 检查工具链路径,或者用绝对路径指定编译器
fatal error: stdio.h: No such file or directory 找不到目标系统的头文件 检查CMAKE_FIND_ROOT_PATH是否指向正确的sysroot
relocation R_RISCV_HI20 ... 链接错误 ABI或march不匹配 确保-march-mabi与目标平台一致
编译成功但目标板运行报错 动态链接库版本不匹配 尝试静态编译(加-static)或检查目标板上的库版本

核心要点总结:

  • 工具链文件的核心就是告诉CMake三件事:目标系统、编译器路径、搜索策略。
  • CMAKE_FIND_ROOT_PATH_MODE_*这几个变量一定要设置对,否则容易把宿主机的库混进来。
  • 编译完成后,用file命令检查可执行文件,确认是RISC-V架构。

好了,这一章的内容就到这里。你跟着走一遍,应该能顺利编译出RISC-V 64位的程序。如果遇到问题,别急,对照上面的表格排查一下,多半能解决。

公众号:蓝海资料掘金营,微信 deep3321