详解GCC编译器常用命令
- C语言进阶
- 16天前
- 259热度
- 0评论
GCC 从入门到实战:编译四阶段 + 常用命令速查
GCC(GNU Compiler Collection)是 Linux 下最常用的 C/C++ 编译器,也是嵌入式开发中不可或缺的工具。
基本格式:gcc [选项] 源文件 -o 输出文件
交叉编译器的用法和
gcc 完全一致,只是前缀不同:
arm-linux-gnueabihf-gcc— ARM Linuxaarch64-linux-gnu-gcc— ARM64 Linuxriscv64-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 → 10110001,ADD → 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 → -o(ESCo,谐音"逃跑")
文件后缀:.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 代码。
预处理器在这一步做三件事:
#include→ 把头文件内容原封不动搬进来(比如stdio.h里printf的声明)- 宏替换 → 把所有宏"文本替换"掉
- 删除注释 → 去掉所有
//和/* */
// 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 # 预处理

打开 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)直接优化成3,减少运行时计算 - 翻译 — 把 C 的
if、for、printf对应成汇编指令(MOV、ADD、CALL)
上一步生成了 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
汇编里的
.text、.rodata 是内存段,告诉链接器"这段内容放哪里":
.text— 代码段,存放可执行指令(通常只读+可执行).rodata— 只读数据段,存放字符串常量等(只读).data— 已初始化的全局/静态变量(可读写).bss— 未初始化的全局/静态变量(不占文件空间,运行时清零)
嵌入式中,链接脚本(-T xxx.ld)就是把这些段映射到具体的 Flash/RAM 地址。

.s是文本文件,记事本就能打开- 汇编语言和 CPU 架构绑定(ARM 和 x86 的指令完全不一样)
- 编译 = 翻译 + 优化,不只是简单替换
1.3 汇编(-c):汇编代码 → 机器码
这一步很"机械",没有优化,只是把每条汇编指令一对一直译成二进制数。
gcc -c main.s -o main.o # 汇编生成机器码文件
gcc -c main.c -o main.o # 直接从源码汇编生成机器码,GCC 会自动先预处理、编译
汇编器(as)把每条指令对应成 CPU 能直接执行的二进制数,比如:
MOV→01010001ADD→00101100
.o是二进制文件,记事本打开是乱码,但 CPU 能识别- 只做直译,不做任何优化(优化都在编译阶段完成)
.o还不能直接运行 — 缺少库函数(如printf)的实现,需要链接后才能变成可执行文件- 可用
nm main.o查看符号表,objdump -d main.o反汇编查看机器码

1.4 链接:把所有 .o 拼成可执行文件
你的代码里写了 printf,但你只是"调用"了它,真正的实现在标准库(libc)里。链接就是把你的代码和库的代码拼到一起。
gcc main.o -o main # 链接生成可执行文件
链接器(ld)在这一步做的事:
- 合并目标文件 — 多个
.o文件合并成一个(多文件项目时) - 解析符号引用 — 你的代码调用了
printf,链接器去libc里找到它的机器码,填上地址 - 生成可执行文件 — 加上程序入口地址、内存布局等信息,生成最终能运行的文件
- 如果调用了库函数却没链接对应的库,这一步会报
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 main 和 gcc 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,虽然都和"函数找不到"有关,但本质原因不同。请解释区别。
提示:一个是找不到声明,一个是找不到定义(机器码)。