☰
RK3588交叉编译Hello World:从x86到ARM64的部署基石
2026/10/5 9:08:05 网站建设 项目流程

按理说,第06篇该进入yolov5s模型转换或者NPU推理的正题了,但我把节奏停一下,先带大家把交叉编译这件事彻底打通。原因是我在这条路上见过太多人,不是死在模型转RKNN,而是死在最简单的hello程序上。接下来我们要做的事——在x86的PC上编译,在香橙派RK3588的ARM64系统上运行——本质上就是从这一个hello开始的。这期不搞虚的,直接把这套工具链环境、编译命令、运行验证和踩坑排查全部走一遍,保证你看完能自己动手跑通。

1. 为什么第06篇要先写一个Hello World

1.1 交叉编译在RK3588部署yolov5s里的真实位置

很多朋友拿到香橙派RK3588之后,第一反应是"板子能跑系统了,我直接在板子上写代码不就行了"。确实,板端是完整的Ubuntu,装个gcc然后本地编译hello,三分钟就能跑起来。但你们有没有想过,为什么RK官方提供的NPU推理demo、rknn-toolkit2配套的C接口工程,几乎全是让你在PC上先交叉编译、再把产物丢到板子上?因为后面的部署链路不是"在板子上写一个独立小程序",而是"PC负责模型转换和业务逻辑开发,板子只负责运行推理与IO"。

拿yolov5s举例。你手上拿到的是yolov5s.pt,第一步是在PC上用rknn-toolkit2把它转成RK3588的NPU能识别的.rknn格式;第二步是写C/C++推理程序,调用NPU运行时库librknnmrt.so;第三步,这个程序是要编译成ARM64架构的二进制,放到板子上执行的。第三步就离不开交叉编译。

如果你直接ssh到板子上用apt装gcc再编译,不是不行。但我实测下来有两个问题:一是香橙派5系这类板子的A55小核在执行编译任务时确实比PC慢一个量级,尤其是工程一旦带上OpenCV、多源文件编译,等待时间非常煎熬;二是板端的开发环境装了各种包以后会变得很"脏",有些编译依赖和运行依赖混在一起,后期排查问题会让你怀疑人生。交叉编译的思路,说白了就是在性能强、环境干净的PC上把ARM64的二进制"造"出来,板子只负责运行。

1.2 一个hello替你验证的底层三件事

交叉编译hello不是一个形式主义的仪式,它真正验证的是接下来所有C/C++工程共用的底层环境。我总结为三件事:

  1. 目标架构是否对:编译产物是不是ARM aarch64格式。如果这一步错了,后面编出来的yolov5s后处理程序丢到板子上,会直接报Exec format error。
  2. 动态链接器是否存在:ARM64 Linux程序需要/lib/ld-linux-aarch64.so.1来加载,如果工具链指定的链接器路径和板端系统不一致,就会出现"明明文件存在却说No such file or directory"的经典灵异现象。
  3. glibc版本是否兼容:工具链编译时依赖的glibc版本不能高于板端系统自带的glibc版本。这是交叉编译最常见的隐性炸弹,后面我会专门讲排查方法。

这三件事现在用一个hello验证清楚,比后续在几百行的推理代码里排查要轻松得多。这也是为什么我坚持"从头到脚"这个系列在进入yolov5s部署正题之前,先插一期hello——它就是一块试金石。

2. 装好交叉编译工具链:这一步很多人其实是错的

2.1 系统自带工具链和厂家SDK工具链怎么选

写这段时真想吐槽一句:网上很多教程一上来就让你去下载某个厂家"交叉编译工具链",下载回来是个.tar.gz,解压后还要手动配PATH、配CROSS_COMPILE变量,一套操作猛如虎,结果编出来的hello跑到板子上直接报glibc版本不够。问题根源不是操作不对,而是这个工具链往往是针对某个特定系统版本(比如老版本Ubuntu、某个Buildroot根文件系统)编译的,glibc版本和你的香橙派当前系统可能差了十万八千里。

我的建议很简单:除非你确定板端用的是厂家配套的根文件系统(比如某些AIO镜像),否则优先用Linux发行版自带的交叉编译工具链。比如PC端是Ubuntu 22.04,直接:

