openEuler RISC-V 进阶实操:交叉编译、RVV 向量验证与自编译内核的踩坑实录

上一篇介绍了怎么在 QEMU 里跑起 openEuler RISC-V 虚拟机,并在 VM 里原生编译了第一个 hello world。但真正开始做 RISC-V 软件移植后你会发现:在 QEMU 模拟的 VM 里编译大软件实在太慢了(慢 10~100 倍),而且 openEuler 25.03 的发行版内核默认不开放向量扩展,跑向量程序直接 SIGILL。这篇文章记录我从"交叉编译 → qemu-user 验证 → RVV 向量踩坑 → 自编译内核"的完整进阶过程,每一步都有实测命令和踩坑记录,希望能帮你少走弯路。


一、WSL 交叉编译:把"编译"从 VM 搬回宿主

1.1 安装交叉工具链

# WSL (Debian/Ubuntu) 一条命令装齐:编译器 + C++ + binutils + C 库 + 调试器
sudo apt install gcc-riscv64-linux-gnu g++-riscv64-linux-gnu \
  binutils-riscv64-linux-gnu libc6-dev-riscv64-cross gdb-multiarch

# 验证版本和 sysroot
riscv64-linux-gnu-gcc --version        # gcc (Ubuntu 14.x) 14.3.0
riscv64-linux-gnu-gcc -print-sysroot   # /usr/riscv64-linux-gnu

⚠️ 坑 1:GCC 15 的 libstdc++ 内嵌向量指令。我一开始图新装了 GCC 15 工具链,结果链接 C++ 程序时静态库里的 libstdc++ 编译进了向量指令,-march 参数根本覆盖不了——交叉编译出的程序在目标机上直接 SIGILL。结论:RISC-V 交叉编译老老实实用 GCC 14,这是当前最稳的版本。

顺带一提:这套工具链是"全家桶"结构——编译器(gcc)、汇编器(as)、链接器(ld)、C 库(glibc)、二进制工具(readelf/objdump/nm/strip)缺一不可,任何一个环节缺失交叉编译都跑不通。装完后可以用 riscv64-linux-gnu-gcc -v 查看配置参数和 sysroot 路径,用 -print-sysroot 直接输出交叉根目录,后续 qemu-user 的 -L 参数和 CMake 的 CMAKE_SYSROOT 都指向它。

1.2 第一个交叉编译:-march 与 -mabi 选型

# hello.c 与入门篇一样,但这次用交叉编译器,目标平台 riscv64
riscv64-linux-gnu-gcc -march=rv64gc -mabi=lp64d -O2 hello.c -o hello

# 验证架构
file hello
# 预期输出:ELF 64-bit LSB executable, UCB RISC-V, RVC, double-float ABI

-march 和 -mabi 是交叉编译最容易踩坑的一对参数,我的理解如下:

参数 含义 常见值
-march 目标 CPU 支持哪些指令扩展 rv64gc(保守通用)、rv64gc_zba_zbb_zbs(RVA22 级)、rv64gcv(带向量)
-mabi 调用约定(参数怎么传、浮点怎么处理) lp64d(64 位长整型+硬浮点,最常用)、lp64(无硬浮点)、ilp32d(32 位嵌入式)

选型铁律(血泪总结)

  1. -march 必须 ≤ 目标机真实 ISA(以目标机 /proc/cpuinfo 为准),写了目标没有的扩展 → 运行时报 Illegal instruction (core dumped)
  2. -mabi=lp64d 需要目标有 D 扩展(双精度浮点);
  3. 同一套 ABI 的二进制才能互相链接:-mabi=lp64d 编译的库只能被同样 ABI 的程序链接,报 relocation truncated 时先查 ABI 是否一致。

另外记住一个容易混淆的点:-march 决定"我能生成哪些指令",-mabi 决定"调用约定和二进制兼容性"。前者错了运行时崩,后者错了链接时就崩——两者作用阶段不同,排查方向也完全不同。我踩过最冤枉的一次,就是忘了目标机 ISA 里没有 zbb,编出来的程序在 VM 上一跑就 Illegal instruction,当时还以为是编译器坏了。

