GCC编译(1)入门概述
Author: Once Day Date: 2026年2月13日
一位热衷于Linux学习和开发的菜鸟,试图谱写一场冒险之旅,也许终点只是一场白日梦...
漫漫长路,有人对你微笑过嘛...
全系列文章可参考专栏: 编译构建工具链_Once-Day的博客-CSDN博客
参考文章:
文章目录
- GCC编译(1)入门概述
-
-
-
- [1. GCC概述](#1. GCC概述)
- [2. GCC编译流程](#2. GCC编译流程)
- [3. GCC 编译组件](#3. GCC 编译组件)
- [4. GCC工具组件](#4. GCC工具组件)
- [5. GCC和G++的区别](#5. GCC和G++的区别)
- [6. 指定C/C++编译版本](#6. 指定C/C++编译版本)
- [7. GCC一步编译和分布编译](#7. GCC一步编译和分布编译)
-
- [5.1 预处理选项(gcc -E)](#5.1 预处理选项(gcc -E))
- [5.2 编译成汇编代码文件(gcc -S)](#5.2 编译成汇编代码文件(gcc -S))
- [5.3 编译成目标文件(gcc -c)](#5.3 编译成目标文件(gcc -c))
- [5.4 链接指定文件](#5.4 链接指定文件)
- [5.5 一些常用的其他gcc命令](#5.5 一些常用的其他gcc命令)
- [8. GCC编译静态库和动态库](#8. GCC编译静态库和动态库)
-
-
1. GCC概述
GCC 最初的全称为 GNU C Compiler,诞生于 GNU 计划早期阶段,目标是为自由软件生态提供一款高质量、可移植的 C 语言编译器。在 20 世纪 80 年代末,商业编译器占据主导地位,GCC 的出现不仅填补了自由编译工具的空白,也为后续类 Unix 系统的软件构建奠定了技术基础。其早期设计强调标准兼容性与跨平台能力,这一理念至今仍贯穿整个体系结构。
随着软件工程规模的扩大,单一语言工具链已难以满足需求。GCC 逐步扩展前端架构,支持 C++、Objective-C、Fortran、Go 等多种语言,名称也演进为 GNU Compiler Collection。其内部通过统一的中间表示 GIMPLE 和后端优化框架实现语言解耦,使不同语言能够共享优化与代码生成模块。这种"多前端、共享后端"的架构设计,是 GCC 能长期演进的重要原因。
在主流 Linux 发行版中,GCC 通常作为基础开发工具预装,配合 glibc、binutils 构成完整构建环境。可以通过如下方式查看版本信息:
bash
gcc --version
若系统默认版本偏低,开发者可手动编译安装新版本 GCC。该过程通常需要现有 GCC 参与构建,并依赖 gmp、mpfr、mpc 等数学库。典型流程如下:
bash
./configure --prefix=/usr/local/gcc-13
make -j$(nproc)
make install
由于编译阶段包含多语言前端与优化器构建,整体耗时较长,对硬件资源要求较高。在工程实践中,往往通过软件仓库升级、容器环境或预编译工具链来替代源码编译,以提高效率并降低维护成本。
2. GCC编译流程
C语言编译流程如下所示:

gcc 在编译 C 语言程序时,本质上执行的是一个分阶段的转换流程,将源代码逐步转化为可执行的机器指令。整个过程可以划分为预处理、编译、汇编和链接四个主要阶段,每个阶段都有明确的输入与输出形式,并由不同的工具协同完成。
第一阶段是预处理,由 cpp 完成。它负责展开宏定义、处理 #include 头文件、条件编译指令以及删除注释等操作。该阶段并不生成机器代码,而是输出一个纯净的中间文本文件,通常以 .i 结尾。可以使用如下命令单独查看预处理结果:
bash
gcc -E main.c -o main.i
第二阶段是编译阶段,gcc 会将预处理后的 C 代码转换为汇编代码。在此过程中,语法分析、语义分析以及中间表示优化都会完成。优化级别(如 -O2)也主要在这一阶段发挥作用。生成的汇编文件通常以 .s 结尾:
bash
gcc -S main.i -o main.s
第三阶段是汇编阶段,由 as 将汇编代码翻译为目标文件,即二进制形式的 .o 文件。此时生成的目标文件已包含机器指令,但尚未解决符号引用问题:
bash
gcc -c main.s -o main.o
最后是链接阶段,由 ld 将多个目标文件与标准库(如 libc)进行符号解析与地址重定位,生成最终可执行文件。默认情况下,gcc 会自动链接标准 C 运行时库:
bash
gcc main.o -o main
整个流程可以抽象为:
main.c
Preprocess
Compile
Assemble
Link
Executable
这种分阶段设计不仅便于调试与问题定位,也使开发者可以通过不同选项精细控制构建行为。
3. GCC 编译组件
| 组件部分 | 描述 |
|---|---|
| c++ | gcc 的一个版木,默认语言设置为 C++,而且在连接的时候自动包含标准 C++ 库。这和 g++ 一样 |
| ccl | 实际的C编译程序 |
| cclplus | 实际的 C++ 编泽程序 |
| collect2 | 在不使用 GNU 连接程序的系统上,有必要运行 collect2 来产生特定的全局初始化代码(例如 C++ 的构造函数和析构函数) |
| configure | GCC 源代码树根目录中的一个脚木。用于设置配置值和创建GCC 编译程序必需的 make 程序的描述文件 |
| crt0.o | GCC 源代码树根目录中的一个脚木。用于设置配置值和创建GCC 编译程序必需的 make 程序的描述文件 |
| cygwin1.dll | Windows 的共享库提供的 API,模拟 UNIX 系统调用 |
| f77 | 该驱动程序可用于编译 Fortran |
| f771 | 实际的 Fortran 编译程序 |
| g++ | gcc 的一个版木,默认语言设置为 C++,而且在连接的时候自动包含标准 C++ 库。这和 c++ 一样 |
| gcc | 该驱动程序等同于执行编译程序和连接程序以产生需要的输出 |
| gcj | 该驱动程序用于编译 Java |
| gnat1 | 实际的 Ada 编译程序 |
| gnatbind | 一种工具,用于执行 Ada 语言绑定 |
| gnatlink | 一种工具,用于执行 Ada 语言连接 |
| jc1 | 实际的 Java 编译程序 |
| libgcc | 该库包含的例程被作为编泽程序的一部分,是因为它们可被连接到实际的可执行程序中。 它们是特殊的例程,连接到可执行程序,来执行基木的任务,例如浮点运算。这些库中的例程通常都是平台相关的 |
| libgcj | 运行时库包含所有的核心 Java 类 |
| libobjc | 对所有 Objective-C 程序都必须的运行时库 |
| libstdc++ | 运行时库,包括定义为标准语言一部分的所有的 C++ 类和函数 |
4. GCC工具组件
| 工具软件 | 描述 |
|---|---|
| addr2line | 给出一个可执行文件的内部地址,addr2line 使用文件中的调试信息将地址翻泽成源代码文件名和行号。该程序是 binutils 包的一部分 |
| ar | -11 |
| as | GNU 汇编器。实际上它是一族汇编器,因为它可以被编泽或能够在各种不同平台上工作。 该程序是 binutils 包的一部分 |
| autoconf | 产生的 shell 脚木自动配置源代码包去编泽某个特定版木的 UNIX |
| c++filt | 程序接受被 C++ 编泽程序转换过的名字(不是被重载的),而且将该名字翻泽成初始形式。 该程序是 binutils 包的一部分 |
| f2c | 是 Fortran 到C的翻译程序。不是 GCC 的一部分 |
| gcov | gprof 使用的配置工具,用来确定程序运行的时候哪一部分耗时最大 |
| gdb | GNU 调试器,可用于检查程序运行时的值和行为 |
| GNATS | GNU 的调试跟踪系统(GNU Bug Tracking System)。一个跟踪 GCC 和其他 GNU 软件问题的在线系统 |
| gprof | 该程序会监督编泽程序的执行过程,并报告程序中各个函数的运行时间,可以根据所提供 的配置文件来优化程序。该程序是 binutils 包的一部分 |
| ld | GNU 连接程序。该程序将目标文件的集合组合成可执行程序。该程序是 binutils 包的一部 |
| libtool | 一个基本库,支持 make 程序的描述文件使用的简化共享库用法的脚木 |
| make | 一个工具程序,它会读 makefile 脚木来确定程序中的哪个部分需要编泽和连接,然后发布必要的命令。它读出的脚木(叫做 makefile 或 Makefile)定义了文件关系和依赖关系。 |
| nlmconv | 将可重定位的目标文件转换成 NetWare 可加载模块(NetWare Loadable Module, NLM)。该 程序是 binutils 的一部分 |
| nm | 列出目标文件中定义的符号。该程序是 binutils 包的一部分 |
| objcopy | 将目标文件从一种二进制格式复制和翻译到另外一种。该程序是 binutils 包的一部分 |
| objdump | 显示一个或多个目标文件中保存的多种不同信息。该程序是 binutils 包的一部分 |
| ranlib | 创建和添加到 ar 文档的索引。该索引被 Id 使用来定位库中的模块。该程序是 binutils 包的一部分 |
| ratfor | Ratfor 预处理程序可由 GCC 激活,但不是标准 GCC 发布版的一部分 |
| readelf | 从 ELF 格式的目标文件显示信息。该程序是 binutils 包的一部分 |
| size | 列出目标文件中每个部分的名字和尺寸。该程序是 binutils 包的一部分 |
| strings | 浏览所有类型的文件,析取出用于显示的字符串。该程序是 binutils 包的一部分 |
| strip | 从目标文件或文档库中去掉符号表,以及其他调试所需的信息。该程序是 binutils 包的一部分 |
| vcg | Ratfor 浏览器从文木文件中读取信息,并以图表形式显示它们。而 vcg 工具并不是 GCC 发布中的一部分,但 -dv 选项可被用来产生 vcg 可以理解的优化数据的格式 |
| windres | Window 资源文件编泽程序。该程序是 binutils 包的一部分 |
5. GCC和G++的区别
在 GNU 工具链中,gcc 与 g++ 本质上共用同一套编译框架与后端优化器,但它们的"默认行为"不同。两者都可以编译 C++ 代码,但 g++ 是为 C++ 语言场景定制的驱动程序,在编译阶段选择 C++ 前端,在链接阶段自动引入 C++ 运行时库。这种差异主要体现在默认参数与链接策略上,而非底层实现能力的不同。
从语言识别角度看,gcc 根据文件扩展名判断使用的前端,例如 .c 使用 C 前端,.cpp 使用 C++ 前端。但如果强制指定参数,二者可以互相替代。例如:
bash
gcc -xc++ demo.cpp
上述命令等价于使用 C++ 前端进行编译。也就是说,g++ 在语法分析与代码生成层面并不具备"额外能力",其优势在于简化使用流程。
真正关键的差异发生在链接阶段。C++ 程序通常依赖 libstdc++ 以及 C++ 运行时支持库,而 g++ 会自动添加 -lstdc++ 和相关启动文件;gcc 在链接时则默认按 C 程序处理,不会自动链接 C++ 标准库。这也是为何直接使用 gcc demo.cpp 可能出现未定义引用错误。等价关系可以近似表示为:
bash
g++ demo.cpp
≈ gcc -xc++ demo.cpp -lstdc++ -shared-libgcc
在实际工程中,若项目包含 C++ 代码,通常统一使用 g++ 作为链接驱动,以避免遗漏标准库依赖;而在混合 C/C++ 项目中,也可以显式控制编译与链接阶段,例如分别使用 gcc -c 编译 C 文件,再用 g++ 完成最终链接。这种策略能够确保符号解析与运行时环境匹配。
6. 指定C/C++编译版本
在实际开发中,GCC 允许通过标准选项显式指定 C 或 C++ 语言标准版本,从而控制语法特性与编译行为。这种机制对于保证跨平台一致性与老项目兼容性尤为重要。不同标准版本在语义规则、关键字支持以及库特性上存在明显差异,因此在构建系统中显式声明标准版本已成为工程实践中的基本要求。
对于 C 语言,可通过 -std= 选项指定标准版本,例如 c89、c99、c11、c17 以及较新的 c23。示例如下:
bash
gcc -std=c11 main.c -o main
若希望启用 GNU 扩展(如语句表达式、内联汇编扩展等),可以使用 gnu11、gnu17 等模式。gnuXX 在对应 ISO 标准基础上增加 GNU 特性,而 cXX 则严格遵循标准。
对于 C++,同样使用 -std= 参数控制版本,例如 c++98、c++11、c++14、c++17、c++20、c++23。示例:
bash
g++ -std=c++20 demo.cpp -o demo
若项目依赖 GNU 扩展(如 typeof 或部分实验性特性),可以选择 gnu++20 等模式。需要注意,标准版本的选择不仅影响语法,还会影响标准库接口可见性,例如 <filesystem> 需要 c++17 及以上。
在大型项目中,通常在构建系统(如 Makefile 或 CMake)中统一指定标准版本,以避免不同模块编译选项不一致导致 ABI 或行为差异。例如:
makefile
CXXFLAGS = -std=c++17 -O2 -Wall
常见版本对C语言编译标准的支持:
| GCC版本 | c89/c90 | c99 | c11 | c17 | GNU90 | GNU99 | GNU11 | GNU17 |
|---|---|---|---|---|---|---|---|---|
| 8.4以上 | c89/c90 | c99 | c11 | c17/c18 | gnu90/gnu89 | gnu99 | gnu11 | gnu17/gnu18 |
| 7.5-5.5 | c89/c90 | c99 | c11 | gnu90/gnu89 | gnu99 | gnu11 | ||
| 4.9.4-4.8.5 | c89/c90 | c99 | c11 | gnu90/gnu99 | gnu99 | gnu11 | ||
| 4.7.4以下 | c89/c90 | c99(部分支持) | c11(部分支持) | gnu90/gnu89 | gnu99(部分支持) | gnu11(部分支持) |
常见版本对C++语言标准的支持程度:
| GCC版本 | c++98/03 | c++11 | c++14 | c++17 | GNU++98 | GNU++11 | GNU++14 | GNU++17 |
|---|---|---|---|---|---|---|---|---|
| 8.4以上 | c++98/c++03 | c++11 | c++14 | c++17 | gnu++98/gnu++03 | gnu++11 | gnu++14 | gnu++17 |
| 7.5-5.5 | c++98/c++03 | c++11 | c++14 | c++1z(部分支持) | gnu++98/gnu++03 | gnu++11 | gnu++14 | gnu++1z(部分支持) |
| 4.9.4-4.8.5 | c++98/c++03 | c++11 | c++1y(部分支持) | gnu++98/gnu++03 | gnu++11 | gnu++1y(部分支持) | ||
| 4.7.4 | c++98 | c++11(部分支持) | gnu++98 | gnu++11(部分支持) |
7. GCC一步编译和分布编译
直接使用 gcc demo.c 时,编译器会自动完成预处理、编译、汇编与链接全过程,并在当前目录生成默认可执行文件 a.out。这种"一步到位"的方式适合小型示例或临时测试。通过 -o 选项可以指定输出文件名,使构建结果更具可读性与可管理性:
bash
gcc demo.c -o demo
上述命令本质上仍然包含完整链接过程,只是改变了最终产物名称。
在工程实践中,往往采用分步编译模式以提升构建效率与可维护性。首先生成头文件依赖关系,用于支持增量编译。当某个头文件变更时,仅重建相关目标文件,而无需全量重编译。常见做法如下:
bash
gcc -MMD -MP -c demo.c -o demo.o
其中 -MMD 会生成 .d 依赖文件,记录头文件引用关系,便于 make 自动追踪依赖。
第二步生成目标文件(.o),此阶段仅进行编译与汇编,不做链接:
bash
gcc -c demo.c -o demo.o
目标文件中包含机器指令与未解析符号信息,可与其他模块独立编译生成的 .o 文件组合使用。
最后由链接器统一处理多个目标文件与库文件,完成符号解析与重定位,生成可执行程序:
bash
gcc demo.o util.o -o app
这种分步流程在多文件项目中尤为关键,可以显著减少重复编译时间,同时为后续引入静态库或动态库提供良好的结构基础。
5.1 预处理选项(gcc -E)
预处理过程会处理预处理命令,删除注释等。展开成完整的源文件。
通常可由以下几种形式:
shell
gcc -E demo.c #直接输出到终端上
gcc -E demo.c -o demo.i #直接输出到指定文件中
gcc -E -C demo.c -o demo.i #取消删除源文件中的注释代码
gcc -E支持的常用选项:
| 选项 | 功能 |
|---|---|
| -D name[=definition] | 在处理源文件之前,先定义宏 name。宏 name 必须是在源文件和头文件中都没有被定义过的。将该选项搭配源代码中的#ifdef name命令使用,可以实现条件式编译。如果没有指定一个替换的值(即省略 =definition),该宏被定义为值 1。 |
| -U name | 如果在命令行或 GCC 默认设置中定义过宏 name,则"取消"name 的定义。-D 和 -U 选项会依据在命令行中出现的先后顺序进行处理。 |
| -include file | 如同在源代码中添加 #include "file" 一样。 |
| -l dir | 同时适用于以引号 "" 和 <> 导入的头文件。当 GCC 在 -iquote 指令指定的目录下搜索头文件失败时,会再自动去 -I 指定的目录中查找。该选项在 GCC 10.1 版本中已被弃用,并建议用 -iquote 选项代替。 |
| -isystem dir | 指定搜索头文件的目录 |
| -idirafter dir | 指定搜索头文件的目录 |
其中,对于指定 #include 搜索路径的几个选项,作用的先后顺序如下:
-
对于用 #include "" 引号形式引入的头文件,首先搜索当前程序文件所在的目录
-
其次再前往 -iquote 选项指定的目录中查找;
-
前往 -I 选项指定的目录中搜索;
-
前往 -isystem 选项指定的目录中搜索;
-
前往默认的系统路径下搜索;
-
前往 -idirafter 选项指定的目录中搜索。
5.2 编译成汇编代码文件(gcc -S)
所谓编译,简单理解就是将预处理得到的程序代码,经过一系列的词法分析、语法分析、语义分析以及优化,加工为当前机器支持的汇编代码。
通常指令有以下几种形式:
shell
gcc -S demo.c #直接在当前目录下生成一个(同名.s)文件
gcc -S demo.c -o test.s #指定输出文件的名字
该指令可以处理非预处理过的源代码或者经过预处理后的源代码。
可通过以下选项为汇编代码添加必要的注释:
shell
gcc -S demo.c -o test.s -fverbose-asm
5.3 编译成目标文件(gcc -c)
汇编其实就是将汇编代码转换成可以执行的机器指令。大部分汇编语句对应一条机器指令,有的汇编语句对应多条机器指令。相对于编译操作,汇编过程会简单很多,它并没有复杂的语法,也没有语义,也不需要做指令优化,只需要根据汇编语句和机器指令的对照表一一翻译即可。
编译级别及以上阶段的代码皆可以用作目标文件:
shell
gcc -c demo.c -o test.o #输出到指定文件
gcc -c demo.s -o demo.o #输出到指定文件
5.4 链接指定文件
对目标文件进行链接:
shell
gcc demo.o -o demo.exe
除开libc库外,其他库都需要手动添加链接库。
静态链接都放在libc.a(achieve,获取),或共享的动态链接文件libc.so中(文件名后缀.so,代表share object,共享对象)。这些链接库一般位于/lib/或/usr/lib/,位于GCC默认的搜索的其他目录。
链接标准数学库文件:
shell
gcc main.c -o main.out -lm
数学库的文件名是libm.a。前缀lib和后缀.a是标准的,m是基本名称,GCC 会在-l选项后紧跟着的基本名称的基础上自动添加这些前缀、后缀,本例中,基本名称为 m。
链接其他目录中的库:
(1)把链接库作为一般的目标文件,为 GCC 指定该链接库的完整路径与文件名。
shell
gcc demo.c -o demo.out /usr/lib/libm.a
(2)使用-L选项,为 GCC 增加另一个搜索链接库的目录:
shell
gcc demo.c -o demo.out -L/usr/lib -lm
可以使用多个-L选项,或者在一个-L选项内使用冒号分割的路径列表。
(3)把包括所需链接库的目录加到环境变量 LIBRARYPATH 中。
5.5 一些常用的其他gcc命令
| 命令选项 | 描述和解释 |
|---|---|
| -ansi | 只支持 ANSI 标准的 C 语法。这一选项将禁止 GNU C 的某些特色, 例如 asm 或 typeof 关键词。 |
| -DMACRO | 以字符串"1"定义 MACRO 宏。 |
| -DMACRO=DEFN | 以字符串"DEFN"定义 MACRO 宏。 |
| -IDIRECTORY | 指定额外的头文件搜索路径DIRECTORY。 |
| -c | 指定额外的函数库搜索路径DIRECTORY。 |
| -lLIBRARY | 连接时搜索指定的函数库LIBRARY。 |
| -m486 | 针对 486 进行代码优化。 |
| -O0 | 不进行优化处理。 |
| -O 或 -O1 | 优化生成代码。 |
| -O2 | 进一步优化。 |
| -O3 | 比 -O2 更进一步优化,包括 inline 函数。 |
| -shared | 生成共享目标文件。通常用在建立共享库时。 |
| -static | 禁止使用共享连接。 |
| -UMACRO | 取消对 MACRO 宏的定义。 |
| -w | 不生成任何警告信息。 |
| -Wall | 生成所有警告信息。 |
| -x | 指定输出文件的语言,可选包括c、c++、assembler、none。none表示让gcc自动猜测。 |
| -M | 生成文件关联的信息,包含目标文件所依赖的所有源代码。 |
8. GCC编译静态库和动态库
在 GCC 工具链中,静态库与动态库的构建本质上都依赖于目标文件(*.o),区别在于链接阶段的组织方式。静态库通过 ar 将多个目标文件归档为 libxxx.a,本质是简单的文件打包;链接时,ld 会将所需符号直接拷贝进最终可执行文件,因此生成的程序不再依赖外部库文件。这种方式有利于部署独立可执行程序,但会增加体积,也不利于统一升级。
通常编译形式如下:
shell
gcc -c foo.c #生成 foo.o目标文件
ar rcs libfoo.a foo.o #生成 libfoo.a 静态库
使用如下(加上了-static选项,一个使用搜索路径,一个直接指定文件)。:
shell
gcc hello.c -static libfoo.a -o hello
gcc hello.c -static -L. -lfoo -o hello
动态库(libxxx.so)则以共享对象形式存在,使用 -shared 生成,并配合 -fPIC 产生位置无关代码。PIC 的核心是通过间接寻址访问全局符号,使代码在任意加载地址均可正确执行,从而支持多个进程共享同一份物理内存页。在现代 Linux 系统中,默认采用 ELF 格式,动态链接依赖 ld-linux.so 进行符号解析和重定位。
动态库的编译方式基本如下:
shell
gcc foo.c -shared -fPIC -o libfoo.so
使用-fPIC生成位置无关代码。
使用如下:无需加上-static选项
shell
gcc hello.c libfoo.so -o hello
gcc hello.c -L. -lfoo -o hello
静态链接与动态链接在行为上存在显著差异,如下所示:
| 维度 | 静态库 *.a |
动态库 *.so |
|---|---|---|
| 链接时机 | 编译期完成 | 运行期解析 |
| 文件体积 | 较大 | 较小 |
| 部署复杂度 | 低 | 需保证运行环境 |
| 升级影响 | 需重新编译 | 可独立替换 |
链接动态库时若未指定 -static,编译器默认优先查找共享库。运行失败通常源于动态加载路径问题。系统加载顺序一般为:rpath → LD_LIBRARY_PATH → /etc/ld.so.cache → 默认路径(如 /lib、/usr/lib)。可通过 ldd 查看依赖关系:
shell
ldd hello
若输出显示 "not found",说明动态库未被定位。
在工程实践中,更推荐使用 rpath 或 RUNPATH 机制嵌入路径,而非长期依赖环境变量。链接阶段可通过:
shell
gcc hello.c -L. -lfoo -Wl,-rpath,'$ORIGIN' -o hello
其中 $ORIGIN 表示可执行文件所在目录,适用于可移植部署场景。动态加载流程可概括如下:
Filesystem ld.so.cache ld-linux Executable Filesystem ld.so.cache ld-linux Executable 启动并请求依赖库 查询缓存 按 rpath / 环境变量 搜索 完成重定位与符号绑定
理解静态与动态链接的差异,有助于在性能、部署和可维护性之间取得平衡。在嵌入式或发布独立工具时,静态链接更具确定性;而在大型系统或插件化架构中,动态库更利于模块化与版本管理。

Once Day
也信美人终作土,不堪幽梦太匆匆......
如果这篇文章为您带来了帮助或启发,不妨点个赞👍和关注!
(。◕‿◕。)感谢您的阅读与支持~~~