编译工具链将程序源代码翻译成可以在计算机上运行的可执行程序。编译过程是由一系列的步骤组成的,每一个步骤都有一个对应的工具。这些工具紧密地工作在一起,前一个工具的输出是后一个工具的输入,像一根链条一样,我们称这一系列工具为编译工具链。编译工具链是与其运行的平台(CPU 架构 和 操作系统)息息相关的。
编译工具链
软件程序的编译过程由一系列的步骤完成,每一个步骤都有一个对应的工具。这些工具紧密地工作在一起,前一个工具的输出是后一个工具的输入,像一根链条一样,我们称这一系列工具为编译工具链(Toolchain)。编译工具链主要包含 编译器等可执行程序 与 标准库(常用函数通用实现) 两大部分。
能独立提供编译工具链的厂家并不多,嵌入式平台则更少,主要就是 ARM、IAR、GNU、LLVM。其中,ARM、IAR 是收费的专用软件,其支持的架构有限,而 GNU 的 GCC 则是一款支持众多架构的开源编译套件;LLVM 则是后起之秀,同样也支持众多架构,目前用的不如 GCC 广泛!如下是常用编译工具链:
编译器
第一代编程语言的是汇编语言,因此,第一代编译器是编译汇编语言的。后来,高级语言逐步流行,但是编译工具链中仍然保留了高级语言到汇编语言这一步骤。而原来的汇编语言的编译器被称为汇编器,高级语言的编译器则就称为编译器。
链接器
高级语言诞生之后,源代码被有效组织为了不同的模块,分别实现在不同的文件中,编译器处理每个源文件,而连机器则负责将各个编译后的二进制文件合并为最终的可执行文件。
实用工具
除了编译器、链接器等工具,编译工具链中通常还会包含一些用来处理编译后二进制文件的辅助工具。例如,以 GCC 中的 objdump
、readelf
为代表的的用来处理对象文件的工具。
标准库
标准库提供了一些通用函数的公共实现,绝大多数工具链都是提供一些预编译好的二进制文件(.o
文件),当我们编译自己的程序时,工具链自动以静态链接库的形式引入到我们的最终可执行程序中。这些公共实现的代码通过 include
中各头文件中的函数接口暴露给用户来引用。
通常,这些预编译的文件还会被打包成 存档文件(
.a
文件) 来提供(编译工具链中有相应工具来解析执行存档文件,例如,GCC 中的ar
)。
编译过程
要编译出最终的可执行程序,通常需要编译、链接、转换这三个阶段。其中,编译即编译器将源码翻译成对象文件,链接即链接器将各个对象文件组合成最终可执行程序。现代编译器通常产生一个通用格式(通常是带有调试信息)的最终可执行程序(ELF 文件),然后使用相应的工具从中提取出实际的纯可执行程序。
具体到编译器,其编译过程通常也是分为多个阶段的。在编译原理这门课程中,我们学过三段式编译器架构,其在编译时要依次经过词法分析、句法分析、语义分析、中间代码生成、代码优化、代码生成 六大阶段。
交叉编译
编译工具链产生可执行程序,但同时,编译工具链本身也是一个可执行程序。构建编译工具链本身时,构建编译工具链使用的平台、编译出的编译工具链运行的平台、使用编译出的编译工具链编译出的程序运行的平台三者可以完全不同。这三者就对应构建时设置 configure 的三个参数 --build
、--host
、--target
。下图是 Windows 上的 MinGW 编译器配置信息:
--build
:这个参数指出了构建编译工具链使用的平台。如果我们不显示指定这个参数的值,那么这个参数的值就会由config.guess
自动识别。--host
:这个参数指出了编译出的编译工具链运行的平台。这个参数的值一般就等于--build
的值。--target
:这个参数指出了使用编译出的编译工具链编译出的程序运行的平台。
通常,本地编译工具链一般就是指的 --build
= --host
= --target
的情况,交叉编译工具链一般是--build
= --host
≠ --target
的情况。不过,基本很少出现--build
≠ --host
的情况(例如,在 Linux 下构建 Windows 下运行的 GCC)。
交叉编译工具链
在当前平台下(例如 x86 架构的 PC)下,直接编译出来程序(或者库文件),其不可以直接在当前的平台运行(或使用),必须放到目标平台上(例如 ARM)才可以运行(或使用),这个过程就叫做交叉编译,使用的编译工具叫做交叉编译工具链。例如 PC 中的 armcc
、iar
、特定架构的 GCC
、特定架构的 LLVM
等。
由于历史原因,我们说到交叉编译工具链通常就是指 GCC
交叉编译工具链又可以根据是否支持 Linux 系统分为 裸机程序交叉编译工具链 和 Linux 程序交叉编译工具链 这两大类。我们上面的举例中,armcc
、iar
都属于裸机交叉编译工具链;而特定架构的 GCC
、特定架构的 LLVM
则根据需要可以支持 Linux 系统,也可以不支持 Linux 系统,因此它既有裸机程序交叉编译工具链,也有 Linux 程序交叉编译工具链。
- 裸机程序交叉编译工具链不能编译 Linux 应用程序,但是,可以用于编译一些嵌入式实时操作系统(FreeRTOS、RT-Thread 等)
- Linux 程序交叉编译工具链不止可以编译 linux 应用程序,也可以编译裸机程序
本地编译工具链
在当前平台(例如 x86 架构的 PC)下,直接编译出来程序(或者库文件),其可以直接在当前的平台运行(或使用)。这个过程就叫做本地编译,使用的编译工具叫做本地编译工具链(简称编译工具链)。例如 PC 上的 VC
、GCC
、LLVM
、TCC
等。
The xPack Project
The xPack Project 是一个开源项目,其提供了一系列开发工具(重点是裸机下的 C/C++ 相关的)在不同平台的下的构建实例,其中就包含各平台的 GCC 交叉编译工具链,它使用一个多版本依赖管理器来管理各个实例。
实际开发中,我们经常会独立使用它提供的某些工具。例如,Eclipse 的嵌入式 C/C++ 插件 Eclipse Embedded CDT 就包含一些 xPack 提供的工具,以此来实现创建、构建、调试和管理 Arm 和 RISC-V 项目。
-
xPack Windows Build Tools:包括在 Windows 上执行构建所需的额外工具(
make
、rm
等) -
xPack GNU Arm Embedded GCC:ARM 维护的官方 GNU ARM 嵌入工具链的一个代替,可以用于 Windows,MacOS和 GNU/Linux 平台。
-
xPack GNU RISC-V Embedded GCC:裸机 RISC-V GCC 发行版,由 SiFive 维护。Windows、macOS 和 GNU/Linux 都有可用的二进制文件。
-
xPack OpenOCD:OpenOCD 的一个新发行版,为更好/更方便地与 OpenOCD 调试插件集成而定制。Windows、macOS 和GNU/Linux 都有可用的二进制文件。
-
xPack QEMU Arm:QEMU(开源机器仿真器)的一个分支,旨在为 Eclipse Embedded CDT 中 的Cortex-M 仿真提供支持。Windows、macOS 和 GNU/Linux都有可用的二进制文件。
GCC
我们使用最多交叉编译工具链就是特定架构的 GNU 工具链,也就是我们我们常说的 GCC 交叉编译工具链。注意,armcc
、iar
也属于交叉编译工具链,但是,一说起交叉编译工具链,大家往往首先想到的就是 GCC,例如,ARM 官方提供的 Arm GNU Toolchain。
随着开源运动的兴起,自由软件基金会开发了自己的开源免费的 C 语言编译器 GNU C Compiler,简称 GCC。GCC 中提供了 C Preprocessor 这个 C 语言的预处理器,简称 CPP。后来 GCC 又加入了对 C++ 等其它语言的支持,所以他的名字也改为 GNU Compiler Collection。
命名规则
一般来说,交叉编译工具链的命名规则是:arch [-vendor] [-os] [-(gnu)abi]-*
。但是,关于这个规则,我并没有找到在哪份官方资料上有介绍,实际有些交叉编译工具链也确实不符合上面的命名规则。如果有谁在官方资料上见到过此规则的详细描述,一定要私信告诉我。
- arch: 表示编译工具链支持的体系架构,如 ARM、MIPS、RISC-V
- vendor: 表示工具链提供商,没有 vendor 时,用 none 代替。
- os: 表示工具链是有操作系统支持的,其编译出的程序可以在 os 给出的操作系统上运行,没有 os 支持时,也用 none 代替,表示裸机。如果同时没有 vendor 和 os 支持,则只用一个 none 代替。例如 arm-none-eabi 中的 none 表示既没有 vendor 也没有 os 支持。目前取值就只有以下两种:
- none
- C 库通常是 newlib
- 提供不需要操作系统的 C 库服务
- 允许为特定硬件目标提供基本系统调用
- 可以用来构建 Bootloader 或 Linux Kernel,不能构建 Linux 用户空间代码
- linux
- 用于 Linux 操作系统的开发
- linux 特有的 C 库的选择:glibc、uClibc-ng、musl
- 支持 Linux 系统调用
- 可以用来构建 Linux 用户空间代码,但也可以构建裸机代码,如 Bootloader 或 Linux Kernel
- none
- abi: 应用二进制接口(Application Binary Interface)
- eabi: 嵌入式应用二进制接口(Embedded Application Binary Interface,EABI),这个是好像是 ARM 搞的一个标准,注意,AArch32 和 AArch64 的 EABI 并不相同。
- gnu: 这个其实是早期 GNU 针对 AArch32 架构使用的名字,后来改名字为 gnueabi
- gnueabi: 其实就是嵌入式应用二进制接口(Embedded Application Binary Interface,EABI),只不过是 GNU 出品的。
- elf: 这个通常用在 64 位裸机架构的编译工具链中
由于 ARM 的绝对市场地位,导致了在网上搜索交叉编译工具链基本都和 ARM 有关系。这个命名规则就源自 ARM,其他架构提供的交叉编译工具链也基本是按照这个命名规则来命名的。
组成部分
GUN 交叉编译工具链中有三个核心组件:Binutils、GCC、C library,如果需要支持 Linux,则还有个 Linux kernel headers。在源代码组织上他们是相互独立的,需要单独进行交叉编译。
- Binutils:GNU Binutils 是一个二进制工具的集合。
- 官网:https://www.gnu.org/software/binutils/。
- 获取源代码:
git clone git://sourceware.org/git/binutils-gdb.git
或者https://ftp.gnu.org/gnu/binutils
,目前最新版是 2.39 - 主要工具
ld
:链接器as
:汇编器gold
:一个新的,更快的,ELF链接器。
- 调试/分析工具和其他工具
addr2line
:将地址转换为文件名和行号。ar
:一个用于创建、修改、提取存档的实用工具c++filt
:过滤器要求编码的c++符号dlltool
:用于构建和使用 dll 的文件elfedit
:用于编辑 ELF 格式文件gprof
:显示分析信息gprofng
:收集和显示应用程序性能数据nlmconv
:将目标代码转换为 NLMnm
:列出目标文件中的符号objcopy
:复制和翻译目标文件objdump
:显示来自目标文件的信息ranlib
:生成归档文件内容的索引readelf
:显示来自任何ELF格式目标文件的信息size
:列出对象或存档文件的节大小strings
:列出文件中的可打印字符串strip
:丢弃符号windmc
:一个 Windows 兼容的消息编译器windres
:Windows 资源文件的编译器
- 需要针对每种 CPU 架构进行配置
- gcc:GNU Compiler Collection
- C、C++、Fortran、Go 等编译器前端
- 各种 CPU 架构的编译器后端
- Provides:
-
编译器本身。例如 cc1 for C、cc1plus for C++
-
编译器调用程序。gcc、g++ 不但调用编译器本身,也调用 binutils 中的 汇编器、连接器
不要被 gcc 这个名字误导,它其实是个 wrapper,会根据输入文件调用一系列其他程序。国外资料中被称为 compiler driver,国内有些资料称为 引导器。
-
目标库:libgcc(gcc 运行时)、libstdc ++(c ++ 库)、libgfortran(fortran运行时)
-
标准 c++ 库的头文件
-
- 构建 gcc 比构建 binutils 要复杂的多
- Linux Kernel headers:构建需要支持 Linux 系统时必须提供。这些头文件定义了用户空间与内核之间的接口(系统调用、数据结构等)。
- 为了构建一个 C 库,需要 Linux 内核头文件中系统调用号的定义、各种结构类型和定义。
- 在内核中,头文件被分开:
- 用户空间可见的头文件,存储在
uapi
目录中:include/uapi/
、arch/<ARCH>/include/uapi/asm
- 内部的内核头文件
- 用户空间可见的头文件,存储在
- 在安装过程中需要使用
- 安装包括一个清理过程,用于从头文件中删除特定于内核的结构体
- 从 Linux 4.8 开始,安装 756 个头文件
- 内核到用户空间 ABI 通常是向后兼容的。Kernel headers 的版本必须等于或者小于目标 Linux 的版本
- C library:
-
提供 POSIX 标准函数的实现,以及其他几个标准和扩展
-
基于 Linux 系统调用
-
几个可用的实现
-
glibc:The GNU C Library 是 Linux C 库的事实标准,我们常见的 Linux 发行版中都使用它。支持众多的架构和操作系统,但是不支持没有 MMU 的平台,不支持静态链接。早些年由于硬件限制及 glibc 本身太大基本不能直接用于嵌入式,如今貌似也可以了。
-
uClibc-ng:以前叫 uClibc,始于 2000 年,支持非常灵活的配置。支持架构很多(包括一些 glibc 不支持的),但是仅支持 Linux 操作系统。支持多种没有 MMU 的架构,如 ARM noMMU、Blackfin 等,支持静态链接。
STM32F MCU 没有 MMU,其嵌入式 Linux 环境中编译工具链就是用的它。
-
musl:始于 2011 年,开发非常积极,最近添加了对于 noMMU 的支持。它非常小,尤其是在静态链接时。兼容性好,并且严格遵循 C 标准。
-
bionic:安卓系统使用
-
其他一些特殊用途的:newlib(用于裸机)、dietlibc、klibc
musl 的作者对于 Linux 常用的这几个库做了一个对比,以下是对比情况图:
-
-
在编译和安装之后,提供了:
- 动态链接器
ld.so
- C 库本身
libc.so
,及其配套库:libm、librt、libpthread、libutil、libnsl、libresolv、libcrypt - C 库的头文件:
stdio.h
、string.h
等等。
- 动态链接器
-
LLVM
LLVM 是一组工具链和编译器技术。回顾 GCC 的历史,虽然它取得了巨大的成功,但开发 GCC 的初衷是提供一款免费的开源的编译器,仅此而已。可后来随着 GCC 支持了越来越多的语言,GCC 架构的问题也逐渐暴露出来。
传统编译器的工作原理基本上都是三段式的,可以分为前端(Frontend)、优化器(Optimizer)、后端(Backend)。前端负责解析源代码,检查语法错误,并将其翻译为抽象的语法树(Abstract Syntax Tree);优化器对这一中间代码进行优化,试图使代码更高效;后端则负责将优化器优化后的中间代码转换为目标机器的代码,这一过程后端会最大化的利用目标机器的特殊指令,以提高代码的性能。
虽然这种三段式的编译器有很多优点,并且被写到了教科书中,但是在实际中这一结构却从来没有被完美实现过。
LLVM 作为后起之秀,从开始就是按照前端(Frontend)、优化器(Optimizer)、后端(Backend)的三段式进行设计,整个编译器框架非常符合人们对于编译器的设计以及非常容易理解和学习。LLVM 的在很大程度上兼容 GNU,本文主要介绍 GNU。
- LLVM 的命名最早源自于底层虚拟机(Low Level Virtual Machine)的首字母缩写,但这个项目并不局限于创建一个虚拟机,开发者因而决定放弃这个缩写的意涵。现在 LLVM 是一个专用名词,表示编译器框架整个项目。
- 目前,很多平台都开始转投 LLVM 了,例如苹果、安卓、ARM 等等
参考
- https://elinux.org/Toolchains
- https://blog.csdn.net/linczone/article/details/45894181
- https://www.codenong.com/cs109691911/
- https://www.cnblogs.com/albertzheng/p/16695616.html