sudo apt update sudo apt install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu

一个关键点:如果PC是Ubuntu 22.04,apt装到的交叉工具链自带的是glibc 2.35;香橙派官方Ubuntu系统(22.04版本)板端glibc也是2.35,这就能保证版本兼容。而某些老教程提供的2018、2019年的工具链,glibc还停在2.28左右,编译出来的程序虽然也能跑,但一旦用了新内核特性或新库函数,链接阶段就会出现找不到符号的问题。

gcc-aarch64-linux-gnu能做的事情其实不少:

  • 编译纯C/C++的普通程序、静态库、动态库;
  • 配合g++交叉编译器编译C++工程(后面yolov5s的C++推理会用到);
  • 加上--sysroot参数后,还可以指定到某个目标根文件系统的头文件和库进行编译,这在实际产品开发中再常用不过。

作为一个对比,我列一下几种工具链方案的适用场景:

方案优点缺点适用场景
apt安装的aarch64-linux-gnu安装简单、更新跟随系统、版本匹配容易只能针对通用glibc,无法定制普通Ubuntu板端系统部署
厂家SDK/toolchain包可针对特定根文件系统裁剪,库版本统一需要手动配置、版本通常较旧使用厂家自带rootfs或量产固件
Buildroot自编译工具链完全可控,sdk内部所有库版本一致编译要花几十分钟甚至更久做完整系统镜像和量产开发

对咱们这个系列来说,香橙派跑的是官方Ubuntu镜像,所以apt方案是第一选择,省事且风险最低。

2.2 装好之后先用三个命令确认工具链可用

安装完成后别急着写hello,先确认三件事。

先看版本号:

aarch64-linux-gnu-gcc --version aarch64-linux-gnu-g++ --version

再做一个极简的编译测试。这里不需要写任何源文件,直接利用gcc从标准输入读代码:

echo 'int main(){return 0;}' | aarch64-linux-gnu-gcc -x c - -o /tmp/test_arm64 file /tmp/test_arm64

如果输出里有ARM aarch64字样,就说明工具链本身是可用的。这一步把"工具链坏了"和"你的代码有问题"这两个变量分开,后面排查起来会省很多时间。

提示:Windows用户建议用WSL2或者虚拟机装Ubuntu来做交叉编译。后面rknn-toolkit2的模型转换工具、各种构建脚本基本都是以bash环境为基准,直接在Windows上操作会白踩一堆坑。

我见过有人在Windows下折腾了一整天,最后发现是路径反斜杠和bash脚本不兼容。这个系列所有编译和部署操作,我都默认在Linux环境里完成,你也趁早统一环境。

2.3 交叉编译器输出的文件到底长什么样

掌握一个习惯:每次交叉编译完,先对产物执行file命令。这比看任何肉眼判断都靠谱。

在PC上跑:

file hello

你会看到类似这样的输出:

hello: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, BuildID[sha1]=c10e..., for GNU/Linux 3.7.0, not stripped

注意关键信息:

  • ARM aarch64:这是给ARM64平台用的
  • interpreter /lib/ld-linux-aarch64.so.1:这是动态链接器路径,后面排查"找不到文件"问题要用
  • dynamically linked:动态链接,依赖外部.so文件

如果你在板子上用本机gcc编译同样一份代码,file结果基本长一样。区别只在于编译工具链所处架构不同。交叉编译之所以叫"交叉",就是因为在x86机器的编译环境里,产出了一份arm64架构的二进制。

3. 先解决PC和板子路径不一致这个隐形炸弹

3.1 为什么我坚持两台设备用相同目录工作

说到交叉编译的坑,"路径不一致"是我遇到过的第一大坑,比工具链版本问题还阴。

动态链接程序里,如果编译时用了绝对路径的RPATH/RUNPATH,那么程序在板子上运行时,会先去那个绝对路径找.so库文件。举个我当年真实踩过的例子:我在PC编译一个用了自定义.so的程序,编译时链接器的-L参数指向/home/me/project/lib,编译链接器把这个路径写进了二进制的RUNPATH(如果你没有显式用-rpath,某些构建系统也会默默记录它),拷到板子上以后,板子这个路径根本不存在,运行时直接抛error while loading shared libraries。