⚠️ 坑 2:GLIBCXX_3.4.32 not found。第一次交叉编译 C++ 程序,拷到 VM 里一跑就报这个错。原因:GCC 14 的 libstdc++ 需要 GLIBCXX_3.4.32,而 openEuler 25.03 只有 3.4.30。解法:链接时加 -static-libstdc++ -static-libgcc(只静态 C++ 运行库,动态 glibc),之后用 readelf -d 检查 NEEDED 只剩 libm/libc/ld-linux 就对了。体积信号:静态版约 8.0M,动态版约 6.1M。

⚠️ 坑 3:链接报 R_RISCV_CALL_PLT ... may bind externally。这是 PIE 下的重定位问题,加 -no-pie -Wl,-z,notext 即可解决。


二、qemu-user:在 x86 宿主上直接跑 riscv64 程序

交叉编译完,下一步是在真机/VM 上验证吗?不用——qemu-user 能在宿主机上直接跑 riscv64 用户态程序,完全不需要 VM,也不需要真实硬件。

2.1 qemu-user 与 qemu-system 的区别

  • qemu-system:全系统模拟,模拟整台机器(CPU + 内存 + 外设),可以装发行版、跑内核——入门篇用的就是它;
  • qemu-user:用户态模拟,只翻译执行单个 ELF 文件,不经过目标内核。速度快、启动秒级,适合跑单元测试和做正确性验证。
qemu-user 相当于:给宿主加了一层"指令翻译器",把 riscv64 的系统调用转发给宿主内核,
把 riscv64 的普通指令逐条翻译执行。所以程序眼里自己是 riscv64 环境,实际上跑在 x86 上。

这也是为什么 qemu-user 能绕过"内核不支持向量"的限制——它根本不加载目标内核,RVV 指令由 QEMU 翻译层直接模拟执行(下文第三节细讲)。对于只需要验证程序逻辑正确性的场景,qemu-user 比起一个完整 VM 轻量太多了。

# 安装
sudo apt install qemu-user

# 在 WSL 上直接跑刚交叉编译的 riscv64 程序
qemu-riscv64 -L /usr/riscv64-linux-gnu ./hello
# 预期输出:Hello, RISC-V!

⚠️ 坑 4:-L <sysroot> 不能省。不加 -L 直接跑会报:

qemu-riscv64: Could not open '/lib/ld-linux-riscv64-lp64d.so.1'

因为动态链接器 ld-linux-riscv64-lp64d.so.1 在交叉 sysroot 里(/usr/riscv64-linux-gnu/lib/),宿主机的 /lib 下根本没有。-L 就是告诉 qemu 去哪个根目录找动态链接器和库。

⚠️ 坑 5:别直接在宿主 shell 里 ./hello。riscv64 ELF 在 x86 宿主上跑会报 bash: ./hello: cannot execute binary file,这是预期行为,走 qemu-user 就对了。

2.2 通用验证三步走(强烈建议固化)

踩了几次"编译时没问题、上 VM 就崩"的坑后,我总结出任何交叉编译产物交付前的三步体检,缺一步都可能把有问题的二进制送到目标机上:

# ① readelf:链接体检 —— 该依赖谁、不该依赖谁
riscv64-linux-gnu-readelf -d app | grep NEEDED
# 预期:只剩 libm / libc / ld-linux;看到 libstdc++.so.6 就是漏了静态链接

# ② objdump:指令体检 —— 声称用了向量,真的编进去了吗
riscv64-linux-gnu-objdump -d app | grep -c vsetvli
# 预期:>0。为 0 说明自动向量化没生效或 -march 被覆盖

# ③ qemu-user:运行体检 —— 宿主机直接跑通(不经目标内核)
qemu-riscv64 -L /usr/riscv64-linux-gnu ./app && echo PASS

我把这三步固化成了 ~/.bashrc 里的一个函数 riscv64-check,之后每次交叉编译完一条命令完成体检。三步全绿才叫"可交付"


三、RVV 向量验证:从 SIGILL 到柳暗花明

3.1 写一个最小 RVV 向量程序

