使用 Clang 进行交叉编译

简介

本文档将指导您选择合适的 Clang 选项,以便将您的代码交叉编译到不同的架构。本文档假定您已经知道如何为主机架构编译代码,并且知道如何选择额外的包含和库路径。

但是,本文档不是“操作指南”,它不会帮助您设置构建系统或 Makefile,也不会帮助您选择合适的 CMake 选项等。此外,它没有涵盖所有可能的选项,也没有包含针对特定架构的具体示例。对于具体示例,交叉编译 LLVM 本身的说明 可能会有所帮助。

阅读完本文档后,您应该熟悉与交叉编译相关的关键问题,以及 Clang 提供的用于执行交叉编译的主要编译器选项。

交叉编译问题

在 GCC 世界中,每个主机/目标组合都有自己的二进制文件、头文件、库等集合。因此,通常只需下载包含所有文件的包,将其解压缩到一个目录,并将构建系统指向该编译器即可,该编译器将了解其位置,并在编译代码时找到所有需要的内容。

另一方面,Clang/LLVM 本质上是一个交叉编译器,这意味着一个程序集可以通过设置-target选项编译到所有目标。这使得希望编译到不同平台和架构的程序员,以及只需要维护一个构建系统的编译器开发人员,以及只需要一组主包的操作系统发行版更容易。

但是,正如对任何交叉编译器而言,并且考虑到不同架构、操作系统和选项的复杂性,找到头文件、库或 binutils 来生成特定于目标的代码并不总是那么容易。因此,您需要特殊的选项来帮助 Clang 了解您要编译到的目标、您的工具在哪里等等。

另一个问题是编译器只带有标准库(如compiler-rtlibcxxlibgcclibm 等),因此您必须找到并提供给构建系统,您软件构建所需的每个特定于目标的库。仅仅拥有主机库是不够的。

最后,并非所有工具链都相同,因此并非所有 Clang 选项都能神奇地发挥作用。某些选项,如--sysroot(它实际上更改了头文件和库的逻辑根目录),假设所有二进制文件和库都在同一目录中,而当您的交叉编译器通过发行版的包管理安装时,情况可能并非如此。因此,对于每种特定情况,您可能需要使用多个选项,并且在大多数情况下,您最终会手动设置包含路径(-I)和库路径(-L)。

总而言之,不同的工具链可以
  • 是特定于主机/目标的,或者更灵活

  • 位于单个目录中,或者分散在您的系统中

  • 默认情况下具有不同的库和头文件集

  • 需要特殊的选项,您的构建系统无法自行确定

Clang 中的一般交叉编译选项

目标三元组

基本选项是定义目标架构。为此,请使用-target <triple>。如果您没有指定目标,CPU 名称将不匹配(因为 Clang 假设主机三元组),并且编译将继续进行,为主机平台创建代码,这将在汇编或链接时导致错误。

三元组的一般格式为<arch><sub>-<vendor>-<sys>-<env>,其中
  • arch = x86_64i386armthumbmips 等。

  • sub = 例如,在 ARM 上:v5v6mv7av7m 等。

  • vendor = pcapplenvidiaibm 等。

  • sys = nonelinuxwin32darwincuda 等。

  • env = eabignuandroidmachoelf 等。

当然,子架构选项可用于其各自的架构,因此“x86v7a”没有意义。只有在存在相关更改时才需要指定供应商,例如 PC 和 Apple 之间。大多数情况下,它可以省略(并且将假定为 Unknown),这将为指定的架构设置默认值。系统名称通常是操作系统(linux、darwin),但可能是特殊的,例如裸机“none”。

当参数不重要时,可以省略它,或者您可以选择unknown,并将使用默认值。如果您选择 Clang 不认识的参数,例如blerg,它将忽略该参数并假定为unknown,这并不总是理想的,因此请小心。

最后,env(环境)选项将选择默认的 CPU/FPU、定义代码的特定行为(PCS、扩展),以及选择正确的库调用等等。

