☰
RK3588交叉编译实战:从hello world到YOLOv5s环境搭建
2026/9/26 12:02:02 网站建设 项目流程

1. 为什么"交叉编译hello"是RK3588开发绕不开的第一道坎

很多人拿到香橙派5之后,第一反应是插电、烧系统、接屏幕,然后在板子上直接写代码编译。这么做在PC上没问题,放到嵌入式板子上就是另一回事了。香橙派5搭载的RK3588是一颗8核ARM64处理器(4个Cortex-A76大核加4个Cortex-A55小核),性能确实强,但它的存储空间、内存资源、散热条件跟一台正常的开发主机没法比。你不可能在板子上装一整套GCC工具链、头文件、CMake、各种依赖库,然后编译一个稍微大一点的项目——编译到一半磁盘满了、温度飙到降频、链接阶段内存不够被OOM杀掉,这些都是家常便饭。

所以嵌入式开发的标准做法是:在x86_64的PC上编译出ARM64能跑的可执行文件,再传到板子上运行。这个"在A平台上编译出B平台能执行的程序"的过程,就叫交叉编译。而"hello world"是验证整条工具链是否打通的最小闭环——它不涉及任何业务逻辑,只验证编译器能不能用、链接器能不能找到正确的库、生成的可执行文件架构对不对、传到板子上能不能跑起来。这一步跑通了,后面编译YOLOv5s的推理程序、编译OpenCV、编译FFmpeg才有意义。

我见过太多人卡在这一步:工具链装了三遍,环境变量配了又改,编译出来的文件一放到板子上就报cannot execute binary file: Exec format error,或者No such file or directory——明明文件就在那里。这些问题的根因往往不是工具链本身有问题,而是架构没对上、动态链接器路径不对、或者用了错误的编译选项。这篇就把交叉编译hello的完整链路拆开讲清楚,从工具链选型到验证方法,每一步都说明白为什么这么做。

提示:交叉编译的核心不是"编译"这个动作,而是"让编译产物在目标平台上正确加载和运行"。理解这一点,后面所有报错你都能自己定位。

2. 工具链选型:为什么我最终用了官方推荐的aarch64-none-linux-gnu

2.1 三种常见工具链的取舍

给RK3588做交叉编译,市面上能用的工具链大致分三类,我逐个说下实际体验。

第一类是Linaro官方发布的aarch64工具链,比如gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu。这类工具链历史悠久、资料多,但版本偏老,对C++17/20的支持不完整。如果你后面要编译YOLOv5s的推理代码,里面用到一些较新的C++特性,老版本GCC会直接报语法错误。我早期用7.5.0编译一个用了std::filesystem的demo,直接编译不过,换成8.x才解决。

第二类是ARM官方维护的GNU Toolchain,也就是arm-gnu-toolchain-xx.x-x86_64-aarch64-none-linux-gnu。这是目前最推荐的选择,版本更新及时,对C++标准支持好,而且命名规范清晰。aarch64-none-linux-gnu这个target triple的含义是:aarch64架构、none表示不绑定特定厂商、linux表示目标操作系统、gnu表示使用glibc。香橙派5跑的是Ubuntu 20.04(glibc 2.31),用这个工具链编译出来的程序能直接跑。

第三类是Buildroot或Yocto生成的工具链。如果你是用Buildroot自己构建的根文件系统,那工具链必须用Buildroot配套生成的那个,因为它的sysroot里包含了目标系统所有的库和头文件。用别的工具链编译出来的程序,很可能因为glibc版本不匹配而跑不起来。但如果你用的是香橙派官方提供的Ubuntu镜像,那就用ARM官方工具链即可。

