
从第1篇到第5篇我们一直在跟香橙派RK3588这个板子打交道烧写Ubuntu、配SSH、挂载硬盘、检查系统资源……说句实话只有到了这篇《06 交叉编译hello》这个系列才算真正开始碰代码。为什么到了第六篇才写第一个程序因为后面要部署yolov5s香橙派RK3588上要跑的很多东西不能靠一条apt install解决。很多时候你需要自己去编译第三方库而RK3588是ARM架构AArch64你平时开发用的电脑大概率是x86架构。在x86电脑上编译出ARM板子能运行的程序这个过程叫交叉编译。文章就是整个系列里最基础的那个“最小闭环”写好一个hello.c用交叉编译工具链生成香橙派上能跑的文件再传上去执行。适合已经烧好系统、能SSH连上板子、但没写过交叉编译的新手。1. 为什么到这一篇才写第一个程序交叉编译的现实理由1.1 在板子上直接编译香吗RK3588核心数是不少主频也不算低看起来直接拿着香橙派当电脑用完全没问题。我第一次用Orange Pi 5的时候也这么想甚至在板子上写过Python的小脚本确实流畅。可一旦碰真正的C/C依赖库情况就不一样了。我试过直接在板子上编译一个OpenCV子模块4个线程一开CPU温度很快就蹿到70度以上风扇开始嘶吼一个小模块能磨蹭十几分钟。这不是RK3588太弱而是嵌入式板子的散热、内存带宽和存储IO天生就不是为“编译大型工程”设计的。编译过程会产生大量临时文件对存储寿命也不友好。所以在主机上交叉编译再把编译产物拷到板子上跑是嵌入式开发里的常规操作。当然交叉编译也有学习成本。但后面部署yolov5s的时候会碰到大量需要自己编译的组件比如OpenCV、RKNN相关运行时、各种内网或开源依赖。现在不把交叉编译的基本流程搞明白到了那一步你会寸步难行。1.2 交叉编译到底在“交叉”什么用一句人话解释在机器A上编译出一种机器B能直接运行的程序就叫交叉编译。A叫宿主机hostB叫目标机target两者架构不一样所以是“交叉”的。你的PC是Intel/AMD的x86或x86_64香橙派RK3588是ARMv8-A 64位架构也就是常见说的AArch64。普通gcc默认编译出来的是本机架构的机器码拿到ARM板子上根本跑不了。所以我们要用专门的目标工具链也就是aarch64-linux-gnu-gcc。它运行在x86电脑上但生成的机器码是ARM指令集。这套工具链里不止一个编译器而是完整的一套gcc负责把C语言翻译成汇编和机器码binutils里的as、ld、objdump、readelf负责汇编、链接和分析编译器背后还要有一套ARM架构的libc运行库和头文件它们通常被放在/usr/aarch64-linux-gnu/目录下。注意交叉编译最怕的就是“意外使用了宿主机的库和头文件”。我刚开始学的时候以为只需要给普通gcc加个-march参数就能生成ARM程序。结果编译出来的文件传到板子上要么报段错误要么提示缺少某个库。原因就是没用真正的交叉工具链sysroot也不对。所谓sysroot简单说就是交叉编译器专用的“根目录”。它里面放着目标平台的头文件、库文件和动态链接器。当你编译hello.c时编译器不会去读x86系统的/usr/include/stdio.h而是去读/usr/aarch64-linux-gnu/include下的ARM版本头文件。这一步前期不重视后面编译大型项目时会非常痛苦。2. 先在x86 Ubuntu主机上把工具链拉起来2.1 安装gcc-aarch64-linux-gnu做交叉编译我最推荐你在x86架构的Ubuntu 20.04或22.04主机上进行。如果你用的Windows请先装一个WSL2或虚拟机里的Ubuntu不要硬在Windows上用各种奇奇怪怪的方案。后面yolov5s要处理CMake、Makefile、依赖库Linux环境最顺手。Debian/Ubuntu系可以直接用apt安装sudo apt update sudo apt install gcc-aarch64-linux-gnu如果需要编译C代码顺手把g也装上sudo apt install g-aarch64-linux-gnu安装完成后系统里会多出这些命令aarch64-linux-gnu-gcc aarch64-linux-gnu-g aarch64-linux-gnu-readelf aarch64-linux-gnu-objdump有些版本会带具体的版本号后缀比如aarch64-linux-gnu-gcc-11。不过一般系统会在/usr/bin/aarch64-linux-gnu-gcc做一个不带版本号的链接直接用即可。如果你用的是RHEL系的发行版理论上也有对应的交叉工具链包但我个人建议还是统一用Ubuntu/Debian。原因很简单RK3588官方镜像大多基于Ubuntu/Debian交叉工具链使用同样的glibc版本和ABI规则踩坑少得多。2.2 装完第一时间验证工具链装完不要急着写代码先确认工具链真的能用。最简单的验证aarch64-linux-gnu-gcc --version能看到版本信息说明编译器本体安装成功。但光看版本不够我建议写一个最小的空文件或者直接跳到下一章用hello.c验证。因为“编译器能启动”和“能找到正确的头文件、库文件”是两码事。之前有朋友装完工具链编译的时候报stdio.h: No such file or directory一脸懵。原因就是它的系统里只有x86的头文件没有安装交叉编译的C库头文件。在Ubuntu上gcc-aarch64-linux-gnu这个包会依赖libc6-dev-arm64-cross正常情况下头文件和库都会装到/usr/aarch64-linux-gnu/下面。如果没装全手动补一下sudo apt install libc6-dev-arm64-cross binutils-aarch64-linux-gnu装完可以用dpkg -l | grep aarch64检查相关包是否都在。还有一个很小的经验如果在某些精简环境里找不到aarch64-linux-gnu-gcc先检查/usr/bin下有没有这个命令再检查PATH。但apt正常安装后PATH都会包含/usr/bin不需要你额外配置。3. 第一行ARM代码hello.c的编写、编译与产物分析3.1 一个带点ARM味的hello正常交叉编译教程里hello.c基本都是#include stdio.h int main(void) { printf(Hello from RK3588!\n); return 0; }这当然没问题。不过我建议在代码里加一点“架构指纹”让你一眼就能看出这个程序确实是为ARM准备的#include stdio.h #include stddef.h int main(void) { #ifdef __aarch64__ printf(Arch: AArch64\n); #else printf(Arch: unknown\n); #endif printf(Pointer size: %zu bytes\n, sizeof(void *)); printf(Hello from RK3588, cross-compiled!\n); return 0; }__aarch64__是gcc在AArch64目标平台上预定义的宏。交叉编译的时候编译器自动定义它普通x86编译不会定义。程序运行时printf会把当前运行平台的架构打出来。这样你就能确认我们交叉编译出来的二进制真的跑在ARM架构上而不是某个模拟器或兼容层里。sizeof(void *)用来输出指针长度AArch64 64位环境下是8字节。这个值是架构级别的常识也能帮你确认程序没有跑在“假的32位环境”。3.2 编译、file、readelf三连在主机上执行aarch64-linux-gnu-gcc -o hello hello.c编译完成后立即用file查看文件格式file hello正常输出hello: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1 ...看到ARM aarch64就说明编译目标没问题。再用readelf看一眼ELF头aarch64-linux-gnu-readelf -h hello重点看Machine那一行应该是AArch64。还可以查看动态解释器的路径aarch64-linux-gnu-readelf -l hello | grep interpreter输出通常是interpreter: /lib/ld-linux-aarch64.so.1。这个路径对第4章排查“not found”至关重要。动态链接的ARM程序在启动时内核会先去加载这个解释器也就是动态链接器然后由它加载libc等共享库。如果板子上找不到这个解释器程序就会报“not found”。正常Ubuntu for ARM系统里有这个文件但如果你的板子系统是精简RootFS就需要特别注意。为了后续对比我再多编一个静态版本aarch64-linux-gnu-gcc -static -o hello_static hello.c对比一下两个文件文件体积特点hello约8KB动态链接依赖板子上的libchello_static接近700KB静态链接运行时不依赖板子上的库为什么静态版本会大这么多因为它把C运行时、printf实现、内存管理等全部塞进了可执行文件里。虽然体积大但它适合放在没有完整系统库的环境里比如initramfs或裁剪过的RootFS。在RK3588的完整Ubuntu下动态链接版本完全够用也更省存储。4. 把二进制送到香橙派RK3588上跑起来4.1 我常用的传输方法假设你已经通过SSH连上了香橙派板子IP是192.168.1.100用户名是orangepi那么在主机上直接执行scp ./hello orangepi192.168.1.100:~/把IP替换成你自己的。如果SSH不在默认22端口用-P指定scp -P 2222 ./hello orangepi192.168.1.100:~/如果你的网络环境没有开SSH也可以先把板子和电脑连到同一个路由器然后在主机上起个HTTP服务python3 -m http.server 8000接着在板子上用wget下载wget http://192.168.1.50:8000/hello这个方法在临时调试时非常好用不需要额外装软件。还有个原始办法是U盘拷贝我就不细说了重点在于别用文本模式传输二进制。4.2 跑一下以及那个经典的not found传到板子上后先给执行权限chmod x ~/hello ./hello如果一切顺利你会看到类似输出Arch: AArch64 Pointer size: 8 bytes Hello from RK3588, cross-compiled!看到这行字说明整个交叉编译链路已经通了。但很多人第一次会碰见这个报错-bash: ./hello: No such file or directory注意这个报错非常容易误导人。它并不是说文件不存在而是内核找不到这个程序需要的“解释器”或某个动态库。你可以按下面顺序排查。第一确认文件确实在ls -l hello第二用file再看一次确认是ARM aarch64。如果你在x86主机上编译时不小心用了普通gcc那这里会显示x86-64板子自然跑不了。第三在板子上看系统的动态链接器是否存在ls -l /lib/ld-linux-aarch64.so.1如果这个文件不存在说明板子系统不是标准Ubuntu/ Debian或者你跑在一个很精简的initramfs环境里。这时候最简单的办法就是回到主机上把hello改成静态编译重新传一次就能跑。第四也可以查动态库依赖ldd hello正常输出应该看到linux-vdso.so.1、libc.so.6等。如果报错说某个.so找不到那就需要去补装对应的arm64运行库或者转移到静态编译排查。还有一个我踩过的坑编译时主机上的glibc版本比板子上的新运行时会报类似version GLIBC_2.34 not found。这是因为动态链接程序在启动时板子上的libc版本太低不满足程序的要求。解决办法是使用与目标系统版本相近的交叉编译工具链或者尽量静态编译。后面你编译yolov5s相关的第三方库时这类“glibc版本不匹配”的问题依然会出现现在提前有印象会省很多时间。4.3 用uname核实一下程序跑起来后再顺手在板子终端执行uname -m大概率输出aarch64。这跟hello程序里打印的Arch: AArch64相互印证。以后如果你怀疑“程序是不是没跑在真ARM环境”就可以用这个组合验证。5. 为yolov5s铺路交叉编译其实是一场依赖迁移5.1 从hello到工程化工具链、CMake与sysroothello只是单个源文件一行命令就能编译。到了yolov5s阶段你面对的不再是一个文件而是OpenCV、RKNN运行时、各种C/C依赖这个时候需要引入工程化构建工具最常用的是CMake。交叉编译CMake工程核心是准备一个toolchain文件告诉CMake“我的编译器是哪一套依赖库到哪里找”。我常用的最小模板如下set(CMAKE_SYSTEM_NAME Linux) set(CMAKE_SYSTEM_PROCESSOR aarch64) set(CMAKE_C_COMPILER aarch64-linux-gnu-gcc) set(CMAKE_CXX_COMPILER aarch64-linux-gnu-g) set(CMAKE_FIND_ROOT_PATH /usr/aarch64-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)解释一下关键点CMAKE_FIND_ROOT_PATH指向ARM架构的库和头文件根目录。MODE_PROGRAM NEVER表示查找程序时可以在宿主机上找因为编译期间需要运行一些宿主工具MODE_LIBRARY和MODE_INCLUDE设置成ONLY表示只在交叉环境的目录里找库和头文件避免抓到x86版本的库。这个文件不一定百分之百适配你的所有项目但hello阶段你只需要理解一个概念交叉编译的核心是“整个依赖树都得换成目标平台的”。工具链安装时把libc放进了/usr/aarch64-linux-gnu/所以hello能直接编过。后面交叉编译OpenCV时你也得先把OpenCV的arm64库放到这个sysroot里或者单独指定路径。错误地让CMake找到了x86的OpenCV编译过程会非常诡异各种undefined reference。5.2 yolov5s的部署路线哪些环节需要交叉编译说到yolov5s我先按自己的经验把RK3588上的部署路线拆开因为不是所有路线都需要交叉编译C代码。第一种是纯Python路线。你在PC上用《rknn-toolkit2》把yolov5s模型转换成RKNN格式然后把模型和Python推理脚本传到香橙派上板子里装好对应的rknpu runtime和Python依赖。大部分Python依赖都有arm64的wheel直接pip安装就行。这条路线对交叉编译的需求很低甚至可以说不需要碰交叉工具链。第二种是C/C路线。如果你想用RKNN提供的C API写一个更高效的推理服务或者在板端做图像预处理、后处理、服务集成那就需要交叉编译你的C/C程序并链接RKNN runtime和OpenCV等库。这时候本篇所讲的交叉编译hello就是热身运动。你会需要在PC上交叉编译一个完整的板端程序然后scp过去跑整体逻辑和hello一模一样。第三种是自己移植路线。如果你不想用RKNN而是想把原版yolov5s跑在RK3588上那通常要面对OpenCV、NCNN或者PyTorch的交叉编译工作量大很多。尤其是板端本地编译PyTorch基本上是噩梦级体验所以大家都会优先找交叉编译方案或者预编译包。我做这类板端项目时有个习惯每碰一个新库先交叉编译它自带的最小demo跑通了再往项目里集成。这样能把“编译问题”和“运行环境问题”分开不会出现改了到头来不知道是库的问题还是自己代码的问题。现在回头看看hello这个例子虽然简单但它覆盖了完整的交叉编译链条工具链安装、源码编写、产物检查、二进制传输、板端运行、动态库依赖排查。这些环节在后期的yolov5s部署中会不断重复。你可以顺手再做一个小实验把hello_static也传到板子上跑一次感受一下静态链接和动态链接在体积、执行上的差异。下一篇文章我会接着这套工具链进入yolov5s相关的依赖准备。如果你卡在哪一步优先检查解释器路径、动态链接器版本和文件权限这三个点覆盖了交叉编译新手遇到的八成问题。