嵌入式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分离。

相关推荐
不做无法实现的梦~1 小时前
PX4 机载电脑 Linux 环境安装、串口、网络、ROS 完整配置
linux·运维·网络
IT界的老黄牛1 小时前
停电后 Redis 集群两节点起不来:fix 完还报 Bad file format?多部分 AOF 修复的正确姿势
运维·redis·缓存
接着奏乐接着舞1 小时前
3D Tiles tileset.jso 数据格式
运维·服务器·3d
李小白202002021 小时前
RK3568 linux6.1 死机
linux·运维·服务器
FreeGo~1 小时前
Linux 系统编程 进程篇 (五)
java·linux·服务器
杨云龙UP2 小时前
Oracle数据库启动失败:ORA-29701、ORA-01565、ORA-17503故障处理记录_20260429
linux·运维·数据库·oracle·centos
Agent产品评测局2 小时前
离散制造业生产流程优化,AI落地实操步骤详解:从传统自动化到企业级智能体的技术范式跃迁
运维·人工智能·ai·自动化
XiYang-DING2 小时前
【Java EE】定时器
java·python·java-ee
Fuly10242 小时前
java面试知识点复习
java·开发语言·面试