一. GCC 核心认知:编译的四个阶段(ESc-iso速记)
GCC 编译 C/C++ 程序并非一步到位,而是分为预处理、编译、汇编、链接四个阶段,每个阶段完成特定任务,最终生成可执行文件。
1.1 阶段 1:预处理
- 核心任务:宏替换、删除注释、条件编译、头文件展开(不进行语法检查);
- 关键选项:
-E(仅执行预处理,停止后续编译); - 输出文件:
.i后缀(预处理后的 C 文件); - 实操命令:
[Lotso@VM-0-3-centos lesson9]$ vim hello.c [Lotso@VM-0-3-centos lesson9]$ gcc -E hello.c -o hello.i [Lotso@VM-0-3-centos lesson9]$ ll total 24 -rw-rw-r-- 1 Lotso Lotso 382 Nov 13 16:50 hello.c -rw-rw-r-- 1 Lotso Lotso 17170 Nov 13 16:50 hello.i- 示例:#include <stdio.h>会被展开为 stdio.h 的全部内容,#define MAX 10会替换所有MAX为 10。
1.2 阶段 2:编译
- 核心任务:检查语法错误,将预处理后的代码(
.i)转换为汇编语言代码; - 关键选项:
-S(仅执行编译,停止后续流程); - 输出文件:
.s后缀(汇编文件); - 实操命令:
[Lotso@VM-0-3-centos lesson9]$ gcc -S hello.c -o hello.s [Lotso@VM-0-3-centos lesson9]$ ll total 28 -rw-rw-r-- 1 Lotso Lotso 327 Nov 13 16:54 hello.c -rw-rw-r-- 1 Lotso Lotso 17104 Nov 13 16:54 hello.i -rw-rw-r-- 1 Lotso Lotso 1094 Nov 13 17:01 hello.s说明:生成的汇编代码与 CPU 架构相关(如 x86_64、ARM),可直接查看指令逻辑。
1.3 阶段 3:汇编(Assembly)
- 核心任务:将汇编代码(
.s)转换为机器可识别的二进制目标文件; - 关键选项:
-c(仅执行汇编,生成目标文件); - 输出文件:
.o后缀(目标文件,二进制格式,不可执行); - 实操命令:
[Lotso@VM-0-3-centos lesson9]$ gcc -c hello.s -o hello.o [Lotso@VM-0-3-centos lesson9]$ ll total 32 -rw-rw-r-- 1 Lotso Lotso 327 Nov 13 16:54 hello.c -rw-rw-r-- 1 Lotso Lotso 17104 Nov 13 16:54 hello.i -rw-rw-r-- 1 Lotso Lotso 2184 Nov 13 17:05 hello.o -rw-rw-r-- 1 Lotso Lotso 1094 Nov 13 17:01 hello.s[Lotso@VM-0-3-centos lesson9]$ objdump -d hello.o hello.o: file format elf64-x86-64 Disassembly of section .text: 0000000000000000 <main>: 0: 55 push %rbp 1: 48 89 e5 mov %rsp,%rbp 4: be 0a 00 00 00 mov $0xa,%esi 9: bf 00 00 00 00 mov $0x0,%edi e: b8 00 00 00 00 mov $0x0,%eax 13: e8 00 00 00 00 callq 18 <main+0x18> 18: be 0a 00 00 00 mov $0xa,%esi 1d: bf 00 00 00 00 mov $0x0,%edi 22: b8 00 00 00 00 mov $0x0,%eax 27: e8 00 00 00 00 callq 2c <main+0x2c> 2c: be 0a 00 00 00 mov $0xa,%esi 31: bf 00 00 00 00 mov $0x0,%edi 36: b8 00 00 00 00 mov $0x0,%eax 3b: e8 00 00 00 00 callq 40 <main+0x40> 40: be 0a 00 00 00 mov $0xa,%esi 45: bf 00 00 00 00 mov $0x0,%edi 4a: b8 00 00 00 00 mov $0x0,%eax 4f: e8 00 00 00 00 callq 54 <main+0x54> 54: be 0a 00 00 00 mov $0xa,%esi 59: bf 00 00 00 00 mov $0x0,%edi 5e: b8 00 00 00 00 mov $0x0,%eax 63: e8 00 00 00 00 callq 68 <main+0x68> 68: be 0a 00 00 00 mov $0xa,%esi 6d: bf 00 00 00 00 mov $0x0,%edi 72: b8 00 00 00 00 mov $0x0,%eax 77: e8 00 00 00 00 callq 7c <main+0x7c> 7c: be 0a 00 00 00 mov $0xa,%esi 81: bf 00 00 00 00 mov $0x0,%edi 86: b8 00 00 00 00 mov $0x0,%eax 8b: e8 00 00 00 00 callq 90 <main+0x90> 90: be 0a 00 00 00 mov $0xa,%esi 95: bf 00 00 00 00 mov $0x0,%edi 9a: b8 00 00 00 00 mov $0x0,%eax 9f: e8 00 00 00 00 callq a4 <main+0xa4> a4: be 0a 00 00 00 mov $0xa,%esi a9: bf 00 00 00 00 mov $0x0,%edi ae: b8 00 00 00 00 mov $0x0,%eax b3: e8 00 00 00 00 callq b8 <main+0xb8> b8: be 0a 00 00 00 mov $0xa,%esi bd: bf 00 00 00 00 mov $0x0,%edi c2: b8 00 00 00 00 mov $0x0,%eax c7: e8 00 00 00 00 callq cc <main+0xcc> cc: be 0a 00 00 00 mov $0xa,%esi d1: bf 00 00 00 00 mov $0x0,%edi d6: b8 00 00 00 00 mov $0x0,%eax db: e8 00 00 00 00 callq e0 <main+0xe0> e0: b8 00 00 00 00 mov $0x0,%eax e5: 5d pop %rbp e6: c3 retq- 说明:目标文件包含机器指令,但缺少库依赖(如 printf 函数的实现),无法直接运行。
[Lotso@VM-0-3-centos lesson9]$ ./hello.o -bash: ./hello.o: Permission denied1.4 阶段 4:链接(Linking)
核心任务:将目标文件(.o)与系统库、第三方库链接,生成可执行文件;
核心逻辑:解析未定义符号(如 printf),从库文件(如 libc.so)中找到实现并关联;
输出文件:默认名为a.out(可通过-o指定名称);
实操命令:
[Scy@VM-0-3-centos lesson9]$ gcc hello.o -o hello.exe [Scy@VM-0-3-centos lesson9]$ ll total 44 -rw-rw-r-- 1 Scy Scy 327 Nov 13 16:54 hello.c -rwxrwxr-x 1 Scy Scy 8360 Nov 13 17:11 hello.exe -rw-rw-r-- 1 Scy Scy 17104 Nov 13 16:54 hello.i -rw-rw-r-- 1 Scy Scy 2185 Nov 13 17:05 hello.o -rw-rw-r-- 1 Scy Scy 1094 Nov 13 17:01 hello.s [Scy@VM-0-3-centos lesson9]$ ./hello.exe 10 10 10 10 10 10 10 10 10 10 101.5 一键编译(合并四阶段)
日常开发中无需分步执行,GCC支持一键完成四阶段编译:
# 也可以 gcc -o hello.exe1 hello.c [Scy@VM-0-3-centos lesson9]$ gcc hello.c -o hello.exe1 [Scy@VM-0-3-centos lesson9]$ ll total 56 -rw-rw-r-- 1 Scy Scy 327 Nov 13 16:54 hello.c -rwxrwxr-x 1 Scy Scy 8360 Nov 13 17:11 hello.exe -rwxrwxr-x 1 Scy Scy 8360 Nov 13 17:15 hello.exe1 -rw-rw-r-- 1 Scy Scy 17104 Nov 13 16:54 hello.i -rw-rw-r-- 1 Scy Scy 2185 Nov 13 17:05 hello.o -rw-rw-r-- 1 Scy Scy 1094 Nov 13 17:01 hello.s [Scy@VM-0-3-centos lesson9]$ ./hello.exe1 10 10 10 10 10 10 10 10 10 10 10本质是 GCC 自动依次执行预处理→编译→汇编→链接,隐藏了中间文件。
二. GCC核心功能选项 && 条件编译 && 编译器和语言的历史(理解编译的过程)
2.1 核心功能速查表:
| 选项 | 功能描述 | 使用示例 |
|---|---|---|
| -E | 只进行预处理,不编译、汇编和链接 | gcc -E main.c -o main.i |
| -S | 编译到汇编语言,不进行汇编和链接 | gcc -S main.c -o main.s |
| -c | 编译到目标代码,生成.o文件 | gcc -c main.c -o main.o |
| -o | 指定输出文件名 | gcc main.c -o myapp |
| -static | 使用静态链接生成可执行文件 | gcc main.c -static -o app_static |
| -g | 生成调试信息,供GDB使用 | gcc -g main.c -o debug_app |
| -shared | 尽量使用动态库,生成较小的文件 | gcc -shared lib.c -o lib.so |
| -O0 | 不进行优化(编译速度最快) | gcc -O0 main.c -o app |
| -O1 | 基本优化(默认级别) | gcc -O1 main.c -o app |
| -O2 | 较多优化,平衡性能与编译时间 | gcc -O2 main.c -o app |
| -O3 | 最高级别优化(可能增加代码大小) | gcc -O3 main.c -o app |
| -w | 禁止所有警告信息 | gcc -w main.c -o app |
| -Wall | 开启所有常见警告信息 | gcc -Wall main.c -o app |
2.2 条件编译的逻辑和应用场景
2.3 编译的过程:编译器和语言的历史![]()
![]()
关键逻辑:
- 为什么任何语言最后都要变成二进制 -> 因为编译器只认识它
- 编译的过程为何是那样的 -> 上面的图中有解释
- 一定是先有语言再有编译器的。并且语言和编译器之间是一个自举的关系
三. 关键原理:静态链接和动态链接
链接阶段的核心是"关联库文件",GCC支持两种链接方式,各有优劣
3.1 静态链接(Static Linking)
- 原理:编译时将依赖的库文件(如
libc.a)全部复制到可执行文件中; - 关键选项:
-static; - 实操命令:
gcc hello.c -o hello_static -static– 强制静态链接生成可执行文件(因为GCC默认是动态链接的) - 安装命令:
sudo yum install glibc-static libstdc++-static -y
- 优点:可执行文件不依赖系统库,移植性强。
- 缺点:会让可执行程序变大,运行的时候,加载到内存:占据更多的内存空间,浪费资源!!!
3.2 动态链接(Dynamic Linking)
- 原理:编译时仅记录库依赖(如
libc.so),程序运行时才加载库文件; - 特点:GCC 默认采用动态链接;
- 特点:GCC 默认采用动态链接;
- 实操命令:
gcc hello.c -o hello_dynamic– 动态链接(默认) - 优点:文件体积小,库文件被多个程序共享,节省内存和磁盘空间;
- 缺点:依赖系统中对应的库文件,一旦库文件缺失,所有程序都无法运行了。
3.2 查看依赖的库文件
通过ldd可以查看可执行程序依赖的动态库
# 动态链接的文件 [Lotso@VM-0-3-centos lesson10]$ ldd hello_dynamic linux-vdso.so.1 => (0x00007ffe61b15000) libc.so.6 => /lib64/libc.so.6 (0x00007f2e519cd000) /lib64/ld-linux-x86-64.so.2 (0x00007f2e51d9b000) # 静态链接的文件 [Lotso@VM-0-3-centos lesson10]$ ldd hello_static not a dynamic executable3.3 通过一个故事感性的理解一下库
3.3.1 阶段一:动态库的故事(共享网吧模式)
小王是一中的新生,他学习刻苦,但也需要偶尔上网放松。一中管理严格,但小王从学长张三那里得知,学校东门左转100米再右转100米有一家“小蚂蚁电竞馆”。小王记下地址,在某个周六中午,他按计划完成部分作业后,前往电竞馆上网。老板给他开了编号1234的电脑,小王愉快地度过了中午。下午2:30,他回到学校继续学习。
后来,越来越多的学生知道了电竞馆,纷纷前去放松。直到月考,全班成绩下滑,校长调查后举报导致了电竞馆关闭。
✅️阶段一理解:
小王(程序)在一中(内存)学习。
张三(编译器/链接器)告诉小王电竞馆的地址(动态库的位置)。
小王去电竞馆上网(程序运行时链接动态库)。
老板给小王开1234号电脑(动态库分配给程序一个函数地址)。
电竞馆关闭(动态库缺失)导致所有学生无法上网(程序无法运行)。
声明:这里是为了方便故事举例说明哈,动态库其实也是在内存里的
图示理解:
┌─────────────┐ 询问地址 ┌─────────────┐ │ 小王 │ ──────────────> │ 张三 │ │ (程序) │ │ (编译器/链接器)│ └─────────────┘ └─────────────┘ │ │ │ 获得地址:"东门左转100m右转100m" │ │ ▼ ▼ ┌─────────────────────────────────────────────┐ │ 一中 │ │ (内存) │ └─────────────────────────────────────────────┘ │ │ 按计划执行:作业→上网→作业 │ ▼ ┌─────────────┐ 上网请求 ┌─────────────┐ │ 小王 │ ──────────────> │ 小蚂蚁电竞馆 │ │ (程序) │ │ (动态库) │ └─────────────┘ └─────────────┘ │ │ 分配1234号电脑 │ (库函数地址) ▼ ┌─────────┐ │ 愉快上网 │ │ (函数执行)│ └─────────┘动态库特点:
✅优点:资源共享,节省内存(多个程序共用同一个库)
❌缺点:库缺失则所有依赖程序都无法运行
3.3.2 阶段二:静态库的故事(自带电脑模式)
小王因不能上网而学习状态不佳,父亲了解情况后,与校长协商并去小蚂蚁电竞馆找老板买下那台1234号电脑,放在学校供小王使用。此后,小王和同学们都可以在学校内直接上网,无需外出。
✅️阶段二理解:
小王的父亲和电竞馆老板(静态库的提供者)将电脑(库函数)买下并安置在学校(静态链接)。
小王和同学们(多个程序)每人都拥有了一份电脑的拷贝(静态链接时每个程序都包含一份库代码)。
现在上网(使用库函数)不依赖校外电竞馆(外部动态库),但每个人都需要占用自己的电脑(内存空间变大)。
图示理解:
┌─────────────┐ 成绩下滑 ┌─────────────┐ │ 小王 │ ──────────────> │ 爸爸 │ │ (程序) │ │ (静态链接器) │ └─────────────┘ └─────────────┘ │ │ │ 协商购买 │ │ │ ▼ ▼ ┌─────────────┐ 购买1234电脑 ┌─────────────┐ │ 校长 │ <────────────── │ 电竞馆老板 │ │ (系统环境) │ │ (库提供者) │ └─────────────┘ └─────────────┘ │ │ 电脑安置在学校 │ ▼ ┌─────────────────────────────────────────────┐ │ 一中 │ │ (内存) │ └─────────────────────────────────────────────┘ │ │ 每个学生都自带电脑 │ (程序各自包含库代码) ▼ ┌─────────────┐ 直接使用 ┌─────────────┐ │ 小王和室友 │ ──────────────> │ 个人电脑 │ │ (程序) │ │ (静态库) │ └─────────────┘ └─────────────┘静态库特点:
✅ 优点:程序独立,不依赖外部环境
❌ 缺点:每个程序都包含库代码,占用更多存储空间和内存
3.3.3 核心概念对比 && 整体总结
| 特性 | 动态库(小蚂蚁电竞馆) | 静态库(个人电脑) |
|---|---|---|
| 资源使用 | 共享,节省资源 | 独占,浪费资源 |
| 依赖性 | 强依赖,库缺失则全崩 | 独立,自给自足 |
| 更新维护 | 更新库即更新所有程序 | 每个程序需重新编译 |
| 内存占用 | 多个程序共享同一份 | 每个程序都有一份拷贝 |
现实世界类比:
编程世界: 现实世界: ┌─────────────────┐ ┌─────────────────┐ │ 动态库 │ │ 共享网吧 │ │ (共享函数集合) │ │ (共享电脑资源) │ └─────────────────┘ └─────────────────┘ │ │ │ 多个程序共用 │ 多个顾客共用 │ 库缺失=所有程序挂掉 │ 网吧关门=所有人都不能上网 │ │ ┌─────────────────┐ ┌─────────────────┐ │ 静态库 │ │ 个人电脑 │ │ (函数嵌入程序内) │ │ (自有电脑资源) │ └─────────────────┘ └─────────────────┘ │ │ │ 每个程序都包含库代码 │ 每个人都有自己的电脑 │ 程序体积大但独立运行 │ 随时可用但占用更多空间整体总结:
通过这两个故事,我们理解了动态库和静态库的区别:
动态库:像是一个共享的电竞馆,多个程序在运行时共享同一个库,节省空间,但一旦库被删除,所有程序都无法运行。
静态库:像是每个人都有自己的电脑,程序在编译时就将库代码复制到可执行文件中,因此程序体积大,但可以独立运行,不依赖外部库。
在编程中,我们根据需求选择使用动态库还是静态库。动态库适合多个程序共享,减少内存占用;静态库适合程序独立发布,避免依赖问题。