解决办法不是什么高深技巧:就是在PC和板子上使用同一个目录路径。我这个系列的工作目录统一为/home/orangepi/rk3588_ws,PC上建一份,板子上也建一份,存放源码、编译产物、模型文件和运行库的位置保持对称。这样即使二进制里残留了编译机的绝对路径,在板子上也能找到同样的位置。

3.2 两个传输方案:scp简单直接,NFS一劳永逸

把文件从PC弄到板子,最直接的是scp。假设板子的IP是192.168.1.100、用户名orangepi:

scp hello orangepi@192.168.1.100:/home/orangepi/rk3588_ws/

需要输入密码,完事儿。适用在前期验证阶段,文件不多、改动不频繁。

但后面你会频繁修改源码、反复编译、把新的二进制丢到板子上测试,再用scp就有点烦躁了。我强烈建议这时候配一个NFS共享目录,把PC上的/home/orangepi/rk3588_ws直接挂载到板子的同一路径下。

PC端开启NFS服务(假设PC IP为192.168.1.10):

sudo apt install nfs-kernel-server echo '/home/orangepi/rk3588_ws 192.168.1.100(rw,sync,no_subtree_check,no_root_squash)' | sudo tee -a /etc/exports sudo exportfs -r

板子端挂载:

sudo mount -t nfs 192.168.1.10:/home/orangepi/rk3588_ws /home/orangepi/rk3588_ws -o nolock

挂载之后,PC上编译的产物在板子上直接就是同一个文件,连scp都不用做。更关键的是,后面如果要在板子上调试运行时的.so加载问题,NFS目录下调试起来非常舒服,改一版跑一下,立刻看到结果。

注意:NFS的no_root_squash加上后,要留意板端和PC端的用户ID。如果板端用户ID是1000、PC端用户ID也是1000,权限基本不用操心;如果对不上,编译产物往往是别人的文件,板子上执行会报Permission denied。最简单的办法是把两边的普通用户都设成相同uid,这在开发阶段能省掉很多权限烂事儿。

4. Hello World实战:从写代码到板子打印出来

4.1 写出一个"带体检功能"的hello.c

普通教程会给你这么一份hello.c:

#include <stdio.h> int main(void) { printf("Hello World!\n"); return 0; }

这当然能跑,但我建议你多写两行,让它顺带打印出当前系统的glibc版本和架构信息,方便后面排查问题:

#include <stdio.h> #include <gnu/libc-version.h> int main(void) { printf("Hello from Orange Pi RK3588!\n"); printf("Calling arch: aarch64\n"); printf("Runtime glibc: %s\n", gnu_get_libc_version()); return 0; }

gnu_get_libc_version()这个函数需要在包含gnu/libc-version.h后才能调用,它返回的是板端运行时libc的版本号。不要小看这行输出,后面排查glibc兼容问题的时候,运行一次这个程序就能立刻确认板端libc版本,不用再去翻strings /lib/aarch64-linux-gnu/libc.so.6。

4.2 交叉编译:动态链接和静态链接怎么选

把hello.c放到PC端的/home/orangepi/rk3588_ws/src目录下,然后执行交叉编译:

cd /home/orangepi/rk3588_ws/src aarch64-linux-gnu-gcc hello.c -o ../hello

这里我故意用了相对路径,把二进制放到和板子一致的工作目录下。

编译出来以后,先看产物大小:

ls -lh ../hello

动态链接的hello一般只有十几KB,因为printf、gnu_get_libc_version这些函数都来自系统动态库。如果你用静态链接:

aarch64-linux-gnu-gcc hello.c -o ../hello_static -static

产物会一下子变成700KB以上,因为整个C运行库都被塞进了可执行文件里。

动态和静态怎么选?我的原则很明确:

编译方式产物大小运行时依赖适用场景
动态链接极小依赖板端系统.so库后续所有正常开发场景
静态链接大几乎无外部依赖临时应急、极简环境调试