RVV(RISC-V 向量扩展)和 x86 SSE/AVX 最大的不同:硬件向量长度(VLEN)对编译器未知,软件必须写成"长度无关"的代码。先理解几个关键词:VLEN 是向量寄存器位宽(常见 128/256 位),ELEN 是单元素最大位宽(默认 64),SEW 是当前操作的元素位宽(e32=32 位元素),LMUL 是寄存器组倍数(m1/m2/m4/m8),而 vsetvli 这条指令负责在运行时设置 SEW/LMUL 并返回本次实际处理的元素数 vl。写向量代码不像写 SSE 那样"假设寄存器是 128 位"——你必须假设任何长度都可能,用运行时查询的结果来控制循环。先看最小示例:

// vec.c —— 查询向量寄存器长度(完整可编译)
#include <riscv_vector.h>
#include <stdio.h>

int main() {
    size_t vlen = __riscv_vsetvlmax_e8m8();   // e8m8 一次能装的最大字节数
    printf("VLEN bytes: %zu\n", vlen);
    return 0;
}

编译(注意 -march 要带 v):

riscv64-linux-gnu-gcc -march=rv64gcv -mabi=lp64d -O2 vec.c -o vec
riscv64-linux-gnu-objdump -d vec | grep -c vsetvli   # >0 说明真的编进了向量指令

3.2 坑 6:VM 里跑向量程序直接 SIGILL(最大拦路虎)

我在 QEMU 启动参数里特意加了 -cpu rv64,v=true,vlen=256,进 VM 后 grep v /proc/cpuinfo 也确实能看到 v 扩展——但一跑向量程序,直接崩溃

Illegal instruction (core dumped)   # 退出码 132 = SIGILL

排查过程:

# ① 确认 CPU 模拟出 v 扩展(确实有)
grep -o '^isa[^,]*' /proc/cpuinfo        # rv64imafdc_zba_zbb_zbs_v ...

# ② 查内核是否允许用户态使用向量 —— 问题在这!
zcat /proc/config.gz | grep RISCV_ISA_V  # CONFIG_RISCV_ISA_V is not set

根因:QEMU 模拟出 v 扩展 ≠ 内核允许用户态使用它。openEuler 25.03 发行版内核显式关闭了 CONFIG_RISCV_ISA_V(尽管上游 Linux 6.6 默认是 y)。内核不开这项 = 不保存/恢复向量寄存器上下文,用户态任何 v 指令直接非法。换什么 -cpu 都没用,因为问题出在内核,不是 QEMU。

顺带说一句:Debian/Ubuntu 的 riscv64 内核默认开 RVV,同样的二进制在两边行为不同——这种"发行版级差异"正是验证工作的价值点,也是我给社区提交验证报告时发现的。

3.3 坑 7:qemu-user 的 -cpu rv64,v=true 不生效

想用 qemu-user 绕开内核限制,结果发现 -cpu rv64,v=trueuser 模式下根本不解析出 v 扩展(该 CPU 名解析不带 v)。正确姿势是用默认 CPU 或 -cpu max

# ✅ 正确:qemu-user 不经目标内核,RVV 由 QEMU 翻译层执行
qemu-riscv64 -L /usr/riscv64-linux-gnu -cpu max ./vec
# 预期输出:VLEN bytes: 32   (256 位向量 = 32 字节)

# 调向量长度:默认 128 位 → 指定 256 位
qemu-riscv64 -L /usr/riscv64-linux-gnu -cpu max,vlen=256 ./vec

这就是绕过内核 RVV 限制的官方推荐路线qemu-riscv64 -L <sysroot> <binary> 直接在 x86 宿主机翻译执行 riscv64 指令,根本不经过目标内核,RVV 由 QEMU 翻译层执行。我实测用它跑通了 RVV 向量加法(bad=0 PASS)、甚至完整的 MLAS RVV SGEMM kernel 11/11 单元测试(vlen=128 和 vlen=256 都过,maxdiff ≤ 0.000156)。结论:发行版缺 RVV 支持时,正确性验证完全不需要 VM/真机。

3.4 坑 8:RVV 循环必须用分段循环(stripmining)

写向量代码时我还踩了一个隐蔽坑:循环如果不按 vlenmax 分段、而是假设一次装完所有元素,超过 vlenmax 时数值会静默错乱(qemu-user 下实测,不报错只给错答案)。RVV 的标准写法是分段循环:

