Skip to main content

一文搞懂交叉编译

1. 什么是交叉编译?

对于没有做过C/C++跨平台开发嵌入式开发的同学,可能不太理解交叉编译的概念,那么什么是交叉编译?它有什么作用?什么时候需要用到交叉编译呢?

要解释什么是交叉编译,我们要结合与交叉编译相对的另一个概念本地编译一起来讲解。

1.1. 本地编译

本地编译是指在当前编译平台下,编译出来的程序只能在该平台下运行。也就是说,编译器和目标程序运行在同一个平台上。

这里再理解一下什么是平台?我们在讲C/C++编译相关领域的概念时,平台是指硬件和操作系统的组合,这里的硬件更确切的说就是CPU架构,如:X86x86_64ARMARM64。因为操作系统可以运行在不同的CPU架构上,但不同的CPU架构采用的指令集是不一样的,如Linux x86_64的程序不能在Linux ARM64上运行。

1.2. 交叉编译

交叉编译可以理解为,在当前编译平台下,编译出来的程序能运行在体系结构不同的另一种目标平台上,但是编译平台本身却不能运行该程序。

比如,我们在x86_64平台的Linux系统上编写程序,并编译成能在ARM64平台的Linux系统上运行的程序,编译得到的程序在x86_64平台的Linux系统上是不能运行的,必须放到ARM64平台的Linux系统上才能正常运行。

1.3. 什么时候需要交叉编译?

正常我们要开发Windows的程序,在Windows上开发并编译就好了;要开发macOS的程序,就买一台苹果电脑,在苹果电脑上开发并编译就好了;这些本地编译就能满足需求了,那什么时候需要交叉编译呢?

我们从交叉编译的概念来猜想一下可能会是什么原因,难倒是“目标平台上不能直接编译出在目标平台上运行的程序”吗?

恭喜你,你猜对了! 之所以需要交叉编译,就是因为:有些目标平台,不能直接编译出能在目标平台上运行的程序。或者能够编译但特别麻烦,没有完整的编译工具链;又或者是目标平台硬件性能比较差,编译速度特别慢。

下面这些目标平台就经常需要用到交叉编译:

  • 嵌入式平台: 如无人机、扫地机器人、运动相机等等。嵌入式设备硬件通常是SoC芯片+Linux系统,或者MCU芯片+RTOS系统,SoC的CPU通常是ARM架构ARM64架构,内嵌的内存和磁盘资源都非常小,MCU的资源更是少的可怜。所以嵌入式的设备,从硬件性能上就很难支撑自己编译自己需要运行的程序。
  • 移动平台:Android手机iOS手机等,不管是Android系统还是iOS系统,底层都是一个Unix的内核,手机的芯片通常是ARM架构。硬件性能也比较有限,关键是没有完整的编译工具链,所以也需要交叉编译。

可以本地编译的平台

需要交叉编译的平台

  • iOS设备
  • Android设备
  • Harmony设备
  • Embedded设备,如树莓派、扫地机/机器人等的SoC系统。

X86_64的Linux系统上交叉编译ARM64平台的Linux程序为例,其编译流程如下:

2. 如何进行交叉编译?

X86_64的Ubuntu系统,编译ARM64平台的Linux程序。

2.1. 前期准备

2.1.1. 安装 adb

# 安装adb
sudo apt-get install android-tools-adb
# 验证adb是否安装成功
adb --version

2.1.2. 查看目标设备的CPU架构

  1. 手机(或目标设备)开启USB调试模式。
  2. 连接手机(或目标设备)到电脑。
  3. 打开命令行窗口,输入adb devices查看已经连接的设备。
adb devices
List of devices attached
PQY0221927018610 device
  1. 通过adb进入设备
# 设备列表只有单个设备时,通过以下命令进入设备
adb shell
# 设备列表有多个设备时,通过以下命令指定设备ID
adb -s <did> shell
  1. 执行以下命令查看目标设备的CPU架构。
uname -m
aarch64
# 或 (手机可以执行以下命令)
getprop ro.product.cpu.abi
arm64-v8a

