嵌入式Linux开发(了解交叉编译工具链的组成)

文章目录

(一)工具链的基本概念

工具链是一组编程工具,用于开发软件、创建软件产品。工具链通常是另一个计算机程序或一组相关程序。通常,工具链里有多个工具,前一个工具的输出结果,是下一个工具的输入,也就是说前一个工具处理完,再交给下一个工具处理。

一个简单工具链可能由三部分组成:编译器和链接器(将源代码转换为可执行程序)、库(为操作系统提供接口)和调试器(用于测试和调试创建的程序)。

GNU工具链是一个广泛收集的、遵守GNU协议的、众多编程工具。这些工具形成一个工具链,用于开发应用程序和操作系统。GNU工具链在Linux、一些BSD系统和嵌入式系统软件的开发中起着至关重要的作用。

讲解

你现在正在学习嵌入式Linux开发,第一个必须搞懂的概念就是"工具链"。想象一下,你要做一道复杂的菜,需要刀、砧板、锅、铲子、调料......这些厨具配合起来,才能把原材料变成一道菜。工具链就是软件开发中的"厨具组合"。

最简单的工具链包含三个环节:

  1. 编译器 :把你写的C/C++代码(人类能读懂)翻译成机器能执行的指令。比如你写了一个 hello.c,编译器把它变成 hello.o(目标文件)。
  2. 链接器 :把多个目标文件和你用到的库(比如 printf 函数所在的库)合并成一个完整的可执行文件。
  3. 调试器:当程序运行出问题时,你可以用调试器一步步观察程序的运行状态,找到错误。

这三个工具是"链式"工作的:源代码 → 编译器 → 目标文件 → 链接器 → 可执行文件 → 调试器(帮忙排查问题)。

在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),它用一套标准化的命名方式,精确描述了一个目标系统是什么。

一个完整的元组包含四个部分:

  1. arch(架构) :CPU类型,如 armaarch64mipsi686
  2. vendor(厂商) :通常填 unknown 或者厂商名,比如 buildrootpoky。autoconf 工具其实不太关心这个字段,但有些构建系统用它来做标识。
  3. os(操作系统)linux(表示Linux系统)、none(表示裸机,无操作系统)。
  4. libc/abi :C库和ABI的组合。常见的有 gnueabihf(使用glibc,且硬件浮点ABI)、uclibcgnueabimusl 等。

有时候会省略 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-uclibcgnueabiarmeb 中的 eb 表示big-endian(大端模式),ARM默认是小端,但有些芯片支持大端。uclibc 是一个更小巧的C库,常用于资源非常紧张的嵌入式设备。gnueabi 表示使用标准的嵌入式ABI(没有hf后缀,可能是软浮点或兼容模式)。
  • mips-img-linux-gnuimg 是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 字段决定了这个工具链生成的代码将与什么操作系统交互。两个最重要的值是 nonelinux

裸机工具链(os = none)

  • 它假设目标系统上没有操作系统。你编写的程序直接控制硬件,没有"系统调用"的概念。
  • 使用的C库通常是 newlib ,一个专门为嵌入式裸机环境设计的轻量级C库。newlib 提供标准的C函数(如 printfmalloc),但这些函数的底层实现需要你自己提供(比如往串口写一个字符的代码)。
  • 这种工具链可以用来编译 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-gccarm-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/libusr/include 等,结构类似于目标板的根文件系统,但它不是用来运行的,而是给编译器和链接器看的。

对初学者的影响

  • 如果你只是学习编译U-Boot、Linux内核和简单的C程序,522MB的Linaro工具链完全足够,还节省硬盘空间。
  • 如果你需要开发复杂的应用程序,依赖很多第三方库(比如使用Qt、OpenCV、gRPC等),使用Buildroot/Yocto生成的SDK能节省大量时间(不必自己逐个交叉编译这些库)。
  • 注意:SDK的1.7G是压缩前的大小,实际部署在电脑上可能还要更大一些。

(十)工具链文件数量对比

Linaro工具链文件个数:ls -lR | grep ".*" | wc -l → 7697

Buildroot 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-gccarm-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/ 目录

里面有一些工具,比如 asldar 等,但没有 前缀 arm-buildroot-linux-gnueabihf-。实际上,上层 bin/ 目录中的带前缀工具,很多是硬链接到这里的无前缀工具吗?不对------更准确地说,gcc在运行时需要调用 asld 等,它会在工具链内部目录中寻找。这些无前缀的工具就是为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.hunistd.h)。
  • lib/usr/lib/:包含C库(如 libc.solibm.so)、动态链接器(ld-linux-armhf.so.3),以及一些其他的库(如 libpthread)。

