经常在X86主机上调试Jetson TX2的朋友,早晚都要面对同一个问题:怎么在Ubuntu上给ARM架构的TX2配置一套能用的交叉编译链。这事看起来简单,实际动手才发现水挺深——工具链版本选错、sysroot缺失、CMake找错依赖库、编译好的程序传到板子上直接Illegal instruction,随便一个坑都能耗掉你一下午。这篇文章就按我自己走通的流程,从工具链安装、环境变量配置、sysroot同步,到CMake工具链文件编写和常见报错排查,完整过一遍。无论你是新手第一次搭,还是老手想换个更干净的方式,都能直接照着抄。
1. 为什么X86主机非要折腾ARM交叉环境
1.1 算力不对等,交叉编译是刚需
先聊点基础的逻辑。TX2是一块ARM架构的嵌入式开发板,虽然它有六核处理器,日常跑点小任务还行,但真要编译大型项目,比如OpenCV、深度学习推理库或者整个应用工程,板载的CPU算力要拖很久,而且内存就那么大,编译时频繁交换内存会让整个板子卡死。最典型的场景是编译Qt或者较高版本的OpenCV,在板子上直接make可能要几十分钟甚至更久,而在X86主机上交叉编译,几分钟就搞定了。
交叉编译的基本思路很简单:X86主机上运行的编译器,生成的目标代码不是X86架构的机器码,而是ARM架构(准确说是AArch64,也就是64位ARM)的机器码,然后把这个二进制文件拷贝到TX2上运行。这套流程绕开了“必须在目标设备上编译”的限制,让开发效率高一个数量级。
有人会问,为什么不直接在TX2上装个SSH然后用远程编译?可以,但前提是你愿意等,而且编译过程会把开发板变成一台高温风扇狂转的机器。实际项目里更常见的做法是:平时在主机上交叉编译,需要验证的时候就scp到板子上跑一下。这套流程熟练之后,迭代速度真的会快很多。
1.2 认清目标架构和系统版本:这是选工具链的前提
在动手装工具链之前,一定先搞清楚两个东西:目标设备的CPU架构和目标系统版本。TX2的CPU是ARMv8-A 64位架构,在系统里查出来就是aarch64。你在TX2终端里执行:
uname -m正常输出是aarch64,这代表64位ARM平台。交叉编译时选用的工具链前缀必须是aarch64-linux-gnu-,不能选成arm-linux-gnueabihf-——那是32位ARM的,前缀搞错了整条路就是歪的。
系统版本同样关键。TX2跑的是Ubuntu(通常基于18.04),对应JetPack/L4T的某个版本。很多人在主机上直接apt install gcc-aarch64-linux-gnu,装完编译也能过,但把二进制往板子上一传,经常会报缺少某个GLIBC版本。原因就是主机Ubuntu仓库里的交叉编译器比较新,编出来的程序可能依赖了TX2系统的glibc里没有的符号。所以我一直不推荐用apt的交叉编译器去做正式项目,除非你能确认两端glibc版本完全对得上。
工具链和系统版本的对应关系大概是这样:
| TX2系统 | 底层Ubuntu版本 | 推荐工具链 |
|---|---|---|
| JetPack 4.x / L4T R32.x | Ubuntu 18.04 | gcc-linaro-7.3.1 / aarch64-linux-gnu-gcc 7.x |
| JetPack 3.x / L4T R28.x | Ubuntu 16.04 | gcc-linaro 4.9 / aarch64-linux-gnu-gcc 5.x |
这里的核心原则就一句话:工具链版本宁可老一点,不能比目标系统新太多,尤其是glibc版本,错位会在运行期给你上眼药。
2. 工具链选型与安装:选对版本能少踩一半的坑
2.1 三种工具链获取渠道的对比
交叉编译工具链不是非要自己下载源码去构建,一般有三种现成的获取方式。
第一种,Ubuntu软件源直接安装gcc-aarch64-linux-gnu。好处是安装省事,一条apt命令搞定,适合快速跑通验证。问题是版本相对较新,跟老版本的L4T系统存在兼容性风险,编译C++项目时也比较容易踩libstdc++ ABI不匹配的坑。
第二种,下载Jetson官方配套的工具链压缩包。这个是最稳的方案,版本跟L4T绑定,基本不会出现glibc错位的问题。缺点是包比较大,下载地址需要去Jetson下载中心自己找对应版本,不能直接apt。
第三种,使用Linaro发布的预编译工具链。Linaro的交叉编译器在业内用得很多,压缩包格式清晰,解压就能用。很多嵌入式开发板官方实际上也是拿Linaro工具链魔改的,稳定性没有问题。
我个人的建议是:如果你是跟着本文第一次配,直接用Linaro或官方工具链压缩包,把版本锁定在你TX2对应L4T的推荐版本上。这样后面的坑能少很多。apt安装的方式虽然快,但做完了你心里没底,不知道啥时候就被glibc版本坑一下。
2.2 L4T官方工具链安装全过程
我这里以最常用的一片场景来演示:TX2跑JetPack 4.x(L4T R32.x),主机是Ubuntu 18.04或20.04,工具链版本选gcc-linaro-7.3.1-2018.05-x86_64_aarch64-linux-gnu。这个版本跟我们项目里用的工具链是一样的,兼容性经得起验证。
先从下载页面把工具链压缩包拿下来,文件名通常是这种格式gcc-linaro-7.3.1-2018.05-x86_64_aarch64-linux-gnu.tar.xz,然后按下面的步骤操作:
# 解压工具链到/opt目录下 sudo tar -xJf gcc-linaro-7.3.1-2018.05-x86_64_aarch64-linux-gnu.tar.xz -C /opt/ # 建一个简洁的软链接,方便后续版本切换 sudo ln -s /opt/gcc-linaro-7.3.1-2018.05-x86_64_aarch64-linux-gnu /opt/aarch64-gcc # 确认编译器能跑 /opt/aarch64-gcc/bin/aarch64-linux-gnu-gcc --version最后一条命令如果输出类似gcc (Linaro GCC 7.3-2018.05) 7.3.1 20180425这样的信息,说明编译器本体没问题。
这里说两个细节。第一,解压命令里的-xJf,-J表示处理xz格式压缩包,如果你的版本不是.tar.xz而是.tar.bz2,要用-xjf,不要记混。第二,Linux发行版如果太干净,可能没装xz解压工具,遇到解压报错先执行sudo apt install xz-utils。
软链接这步不是必须的,但很值得做。因为不同L4T版本用的工具链版本可能不同,你以后大概率还要装第二套、第三套交叉工具链。统一放在/opt下,再通过/opt/aarch64-gcc这种软链接指到当前要用的那套,后续切换版本只需要改链接,不需要动环境变量和构建脚本。
2.3 环境变量一次配到位
工具链装好后,每次用之前都要把可执行文件目录加到PATH里,同时约定好几个全局变量。不建议每次都手动敲export,直接写进~/.bashrc,一劳永逸。
# 打开.bashrc,把下面几行追加进去 # 交叉编译工具链路径 export PATH=/opt/aarch64-gcc/bin:$PATH # 工具链前缀,很多Makefile和构建系统会读取这个变量 export CROSS_COMPILE=aarch64-linux-gnu- # 常用编译器别名 export CC=${CROSS_COMPILE}gcc export CXX=${CROSS_COMPILE}g++ # sysroot路径,后面会详细说明,先占个位置 export SYSROOT=${HOME}/tx2_sysroot # 给gcc/g++全局指定sysroot export CFLAGS="--sysroot=${SYSROOT}" export CXXFLAGS="--sysroot=${SYSROOT}" export LDFLAGS="--sysroot=${SYSROOT}"保存后执行source ~/.bashrc,然后验证一下:
which aarch64-linux-gnu-gcc aarch64-linux-gnu-gcc -v 2>&1 | grep Target cd /tmp && echo 'int main(){return 0;}' > t.c && aarch64-linux-gnu-gcc -o t t.c && file tfile t的输出如果能看到ELF 64-bit LSB executable, ARM aarch64,恭喜,工具链这部分已经通了。
需要提醒的是,CFLAGS里指定--sysroot是一个全局兜底策略,能够保证即使某些老旧Makefile没有显式传入--sysroot,编译器也会自动去目标系统的头文件目录里找。这个策略我自己一直在用,确实能减少大量“编译时找不到头文件”“链接时混入主机库”的糟心事。
3. 交叉编译的灵魂:sysroot准备与校验
3.1 sysroot是什么,为什么少了它一定翻车
工具链装好了,很多教程到这一步就直接说“可以编译了”,实际上还差最核心的一块——sysroot。
什么叫sysroot?简单说,就是TX2根文件系统的一个完整副本。交叉编译的时候,编译器要去找目标平台的头文件(比如/usr/include/stdio.h)和库文件(比如libc.so.6、libstdc++)。如果你不指定sysroot,一个安装在新版Ubuntu主机上的编译器,默认就会去主机的/usr/include和/usr/lib里找头文件和库。结果就是,编译出来的二进制文件在链接阶段用的是X86的库、X86的路径,到了TX2上根本跑不起来,表现就是文件格式虽然对了,但动态链接器的路径指向完全不对,或者链接时直接报“找不到-lc”。
打个比方,你用中餐的菜谱,却跑去西餐的厨房里拿调料,做出来肯定不是那个味,甚至根本没法吃。
sysroot的正确来源有两种。第一种是直接用TX2上现成的根文件系统,通过rsync把整盘同步到主机;第二种是用L4T官方发布的rootfs压缩包解压出来。第一种最准确,因为板子上装了什么额外依赖、改了什么系统配置,都会同步过来,所以我更推荐直接从TX2拉取。
3.2 从TX2把根文件系统“搬”到主机的命令与细节
首先确认你能SSH登录TX2。网络方面保证主机和TX2在同一个局域网,然后先在TX2终端设置root账户密码:
sudo passwd root设置好之后,回主机的终端,创建sysroot目录并且开始rsync同步:
mkdir -p ${HOME}/tx2_sysroot sudo rsync -avx \ --exclude='/proc' \ --exclude='/sys' \ --exclude='/dev' \ --exclude='/tmp' \ --exclude='/run' \ --exclude='/mnt' \ --exclude='/media' \ --exclude='/lost+found' \ --exclude='/var/swap' \ root@{TX2_IP}:/ ${HOME}/tx2_sysroot/这里{TX2_IP}替换成你板子的实际IP,比如192.168.1.100。
同步过程通常持续几分钟,取决于网络速度和TX2存储容量,一般会在1到2GB上下。同步完成后,重点检查几个目录是否存在:
ls ${HOME}/tx2_sysroot/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1 ls ${HOME}/tx2_sysroot/usr/include/stdio.h ls ${HOME}/tx2_sysroot/usr/lib/aarch64-linux-gnu/libc.so这三个文件都存在,sysroot骨架基本就立住了。
这里面的坑主要在于rsync的排除参数。/proc、/sys、/dev这些虚拟文件系统是动态生成的,如果不过滤掉,rsync会在扫描过程中被无底洞一样的伪文件卡住,甚至会同步出一些奇怪的设备节点,污染你的sysroot。-x参数的作用是“不跨文件系统”,防止不知不觉把挂载的额外分区也一起拉进来。
另外,用root账号登录TX2同步是为了保留文件权限的原始归属,避免后续编译阶段因为权限问题报莫名其妙的错。如果你用普通用户同步,后面有一部分系统头文件可能因为权限不足没同步全,排查起来相当头疼。
3.3 同步完成后的自检与坑位预告
sync完成不代表万事大吉,我一般还会做三件事来确认sysroot可用。
第一,确认libc版本和目标系统一致。直接看符号版本:
${HOME}/tx2_sysroot/lib/aarch64-linux-gnu/libc.so.6输出里会带版本信息,正常情况应该跟TX2上ldd --version看到的一致。如果不一致,说明同步时可能抓错了源。
第二,检查链接器路径。交叉编译器在链接动态程序时,写进二进制里的动态链接器路径是/lib/ld-linux-aarch64.so.1,它在sysroot里的实际位置也要对得上。如果发现sysroot里这个路径缺失,编译出来的程序一传上板子就会报No such file or directory,而且是运行不了那种。
第三,养成同步后做一次最小编译验证的习惯。先不碰复杂库,写一个最普通的hello.c,用动态链接方式交叉编译,然后scp到TX2跑一下。这一步是快速确认整个链路是否通畅的最有效方法,具体操作我放在第4部分细说。
4. 实操打通:从hello到带依赖的CMake工程
4.1 第一关:动态链接hello交叉编译并上板验证
先来一条通往成功的路径。
新建一个测试目录,写一个hello.c:
#include <stdio.h> int main() { printf("hello from TX2 aarch64\n"); return 0; }然后交叉编译:
cd ~/cross_test aarch64-linux-gnu-gcc --sysroot=$SYSROOT -o hello hello.c注意这里的--sysroot=$SYSROOT必须带上,也就是我们刚才在.bashrc里配置的环境变量。编译完不要急着scp,先在本机查看二进制格式:
file hello输出里应当有ELF 64-bit LSB executable, ARM aarch64字样,并且后面跟着dynamically linked。如果没有dynamically linked而是statically linked,说明你可能在某个环节用了-static,这不影响在板子上运行,但不利于验证sysroot里的动态链接配置。
接着用readelf看它依赖哪些动态库:
aarch64-linux-gnu-readelf -d hello | grep NEEDED正常会看到libc.so.6。再去sysroot里确认这个库文件确实存在:
find $SYSROOT -name 'libc.so.6' -path '*aarch64*'都通过之后,把程序传上TX2:
scp hello ubuntu@{TX2_IP}:~/ ssh ubuntu@{TX2_IP} ./hello屏幕上打出hello from TX2 aarch64,整套交叉编译链路就彻底通了。
4.2 CMake工具链文件:一次性把依赖路径锁死
实际项目不可能都是单文件,用CMake管理工程的话,需要额外写一份toolchain.cmake。这份文件本质上就是告诉CMake三个信息:目标系统是什么、用什么编译器、去哪里找头文件和库。
下面这份工具链文件是我长期在用的模板,可以直接复制改参数:
# toolchain.cmake set(CMAKE_SYSTEM_NAME Linux) set(CMAKE_SYSTEM_PROCESSOR aarch64) # 从环境变量里读取路径 set(SYSROOT $ENV{SYSROOT}) set(CROSS_TOOLCHAIN $ENV{CROSS_TOOLCHAIN}) # 指定sysroot set(CMAKE_SYSROOT "${SYSROOT}") # 指定交叉编译器 set(CMAKE_C_COMPILER "${CROSS_TOOLCHAIN}/bin/aarch64-linux-gnu-gcc") set(CMAKE_CXX_COMPILER "${CROSS_TOOLCHAIN}/bin/aarch64-linux-gnu-g++") # 设置查找根路径和查找模式 set(CMAKE_FIND_ROOT_PATH "${SYSROOT}") set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)这里有几个点必须解释清楚。
CMAKE_FIND_ROOT_PATH的含义是“把哪些目录当作目标平台的根”。有了它,再配合下面三个FIND_ROOT_PATH_MODE,CMake里的find_package、find_library、find_path就会把搜索范围限制在sysroot里,而不会跑到主机/usr/lib里找X86库。
PROGRAM对应的模式设置成NEVER,意思是找可执行程序的时候还是去主机目录找,因为cmake、make这些工具跑在主机上,不能去sysroot里找主机的构建工具。
LIBRARY和INCLUDE设置成ONLY,是很关键的一步。如果不加这个限制,CMake默认会同时搜主机路径和sysroot路径,一旦主机上装了版本不一样的同名开发库,查找结果就不可控了,你以为是ARM的库,实际链接的可能是X86版本,最后link阶段报出几百条undefined reference。
用这个工具链文件配置工程:
cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=toolchain.cmake cmake --build build -j4-j4是我比较保守的选择。交叉编译过程中,每个编译进程的宿主是X86主机,理论上开更多并行没问题,但如果你主机的内存不太宽裕,或者sysroot所在磁盘是机械硬盘,并行太高容易IO卡死,甚至内存吃紧触发OOM。稳妥起见,先-j4跑通再调高。
4.3 带OpenCV依赖的工程示例
单文件hello跑通后,很多项目会进入下一个阶段:依赖第三方库。这里举一个最常见的OpenCV例子。
先说前提。要交叉编译带OpenCV的程序,sysroot里必须已经有ARM版的OpenCV库和头文件。TX2上如果预装了OpenCV(很多JetPack系统自带),那么在你rsync同步rootfs的时候,它已经跟着进来了。确认一下sysroot里这些路径是否存在:
ls $SYSROOT/usr/include/opencv4/opencv2/opencv.hpp ls $SYSROOT/usr/lib/aarch64-linux-gnu/cmake/opencv4/如果存在,就可以继续。
写一个简单的工程,main.cpp:
#include <opencv2/opencv.hpp> #include <iostream> int main() { cv::Mat img(200, 200, CV_8UC3, cv::Scalar(0, 255, 0)); std::cout << "OpenCV version: " << cv::getVersionString() << std::endl; cv::imwrite("green.png", img); return 0; }CMakeLists.txt:
cmake_minimum_required(VERSION 3.10) project(hello_opencv CXX) find_package(OpenCV REQUIRED) add_executable(hello_opencv main.cpp) target_link_libraries(hello_opencv ${OpenCV_LIBS})然后正常用toolchain文件去configure:
cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=toolchain.cmake cmake --build build -j4如果find_package阶段没找到OpenCV,可以用下面的方式显式指定搜索路径:
cmake -B build -S . \ -DCMAKE_TOOLCHAIN_FILE=toolchain.cmake \ -DOpenCV_DIR=$SYSROOT/usr/lib/aarch64-linux-gnu/cmake/opencv4编译成功之后把hello_opencv和它依赖的OpenCV相关动态库一起传到TX2上跑。如果TX2系统里本身就有OpenCV,那么程序直接跑没问题;如果是从别的设备同步过来的rootfs,还得把依赖的动态库一并带过去。
这里有个实际项目里会遇到的隐藏坑:OpenCVConfig.cmake里可能会硬编码/usr/lib/aarch64-linux-gnu/libopencv_core.so这类绝对路径。CMake在导入target时,如果发现这里是绝对路径,就会直接把这个主机地址塞进链接命令里,不会自动加上sysroot前缀,最后编译会失败。遇到这种问题,要么手动编辑sysroot里的OpenCVConfig.cmake,要么在toolchain文件里加一句:
set(CMAKE_IGNORE_PATH /usr/lib /usr/include /usr/lib/aarch64-linux-gnu)这句的作用是强制CMake忽略主机路径,让后端的导入库路径也能被“翻译”到sysroot前缀下。这个坑我实际踩过,遇到的时候一度很莫名,排查了蛮久才定位到是配置文件的绝对路径问题。
5. 高频报错与排查记录
5.1 编译期常见错误速查表
交叉编译环境的报错,很多都有固定的表现特征。我把这几年用得最多的排查点整理成表,方便你对着查:
| 报错信息 | 大概率原因 | 处理思路 |
|---|---|---|
fatal error: stdio.h: No such file or directory | 编译命令没有指定sysroot | 确认编译和链接都带--sysroot,不能用主机头文件目录 |
cannot find -lc/cannot find -lstdc++ | sysroot里库文件不完整 | 检查/usr/lib/aarch64-linux-gnu和/lib/aarch64-linux-gnu是否有对应.so文件 |
cannot find crtbegin.o | --sysroot传了但指针指向的目录不是真正rootfs | 用ls $SYSROOT/usr/lib/aarch64-linux-gnu看下结构是否完整 |
/usr/bin/ld: skipping incompatible ... when searching for ... | 链接器搜到了主机X86的库 | 说明FIND_ROOT_PATH没生效,CMake工程优先检查toolchain文件 |
GLIBC_2.28 not found | 工具链生成的目标链接到新版glibc符号 | 换用与L4T版本匹配的旧工具链,不要用最新版apt交叉编译器 |
编译时进程被Killed | 内存不足,并行编译线程太高 | 调低-j参数,先-j2跑一次试试 |
unrecognized command line option | 工具链版本过老 | 降低标准要求,或者换新版工具链,首选官方配套版本 |
报错里出现x86_64字样 | 编译器不是交叉编译器 | 确认which aarch64-linux-gnu-gcc在PATH前面,且CMake的C_COMPILER正确 |
这里面我想单独强调一下“跳过不兼容库”的报错。它的本质是链接器在搜索路径里同时看到了ARM库和X86库,而CMake的搜索顺序恰好先拿到了X86库。这个情况在主机上恰好装了原生开发库的时候特别容易发生,也是很多人交叉编译OpenCV的时候会撞见的问题。解决办法就是确保toolchain文件里的FIND_ROOT_PATH_MODE_LIBRARY为ONLY,并配合CMAKE_IGNORE_PATH把主机路径全部挡掉。
5.2 运行期异常与几个容易忽视的细节
编译期过了,传输到板子上运行又会出现另一批问题。
最常见的是cannot execute binary file: Exec format error。看到这个报错,你先确认是不是在X86主机上直接执行了ARM程序——如果是,那很正常,换到TX2上跑就行。但如果你确实在TX2上执行还报这个,那就是二进制格式本身出了问题,可以用readelf检查:
aarch64-linux-gnu-readelf -h hello | grep Machine输出应当是ARM AArch64。如果输出是x86_64,说明编译过程中调用了宿主机的编译器,回去查CMake工具链或者Makefile里的CC环境变量。
然后是error while loading shared libraries。这类报错一般出现在你依赖了一些非系统自带的共享库时。TX2上如果没有提前装这个依赖,就会在运行时提示找不到.so文件。排查思路是先在TX2上执行ldconfig -p看看系统里有没有这个库,没有就把主机sysroot里对应的.so用scp拷过去,放到/usr/lib/aarch64-linux-gnu/下再sudo ldconfig。这个过程中特别要注意,拷过去的是ARM版库,别不小心把主机上的X86库传上去,否则运行时要么报格式错误,要么跑着跑着段错误。
再就是Illegal instruction (core dumped)。这个报错出现说明程序二进制里包含TX2 CPU不支持的指令。通常是因为编译时在-march或-mcpu参数里指定了过高版本的ARM架构,比如你指定了-march=armv8.2-a,但TX2是ARMv8-A,不支持部分新指令,于是运行到那个指令时就崩了。最稳妥的做法是编译时别加这些激进参数,直接用默认的armv8-a,或者明确传-march=armv8-a确保下限。
关于默认的链接参数,我建议做一次实打实的检查。在编译hello的时候,用一个空看板看链接器实际执行了什么:
aarch64-linux-gnu-gcc --sysroot=$SYSROOT -v -o hello hello.c 2>&1 | grep -E "collect2|--sysroot"你会在输出里看到--sysroot=${HOME}/tx2_sysroot确实被传给了链接器。如果这个路径没出现,说明你的CFLAGS环境变量没有生效,或者被Makefile覆盖了。正式工程里可以用CMake的CMAKE_SYSROOT来管,但遇到哪里都查不到原因的问题时,回到这个基础验证点往往能快速定位。
最后再说一个容易踩的细节:不要把整个$SYSROOT目录在多个项目之间共享并随意写入。有些构建系统会往sysroot的/usr/lib目录里安装新编译出的库,一旦弄脏了,可能影响后续所有交叉编译项目。建议是每个正式项目独立维护一份sysroot快照,或者干脆保持sysroot只读,新装的库统一放到项目自己的lib目录里。
我个人的习惯是每次配置完环境,都会做一遍“同步 → 最小编译 → 上板运行”的完整闭环,确认全链路通畅再开始干正事。这能省掉后续大量时间。交叉编译环境这种东西,配的时候多点耐心,后面编写代码就开心多了。