交叉编译总结

本文是交叉编译入门及必要配置方法总结,目的为新手介绍如何进入交叉编译的世界,并附带两个重要列子:
第一个是使用cmake进行交叉编译
第二个是交叉编译Protobuf

交叉编译的目的是在一台架构A主机平台上编译另一种架构B目标平台的二进制文件或者库,交叉编译在目标系统平台(开发出来的应用程序序所运行的平台)难以或不容易编译时非常有用。主要体现在以下四个方面:

  • 性能: 目标平台比主机平台性能差,许多专用的嵌入式硬件被设计为低成本和低功耗,导致编译慢
  • 资源: 整个编译过程是非常消耗资源的,嵌入式系统往往没有足够的内存或磁盘空间
  • 初始状态: 即便当目标平台性能资源都充足时,第一个在目标平台上运行的本地编译器总需要通过交叉编译获得
  • 灵活兼容: 完整的Linux编译环境需要很多支持包,交叉编译使我们不需要花时间将各种支持包移植到目标板上

确定主机平台及目标平台

执行交叉编译最基本的就是明确自己主机平台和目标平台的架构。

  1. 主机平台
    主机平台一般都是日常使用的PC平台,为通用的Intel或者AMD的CPUx86架构,分32bit和64bit两种。当然也不排除使用其他平台架构作为主机平台。
  2. 目标平台
    目标平台就比较多,较为主流的有arm、mips、PowerPC等。每个平台下面都有各自的细分,主要还是分为32bit和64bit。
    以arm为例,arm平台的指令集也在不断更新,目前存在比较多的是armv7和armv8两个平台。armv7为32位。armv8兼容32位和64位,有aarch32和aarch64。
    mips平台在嵌入式领域也比较常见,龙芯就是采用mips架构进行开发的,为mips64。

获取交叉编译工具链

要执行交叉编译就需要有对应的交叉编译工具链,称之为工具链就是编译都是一整套工具,并不是单一的工具。比较熟知的就是gcc/g++编译器和ld连接器。

交叉编译工具链命名规则

通常交叉编译工具链命名规则为:arch-core-kernel-system。其中:

arch:目标平台架构,如上文提到的arm,mips等;
core:有两种种情况,第一是CPU Core,如Cortex A8;第二是指定工具链的供应商。如果没有特殊指定,则留空不填。这一组命名比较灵活,有以厂家名称命名的,有以开发者命名的,也有以开发板命名的,或者直接是none或cross的;
kernel: 目标平台的OS,见过的有linux,uclinux,bare-metal(无OS);
system:嵌入式应用二进制接口(Embedded Application Binary Interface),交叉编译工具链所选择的库函数和目标映像的规范,如gnu,gnueabi等。其中gnu等价于glibc+oabi;gnueabi等价于glibc+eabi。若不指定,则也可以留空不填;

上述命名规则并不是统一的规范,使用的时候作为参考就行。我使用的交叉编译工具链名称为:gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu。

获取交叉编译工具链两个途径:

  1. 直接下载知名厂家已经编译好的工具链。这些编译好的交叉编译工具链一般来说更加稳定,用的人多。
  1. 自己编译交叉编译工具链
    这个一般不推荐,但是在无法下载到对应版本的工具链或者工具链某些c++特性不能支持等情况就需要自己根据自己的需要进行编译。
    编译交叉编译工具链的工具:
  • crosstool-NG
  • Buildroot
  • Embedded Linux Development Kit (ELDK)

配置交叉编译环境

获取到对应的交叉编译工具链之后,需要在主机平台配置交叉编译环境。
首先解压交叉编译工具链。一般工具链的目录结构如下:

drwxr-xr-x 7   4096 Dec  4 19:53 aarch64-linux-gnu/
drwxr-xr-x 2   4096 Dec  4 19:55 bin/
-rw-r--r-- 1  11300 Dec  4 19:53 gcc-linaro-7.5.0-2019.12-linux-manifest.txt
drwxr-xr-x 3   4096 Dec  4 19:51 include/
drwxr-xr-x 3   4096 Dec  4 19:55 lib/
drwxr-xr-x 3   4096 Dec  4 19:37 libexec/
drwxr-xr-x 8   4096 Dec  4 19:51 share/