我最终选的是ARM GNU Toolchain 10.3或11.3版本,原因是:版本足够新能支持现代C++,同时glibc版本(2.33/2.35)跟Ubuntu 20.04的2.31兼容——注意这里有个坑,工具链的glibc版本不能高于目标系统的glibc版本,否则编译出来的程序在板子上会因为找不到对应版本的符号而报错。10.3和11.3的glibc分别是2.33和2.35,理论上高于2.31,但实际测试中只要不调用新版本特有的符号,程序能正常运行。如果你追求绝对稳妥,可以用9.2版本,它的glibc是2.31,跟Ubuntu 20.04完全一致。

2.2 下载与目录规划

工具链不要随便解压到桌面或者下载目录,后面环境变量配起来会乱。我习惯在/opt下建一个专门的目录:

sudo mkdir -p /opt/toolchain cd /opt/toolchain # 假设你已经下载了arm-gnu-toolchain-11.3.rel1-x86_64-aarch64-none-linux-gnu.tar.xz sudo tar -xf arm-gnu-toolchain-11.3.rel1-x86_64-aarch64-none-linux-gnu.tar.xz

解压后目录结构是这样的:

/opt/toolchain/arm-gnu-toolchain-11.3.rel1-x86_64-aarch64-none-linux-gnu/ ├── bin/ │ ├── aarch64-none-linux-gnu-gcc │ ├── aarch64-none-linux-gnu-g++ │ ├── aarch64-none-linux-gnu-ld │ └── ... ├── lib/ ├── include/ └── aarch64-none-linux-gnu/ └── libc/ # 这就是sysroot

bin目录下是各种工具,aarch64-none-linux-gnu/libc是sysroot,里面包含了目标平台的libc、头文件等。交叉编译时,编译器会默认去这个sysroot里找头文件和库,而不是去你PC的/usr/include找——这一点很关键,很多人编译报"找不到xxx.h",就是因为编译器错误地用了主机的头文件。

2.3 环境变量配置的两种方式

配置环境变量有两种方式,我推荐第二种。

第一种是临时生效,只在当前终端有效:

export PATH=/opt/toolchain/arm-gnu-toolchain-11.3.rel1-x86_64-aarch64-none-linux-gnu/bin:$PATH

第二种是写进~/.bashrc,永久生效:

echo 'export PATH=/opt/toolchain/arm-gnu-toolchain-11.3.rel1-x86_64-aarch64-none-linux-gnu/bin:$PATH' >> ~/.bashrc source ~/.bashrc

配完之后验证一下:

aarch64-none-linux-gnu-gcc -v

如果输出了gcc版本信息,说明PATH配对了。如果提示command not found,检查路径拼写,特别是版本号那一段,很容易打错。

注意:不要用export CROSS_COMPILE=aarch64-none-linux-gnu-这种方式来配,那个是给内核编译用的,用户态程序编译直接靠PATH找工具就行。

3. 从hello.c到可执行文件:每一步到底发生了什么

3.1 写一个"不那么简单"的hello

很多人写hello就是printf("hello\n"),但这样验证不出什么东西。我建议写一个稍微带点信息的版本,把编译时间、架构信息都打出来:

#include <stdio.h> int main(void) { printf("Hello from RK3588 cross compile!\n"); printf("Compiled on: %s %s\n", __DATE__, __TIME__); printf("Pointer size: %zu bytes\n", sizeof(void*)); return 0; }

sizeof(void*)在64位ARM上是8,在32位ARM上是4。如果编译出来的程序在板子上打印出8,说明确实是64位程序;如果打印出4,那说明工具链用错了,编成了32位。这是一个非常直观的验证手段。

3.2 编译命令的每个参数都值得说清楚

最基础的编译命令是:

aarch64-none-linux-gnu-gcc hello.c -o hello

但实际项目中,我建议加上几个参数,让编译过程更可控:

aarch64-none-linux-gnu-gcc hello.c -o hello -static -O2 -Wall