size_t vlmax = __riscv_vsetvlmax_e32m1();   // 一次最多处理几个 f32
for (size_t vl; n > 0; n -= vl, a += vl, b += vl, c += vl) {
    vl = __riscv_vsetvl_e32m1(n);           // 本次处理 min(n, vlenmax) 个
    vfloat32m1_t va = __riscv_vle32_v1(a, vl);
    vfloat32m1_t vb = __riscv_vle32_v1(b, vl);
    vfloat32m1_t vc = __riscv_vfadd_vv_f32m1(va, vb, vl);
    __riscv_vse32_v1(c, vc, vl);
}

每个循环迭代用 vsetvl 重新截断本次要处理的元素数,这是 RVV 与固定宽度 SIMD 最本质的思维差异。如果总元素数超过 vlenmax 而你只循环了一次,QEMU 不会报错——它只是默默地把超出部分留在寄存器里没写回内存,你拿到的是错乱的结果。这种"静默出错"比崩溃更可怕,排查起来极费时间。


四、自编译内核:从根上解决向量支持

qemu-user 能验证正确性,但真要在 openEuler VM 里跑向量程序,还得自编一个开了 CONFIG_RISCV_ISA_V 的内核。这也是最完整的进阶闭环。

4.1 获取 OLK 源码

OLK = Open Euler Kernel,openEuler 官方内核仓,全架构同一仓库多分支:

# 浅克隆 OLK-6.6(约 200+MB,--depth 1 省流量)
git clone --depth 1 -b OLK-6.6 https://gitee.com/openeuler/kernel.git ~/riscv/oe/olk-6.6

# 确认版本(Makefile 前 5 行)
head -5 ~/riscv/oe/olk-6.6/Makefile
# VERSION = 6 / PATCHLEVEL = 6 / SUBLEVEL = 0 / EXTRAVERSION = -72.6.0.56.oe2503

4.2 config 配置:三条路 + 一个大坑

export ARCH=riscv
export CROSS_COMPILE=riscv64-linux-gnu-
cd ~/riscv/oe/olk-6.6

# 路线 A(推荐):用 VM 现役内核的 config,保证与官方行为一致
# VM 里执行:zcat /proc/config.gz > /tmp/oe.config
# 然后 scp 回 WSL
cp ~/riscv/oe/current.config build/.config

# 路线 B:源码自带 defconfig(实测 7396 行)
make O=build openeuler_defconfig

⚠️ 坑 9:defconfig ≠ 发行版实际 config。这是我最惊讶的发现:OLK 主仓的 openeuler_defconfig 实测 CONFIG_RISCV_ISA_V=y(RVA23 方向已默认开),但 25.03 发行版内核 config 显式关了它——官方发布内核的 config 在 src-openeuler/kernel 的 spec 里,跟源码树 defconfig 不一致。要复刻官方行为就用路线 A(现役 config),要新能力就用 defconfig 再微调。

4.3 显式开启向量支持

# 在现役 config 基础上开向量 + IKCONFIG(/proc/config.gz 依赖它)
./scripts/config --file build/.config -e RISCV_ISA_V -e IKCONFIG
make O=build olddefconfig

# 关键:验证开关真的生效(别信 menuconfig 显示)
grep RISCV_ISA_V build/.config    # 必须看到 CONFIG_RISCV_ISA_V=y

4.4 交叉编译内核

make O=build -j6 Image dtbs modules
# 产物:
#   build/arch/riscv/boot/Image   —— 内核镜像(QEMU -kernel 用它)
#   vmlinux                       —— ELF(调试用)
#   build/.config                 —— 最终生效 config

file build/arch/riscv/boot/Image  # ELF 64-bit LSB executable, UCB RISC-V

⚠️ 坑 10:WSL 内存雪崩。第一次我用 -j$(nproc)(8 核)并行编译 + 4G VM,直接把 WSL 的 32GB 内存打满,WSL 服务整个挂死(0x8007274c),只能 wsl --shutdown 重启。铁律:限 -j6,这是本项目反复验证的教训。

⚠️ 坑 11:报 certs/openeuler-cert.pem 不存在。openEuler 内核 spec 会带签名证书路径,源码树里没有。解法:make menuconfig 里清空 Cryptographic API > Certificates for signature checking > Additional X.509 keys,或 config 关 CONFIG_MODULE_SIG