我们重点需要关注的是aarch64-linux-gnu和bin这两个目录

【aarch64-linux-gnu】

【aarch64-linux-gnu】可能不同的交叉编译工具链名称不同,主要是平台架构不同,其他名称可能为【arm-linux-gnueabihf】。根据交叉编译工具链的名称不同而不同。
这个目录下的文件结构如下:

drwxr-xr-x 2  4096 Dec  4 19:54 bin/
drwxr-xr-x 3  4096 Dec  4 19:48 include/
drwxr-xr-x 3  4096 Dec  4 19:33 lib/
drwxr-xr-x 3  4096 Dec  4 19:55 lib64/
drwxr-xr-x 7  4096 Dec  4 19:53 libc/

在【aarch64-linux-gnu/bin】目录下有部分交叉编译工具,但是不全。其他几个目录都是必要的依赖库和头文件。我们重点关注的是【aarch64-linux-gnu/libc】,以后编译的其他依赖都需要安装在这个目录下的usr目录。因为这个目录类似于Linux系统的根目录,文件结构如下:

drwxr-xr-x  2  4096 Dec  4 19:41 etc/
drwxr-xr-x  3  4096 Dec  4 19:55 lib/
drwxr-xr-x  2  4096 Dec  4 19:41 sbin/
drwxr-xr-x 10  4096 Dec 25 17:37 usr/
drwxr-xr-x  3  4096 Dec  4 19:41 var/

在【aarch64-linux-gnu/libc/usr】目录下有lib和include目录。在交叉编译其他依赖的时候,指定的安装目录【–prefix】一般指定到这个目录中。

【bin】目录

在交叉编译工具链文件夹根目录下的【bin】文件夹是全部的交叉编译工具。比如gcc、g++编译器。可以在这个文件夹下执行以下编译器二进制文件,查看下版本,看看交叉编译器是否可用。
我们需要做的是将这个文件夹路径添加到环境变量PATH中:

export PATH=$PATH:/path/to/your/crosscompile-toolchain/bin

开始交叉编译

首先我们以一个传统的例子为例:

  1. 第一步编写一个hello world程序。
#include <stdio.h>

void main()
{
    printf("Hello world!!\n");
}
  1. 我们就像使用主机平台gcc编译程序一样的过程编译hello world,这里以我的环境为例:
aarch64-linux-gnu-gcc helloworld.c -o helloworld

然后会生成一个可执行程序helloworld。你直接运行的话会报以下错误:

-bash: ./helloworld: cannot execute binary file: Exec format error

提示可执行程序格式错误。这是正常的。接着我们拷贝到目标平台上执行以下,检测是否正确输出“Hello world!!”。

这是基本的编译过程,稍微复杂点的也是一样,比如连接库文件的时候。只不过都需要使用交叉编译工具链来完成。
接下来我们讲两个复杂例子,第一个是使用cmake时的交叉编译方式,第二个是Protobuf的交叉编译。

使用cmake的交叉编译方式

在原项目使用的是cmake编译或者希望使用cmake进行交叉编译的时候。需要有几个变量进行定义。

set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)
set(CMAKE_SYSROOT /home/devel/rasp-pi-rootfs)