逐个解释:

  • -static:静态链接。这个参数在交叉编译hello阶段特别有用,因为它把libc也打包进可执行文件,板子上不需要有对应的动态库就能跑。缺点是文件体积大(几MB),优点是绝对不会出现动态链接器找不到的问题。等你验证完工具链没问题,再改成动态链接去验证库路径。
  • -O2:优化级别。hello用不上,但养成习惯,后面编译YOLOv5s推理代码时优化级别直接影响推理速度。
  • -Wall:打开所有警告。交叉编译时警告往往能提前暴露架构相关的问题,比如指针截断、类型不匹配。

编译完成后,用file命令检查产物架构:

file hello

正确输出应该是:

hello: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, for GNU/Linux 3.7.0, not stripped

如果看到的是x86-64,说明你用了主机的gcc而不是交叉编译器;如果看到的是ARM, EABI5(32位),说明工具链选错了。这一步是交叉编译验证的第一道关卡,必须确认架构是ARM aarch64。

3.3 动态链接版本:验证sysroot是否正确

静态版本跑通后,再试动态链接:

aarch64-none-linux-gnu-gcc hello.c -o hello_dynamic -O2 -Wall

用readelf看它依赖哪些动态库:

aarch64-none-linux-gnu-readelf -d hello_dynamic | grep NEEDED

输出大概是:

0x0000000000000001 (NEEDED) Shared library: [libc.so.6]

再看它指定的动态链接器路径:

aarch64-none-linux-gnu-readelf -l hello_dynamic | grep interpreter

输出:

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

这个路径/lib/ld-linux-aarch64.so.1是目标板上的路径,不是PC上的。板子的Ubuntu系统里必须有这个文件,否则程序跑不起来。香橙派官方的Ubuntu镜像里默认就有,所以一般不用担心。但如果你是自己构建的根文件系统,就要确认这个文件存在。

提示:如果板子上报No such file or directory,但文件明明存在,99%是动态链接器路径不对。用readelf -l看interpreter那一行,然后去板子上确认那个路径下的文件是否存在。

4. 把hello传到板子上:几种传输方式的实测对比

4.1 scp:最通用但有前提

最常用的方式是scp:

scp hello orangepi@192.168.1.100:/home/orangepi/

前提是板子已经联网,而且开了SSH服务。香橙派官方的Ubuntu镜像默认装了openssh-server,开机就能用。默认用户名密码一般是orangepi/orangepi。

scp的优点是简单直接,缺点是依赖网络。如果板子还没配好网络,或者你在一个没有路由器的环境里,scp就用不了。

4.2 U盘:最土但最可靠

我调试初期最常用的其实是U盘。把编译好的hello拷到U盘,插到板子上,mount一下,cp过去。听起来很原始,但它不依赖任何网络配置,板子只要能启动就能用。

# 在板子上操作 lsblk # 找到U盘设备名,一般是sda1 sudo mount /dev/sda1 /mnt cp /mnt/hello ~/ sudo umount /mnt

U盘的坑在于文件系统格式。如果U盘是NTFS格式,Ubuntu默认可能不带ntfs-3g驱动,mount会失败。用FAT32或ext4格式最稳妥。另外,从U盘拷过来的文件可执行权限会丢失,需要手动加:

chmod +x hello

4.3 ADB:如果你烧的是Android系统

有些人的RK3588板子烧的是Android而不是Ubuntu,那就用adb:

adb push hello /data/local/tmp/ adb shell chmod +x /data/local/tmp/hello adb shell /data/local/tmp/hello

但要注意,Android的libc是bionic而不是glibc,用aarch64-none-linux-gnu工具链编译出来的程序在Android上跑不了。Android要用NDK的工具链,这是另一套东西。这篇讲的是Linux环境,Android的情况不展开。

4.4 传输方式对比

方式前提条件优点缺点
scp板子联网+SSH快、可脚本化依赖网络
U盘无不依赖网络手动操作、权限丢失
ADBAndroid系统集成度高仅限Android
NFS挂载主机开NFS服务实时同步配置复杂

我个人的习惯是:调试阶段用U盘,稳定后用scp。因为调试阶段网络配置经常变,U盘最省心。

