工具链是嵌入式 Linux 的第一个元素,也是项目的起点。 您将使用它来编译将在您的设备上运行的所有代码。 你在这个早期阶段所做的选择将对最终结果产生深远的影响。 通过使用处理器的最佳指令集,您的工具链应该能够有效地利用您的硬件。 它应该支持您需要的语言,并可靠地实现了可移植操作系统接口(POSIX)和其他系统接口。
您的工具链应该在整个项目中保持不变。 换句话说,一旦您选择了工具链,坚持使用它是很重要的。 在项目期间以不一致的方式更改编译器和开发库将导致细微的错误。 也就是说,当发现安全缺陷或 bug 时,最好还是更新您的工具链。
获得工具链可以像下载并安装 tar 文件一样简单,也可以像从源代码构建整个工具链一样复杂。 在本章中,我将在名为Crossstool-NG的工具的帮助下采用后一种方法,这样我就可以向您展示创建工具链的细节。 稍后,在第 6 章,选择构建系统中,我将切换到使用构建系统生成的工具链,这是获得工具链的更常用方法。 当我们读到第 14 章,从 BusyBox Runit开始,我们将通过下载预构建的 Linaro 工具链与 Buildroot 一起使用来节省一些时间。
在本章中,我们将介绍以下主题:
- 工具链简介
- 查找工具链
- 使用 Crossstool-NG 工具构建工具链
- 工具链的解剖
- 与库链接-静态和动态链接
- 交叉编译的艺术
要按照示例操作,请确保您具备以下条件:
- 基于 Linux 的主机系统,具有
autoconf
、automake
、bison
、bzip2
、cmake
、flex
、g++
、gawk
、gcc
、gettext
、git
、gperf
、help2man
、libncurses5-dev
、libstdc++6
、libtool
、libtool-bin
、make
、patch
、python3-dev
、。rsync
、texinfo
、unzip
、wget
和xz-utils
或其等价物已安装。
我推荐使用 Ubuntu20.04LTS 或更高版本,因为在撰写本文时,本章中的练习都是针对该 Linux 发行版进行测试的。 下面是在 Ubuntu 20.04 LTS 上安装所有必需软件包的命令:
$ sudo apt-get install autoconf automake bison bzip2 cmake \ flex g++ gawk gcc
gettext git gperf help2man libncurses5-dev libstdc++6 libtool \ libtool-bin make
patch python3-dev rsync texinfo unzip wget xz-utils
本章的所有代码都可以在本书的 giHub 存储库的Chapter02
文件夹中找到:https://github.com/PacktPublishing/Mastering-Embedded-Linux-Programming-Third-Edition
工具链是将源代码编译成可在目标设备上运行的可执行文件的一组工具,包括编译器、链接器和运行时库。 最初,您需要一个组件来构建嵌入式 Linux 系统的其他三个元素:引导加载程序、内核和根文件系统。 它必须能够编译用汇编语言、C 和 C++编写的代码,因为这些都是基本开放源码包中使用的语言。
通常,LINUX 的工具链基于 GNU 项目的组件 (http://www.gnu.org),在撰写本文时的大多数情况下,这一点仍然适用于。 然而,在过去的几年里,Clang编译器和相关的低级虚拟机(LLVM)项目(GNU)已经发展到了可以替代 http://llvm.org 工具链的地步。 LLVM 和基于 GNU 的工具链之间的一个主要区别是许可;LLVM 拥有 BSD 许可证,而 GNU 拥有 GPL。
Clang 也有一些的技术优势,比如更快的编译速度和更好的诊断能力,但 GNU GCC 的优势是与现有的代码库兼容,并支持广泛的架构和操作系统。 虽然花了几年时间才做到这一点,但 Clang 现在可以编译嵌入式 Linux 所需的所有组件,是 GNU 的可行替代品。 要了解更多信息,请参阅https://www.kernel.org/doc/html/latest/kbuild/llvm.html。
对于如何在https://clang.llvm.org/docs/CrossCompilation.html使用 clang 进行交叉编译有很好的描述。 如果您想将其作为嵌入式 Linux 构建系统的一部分使用,EmbToolkit(Clang)完全支持 GNU 和 LLVM/https://embtoolkit.org 工具链,很多人正在使用 Clang with Buildroot 和 Yocto 项目。 我将在第 6 章,选择构建系统中介绍嵌入式构建系统。 同时,本章重点介绍 GNU 工具链,因为它仍然是 Linux 最流行和最成熟的工具链。
标准 GNU 工具链由三个主要组件组成:
- Binutils:一组二进制实用程序,包括汇编器和链接器。 它在http://gnu.org/software/binutils上提供。
- GNU 编译器集合(GCC):这些是针对 C 和其他语言的编译器,具体取决于 GCC 的版本,包括 C++、Objective-C、Objective-C++、JAVA、FORTRAN、Ada 和 Go。 它们都使用一个共同的后端来生成汇编器代码,并将这些代码提供给 GNU 汇编器。 可在http://gcc.gnu.org/购买。
- C 库:基于 POSIX 规范的标准化应用编程接口(API),它是应用的操作系统内核 的主要接口。 正如我们将在本章后面看到的那样,有几个 C 库需要考虑。
除此之外,您还需要一份 Linux 内核头的副本,其中包含直接访问内核时所需的定义和常量。 现在,您需要它们能够编译 C 库,但稍后在编写程序或编译与特定 Linux 设备交互的库(例如,通过 Linux 帧缓冲区驱动程序显示图形)时也需要它们。 这不仅仅是在内核源代码的include
目录中复制头文件的问题。 这些头文件仅在内核中使用,并且包含的定义如果在其原始状态下用于编译常规的 Linux 应用,将会导致冲突。
相反,您将需要生成一组经过清理的内核标头,我已经在第 5 章,构建根文件系统中进行了说明。
内核头部是否从您将要使用的 Linux 的确切版本生成通常并不重要。 因为内核接口总是向后兼容的,所以只需要标头来自与您在目标上使用的内核相同或更早的内核。
大多数人认为GNU 调试器(gdb)也是工具链的一部分,通常在这一点上构建它。 我将在第 19 章,使用 GDB调试中讨论 GDB。
既然我们已经讨论了内核头,并了解了工具链的组件是什么,那么让我们来看看不同类型的工具链。
出于我们的目的,有两种类型的工具链:
- 原生:此工具链在与其生成的程序相同类型的系统(有时是相同的实际系统)上运行。 这是台式机和服务器的常见情况,并且在某些类型的嵌入式设备上变得流行起来。 例如,运行 Debian for ARM 的 Raspberry PI 拥有自托管的本机编译器。
- Cross:此工具链在与目标系统不同的系统类型上运行,允许在速度较快的台式 PC 上完成开发,然后加载到嵌入式目标上进行测试。
几乎所有嵌入式 Linux 开发都是使用交叉开发工具链完成的,部分原因是大多数嵌入式设备不太适合程序开发,因为它们缺乏计算能力、内存和存储,但也因为它将主机环境和目标环境分开。 例如,当主机和目标使用相同的体系结构x86_64
时,后一点尤其重要。 在这种情况下,很容易在主机上进行本机编译,然后简单地将二进制文件复制到目标。
这在一定程度上是可行的,但很可能主机发行版接收更新的频率比目标版本更高,或者为目标构建代码的不同工程师的主机开发库版本略有不同。 随着时间的推移,开发系统和目标系统将会分道扬镳,您将违反工具链应该在整个项目生命周期中保持不变的原则。 如果确保主机和目标生成环境彼此步调一致,则可以使用此方法。 然而,一种更好的方法是将主机和目标分开,而跨工具链是实现这一点的方法。
然而,也有一种相反的观点支持本土化发展。 交叉开发带来了交叉编译目标所需的所有库和工具的负担。 我们将在后面标题为交叉编译的艺术一节中看到,交叉开发并不总是简单的,因为许多开放源码包不是以这种方式设计的。 集成的构建工具(包括 Buildroot 和 Yocto Project)通过封装规则来帮助交叉编译您在典型嵌入式系统中需要的一系列包,但是如果您想编译大量额外的包,则最好在本地编译它们。 例如,使用交叉编译器为 Raspberry PI 或 Beaglebone 构建 Debian 发行版将非常困难。 相反,它们是本地编译的。
从头开始创建本地构建环境并非易事。 首先,您仍然需要一个交叉编译器来在目标上创建本地构建环境,然后使用该环境来构建包。 然后,为了在合理的时间内执行本机构建,您需要一个配置良好的目标板的构建群,或者您可以使用Quick Emulator(QEMU)来模拟目标。
同时,在本章中,我将重点介绍一个更主流的交叉编译环境,它相对容易设置和管理。 我们将首先了解一个目标 CPU 体系结构与另一个目标 CPU 体系结构的不同之处。
工具链必须根据目标 CPU 的能力构建,包括以下内容:
- CPU 体系结构:ARM、无互锁流水线级的微处理器(MIPS)、x86_64 等。
- 大端或小端操作:某些 CPU 可以同时在两种模式下运行,但每种模式的机器代码都不同。
- 浮点支持:并非所有版本的嵌入式处理器都实现硬件浮点单元,在这种情况下,工具链必须配置为调用软件浮点库。
- 应用二进制接口(ABI):用于在函数调用之间传递参数的调用约定。
对于许多架构,ABI 在处理器系列中是恒定的。 一个值得注意的例外是 ARM。 ARM 体系结构在本世纪头十年末过渡到扩展应用二进制接口(EABI),导致以前的 ABI 被命名为旧应用二进制接口(OABI)。 虽然 OABI 现在已经过时,但您将继续看到对 EABI 的引用。 从那时起,根据传递浮点参数的方式,EABI 被一分为二。
最初的 EABI 使用通用(整数)寄存器,而较新的扩展应用二进制接口 Hard-Float(EABIHF)使用浮点寄存器。 EABIHF 的浮点运算速度要快得多,因为它消除了在整数和浮点寄存器之间进行复制的需要,但它与没有浮点单元的 CPU 不兼容。 因此,需要在两个不兼容的 ABI 之间做出选择;您不能混合匹配这两个,因此您必须在这个阶段做出决定。
GNU 使用前缀来表示工具链中每个工具的名称,以标识可以生成的各种组合。 它由由短划线分隔的三个或四个组件组成的元组组成,如下所述:
- CPU:这是 CPU 架构,例如 ARM、MIPS 或 x86_64。 如果 CPU 同时具有两种字符顺序模式,则可以通过为小端字符顺序添加
el
或为大端字符顺序添加eb
来区分它们。 很好的例子是 Little-endian MIPSmipsel
和 Big-endian armarmeb
。 - 供应商:这标识工具链的提供商。 示例包括
buildroot
、poky
或仅unknown
。 有时它被完全省略了。 - 内核:出于我们的目的,它始终是
linux
。 - 操作系统:用户空间组件的名称,可以是
gnu
或musl
。 ABI 也可以附加在这里,因此对于 ARM 工具链,您可能会看到gnueabi
、gnueabihf
、musleabi
或musleabihf
。
您可以通过使用gcc
的-dumpmachine
选项找到构建工具链时使用的元组。 例如,您可能会在主机上看到以下内容:
$ gcc -dumpmachine
x86_64-linux-gnu
这个元组表示 CPU 为x86_64
,内核为linux
,用户空间为gnu
。
重要音符
当本机编译器安装在机器上时,通常会创建指向工具链中每个工具的无前缀链接,以便您可以使用gcc
命令调用 C 编译器。
以下是使用交叉编译器的示例:
$ mipsel-unknown-linux-gnu-gcc -dumpmachine
mipsel-unknown-linux-gnu
这个元组表示小端 MIPS 的 CPU、unknown
供应商、linux
的内核和gnu
的用户空间。
Unix 操作系统的编程接口是用 C 语言定义的,现在由 POSIX 标准定义。 C 库是该接口的实现;它是 Linux 程序的内核网关,如下图所示。 即使您使用另一种语言(可能是 Java 或 Python)编写程序,相应的运行时支持库最终也必须调用 C 库,如下所示:
图 2.1-C 库
当 C 库需要内核的服务时,它会使用内核系统调用接口在用户空间和内核空间之间进行转换。 可以通过直接调用内核系统来绕过 C 库,但这很麻烦,而且几乎从不需要。
有几个 C 库可供选择。 主要选项如下:
- glibc:这是标准的 GNUC 库,可从https://gnu.org/software/libc获得。 它很大,而且直到最近还不太容易配置,但它是 POSIX API 最完整的实现。 许可证为 LGPL 2.1。
- MUSL libc:这是在https://musl.libc.org提供的。
musl libc
库相对较新,但是作为 GNUlibc
的一个符合标准的小型替代方案,它已经获得了很多关注。 对于 RAM 和存储容量有限的系统来说,它是一个很好的选择。 它有麻省理工学院的执照。 - uClibc-ng:这是在https://uclibc-ng.org提供的。
u
实际上是一个希腊语mu
字符,表示这是微控制器 C 库。 它最初是为与 uClinux(用于没有内存管理单元的 CPU 的 Linux)配合使用而开发的,但后来已被修改为与完整的 Linux 配合使用。uClibc-ng
库是最初的uClibc
项目(https://uclibc.org)的分支,不幸的是该项目年久失修。 这两个版本都获得了 LGPL 2.1 的许可。 - eglibc:这是在http://www.eglibc.org/home提供的。 现在已经过时了,
eglibc
是glibc
的分支,经过修改使其更适合嵌入式使用。 其中,eglibc
增加了配置选项和对glibc
未涵盖的体系结构的支持,特别是 PowerPC E500 CPU 核心。 在版本 2.20 中,eglibc
中的代码库被重新合并到glibc
中。 不再维护eglibc
库。
那么,该选哪一个呢? 我的建议是,只有在使用 uClinux 的情况下才使用uClibc-ng
。 如果您的存储或 RAM 非常有限,则musl libc
是一个很好的选择,否则,请使用glibc
,如此流程图所示:
图 2.2-选择 C 库
您对 C 库的选择可能会限制您对工具链的选择,因为并非所有预先构建的工具链都支持所有 C 库。
对于您的交叉开发工具链,您有三个选择:您可以找到一个符合您需求的现成工具链;您可以使用由嵌入式构建工具生成的工具链,这将在第 6 章,选择构建系统中介绍;或者您也可以按照本章后面的描述自己创建一个工具链。
预先构建的跨工具链是一个很有吸引力的选择,因为您只需下载和安装它,但您仅限于该特定工具链的配置,并且您依赖于您从其获得它的个人或组织。
最有可能的是,它将是其中之一:
- SoC 或电路板供应商。 大多数供应商都提供 Linux 工具链。
- 致力于为给定体系结构提供系统级支持的联盟。 例如,Linaro(https://www.linaro.org)为 ARM 架构预建了工具链。
- 第三方 Linux 工具供应商,如 Mentor Graphics、Timesys 或 MontaVista。
- 桌面 Linux 发行版的跨工具包。 例如,基于 Debian 的发行版有用于交叉编译 ARM、MIPS 和 PowerPC 目标的包。
- 由集成的嵌入式构建工具之一生成的二进制 SDK。 Yocto 项目在 http://downloads.yoctoproject.org/releases/yocto/yocto-[version]/toolchain.上有一些例子
- 一个你再也找不到的论坛链接。
在所有这些情况下,您都必须决定提供的预构建工具链是否符合您的要求。 它是否使用您喜欢的 C 库? 提供商是否会为您提供安全修复和错误的更新,请记住我对第 1 章、从开始的支持和更新的评论。 如果你对其中任何一个的回答都是否定的,那么你应该考虑创建你自己的。
不幸的是,构建工具链并非易事。 如果您真的想自己做整个事情,请看一看Cross Linux 从头开始(https://trac.clfs.org)。 在那里,您可以找到有关如何创建每个组件的分步说明。
一种更简单的替代方法是使用 Crossstool-NG,它将流程封装到一组脚本中,并且有一个菜单驱动的前端。 不过,你仍然需要一定程度的知识,才能做出正确的选择。
使用 Buildroot 或 Yocto Project 这样的构建系统更简单,因为它们会在构建过程中生成工具链。 这是我首选的解决方案,如我在第 6 章,选择构建系统中所示。
随着 Crossstool-NG 的优势,构建您自己的工具链肯定是一个有效且可行的选择。 接下来让我们看看如何做到这一点。
几年前,DanKegel 编写了一组用于生成交叉开发工具链的脚本和生成文件,并将其称为 Crossstool(http://kegel.com/crosstool/)。 2007 年,扬恩·E·莫林(Yann E.Morin)利用这个基地创造了下一代交叉凳-NG(https://crosstool-ng.github.io)。 今天,它是目前为止从源代码创建独立交叉工具链的最方便的方式。
在本节中,我们将使用 Crossstool-NG 为 Beaglebone Black 和 QEMU 构建工具链。
在可以从源代码构建 Crossstool-NG 之前,您首先需要在主机上安装本地工具链和一些构建工具。 有关 Crossstool-NG 的构建和运行时依赖项的完整列表,请参阅本章开头的技术要求一节。
接下来,从 Crossstool-NG Git 存储库获取当前版本。 在我的示例中,我使用了版本1.24.0
。 将其解压并创建前端菜单系统ct-ng
,如以下命令所示:
$ git clone https://github.com/crosstool-ng/crosstool-ng.git
$ cd crosstool-ng
$ git checkout crosstool-ng-1.24.0
$ ./bootstrap
$ ./configure --prefix=${PWD}
$ make
$ make install
--prefix=${PWD}
选项意味着程序将安装到当前目录中,这样就不需要 root 权限,如果要将其安装在默认位置/usr/local/share
,则需要 root 权限。
我们现在有了个 Crossstool-NG 的工作安装,我们可以用它来构建交叉工具链。 键入bin/ct-ng
以启动交叉架菜单。
Crosstool-NG 可以构建许多不同的工具链组合。 为了简化初始配置,它附带了一组涵盖许多常见用例的示例。 使用bin/ct-ng list-samples
生成列表。
Beaglebone Black 拥有 TI AM335x SoC,内含 ARM Cortex A8 内核和 VFPv3 浮点单元。 由于 Beaglebone Black 有足够的 RAM 和存储空间,我们可以使用glibc
作为 C 库。 最近的样本是arm-cortex_a8-linux-gnueabi
。
您可以通过在名称前加上show-
前缀来查看默认配置,如
所示:
$ bin/ct-ng show-arm-cortex_a8-linux-gnueabi
[G...] arm-cortex_a8-linux-gnueabi
Languages : C,C++
OS : linux-4.20.8
Binutils : binutils-2.32
Compiler : gcc-8.3.0
C library : glibc-2.29
Debug tools : duma-2_5_15 gdb-8.2.1 ltrace-0.7.3 strace-4.26
Companion libs : expat-2.2.6 gettext-0.19.8.1 gmp-6.1.2 isl-0.20 libelf-0.8.13 libiconv-1.15 mpc-1.1.0 mpfr-4.0.2 ncurses-6.1 zlib-1.2.11
Companion tools :
这与我们的要求非常匹配,只是它使用了eabi
二进制接口,该接口在整数寄存器中传递浮点参数。 为此,我们更喜欢使用硬件浮点寄存器,因为它会加速具有float
和double
参数类型的函数调用。 您可以稍后更改配置,因此目前您应该选择此目标配置:
$ bin/ct-ng arm-cortex_a8-linux-gnueabi
此时,您可以使用 Configuration(配置)菜单命令menuconfig
查看配置并进行更改:
$ bin/ct-ng menuconfig
菜单系统基于 Linux 内核menuconfig
,因此任何配置过内核的人都会熟悉用户界面的导航。 如果没有,请参考第 4 章,配置和构建内核,了解menuconfig
的说明。
在这一点上,我建议您进行三项配置更改:
- 在路径和 Misc****选项中,禁用将工具链渲染为只读
(
CT_PREFIX_DIR_RO
)。 - 在目标选项|浮点中,选择硬件(FPU)(
CT_ARCH_FLOAT_HW
)。 - 在目标选项中,为输入
neon
作为使用特定 FPU**。**
如果您想在安装工具链之后将库添加到工具链中,那么第一个是必要的,稍后我将在与库的链接一节中对此进行描述。 第二个选择eabihf
二进制接口,原因如前所述。 第三个是成功构建 Linux 内核所必需的。 括号中的名称是存储在配置文件中的配置标签。 进行更改后,退出menuconfig
菜单并保存配置。
现在,您可以通过键入以下命令,使用 Crossstool-NG 根据您的规范获取、配置和构建组件:
$ bin/ct-ng build
构建过程大约需要半个小时,之后您会发现您的工具链出现在~/x-tools/arm-cortex_a8-linux-gnueabihf
中。
接下来,让我们构建一个针对 QEMU 的工具链。
在 QEMU 目标上,您将模拟一个 ARM 通用 PB 评估板,它具有 ARM926EJ-S 处理器内核,可实现 ARMv5TE 指令集。 您需要生成一个与规范匹配的 Crossstool-NG 工具链。 该过程与 Beaglebone Black 的过程非常相似。
您可以从运行bin/ct-ng list-samples
开始,找到可以使用的良好基本配置。 没有完全匹配,因此请使用通用目标arm-unknown-linux-gnueabi
。 如图所示选择它,首先运行distclean
以确保没有上一次构建留下的工件:
$ bin/ct-ng distclean
$ bin/ct-ng arm-unknown-linux-gnueabi
与 Beaglebone Black 一样,您可以使用配置菜单命令bin/ct-ng menuconfig
查看配置并进行更改
。 只有一项改变是必要的:
- 在路径和其他选项中,禁用将工具链渲染为只读
(
CT_PREFIX_DIR_RO
)。
现在,使用如下所示的命令构建工具链:
$ bin/ct-ng build
和以前一样,建造将需要大约半个小时。 工具链将安装在~/x-tools/arm-unknown-linux-gnueabi
中。
您将需要一个工作交叉工具链来完成下一节中的练习。
为了了解典型工具链中的是什么,我想检查一下您刚刚创建的 Crossstool-NG 工具链。 这些示例使用为 Beaglebone Black 创建的 ARM Cortex A8 工具链,前缀为arm-cortex_a8-linux-gnueabihf-
。 如果您为 QEMU 目标构建了 ARM926EJ-S 工具链,则前缀将改为arm-unknown-linux-gnueabi
。
ARM Cortex A8 工具链位于目录~/x-tools/arm-cortex_a8-linux-gnueabihf/bin
中。 在那里,您将找到交叉编译器arm-cortex_a8-linux-gnueabihf-gcc
。 要使用它,您需要使用以下命令将该目录添加到您的路径中:
$ PATH=~/x-tools/arm-cortex_a8-linux-gnueabihf/bin:$PATH
现在您可以使用一个简单的helloworld
程序,它在 C 语言中如下所示:
#include <stdio.h>
#include <stdlib.h>
int main (int argc, char *argv[])
{
printf ("Hello, world!\n");
return 0;
}
您可以这样编译它:
$ arm-cortex_a8-linux-gnueabihf-gcc helloworld.c -o helloworld
您可以通过使用file
命令打印文件类型来确认它已经交叉编译:
$ file helloworld
helloworld: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, for GNU/Linux 4.20.8, with debug_info, not stripped
既然您已经验证了您的交叉编译器可以工作,那么让我们来仔细看看。
假设您刚刚收到了一个工具链,并且您想要更多地了解它是如何配置的。 您可以通过查询gcc
找到很多信息。 例如,要查找版本,可以使用--version
:
$ arm-cortex_a8-linux-gnueabihf-gcc --version
arm-cortex_a8-linux-gnueabihf-gcc (crosstool-NG 1.24.0) 8.3.0
Copyright (C) 2018 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
要了解其配置方式,请使用-v
:
$ arm-cortex_a8-linux-gnueabihf-gcc -v
Using built-in specs.
COLLECT_GCC=arm-cortex_a8-linux-gnueabihf-gcc
COLLECT_LTO_WRAPPER=/home/frank/x-tools/arm-cortex_a8-linux-gnueabihf/libexec/gcc/arm-cortex_a8-linux-gnueabihf/8.3.0/lto-wrapper
Target: arm-cortex_a8-linux-gnueabihf
Configured with: /home/frank/crosstool-ng/.build/arm-cortex_a8-linux-gnueabihf/src/gcc/configure --build=x86_64-build_pc-linux-gnu --host=x86_64-build_pc-linux-gnu --target=arm-cortex_a8-linux-gnueabihf --prefix=/home/frank/x-tools/arm-cortex_a8-linux-gnueabihf --with-sysroot=/home/frank/x-tools/arm-cortex_a8-linux-gnueabihf/arm-cortex_a8-linux-gnueabihf/sysroot --enable-languages=c,c++ --with-cpu=cortex-a8 --with-float=hard --with-pkgversion='crosstool-NG 1.24.0' --enable-__cxa_atexit --disable-libmudflap --disable-libgomp --disable-libssp --disable-libquadmath --disable-libquadmath-support --disable-libsanitizer --disable-libmpx --with-gmp=/home/frank/crosstool-ng/.build/arm-cortex_a8-linux-gnueabihf/buildtools --with-mpfr=/home/frank/crosstool-ng/.build/arm-cortex_a8-linux-gnueabihf/buildtools --with-mpc=/home/frank/crosstool-ng/.build/arm-cortex_a8-linux-gnueabihf/buildtools --with-isl=/home/frank/crosstool-ng/.build/arm-cortex_a8-linux-gnueabihf/buildtools --enable-lto --with-host-libstdcxx='-static-libgcc -Wl,-Bstatic,-lstdc++,-Bdynamic -lm' --enable-threads=posix --enable-target-optspace --enable-plugin --enable-gold --disable-nls --disable-multilib --with-local-prefix=/home/frank/x-tools/arm-cortex_a8-linux-gnueabihf/arm-cortex_a8-linux-gnueabihf/sysroot --enable-long-long
Thread model: posix
gcc version 8.3.0 (crosstool-NG 1.24.0)
这里有很多输出,但需要注意的有趣事情如下:
--with-sysroot=/home/frank/x-tools/arm-cortex_a8-linux-gnueabihf/arm-cortex_a8-linux-gnueabihf/sysroot
:这是默认的sysroot
目录,说明见下一节。--enable-languages=c,c++
:使用它,我们同时启用了 C 和 C++ 语言。--with-cpu=cortex-a8
:代码是为 ARM Cortex A8 内核生成的。--with-float=hard
:为浮点单元生成操作码,并使用 VFP 寄存器作为参数。--enable-threads=posix
:这将启用 POSIX 线程。
这些是编译器的默认设置。 您可以在gcc
命令行上覆盖它们中的大多数。 例如,如果要针对不同的 CPU 进行编译,可以通过将-mcpu
添加到命令行来覆盖配置的设置--with-cpu
,如下所示:
$ arm-cortex_a8-linux-gnueabihf-gcc -mcpu=cortex-a5 \ helloworld.c \
-o helloworld
您可以使用--target-help
打印出可用的体系结构特定选项范围,如下所示:
$ arm-cortex_a8-linux-gnueabihf-gcc --target-help
您可能想知道在这一点上获得正确的配置是否重要,因为您总是可以按如下所示进行更改。 答案取决于你预期使用它的方式。 如果您计划为每个目标创建一个新的工具链,那么在开始时设置所有内容是有意义的,因为这将降低以后出错的风险。 跳到第 6 章,选择构建系统,我称之为 Buildroot 哲学。 另一方面,如果您想要构建一个通用的工具链,并且您准备在为特定目标构建时提供正确的设置,那么您应该使基本工具链成为通用的,这就是 Yocto 项目处理事情的方式。 前面的示例遵循 Buildroot 的理念。
工具链sysroot
是一个目录,其中包含库、头文件和其他配置文件的子目录。 它可以在通过--with-sysroot=
配置工具链时设置,也可以使用--sysroot=
在命令行上设置。 您可以使用-print-sysroot
查看默认sysroot
的位置:
$ arm-cortex_a8-linux-gnueabihf-gcc -print-sysroot
/home/frank/x-tools/arm-cortex_a8-linux-gnueabihf/arm-cortex_a8-linux-gnueabihf/sysroot
您将在sysroot
中找到以下子目录:
lib
:包含 C 库和动态链接器/加载器的共享对象ld-linux
usr/lib
:C 库的静态库存档文件,以及后续可能安装的任何其他库usr/include
:包含所有库的标头usr/bin
:包含在目标系统上运行的实用程序,如ldd
命令usr/share
:用于本地化和国际化的sbin
:提供ldconfig
实用程序,用于优化库加载路径
显然,在开发主机上需要其中的一些来编译程序,而其他的,例如共享库和ld-linux
,在运行时需要在目标上。
下面是用于调用 GNU 工具链的各种其他组件的命令列表,并附有简要说明:
addr2line
:通过读取可执行文件中的调试符号表,将程序地址转换为文件名和编号。 在对系统崩溃报告中打印的地址进行解码时,它非常有用。ar
:存档实用程序用于创建静态库。as
:这是 GNU 汇编器。c++filt
:这是用来拆分 C++和 Java 符号的。cpp
:这是 C 预处理器,用于扩展#define
、#include
和其他类似指令。 您很少需要单独使用它。elfedit
:用于更新 ELF 文件的 ELF 头。g++
:这是 GNU C++前端,它假设源文件包含 C++代码。gcc
:这是 GNU C 前端,它假设源文件包含 C 代码。gcov
:这是一个代码覆盖工具。gdb
:这是 GNU 调试器。gprof
:这是一个程序分析工具。ld
:这是 GNU 链接器。nm
:列出目标文件中的符号。objcopy
:用于复制和翻译目标文件。objdump
:用于显示目标文件中的信息。ranlib
:这将在静态库中创建或修改索引,从而使链接阶段更快。readelf
:它以 ELF 对象格式显示有关文件的信息。size
:列出部分大小和总大小。strings
:这将显示文件中的可打印字符串。strip
:这用于从调试符号表中剥离目标文件,从而使其更小。 通常,您会剥离放到目标上的所有可执行代码。
现在我们将从命令行工具切换到 C 库的主题。
C 库不是单个库文件。 它由四个主要部分组成,它们共同实现 POSIX API:
libc
:包含众所周知的 POSIX 函数(如printf
、open
、close
、read
、write
等)的主 C 库libm
:包含数学函数,如cos
、exp
和log
libpthread
:包含名称以 以pthread_
开头的所有 POSIX 线程函数librt
:具有 POSIX 的实时扩展,包括共享内存和异步 I/O
第一个参数libc
始终链接在一起,但其他参数必须与-l
选项显式链接。 -l
的参数是去掉了lib
的库名。 例如,通过调用sin()
计算正弦函数的程序将使用-lm
与libm
链接:
$ arm-cortex_a8-linux-gnueabihf-gcc myprog.c -o myprog -lm
您可以使用readelf
命令验证此程序或任何其他程序中链接了哪些库:
$ arm-cortex_a8-linux-gnueabihf-readelf -a myprog | grep "Shared library"
0x00000001 (NEEDED) Shared library: [libm.so.6]
0x00000001 (NEEDED) Shared library: [libc.so.6]
共享库需要一个运行时链接器,您可以使用以下命令公开该链接器:
$ arm-cortex_a8-linux-gnueabihf-readelf -a myprog | grep "program interpreter"
[Requesting program interpreter: /lib/ld-linux-armhf.so.3]
这非常有用,我有一个名为list-libs
的脚本文件,您可以在MELP/list-libs
的图书代码归档中找到它。 它包含以下命令:
#!/bin/sh
${CROSS_COMPILE}readelf -a $1 | grep "program interpreter"
${CROSS_COMPILE}readelf -a $1 | grep "Shared library"
除了 C library 的四个组件之外,我们还可以链接到其他库文件。我们将在下一节中研究如何做到这一点。
您为 Linux 编写的任何应用,无论是用 C 还是 C++编写的,都将与libc
C 库链接。 这是非常基本的,您甚至不需要告诉gcc
或g++
就可以这么做,因为它总是链接到libc
。 您可能想要链接的其他库必须通过-l
选项显式地命名为。
库代码可以通过两种不同的方式链接:静态的,意味着应用调用的所有库函数及其依赖项都从库存档中提取并绑定到可执行文件中;动态的,意味着对库文件和这些文件中的函数的引用是在代码中生成的,但实际的链接是在运行时动态完成的。 您可以在MELP/Chapter02/library
中的图书代码归档中找到以下示例的代码。
我们将从静态链接开始。
静态链接在少数情况下很有用。 例如,如果您正在构建一个仅由 BusyBox 和一些脚本文件组成的小型系统,静态链接 BusyBox 会更简单,并且不必复制运行时库文件和链接器。 它也会更小,因为您只链接了应用使用的代码,而不是提供整个 C 库。 如果需要在存放运行时库的文件系统可用之前运行程序,则静态链接也很有用。
您可以通过将-static
添加到命令行来静态链接所有库:
$ arm-cortex_a8-linux-gnueabihf-gcc -static helloworld.c -o helloworld-static
您会注意到,二进制文件的大小会急剧增加:
$ ls -l
total 4060
-rwxrwxr-x 1 frank frank 11816 Oct 23 15:45 helloworld
-rw-rw-r-- 1 frank frank 123 Oct 23 15:35 helloworld.c
-rwxrwxr-x 1 frank frank 4140860 Oct 23 16:00 helloworld-static
静态链接从通常名为lib[name].a
的库归档中提取代码。 在前面的例子中,它是libc.a
,它在[sysroot]/usr/lib
中:
$ export SYSROOT=$(arm-cortex_a8-linux-gnueabihf-gcc -print-sysroot)
$ cd $SYSROOT
$ ls -l usr/lib/libc.a
-rw-r--r-- 1 frank frank 31871066 Oct 23 15:16 usr/lib/libc.a
注意,语法导出SYSROOT=$(arm-cortex_a8-linux-gnueabihf-gcc -print-sysroot)
将指向sysroot
的路径放在 shell 变量SYSROOT
中,这使得示例更加清晰。
创建静态库就像使用ar
命令创建目标文件存档一样简单。 如果我有两个名为test1.c
和test2.c
的源文件,并且我想创建一个名为libtest.a
的静态库,那么我将执行以下操作:
$ arm-cortex_a8-linux-gnueabihf-gcc -c test1.c
$ arm-cortex_a8-linux-gnueabihf-gcc -c test2.c
$ arm-cortex_a8-linux-gnueabihf-ar rc libtest.a test1.o test2.o
$ ls -l
total 24
-rw-rw-r-- 1 frank frank 2392 Oct 9 09:28 libtest.a
-rw-rw-r-- 1 frank frank 116 Oct 9 09:26 test1.c
-rw-rw-r-- 1 frank frank 1080 Oct 9 09:27 test1.o
-rw-rw-r-- 1 frank frank 121 Oct 9 09:26 test2.c
-rw-rw-r-- 1 frank frank 1088 Oct 9 09:27 test2.o
然后,我可以使用以下命令将libtest
链接到我的helloworld
程序:
$ arm-cortex_a8-linux-gnueabihf-gcc helloworld.c -ltest \
-L../libs -I../libs -o helloworld
现在,让我们使用动态链接重新构建相同的程序。
部署库的一种更常见的方法是作为在运行时链接的共享对象,这可以更有效地利用存储和系统内存,因为只需要加载代码的一个副本。 它还可以方便地更新库文件,而无需重新链接所有使用它们的程序。
共享库的目标代码必须与位置无关,以便运行时链接器可以在内存中的下一个空闲地址自由定位它。 为此,请将-fPIC
参数添加到gcc
,然后使用-shared
选项将其链接:
$ arm-cortex_a8-linux-gnueabihf-gcc -fPIC -c test1.c
$ arm-cortex_a8-linux-gnueabihf-gcc -fPIC -c test2.c
$ arm-cortex_a8-linux-gnueabihf-gcc -shared -o libtest.so test1.o test2.o
这将创建共享库libtest.so
。 要将应用链接到该库,需要添加-ltest
,这与上一节提到的静态情况完全相同,但这一次代码不包括在可执行文件中。 相反,有一个对运行时链接器必须解析的库的引用:
$ arm-cortex_a8-linux-gnueabihf-gcc helloworld.c -ltest \
-L../libs -I../libs -o helloworld
$ MELP/list-libs helloworld
[Requesting program interpreter: /lib/ld-linux-armhf.so.3]
0x00000001 (NEEDED) Shared library: [libtest.so.6]
0x00000001 (NEEDED) Shared library: [libc.so.6]
该程序的运行时链接器是/lib/ld-linux-armhf.so.3
,它必须存在于目标的文件系统中。 链接器将在默认搜索路径/lib
和/usr/lib
中查找libtest.so
。 如果您还希望它在其他目录中查找库,您可以在LD_LIBRARY_PATH
shell 变量中放置一个冒号分隔的路径列表:
$ export LD_LIBRARY_PATH=/opt/lib:/opt/usr/lib
因为共享库与它们链接的可执行文件是分开的,所以我们在部署它们时需要知道它们的版本。
共享库的好处之一是它们可以独立于使用它们的程序进行更新。
库更新有两种类型:
- 以向后兼容的方式修复错误或添加新功能的那些
- 那些破坏与现有应用兼容性的问题
GNU/Linux 有一个版本控制方案来处理这两种情况。
每个库都有一个发布版本和一个端口号。 发布版本只是一个附加到库名后面的字符串;例如,JPEG 图像库libjpeg
当前的版本是8.2.2
,因此该库被命名为libjpeg.so.8.2.2
。 有一个名为libjpeg.so
的符号链接指向libjpeg.so.8.2.2
,因此当您使用-ljpeg
编译程序时,您将链接到当前版本。 如果安装版本8.2.3
,链接将更新,您将改为与该版本链接。
现在假设版本9.0.0
出现,它打破了向后兼容性。 现在,从libjpeg.so
到libjpeg.so.9.0.0
的链接指向libjpeg.so.9.0.0
,因此任何新程序都会链接到新版本,这可能会在到libjpeg
的接口更改时抛出编译错误,开发人员可以修复这些错误。
目标上任何未重新编译的程序都会以某种方式失败,因为它们仍在使用旧接口。 这就是称为soname的对象提供帮助的地方。 Soname 对构建库时的接口编号进行编码,并由运行时链接器在加载库时使用。 它的格式为<library name>.so.<interface number>
。 对于libjpeg.so.8.2.2
,soname 是libjpeg.so.8
,因为构建libjpeg
共享库时的 e 端口号是8
:
$ readelf -a /usr/lib/x86_64-linux-gnu/libjpeg.so.8.2.2 \
| grep SONAME
0x000000000000000e (SONAME) Library soname: [libjpeg.so.8]
用它编译的任何程序都将在运行时请求libjpeg.so.8
,这将是目标系统上指向libjpeg.so.8.2.2
的符号链接。 安装libjpeg
的版本9.0.0
时,其名称将为libjpeg.so.9
,因此可能会在同一系统上安装同一个库的两个不兼容版本。 与libjpeg.so.8.*.*
链接的程序将加载libjpeg.so.8
,与libjpeg.so.9.*.*
链接的程序将加载libjpeg.so.9
。
这就是为什么,当您查看/usr/lib/x86_64-linux-gnu/libjpeg*
的目录列表时,您会发现以下四个文件:
libjpeg.a
:这是用于静态链接的库存档。libjpeg.so -> libjpeg.so.8.2.2
:这是一个符号链接,用于动态链接。libjpeg.so.8 -> libjpeg.so.8.2.2
:这是一个符号链接,在运行时加载库时使用。libjpeg.so.8.2.2
:这是实际的共享库,在编译时和运行时都使用。
前两个只需要在构建时在主机上使用,后两个在运行时需要在目标上使用。
虽然您可以直接从命令行调用各种 GNU 交叉编译工具,但此技术不会超出helloworld
这样的玩具示例。 要真正有效地进行交叉编译,我们需要将交叉工具链与构建系统结合起来。
拥有一个有效的交叉工具链是旅程的起点,而不是终点。 在某些情况下,您可能希望开始交叉编译目标上所需的各种工具、应用和库。 其中许多将是开放源码的包,每个包都有自己的编译方法和自己的特点。
有一些常见的构建系统,包括:
- 纯 Make 文件,其中工具链通常由
make
变量CROSS_COMPILE
控制 - GNU 构建系统称为,即AutoTools
- CMake(https://cmake.org)
构建一个基本的嵌入式 Linux 系统都需要 Autotools 和 Make 文件。 CMake 是跨平台的,多年来得到了越来越多的采用,特别是在 C++社区中。 在本节中,我们将介绍所有三种构建工具。
一些重要的包非常容易交叉编译,包括 Linux 内核、U-Boot 引导加载程序和 BusyBox。 对于其中的每一个,您只需要将工具链前缀放在make
变量CROSS_COMPILE
中,例如,arm-cortex_a8-linux-gnueabi-
。 请注意尾部的破折号-
。
因此,要编译 BusyBox,您需要键入以下内容:
$ make CROSS_COMPILE=arm-cortex_a8-linux-gnueabihf-
或者,您可以将其设置为 shell 变量:
$ export CROSS_COMPILE=arm-cortex_a8-linux-gnueabihf-
$ make
对于 U-Boot 和 Linux,您还必须将make
变量ARCH
设置为它们支持的机器体系结构之一,我将在第 3 章、All About Bootloaders和第 4 章、配置和构建内核中介绍这一点。
AutoTools 和 CMake 都可以生成生成文件。 AutoTools 只生成 Make 文件,而 CMake 支持其他构建项目的方式,这取决于我们针对的是哪种 平台(在我们的例子中,严格地说是 Linux)。 让我们先看一下使用 Autotools 交叉编译 。
名称 Autotools 指的是在许多开源项目中用作构建系统的组工具。 这些组件以及个相应的项目页面如下所示:
- GNU Autoconf(https://www.gnu.org/soft/autoconf/autoconf.html)
- GNU Automake(https://www.gnu.org/savannah-checkouts/gnu/automake/)
- GNUhttps://www.gnu.org/software/libtool/libtool.html(LIBTool)
- Gnulib(https:/www.gnu.org/ Software/gnulib/
AutoTools 的作用是消除可能编译软件包的不同类型系统之间的差异,包括不同版本的编译器、不同版本的库、不同位置的头文件以及与其他软件包的依赖关系。
使用 AutoTools 的包附带一个名为configure
的脚本,该脚本检查依赖项并根据找到的内容生成 makefile。 configure
脚本还可以让您启用或禁用某些功能。 您可以通过运行./configure --help
找到提供的选项。
要为本机操作系统配置、构建和安装软件包,通常需要运行以下三个命令:
$ ./configure
$ make
$ sudo make install
AutoTools 也能够处理交叉开发。 您可以通过设置以下 shell 变量来影响已配置脚本的行为:
-
CC
:C 编译器命令。 -
CFLAGS
:附加的 C 编译器标志。 -
CXX
:C++编译器命令。 -
CXXFLAGS
:附加的 C++编译器标志。 -
LDFLAGS
:附加链接器标志;例如,如果非标准目录<lib dir>
中有库,则可以通过添加-L<lib dir>.
将其添加到库搜索路径 -
LIBS
:包含要传递给链接器的其他库的列表;例如,数学库的列表为-lm
。 -
CPPFLAGS
:包含 C/C++预处理器标志;例如,可以添加-I<include dir>
以在非标准目录<include dir>.
中搜索标头 -
CPP
:要使用的 C 预处理器。
有时仅设置CC
变量就足够了,如下所示:
$ CC=arm-cortex_a8-linux-gnueabihf-gcc ./configure
在其他时候,这将导致如下错误:
[…]
checking for suffix of executables...
checking whether we are cross compiling... configure: error: in '/home/frank/sqlite-autoconf-3330000':
configure: error: cannot run C compiled programs.
If you meant to cross compile, use '--host'.
See 'config.log' for more details
失败的原因是configure
经常试图通过编译代码片段并运行它们来发现工具链的功能,以查看会发生什么,如果程序已经交叉编译,这将无法工作。
重要音符
交叉编译时将--host=<host>
传递给configure
,以便configure
在您的系统中搜索针对指定<host>
平台的交叉编译工具链。 这样,configure
就不会在配置步骤中尝试运行非本机代码片段。
AutoTools 了解编译软件包时可能涉及的三种不同类型的计算机:
- Build:构建包的计算机,默认为当前计算机。
- 主机:将在其上运行程序的计算机。 对于本机编译,此字段留空,默认为与内部版本相同的计算机。 在交叉编译时,将其设置为工具链的元组。
- Target:程序将为其生成代码的计算机。 您可以在构建交叉编译器时设置此设置。
因此,要交叉编译,只需覆盖宿主,如下所示:
$ CC=arm-cortex_a8-linux-gnueabihf-gcc \
./configure --host=arm-cortex_a8-linux-gnueabihf
最后需要注意的是,默认安装目录是<sysroot>/usr/local/*
。
您通常会将其安装在<sysroot>/usr/*
中,这样头文件和库就会从它们的默认位置拾取。
配置典型自动工具包的完整命令如下所示:
$ CC=arm-cortex_a8-linux-gnueabihf-gcc \
./configure --host=arm-cortex_a8-linux-gnueabihf --prefix=/usr
让我们更深入地研究 Autotools,并使用它交叉编译一个流行的库。
SQLite 库实现了一个简单的关系数据库,在嵌入式设备上非常流行。 您可以从获取一份 SQLite 开始:
$ wget http://www.sqlite.org/2020/sqlite-autoconf-3330000.tar.gz
$ tar xf sqlite-autoconf-3330000.tar.gz
$ cd sqlite-autoconf-3330000
接下来,运行configure
脚本:
$ CC=arm-cortex_a8-linux-gnueabihf-gcc \
./configure --host=arm-cortex_a8-linux-gnueabihf --prefix=/usr
这似乎奏效了! 如果失败,将有错误信息打印到终端并记录在config.log
中。 请注意,已经创建了几个 makefile,因此现在您可以构建它:
$ make
最后,通过设置make
变量DESTDIR
将其安装到工具链目录中。 如果不这样做,它将尝试将其安装到主机计算机的/usr
目录中,而这不是您想要的:
$ make DESTDIR=$(arm-cortex_a8-linux-gnueabihf-gcc -print-sysroot) install
您可能会发现最后一个命令失败,并出现文件权限错误。 Crossstool-NG 工具链默认是只读的,这就是为什么在构建它时将CT_PREFIX_DIR_RO
设置为y
很有用。 另一个常见问题是工具链安装在系统目录中,如/opt
或/usr/local
。 在这种情况下,您在运行安装时需要root
权限。
安装后,您会发现个文件已添加到您的工具链中:
<sysroot>/usr/bin: sqlite3
:这是 SQLite 的命令行界面,您可以在目标系统上安装和运行它。<sysroot>/usr/lib
:libsqlite3.so.0.8.6
、libsqlite3.so.0
、libsqlite3.so
、libsqlite3.la
、libsqlite3.a
:这些是共享的 和静态库。<sysroot>/usr/lib/pkgconfig: sqlite3.pc
:这是包配置文件,如下节所述。<sysroot>/usr/lib/include: sqlite3.h
、sqlite3ext.h
:这些是头文件。<sysroot>/usr/share/man/man1: sqlite3.1
:这是手册页。
现在,您可以通过在
链接阶段添加-lsqlite3
来编译使用sqlite3
的程序:
$ arm-cortex_a8-linux-gnueabihf-gcc -lsqlite3 sqlite-test.c -o sqlite-test
这里,sqlite-test.c
是一个假想的调用 SQLite 函数的程序。 由于sqlite3
已安装到sysroot
中,因此编译器将毫不费力地找到头文件和库文件。 如果它们安装在其他地方,则必须添加-L<lib dir>
和-I<include dir>
。
当然,还会有运行时依赖关系,您必须按照第 5 章,构建 根文件系统中所述,将适当的文件安装到目标目录中 。
为了交叉编译库或包,它的依赖项首先需要进行
交叉编译。 Autotools 依赖于名为pkg-config
的实用程序来收集有关 Autotools 交叉编译的包的重要信息。
跟踪包依赖关系相当复杂。 Package 配置实用程序pkg-config
(https://www.freedesktop.org/wiki/Software/pkg-config/)通过在[sysroot]/usr/lib/pkgconfig
中保存 AutoTools 软件包的数据库,帮助跟踪安装了哪些软件包以及每个软件包所需的编译标志。 例如,SQLite3 的名称为sqlite3.pc
,包含需要使用它的其他包所需的基本信息:
$ cat $(arm-cortex_a8-linux-gnueabihf-gcc -print-sysroot)/usr/lib/pkgconfig/sqlite3.pc
# Package Information for pkg-config
prefix=/usr
exec_prefix=${prefix}
libdir=${exec_prefix}/lib
includedir=${prefix}/include
Name: SQLite
Description: SQL database engine
Version: 3.33.0
Libs: -L${libdir} -lsqlite3
Libs.private: -lm -ldl -lpthread
Cflags: -I${includedir}
您可以使用pkg-config
以可以直接馈送到gcc
的形式提取信息。 对于像libsqlite3
这样的库,您需要知道库名(--libs
)和任何特殊的 C 标志(--cflags
):
$ pkg-config sqlite3 --libs --cflags
Package sqlite3 was not found in the pkg-config search path.
Perhaps you should add the directory containing 'sqlite3.pc'
to the PKG_CONFIG_PATH environment variable
No package 'sqlite3' found
哎呀! 该操作失败,因为它正在查找主机的sysroot
,而主机上尚未安装libsqlite3
的开发包。 您需要通过设置PKG_CONFIG_LIBDIR
外壳变量将其指向目标工具链的sysroot
:
$ export PKG_CONFIG_LIBDIR=$(arm-cortex_a8-linux-gnueabihf-gcc \
-print-sysroot)/usr/lib/pkgconfig
$ pkg-config sqlite3 --libs --cflags
-lsqlite3
现在输出为-lsqlite3
。 在这种情况下,您已经知道了这一点,但通常您不会知道,所以这是一项有价值的技术。 要编译的最终命令如下
:
$ export PKG_CONFIG_LIBDIR=$(arm-cortex_a8-linux-gnueabihf-gcc \
-print-sysroot)/usr/lib/pkgconfig
$ arm-cortex_a8-linux-gnueabihf-gcc $(pkg-config sqlite3 --cflags --libs) \
sqlite-test.c -o sqlite-test
许多配置脚本读取pkg-config
生成的信息。 这可能会在交叉编译时导致错误,我们将在下面看到这一点。
sqlite3
是一个行为良好的包,可以很好地交叉编译,但并不是所有的包都是相同的。 典型的痛点包括以下:
- 为库(如
zlib
)自行开发的构建系统,这些库具有与上一节中描述的 AutoToolsconfigure
不同的configure
脚本 - 配置从主机读取
pkg-config
信息、标头和其他文件的脚本,而不考虑--host
覆盖 - 坚持尝试运行交叉编译代码的脚本
每种情况都需要仔细分析错误和configure
脚本的附加参数以提供正确的信息,或者对代码进行补丁以完全避免问题。 请记住,一个包可能有许多依赖项,特别是具有使用 GTK 或 Qt 的图形界面的程序,或者处理多媒体内容的程序。 例如,mplayer
是一种流行的多媒体内容播放工具,它依赖于 100 多个库。 要把它们全部建造起来,需要几周的时间。
因此,我不建议以这种方式为目标手动交叉编译组件,除非别无选择或要构建的包数量很少。 更好的方法是使用 Buildroot 或 Yocto Project 等构建工具,或者通过为目标体系结构设置本地构建环境来完全避免这个问题。 现在您可以明白为什么像 Debian 这样的发行版总是本地编译的。
CMake 更像是一个元构建系统,因为它依赖底层平台的本地工具来构建软件。 在 Windows 上,CMake 可以为 Microsoft Visual Studio 生成项目文件,在 MacOS 上,它可以为 Xcode 生成项目文件。 集成每个主要平台的主要 IDE 并非易事,这解释了 CMake 作为领先的跨平台构建系统解决方案的成功。 CMake 还可以在 Linux 上运行,它可以与您选择的交叉编译工具链结合使用。
要为本机 Linux 操作系统配置、构建和安装软件包,请运行以下命令:
$ cmake .
$ make
$ sudo make install
在 Linux 上,本机构建工具是 GNUmake
,因此 CMake 默认生成 makefile 供我们构建。 通常,我们希望执行源代码外的构建,以便目标文件和其他构建构件与源文件保持分离。
要在名为_build
的子目录中配置源代码外构建,请运行以下命令
:
$ mkdir _build
$ cd _build
$ cmake ..
这将在CMakeLists.txt
所在的项目目录内的_build
子目录中生成 makefile。 CMakeLists.txt
文件相当于基于 AutoTools 的项目的configure
脚本的 CMake。
然后,我们可以在_build
目录内构建源代码外的项目,并像前面一样安装软件包:
$ make
$ sudo make install
CMake 使用绝对路径,因此一旦生成了 makefile,就不能复制或移动_build
子目录,否则后续的make
步骤可能会失败。 请注意,CMake 默认将包安装到系统目录(如/usr/bin
)中,即使是在源代码外的构建中也是如此。
要生成 makefile 以便make
在_build
子目录中安装程序包,请将前面的cmake
命令替换为以下命令:
$ cmake .. -D CMAKE_INSTALL_PREFIX=../_build
我们不再需要在make install
前面加上sudo
,因为我们不需要提升权限就可以将程序包文件复制到_build
目录中。
类似地,我们可以使用另一个 CMake 命令行选项来生成用于交叉编译的生成文件:
$ cmake .. -D CMAKE_C_COMPILER="/usr/local/share/x-tools/arm-cortex_a8-linux-gnueabihf-gcc"
但是,使用 CMake 进行交叉编译的最佳实践是创建一个工具链文件,该文件除了设置针对嵌入式 Linux 的其他相关变量外,还设置CMAKE_C_COMPILER
和CMAKE_CXX_COMPILER
。
当我们以模块化的方式设计软件时,CMake 通过在库和组件之间强制实施 定义良好的 API 边界来工作得最好。
以下是在 CMake 中反复出现的一些关键术语:
target
:软件组件,如库或可执行文件。properties
:包括构建目标所需的源文件、编译器选项和链接库。package
:一个配置外部构建目标的 CMake 文件,就像它是在您的CMakeLists.txt
本身中定义的一样。
例如,如果我们有一个名为dummy
的基于 CMake 的可执行文件,它需要依赖于 SQLite,我们可以定义以下CMakeLists.txt
:
cmake_minimum_required (VERSION 3.0)
project (Dummy)
add_executable(dummy dummy.c)
find_package (SQLite3)
target_include_directories(dummy PRIVATE ${SQLITE3_INCLUDE_DIRS})
target_link_libraries (dummy PRIVATE ${SQLITE3_LIBRARIES})
find_package
命令搜索包(在本例中为SQLite3
)并将其导入,以便可以将外部目标作为依赖项添加到dummy
可执行文件的target_link_libraries
列表中以进行链接。
CMake 为流行的 C 和 C++包(包括 OpenSSL、Boost 和 Protobuf)提供了大量的查找器,使本机开发比单纯使用 makefile 更有效率。
PRIVATE
限定符可防止标头和标志等详细信息泄漏到dummy
目标之外。 当构建的目标是库而不是可执行文件时,使用PRIVATE
更有意义。 将目标视为模块,并在使用 CMake 定义您自己的目标时,尝试最小化其暴露的表面积。 只有在绝对必要时才使用PUBLIC
限定符,并对仅包含标题的库使用INTERFACE
限定符。
将您的应用建模为目标之间有边的依赖图。 该图不仅应该包括您的应用直接链接到的库,还应该包括任何可传递的依赖项。 为获得最佳效果,请删除图表中看到的所有循环或其他不必要的独立项。 通常,最好在开始编码之前执行此练习。 一个小小的计划就可以决定一个干净、易于维护的CMakeLists.txt
和一个没人愿意碰的难以捉摸的烂摊子之间的区别。
工具链始终是您的起点;接下来的一切都依赖于拥有一个工作可靠的工具链。
您可以从一个工具链开始-可能使用 Crossstool-NG 构建或从 Linaro 下载-并使用它编译目标上需要的所有包。 或者,您可以使用 Buildroot 或 Yocto Project 等构建系统,将工具链作为从源代码生成的发行版的一部分获得。 当心作为硬件包的一部分免费提供给您的工具链或发行版;它们通常配置不佳且没有维护。
一旦拥有了工具链,您就可以使用它来构建嵌入式 Linux 系统的其他组件。 在下一章中,您将了解引导加载程序,它使您的设备栩栩如生,并开始引导过程。 我们将使用在本章中构建的工具链为 Beaglebone Black 构建一个有效的引导加载器。
以下是几个视频,它们捕捉了跨工具链的最新技术,并在撰写本文时构建了系统:
- 2020 年工具链和交叉编译器的新视角,作者:Bernhard“Bero”Rosenkränzer:https://www.youtube.com/watch?v=BHaXqXzAs0Y
- 模块化设计的现代 CMake,Mathieu Ropert: https://www.youtube.com/watch?v=eC9-iRN2b04