详解GCC编译器常用命令

GCC 从入门到实战:编译四阶段 + 常用命令速查

GCC(GNU Compiler Collection)是 Linux 下最常用的 C/C++ 编译器,也是嵌入式开发中不可或缺的工具。

基本格式:gcc [选项] 源文件 -o 输出文件

💡 嵌入式中的交叉编译

交叉编译器的用法和 gcc 完全一致,只是前缀不同:
  • arm-linux-gnueabihf-gcc — ARM Linux
  • aarch64-linux-gnu-gcc — ARM64 Linux
  • riscv64-unknown-elf-gcc — RISC-V 裸机

1. GCC 编译的四个阶段总览

源码(.c) → 预处理(.i) → 编译(.s) → 汇编(.o) → 链接(可执行文件)
             -E          -S          -c         默认
💡 用「翻译」来理解整个流程

你写的 C 代码 = 用人话写的操作指令:"算 1 加 2,把结果打印出来"
  • C 源码 = 人话指令,机器完全看不懂
  • 预处理 = 把 #include 引用的代码搬进来、把宏替换掉,整理成一份完整的 C 代码
  • 编译 = 翻译成 CPU 的指令助记符(汇编):"把 1 放进寄存器 A,把 2 放进寄存器 B,执行加法"—— 文字版,人还能看懂
  • 汇编 = 查表把每条助记符换成二进制数:MOV → 10110001ADD → 00000100 —— 纯 01,人看不懂了
  • 链接 = 你的代码只写了"打印",但 printf 的实现在标准库里 → 把它们拼到一起,生成完整的可执行文件
阶段 命令 工具 输入 输出 用途
1.预处理 gcc -E main.c -o main.i cpp(C Preprocessor) .c .i 查看宏展开、头文件包含
2.编译 gcc -S main.c -o main.s cc1(C Compiler) .i .s 查看汇编代码
3.汇编 gcc -c main.c -o main.o as(Assembler) .s .o 生成目标文件
4.链接 gcc main.o -o main ld(Linker) .o + 库 可执行文件 最终程序
一步到位 gcc main.c -o main 可执行文件 最常用

选项字母-E-S-c-oESCo,谐音"逃跑")
文件后缀.i → .s → .o → a.out

ℹ️ -save-temps:一次生成所有中间文件
gcc -Wall -save-temps main.c -o main
# 会生成:main.i  main.s  main.o  main

1.1 预处理(-E):展开宏、搬入头文件

还没开始翻译,先把"缩写"和"引用"全部展开,整理成一份完整的 C 代码。

预处理器在这一步做三件事

  1. #include → 把头文件内容原封不动搬进来(比如 stdio.hprintf 的声明)
  2. 宏替换 → 把所有宏"文本替换"掉
  3. 删除注释 → 去掉所有 ///* */
// main.c
#include <stdio.h>
#define ADD(a, b) (a) + (b)

int main() {
    int x = ADD(1, 2);
    printf("x=%d\n", x);
    return 0;
}
gcc -E main.c -o main.i   # 预处理

image-20260314130236893

打开 main.i,看到:

  • #include <stdio.h> 被替换成了近千行 stdio.h 的源码
  • ADD(1, 2) 被替换成了 (1) + (2) —— 纯文本替换,不做计算
// main.i(简化示意)
// ... stdio.h 展开的几千行声明 ...
extern int printf(const char *, ...);

int main() {
    int x = (1) + (2);        // ADD(1,2) 被替换了
    printf("x=%d\n", x);
    return 0;
}
ℹ️ 关键特点
  • .i 还是纯 C 代码,只是更长了(头文件展开)
  • 宏是文本替换,不是函数调用 — 所以宏参数要加括号 (a)+(b) 防止优先级出错
  • 所有 # 开头的指令(#define#ifdef#include)在这一步全部处理完毕

1.2 编译(-S):C 代码 → 汇编代码

这是「高级语言 → 低级语言」的关键一步,也是最"智能"的一步。