5. 板子上跑不起来?按这个顺序排查

5.1 第一层:文件能不能执行

chmod +x hello ./hello

如果报Permission denied,就是没加执行权限。如果报cannot execute binary file: Exec format error,说明架构不对,回到第3步用file命令检查。

5.2 第二层:动态链接器找不找得到

如果报No such file or directory,但ls -l hello明明能看到文件,那就是动态链接器的问题。用readelf -l hello | grep interpreter看它要哪个链接器,然后去板子上确认:

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

如果这个文件不存在,要么换成静态编译(加-static),要么在板子上装对应的libc包。

5.3 第三层:依赖库版本对不对

如果报version 'GLIBC_2.34' not found,说明你编译时用的glibc版本高于板子上的。解决办法有两个:换用更低版本的交叉工具链,或者在板子上升级glibc(不推荐,容易搞崩系统)。

查看板子glibc版本:

ldd --version

查看编译产物需要的glibc版本:

aarch64-none-linux-gnu-readelf -V hello | grep GLIBC

两边对比一下,编译产物要求的版本不能高于板子提供的版本。

5.4 第四层:CPU特性对不对

RK3588是ARMv8.2-A架构,支持一些较新的指令。如果你编译时用了-march=armv8.2-a,但板子的内核没开对应的支持,可能会报Illegal instruction。交叉编译hello阶段不要加-march参数,用默认的就行。等确认工具链没问题了,再针对RK3588做指令集优化。

注意:Illegal instruction这个报错很隐蔽,因为程序能启动,只是在执行到某条特定指令时才崩。排查方法是把优化级别降到-O0,看是否还崩。如果-O0不崩、-O2崩,基本就是指令集的问题。

6. 从hello延伸到YOLOv5s:交叉编译环境的复用

6.1 为什么hello跑通了,YOLOv5s还不一定能编译

hello只依赖libc,而YOLOv5s的推理程序依赖OpenCV、可能还依赖RKNN SDK、pthread、数学库等等。hello跑通只证明工具链本身没问题,不证明所有依赖库都能找到。

编译YOLOv5s推理程序时,最常见的报错是fatal error: opencv2/opencv.hpp: No such file or directory。这是因为交叉编译器的sysroot里没有OpenCV的头文件。解决办法是自己交叉编译OpenCV,然后把它安装到sysroot里,或者用-I和-L参数手动指定头文件和库的路径。

6.2 sysroot的扩展方法

假设你把OpenCV交叉编译后安装到了/opt/rk3588/opencv,编译YOLOv5s程序时可以这样指定:

aarch64-none-linux-gnu-g++ yolov5_demo.cpp -o yolov5_demo \ -I/opt/rk3588/opencv/include \ -L/opt/rk3588/opencv/lib \ -lopencv_core -lopencv_imgproc -lopencv_dnn \ -Wl,-rpath-link=/opt/rk3588/opencv/lib

-Wl,-rpath-link这个参数很关键,它告诉链接器在链接阶段去哪里找依赖库。不加的话,链接时可能报undefined reference。

6.3 把常用路径写进环境变量

每次编译都敲一长串-I和-L太累,可以写进环境变量:

export CPLUS_INCLUDE_PATH=/opt/rk3588/opencv/include:$CPLUS_INCLUDE_PATH export LIBRARY_PATH=/opt/rk3588/opencv/lib:$LIBRARY_PATH export LD_LIBRARY_PATH=/opt/rk3588/opencv/lib:$LD_LIBRARY_PATH

但要注意,LD_LIBRARY_PATH影响的是运行时的库查找,交叉编译阶段主要靠LIBRARY_PATH和-L。而且LD_LIBRARY_PATH设成主机路径后,在板子上跑程序时如果也带着这个变量,会去找一个不存在的路径,反而出问题。所以交叉编译环境和板子运行环境要分开管理,不要混用同一套环境变量。