看起来静态链接好像很省心,但我必须提醒你:后面的RKNPU推理程序一定会依赖librknnmrt.so这样的动态库,你没法把它静态塞进去。所以从一开始就要习惯动态链接的方式,尽早把板端glibc和工具链的兼容性问题暴露出来。这个hello项目,本质就是把动态链接环境先跑通。

4.3 上传、赋权、运行,三步走

在PC上执行:

scp ../hello orangepi@192.168.1.100:/home/orangepi/rk3588_ws/

再ssh进板子:

ssh orangepi@192.168.1.100 cd /home/orangepi/rk3588_ws ./hello

如果你直接执行报Permission denied,看一下权限:

ls -l hello

交叉编译出来的文件默认带rwxr-xr-x权限,但scp不会保留可执行位,所以经常需要补一刀:

chmod +x hello

运行成功后,你会看到类似这样的输出:

Hello from Orange Pi RK3588! Calling arch: aarch64 Runtime glibc: 2.35

看到这行输出,恭喜,你的交叉编译工具链和板端环境的底层兼容性已经完全打通了。接下来编译任何C/C++工程,底层环境都不用再怀疑。

5. 如果你在现场碰了这些钉子:运行失败排查手册

5.1 "No such file or directory"其实不是文件不存在

这是我在群里回答得最多的一个问题。用户在PC上交叉编译完hello,scp到板子,执行./hello,结果报:

./hello: No such file or directory

他反复确认ls -l hello,文件明明在。为什么会报不存在?

原因大概率是:当前被加载的动态链接器路径在板子上不存在,或者格式无法识别。之前file输出里的interpreter /lib/ld-linux-aarch64.so.1就是线索。如果你的板端系统使用某种简化rootfs,没有这个文件,系统内核在启动这个二进制时就会对外表现为"找不到文件"。

先用readelf看一下二进制请求的解释器路径:

readelf -l hello | grep interpreter

输出类似于:

[Requesting program interpreter: /lib/ld-linux-aarch64.so.1]

再到板子上检查这个文件是否存在:

ls -l /lib/ld-linux-aarch64.so.1

如果缺乏的是动态链接器,有两种做法:一个是从板端系统的软件源补装libc6-arm64-cross或对应基础包;另一个更粗暴也最常用的办法是换用和板端系统匹配的工具链重新编译。老工具链指定老路径的情况虽然少见,但一旦遇上,这个命令能帮你快速锁定。

5.2 glibc版本不一致:工具链与板端系统的老新问题

hello能在PC上正常编译,不代表板子一定能运行。如果工具链自带的glibc高于板端系统的glibc,程序会在加载阶段报:

./hello: /lib/aarch64-linux-gnu/libc.so.6: version 'GLIBC_2.36' not found

这种现象在"PC apt工具链很新 + 板端系统偏旧"或者"老工具链 + 新板系统"的时候都容易出现。

排查套路分两步。先看二进制需要的最低glibc版本:

readelf --version-info hello

关注GLIBC_2.xx字段。然后在板子上看实际libc提供的版本:

strings /lib/aarch64-linux-gnu/libc.so.6 | grep GLIBC_ | tail

如果板端的最高版本小于程序要求的最低版本,立刻就能确诊。

解决的办法按优先级:

  1. 把工具链换成和板端系统相同或更旧版本。最简单的就是检查PC apt源里的编译器版本,以及板端系统版本是否一致,比如都是Ubuntu 22.04基本不会踩这个雷;
  2. 临时用-static静态编译绕过运行库版本问题,但这只是应急方案;
  3. 如果板子非要跑新版库,那就得重新做一个系统镜像或者手动升级板端基础库,风险较大,开发阶段不建议。

见过太多人一上来就用某个老教程的2019年工具链编yolov5s的C++部署代码,然后被一堆GLIBC版本符号折磨。我的建议是:先把版本匹配这关过了,再谈模型优化。

5.3 在板子上用这些命令验证运行环境

最后补充几个板端常用的验证命令。注意一个很容易搞混的点:ldd是一个shell脚本,它的原理是执行目标平台的动态链接器去解析依赖。如果你在PC上对一个ARM64的hello执行ldd hello,大概率会得到not a dynamic executable或者直接报错,因为PC上的动态链接器是x86的,没法正确加载ARM64的二进制。正确的做法是ssh到板子上,再对hello执行ldd:

