🐧 linux-02 交叉编译是什么?给 ARM 单片机编译程序的第一步

🐧 Linux 上手 · 第 2 / 2 章
💬 你在 PC 上 gcc hello.c 生成的程序,拷到 STM32 上连影子都跑不出来——CPU 说的是不同的"方言"。
这一章在干嘛? 带你搞懂为什么嵌入式开发需要「交叉编译」,工具链里每个工具干什么活,以及怎么用一条 make 替代手敲五行命令。学完之后,看到 arm-none-eabi-gcc 这种长串名字不再发懵。
先看现场本机 vs 交叉工具链组成安装第一次编译编译四阶段Makefile 入门3 个常见坑小测验

一、案发现场:编译没问题,运行全乱套

你在 Linux 电脑上写了个最简单的 C 程序,编译运行一气呵成:

# 写代码
$ cat hello.c
int main() { return 42; }

# 编译 + 运行
$ gcc hello.c -o hello
$ ./hello; echo $?
42

你觉得很简单。然后把同一个 hello 文件拷到 STM32 开发板上运行,收获一行:

-sh: ./hello: cannot execute binary file: Exec format error

或者更干脆——板子根本就没有 shell,你的程序得通过 J-Link/STLink 烧到 Flash 里去。这时候你打开 STM32 的工程,发现别人用的编译器叫 arm-none-eabi-gcc,而不是你电脑上的 gcc

问题的本质:你的电脑 CPU 是 x86_64 架构,说的是一套指令「方言」;STM32 是 ARM Cortex-M 架构,说的是另一套。gcc 默认把 C 代码翻译成本机 CPU 能懂的指令,所以编译出来的程序在单片机上就是天书。

二、本机编译 vs 交叉编译:一张图看懂

「交叉编译」听起来很高深,其实就是:在 A 机器上编译,拿去 B 机器上运行。A 叫「宿主机(Host)」,B 叫「目标机(Target)」。

本机编译(Native) 宿主机 x86_64 gcc + 运行 程序 x86_64 在同一台跑 ✓ 交叉编译(Cross) 宿主机 x86_64 arm-none-eabi-gcc 程序 ARM 拷到单片机跑 ✓ 常见错误:拿本机 gcc 编译给 ARM 用 宿主机 gcc 翻译成 x86_64 指令 ARM 单片机 看不懂 ✗ Exec format error 关键区别:编译器必须「面向目标 CPU」生成指令,而不是面向自己所在的宿主机
本机编译与交叉编译对比:箭头方向代表「编译产物流向哪里运行」

日常用电脑开发单片机,就是典型的交叉编译场景:你的 Linux 电脑是宿主机,STM32/FPGA 板子是目标机。

三、工具链里到底有什么

你可能会看到「工具链(Toolchain)」这个词。它不是一个程序,而是一组工具的集合,像流水线一样把源代码变成可执行文件。核心成员有四位:

工具干什么类比
arm-none-eabi-gcc编译器,把 .c 翻译成汇编翻译官
arm-none-eabi-ld链接器,把多个 .o 拼成最终程序装订工
arm-none-eabi-objcopy格式转换,把 ELF 变成 .bin/.hex打包员
arm-none-eabi-gdb调试器,配合 J-Link 单步调试质检员
名字怎么读? arm-none-eabi-gcc 拆开看:arm(目标架构)- none(无操作系统,即裸机)- eabi(嵌入式应用二进制接口)- gcc(编译器本体)。记住这个拆法,以后看到 aarch64-linux-gnu-gcc 就能猜出是给 64 位 ARM Linux 用的。

四、安装 ARM GCC 工具链

在 Ubuntu / OpenCloudOS 上,一行命令就能装好裸机开发的工具链:

# Debian / Ubuntu 系
$ sudo apt install gcc-arm-none-eabi

# 或者直接下载官方预编译包(版本更新)
$ wget https://developer.arm.com/-/media/Files/downloads/gnu/13.2.rel1/binrel/arm-gnu-toolchain-13.2.rel1-x86_64-arm-none-eabi.tar.xz
$ sudo tar xf arm-gnu-toolchain-*.tar.xz -C /opt/

# 加到 PATH
$ echo 'export PATH="/opt/arm-gnu-toolchain-13.2.rel1-x86_64-arm-none-eabi/bin:$PATH"' >> ~/.bashrc
$ source ~/.bashrc

装完验证一下:

$ arm-none-eabi-gcc --version
arm-none-eabi-gcc (Arm GNU Toolchain 13.2.Rel1) 13.2.1
$ arm-none-eabi-gcc -print-multi-lib
.;m32; thumb;/arm/thumb/v7-m;--specs=nosys.specs ...

看到版本号就说明安装成功。-print-multi-lib 列出了支持的各种 CPU 子架构,Cortex-M3/M4/M7 都在里面。

五、第一次交叉编译

我们来编译一个最小程序,确认工具链能跑通:

# hello.c —— 一个不需要操作系统的裸机程序
$ cat hello.c
volatile unsigned int *reg = (void*)0x48000000;
int main() {
    *reg = 0x42;  // 往地址 0x48000000 写一个值
    while(1);     // 裸机程序不能 return
}