CPU、FPU、ABI

指定了目标后,就是时候选择要编译到的硬件了。对于每种架构,都将选择一组默认的 CPU/FPU/ABI,因此您几乎总是需要通过标志更改它。

典型的标志包括
  • -mcpu=<cpu-name>,例如 x86-64、swift、cortex-a15

  • -mfpu=<fpu-name>,例如 SSE3、NEON,控制可用的 FP 单位

  • -mfloat-abi=<fabi>,例如 soft、hard,控制使用哪些寄存器进行浮点运算

默认值通常是公分母,因此 Clang 不会生成会导致错误的代码。但这同时也意味着您不会获得针对特定硬件的最佳代码,这意味着它可能比预期慢几个数量级。

例如,如果您的目标是arm-none-eabi,默认的 CPU 将是使用软浮点的arm7tdmi,这在现代内核上非常慢,而如果您的三元组是armv7a-none-eabi,它将是使用 NEON 的 Cortex-A8,但仍然使用软浮点,这要好得多,但仍然不是很好。

工具链选项

有三个主要选项用于控制对您的交叉编译器的访问:--sysroot-I-L。最后两个选项众所周知,但它们对于特定于目标的额外库和头文件尤其重要。

拥有交叉编译器主要有两种方式

  1. 当您将交叉编译器从 zip 文件解压缩到一个目录中时,您必须使用--sysroot=<path>。该路径是您解压缩文件所在的根目录,Clang 将在其中查找目录binlibinclude

    在这种情况下,您的设置应该已经完成(如果不需要额外的头文件或库),因为 Clang 将在其中找到所有需要的二进制文件(汇编器、链接器等)。

  2. 当您通过包管理器安装(现代 Linux 发行版提供了交叉编译器包)时,请确保您设置的目标三元组也是您的交叉编译器工具链的前缀。

    在这种情况下,Clang 将找到其他二进制文件(汇编器、链接器),但并不总是找到目标头文件和库所在的位置。人们经常向 Clang 添加特定于系统的线索,但随着事物不断变化,更有可能找不到,而不是找到。

    因此,在这里,如果您手动指定包含/库目录(通过-I-L),会更安全。

特定于目标的库

作为构建的一部分编译的所有库都将交叉编译到您的目标,并且您的构建系统可能会在正确的位置找到它们。但是,所有通常针对其进行检查的依赖项(如libxmllibz 等)都将与主机平台匹配,而不是目标。

因此,如果构建系统不知道您要交叉编译代码,它将错误地获取所有依赖项,并且您的编译将在构建时失败,而不是配置时失败。

此外,为您的目标查找库并不像为您的主机机器那样容易。没有多少交叉库作为包提供给大多数操作系统,因此您要么从源代码交叉编译它们,要么下载针对您的目标平台的包,解压缩库和头文件,将它们放在特定目录中,并添加指向它们的-I-L

此外,某些库在不同目标平台上具有不同的依赖关系,因此用于在主机上查找依赖关系的配置工具可能会错误地获取目标平台的依赖关系列表。这意味着,当设置自己的库路径时,构建的配置可能会出现错误,您需要通过其他标志(configure、Make、CMake 等)进行补充。

多库

当您想交叉编译到多个配置,例如硬浮点 ARM 和软浮点 ARM 时,您需要有多个库副本和(可能)头文件。

一些 Linux 发行版支持多库,它可以更轻松地为您处理这些问题。但如果您不小心,例如忘记指定 -ccc-gcc-name armv7l-linux-gnueabihf-gcc(使用硬浮点),Clang 将选择 armv7l-linux-gnueabi-ld(使用软浮点),并会发生链接器错误。

如果您针对不同的环境进行编译,例如 gnueabiandroideabi,也会出现这种情况。编译和运行可能会成功,但会产生运行时错误,这些错误更难追踪和修复。