初学者的关键理解 :当你使用 -lstdc++ 链接时,链接器会去 sysroot/usr/liblibstdc++.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: ARMFlags: 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上运行。

嵌入式开发离不开它,主要有两个原因:

  1. 资源限制:开发板的CPU主频低(几百MHz)、内存小(可能只有128MB)、存储空间有限(几百MB)。如果直接在开发板上编译,一个稍微复杂的程序可能要花几个小时,而且很可能内存不足。PC的CPU强劲、内存几十GB,编译速度快得多。
  2. 运行环境不完整:大多数嵌入式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)、mipsriscv64 等。
  • unknown :厂商字段,通常情况下填 unknownbuildrootpoky。这个字段对编译过程没有实际影响,可以忽略。
  • 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/includeusr/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 80386Machine: x86_64,说明是x86架构,放ARM板上当然无法运行。

另外,还可以看 Flags 字段,例如 hard-float ABI 表示使用了硬件浮点,你的板子必须支持硬件浮点(开发板一般都支持)。如果显示 soft-float,也能运行但效率低。

readelf 是调试交叉编译问题的最基本工具。我每次编译完一个重要的程序,都会先用它确认一下架构,避免浪费时间排查错误。


第6问:arm-linux-gnueabihf-gccarm-none-eabi-gcc 有什么区别?分别在什么场景下使用?

面试官 :我注意到两种常见的工具链:arm-linux-gnueabihf-gccarm-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:假设没有操作系统。你无法使用 openreadwrite 这类系统调用函数。C库(newlib)只提供纯计算类函数(如 memcpystrlen),底层硬件操作需要你自己实现(例如写一个 _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"程序并在板子上跑起来。请你列出你从零开始的步骤,并解释每一步用到了我们刚才讨论的哪些概念。

你的回答

好的,我会按以下步骤做:

  1. 确定目标平台的元组 :查芯片手册或板子资料,确认CPU架构(比如ARMv7-A)、是否支持硬件浮点(通常支持)、C库用glibc还是musl。最终确定元组类似 arm-unknown-linux-gnueabihf
  2. 获取交叉编译工具链 :从Linaro官网或芯片厂商提供的BSP中下载对应元组的预编译工具链,解压到我的Ubuntu PC(Host机器)。并把 bin 目录加到PATH环境变量。
  3. 验证工具链 :运行 arm-linux-gnueabihf-gcc --versionarm-linux-gnueabihf-gcc -print-sysroot,确认可用并记录sysroot路径。
  4. 编写源代码 :创建 hello.c,内容就是一个简单的 printf("Hello Embedded\n");
  5. 交叉编译 :执行 arm-linux-gnueabihf-gcc -static -o hello hello.c。这里 -static 是为了静态链接,避免动态库依赖,简化首次部署。这一步用到了交叉编译器
  6. 验证生成的文件readelf -h hello | grep Machine,确认输出是 ARM。这一步用到了 readelf 工具。
  7. 部署到开发板 :通过TFTP、U盘或者直接用 scphello 文件传到开发板的根文件系统中(比如 /tmp 目录)。
  8. 在开发板上运行 :通过串口登录开发板,执行 chmod +x /tmp/hello 然后 /tmp/hello。如果输出 Hello Embedded,说明环境搭建成功。

涉及的核心概念:交叉编译、元组(tuple)、sysroot(虽然静态链接绕过了动态库查找,但概念仍存在)、readelf验证、Host/Target分离。

相关推荐
码云数智-园园几秒前
C++20 Modules 模块详解
java·开发语言·spring
程序员黑豆3 分钟前
JDK 下载安装与配置详细教程
java·前端·ai编程
霸道流氓气质9 分钟前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush412 分钟前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行52026 分钟前
Linux 11 动态监控指令top
linux
小宇宙Zz32 分钟前
Maven依赖冲突
java·服务器·maven
swordbob34 分钟前
NIO的channel中什么是 fd(File Descriptor,文件描述符)
java·开发语言·nio
咖啡八杯1 小时前
GoF设计模式——享元模式
java·spring·设计模式·享元模式
十五喵源码网1 小时前
基于springboot2+vue2的租房管理系统
java·毕业设计·springboot·论文笔记
摇滚侠1 小时前
IDEA 创建 Java 项目 手动整合 SSM 框架
java·ide·intellij-idea