Skip to content

第 35 章 交叉编译与 imx-forge 衔接

Part 7 · 开发工具链


引子

34 章走过来,你能装系统、敲命令、管文件、编脚本、写代码、查网络。

但所有这些,都在 x86 的 Ubuntu 上做的。

你的目标设备——i.MX 开发板——是 ARM 架构的。你在 x86 上用 gcc 编译出来的那个可执行文件,ARM 上跑不了。不是「跑得慢」,是「完全跑不了」——它们说的是不同的机器语言。

这就像你写了一份中文报告,递给一个只会日语的同事。内容再好,对方也读不了。你需要一个翻译——在计算机的世界里,这个翻译叫做交叉编译器(cross compiler)

这一章是前 34 章的终点,也是 imx-forge 项目的起点。我们要做的事情很简单:让你的 x86 电脑能编译出 ARM 板子上能跑的程序。


背景与动机

回顾:34 章建立的三条主线

在踏入嵌入式世界之前,值得回头看看我们走了多远。34 章的内容不是散落的碎片,它们沿着三条主线交织生长:

第一条线:环境与命令行。 从 Ch1 的 WSL2 到 Ch5 的 Docker,我们搭建了开发环境。从 Ch6 的 Shell 到 Ch11 的压缩归档,我们拿到了命令行的通行证。这条线解决的问题是——你能在 Linux 里活下来。

第二条线:系统管理。 从 Ch15 的用户权限到 Ch20 的 systemd 服务管理,从 Ch21 的网络配置到 Ch24 的文件传输。这条线解决的问题是——你能管理一台 Linux 机器,让它按你的意志运行。

第三条线:开发工具。 从 Ch31 的 GCC 和 Makefile,到 Ch32 的 GDB 调试,到 Ch33 的二进制工具箱,到 Ch34 的 Git 版本管理。这条线解决的问题是——你能写代码、编译代码、调试代码、管理代码。

三条线汇聚到这一章。现在的问题是:代码写好了,怎么让它跑在 ARM 上?

为什么需要交叉编译

这个问题有一个反直觉的答案。

不是「因为 ARM 性能太弱,跑不了编译器」——事实上 ARM 芯片完全能跑 GCC,树莓派上编译 Linux 内核虽然慢但真的能做到。

真正的原因是效率。你的 x86 开发机有 8 核 16 线程、32GB 内存、NVMe 固态硬盘;你的 i.MX6ULL 开发板有一颗 Cortex-A7 单核、512MB 内存、SD 卡存储。在同一块开发板上编译一个 Linux 内核,可能要两三个小时;在你的开发机上交叉编译,只要几分钟。

所以嵌入式开发的经典模式是:在 x86 主机上编译,把编译产物传到 ARM 目标机上运行。 这就是所谓的「宿主机-目标机」开发模式。


概念层

「交叉」到底在交叉什么

回到开头那个翻译的类比——交叉编译器就像一个双语翻译官:它自己住在 x86 的世界里(它本身是一个 x86 程序),但它产出的文件是给 ARM 世界的读者看的(它编译出来的二进制文件是 ARM 指令集)。

你可以把它理解为一位「中译日的翻译家」——他本人用中文思考、用中文的笔写字,但他写出来的内容是日文。读者只看日文部分,不需要知道翻译家用什么语言在工作。

但这个类比有一个地方是错的:真正的翻译家产出的是自然语言文本,读者和人之间是「理解」的问题;而编译器产出的是机器码,CPU 和机器码之间是「执行」的问题——ARM CPU 物理上无法解码 x86 的机器指令,这不是「理解不了」的级别,是「电路不支持」的级别。

所以交叉编译器做的事情,比翻译更硬核:它在 x86 的硬件上,模拟出一整套 ARM 的编译逻辑——ARM 的指令编码规则、ARM 的函数调用约定、ARM 的 ELF 文件格式——然后用这些规则生成 ARM 二进制文件。

GNU 目标三元组

交叉编译工具链的每一个工具,名字前面都有一串奇怪的前缀。比如:

arm-none-linux-gnueabihf-gcc
arm-none-linux-gnueabihf-objdump
arm-none-linux-gnueabihf-strip

这串前缀叫做目标三元组(target triplet)——更准确地说是四段式。它完整描述了「这个工具链要生成什么平台的代码」:

含义arm-none-linux-gnueabihf 的值
CPU 架构目标处理器的指令集arm(ARMv7)
厂商目标平台厂商none(通用,不绑定特定厂商)
操作系统目标平台操作系统linux(Linux 用户态)
ABI应用二进制接口gnueabihf(GNU EABI,硬件浮点)

你可能还会遇到其他前缀,区别如下:

  • arm-linux-gnueabihf-:Ubuntu 系统仓库里的交叉编译工具链,用于编译 ARM Linux 用户态程序
  • arm-none-eabi-:**裸机(bare-metal)**工具链,用于编译没有操作系统的 ARM 程序(比如 STM32 单片机固件)
  • arm-none-linux-gnueabihf-:ARM 官方发布的独立工具链,用于编译 ARM Linux 程序——imx-forge 项目使用的就是这个

回到那个翻译官的类比:arm-none-linux-gnueabihf 就是在说「这位翻译官专门做中译日,而且只翻译在日本使用的正式文书」。arm-none-eabi 则是「翻译给日本通用的、不限场合的简短便签」。目标不同,翻译风格不同——工具链也是一样。

QEMU 用户态模拟——没有板子也能跑 ARM 程序

交叉编译完,得到一个 ARM 二进制文件。你的 x86 机器直接运行它会怎样?

bash
$ ./hello
bash: ./hello: cannot execute binary file: Exec format error

「格式错误」——x86 的内核看不懂 ARM 的 ELF 文件头,拒绝执行。

但有一个办法可以让 x86 运行 ARM 程序:QEMU 用户态模拟。QEMU(Quick EMUlator)是一个开源的机器模拟器,它的用户态模式可以在 x86 上逐条翻译并执行 ARM 指令——不是真的 ARM 在跑,而是 x86 在「假装」自己是 ARM。

这意味着,即使你现在手头没有 i.MX 开发板,也能在 x86 电脑上运行你交叉编译出来的 ARM 程序,验证它的逻辑是否正确。


实践层

4.1 用系统包体验交叉编译

在正式进入 imx-forge 项目之前,先用 Ubuntu 仓库里的交叉编译工具链做一个快速体验。这一步的目的是建立直觉——交叉编译和本地编译在操作上几乎没有区别,唯一的改变是编译器的名字。

安装工具链和 QEMU

bash
# 安装 ARM 交叉编译工具链(Ubuntu 系统仓库版本)
$ sudo apt install gcc-arm-linux-gnueabihf

# 安装 QEMU 用户态模拟器
$ sudo apt install qemu-user-static binfmt-support

安装完成后,验证工具链可用:

bash
$ arm-linux-gnueabihf-gcc --version
# 预期输出(版本号随 Ubuntu 版本不同)
arm-linux-gnueabihf-gcc (Ubuntu 13.2.0-7ubuntu1) 13.2.0
...

看到版本号输出,工具链就位了。

编译第一个 ARM 程序

写一个最简单的 C 程序:

c
// hello.c
#include <stdio.h>

int main(void) {
    printf("Hello from ARM!\n");
    printf("sizeof(pointer) = %zu\n", sizeof(void *));
    return 0;
}

用交叉编译器编译:

bash
# 静态链接——把所有库打包进可执行文件,方便在 QEMU 里直接运行
$ arm-linux-gnueabihf-gcc -static -o hello hello.c

注意这里只换了一个编译器名字,编译选项和本地编译完全一样。

检查产物——真的是 ARM 吗

bash
$ file hello
# 预期输出
hello: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV),
statically linked, BuildID[sha1]=..., not stripped

ELF 32-bitARM——确认是 ARM 二进制文件。用本地的 gcc 编译同样的代码,你会看到 x86-64 而不是 ARM

在 x86 上运行 ARM 程序

bash
$ qemu-arm-static ./hello
# 预期输出
Hello from ARM!
sizeof(pointer) = 4

运行成功。sizeof(pointer) = 4 说明这是 32 位 ARM 程序(指针占 4 字节,而不是 64 位的 8 字节)。

⚠️ 踩坑预警 如果你省略了 -static 选项(动态链接),直接用 qemu-arm-static ./hello 会报错:

./hello: No such file or directory

这条报错极其误导——文件明明就在那里。真正的原因是动态链接的 ARM 程序需要 ARM 版的动态链接器(ld-linux-armhf.so.3),而 x86 系统上没有。两种解决办法:

  1. -static 做静态链接(简单,推荐初学时使用)
  2. -L /usr/arm-linux-gnueabihf 指定 sysroot(让 QEMU 去找 ARM 版的系统库)