set(tools /home/devel/gcc-4.7-linaro-rpi-gnueabihf)
set(CMAKE_C_COMPILER ${tools}/bin/arm-linux-gnueabihf-gcc)
set(CMAKE_CXX_COMPILER ${tools}/bin/arm-linux-gnueabihf-g++)

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_SYSTEM_NAME】指定目标平台系统名称
【CMAKE_SYSTEM_PROCESSOR】指定目标平台CPU架构
【CMAKE_SYSROOT】这个很重要了,这个需要填写的是刚才交叉编译工具链根目录下的【aarch64-linux-gnu/libc】目录,这个目录当时介绍的时候说它类似主机平台Linux系统根目录,在这里指定的话,cmake交叉编译中会在这个目录下寻找安装的其他目标平台依赖程序。
【tools】【CMAKE_C_COMPILER】【CMAKE_CXX_COMPILER】这三个变量主要是为了指定交叉编译工具的gcc、g++位置。
【CMAKE_FIND_ROOT_PATH_MODE_PROGRAM】以及下面的【X_LIBRARY】【X_INCLUDE】【X_PACKAGE】三个变量都是指示CMake在执行find_X命令时的行为。有三个可选项,分别是NEVER,ONLY,BOTH。

  • NEVER表示不在你CMAKE_SYSROOT设置的目录下进行查找;
  • ONLY表示只在这个路径下查找;
  • BOTH表示先查找这个路径,再查找全局路径。

上面的变量有两种方式跟原先的CMakeLists结合:

  1. 直接将上述几行变量定义代码放到原先CMakeLists的最开始处。有一点需要注意的是一定是在【project()】命令之前
  2. 将上述几行变量定义代码放到单独的文件中,命名为XXX.cmake,然后执行cmake命令的时候使用【-DCMAKE_TOOLCHAIN_FILE=XXX.cmake】

:推荐第二种方式,但是第二种方式在进行交叉编译的时候,执行cmake容易出现无限循环检测,直到发送错误。

另外cmake在寻找依赖的时候一般去刚才指定的CMAKE_SYSROOT这个目录中找,通常是交叉编译工具链的【aarch64-linux-gnu/libc】目录。因此交叉编译的依赖最好都安装在这个目录中。

Protobuf的交叉编译方式

这里额外介绍Protobuf的原因是,在交叉编译二进制的时候有时候需要对应主机平台的二进制协助,而且最好是版本一致的。Protobuf就是这样的,还有libmysqlclient。
所以首先先编译一个主机平台的Protobuf,然后再开始目标平台的编译。如果主机平台的Protobuf之前就有就可以直接用(版本最好一致)。
目标平台的编译过程:

  1. Protobuf的压缩包里有一个【config.guess】工具,可以放到目标平台执行以下,会返回目标平台的架构、位数和系统。如:aarch64-linux-gnu
  2. 然后使用如下命令开始交叉编译,其中需要修改的是参数是【–host】【CC】【CXX】【–with-protoc】【–prefix】:
./configure --host=aarch64-linux-gnu CC=aarch64-linux-gnu-gcc CXX=aarch64-linux-gnu-g++ --with-protoc=/usr/bin/protoc --prefix=/path/to/your/toolchain/aarch64-linux-gnu/libc/usr

参数解释:
【–host】:就是用【config.guess】获取的;
【CC】【CXX】:交叉编译工具链gcc和g++名称;
【–with-protoc】:已经编译好的主机平台protoc二进制文件路径;
【–prefix】:交叉编译后安装目录。

总结

交叉编译的入门介绍就结束了。其实主要就几个步骤:

  1. 确定主机平台和目标平台;
  2. 获取对应交叉编译工具链;
  3. 开始交叉编译。

其实第三步还是比较复杂的,不同的软件各有不同,但是也是有一些共通的地方,就是:

  1. 传入交叉编译工具的名称,如gcc和g++,其他还可能需要ld等。
  2. 传入目标平台的架构、系统等详细信息,因为交叉编译的时候,软件本身无法去检测平台的特性,需要明确传入。
  3. 安装目录(可选)
    主要是这些参数的传入方式不同,需要具体用到的时候具体去查一下。

其他语言

上文整体上是介绍C/C++ 为主要方向的交叉编译过程。其实还有很多其他语言也是需要交叉编译的,比如go,rust。基本的理念是一致的,就是在编译的时候告知目标平台架构。
go语言的交叉编译比较简单,主要涉及GOOS和GOARCH两个参数,GOOS设置目标平台操作系统,GOARCH设置目标平台架构:

CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build

这里不展开讲了,大家遇到的话可以针对性的查。
以上就是全部内容了,觉得有用就点赞啊!!!