15、深入解析GNU跨平台开发工具链
本文深入解析了GNU跨平台开发工具链的构建过程,涵盖核心组件如binutils、GCC、glibc及Linux内核头文件的作用与配置方法。详细探讨了构建系统、宿主系统与目标系统的区别,GNU三元组命名规范,C库和线程库的选择策略,以及版本兼容性问题的解决方案。通过流程图梳理构建步骤,并提供常见错误排查建议,帮助开发者高效构建稳定可靠的交叉编译环境,适用于嵌入式Linux系统开发。
深入解析GNU跨平台开发工具链
1. 工具链概述
跨平台开发工具链包含多种二进制实用工具,如 ld 链接器、 gas 汇编器、 gcc 编译器、 ar 归档器以及 gcc 编译器集合,同时还涉及 glibc 或其他替代C库。此外,还会涉及获取和构建GDB源级符号调试器、Valgrind内存检查器以及Eclipse图形集成开发环境。
大多数工具链组件都属于GNU项目,可从FSF的FTP站点( ftp://ftp.gnu.org/gnu )或其镜像站点下载。 binutils 包位于 binutils 目录,GCC包位于 gcc 目录, glibc 包位于 glibc 目录。对于非GNU项目的组件,需了解其创建者及获取方式。
2. 构建GNU工具链简介
配置和构建合适的GNU工具链是一项复杂且精细的工作,需要深入理解不同软件包之间的依赖关系、各自的角色、软件包版本状态,并进行大量繁琐的工作。
3. 相关术语与GNU配置名称
在构建和使用跨工具链时,涉及以下几个系统相关术语:
- 构建系统(build) :用于构建工具链的系统。
- 宿主系统(host) :运行工具链的系统。
- 目标系统(target) :工具链为其生成二进制文件的系统。
对于标准的非嵌入式应用,这三者可能相同;但在大多数嵌入式场景中,构建系统和宿主系统通常是开发者的工作站,而目标系统则是正在开发应用的嵌入式开发板。
使用GNU配置和构建系统构建软件时,通过GNU配置文件中的名称指定构建、宿主和目标系统,这些名称遵循标准格式: cpu - manufacturer - kernel - os 。其中, kernel 组件是可选的,仅包含 cpu 、 manufacturer 和 os 的三元组合也很常见。各组件含义如下:
- cpu :系统的芯片架构,若存在大端和小端变体,小端变体通常在架构名称后加 el 表示。
- manufacturer :使用上述CPU的特定制造商或板卡系列,对工具链本身影响不大,常指定为未知机器类型或省略。
- kernel :主要用于GNU/Linux系统,有时为简洁起见会省略。
- os :系统使用的操作系统(或ABI)名称,也可用于描述无操作系统的嵌入式系统,此时表示目标文件格式,如Elf或COFF。
以下是一些可能的宿主、目标或构建三元组示例:
| 三元组 | 描述 |
| — | — |
| i386 - pc - linux - gnu | PC风格的x86 Linux系统 |
| powerpc - 8540 - linux - gnu | Freescale 8540 PowerQuickIII Linux系统 |
| mips - unknown - linux | 来自未指定制造商的大端MIPS Linux系统 |
| mipsel - linux | 来自未指定制造商的小端MIPS Linux系统 |
| xscale - unknown - linux | 来自未指定制造商的XScale(原StrongARM)Linux系统 |
通常,跨工具链组件名称会以目标三元组为前缀。例如,Freescale 8541 PowerQuickIII Linux系统的交叉编译器名为 powerpc - 8540 - linux - gnu - gcc ,小端MIPS Linux系统的链接器名为 mipsel - linux - ld 。
4. Linux内核头文件
构建工具链所需的第一个组件是Linux内核头文件。由于作为工具链一部分的C库是对原始Linux内核系统调用的封装,为应用程序员提供更友好的API,因此编译该库需要描述内核API的部分Linux内核头文件。
理论上,应使用与目标系统相同版本的Linux内核头文件构建工具链,但实际中,由于Linux内核的ABI很少变化,使用不同但相似版本的内核头文件也很常见。
在Linux 2.6系列之前,C库构建基于 include/asm - architecture 和 include/linux 目录中内核头文件的直接副本。但自Linux 2.6发布后,这种方式不再支持,因为内核头文件包含不适合用户空间应用的代码,可能会破坏用户程序(包括C库)的构建。现在,构建使用经过清理的Linux内核头文件,适用于C库等用户空间代码。从Linux内核2.6.23版本开始,内核源码提供了自动生成这种“清理”版本内核头文件的Make目标。对于早期版本,可使用 http://headers.cross - lfs.org 上的外部工具完成相同任务。
从内核源码目录执行以下命令,将 ppc 替换为你的架构, headers/ 替换为你希望安装清理后头文件的目录路径:
$ make ARCH=ppc headers_check
$ make ARCH=ppc INSTALL_HDR_PATH=headers/ headers_instal;
5. Binutils
工具链的另一个重要组件是 binutils 包,它包含用于操作二进制目标文件的常用实用工具,其中最重要的是GNU汇编器 as 和链接器 ld 。以下是 binutils 包中实用工具的完整列表:
| 工具 | 用途 |
| — | — |
| as | GNU汇编器 |
| ld | GNU链接器 |
| gasp | GNU汇编器预处理器 |
| ar | 创建和操作归档内容 |
| nmu | 列出目标文件中的符号 |
| objcopy | 复制和转换目标文件 |
| objdump | 显示目标文件内容信息 |
| ranlib | 生成归档内容的索引 |
| readelf | 显示ELF格式目标文件的信息 |
| size | 列出目标文件各部分的大小 |
| strings | 打印目标文件中的可打印字符串 |
| strip | 从目标文件中去除符号 |
| c++filt | 将重载C++函数产生的低级、混淆汇编标签转换为用户级名称 |
| addr2line | 将地址转换为原始源文件中的行号 |
需要注意的是, as 支持多种处理器架构,但不一定能识别与其他汇编器相同的语法,其识别的语法实际上是受BSD 4.2汇编语言启发的与机器无关的语法。
6. C库
当前Linux系统最常用的标准C库是GNU C库,通常缩写为 glibc 。 glibc 是一个可移植的高性能C库,支持所有相关标准(如ISO C 99、POSIX.1c、POSIX.1j、POSIX.1d、Unix98和Single Unix Specification),还支持国际化、复杂的名称解析、时区信息以及通过NSS(名称服务切换)和PAM(可插拔认证模块)架构进行认证。
glibc 开发项目的主网站为 http://www.gnu.org/software/libc ,包含开发源码树、错误数据库和许多资源链接。支持的所有平台列表可在 http://www.gnu.org/software/libc/ports.html 找到,库本身可从 http://ftp.gnu.org/gnu/glibc 的镜像站点下载。
对于近期的 glibc 版本,支持的架构分为核心维护者支持的架构(如x86、PowerPC、SPARC、SuperH及其64位版本,对嵌入式系统开发者来说最感兴趣)和 glibc 核心组外志愿者支持的架构(目前为Arm和MIPS)。后者的代码在单独的 glibc - ports 包中,可从同一位置下载。
glibc 是一个强大、完整且现代的C库,适用于许多系统设计。许多嵌入式Linux系统,特别是电信和网络市场领域的系统,都基于它构建。然而,由于它并非为嵌入式系统设计,对于资源有限的嵌入式Linux系统(如消费电子设备)开发者来说,其资源使用效率可能不太理想。
glibc 功能丰富但体积庞大,且模块化程度不高,移除功能繁琐甚至有时无法实现。此外, glibc 的设计和实现传统上更注重性能而非资源使用,例如为了提高运行速度而牺牲了RAM利用率。
最小化的 glibc 库文件可能占用多达2MB的存储空间,而且由于几乎每个应用都要根据C库头文件进行编译, glibc 还会影响应用可执行文件和其他库的大小。使用替代C库构建的可执行文件可能只有使用 glibc 构建的可执行文件大小的一半,但节省50%的情况很少见,差异范围很大,有时甚至与使用 glibc 构建的可执行文件大小相同。在应用运行时的RAM使用上也有类似但通常不太明显的影响。
一般来说,对于Flash存储空间为16MB或更大的项目, glibc 是一个不错的选择。如果项目需要更小的RAM,可考虑流行的嵌入式替代方案,如 uClibc 和 diet libc 。
构建新工具链时面临的第一个决策是选择使用哪个C库。由于C库既是工具链的一部分(作为编译器的一部分支持C++和其他语言,以及以库头文件的形式存在),也是运行时镜像的一部分(代码库本身以及为使用它而编译的分配代码),所以后续更改此决策会影响整个系统。
7. 线程库
线程是一种流行的现代编程技术,涉及在同一进程地址空间中运行多个独立的异步任务。在Linux 2.6系列之前,Linux内核对线程的支持非常有限。为填补这一空白,开发了几种不同的线程库,它们在用户空间实现了大部分所需支持,仅需内核提供少量协助。其中最常见的是 LinuxThreads 库,它实现了POSIX线程标准,直到Linux 2.5版本都作为 glibc 的附加组件分发。 LinuxThreads 是一个有价值的项目,但由于当时Linux内核对线程支持较弱,它存在可扩展性和标准兼容性方面的问题。例如, LinuxThreads 中的 getpid() 系统调用和信号处理不符合POSIX标准,这是由于内核的限制所致。
Linux 2.6系列发布时引入了新的线程实现——New POSIX Threading Library(NPTL)。NPTL依赖Linux内核的线程支持,其关键组件快速用户空间互斥锁(futex)提供了一个健壮的、符合POSIX标准的线程实现,可扩展到数千个线程。NPTL现在是受支持的Linux线程库,作为近期版本 glibc 的一部分分发。
对于使用近期内核版本和 glibc 版本的新项目,NPTL是首选的线程库。然而,由于所有Linux内核版本在2.6.16之前都存在影响线程库的bug,而且嵌入式系统开发者有时会基于较旧的内核和 glibc 版本构建系统(主要是由于供应商支持问题),所以 LinuxThreads 仍然是一个可行的选择,特别是当系统预计只使用非常有限的线程时。
由于 LinuxThreads 和NPTL至少大致符合POSIX标准,你也可以先使用 LinuxThreads ,然后迁移到NPTL。可以使用 confstr() 函数在运行时测试正在使用的线程库实现:
#define _XOPEN_SOURCE
#include <unistd.h>
#include <stdio.h>
int main(void)
{
char name[128];
confstr (_CS_GNU_LIBPTHREAD_VERSION, name, sizeof(name));
printf ("Pthreads lib is: %s\n", name);
return 0;
}
8. 组件版本
构建工具链的第一步是选择要使用的每个组件(GCC、 glibc 和 binutils )的版本。由于这些软件包是独立维护和发布的,并非所有版本组合都能正常构建。你可以尝试使用每个组件的最新版本,但这种组合也不能保证一定可行。
为选择合适的版本,需要针对宿主和目标系统测试特定的版本组合。你可能运气好找到之前测试过的组合,如果没有,就从每个软件包的最新稳定版本开始,如果构建失败,就依次尝试更旧的版本。
有时,版本号最高的版本可能没有经过足够的测试,不能被认为是“稳定”的。例如,当 glibc 2.3 发布时,在 2.3.1 版本可用之前,继续使用 glibc 2.2.5 可能是更好的选择。
在撰写本文时, binutils 的最新版本是2.18,GCC的最新版本是4.2.2, glibc 的最新版本是2.7。大多数情况下, binutils 能成功构建,无需更改版本。假设GCC 4.2.2即使提供了所有适当的配置标志也无法构建,那么就需要回退到GCC 4.2.1,如果仍然失败,就尝试4.2,依此类推。
但要注意,不能无限期地回退版本,因为最新的软件包版本期望其他软件包提供特定的功能。因此,如果后续的软件包构建失败,可能需要回退到之前成功构建的软件包的旧版本。例如,如果需要回退到 glibc 2.6.0 ,那么即使最新的GCC和 binutils 可能编译正常,也可能需要将GCC改为4.1, binutils 改为2.17。
此外,为使某些版本能为目标系统正确构建,通常需要应用补丁。可在为每个处理器架构提供的网站和邮件列表中找到这些补丁和软件包版本建议。
以下是在互联网上查找补丁和兼容版本组合的好地方:
- Debian Linux发行版源软件包(每个软件包包含该软件包支持的所有架构所需的补丁),可在 http://www.debian.org/distrib/packages 获取。
- Cross Compiled Linux From Scratch,在线地址为 http://cross - lfs.org/view/1.0.0 。
- CrossTool构建矩阵,地址为 http://www.kegel.com/crosstool/crosstool - 0.43/buildlogs 。
许多其他版本组合也可能可行,你可以尝试比这些网站上列出的更新的版本,采用前面讨论的方法,从最新版本开始,必要时逐个回退版本。最坏的情况下,你需要使用这些网站上描述的版本组合。
每当你发现一个能成功编译的新版本组合时,一定要测试生成的工具链,确保它确实可用。有些版本组合可能编译成功,但在使用时会失败。例如,使用GCC 2.95.3在x86宿主系统上为PPC目标系统构建 glibc 2.2.3 时,编译能成功,但生成的库是有问题的,在目标系统上使用时会导致核心转储。在这种情况下,回退到 glibc 2.2.1 可以获得可用的C库。
此外,有些版本组合在处理器家族中的某些处理器上能正常工作,但在同一家族的其他处理器上可能失败。例如,早于2.2版本的 glibc 在大多数PPC处理器上工作正常,但在MPC8xx系列处理器上会出现问题,因为 glibc 假设所有PPC处理器的缓存行为32字节,而MPC8xx系列处理器的缓存行为16字节。2.2版本的 glibc 通过假设所有PPC处理器的缓存行为16字节解决了这个问题。
9. 额外的构建要求
要构建跨平台开发工具链,你需要一个可用的本地工具链。大多数主流发行版将此工具链作为其软件包的一部分提供。如果你的工作站上没有安装,或者为了节省空间而选择不安装,那么此时需要根据你的发行版使用适当的程序进行安装。例如,对于Red Hat发行版,你需要安装适当的RPM软件包。
综上所述,构建GNU跨平台开发工具链是一个复杂的过程,需要仔细考虑各个组件的选择、版本的兼容性以及额外的构建要求。通过合理选择和测试,你可以构建出适合自己项目需求的工具链。
深入解析GNU跨平台开发工具链
10. 构建工具链的流程梳理
为了更清晰地呈现构建GNU跨平台开发工具链的过程,我们可以用一个mermaid流程图来展示:
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px
A([开始]):::startend --> B(选择组件版本):::process
B --> C{版本是否兼容?}:::decision
C -- 是 --> D(获取Linux内核头文件):::process
C -- 否 --> E(调整版本):::process
E --> B
D --> F(安装本地工具链):::process
F --> G(下载binutils、GCC、glibc等包):::process
G --> H(构建binutils):::process
H --> I(构建GCC):::process
I --> J(构建glibc):::process
J --> K(测试工具链):::process
K --> L{工具链是否可用?}:::decision
L -- 是 --> M([结束]):::startend
L -- 否 --> N(检查问题并调整):::process
N --> B
从这个流程图可以看出,构建工具链是一个循环迭代的过程,需要不断地检查和调整,以确保最终构建出可用的工具链。
11. 各组件在工具链中的作用分析
- Linux内核头文件
- 它是工具链构建的基础,为C库的编译提供了内核API的描述。如果内核头文件选择不当,可能会导致C库编译失败,进而影响整个工具链的构建。例如,在Linux 2.6版本前后,内核头文件的使用方式发生了变化,需要开发者根据不同的版本进行相应的处理。
- Binutils
binutils中的汇编器和链接器是将源代码转换为可执行文件的关键工具。as汇编器将汇编代码转换为目标文件,ld链接器将多个目标文件和库文件链接成一个可执行文件。其他工具如ar用于创建和管理归档文件,objdump用于查看目标文件的信息,这些工具在调试和优化程序时都非常有用。
- C库
- C库为应用程序提供了基本的函数和服务,如输入输出、内存管理、字符串处理等。
glibc功能强大,但资源占用较大;而像uClibc和diet libc等替代C库则更适合资源受限的嵌入式系统。选择合适的C库直接影响到应用程序的大小、性能和可移植性。
- C库为应用程序提供了基本的函数和服务,如输入输出、内存管理、字符串处理等。
- 线程库
- 线程库支持多线程编程,提高程序的并发性能。
LinuxThreads在早期Linux内核中发挥了重要作用,但由于内核支持的限制,存在一些兼容性问题;NPTL则利用了新内核的特性,提供了更强大和标准的线程支持。开发者需要根据项目使用的内核版本和线程需求来选择合适的线程库。
- 线程库支持多线程编程,提高程序的并发性能。
12. 版本选择与兼容性问题的解决策略
版本选择是构建工具链的关键环节,以下是一些解决版本兼容性问题的策略:
- 参考历史数据 :可以从Debian Linux发行版源软件包、Cross Compiled Linux From Scratch和CrossTool构建矩阵等网站获取经过测试的版本组合信息,以此作为初始版本选择的参考。
- 逐步回退 :如果最新版本组合无法构建成功,按照从高到低的顺序逐步回退版本。例如,先尝试最新稳定版本,如果失败则回退一个版本,直到找到能成功构建的版本组合。
- 补丁应用 :在一些情况下,通过应用特定的补丁可以解决版本兼容性问题。可以在各处理器架构的相关网站和邮件列表中查找合适的补丁。
- 全面测试 :每次找到一个看似可行的版本组合后,都要对生成的工具链进行全面测试,包括功能测试、性能测试等,确保工具链在实际使用中不会出现问题。
13. 构建工具链的常见错误及解决方案
在构建GNU跨平台开发工具链的过程中,可能会遇到各种错误,以下是一些常见错误及对应的解决方案:
| 常见错误 | 可能原因 | 解决方案 |
| — | — | — |
| 编译失败,提示缺少依赖库 | 本地工具链未安装完整或依赖库版本不兼容 | 安装或更新本地工具链,确保所有依赖库都已正确安装 |
| 链接错误,提示符号未定义 | 库文件路径配置错误或库版本不匹配 | 检查库文件路径配置,确保使用的库版本与工具链组件兼容 |
| 运行时出现核心转储 | 工具链组件版本不兼容或生成的库文件有问题 | 调整工具链组件版本,回退到之前能正常工作的版本,或者应用补丁修复问题 |
| 构建过程中提示内核头文件错误 | 内核头文件版本不匹配或未正确清理 | 使用与目标系统匹配的内核头文件版本,并按照正确的方法清理和安装内核头文件 |
14. 总结与建议
构建GNU跨平台开发工具链是一个复杂且具有挑战性的任务,需要开发者具备扎实的知识和丰富的经验。在整个过程中,要注意以下几点:
- 深入理解各组件 :了解每个组件的功能、作用和依赖关系,这样才能在遇到问题时准确判断并解决。
- 谨慎选择版本 :版本兼容性是构建工具链的关键,要通过参考历史数据、逐步回退和全面测试等方法选择合适的版本组合。
- 及时获取帮助 :当遇到困难时,可以利用互联网资源,如相关的网站、邮件列表和论坛,获取其他开发者的经验和帮助。
通过遵循以上原则和方法,开发者可以成功构建出适合自己项目需求的GNU跨平台开发工具链,为后续的软件开发工作奠定坚实的基础。
更多推荐
所有评论(0)