现在你已经亲手走完了交叉编译的完整闭环:在 x86 上编译 ARM 程序,在 x86 上通过 QEMU 验证。接下来要做的事情,是把这套能力接入真正的嵌入式项目。

4.2 搭建 imx-forge 开发环境

imx-forge 是一个面向 NXP i.MX6ULL 处理器的嵌入式 Linux 开源项目。它提供完整的从零构建嵌入式 Linux 系统的学习路径:工具链 → U-Boot → Linux 内核 → 根文件系统 → 驱动开发。

imx-forge 没有使用 Ubuntu 系统仓库里的 gcc-arm-linux-gnueabihf,而是使用 ARM 官方发布的 Arm GNU Toolchain 15.2.rel1——一个独立的、最新版本的交叉编译工具链。工具链前缀是 arm-none-linux-gnueabihf-

克隆项目

bash
# 克隆仓库(--recurse-submodules 很关键,third_party/ 下的源码是子模块)
$ git clone --recurse-submodules https://github.com/Awesome-Embedded-Learning-Studio/imx-forge.git
$ cd imx-forge

⚠️ 踩坑预警 如果你忘了 --recurse-submodulesthird_party/ 目录会是空的。补救方法:

bash
git submodule update --init --recursive

这个过程会拉取 U-Boot、Linux 内核、BusyBox 等源码,体积约 1-2 GB,取决于网络速度。

Docker 环境(推荐方式)

imx-forge 推荐使用 Docker 容器作为开发环境——工具链、依赖库、编译脚本全部预装在镜像里,省去了手动配置的麻烦。

我们在 Ch5 已经搭建过 Docker,现在直接用它:

bash
# 构建 Docker 镜像
$ cd docker && docker build -t imx-forge:latest . && cd ..

# 国内用户使用加速镜像
$ cd docker && docker build -f Dockerfile.cn -t imx-forge:latest . && cd ..

构建过程需要几分钟,会下载 Ubuntu 24.04 基础镜像并安装所有依赖。构建完成后启动容器:

bash
# 启动容器,挂载项目目录
$ docker run -it --rm -v $(pwd):/workspace imx-forge:latest

进入容器后,首先验证工具链:

bash
# 在容器内执行
$ arm-none-linux-gnueabihf-gcc --version
# 预期输出
arm-none-linux-gnueabihf-gcc (Arm GNU Toolchain 15.2.rel1) 15.2.1 20251203
...

看到 15.2.1,工具链就位。这是 2025 年 12 月发布的版本——比很多开发板厂商 SDK 里附带的 GCC 7.x(2017 年)新了整整八年。

手动安装工具链(可选)

如果你不想用 Docker,也可以在主机上手动安装工具链:

bash
# 下载 ARM GNU Toolchain 15.2.rel1
$ wget https://developer.arm.com/-/media/Files/downloads/gnu/15.2.rel1/binrel/arm-gnu-toolchain-15.2.rel1-x86_64-arm-none-linux-gnueabihf.tar.xz

# 解压
$ tar xf arm-gnu-toolchain-*.tar.xz

# 移动到 /opt(统一管理,方便多版本共存)
$ sudo mv arm-gnu-toolchain-15.2.rel1-x86_64-arm-none-linux-gnueabihf /opt/arm-gnu-toolchain

# 加入 PATH(写入 ~/.bashrc 永久生效)
$ echo 'export PATH=/opt/arm-gnu-toolchain/bin:$PATH' >> ~/.bashrc
$ source ~/.bashrc

# 验证
$ arm-none-linux-gnueabihf-gcc --version

⚠️ PATH 里要加的是 bin/ 目录 一个很多人踩过的坑:把 PATH 写成 /opt/arm-gnu-toolchain 而不是 /opt/arm-gnu-toolchain/bin。结果系统找不到编译器。如果你遇到 command not found,第一步检查 echo $PATH 里有没有 bin

4.3 首次编译——点燃启动链条

工具链就位后,我们来做一件有仪式感的事情:编译整套嵌入式 Linux 系统。

imx-forge 提供了一个一键构建脚本 release-all.sh,它会按顺序编译四个组件:

工具链 (已有)

U-Boot (Bootloader)

Linux 内核

BusyBox (用户态工具集)

根文件系统打包

