文章目录
-
- (一)工具链的基本概念
- (二)什么是交叉编译器
- (三)交叉编译工具链的组成与三台机器概念
- (四)系统定义的元组(Tuple)表示法
- (五)元组详细示例
- [(六)OS字段的两个重要值:none 和 linux](#(六)OS字段的两个重要值:none 和 linux)
- [(七)工具链 vs SDK](#(七)工具链 vs SDK)
- [(八)SDK目录结构对比(Buildroot SDK vs Linaro工具链)](#(八)SDK目录结构对比(Buildroot SDK vs Linaro工具链))
- [(九)Linux工具链大小 vs 自编译SDK大小](#(九)Linux工具链大小 vs 自编译SDK大小)
- (十)工具链文件数量对比
- (十一)如何获取交叉编译工具链
- [(十二)sysroot 的概念](#(十二)sysroot 的概念)
- [(十三)Buildroot 工具链的顶层目录结构](#(十三)Buildroot 工具链的顶层目录结构)
- [(十四)arm-buildroot-linux-gnueabihf/ 子目录详解](#(十四)arm-buildroot-linux-gnueabihf/ 子目录详解)
- (十五)目标板根文件系统与主机sysroot的对比
- [(十六)通过 readelf 确认程序是ARM架构](#(十六)通过 readelf 确认程序是ARM架构)
- 面试官提问环节(嵌入式Linux工具链与交叉编译)
-
- 第1问:什么是交叉编译器?为什么嵌入式开发离不开它?
- [第2问:你能解释一下工具链的"元组"命名方式吗?比如 `arm-unknown-linux-gnueabihf` 每个部分代表什么?](#第2问:你能解释一下工具链的“元组”命名方式吗?比如
arm-unknown-linux-gnueabihf每个部分代表什么?) - 第3问:工具链和SDK有什么区别?什么时候用工具链,什么时候用SDK?
- 第4问:什么是sysroot?为什么交叉编译时它很重要?
- 第5问:如何验证一个可执行文件是为ARM架构编译的?
- [第6问:`arm-linux-gnueabihf-gcc` 和 `arm-none-eabi-gcc` 有什么区别?分别在什么场景下使用?](#第6问:
arm-linux-gnueabihf-gcc和arm-none-eabi-gcc有什么区别?分别在什么场景下使用?) - 第7问:本地编译、交叉编译、以及"三台机器"(Build、Host、Target)你能画个图说清楚吗?
- [第8问:总结 - 领导考察你学习成果的一道综合题](#第8问:总结 - 领导考察你学习成果的一道综合题)
(一)工具链的基本概念
工具链是一组编程工具,用于开发软件、创建软件产品。工具链通常是另一个计算机程序或一组相关程序。通常,工具链里有多个工具,前一个工具的输出结果,是下一个工具的输入,也就是说前一个工具处理完,再交给下一个工具处理。
一个简单工具链可能由三部分组成:编译器和链接器(将源代码转换为可执行程序)、库(为操作系统提供接口)和调试器(用于测试和调试创建的程序)。
GNU工具链是一个广泛收集的、遵守GNU协议的、众多编程工具。这些工具形成一个工具链,用于开发应用程序和操作系统。GNU工具链在Linux、一些BSD系统和嵌入式系统软件的开发中起着至关重要的作用。
讲解:
你现在正在学习嵌入式Linux开发,第一个必须搞懂的概念就是"工具链"。想象一下,你要做一道复杂的菜,需要刀、砧板、锅、铲子、调料......这些厨具配合起来,才能把原材料变成一道菜。工具链就是软件开发中的"厨具组合"。
最简单的工具链包含三个环节:
- 编译器 :把你写的C/C++代码(人类能读懂)翻译成机器能执行的指令。比如你写了一个
hello.c,编译器把它变成hello.o(目标文件)。 - 链接器 :把多个目标文件和你用到的库(比如
printf函数所在的库)合并成一个完整的可执行文件。 - 调试器:当程序运行出问题时,你可以用调试器一步步观察程序的运行状态,找到错误。
这三个工具是"链式"工作的:源代码 → 编译器 → 目标文件 → 链接器 → 可执行文件 → 调试器(帮忙排查问题)。
在Linux世界中,最著名的就是 GNU工具链,它包括:
- gcc:C/C++编译器
- ld:链接器
- gdb:调试器
- binutils :一堆辅助工具,比如
objdump(查看目标文件内容)、strip(去掉调试信息减小文件体积)等
作为初学者,你只需要知道 :工具链就是你用来"造程序"的一套工具。在嵌入式Linux中,我们几乎天天跟GNU工具链打交道。后面你会亲手使用 arm-linux-gnueabihf-gcc 来编译你的第一个ARM程序,它就是GNU工具链的一个"变种"。
(二)什么是交叉编译器
交叉编译器:在平台A上使用它能够生成程序,这个程序是运行在平台B上的。例如,在PC上运行程序,但这个程序是在Android智能手机上运行的,这个编译器就是交叉编译器。
在PC上为其他平台(目标平台)编译代码时,需要交叉编译器。能否直接在目标平台上编译程序?比如在ARM板上编译程序?大多时候不可行,因为ARM板资源很可能受限。
交叉编译器的基本用途是将构建环境与目标环境分开。适用场景:
- 设备资源极其有限的嵌入式计算机(如微波炉里的微控制器)
- 为多个不同目标平台编译同一套代码
- 在强大的服务器上编译,然后部署到小设备
- 引导一个新平台(先交叉编译出一个基础系统)
讲解:
你已经知道什么是工具链了。现在要认识一个很重要的变种:交叉编译器。
普通编译,是在你的电脑上编译出能在你电脑上运行的程序。比如你在Ubuntu上用 gcc hello.c -o hello,生成的 hello 只能在x86 Linux上运行。
交叉编译,是在你的电脑上编译出能在另一个完全不同架构的机器 上运行的程序。比如你在x86电脑上用 arm-linux-gnueabihf-gcc hello.c -o hello_arm,生成的 hello_arm 拿到ARM开发板上才能运行。
为什么要这样做?
你手上的i.MX6ULL开发板,只有几百兆赫兹的CPU,几十到几百兆的内存,而且它跑的是Linux系统,但上面没有安装gcc编译器(即使安装了,编译一个稍微大点的程序也会慢得让你崩溃)。所以,我们利用强大的PC来完成编译工作,然后把编译好的程序拷贝到开发板上直接运行。
交叉编译器适用的典型场景:
- 资源受限的嵌入式设备:比如一个智能手环、一个路由器、一个微波炉控制板,它们的内存可能只有几兆字节,根本装不下编译器。
- 同一套代码编译出多个版本:公司可能做一个产品,同时支持ARM、MIPS、RISC-V三种架构的芯片,用交叉编译器就能轻松搞定。
- 服务器集中编译:在公司的编译服务器上(高性能),为所有开发者的测试板编译镜像。
初学者需要理解的核心 :交叉编译器是一个运行在"主机"(你的PC)上、但生成"目标机"(开发板)代码的特殊编译器。它的名字通常带有目标架构的前缀,比如 arm-linux-、riscv64-unknown-linux- 等。
(三)交叉编译工具链的组成与三台机器概念
- 它是一组工具,用来将源代码构建为可以运行在其他平台的二进制代码
- 不同的CPU架构
- 不同的ABI
- 不同的操作系统
- 不同的C库
- 三台机器参与构建过程
- Build(构建机器):使用GCC的源码,制作交叉编译工具链。
- Host(主机):使用交叉编译工具链,编译出程序。
- Target(目标机器):程序执行的地方。
- 本地工具链:build == host == target
- 交叉编译工具链:build == host != target
讲解:
帮你理清了"三台机器"的概念,这在交叉编译中非常重要。你不需要成为构建工具链的专家,但理解这三个角色能帮你读懂很多编译文档。
- Build机器:用来"制造"交叉编译工具链的那台电脑。通常,我们从网上下载别人已经做好的交叉编译工具链(比如从Linaro官网),所以你不必亲自做这一步。Build机器的架构通常是x86_64。
- Host机器 :运行 交叉编译工具链的电脑。也就是说,你敲
arm-linux-gcc命令的那台电脑。在绝大多数场景下,Build和Host是同一台电脑(你的PC)。 - Target机器:你编译出来的程序最终要运行的设备,也就是你的ARM开发板。
本地编译:如果 Build == Host == Target,那就是普通编译。比如你在自己的Ubuntu电脑上编译一个程序,然后在这台电脑上运行。
交叉编译:Build == Host,但 Target 不同。这就是我们嵌入式开发的常态。
还提到,交叉编译链需要处理不同的:
- CPU架构(ARM vs x86)
- ABI(应用程序二进制接口,决定了函数调用时参数如何传递、寄存器如何使用)
- 操作系统(Linux vs 裸机)
- C库(glibc、musl、uclibc等)
初学者要知道 :当你下载交叉编译工具链时,你会看到类似 arm-linux-gnueabihf 这样的名字,里面的"gnueabihf"就指定了ABI和C库类型。选错了会导致编译出来的程序在开发板上无法运行。
(四)系统定义的元组(Tuple)表示法
- autoconf 定义了system definitions的概念,表示为tuples(元组)
- 系统定义描述了一个系统:CPU架构、操作系统、芯片厂商、ABI、C库
- 定义方式:
<arch>-<vendor>-<os>-<libc/abi>(完整名称)<arch>-<os>-<libc/abi>
讲解:
当你接触不同的交叉编译工具链时,会看到很多类似 arm-unknown-linux-gnueabihf 这样的字符串。这是 系统元组(tuple),它用一套标准化的命名方式,精确描述了一个目标系统是什么。
一个完整的元组包含四个部分:
- arch(架构) :CPU类型,如
arm、aarch64、mips、i686。 - vendor(厂商) :通常填
unknown或者厂商名,比如buildroot、poky。autoconf 工具其实不太关心这个字段,但有些构建系统用它来做标识。 - os(操作系统) :
linux(表示Linux系统)、none(表示裸机,无操作系统)。 - libc/abi :C库和ABI的组合。常见的有
gnueabihf(使用glibc,且硬件浮点ABI)、uclibcgnueabi、musl等。
有时候会省略 vendor,变成 <arch>-<os>-<libc/abi>,比如 arm-linux-gnueabihf。
举个例子:
arm-buildroot-linux-gnueabihf:ARM架构,由Buildroot构建,目标操作系统是Linux,使用glibc库,硬件浮点ABI。arm-none-eabi:ARM架构,裸机(无操作系统),使用ARM的EABI(嵌入式ABI)。这种工具链主要用于编译BootLoader、裸机程序、RTOS。
初学者要记住 :当你拿到一个工具链,第一件事就是看它的元组字符串。你可以通过 gcc -dumpmachine 命令查看当前工具链的目标元组。这个字符串会出现在工具链中所有可执行文件的前缀上,比如 arm-buildroot-linux-gnueabihf-gcc。
(五)元组详细示例
<arch>-<vendor>-<os>-<libc/abi>
arm-foo-none-eabi:针对ARM架构的裸机工具链,来自供应商foo。arm-unknown-linux-gnueabihf:针对ARM架构的Linux工具链,来自未知供应商,使用EABIhf ABI和glibc C库。armeb-linux-uclibcgnueabi:针对ARM大端(big-endian)架构的Linux工具链,使用uClibc C库和EABI ABI。mips-img-linux-gnu:针对MIPS架构的Linux工具链,使用glibc C库,由Imagination Technologies提供。
讲解:
这给出了更具体的例子,帮助你理解元组中每个字段的实际取值。
arm-foo-none-eabi:这里foo是一个假设的供应商名,none表示没有操作系统,eabi是嵌入式ABI。这种工具链通常用于编译STM32这类单片机程序(裸机开发),不能用来编译Linux应用程序,因为缺少Linux系统调用支持。arm-unknown-linux-gnueabihf:这是我们最熟悉的类型。unknown表示供应商未知或无关紧要,linux表示目标操作系统是Linux,gnueabihf表示使用glibc库并且硬件浮点ABI(浮点参数通过浮点寄存器传递,效率更高)。如果你用的是i.MX6ULL开发板,很可能就是这种工具链。armeb-linux-uclibcgnueabi:armeb中的eb表示big-endian(大端模式),ARM默认是小端,但有些芯片支持大端。uclibc是一个更小巧的C库,常用于资源非常紧张的嵌入式设备。gnueabi表示使用标准的嵌入式ABI(没有hf后缀,可能是软浮点或兼容模式)。mips-img-linux-gnu:img是Imagination Technologies的缩写,他们生产MIPS架构的CPU。gnu表示使用glibc库,没有特别指定ABI细节。
初学者的关键 :你在为开发板选择工具链时,必须确保元组中的架构、操作系统、ABI与你的板子完全匹配。如果你的板子系统是Linux,就不能用 none 的工具链;如果你的板子支持硬件浮点,最好用 hf 版本,否则性能会差很多。
(六)OS字段的两个重要值:none 和 linux
- none 用于 bare-metal toolchains(裸机工具链)
- 用于没有操作系统的开发。
- 使用的C库一般是newlib。
- 提供不需要操作系统的C库服务。
- 可用于构建bootloader程序或Linux kernel,不能用于构建Linux用户空间代码。
- linux for Linux toolchains(linux工具链)
- 用于Linux操作系统开发。
- 选择特定于Linux的C库:glibc、uclibc、musl。
- 支持Linux系统调用。
- 可用于构建Linux用户空间代码,也可用于构建引导加载程序或内核本身等裸机代码。
讲解:
元组中的 os 字段决定了这个工具链生成的代码将与什么操作系统交互。两个最重要的值是 none 和 linux。
裸机工具链(os = none):
- 它假设目标系统上没有操作系统。你编写的程序直接控制硬件,没有"系统调用"的概念。
- 使用的C库通常是 newlib ,一个专门为嵌入式裸机环境设计的轻量级C库。newlib 提供标准的C函数(如
printf、malloc),但这些函数的底层实现需要你自己提供(比如往串口写一个字符的代码)。 - 这种工具链可以用来编译 BootLoader (如U-Boot)和 Linux内核 ,因为这两者本质上是不依赖操作系统的裸机程序。但是,它不能 用来编译Linux用户空间的应用程序(比如一个普通的
hello.c),因为用户空间程序需要通过系统调用与内核交互,裸机工具链不知道什么是系统调用。
Linux工具链(os = linux):
- 它假设目标系统上运行着Linux内核。编译出的程序通过"系统调用"请求内核提供服务(如读写文件、网络通信)。
- C库可以是 glibc (最完整、体积较大)、uclibc (轻量级)、musl(现代轻量级)。
- 这种工具链不仅能编译用户空间程序,也能编译BootLoader和内核(因为内核和BootLoader其实不需要C库的系统调用部分)。所以在很多实践中,开发者会直接用Linux工具链来编译一切。
初学者怎么选:
- 如果你只是做嵌入式Linux应用开发(写业务逻辑代码),一定要选
linux工具链。 - 如果你在做裸机编程或者学习写BootLoader,可以用
none工具链(但很多教程也直接用Linux工具链,只要避开C库的系统调用依赖即可)。
(七)工具链 vs SDK
- 工具链(cross compilation):只有编译器、binutils 和 C 库。
- SDK(software development kit):一个工具链,加上一些(可能很大)为目标架构构建的库头文件,以及在构建软件时有用的其他本地工具。
- OpenEmbedded 或 Yocto、Buildroot等构建系统通常可以:
- 使用现有工具链作为输入,或构建自己的工具链
- 除了生成根文件系统之外,他们还可以生成SDK以允许应用程序开发人员为目标构建应用程序/库。
讲解:
你可能会听到两个词:工具链 和 SDK。它们是有关联但不同的东西。
工具链(最小集合):
- 包含:交叉编译器(gcc)、汇编器(as)、链接器(ld)、库归档工具(ar)、调试器(gdb)等。
- 包含:C库(如glibc)的二进制和头文件。
- 通常工具链的压缩包只有几十到几百兆字节。
SDK(更完整的开发包):
- 在工具链的基础上,额外添加了你目标平台上常用的第三方库的头文件和二进制文件。比如,你的应用程序需要用libcurl(网络请求库)、libjson-c(解析JSON),SDK里会预先为你编译好这些库,并放上头文件。
- 还会包含一些辅助工具(比如模拟器、性能分析工具)。
- SDK的规模通常大得多,可能几个GB。
构建系统的作用:
- Buildroot / Yocto 这类工具可以帮你从源码编译出一个完整的根文件系统。同时,它们也可以"吐出"一个 SDK:你把SDK安装到PC上,然后就可以用它来开发应用程序,而不需要重新搭建整个构建环境。
初学者的理解:如果你只是自己学习,从Linaro下载一个工具链就足够编译内核和简单程序了。如果你在公司做产品,通常会使用Yocto生成的SDK,因为它包含了所有板级定制的库,确保应用程序与系统固件完全兼容。
(八)SDK目录结构对比(Buildroot SDK vs Linaro工具链)
上半部分是arm-buildroot-linux-gnueabihf_sdk-buildroot目录,包含bin/、etc/、include/、lib/、libexec/、man/、sbin/、share/、usr/等。下半部分是
gcc-linaro-6.2.1-2016.11-x86_64_arm-linux-gnueabihf目录,包含arm-linux-gnueabihf/、bin/、include/、lib/、libexec/、share/等。
讲解:
这张图直观展示了两种不同的工具链/SDK的目录布局。左边的 arm-buildroot-linux-gnueabihf_sdk-buildroot 是一个完整的SDK,右边的 gcc-linaro-... 是一个相对精简的工具链。
Linaro工具链(下面) 的结构:
bin/:存放arm-linux-gnueabihf-gcc、arm-linux-gnueabihf-ld等可执行文件。arm-linux-gnueabihf/:这是一个子目录,里面包含目标架构的库和头文件(相当于sysroot)。lib/、libexec/:存放工具链自身运行时需要的库(例如libgcc_s.so)。include/:一些通用头文件。share/:文档、man手册等。
Buildroot SDK(上面) 的结构:
- 多了
etc/、sbin/、usr/、man/、mkspecs/等目录。 etc/可能包含一些配置文件。sbin/是系统管理工具。usr/下面通常包含更多开发和运行所需的文件。- 整体看起来更像一个"小型的根文件系统"的一部分。
为什么有这些差异:
- Linaro工具链的目标是提供一个纯净的、可移植的交叉编译环境,它只包含编译必需的最小集合。
- Buildroot SDK 的目标是让应用开发者拿到后,能够直接使用与目标板上完全一致的库和头文件进行开发,减少"编译时版本A,运行时版本B"的问题。所以它会包含更多内容。
初学者不必纠结目录细节,你只需要知道:
- 工具链的
bin目录要加到你的PATH环境变量中。 - 很多编译脚本需要知道
sysroot的位置(可以通过arm-linux-gnueabihf-gcc -print-sysroot查看)。 - SDK 通常还会提供一个
relocate-sdk.sh脚本,用于在你把SDK移动到不同路径后,修正内部的绝对路径引用。
(九)Linux工具链大小 vs 自编译SDK大小
Linaro工具链(gcc-linaro-6.2.1-2016.11-x86_64_arm-linux-gnueabihf)总大小:522M
- share: 69M
- git: 932K
- lib: 33M
- bin: 39M
- libexec: 70M
- include: 304K
- arm-linux-gnueabihf: 311M
自编译SDK(arm-buildroot-linux-gnueabihf_sdk-buildroot)总大小:1.7G
- share: 66M
- arm-buildroot-linux-gnueabihf: 742M
- man: 80K
- lib: 607M
- bin: 159M
- sbin: 3.7M
- mkspecs: 3.3M
- etc: 108K
- libexec: 82M
- include: 46M
讲解:
实际数字告诉你:SDK比单纯工具链大得多。Linaro工具链只有522MB,而Buildroot生成的SDK达到了1.7GB,是前者的3倍多。
为什么SDK这么大?
- 更多的库:SDK里不仅包含C库,还包含了很多为ARM编译好的第三方库(例如OpenSSL、libz、libpng等)。这些库在Linaro工具链里是没有的,你需要自己交叉编译。但SDK已经替你做好了。
- 调试符号和静态库:SDK为了便于调试,往往保留了符号信息和静态库版本(.a文件),这些文件体积较大。
- 头文件更多:包含了所有这些库的头文件。
- 目标系统的根文件系统模拟 :
arm-buildroot-linux-gnueabihf/目录通常是一个完整的sysroot,里面有usr/lib、usr/include等,结构类似于目标板的根文件系统,但它不是用来运行的,而是给编译器和链接器看的。
对初学者的影响:
- 如果你只是学习编译U-Boot、Linux内核和简单的C程序,522MB的Linaro工具链完全足够,还节省硬盘空间。
- 如果你需要开发复杂的应用程序,依赖很多第三方库(比如使用Qt、OpenCV、gRPC等),使用Buildroot/Yocto生成的SDK能节省大量时间(不必自己逐个交叉编译这些库)。
- 注意:SDK的1.7G是压缩前的大小,实际部署在电脑上可能还要更大一些。
(十)工具链文件数量对比
Linaro工具链文件个数:
ls -lR | grep ".*" | wc -l→ 7697Buildroot SDK文件个数:
ls -lR | grep ".*" | wc -l→ 41141
讲解:
除了总大小,文件数量也是一个很有趣的指标。Linaro工具链只有大约7700个文件,而Buildroot SDK有41141个文件,是前者的5倍多。
为什么文件数量差这么多?
- SDK包含了大量头文件(.h),每个头文件虽然小,但数量非常多。因为每个库都会导出多个头文件。
- SDK可能还包含了大量的符号链接(例如
libc.so指向libc.so.6)。 - SDK的
bin/目录下除了交叉编译工具本身,还可能有很多辅助脚本。 - 另外,Buildroot SDK可能包含了多个版本的库(比如调试版本、发布版本、静态库、动态库),每个版本都是一组文件。
初学者的实际感受:
- 当你解压Linaro工具链时,几乎是瞬间完成。
- 解压Buildroot SDK时,时间明显更长,并且你会看到大量文件名在屏幕上滚动。
- 在文件系统中搜索或复制SDK也会更慢。
结论:SDK的强大功能是以空间和文件数量为代价的。在有限的虚拟机磁盘或公司服务器上,这种差异可能要纳入考虑。但一般情况下,现代硬盘足够大,你不用担心。
(十一)如何获取交叉编译工具链
- 获取现成的
- 来自发行版系统内:Ubuntu和Debian有许多现成的交叉编译器。
- 来自不同组织:芯片原厂、Bootlin、ARM官方、Linaro、gnutoolchains、riscv-collab等。
- 自己编译构建
- Crosstool-NG
- 嵌入式Linux构建系统:Yocto、Buildroot、OpenWRT等。
- 参考文档
讲解:
你不必自己从头构建交叉编译工具链(那是一个非常复杂的过程)。通常有两条路:获取现成的 或 让构建系统自动生成。
获取现成工具链(推荐初学者):
- Ubuntu/Debian 软件源 :直接
sudo apt install gcc-arm-linux-gnueabihf。但版本通常较旧,适合快速测试。 - Linaro:提供高质量的ARM和AArch64工具链,版本新,稳定。很多开发板厂商都基于Linaro。
- ARM官方:ARM公司也提供自己的GNU工具链(以前叫ARM DS-5,现在有免费版本)。
- Bootlin:提供多种架构(ARM、RISC-V、MIPS等)的预编译工具链,非常方便。
- 芯片原厂:比如NXP的BSP包里会自带一个工具链,与他们的内核版本匹配度最好。
自己构建工具链:
- Crosstool-NG:一个专门用来构建交叉工具链的工具,有菜单配置界面,像配置内核一样。如果你有特殊需求(比如需要非常古老的glibc版本),可以用它。
- Buildroot / Yocto:这些构建系统在构建整个嵌入式Linux系统时,会自动构建所需的交叉工具链。你不需要单独操作,一切自动化。
初学者建议:
- 如果你跟着教程走,教程会明确告诉你下载哪个具体的工具链(比如Linaro 6.2.1)。直接按教程来。
- 如果你想自己尝试,优先从Linaro或者Bootlin官网下载,避免使用Ubuntu源的旧版本。
(十二)sysroot 的概念
- sysroot 是头文件和库的逻辑根目录。
- gcc 寻找头文件,ld 寻找库的地方。
- gcc 和 binutils 都是使用 --with-sysroot= 参数构建的。
- 内核头文件和C库安装在 。
- 如果工具链已经移动到不同的位置,如果它在 --prefix 的子目录中,gcc 仍然会找到它的 sysroot,但是需要在编译时指定如下参数:
--with-sysroot=。- 可以在运行时使用 gcc 的 --sysroot 选项覆盖。
- 可以使用
print-sysroot选项打印当前的 sysroot。
讲解:
sysroot 是交叉编译中非常重要的概念。简单来说,sysroot 就是一个"假的根目录",里面存放了目标系统需要的头文件和库文件。
当你的交叉编译器要编译一个程序时,它需要知道:
- 从哪里找
stdio.h等标准头文件。 - 从哪里找
libc.so等标准库。
在本地编译时,编译器默认去 /usr/include 和 /usr/lib 找。但在交叉编译时,你不能 让它去你PC的 /usr/include,因为那里是x86的头文件,不是ARM的。所以你必须给它指定一个 sysroot,这个sysroot里放着ARM版本的头文件和库。
典型的 sysroot 目录结构如下(来自Buildroot SDK):
text
sysroot/
├── bin/
├── dev/
├── etc/
├── lib/ ← ARM版本的C库和动态链接器(如 ld-linux-armhf.so.3)
├── usr/
│ ├── include/ ← ARM版本的头文件(Linux内核头文件、C库头文件等)
│ └── lib/ ← ARM版本的动态库(libc.so, libm.so等)
└── ...
如何得知工具链的 sysroot 路径?
执行:
bash
arm-buildroot-linux-gnueabihf-gcc -print-sysroot
它会输出类似 /home/user/sdk/sysroot 的路径。
编译时临时修改 sysroot:
如果你有多个sysroot,可以在编译时加参数:
bash
arm-linux-gcc --sysroot=/path/to/another/sysroot -o myapp myapp.c
初学者常见错误 :编译时遇到"找不到头文件"或"链接错误",很可能是 sysroot 设置不正确,或者工具链没有正确打包sysroot。这时候可以手动指定 --sysroot 或检查 -print-sysroot 的输出。
(十三)Buildroot 工具链的顶层目录结构
Buildroot生成的交叉编译工具链顶层目录列表:
- arm-buildroot-linux-gnueabihf/
- bin/
- include/
- lib/
- libexec/
- share/
讲解:
当你用Buildroot生成工具链后(或者下载了Buildroot SDK),解压后会看到这些顶层目录。我们用一张表格来说明每个目录的作用:
| 目录 | 作用 |
|---|---|
bin/ |
存放用户直接调用的工具,比如 arm-buildroot-linux-gnueabihf-gcc、arm-buildroot-linux-gnueabihf-ld 等。你应该把这个目录加入PATH。 |
arm-buildroot-linux-gnueabihf/ |
这是一个非常重要的子目录。它里面通常包含 sysroot/、lib/、bin/(有限的一些工具)、include/(C++头文件)等。主要是目标架构相关的文件。 |
include/ |
通常是供主机(host)使用的头文件,比较少用。 |
lib/ |
存放主机上的库文件,比如 libstdc++.so(供主机上的gcc运行使用),不是目标板的库。 |
libexec/ |
存放gcc的内部可执行文件(如 cc1),普通用户很少直接接触。 |
share/ |
存放文档、man手册、gcc 的配置数据等。 |
初学者注意:
- 你99%的时间会待在
bin/目录调用编译工具。 - 当你需要查看目标板的头文件时,可以进入
arm-buildroot-linux-gnueabihf/sysroot/usr/include。 - 不要随意修改这些目录的结构,否则工具链可能失效。如果必须移动整个工具链的位置,记得运行SDK自带的
relocate-sdk.sh脚本(如果有)。
(十四)arm-buildroot-linux-gnueabihf/ 子目录详解
- arm-buildroot-linux-gnueabihf/
- bin/(有限的 binutils 程序集,没有交叉编译前缀。带有前缀的硬链接。这是gcc找到它们的地方)
- include/c++/7.5.0/(由gcc安装的C++标准库的头文件,不是sysroot的一部分)
- lib/(为目标构建的gcc运行时库:libatomic、libgcc、libitm、libstdc++、libsupc++)
- sysroot/(包含lib/、usr/lib/:C库和gcc运行时库;usr/include/:Linux内核和C库头文件)
讲解:
这个是Buildroot工具链中最核心的子目录。我们拆开看:
1. bin/ 目录 :
里面有一些工具,比如 as、ld、ar 等,但没有 前缀 arm-buildroot-linux-gnueabihf-。实际上,上层 bin/ 目录中的带前缀工具,很多是硬链接到这里的无前缀工具吗?不对------更准确地说,gcc在运行时需要调用 as、ld 等,它会在工具链内部目录中寻找。这些无前缀的工具就是为gcc内部调用准备的。用户不需要直接使用它们。
2. include/c++/7.5.0/ :
这是C++标准库的头文件(比如 <iostream>、<vector>)。注意,它们并不是放在 sysroot/usr/include 下面的。因为C++标准库是gcc的一部分,不是操作系统C库的一部分。编译器会优先在这里找C++头文件。
3. lib/ :
存放gcc运行时库,这些库是编译器在编译时需要链接到最终程序中的(或者在运行时被动态加载)。常见的有:
libgcc.a/libgcc_s.so:提供了一些编译器内部函数(比如64位整数的除法、异常处理)。libatomic.a:当目标架构不原生支持某些原子操作时,提供软件实现的原子函数。libstdc++.a/libstdc++.so:C++标准库的实现。libsupc++.a:C++语言支持库(不包含标准库容器等)。
4. sysroot/ :
这才是真正的sysroot根目录。它的内部结构:
usr/include/:包含Linux内核头文件和C库头文件(如stdio.h、unistd.h)。lib/和usr/lib/:包含C库(如libc.so、libm.so)、动态链接器(ld-linux-armhf.so.3),以及一些其他的库(如libpthread)。
初学者的关键理解 :当你使用 -lstdc++ 链接时,链接器会去 sysroot/usr/lib 找 libstdc++.so,但也可能去 ../lib 找同名库。这取决于构建配置。平时你不需要关心这个细节,但了解后可以帮你排查"未定义的引用"这类链接错误。
(十五)目标板根文件系统与主机sysroot的对比
Target(目标单板)路径:/
包含:bin、boot、dev、etc、lib、linuxrc、media、mnt、proc、root、run、sbin、selinux、sys、tmp、usr、var
Host(主机)路径:arm-buildroot-linux-gnueabihf/sysroot
包含:bin、dev、etc、lib、lib32 -> lib、media、mnt、opt、proc、root、run、sbin、sys、tmp、usr、var
讲解:
这张图对比了两个不同的"根目录":一个是开发板上的根文件系统 (Target),另一个是主机上工具链的 sysroot(Host)。
为什么它们如此相似?
因为 sysroot 本质上就是模拟了目标板的根文件系统的一部分,尤其是 /usr/include 和 /usr/lib。当编译器编译程序时,它需要"假装"自己运行在目标板上,所以 sysroot 提供了与目标板一致的目录布局。
但是,它们不是完全相同的:
- 目标板的根文件系统是用来运行 的,包含完整的可执行程序(
/bin/sh、/sbin/init)、配置文件(/etc/passwd)、设备节点(/dev/ttyS0)、以及应用程序。 - sysroot 是用来编译 的,它只包含编译和链接所必需的头文件和库文件。它通常不包含可执行的命令(比如没有
/bin/ls),不包含设备节点,也不需要/proc、/sys这些挂载点。
相似的目录:
lib/:目标板上有动态库和动态链接器;sysroot里也有相同的库文件。usr/lib/、usr/include/:类似。etc/:sysroot里的etc可能非常精简,只有ld.so.conf等少数文件。
不同的目录:
boot/:sysroot里通常没有,因为内核镜像不需要参与编译。dev/、proc/、sys/、tmp/、run/等:sysroot里是空目录或占位符,因为编译用不到它们。
初学者的经验 :当你编写CMake或Makefile时,经常需要指定 CMAKE_FIND_ROOT_PATH 为 <sysroot>/usr,让编译工具只在sysroot内查找头文件和库,而不是去宿主机的 /usr 里找。如果不小心链接到了宿主机的x86库,会产生"架构不兼容"的错误。
(十六)通过 readelf 确认程序是ARM架构
(第一次):
readelf -a ./curl的部分输出:
- Class: ELF32
- Data: 2's complement, little endian
- Machine: ARM
- Flags: 0x5000400, Version5 EABI, hard-float ABI
(第二次):
类似输出,但开始指针地址、节头表偏移等有细微差异,但
Machine: ARM和Flags: hard-float ABI一致。
讲解:
readelf 是一个非常有用的工具(属于binutils),用来显示ELF格式文件(可执行文件、目标文件、库文件)的详细信息。这里对 curl 程序运行 readelf -a,查看它的ELF头。
关键字段解析:
- Class: ELF32:说明这是一个32位的程序。ARM Cortex-A7是32位,所以这里显示ELF32。如果是64位的ARM(如Cortex-A53),会显示ELF64。
- Data: 2's complement, little endian:小端模式。ARM默认小端,这与x86相同。大端设备很少见。
- Machine: ARM :明确告诉你这个可执行文件是为ARM架构编译的。如果它显示
Machine: Intel 80386,说明是x86程序,放开发板上跑不了。 - Flags: 0x5000400, Version5 EABI, hard-float ABI :这一行很重要。
Version5 EABI:使用ARM的第五版嵌入式ABI(EABI)。这是ARM Linux的标准ABI。hard-float ABI:表示浮点参数通过浮点寄存器传递(硬件浮点)。如果你的工具链使用gnueabihf(hf后缀),就会产生这个标记。如果你的工具链是gnueabi(软浮点),这里可能显示软浮点或VFP。
如何使用 readelf 验证你的程序:
当你用交叉编译器编译出一个程序,传到开发板上运行之前,先用 readelf -h <程序名>(或 -a)检查一下它的 Machine 字段。确保是 ARM 而不是 x86。如果错了,说明你用了错误的编译器。
示例:
bash
arm-linux-gnueabihf-gcc -o myapp myapp.c
readelf -h myapp | grep Machine
# 输出: Machine: ARM
初学者的意义 :这是排查"Segmentation fault"或者其他运行错误的第一步。很多初学者在PC上编译完程序就拷贝到开发板,然后报错找不到文件或无法执行,就是因为架构不对。用 readelf 看一眼,问题一目了然。
总结 :此文档带你完整走过了"工具链与交叉编译"的知识点,从最基础的工具链定义,到交叉编译原理,再到元组命名、sysroot、SDK与工具链的区别,最后用 readelf 验证。这些是嵌入式Linux开发者每天都要面对的概念和工具。作为初学者,你不用一次记住所有细节,但至少要理解:
- 交叉编译是"在PC上编译ARM程序"。
- 工具链的名字(元组)决定了目标平台。
- sysroot 是编译时的"虚拟根目录"。
- 用
readelf确认程序架构是最实用的技能之一。
面试官提问环节(嵌入式Linux工具链与交叉编译)
第1问:什么是交叉编译器?为什么嵌入式开发离不开它?
面试官:你以前做PC软件开发,现在转嵌入式Linux。请你解释一下,什么是交叉编译器?为什么我们不在开发板上直接编译程序?
你的回答:
交叉编译器是指在一个平台上(比如我的x86 PC)运行,却能生成另一个平台(比如ARM开发板)上可执行代码的编译器。例如,我在PC上用 arm-linux-gnueabihf-gcc 编译 hello.c,得到的可执行文件只能在ARM Linux上运行,不能在PC上运行。
嵌入式开发离不开它,主要有两个原因:
- 资源限制:开发板的CPU主频低(几百MHz)、内存小(可能只有128MB)、存储空间有限(几百MB)。如果直接在开发板上编译,一个稍微复杂的程序可能要花几个小时,而且很可能内存不足。PC的CPU强劲、内存几十GB,编译速度快得多。
- 运行环境不完整:大多数嵌入式Linux开发板并没有安装完整的编译工具链(GCC、binutils等)。即使安装,也要占用大量存储空间,而且配置复杂。
所以,标准的做法是:PC作为编译机 ,开发板作为运行机,用交叉编译器连接这两个世界。
第2问:你能解释一下工具链的"元组"命名方式吗?比如 arm-unknown-linux-gnueabihf 每个部分代表什么?
面试官 :你在选择交叉编译工具链时,会看到很多类似 arm-unknown-linux-gnueabihf 的字符串。请给我拆解一下,每个字段的含义是什么?
你的回答:
这个字符串叫做"系统元组"(System Tuple),用来精确描述一个目标系统。格式是:<架构>-<厂商>-<操作系统>-<C库/ABI>。
拿 arm-unknown-linux-gnueabihf 举例:
- arm :CPU架构,这里是32位ARM。其他常见的有
aarch64(64位ARM)、mips、riscv64等。 - unknown :厂商字段,通常情况下填
unknown或buildroot、poky。这个字段对编译过程没有实际影响,可以忽略。 - linux :操作系统,表示这个工具链生成的代码要在Linux下运行。如果是
none,表示裸机(无操作系统)。 - gnueabihf :C库和ABI的组合。
gnu表示使用GNU C库(glibc);eabihf表示嵌入式应用程序二进制接口(EABI),且使用硬件浮点(Hard Float)------浮点参数通过浮点寄存器传递,效率高。如果是gnueabi(没有hf),一般使用软浮点或VFP的兼容模式。
理解元组后,当我看到 arm-none-eabi 就知道这是裸机工具链(可以用来编译U-Boot、内核,但不能编译Linux应用程序);看到 arm-linux-gnueabihf 就知道这是Linux应用程序开发需要的工具链。
第3问:工具链和SDK有什么区别?什么时候用工具链,什么时候用SDK?
面试官:请你区分一下"交叉编译工具链"和"SDK"这两个概念。在实际开发中,我该选用哪个?
你的回答:
交叉编译工具链是最小集合,只包含:
- 编译器(gcc)
- 汇编器、链接器(as、ld)
- 调试器(gdb)
- 必要的C库头文件和二进制(glibc等)
- 一些辅助工具(ar、objcopy、strip等)
通常体积在几百MB,文件数量几千到一万。
SDK(软件开发套件) 是在工具链的基础上,额外增加了:
- 为目标系统预先编译好的第三方库(比如 OpenSSL、libcurl、Qt、OpenCV 的头文件和动态库/静态库)
- 可能还包含示例代码、交叉编译的配置脚本、甚至一个模拟器
体积常常达到几个GB,文件数量几万到十几万。
选择建议:
- 只用工具链:如果你只需要编译U-Boot、Linux内核,或者写一个完全不依赖第三方库的小型C程序,工具链就足够了。例如学习阶段,用Linaro的工具链完全没问题。
- 使用SDK:如果你的应用程序依赖很多复杂的第三方库(比如需要HTTPS请求的libcurl、需要解析JSON的libjson-c、需要图形界面的Qt),使用Buildroot或Yocto生成的SDK能节省大量自己交叉编译这些库的时间,并且保证库版本与目标系统完全一致。
在公司产品开发中,一般都会使用SDK,因为产品往往依赖大量库。
第4问:什么是sysroot?为什么交叉编译时它很重要?
面试官:我在用交叉编译器编译程序时,偶尔会碰到"找不到头文件"的错误,后来有人告诉我要检查sysroot。你能解释一下sysroot是什么,以及它如何帮助找到头文件和库吗?
你的回答:
sysroot 是一个"虚拟根目录",里面存放了目标系统 的头文件和库文件。交叉编译器默认会到这个目录下查找 usr/include、usr/lib 等。
在本地编译时(比如在PC上直接gcc),编译器默认去宿主机的 /usr/include 找头文件,去 /usr/lib 找库。但交叉编译时必须阻止这种行为,因为宿主机的头文件和库是x86架构的,不能用于ARM。所以我们需要把ARM版本的头文件和库放在一个独立的目录里(sysroot),然后告诉编译器去那里找。
典型的sysroot目录结构:
text
<工具链路径>/arm-unknown-linux-gnueabihf/sysroot/
├── usr/
│ ├── include/ ← ARM版头文件(stdio.h, unistd.h, 内核头文件等)
│ └── lib/ ← ARM版动态库(libc.so, libm.so, ld-linux-armhf.so.3)
└── lib/ ← 也可能直接放部分库
如何查看当前工具链的sysroot路径?
bash
arm-linux-gnueabihf-gcc -print-sysroot
为什么重要? 如果sysroot设置不正确,编译器会去错误的地方找头文件,导致"文件不存在"或"架构不匹配"的错误。有些工具链打包不完整,缺少sysroot,就需要手动用 --sysroot 参数指定。
第5问:如何验证一个可执行文件是为ARM架构编译的?
面试官:你把一个程序拷贝到开发板上,运行时报错"cannot execute binary file: Exec format error"。你怀疑是架构编译错了。请问你应该用什么命令来确认这个程序的架构?
你的回答:
使用 readelf 命令,具体是 readelf -h <程序名> 或 readelf -a <程序名>。查看输出中的 Machine 字段。
例如:
bash
readelf -h myapp | grep Machine
如果输出 Machine: ARM,说明是ARM架构,开发板能运行;如果输出 Machine: Intel 80386 或 Machine: x86_64,说明是x86架构,放ARM板上当然无法运行。
另外,还可以看 Flags 字段,例如 hard-float ABI 表示使用了硬件浮点,你的板子必须支持硬件浮点(开发板一般都支持)。如果显示 soft-float,也能运行但效率低。
readelf 是调试交叉编译问题的最基本工具。我每次编译完一个重要的程序,都会先用它确认一下架构,避免浪费时间排查错误。
第6问:arm-linux-gnueabihf-gcc 和 arm-none-eabi-gcc 有什么区别?分别在什么场景下使用?
面试官 :我注意到两种常见的工具链:arm-linux-gnueabihf-gcc 和 arm-none-eabi-gcc。它们有什么区别?我什么时候该用哪个?
你的回答:
它们的核心区别在于 目标操作系统 和 C库。
| 工具链 | 元组中OS字段 | C库 | 适用场景 |
|---|---|---|---|
arm-linux-gnueabihf-gcc |
linux |
glibc(或uclibc/musl) | 编译 Linux用户空间程序、内核、BootLoader(可选) |
arm-none-eabi-gcc |
none(裸机) |
newlib(轻量级,无系统调用) | 裸机程序、RTOS、BootLoader、单片机(STM32) |
具体解释:
arm-linux-gnueabihf:假设目标系统运行Linux,因此编译出的程序可以通过 系统调用(svc指令) 请求内核服务。C库(glibc)会封装这些系统调用。你写的printf最终会调用write系统调用把数据写到标准输出。这个工具链也可以编译BootLoader和内核,因为内核和BootLoader不使用C库的系统调用部分(它们直接操作硬件)。arm-none-eabi:假设没有操作系统。你无法使用open、read、write这类系统调用函数。C库(newlib)只提供纯计算类函数(如memcpy、strlen),底层硬件操作需要你自己实现(例如写一个_write函数来输出字符)。这个工具链通常用于编译Cortex-M系列单片机的代码,或者U-Boot的某些阶段。
实际开发选择:
- 做嵌入式Linux应用程序开发 → 必须用
linux工具链。 - 编译U-Boot或Linux内核 → 既可以用
linux工具链,也可以用none工具链,但一般用linux更方便。 - 学习裸机编程(比如嘉立创的STM32教程) → 用
none-eabi。
第7问:本地编译、交叉编译、以及"三台机器"(Build、Host、Target)你能画个图说清楚吗?
面试官:请你用一个具体的例子,说明什么是Build机器、Host机器、Target机器。本地编译和交叉编译分别对应什么关系?
你的回答:
"三台机器"定义:
- Build机器 :用来 构建 工具链本身的机器(比如,从GCC源码编译出交叉编译器的那台电脑)。
- Host机器 :运行 交叉编译器的机器(你在哪台电脑上敲
arm-linux-gcc命令)。 - Target机器:编译出来的程序最终要运行的设备。
实际场景(以我们学习为例):
- 我从Linaro官网下载了一个预编译的交叉工具链(文件名为
gcc-linaro-6.2.1-2016.11-x86_64_arm-linux-gnueabihf.tar.xz)。这个工具链是在某台Build机器上构建好的,但我不用关心。 - 我把工具链解压到我的Ubuntu PC(x86_64)上,然后在这个PC上执行
arm-linux-gnueabihf-gcc -o hello hello.c。这台PC就是 Host机器。 - 编译出的
hello程序拷贝到i.MX6ULL开发板上运行。这块开发板就是 Target机器。
本地编译 :Build == Host == Target。例如,我在Ubuntu PC上直接用 gcc 编译一个程序,然后在这台PC上运行。所有机器都是一台。
交叉编译:Build == Host(通常是这样),但是 Target 不同。即我们在PC上编译,但程序在开发板上运行。这也是我们嵌入式的标准模式。
还有一种复杂情况:Canadian Cross(Build != Host != Target),比如在x86 Linux上构建一个运行在Windows上的交叉编译器(输出ARM代码),这种极少见,初学者不必深究。
第8问:总结 - 领导考察你学习成果的一道综合题
面试官:假设你刚加入一个嵌入式Linux项目,项目使用一款新的ARM芯片。领导丢给你一块开发板,让你搭建开发环境,并编译一个最简单的"Hello World"程序并在板子上跑起来。请你列出你从零开始的步骤,并解释每一步用到了我们刚才讨论的哪些概念。
你的回答:
好的,我会按以下步骤做:
- 确定目标平台的元组 :查芯片手册或板子资料,确认CPU架构(比如ARMv7-A)、是否支持硬件浮点(通常支持)、C库用glibc还是musl。最终确定元组类似
arm-unknown-linux-gnueabihf。 - 获取交叉编译工具链 :从Linaro官网或芯片厂商提供的BSP中下载对应元组的预编译工具链,解压到我的Ubuntu PC(Host机器)。并把
bin目录加到PATH环境变量。 - 验证工具链 :运行
arm-linux-gnueabihf-gcc --version和arm-linux-gnueabihf-gcc -print-sysroot,确认可用并记录sysroot路径。 - 编写源代码 :创建
hello.c,内容就是一个简单的printf("Hello Embedded\n");。 - 交叉编译 :执行
arm-linux-gnueabihf-gcc -static -o hello hello.c。这里-static是为了静态链接,避免动态库依赖,简化首次部署。这一步用到了交叉编译器。 - 验证生成的文件 :
readelf -h hello | grep Machine,确认输出是ARM。这一步用到了readelf工具。 - 部署到开发板 :通过TFTP、U盘或者直接用
scp将hello文件传到开发板的根文件系统中(比如/tmp目录)。 - 在开发板上运行 :通过串口登录开发板,执行
chmod +x /tmp/hello然后/tmp/hello。如果输出Hello Embedded,说明环境搭建成功。
涉及的核心概念:交叉编译、元组(tuple)、sysroot(虽然静态链接绕过了动态库查找,但概念仍存在)、readelf验证、Host/Target分离。