ldd hello

输出里会列出hello依赖的.so文件路径。比如:

linux-vdso.so.1 (0x0000xxxx) libc.so.6 => /lib/aarch64-linux-gnu/libc.so.6 (0x0000xxxx) /lib/ld-linux-aarch64.so.1 (0x0000xxxx)

如果输出里出现not found,那就是某个依赖库在板端缺失,顺着路径去找就行了。

此外,我强烈建议在板子上装一个strace:

sudo apt install strace strace ./hello

当程序运行失败时,strace会把加载过程的每个系统调用列出来,你会看到它卡在哪个openat、哪个execve上,比瞎猜高效得多。

6. 交叉编译与下一步YOLOv5s部署的衔接

6.1 从hello到librknnmrt.so,链路怎么走

一个hello打通了交叉编译工具链,但实际上真正的挑战还在后面。接下来在RK3588上部署yolov5s,大致链路是这样的:

  1. 在PC上把yolov5s.pt用rknn-toolkit2转换成.rknn格式模型;
  2. 在PC上用C/C++写推理主程序,调用NPU runtime库librknnmrt.so;
  3. 头文件包含rknn_api.h,链接时加上-lrknnmrt和库路径;
  4. 交叉编译生成ARM64的推理程序,连同.rknn模型一起传到板子;
  5. 板端执行程序,NPU加载模型完成推理。

在这个链路里,hello里做过的每一步都会复用:交叉编译、动态链接、路径统一、版本排查、ldd/strace验证。尤其librknnmrt.so本身就是ARM64的.so,它在板端被加载时的路径、依赖关系、glibc版本要求,排查思路和hello一模一样。这也是为什么我一直劝大家别跳过hello直接上模型。

6.2 为什么我不建议直接在板子上本地编译推理程序

有些朋友可能觉得:反正绕来绕去,不如直接在板子上g++一把梭。我在香橙派上实测过,本地编译一个小工程确实可以,但代价不小。

一来,香橙派5系的小核(A55)在编译时比较慢。你写个hello感觉不出来,但工程一变大、模板展开一多,CPU占用瞬间拉满,风扇呼呼转,编译时间成倍增长。二来,板子上的开发环境通常装了各种调试工具、Python环境和系统更新包,编译器的版本、头文件路径、库路径都可能在某个升级之后悄悄变化,导致一个昨天还能编过的工程今天突然编不过。交叉编译把编译环境固化在PC上,板子始终保持"干净的运行环境",这个理念在后期调试模型推理、定位性能问题时特别好用。

6.3 给"从头到脚"系列读者的实战目录建议

既然PC端和板端的目录已经统一为/home/orangepi/rk3588_ws,建议把这个目录好好规划一下。我目前用的是这样一套结构:

rk3588_ws/ ├── src/ # 所有源码,包括本次hello.c ├── deps/ # 第三方依赖库,比如rknn runtime的.h和.so ├── models/ # rknn格式模型文件 ├── deploy/ # 交叉编译后的可执行文件和配套脚本 └── logs/ # 各种运行日志、性能测试记录

这个目录结构的好处是:源码、依赖、模型、产物分得很清楚,后面写部署脚本、做性能对比实验的时候,不会出现"到处找文件"的混乱。

交叉编译hello这件事,做完了你会发现它小得不能再小,但它把整个"PC交叉编译到ARM64板子"的工作流完整走了一遍。后面的yolov5s部署,不管是改模型、调后处理、优化NPU推理,都是在这个工作流上加功能而已。工具链和环境稳了,剩下的就是模型本身的活了。

最后再分享一个小经验:在PC上交叉编译成功并不等于万事大吉,编译器和它背后的一整套底层环境是"PC与板子之间的桥",而验证这座桥是否真的通的唯一标准,就是那个最简单的hello能否在板子上打印出一行字。这个系列后续每篇文章都会建立在这个已验证过的桥上,你把这期扎扎实实走完,后面会顺很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询