第48章:交叉编译实战——以ARM Linux为例,配置交叉编译环境

说实话,交叉编译这个话题,很多刚入行的朋友一听就头大。我当年第一次接触时也懵——明明在电脑上编译得好好的,为什么非要搞个什么「交叉」?

嗯,原因其实很简单:你的开发机是x86架构,但目标设备是ARM架构。两种CPU的指令集完全不同,你总不能把x86的可执行文件直接丢到ARM板子上跑吧?

交叉编译,说白了就是:在一种架构上编译出另一种架构能跑的程序。今天我就带你走一遍完整的ARM交叉编译环境配置流程。

48.1 交叉编译的核心概念

先理清几个关键术语,不然后面容易绕晕。

术语 含义 我的理解
Host 你正在使用的开发机器 通常是x86_64的PC或服务器
Target 程序最终运行的设备 比如ARM Linux开发板
交叉工具链 一套能在Host上生成Target代码的工具 包括编译器、链接器、库等
Sysroot 目标系统的根文件系统镜像 里面放着目标板的头文件和库

我个人习惯把交叉工具链想象成「翻译官」——它把你的代码翻译成ARM能听懂的语言,但翻译过程是在你的x86电脑上完成的。

48.2 获取ARM交叉工具链

工欲善其事,必先利其器。第一步就是搞到一套可用的交叉编译器。

常用的方案有这么几种:

  • Linaro GCC:ARM官方推荐的版本,稳定可靠。我项目里一直用这个。
  • Buildroot / Yocto:自己从头构建工具链,适合深度定制。
  • 发行版自带:比如Ubuntu的gcc-arm-linux-gnueabihf包。

我个人建议初学者直接用Linaro的预编译版本,省心。下载后解压就行:

# 以ARM 32位硬浮点为例
wget https://releases.linaro.org/components/toolchain/binaries/latest-7/arm-linux-gnueabihf/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz
tar -xf gcc-linaro-*.tar.xz -C /opt/
export PATH=/opt/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin:$PATH

验证一下是否装好了:

arm-linux-gnueabihf-gcc --version
# 应该能看到版本信息,而不是"command not found"
注意: 我曾经遇到过PATH没加对,结果调用了系统自带的gcc,编译出来的东西在ARM板子上直接段错误。排查了半天才发现是编译器用错了。所以一定要确认你调用的确实是交叉编译器。

48.3 用CMake配置交叉编译

CMake本身不直接做交叉编译,它需要你提供一个工具链文件(toolchain file)。这个文件告诉CMake:编译器是谁、目标架构是什么、sysroot在哪里。

来看一个典型的工具链文件,我命名为arm-linux-gnueabihf.cmake

# 目标系统名称
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)

# 指定交叉编译器
set(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc)
set(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++)

# 指定sysroot路径(如果目标板有完整的根文件系统)
set(CMAKE_SYSROOT /path/to/your/sysroot)

# 搜索库和头文件的路径
set(CMAKE_FIND_ROOT_PATH /path/to/your/sysroot)

# 只在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)

然后编译时这样用:

mkdir build-arm && cd build-arm
cmake .. -DCMAKE_TOOLCHAIN_FILE=../arm-linux-gnueabihf.cmake
make

就这么简单。CMake会自动调用交叉编译器,链接ARM的库,生成ARM的可执行文件。

小技巧: 如果你没有完整的sysroot,可以先用CMAKE_SYSROOT指向一个空目录,然后手动指定库路径。但说实话,这样容易出问题。我建议还是从目标板上拷一份完整的根文件系统过来。

48.4 实战:编译一个Hello World

光说不练假把式。我们来写个最简单的程序验证一下。

先写main.cpp

#include <iostream>
int main() {
    std::cout << "Hello from ARM Linux!" << std::endl;
    return 0;
}

再写CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(HelloARM CXX)
add_executable(hello_arm main.cpp)

然后按上面说的方式编译。编译完成后,用file命令检查一下:

$ file hello_arm
hello_arm: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, for GNU/Linux 3.2.0, not stripped

看到「ARM」字样了吗?说明编译成功了。把这个文件拷到你的ARM板子上,直接运行就行。

48.5 处理第三方库的交叉编译

实际项目中很少只有一个源文件。你大概率会用到OpenCV、Boost、SQLite这些库。这时候交叉编译的复杂度就上来了。

我的经验是:所有第三方库都要用交叉编译器重新编译一遍。你不能直接拿x86上装好的库来链接,架构不对。

举个例子,交叉编译OpenCV时,CMake命令大概长这样:

cmake .. \
  -DCMAKE_TOOLCHAIN_FILE=../arm-linux-gnueabihf.cmake \
  -DCMAKE_INSTALL_PREFIX=/opt/arm-sysroot/usr/local \
  -DBUILD_opencv_world=ON \
  -DWITH_GTK=OFF \
  -DWITH_QT=OFF

这里我关掉了GTK和QT支持,因为目标板上可能没有这些图形库。你想想看,如果硬要链接一个不存在的库,编译能过才怪。

核心原则: 交叉编译时,所有依赖都必须来自同一个sysroot。混用不同来源的库,轻则链接失败,重则运行时崩溃。

48.6 常见坑与避坑指南

做交叉编译这几年,我踩过的坑真不少。挑几个典型的说说:

  • 动态链接库版本不匹配:我曾经在板子上运行程序时,报错GLIBC_2.28 not found。原因是编译用的sysroot里glibc版本比板子上的高。解决办法:用和板子相同版本的sysroot重新编译。
  • 硬浮点 vs 软浮点:ARM有硬浮点(hf)和软浮点(sf)两种ABI。编译器和库必须一致。用arm-linux-gnueabihf编译的程序,不能在纯软浮点的系统上跑。
  • CMake缓存污染:如果你先在本机编译了一次,然后切到交叉编译,一定要清空build目录。CMake会缓存之前的检测结果,导致交叉编译失败。

48.7 知识体系总览

下面这张图帮你理清交叉编译的完整流程:

交叉编译环境配置流程 开发机 (x86_64) 交叉工具链 目标板 Sysroot CMake 工具链文件 (toolchain.cmake) cmake -DCMAKE_TOOLCHAIN_FILE=... && make ARM 可执行文件 (ELF) scp / nfs 部署到目标板

从图上可以看得很清楚:开发机、工具链、sysroot三者缺一不可。CMake工具链文件就是连接这三者的桥梁。

48.8 写在最后

交叉编译其实没那么神秘。你只要记住三点:

  • 编译器要用对(交叉编译器,不是本机gcc)
  • 库要用对(来自目标板的sysroot)
  • ABI要匹配(硬浮点/软浮点、glibc版本等)

做到这三点,基本不会出大问题。剩下的就是多练、多踩坑、多总结。嗯,今天就到这里吧。


公众号:蓝海资料掘金营,微信 deep3321