4.5 替换 VM 内核(推荐先直启验证)

替换内核有两条路线,先直启、再装盘是最稳的顺序:

路线 1:QEMU -kernel 直启新内核(推荐做实验,不动 VM 磁盘)

qemu-system-riscv64 -machine virt -smp 8 -m 4G \
  -kernel ~/riscv/oe/olk-6.6/build/arch/riscv/boot/Image \
  -append 'root=/dev/vda1 rw console=ttyS0 swiotlb=1 selinux=0 highres=off earlycon' \
  -drive file=<你的qcow2>,format=qcow2,id=hd0 \
  -device virtio-blk-device,drive=hd0 \
  -netdev user,id=n1,hostfwd=tcp::12056-:22 \
  -device virtio-net-device,netdev=n1 -nographic

⚠️ 坑 12:-kernel 只认 Image,不认 vmlinuz。磁盘里 /boot/vmlinuz-*压缩过的内核(uImage 格式,带自解压头),QEMU -kernel 期望的是未压缩的 raw 内核 Imagearch/riscv/boot/Image)。格式不对,启动直接崩或没反应。

路线 2:完整安装进 VM(装盘,可重启生效)

# 宿主:打包模块
make INSTALL_MOD_PATH=./modroot modules_install
tar czf ~/riscv/oe/kmods.tgz -C ./modroot lib
scp -P 12055 build/arch/riscv/boot/Image ~/riscv/oe/kmods.tgz linght153@127.0.0.1:~/

# VM 内:
sudo cp ~/Image /boot/vmlinuz-6.6.0-custom
sudo tar xzf ~/kmods.tgz -C /
sudo dracut --kver 6.6.0-custom -f /boot/initramfs-6.6.0-custom.img
sudo grub2-mkconfig -o /boot/grub2/grub.cfg
sudo grubby --set-default /boot/vmlinuz-6.6.0-custom
# 重启前先 grubby --info=ALL 确认新条目存在(VM 重启由你自己手动执行)

4.6 验证闭环:向量真的可用了

uname -r                                   # 你的自定义版本号
zcat /proc/config.gz | grep RISCV_ISA_V    # CONFIG_RISCV_ISA_V=y

# 编一个向量程序跑——之前必然 SIGILL,现在正常输出
gcc -march=rv64gcv -O2 -o vec vec.c && ./vec
# 输出:VLEN bytes: 32(QEMU virt 默认 VLEN 256)

那一刻看到向量程序第一次在 openEuler VM 里正常输出,之前所有的坑都值了。


结尾总结

环节 核心结论 关键坑
交叉编译 -march ≤ 目标 ISA,-mabi=lp64d GCC 15 别用;GLIBCXX 3.4.32 加静态链接
qemu-user -L <sysroot> 不能省,秒级验证 动态链接器找不到;./hello 二进制格式错
验证三步走 readelf 查链接、objdump 查指令、qemu-user 查运行 三步全绿才可交付
RVV 验证 发行版内核可能不开 ISA_V → qemu-user -cpu max 绕过 VM 里 SIGILL 是内核问题不是 QEMU;循环必须分段
自编译内核 现役 config 小改 + scripts/config -e 开开关 defconfig≠发行版 config;-j6-kernel 只认 Image

最大的认知升级:QEMU 模拟出扩展 ≠ 内核允许用户态使用。遇到 SIGILL 先查目标内核的 config,而不是怀疑编译器或 QEMU;而 qemu-user 这把"绕过内核"的钥匙,让 RISC-V 软件的正确性验证可以完全脱离真机和 VM 进行。这套"交叉编译 + qemu-user 验证 + VM 只跑产物"的工作流,现在已经成为我日常做 RISC-V 软件移植的标准姿势。

给后来者的建议:按"先交叉编译 → 再 qemu-user 跑通 → 需要真机性能/内核特性时再上 VM 或真机"的顺序推进,每一步的验证手段都简单直接;遇到 Illegal instruction 先查 /proc/cpuinfo 和内核 config,别一头扎进编译参数里。RISC-V 生态还年轻,坑多但成长快,把这些实测记录沉淀下来,就是给社区最好的贡献。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