openEuler RISC-V 进阶实操:交叉编译、RVV 向量验证与自编译内核的踩坑实录
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 位嵌入式) |
选型铁律(血泪总结):
-march必须 ≤ 目标机真实 ISA(以目标机/proc/cpuinfo为准),写了目标没有的扩展 → 运行时报Illegal instruction (core dumped);-mabi=lp64d需要目标有 D 扩展(双精度浮点);- 同一套 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=true 在 user 模式下根本不解析出 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 内核 Image(arch/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 生态还年轻,坑多但成长快,把这些实测记录沉淀下来,就是给社区最好的贡献。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)