gcc hello.c 生成的程序,拷到 STM32 上连影子都跑不出来——CPU 说的是不同的"方言"。make 替代手敲五行命令。学完之后,看到 arm-none-eabi-gcc 这种长串名字不再发懵。你在 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。
gcc 默认把 C 代码翻译成本机 CPU 能懂的指令,所以编译出来的程序在单片机上就是天书。「交叉编译」听起来很高深,其实就是:在 A 机器上编译,拿去 B 机器上运行。A 叫「宿主机(Host)」,B 叫「目标机(Target)」。
日常用电脑开发单片机,就是典型的交叉编译场景:你的 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 用的。在 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) 死循环
看到 movs、ldr、str、b 这些就是 ARM Thumb 指令——你的 C 代码已经被成功翻译成单片机能懂的"方言"了。
gcc 编译同一个文件,objdump -d 出来的会是 mov、lea、ret 等 x86 指令。这就是交叉编译器存在的意义。你敲一行 gcc hello.c -o hello,背后其实跑了四道工序。理解这四步,以后报错信息就知道出在哪一环:
平时 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
每次编译都要敲一长串命令太累。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
make 会报 missing separator 错误。用法很简单:
$ make # 编译 hello.elf $ make flash # 转 bin + 烧录 $ make clean # 清理中间文件
Makefile 的核心逻辑是依赖关系:hello.elf 依赖 hello.c,只有 hello.c 被修改过才会重新编译。这就避免了每次都全量编译。
坑 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。
gcc 直接编译给 STM32 用的程序?gcc 默认面向宿主机 CPU(x86_64)生成指令,STM32 是 ARM 架构,CPU 看不懂 x86 指令,会报 Exec format error。需要用 arm-none-eabi-gcc 这类交叉编译器。arm-none-eabi-gcc 名字里 none 代表什么?linux-gnu,如 aarch64-linux-gnu-gcc。-c 参数停在哪个阶段?.o 文件。还没链接,不能直接运行。-E 停在预处理,-S 停在编译(产出汇编 .s)。