我们可以看到ADB连接的手机,CPU架构为ARM64。

2.2. 使用 Ubuntu 官方交叉编译工具链

2.2.1. 下载交叉编译工具链

【举例】:

x86_64的Ubuntu系统下交叉编译aarch64架构下Ubuntu22.04执行的C++程序。

# 更新软件包列表
sudo apt update

# 安装交叉编译工具链
sudo apt install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu

# 安装必要的库文件(Ubuntu 22.04 aarch64)
sudo apt install libstdc++-12-dev-arm64-cross

2.2.2. 基础编译命令

# 编译简单的 C++ 程序
aarch64-linux-gnu-g++ -o program program.cpp

# 指定 C++ 标准
aarch64-linux-gnu-g++ -std=c++17 -o program program.cpp

# 静态链接
aarch64-linux-gnu-g++ -static -o program program.cpp

【示例】:交叉编译一下HelloWorld程序

// Test.cpp

#include <iostream>

int main()
{
std::cout << "Hello world!" << std::endl;
return 0;
}

本地机器上编译:

aarch64-linux-gnu-g++ -o Test Test.cpp

本地机器上运行:


./Test
aarch64-binfmt-P: Could not open '/lib/ld-linux-aarch64.so.1': No such file or directory

目标设备上运行:

./Test
Hello world!

2.3. 使用 CMake 进行交叉编译

2.3.1. 项目目录结构

./TestProject/
├── CMakeLists.txt
├── Test.cpp
└── toolchain-aarch64.cmake

Test.cpp:

同上面的HelloWorld程序。

toolchain-aarch64.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)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)

CMakeLists.txt:

# cmake version
cmake_minimum_required(VERSION 3.22.0)

# project name
project(TestProject VERSION 1.0.0.0 LANGUAGES CXX)

# add executable
add_executable(${PROJECT_NAME} Test.cpp)

2.3.2. 编译命令

# 配置项目
cmake -S ./ -B ./build -DCMAKE_TOOLCHAIN_FILE=./toolchain-aarch64.cmake
# 编译
cd build/
make

2.3.3. 运行程序

通过ADB或SCP推送到目标设备:

scp ./Test stark@ip:/home/stark/spencer

在目标设备上执行

./TestProject
Hello world!

2.4. 使用 Docker 容器进行交叉编译

2.4.1. 创建 Dockerfile

FROM ubuntu:22.04

RUN apt-get update && \
apt-get install -y \
gcc-aarch64-linux-gnu \
g++-aarch64-linux-gnu \
build-essential \
cmake \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /workspace

2.4.2. 使用 Docker 编译

# 构建 Docker 镜像
docker build -t aarch64-cross-compile .

# 运行容器并编译
docker run -v $(pwd):/workspace -it aarch64-cross-compile \
aarch64-linux-gnu-g++ -o program program.cpp

2.3.4. 比较构建产物

我们可以用file命令来查看编译的可执行文件的文件类型(架构信息)。

X86_64

file ./TestProject
./TestProject: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=53f5025de95889a86814213b7fbdc8d6cb18da63, for GNU/Linux 3.2.0, not stripped

ARM64

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

3. 什么是交叉编译工具链

软件的编译过程由一系列的步骤完成,每一个步骤都有一个对应的工具。这些工具紧密地工作在一起,前一个工具的输出是后一个工具的输入,像一根链条一样,我们称这些工具为工具链。

如Linux系统上的GCC编译工具链:Linux系统上,通常只需要使用gcc就可以完成整个编译过程。但不要被gcc的名字误导,事实上,gcc并不是一个编译器,而是一个驱动程序。在整个的编译过程中,gcc就像一个包工头一样,编译过程中的每一个环节由具体的工具负责。比如编译过程由cc1负责,汇编过程由as负责,链接过程由ld负责。

而用于交叉编译的一整套工具也被称之为交叉编译工具链。如:gcc-aarch64-linux-gnu + g++-aarch64-linux-gnu + cmake

4. 参考文档