6.4 一个实用的Makefile模板

实际项目中,我习惯用一个Makefile来管理交叉编译:

CROSS_COMPILE = aarch64-none-linux-gnu- CC = $(CROSS_COMPILE)gcc CXX = $(CROSS_COMPILE)g++ CFLAGS = -O2 -Wall -I/opt/rk3588/opencv/include LDFLAGS = -L/opt/rk3588/opencv/lib -Wl,-rpath-link=/opt/rk3588/opencv/lib LIBS = -lopencv_core -lopencv_imgproc -lopencv_dnn -lpthread TARGET = yolov5_demo SRCS = yolov5_demo.cpp $(TARGET): $(SRCS) $(CXX) $(CFLAGS) $^ -o $@ $(LDFLAGS) $(LIBS) clean: rm -f $(TARGET)

这样每次只需要make就行,不用重复敲参数。换工具链时只改CROSS_COMPILE一个变量。

7. 几个我踩过的坑和对应的解法

7.1 坑一:工具链版本和板子glibc不匹配

这个坑我踩过两次。第一次是用了GCC 12的工具链,glibc 2.36,板子是Ubuntu 20.04的glibc 2.31,编译出来的程序报GLIBC_2.34 not found。第二次是用了GCC 7.5的老工具链,编译一个用了std::filesystem的程序,直接编译不过。

解法:先确认板子的glibc版本(ldd --version),然后选一个glibc版本不高于它的工具链。Ubuntu 20.04对应glibc 2.31,选GCC 9.2或10.3比较稳妥。

7.2 坑二:PATH里有多个交叉编译器

如果你之前装过别的交叉工具链,PATH里可能有多个aarch64-*-gcc。which aarch64-none-linux-gnu-gcc确认一下用的是哪个。更隐蔽的是,有些工具链的前缀一样但版本不同,比如两个都是aarch64-none-linux-gnu-gcc,但一个在/usr/bin一个在/opt。PATH顺序决定了用哪个。

解法:用绝对路径调用编译器,或者在脚本里显式设置PATH,不要依赖默认PATH。

7.3 坑三:编译出来的程序在板子上跑,但行为不对

有一次编译一个用到char类型符号判断的程序,在PC上跑没问题,在板子上跑结果不对。原因是ARM默认char是无符号的,x86默认char是有符号的。这个差异在涉及位运算和符号扩展时会暴露出来。

解法:编译时加-fsigned-char强制char为有符号,跟x86行为一致。或者代码里显式用signed char和unsigned char,不依赖默认行为。

7.4 坑四:静态编译后文件太大

-static编译出来的hello有几MB,如果项目里有很多个可执行文件,加起来占的空间很可观。而且静态链接的程序无法使用动态库的热修复,如果libc有安全更新,静态程序不受影响但也享受不到修复。

解法:调试阶段用静态,确认工具链没问题后改成动态。动态版本的文件通常只有几十KB。

8. 验证清单:交叉编译环境是否真正可用

在进入YOLOv5s编译之前,用下面这个清单逐项确认:

检查项命令预期结果
编译器可用aarch64-none-linux-gnu-gcc -v输出版本信息
架构正确file helloARM aarch64
位数正确板子上运行./helloPointer size: 8
动态链接器存在ls /lib/ld-linux-aarch64.so.1文件存在
glibc版本兼容ldd --version对比readelf -V板子版本≥程序要求
执行权限ls -l hello有x权限

这六项全过了,说明交叉编译环境完全可用,可以开始编译OpenCV、RKNN SDK、YOLOv5s推理程序了。任何一项没过,回到对应章节排查。

我个人在实际操作中的体会是,交叉编译最耗时间的不是编译本身,而是环境配置和问题排查。把hello这一步做扎实,把工具链版本、sysroot路径、动态链接器这些基础概念搞清楚,后面编译再大的项目也只是参数多少的问题,不会在根子上卡住。

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

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

立即咨询