在容器内(或配置好工具链的主机上)执行:

bash
$ ./scripts/release-all.sh

这个过程在 8 核机器上大约需要 10-20 分钟。首次编译会拉取所有源码并执行完整编译,后续的增量编译会快很多。

编译完成后,查看产物:

bash
$ ls out/release-latest/
# 预期输出
busybox  images  linux  rootfs  uboot
  • out/release-latest/uboot/ —— U-Boot 引导程序二进制文件
  • out/release-latest/linux/ —— Linux 内核镜像和设备树
  • out/release-latest/busybox/ —— BusyBox 用户态工具
  • out/release-latest/rootfs/ —— 根文件系统
  • out/release-latest/images/ —— 打包好的完整系统镜像(可直接烧录到 SD 卡)

如果你想单独编译某个组件:

bash
$ ./scripts/build_helper/build-uboot.sh       # 只编译 U-Boot
$ ./scripts/build_helper/build-linux.sh       # 只编译内核
$ ./scripts/build_helper/build-busybox.sh     # 只编译 BusyBox

到这里,你已经完成了从「零 Linux 基础」到「能编译一整套嵌入式 Linux 系统」的跨越。


练习题

走到这里,机制应该已经清楚了。下面几道题帮你检验交叉编译的理解是否到位,建议先不看提示独立想。

练习 35.1 ⭐(理解)

你分别在 x86 主机和 ARM 开发板上执行 gcc -o test test.c 编译同一个源文件。两个 test 文件能在对方的平台上运行吗?为什么?

提示:回忆一下「目标三元组」——gcc 生成的是当前平台的机器码。

练习 35.2 ⭐⭐(应用)

在 x86 主机上,不用交叉编译工具链,直接执行 arm-none-linux-gnueabihf-gcc -static -o hello hello.c 编译一个程序,然后用 file 命令检查产物。再尝试不用 -static 编译,分别用 qemu-arm-static 运行两个版本,观察区别。解释你看到的现象。

提示:动态链接的 ARM 程序需要 ARM 版的动态链接器,静态链接的程序是自包含的。

练习 35.3 ⭐⭐⭐(思考)

arm-none-linux-gnueabihf-gccarm-none-eabi-gcc 都是 ARM 交叉编译器,但生成的程序不能混用。前者编译的程序可以直接在 ARM Linux 上运行,后者编译的程序通常需要烧录到裸机硬件。请结合「目标三元组」中的操作系统字段(linux vs 空缺),解释为什么会有这个区别。如果你要为 i.MX6ULL 编译 Linux 内核驱动模块,应该选哪个?


本章回响

本章做的事情,表面上是安装了一个工具、跑了几个命令,但真正建立的核心认知是:交叉编译的本质,是在一个平台上模拟另一个平台的编译规则。 你的 x86 机器不会变成 ARM,但它可以学会说 ARM 的机器语言——编译器就是这个翻译官。

还记得开头那个类比吗——交叉编译器像一个双语翻译官,用 x86 的方式工作,但产出 ARM 的成果。现在你应该能看出这个类比的局限了:翻译官翻译的是信息,而交叉编译器翻译的是指令——ARM CPU 物理上不具备执行 x86 指令的能力,这不是「读不懂」,而是「电路不支持」。所以交叉编译不是可选的优化,而是必需的桥梁。

理解了这一点,嵌入式开发的大门就打开了。34 章积累的三条主线——命令行生存能力、系统管理能力、开发工具链——在这里汇聚成了一个完整的闭环。你已经能在 x86 上交叉编译出 ARM 的 U-Boot 引导程序、Linux 内核、根文件系统。下一步,是深入理解这些组件各自在做什么:

  • U-Boot——开发板上电后第一个运行的程序,负责初始化硬件、加载内核。U-Boot 教程
  • Linux 内核——进程调度、内存管理、设备驱动框架的核心。内核教程
  • 根文件系统——内核启动后挂载的第一套用户态文件和工具。RootFS 教程
  • 驱动开发——连接硬件和应用的桥梁,嵌入式开发的核心技能。驱动教程

如果你还没买开发板,也不必着急。imx-forge 的项目文档(入门路线图)会帮你规划接下来的学习路径。这 35 章建立的底层能力——命令行、权限、网络、脚本、编译、调试——每一条都会在嵌入式开发的实战中反复派上用场。

旅途才刚刚开始。


← 上一章专栏首页 →

Built with VitePress