掌握嵌入式Linux编程2工具链
2工具链
工具链是嵌入式Linux的第一个元素,也是你项目的起点。你将用它来编译所有将在你的设备上运行的代码。你在这个早期阶段做出的选择将对最终结果产生深远的影响。你的工具链应该能够通过使用处理器的最佳指令集来有效地利用你的硬件。它应该支持你所需要的语言,并对便携式操作系统接口(POSIX)和其他系统接口有一个坚实的实现。
你的工具链应该在整个项目中保持不变。换句话说,一旦你选择了你的工具链,就一定要坚持下去。在一个项目中以不一致的方式改变编译器和开发库会导致微妙的错误。也就是说,当发现安全缺陷或错误时,最好还是要更新你的工具链。
获得工具链可以像下载和安装TAR文件一样简单,也可以像从源代码构建整个东西一样复杂。在本章中,我采取了后一种方法,在crosstool-NG的工具的帮助下,我可以向你展示创建工具链的细节。稍后,在第6章 "选择构建系统 "中,我将转向使用构建系统生成的工具链,这是更常用的获得工具链的方法。当我们读到第14章 "从BusyBox runit开始 "时,我们将下载一个预先构建好的Linaro工具链来和Buildroot一起使用,这样可以节省一些时间。
技术要求
基于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, and xz-utils或其对应物。
我推荐使用Ubuntu 20.04 LTS或更高版本,因为本章的练习在写作时都是在该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
本章的所有代码都可以在本书GitHub仓库的Chapter02文件夹中找到:https://github.com/PacktPublishing/Mastering-Embedded-Linux-Programming-Third-Edition。
工具链的介绍
工具链是一套将源代码编译成可执行文件的工具,可以在你的目标设备上运行,包括编译器、链接器和运行时库。最初,你需要工具链来构建嵌入式Linux系统的其他三个要素:引导程序、内核和根文件系统。它必须能够编译用汇编、C和C++编写的代码,因为这些都是基础开源包中使用的语言。
通常情况下,Linux的工具链是基于GNU项目的组件
(http://www.gnu.org),在写这篇文章时,大多数情况下仍是如此。然而,在过去的几年中,Clang编译器和相关的低级虚拟机(LLVM)项目(http://llvm.org)已经发展到了现在可以替代GNU工具链的程度。LLVM和基于GNU的工具链之间的主要区别是许可;LLVM用BSD许可,而GNU用GPL。
Clang也有一些技术上的优势,比如更快的编译和更好的诊断,但GNU GCC的优势在于与现有代码库的兼容性以及对各种架构和操作系统的支持。虽然花了一些年的时间,Clang现在可以编译嵌入式Linux所需的所有组件,是GNU的可行的替代方案。要了解更多的情况,请看https://www.kernel.org/doc/html/latest/kbuild/llvm.html。
关于如何使用Clang进行交叉编译,在https://clang.llvm.org/docs/CrossCompilation.html,有很好的描述。如果你想把它作为嵌入式Linux构建系统的一部分,EmbToolkit(https://embtoolkit.org)完全支持GNU和LLVM/Clang工具链,而且很多人正在努力将Clang用于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就有自我托管的本地编译器。
- 交叉
这种工具链在与目标系统不同的类型上运行,允许在快速的桌面PC上进行开发,然后加载到嵌入式目标上进行测试。
几乎所有的嵌入式Linux开发都是使用交叉开发工具链完成的,部分原因是大多数嵌入式设备由于缺乏计算能力、内存和存储空间而不太适合进行程序开发,但也因为它使主机和目标环境分离。当主机和目标机使用相同的架构时,例如x86_64,后一点尤其重要。在这种情况下,在主机上进行本地编译并简单地将二进制文件复制到目标机上是很诱人的。
这在一定程度上是可行的,但很可能主机发行版比目标版更经常地收到更新,或者为目标版编写代码的不同工程师对主机开发库的版本略有不同。随着时间的推移,开发系统和目标系统将出现分歧,你将违反工具链应该在项目的整个生命周期内保持不变的原则。如果你能确保主机和目标构建环境是同步的,你就能使这种方法发挥作用。然而,更好的方法是保持主机和目标的分离,而交叉工具链就是这样做的方法。
然而,有一个支持本地开发的反驳理由。交叉开发会造成交叉编译你的目标所需的所有库和工具的负担。我们将在后面的 "交叉编译的艺术 "一节中看到,交叉开发并不总是简单的,因为许多开源包并不是被设计成以这种方式构建的。集成构建工具,包括Buildroot和Yocto项目,通过封装规则来帮助你交叉编译一系列你在典型的嵌入式系统中需要的软件包,但是如果你想编译大量的额外软件包,那么最好是原生编译它们。例如,使用交叉编译器为Raspberry Pi或BeagleBone构建一个Debian发行版将是非常困难的。相反,它们是被本地编译的。
从头开始创建一个本地构建环境并不容易。首先,你仍然需要交叉编译器来在目标上创建本地构建环境,然后用它来构建软件包。然后,为了在合理的时间内进行本地构建,你将需要一个由配置良好的目标板组成的构建农场,或者你可以使用Quick EMUlator(QEMU)来模拟目标。
CPU架构
工具链必须根据目标CPU的能力来构建,这包括以下内容:
- CPU架构: ARM、无互锁流水线阶段的微处理器(MIPS Microprocessor without Interlocked Pipelined Stages)、x86_64,等等。
- Big-或little-endian操作: 有些CPU可以在两种模式下运行,但每种模式下的机器代码是不同的。
- 浮点支持: 并非所有版本的嵌入式处理器都实现了硬件浮点单元,在这种情况下,工具链必须被配置为调用软件浮点库。
- 应用二进制接口(Application Binary Interface): 用于在函数调用之间传递参数的调用惯例。
在许多体系结构中,ABI在整个处理器系列中是不变的。值得注意的例外是ARM。ARM 架构在 2000 年代末过渡到扩展应用二进制接口 (EABI),导致以Extended Application Binary Interface前的 ABI 被命名为旧应用二进制接口 (OABI Old Application Binary Interface )。虽然OABI现在已经过时了,但你会继续看到对EABI的引用。从那时起,根据浮点参数的传递方式,EABI已经一分为二。
最初的EABI使用通用(整数)寄存器,而较新的Extended Application Binary Interface Hard-Float(EABIHF)使用浮点寄存器。EABIHF的浮点运算速度明显更快,因为它不需要在整数和浮点寄存器之间进行复制,但它与没有浮点单元的CPU不兼容。因此,你必须在两个不兼容的ABI之间做出选择;你不能混合两者,因此你必须在这个阶段做出决定。
GNU在工具链中的每个工具的名称上使用了一个前缀,它标识了可以生成的各种组合。它由一个用破折号隔开的三或四个组件组成,如下面所述:
- CPU: 这是CPU架构,如ARM、MIPS或x86_64。如果CPU有两种endian模式,可以通过添加el来区分小端模式或eb来区分大端模式。很好的例子是小字节的MIPS,mipsel,和大字节的ARM,armeb。
- 供应商: 这标识了工具链的提供者。例子包括buildroot、poky,或者只是未知。有时它被完全忽略了。
- 内核: 对于我们的目的,它总是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-表示小-endian MIPS的CPU,未知的供应商,linux的内核,和gnu的用户空间。
选择C库
Unix操作系统的编程接口是由C语言定义的,现在由POSIX标准定义。C库是该接口的实现;它是Linux程序进入内核的门户,如下图所示。即使你用其他语言编写程序,也许是Java或Python,各自的运行时支持库最终都要调用C库,如图所示:
每当C库需要内核的服务时,它将使用内核的系统调用接口在用户空间和内核空间之间转换。可以绕过C库,直接进行内核系统调用,但是这很麻烦,几乎没有必要。
有几个C库可以选择。主要的选择有以下几种:
-
glibc: 这是标准的GNU C库,可以在https://gnu.org/software/libc。它很大,而且直到最近还不是很好配置,但它是POSIX API的最完整的实现。它的许可证是LGPL 2.1。
-
musl libc: https://musl.libc.org。musl libc库相对较新,但作为GNU libc的小型和符合标准的替代品,它已经得到了很多关注。对于内存和存储空间有限的系统来说,它是一个不错的选择。它有一个MIT许可证。
-
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。如果你的存储空间或内存非常有限,那么musl libc是一个不错的选择,否则就使用glibc:
你对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-[版本]/toolchain有一些例子。
- 已经找不到的论坛的链接。
在所有这些情况下,你必须决定所提供的预建工具链是否符合你的要求。它是否使用你喜欢的C库?供应商是否会给你提供安全修复和错误的更新,请记住我在第一章 "起步 "中对支持和更新的评论。如果你的答案是否定的,那么你应该考虑创建你自己的。
不幸的是,建立工具链不是一件容易的事。如果你真的想自己来做这件事,可以看看Cross Linux From Scratch(https://trac.clfs.org)。在那里,你可以找到关于如何创建每个组件的分步说明。
一个更简单的选择是使用crosstool-NG,它将这个过程封装成一组脚本,并有菜单驱动的前端。不过,你仍然需要相当程度的知识,只是为了做出正确的选择。
使用Buildroot或Yocto项目这样的构建系统更简单,因为它们会在构建过程中生成一个工具链。这是我的首选方案,正如我在第6章 "选择构建系统 "中所示。
随着crosstool-NG的兴起,建立你自己的工具链当然是一个有效和可行的选择。接下来让我们看看如何做到这一点。
使用crosstool-NG构建工具链
参见:https://crosstool-ng.github.io/docs/install/
几年前,Dan Kegel写了一套用于生成跨开发工具链的脚本和makefiles,并称之为crosstool(http://kegel.com/crosstool/)。2007年,Yann E. Morin在此基础上创建了下一代的crosstool,即crosstool-NG(https://crosstool-ng.github.io)。今天,它是迄今为止从源头上创建独立的交叉工具链的最方便的方法。
在本节中,我们将使用crosstool-NG来为BeagleBone Black和QEMU。
安装crosstool-NG
在你从源代码构建crosstool-NG之前,你首先需要在你的主机上安装本地工具链和一些构建工具。关于crosstool-NG的完整构建和运行时依赖项,请参见本章开头的技术要求部分。
为BeagleBone Black构建工具链
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-5.16.9
Binutils : binutils-2.38
Compiler : gcc-11.2.0
C library : glibc-2.35
Debug tools : duma-2_5_15 gdb-11.2 ltrace-0.7.3 strace-5.16
Companion libs : expat-2.4.1 gettext-0.21 gmp-6.2.1 isl-0.24 libelf-0.8.13 libiconv-1.16 mpc-1.2.1 mpfr-4.1.0 ncurses-6.2 zlib-1.2.12
Companion tools :
这与我们的要求很接近,除了它使用eabi二进制接口,以整数寄存器传递浮点参数。我们更倾向于使用硬件浮点寄存器来实现这一目的,因为这样可以加快有浮点和双倍参数类型的函数调用速度。你以后可以改变配置,所以现在你应该选择这个目标配置:
$ bin/ct-ng arm-cortex_a8-linux-gnueabi
CONF arm-cortex_a8-linux-gnueabi
#
# configuration written to .config
#
***********************************************************
Initially reported by: Yann E. MORIN
URL: http://ymorin.is-a-geek.org/
***********************************************************
Now configured for "arm-cortex_a8-linux-gnueabi"
使用配置菜单命令menuconfig审查配置并进行修改:
$ bin/ct-ng menuconfig
该菜单系统是基于Linux内核的menuconfig,因此,任何配置过内核的人都会熟悉用户界面的导航。如果没有,请参阅第4章,配置和构建内核,了解menuconfig的描述。
建议你做三个配置上的改变:
- 在Paths and misc options中,禁用Render the toolchain read-only(CT_PREFIX_DIR_RO).
(ct_prefix_dir_ro)。 - Target options | Floating point,选择硬hardware (FPU) (CT_ARCH_FLOAT_HW)。
- Target options | neon,选择Use specific FPU。
如果你想在工具链安装后将库添加到工具链中,第一个选项是必要的,我将在后面的用库链接部分介绍。第二个选择eabihf二进制接口,原因在前面已经讨论过了。第三项是成功构建Linux内核所需要的。括号里的名字是存储在配置文件中的配置标签。当你做了修改后,退出menuconfig菜单,并像以前一样保存配置。
现在你可以使用crosstool-NG来获取、配置,并按照你的规范来构建组件,输入以下命令:
$ bin/ct-ng build
构建将花费大约半小时,之后你会发现你的工具链出现在~/x-tools/arm-cortex_a8-linux-gnueabihf。 如果出现下载zlib失败,参考https://github.com/crosstool-ng/crosstool-ng/issues/1337,手动下载拷贝即可。
为QEMU构建工具链
在QEMU目标上,你将模拟具有ARM926EJ-S处理器内核的ARM-versatile PB评估板,它实现了ARMv5TE指令集。你需要生成符合规范的crosstool-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。只有一个改动是必要的:
在Paths and misc options中,禁用Render the toolchain read-only
(ct_prefix_dir_ro)。
现在,用这里的命令构建工具链:
$ bin/ct-ng build
和以前一样,构建将需要大约半小时。工具链将被安装在~/x-tools/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程序:
#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 5.16.9, with debug_info, not stripped
了解你的交叉编译器
可以通过查询gcc发现很多东西。例如,要找到版本,你可以使用--版本:
$ arm-cortex_a8-linux-gnueabihf-gcc --version
arm-cortex_a8-linux-gnueabihf-gcc (crosstool-NG 1.25.0) 11.2.0
Copyright (C) 2021 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/andrew/x-tools/arm-cortex_a8-linux-gnueabihf/libexec/gcc/arm-cortex_a8-linux-gnueabihf/11.2.0/lto-wrapper
Target: arm-cortex_a8-linux-gnueabihf
Configured with: /opt/crosstool/.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/andrew/x-tools/arm-cortex_a8-linux-gnueabihf --exec_prefix=/home/andrew/x-tools/arm-cortex_a8-linux-gnueabihf --with-sysroot=/home/andrew/x-tools/arm-cortex_a8-linux-gnueabihf/arm-cortex_a8-linux-gnueabihf/sysroot --enable-languages=c,c++ --with-cpu=cortex-a8 --with-fpu=neon --with-float=hard --with-pkgversion='crosstool-NG 1.25.0' --enable-__cxa_atexit --disable-libmudflap --disable-libgomp --disable-libssp --disable-libquadmath --disable-libquadmath-support --disable-libsanitizer --disable-libmpx --disable-libstdcxx-verbose --with-gmp=/opt/crosstool/.build/arm-cortex_a8-linux-gnueabihf/buildtools --with-mpfr=/opt/crosstool/.build/arm-cortex_a8-linux-gnueabihf/buildtools --with-mpc=/opt/crosstool/.build/arm-cortex_a8-linux-gnueabihf/buildtools --with-isl=/opt/crosstool/.build/arm-cortex_a8-linux-gnueabihf/buildtools --enable-lto --enable-threads=posix --enable-target-optspace --enable-plugin --enable-gold --disable-nls --disable-multilib --with-local-prefix=/home/andrew/x-tools/arm-cortex_a8-linux-gnueabihf/arm-cortex_a8-linux-gnueabihf/sysroot --enable-long-long
Thread model: posix
Supported LTO compression algorithms: zlib
gcc version 11.2.0 (crosstool-NG 1.25.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
果你计划为每个目标创建工具链,那么在一开始就设置好是有意义的,因为这将减少以后出错的风险。另一方面,如果你想构建通用的工具链,并且准备在为特定目标构建时提供正确的设置,那么你应该使基础工具链通用化,这就是Yocto项目处理事情的方式。前面的例子都是遵循Buildroot的理念。
sysroot、库和头文件
工具链的系sysroot是包含库、头文件和其他配置文件的子目录的目录。它可以在配置工具链时通过 --with-sysroot= 设置,也可以在命令行中通过 --sysroot= 设置。 你可以通过使用 -print-sysroot 看到默认的系统根目录的位置:
$ arm-cortex_a8-linux-gnueabihf-gcc -print-sysroot
/home/andrew/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:主C库,包含众所周知的POSIX函数,如printf、open、close、read、write,等等。
- libm: 包含数学函数,如cos、exp和log
- libpthread: 包含了所有的POSIX线程函数,名称以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 code/helloworld | grep "Shared library"
0x00000001 (NEEDED) Shared library: [libc.so.6]
共享库需要运行时链接器:
$ arm-cortex_a8-linux-gnueabihf-readelf -a code/helloworld | grep "program interpreter"
[Requesting program interpreter: /lib/ld-linux-armhf.so.3]
链接库
你为Linux编写的任何应用程序,无论是C语言还是C++语言,都会与lic 库链接。你甚至不需要告诉gcc或g++去做,因为它总是链接libc。其他你可能想要链接的库必须通过-l选项明确指定。
库的代码可以用两种不同的方式连接:静态的,意味着你的应用程序所调用的所有库函数和它们的依赖关系都是从库的归档文件中提取并绑定到你的可执行文件中;动态的,意味着对库文件和这些文件中的函数的引用是在代码中生成的,但实际的连接是在运行时动态进行的。
静态库
如果你正在构建小程序,静态链接会更简单,避免复制运行时库文件和链接程序。它也会更小,因为只链接你的应用程序使用的代码,而不是提供整个C库。如果你需要在存放运行库的文件系统可用之前运行程序,静态链接也很有用。
你可以通过在命令行中添加-static来静态链接所有库:
$ arm-cortex_a8-linux-gnueabihf-gcc -static helloworld.c -o helloworld-static
$ ls -lh he*
-rwxrwxr-x 1 andrew andrew 12K 6月 21 15:39 helloworld
-rwxrwxr-x 1 andrew andrew 123 6月 26 15:58 helloworld.c
-rwxrwxr-x 1 andrew andrew 2.9M 6月 26 15:58 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
创建静态库就像使用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
$ arm-cortex_a8-linux-gnueabihf-gcc helloworld.c -ltest -L../libs -I../libs -o helloworld
静共享库
部署库的更常见的方式是在运行时链接的共享对象,这可以更有效地利用存储和系统内存,因为只需要加载一份代码。它还使更新库文件变得容易,而不必重新链接所有使用它们的程序。
共享库的目标代码必须是独立于位置的,这样运行时链接器就可以自由地在内存中的下一个空闲地址处找到它。要做到这一点,在gcc中加入-fPIC参数,然后用-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,它必须存在于目标的文件系统中。链接器将在默认的搜索路径中寻找libtest.so: /lib和/usr/lib。如果你想让它也寻找其他目录中的库,你可以在LD_LIBRARY_PATH壳变量中放置用冒号分隔的路径列表:
$ 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的接口发生变化时,可能会出现编译错误,开发人员可以修复。
目标机上任何没有重新编译的程序都会以某种方式失败,因为它们仍在使用旧的接口。这时,soname的对象就有帮助了。soname编码了库建立时的接口号,运行时链接器在加载库时使用它。它的格式是:<库名>.so.<接口号>。对于libjpeg.so.8.2.2,子名是libjpeg.so.8,因为libjpeg共享库建立时的接口号是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这。为了真正有效地进行交叉编译,我们需要把交叉工具链和构建系统结合起来。
参考资料
- 软件测试精品书籍文档下载持续更新 https://github.com/china-testing/python-testing-examples 请点赞,谢谢!
- 本文涉及的python测试开发库 谢谢点赞! https://github.com/china-testing/python_cn_resouce
- python精品书籍下载 https://github.com/china-testing/python_cn_resouce/blob/main/python_good_books.md
交叉编译的艺术
拥有有效的交叉工具链是起点,而不是终点。在某些时候,你会想开始交叉编译你在目标上需要的各种工具、应用程序和库。它们中的许多都是开源包,每一个都有自己的编译方法和自己的特殊性。
有一些常见的构建系统,包括以下几种:
- 纯粹的makefiles,其中的工具链通常由make变量CROSS_COMPILE控制
- Autotools: GNU构建系统
- CMake(https://cmake.org)
即使是构建基本的嵌入式Linux系统,也需要Autotools和makefiles。CMake是跨平台的,多年来被越来越多的人采用,特别是在C++社区。在本节中,我们将介绍这三种构建工具。
简单的makefile
一些重要的软件包的交叉编译非常简单,包括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设置为它们所支持的机器架构之一。
Autotools和CMake都可以生成makefile。Autotools只生成makefiles,而CMake则支持其他构建项目的方式,这取决于我们所针对的
我们的目标平台(在我们的例子中严格来说是Linux)。
Autotools
Autotools这个名字是指一组工具,在许多开源项目中被用作构建系统。这些组件,连同相应的项目页面,如下所示:
- GNU Autoconf (https://www.gnu.org/software/autoconf/autoconf.html)
- GNU Automake (https://www.gnu.org/savannah-checkouts/gnu/automake/)
- GNU Libtool (https://www.gnu.org/software/libtool/libtool.html)
- Gnulib (https://www.gnu.org/software/gnulib/)
Autotools的作用是抹平软件包可能被编译的不同类型的系统之间的差异,考虑到不同版本的编译器、不同版本的库、不同位置的头文件以及与其他软件包的依赖关系。
使用Autotools的软件包有一个名为configure的脚本,它检查依赖关系,并根据发现的情况生成makefiles。configure脚本还可以让你有机会启用或禁用某些功能。你可以通过运行./configure --help找到所提供的选项。
要配置、构建和安装本地操作系统的软件包,你通常要运行以下三个命令:
$ ./configure
$ make
$ sudo make install
Autotools也能够处理交叉开发。你可以通过设置这些shell变量来影响配置的脚本的行为:
- CC: C编译器的命令。
- CFLAGS: 额外的C编译器标志。
- CXX: C++编译器命令。
- CXXFLAGS: 额外的C++编译器标志。
- LDFLAGS: 额外的链接器标志;例如,如果你在非标准的目录
下有库,你将把它添加到库搜索路径上 加入 -L 。 - LIBS: 包含一个要传递给链接器的额外库的列表;例如、 -lm表示数学库。
- CPPFLAGS: 包含C/C++预处理器的标志;例如,你可以添加-I
来搜索非标准目录中的头文件 。 - 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=
Autotools理解在编译软件包时可能涉及的三种不同类型的机器:
- Build: 构建软件包的计算机,默认为当前机器。
- Host: 程序将要运行的计算机。对于本地编译,这部分留空,默认为与Build相同的计算机。当你进行交叉编译时,把它设置为你的工具链的元组。
- Target: 程序将生成代码的计算机。你会在构建交叉编译器时设置这个。
因此,为了交叉编译,你只需要覆盖主机,如下所示:
$ CC=arm-cortex_a8-linux-gnueabihf-gcc ./configure --host=arm-cortex_a8-linux-gnueabihf
最后要注意的是,默认的安装目录是
你通常会把它安装在
配置典型的Autotools软件包的完整命令如下:
$ CC=arm-cortex_a8-linux-gnueabihf-gcc ./configure --host=arm-cortex_a8-linux-gnueabihf
例子--SQLite
$ wget http://www.sqlite.org/2020/sqlite-autoconf-3330000.tar.gz
$ tar xf sqlite-autoconf-3330000.tar.gz
$ cd sqlite-autoconf-3330000
$ CC=arm-cortex_a8-linux-gnueabihf-gcc ./configure --host=arm-cortex_a8-linux-gnueabihf --prefix=/usr # 日志config.log
$ make # 或 $ make DESTDIR=$(arm-cortex_a8-linux-gnueabihf-gcc -print-sysroot) install
你可能会发现,最后的命令会因为文件权限错误而失败。crosstool-NG工具链默认是只读的,这就是为什么在构建它时将CT_PREFIX_DIR_RO设置为y是很有用的。另一个常见的问题是,工具链被安装在系统目录下,例如/opt或/usr/local。在这种情况下,你在运行安装时将需要root权限。
安装后,你应该发现各种文件已经被添加到你的工具链中:
/usr/bin: sqlite3: 这是一个SQLite的命令行接口,你可以在目标机上安装和运行。 /usr/lib: libsqlite3.so.0.8.6, libsqlite3.so.0, libsqlite3.s, libsqlite3.la, libsqlite3.a: 这些是共享和静态库。 /usr/lib/pkgconfig: sqlite3.pc: 这是软件包的配置文件,如下一节所述。 /usr/lib/include: sqlite3.h, sqlite3ext.h: 这些是头文件。 /usr/share/man/man1: sqlite3.1: 这是手册页。
现在你可以编译使用sqlite3的程序,在 链接阶段添加-lsqlite3:
$ arm-cortex_a8-linux-gnueabihf-gcc -lsqlite3 sqite-test.c -o sqite-test
$ arm-cortex_a8-linux-gnueabihf-gcc -lsqlite3 sqlite-test.c -o sqlite-test
这里,sqlite-test.c是调用SQLite函数的假想程序。由于sqlite3已经被安装到系统根中,编译器会顺利找到头文件和库文件。如果它们被安装在其他地方,你将不得不添加-L
当然,也会有运行时的依赖性,你将不得不在目标目录中安装适当的文件安装到目标目录中,如第5章 "建立根文件系统 "中所述。
为了交叉编译一个库或包,它的依赖项首先需要被交叉编译。Autotools依靠pkg-config工具来收集关于由Autotools交叉编译的软件包的重要信息。
包的配置
$ 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
失败的原因是它在主机的系统根中寻找,而主机上还没有安装libsqlite3的开发包。你需要通过设置PKG_CONFIG_LIBDIR壳变量将其指向目标工具链的系统根:
$ 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等库的自制构建系统,其配置脚本的行为与上一节所述的Autotools configure不一样
- 配置脚本从主机上读取 pkg-config 信息、头文件和其他文件,而无视 --host 覆盖。
- 坚持尝试运行交叉编译代码的脚本
每种情况都需要仔细分析错误,给配置脚本增加参数以提供正确的信息,或者给代码打补丁以完全避免问题。请记住,软件包可能有许多依赖关系,特别是那些使用GTK或Qt的图形界面的程序,或者处理多媒体内容的程序。举个例子,mplayer,这是一个流行的播放多媒体内容的工具,与100多个库有依赖关系。要想把它们全部建立起来,需要花费数周的时间。
因此,我不建议以这种方式为目标手动交叉编译组件,除非没有其他选择,或者需要编译的软件包数量很少。更好的方法是使用Buildroot或Yocto项目这样的构建工具,或者通过为你的目标架构设置本地构建环境来完全避免这个问题。现在你可以明白为什么像Debian这样的发行版总是在本地编译了。
CMake
CMake更像是一个元构建系统,因为它依赖于底层平台的本地工具来构建软件。在Windows上,CMake可以为Microsoft Visual Studio生成项目文件,在macOS上,它可以为Xcode生成项目文件。与每个主要平台的主要IDE集成不是简单的任务,这解释了CMake作为领先的跨平台构建系统解决方案的成功。CMake也可以在Linux上运行,在那里它可以和你选择的交叉编译工具链一起使用。
要配置、构建和安装本地Linux操作系统的软件包,请运行以下命令:
$ cmake .
$ make
$ sudo make install
在Linux上,本地构建工具是GNU make,因此CMake默认生成makefile文件供我们构建使用。很多时候,我们想进行源外构建,以便对象文件和其他构建工件与源文件保持分离。
要在名为_build的子目录中配置源外构建,请运行
以下命令:
$ mkdir _build
$ cd _build
$ cmake ..
这将在CMakeLists.txt所在的项目目录下的_build子目录中生成makefiles。CMakeLists.txt文件相当于基于Autotools的项目的CMake配置脚本。
然后,我们可以从_build目录中构建项目的源代码,并像以前一样安装软件包:
$ make
$ sudo make install
CMake使用绝对路径,所以一旦生成了makefiles,_build子目录就不能被复制或移动,否则任何后续的make步骤都可能失败。注意,CMake默认将软件包安装到系统目录中,如/usr/bin,即使是源外构建。
要生成 makefiles 以使 make 将软件包安装在 _build 子目录中,请用下面的命令替换之前的 cmake 命令:
$ cmake .. -D CMAKE_INSTALL_PREFIX=../_build
我们不再需要在make install前加上sudo,因为我们不需要升高权限来复制包文件到_build目录中。
同样地,我们可以使用另一个CMake命令行选项来生成用于交叉编译的makefiles:
$ 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文件,用于配置目标: 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})
CMake为流行的C和C++包配备了大量的搜索器,包括OpenSSL、Boost和protobuf,这使得本地开发比我们只使用纯粹的makefiles更有成效。
PRIVATE修饰符可以防止诸如头文件和标志等细节泄露到假目标之外。当被构建的目标是库而不是可执行文件时,使用PRIVATE更有意义。在使用CMake定义你自己的目标时,把目标看作是模块,并试图最小化其暴露的表面区域。只有在绝对必要时才使用PUBLIC修饰符,并对仅有头文件的库使用INTERFACE修饰符。
将你的应用程序建模为一个具有目标之间的边的依赖图。这个图不仅应该包括你的应用程序直接链接到的库,还应该包括任何横向的依赖关系。为了获得最佳效果,请删除图中的任何循环或其他不必要的独立性。在你开始编码之前,通常最好进行这个练习。一个干净的、易于维护的CMakeLists.txt和一个无人问津的乱七八糟的东西之间,稍加计划就能产生不同的效果。
总结
工具链始终是你的起点;接下来的一切都取决于是否有有效的、可靠的工具链。
你可以从什么都不做开始,只用工具链--也许是用crosstool-NG构建的,或者是从Linaro下载的--用它来编译你在目标上需要的所有软件包。或者你可以获得工具链,作为使用Buildroot或Yocto项目等构建系统从源代码生成的发行版的一部分。小心那些作为硬件包的一部分免费提供给你的工具链或发行版;它们通常配置很差而且没有得到维护。
一旦你有了一个工具链,你就可以用它来构建你的嵌入式Linux系统的其他组件。在下一章中,你将学习引导程序,它将你的设备带入生活并开始启动过程。我们将使用本章中建立的工具链为BeagleBone Black建立一个工作的引导程序。
进一步阅读
这里有几段视频,记录了写作时交叉工具链和构建系统的技术状况:
Bernhard "Bero" Rosenkränzer的《2020年工具链和交叉编译器的新视角》:https://www.youtube.com/watch?v=BHaXqXzAs0Y
用于模块化设计的现代CMake,作者Mathieu Ropert:
https://www.youtube.com/watch?v=eC9-iRN2b04