# 编译(只编译不链接,先看能不能翻译成功)
$ arm-none-eabi-gcc -c hello.c -mcpu=cortex-m4 -mthumb -o hello.o

# 检查:生成的是 ARM 指令吗?
$ arm-none-eabi-objdump -d hello.o
hello.o:     file format elf32-littlearm
...
00000000 <main>:
   0:	movs	r1, #66	; 0x42
   2:	ldr	r3, [pc, #4]
   4:	str	r1, [r3, #0]
   6:	b	6 <main+0x6>	; while(1) 死循环

看到 movsldrstrb 这些就是 ARM Thumb 指令——你的 C 代码已经被成功翻译成单片机能懂的"方言"了。

对照一下:如果用本机 gcc 编译同一个文件,objdump -d 出来的会是 movlearet 等 x86 指令。这就是交叉编译器存在的意义。

六、编译的四个阶段:从 .c 到 .elf

你敲一行 gcc hello.c -o hello,背后其实跑了四道工序。理解这四步,以后报错信息就知道出在哪一环:

hello.c 源代码 预处理 cpp / -E 展开 #include #define 编译 cc1 / -S C → 汇编 .s 汇编 as / -c .s → 目标码 .o 链接 ld / 默认 .o → .elf 可执行 .elf 烧录文件 命令对照:-E 只预处理 → -S 到汇编 → -c 到目标码 → 不加参数全做完 $ arm-none-eabi-gcc -E hello.c -o hello.i # 只看预处理结果 $ arm-none-eabi-gcc -S hello.c -o hello.s # 只看汇编结果
编译四阶段:预处理 → 编译 → 汇编 → 链接,每一步都可以单独停下来查看中间产物

平时 gcc hello.c -o hello 一口气跑完四步,但加参数就能在任意一步停下来:

# 只做预处理(展开宏和头文件)
$ arm-none-eabi-gcc -E hello.c -o hello.i

# 只做编译(C → 汇编,调试优化问题时很有用)
$ arm-none-eabi-gcc -S hello.c -o hello.s

# 只做汇编(汇编 → 目标码)
$ arm-none-eabi-gcc -c hello.c -o hello.o

# 链接 + 转格式
$ arm-none-eabi-gcc hello.o -nostdlib -T linker.ld -o hello.elf
$ arm-none-eabi-objcopy -O binary hello.elf hello.bin

七、Makefile 入门:告别手敲五行命令

每次编译都要敲一长串命令太累。make 就是个自动化助手:你把命令写成 Makefile,以后只要敲 make 就行。

先看一个最小 Makefile:

# Makefile —— 最小交叉编译示例
CC = arm-none-eabi-gcc
CFLAGS = -mcpu=cortex-m4 -mthumb -O2 -Wall

hello.elf: hello.c
	$(CC) $(CFLAGS) hello.c -nostdlib -T linker.ld -o hello.elf

flash: hello.elf
	arm-none-eabi-objcopy -O binary hello.elf hello.bin
	st-flash write hello.bin 0x08000000

clean:
	rm -f hello.o hello.elf hello.bin
踩坑预警:Makefile 里命令行前面必须是 Tab 键,不是空格!如果你的编辑器自动把 Tab 转成空格,make 会报 missing separator 错误。

用法很简单:

$ make          # 编译 hello.elf
$ make flash    # 转 bin + 烧录
$ make clean    # 清理中间文件

Makefile 的核心逻辑是依赖关系hello.elf 依赖 hello.c,只有 hello.c 被修改过才会重新编译。这就避免了每次都全量编译。

八、3 个新手必踩的坑

坑 1:装了工具链但 command not found

说明 PATH 没配对。验证:which arm-none-eabi-gcc,如果没有输出,检查 ~/.bashrc 里的 export PATH 行是否写对了路径。

坑 2:链接时报 cannot find -lc

裸机开发没有标准 C 库,链接时加 -nostdlib-specs=nosys.specs 跳过 libc。如果你确实要用 printf,要么自己实现 _write,要么用 Newlib 半主机模式(semihosting)。

坑 3:-mcpu 写错导致指令不兼容

Cortex-M3 写成 -mcpu=cortex-m4 可能生成 M4 独有指令,在 M3 上触发 HardFault。对照表:M0→cortex-m0,M3→cortex-m3,M4/M33→cortex-m4,M7→cortex-m7。

九、小测验

Q1:为什么不能用 gcc 直接编译给 STM32 用的程序?
因为 gcc 默认面向宿主机 CPU(x86_64)生成指令,STM32 是 ARM 架构,CPU 看不懂 x86 指令,会报 Exec format error。需要用 arm-none-eabi-gcc 这类交叉编译器。
Q2:arm-none-eabi-gcc 名字里 none 代表什么?
代表「无操作系统」,即裸机(bare-metal)开发。如果是给 Linux 系统的 ARM 编译,工具链名会带 linux-gnu,如 aarch64-linux-gnu-gcc
Q3:编译四阶段里,-c 参数停在哪个阶段?
停在「汇编」阶段,产物是目标码 .o 文件。还没链接,不能直接运行。-E 停在预处理,-S 停在编译(产出汇编 .s)。
← 上一篇🏠 Linux 上手下一篇 →