编译器在这一步做了三件事:

  1. 语法检查 — 少写分号、括号不匹配,这一步报错
  2. 优化 — 比如 (1) + (2) 直接优化成 3,减少运行时计算
  3. 翻译 — 把 C 的 ifforprintf 对应成汇编指令(MOVADDCALL

上一步生成了 main.i,继续往下走:

gcc -S main.i -o main.s   # 从预处理结果编译,更连贯
gcc -S main.c -o main.s   # 直接从源码编译也行,GCC 会自动先预处理

生成的 main.s(文本格式,人类能看懂):

    .section  .rodata        ; 只读数据段(存放字符串常量)
.LC0:
    .string "x=%d\n"        ; printf 的格式字符串

    .text                    ; 代码段(存放可执行指令)
    .globl  main             ; 声明 main 为全局符号
main:
    pushq   %rbp
    movq    %rsp, %rbp
    subq    $16, %rsp
    movl    $3, -4(%rbp)     ; (1)+(2) 被优化成 3 了
    movl    -4(%rbp), %eax
    movl    %eax, %esi       ; 第2个参数:x 的值
    leaq    .LC0(%rip), %rax
    movq    %rax, %rdi       ; 第1个参数:格式字符串 "x=%d\n"
    movl    $0, %eax
    call    printf@PLT       ; 调用 printf
    movl    $0, %eax         ; return 0
    leave
    ret
💡 关于段(Section)

汇编里的 .text.rodata内存段,告诉链接器"这段内容放哪里":
  • .text — 代码段,存放可执行指令(通常只读+可执行)
  • .rodata — 只读数据段,存放字符串常量等(只读)
  • .data — 已初始化的全局/静态变量(可读写)
  • .bss — 未初始化的全局/静态变量(不占文件空间,运行时清零)

嵌入式中,链接脚本(-T xxx.ld)就是把这些段映射到具体的 Flash/RAM 地址。

image-20260314135334843

ℹ️ 关键特点
  • .s文本文件,记事本就能打开
  • 汇编语言和 CPU 架构绑定(ARM 和 x86 的指令完全不一样)
  • 编译 = 翻译 + 优化,不只是简单替换

1.3 汇编(-c):汇编代码 → 机器码

这一步很"机械",没有优化,只是把每条汇编指令一对一直译成二进制数。

gcc -c main.s -o main.o   # 汇编生成机器码文件
gcc -c main.c -o main.o   # 直接从源码汇编生成机器码,GCC 会自动先预处理、编译

汇编器(as)把每条指令对应成 CPU 能直接执行的二进制数,比如:

  • MOV01010001
  • ADD00101100
ℹ️ 关键特点
  • .o二进制文件,记事本打开是乱码,但 CPU 能识别
  • 只做直译,不做任何优化(优化都在编译阶段完成)
  • .o 还不能直接运行 — 缺少库函数(如 printf)的实现,需要链接后才能变成可执行文件
  • 可用 nm main.o 查看符号表,objdump -d main.o 反汇编查看机器码

image-20260314140147858


1.4 链接:把所有 .o 拼成可执行文件

你的代码里写了 printf,但你只是"调用"了它,真正的实现在标准库(libc)里。链接就是把你的代码库的代码拼到一起。

gcc main.o -o main   # 链接生成可执行文件

链接器(ld)在这一步做的事:

  1. 合并目标文件 — 多个 .o 文件合并成一个(多文件项目时)
  2. 解析符号引用 — 你的代码调用了 printf,链接器去 libc 里找到它的机器码,填上地址
  3. 生成可执行文件 — 加上程序入口地址、内存布局等信息,生成最终能运行的文件
ℹ️ 关键特点
  • 如果调用了库函数却没链接对应的库,这一步会报 undefined reference 错误
  • 比如用了 sin()math.h),必须加 -lm 告诉链接器去链接数学库
  • 链接分静态链接(把库代码复制进来,文件大但独立)和动态链接(运行时才去找库,文件小但依赖 .so/.dll

2. 常用选项速查

选项 含义 说明
-o output 指定输出文件名
-Wall Warning all 开启所有常见警告
-Wextra 比 -Wall 更严格的额外警告
-Werror 把警告当错误,有警告就编译失败
-g 生成调试信息,配合 gdb 使用
-c compile 只编译不链接,生成 .o 文件
-S 只编译到汇编,生成 .s 文件
-E 只做预处理,展开宏和头文件
-O0/1/2/3/s Optimize 优化等级
-std= standard 指定 C 标准版本
-I Include 添加头文件搜索路径
-L Library path 添加库文件搜索路径
-l link 链接指定的库
-D Define 定义宏,如 -DDEBUG
-v verbose 显示详细编译过程
-save-temps 保留所有中间文件(.i.s.o

3. 警告与调试

# 建议始终带上 -Wall
gcc -Wall main.c -o main

# 更严格的警告
gcc -Wall -Wextra main.c -o main

# 把警告当错误,有警告就编译失败
gcc -Wall -Werror main.c -o main

# 生成调试信息,配合 gdb 使用
gcc -g main.c -o main
gdb ./main
💡 嵌入式项目推荐的警告组合

```bash
CFLAGS = -Wall -Wextra -Werror -Wshadow -Wconversion -Wstrict-prototypes
```

AddressSanitizer:内存错误检测利器

# 编译时加 -fsanitize=address,能检测数组越界、空指针等
gcc -g -fsanitize=address main.c -o main
// bug.c — 故意越界
int main(void) {
    int arr[5] = {1, 2, 3, 4, 5};
    return arr[10];  // 越界!
}
$ gcc -g -fsanitize=address bug.c -o bug && ./bug
# ERROR: AddressSanitizer: stack-buffer-overflow
#     #0 0x4005e7 in main bug.c:4  ← 直接告诉你第几行出错

4. 优化等级

选项 说明 适用场景
-O0 不优化(默认),编译快 开发调试
-O1 基本优化 一般使用
-O2 推荐优化,性能与安全的平衡 发布版本
-O3 激进优化,可能增大体积 性能敏感
-Os 优化体积(基于 -O2 但关闭增大体积的优化) 嵌入式(Flash 有限)
-Og 优化调试体验(GCC 4.8+) 调试阶段
gcc -O0 main.c -o main   # 调试用
gcc -Os main.c -o main   # 嵌入式用,最小体积
gcc -O2 main.c -o main   # 发布用
⚠️ 优化的"副作用"

高优化等级可能导致变量被优化掉、代码顺序变化。嵌入式中访问硬件寄存器必须用 volatile,否则编译器可能优化掉读写操作:

```c
// ❌ 编译器可能优化掉这个循环
while (*status_reg == 0) { }
// ✅ volatile 告诉编译器每次都要真的去读
while (*(volatile uint32_t *)status_reg == 0) { }
```


5. 头文件搜索路径

选项 作用 搜索范围
-I <dir> 添加头文件搜索路径 <>"" 都有效
-iquote <dir> 添加头文件搜索路径 "" 有效

搜索顺序:

包含方式 搜索顺序
#include <file> -I 目录 → 系统目录(/usr/include
#include "file" 当前目录 → -iquote 目录 → -I 目录 → 系统目录
gcc -I ./include -I ./drivers/inc main.c -o main

-D:命令行定义宏

# 等价于在代码开头写 #define DEBUG 和 #define VERSION 3
gcc -DDEBUG -DVERSION=3 main.c -o main
💡 嵌入式常用

```bash
gcc -DSTM32F407 -DHSE_VALUE=8000000 main.c
```


6. 多文件编译

# 方式1: 一步编译
gcc main.c utils.c -o app

# 方式2: 分步编译(大项目推荐,修改单文件只需重新编译该文件)
gcc -c main.c -o main.o
gcc -c utils.c -o utils.o
gcc main.o utils.o -o app

7. Makefile — 自动化多文件编译

文件多了每次手敲 gcc 太累,Makefile 帮你一个 make 搞定。

基本语法

目标: 依赖
    命令           # 注意: 必须用 Tab 缩进,不能用空格!

示例:编译 main.c + utils.c

CC = gcc
CFLAGS = -Wall -g

app: main.o utils.o
    $(CC) $^ -o $@

%.o: %.c
    $(CC) $(CFLAGS) -c $< -o $@

clean:
    rm -f *.o app

%.o: %.c模式规则,意思是"任何 .o 文件都由同名 .c 文件编译而来",不用再给每个文件单独写规则了。

使用方法

make          # 编译项目(只重新编译修改过的文件)
make clean    # 清理所有编译产物
make -j4      # 4线程并行编译(大项目更快)

常用变量

变量 含义 常见值
CC C 编译器 gcc
CFLAGS 编译选项 -Wall -g
LDFLAGS 链接选项 -lm -lpthread
$@ 当前目标名
$< 第一个依赖
$^ 所有依赖

进阶:区分调试/发布版本

CC = gcc
CFLAGS = -Wall -std=gnu11

ifeq ($(DEBUG), 1)
    CFLAGS += -g -Og -DDEBUG -fsanitize=address
    LDFLAGS += -fsanitize=address
else
    CFLAGS += -O2 -DNDEBUG
endif

app: main.o utils.o
    $(CC) $(LDFLAGS) $^ -o $@

%.o: %.c
    $(CC) $(CFLAGS) -c $< -o $@

clean:
    rm -f *.o app
make DEBUG=1    # 调试版本
make            # 发布版本

为什么用 Makefile?

手动编译 Makefile
每次敲一堆 gcc 命令 一个 make 搞定
全部重新编译 只重编修改过的文件
容易漏掉文件 自动处理依赖关系

8. 链接库

# 链接数学库 libm(使用 math.h 中的 sin/cos/sqrt 等)
gcc main.c -o main -lm

# 链接多线程库 libpthread
gcc main.c -o main -lpthread

# 指定头文件搜索路径
gcc -I./include main.c -o main

# 指定库文件搜索路径
gcc -L./lib main.c -o main -lmylib

静态链接 vs 动态链接

特性 静态链接(.a 动态链接(.so
链接时机 编译时 运行时
可执行文件大小 较大(包含库代码) 较小(只有引用)
运行依赖 (独立运行) 需要系统有 .so 文件
更新库 需重新编译 替换 .so 即可
嵌入式场景 裸机/RTOS 常用 嵌入式 Linux 常用
# 强制静态链接
gcc -static main.c -o main

# 查看动态库依赖
ldd main

9. C 语言标准

gcc -std=c99 main.c -o main    # C99(for循环内定义变量、// 注释)
gcc -std=c11 main.c -o main    # C11(多线程、泛型宏)
gcc -std=c17 main.c -o main    # C17(bug修复版)
gcc -std=gnu11 main.c -o main  # C11 + GNU 扩展(GCC 默认,支持 typeof 等)
💡 嵌入式推荐

裸机/RTOS 项目推荐 -std=gnu11,很多嵌入式头文件(如 CMSIS)依赖 C99/C11 特性和 GNU 扩展。

10. 嵌入式开发速查

交叉编译基本用法

# ARM Cortex-M(裸机)
arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb -Os main.c -o main.elf

# ARM Linux(开发板应用)
arm-linux-gnueabihf-gcc -Wall -O2 main.c -o main

嵌入式常用选项

选项 作用
-mcpu=cortex-m4 指定目标 CPU
-mthumb 使用 Thumb 指令集(代码更小)
-mfloat-abi=hard 硬件浮点
-mfpu=fpv4-sp-d16 指定浮点单元(Cortex-M4F)
-Os 优化代码体积(Flash 有限)
-ffunction-sections 每个函数单独成段
-fdata-sections 每个数据单独成段
-Wl,--gc-sections 链接时删除未使用的段(减小体积神器
-T xxx.ld 指定链接脚本(定义 Flash/RAM 布局)
-nostdlib 不链接标准库(裸机开发)
-Wl,-Map=output.map 生成 MAP 文件(查看内存分配)
-Wl,--print-memory-usage 打印 Flash/RAM 占用量

体积优化三件套

arm-none-eabi-gcc \
    -Os \                           # 优化体积
    -ffunction-sections -fdata-sections \  # 每个函数/数据单独成段
    -Wl,--gc-sections \             # 链接时裁剪未使用的段
    main.c -T stm32.ld -o firmware.elf

11. GCC 配套工具速查

工具 作用 常用命令
nm 查看符号表 nm -S main.o
objdump 反汇编 objdump -d -S main.o
readelf 查看 ELF 文件信息 readelf -a main.elf
size 查看各段大小(text/data/bss) size main.elf
strings 提取可打印字符串 strings firmware.bin
strip 删除调试信息(减小体积) strip main
objcopy 格式转换(ELF→BIN/HEX) objcopy -O binary main.elf main.bin
addr2line 地址→源码行号 addr2line -e main.elf 0x8001234
ar 创建静态库 ar rcs libfoo.a foo.o bar.o
ldd 查看动态库依赖 ldd main

12. 常见报错与排查

错误信息 阶段 原因 解决
fatal error: xxx.h: No such file 预处理 头文件路径未指定 添加 -I <dir>
implicit declaration of function 编译 函数未声明 包含正确的头文件
undefined reference to 'xxx' 链接 函数未定义或库未链接 添加 -l<lib> 或补源文件
multiple definition of 'xxx' 链接 同一符号多处定义 全局变量加 extern
cannot find -lxxx 链接 找不到库文件 添加 -L <dir> 或安装库
region 'FLASH' overflowed 链接 代码超出 Flash 容量 -Os + --gc-sections
Segmentation fault 运行 空指针/越界/栈溢出 -g -fsanitize=address 重编
error while loading shared libraries 运行 找不到动态库 设置 LD_LIBRARY_PATH

13. 实用场景速查

场景 命令
日常练习 gcc -Wall main.c -o main && ./main
调试段错误 gcc -g -Wall main.c -o main && gdb ./main
内存错误检测 gcc -g -fsanitize=address main.c -o main
看宏展开了啥 gcc -E main.c -o main.i
看汇编代码 gcc -S main.c -o main.s
保留所有中间文件 gcc -save-temps main.c -o main
用了 math.h 编译报错 gcc main.c -o main -lm
比较优化前后性能 gcc -O0 main.c -o v0 && gcc -O2 main.c -o v2
指定 C 标准 gcc -std=c99 -Wall main.c -o main
查看 gcc 版本 gcc --version
查看所有预定义宏 gcc -dM -E - < /dev/null
显示详细编译过程 gcc -v main.c -o main
📋 一句话速记

调试用 -g -Og -fsanitize=address -Wall发布用 -Os -DNDEBUG -Werror嵌入式加 -mcpu -mthumb -T xxx.ld --gc-sections

14. 自测题

看看你掌握了多少,答案都在文章里 👆

Q1:GCC 编译四阶段的顺序是什么?每步分别用什么选项单独执行?

预处理(-E)→ 编译(-S)→ 汇编(-c)→ 链接(默认)
口诀:**ESCo**(逃跑)

Q2:.i.s.o 三种中间文件,哪些是文本文件?哪些是二进制文件?

- .i — 文本(纯 C 代码,展开了头文件和宏)
- .s — 文本(汇编代码,人还能看懂)
- .o — **二进制**(机器码,记事本打开是乱码)

Q3:编译阶段和汇编阶段都是"翻译",它们的核心区别是什么?

- 编译 = **翻译 + 优化**(智能的,会做语法检查、常量折叠等优化)
- 汇编 = **一对一直译**(机械的,只是查表把助记符换成二进制,不做任何优化)

Q4:用了 math.h 中的 sin() 函数,编译时报 undefined reference to 'sin',怎么解决?

-lm 链接数学库:gcc main.c -o main -lm

Q5:-I-iquote 有什么区别?

- -I 是万能的,#include <file>#include "file" 都搜索
- -iquote 是专用的,**只对** #include "file" 有效

Q6:在 Makefile 中,$@$<$^ 分别代表什么?

- $@ — 当前目标名
- $< — 第一个依赖
- $^ — 所有依赖

Q7:嵌入式开发中,"体积优化三件套"是哪三个选项?

1. -Os(优化体积)
2. -ffunction-sections -fdata-sections(每个函数/数据单独成段)
3. -Wl,--gc-sections(链接时裁剪未使用的段)

Q8:静态链接和动态链接的最大区别是什么?嵌入式裸机通常用哪种?

- 静态链接:把库代码复制进可执行文件,文件大但独立运行
- 动态链接:运行时才去找库,文件小但依赖 .so 文件
- 嵌入式裸机/RTOS 通常用**静态链接**(没有操作系统来加载动态库)


15. 思考题

这些问题没有标准答案,动手试试或查资料,能帮你加深理解 🤔

1. 为什么嵌入式中访问硬件寄存器必须用 volatile
提示:想想编译器的优化阶段会对 while (*reg == 0) {} 做什么。

2. gcc -O2 main.c -o maingcc main.c -O2 -o main 的结果一样吗?GCC 选项的顺序重要吗?
提示:动手试试,对比生成的可执行文件。

3. 为什么大型项目用分步编译(先 -c 再链接)而不是一步编译?
提示:如果项目有 100 个 .c 文件,你只改了其中 1 个,两种方式分别需要编译多少个文件?

4. #include <stdio.h>#include "stdio.h" 都能编译通过,它们的搜索路径有什么不同?为什么推荐系统头文件用 <>

5. 用 gcc -save-temps main.c -o main 生成所有中间文件后,打开 main.i 看看有多少行。为什么一个只有 10 行的 C 文件,预处理后会变成几千行?

6. 链接器报 undefined reference 和编译器报 implicit declaration of function,虽然都和"函数找不到"有关,但本质原因不同。请解释区别。
提示:一个是找不到声明,一个是找不到定义(机器码)


参考资料