GCC 编译器选项详解:从总体选项到链接器与目录选项

本文是 GCC 编译器系列的进阶篇,详细介绍 GCC 的各种编译选项,包括总体选项(-c、-S、-E、-o、-v)、警告选项(-Wall)、调试选项(-g)、优化选项(-O0~-O3)、链接器选项(-l、-static、-shared、-nostdlib 等)以及目录选项(-I、-L、-B)。通过大量示例和对比,帮助你真正理解每个选项的作用,避免在实际开发中踩坑。


1. 准备工作:示例代码

为了演示这些选项,我们先创建三个简单的文件:main.csub.csub.h

main.c

c

#include <stdio.h>
#include "sub.h"

int main(int argc, char *argv[])
{
    int i;          // 这个变量定义后从未使用,用来演示 -Wall 警告
    printf("Main fun!\n");
    sub_fun();
    return 0;
}

sub.h

c

void sub_fun(void);

sub.c

c

#include <stdio.h>

void sub_fun(void)
{
    printf("Sub fun!\n");
}

这三个文件将作为我们编译实验的基础。


2. GCC 总体选项(Overall Options)

总体选项控制 GCC 在编译过程的哪个阶段停止,以及如何指定输出文件。

2.1 -c – 只编译不链接

作用:对源文件进行预处理、编译、汇编,生成目标文件(.o 文件),但不进行链接

典型用法

bash

gcc -c -o main.o main.c
gcc -c -o sub.o sub.c
  • -o main.o 指定输出的目标文件名。如果不指定,默认会把 main.c 编译成 main.o(把 .c 后缀替换成 .o)。
  • 生成的目标文件是二进制机器码,但其中的函数地址尚未确定(例如 printf 的地址还是空缺),需要链接后才能运行。

为什么需要 -c:在大型项目中,我们通常分别编译每个 .c 文件,最后再统一链接。这样修改一个文件后,只需重新编译那个文件,可以大大节省时间。

2.2 -S – 只编译不汇编

作用:进行预处理和编译,生成汇编代码文件(.s 文件),不进行汇编和链接

典型用法

bash

gcc -S -o main.s main.c

生成的 main.s 是文本格式的汇编代码,你可以用文本编辑器打开查看。这对于理解编译器如何将 C 代码转换为汇编非常有帮助。

2.3 -E – 只预处理不编译

作用:仅执行预处理(展开宏、包含头文件、处理条件编译),将结果输出到标准输出(屏幕),不编译、不汇编、不链接。

典型用法

bash

gcc -E main.c

如果想把预处理结果保存到文件:

bash

gcc -E -o main.i main.c

实用技巧:当你想确认某个宏是否被正确定义,或者头文件被哪个路径包含时,可以用 -E 查看。例如:

bash

gcc -E -dM main.c | grep "printf"

-dM 选项会列出所有宏定义,结合 -E 可以快速查看所有宏。

2.4 -o file – 指定输出文件名

作用:无论 GCC 处于哪个阶段(预处理、编译、汇编、链接),-o 都可以用来指定输出文件的名称。

默认行为(不使用 -o 时):

  • 链接生成的可执行文件默认名为 a.out
  • 编译生成的目标文件(-c)默认将源文件后缀 .c.i.s 替换为 .o
  • 汇编文件(-S)默认替换为 .s
  • 预处理(-E)默认输出到屏幕。

示例

bash

gcc main.c                # 生成 a.out
gcc -o myapp main.c       # 生成 myapp
gcc -c -o myobj.o main.c  # 生成 myobj.o

2.5 -v – 显示详细的编译过程

作用:输出 GCC 的内部执行细节,包括:

  • 配置信息(GCC 是如何编译出来的)
  • 调用的子程序(如 cc1ascollect2ld)及其参数
  • 搜索路径(头文件、库文件)

典型用法

bash

gcc -v -o test main.c sub.c

输出会很长,但你可以从中看到:

  • cc1 是真正的 C 编译器(预处理+编译)
  • as 是汇编器
  • collect2ld 是链接器
  • 链接时默认加入了哪些启动文件(crt1.ocrti.o 等)和库文件(-lc-lgcc 等)

实用技巧:在交叉编译时,使用 arm-linux-gnueabihf-gcc -v 可以查看交叉编译器的默认头文件路径和库路径。


3. 警告选项(Warning Option):-Wall

作用:打开几乎所有常见的编译警告。编译器会提示你可能存在问题的代码,但不会影响生成目标文件。

示例:我们的 main.c 中定义了一个从未使用的变量 i。如果不加 -Wall,编译时不会有任何提示:

bash

gcc -c -o main.o main.c   # 没有任何输出

加上 -Wall 后:

bash

gcc -Wall -c -o main.o main.c

输出:

text

main.c: In function 'main':
main.c:6:9: warning: unused variable 'i' [-Wunused-variable]
     int i;
         ^

💡 最佳实践:在开发阶段,强烈建议总是加上 -Wall(甚至可以加上 -Wextra-Werror),尽早发现潜在问题。很多难以定位的 bug 其实就是一个小小的警告导致的。


4. 调试选项(Debugging Option):-g

作用:在生成的目标文件或可执行文件中嵌入调试信息(如变量名、行号、函数名等),供调试器(如 GDB)使用。

典型用法

bash

gcc -g -c -o main.o main.c
gcc -g -o test main.o sub.o

调试信息的格式可以是 stabsCOFFDWARF 等,不同系统默认不同。对于 Linux,通常使用 DWARF。GDB 可以自动识别。

注意

  • 添加 -g 会使可执行文件变大,但不会影响程序运行逻辑。
  • 发布正式版本时,通常不加 -g(或者生成单独的调试符号文件)。
  • -g 和优化选项(如 -O2)可以同时使用,但优化后的代码在调试时可能会跳来跳去(因为代码被重排了),调试体验较差。

5. 优化选项(Optimization Options)

GCC 提供了多个级别的优化,用 -O(大写字母 O)后跟数字表示。优化级别越高,编译时间越长,生成的代码运行速度越快(或者体积越小),但调试越困难。

5.1 各优化级别对比

选项 含义 特点
-O0 不优化(默认) 编译最快,代码最易调试,每条 C 语句都对应清晰的机器码
-O1-O 基本优化 尝试减少代码大小和执行时间,不进行空间换时间的优化
-O2 进一步优化 打开几乎所有不涉及空间换时间的优化(如指令调度、跳转线程等),推荐用于正式发布
-O3 更激进优化 -O2 基础上增加函数内联(-finline-functions)、循环展开等,可能导致代码体积变大
-Os 优化代码大小 -O2 基础上,执行不增加代码大小的优化,适合嵌入式系统

示例:使用 -O2 编译

bash

gcc -O2 -c -o main.o main.c
gcc -O2 -c -o sub.o sub.c
gcc -O2 -o test main.o sub.o

🧠 建议:开发调试阶段用 -O0 或不用 -O,正式发布用 -O2。除非明确需要,尽量避免 -O3,因为它可能引入难以察觉的 bug(例如浮点运算顺序改变)。

5.2 多个 -O 选项的处理

如果指定了多个 -O 选项,最后一个生效

bash

gcc -O1 -O3 -c main.c   # 实际使用 -O3

6. 链接器选项(Linker Options)

链接器(ldcollect2)负责将多个目标文件(.o)和库文件组合成可执行文件。GCC 可以通过以下选项影响链接过程。

6.1 object-file-name – 直接指定目标文件

作用:在命令行中直接写上目标文件名(以 .o 结尾)或库文件名(以 .a 结尾),GCC 会将其传递给链接器。

示例

bash

gcc -o test main.o sub.o

这里 main.osub.o 就是目标文件。

6.2 -llibrary – 链接指定的库

作用:链接名为 library 的库文件。GCC 会自动在库名前加上 lib,后缀加上 .a(静态库)或 .so(动态库),并在标准库路径(以及 -L 指定的路径)中搜索。

示例:链接数学库 libm.so

bash

gcc -o calc calc.c -lm
  • -lm 表示链接 libm.solibm.a

注意

  • -l 选项的顺序很重要。如果 a.o 调用了 b.o 中的函数,那么 -la 应该出现在 -lb 之前。链接器按顺序处理,默认只向前查找未定义的符号。
  • 可以使用 -L 指定额外的库搜索路径(见目录选项)。

6.3 -static – 静态链接

作用:阻止使用共享库(动态库),强制将所有库的代码复制到最终的可执行文件中。

对比

bash

gcc -o test_dynamic main.o sub.o          # 动态链接
gcc -o test_static main.o sub.o -static   # 静态链接
ls -lh test_*

输出示例:

text

-rwxr-xr-x 1 book book 6.5K  test_dynamic
-rwxr-xr-x 1 book book 534K  test_static

静态链接的文件体积大很多,但可以独立运行,不需要目标系统上有对应的动态库。

适用场景

  • 目标系统缺少动态库或版本不匹配
  • 嵌入式系统或 initramfs 中不希望依赖外部库

6.4 -shared – 生成共享库(动态库)

作用:生成一个共享目标文件(.so),可以被其他程序动态加载。

示例:制作 libsub.so

bash

gcc -c -fPIC -o sub.o sub.c          # 生成位置无关代码
gcc -shared -o libsub.so sub.o       # 生成动态库

使用动态库编译程序:

bash

gcc -o test main.o -L. -lsub

运行时需要确保 libsub.so 在动态链接器的搜索路径中(如 /usr/lib 或通过 LD_LIBRARY_PATH 指定)。

6.5 -nostartfiles – 不链接启动文件

作用:不链接系统标准的启动文件(如 crt1.ocrti.ocrtend.ocrtn.o),但仍然链接标准库。

示例

bash

gcc -nostartfiles -o test main.o sub.o

链接时会警告找不到入口符号 _start(因为启动文件通常定义了 _start,它负责调用 main)。编译出来的程序无法正常运行,这个选项通常用于编译操作系统内核或 bootloader,这些程序自己定义入口点。

6.6 -nostdlib – 不链接启动文件和标准库

作用:既不链接启动文件,也不链接任何标准库(如 libc)。

示例

bash

gcc -nostdlib -o test main.o sub.o

会产生大量未定义引用错误(因为 printflibc 中)。这个选项常用于裸机程序内核bootloader,它们自己实现所有需要的函数。

6.7 -Xlinker option – 直接传递选项给链接器

作用:将 option 原样传递给链接器。如果需要传递带参数的选项,需要写两次 -Xlinker

示例:传递 -assert definitions 给链接器

bash

gcc -o test main.o sub.o -Xlinker -assert -Xlinker definitions

6.8 -Wl,option – 更简洁地传递选项给链接器

作用:将 option 传递给链接器,多个选项可以用逗号分隔。

示例:传递 -Map=output.map(生成链接映射文件)

bash

gcc -o test main.o sub.o -Wl,-Map=output.map

这是最常用的方式,比 -Xlinker 更简洁。

6.9 -u symbol – 强制链接未使用的符号

作用:让链接器认为 symbol 是一个未定义符号,从而强制从库中拉入定义该符号的目标文件。

示例:假设静态库 libfoo.a 中有函数 foo_init,但程序中没有直接调用它,你想把它也链接进来:

bash

gcc -o test main.o -lfoo -u foo_init

7. 目录选项(Directory Options)

这些选项用于指定头文件和库文件的搜索路径。

7.1 -Idir – 添加头文件搜索路径

作用:在头文件的搜索路径列表中添加 dir 目录。

搜索规则

  • 对于 #include <file>:只搜索系统标准目录 + -I 指定的目录。
  • 对于 #include "file":先从当前文件所在目录搜索,再从 -I 指定的目录和系统标准目录搜索。

示例

bash

gcc -c -I /my/include -o main.o main.c

💡 如果有多个 -I 目录,先出现的目录优先搜索

7.2 -I- – 控制搜索路径的分隔

作用:将 -I 目录分为两组:

  • -I- 之前-I 目录只用于搜索 #include "file"(用户头文件)。
  • -I- 之后-I 目录用于搜索所有 #include(包括 <>"")。

此外,-I- 还会禁止当前目录成为 #include "file" 的第一搜索位置。

这个选项在现代 GCC 中已经不太常用,更推荐使用 -iquote-isystem 等更细粒度的选项。但了解它有助于理解旧代码。

7.3 -Ldir – 添加库文件搜索路径

作用:在 -l 的搜索路径列表中添加 dir 目录。链接器会在这些目录中查找 libname.alibname.so

示例

bash

gcc -o test main.o -L. -lsub

这条命令会在当前目录(.)中搜索 libsub.solibsub.a

注意-L 只影响链接阶段,不影响编译阶段。

7.4 -Bprefix – 指定工具前缀

作用:指定 GCC 在调用子工具(如 cc1asld)时使用的前缀。这个前缀可以是目录路径,也可以是文件名前缀。

示例:使用 /usr/local/my-gcc-tools/ 下的工具

bash

gcc -B/usr/local/my-gcc-tools/ -c main.c

GCC 会尝试执行 /usr/local/my-gcc-tools/cc1/usr/local/my-gcc-tools/as 等。

环境变量:也可以通过 GCC_EXEC_PREFIX 环境变量设置类似效果。-B 优先级更高。

这个选项在交叉编译时可能用到,但通常我们直接使用完整的交叉编译工具链名称(如 arm-linux-gnueabihf-gcc)即可,不需要手动 -B


8. 其他工具:ldobjdumpobjcopy 简介

在开发普通应用程序时,我们很少直接调用 ld(链接器)、objdump(显示目标文件信息)、objcopy(复制和转换目标文件)。但在裸机开发bootloader内核移植调试时,这些工具非常有用。

  • ld:链接器,将目标文件和库文件组合成可执行文件。GCC 在内部会调用它。

  • objdump:反汇编、查看目标文件的段信息、符号表等。例如:

    bash

    objdump -d test          # 反汇编 text 段
    objdump -t test          # 显示符号表
    
  • objcopy:转换目标文件格式,例如将 ELF 文件转换为纯二进制文件(用于烧录到 Flash):

    bash

    arm-linux-objcopy -O binary test test.bin
    

这些工具的具体用法将在裸机或内核相关章节详细讲解,此处只需知道它们的存在。


9. 综合示例:从源码到可执行文件的完整流程

我们用前面三个文件演示一个完整的、带优化的编译链接过程。

bash

# 1. 分别编译,生成目标文件(使用 -O2 优化,-Wall 警告,-g 调试信息)
gcc -Wall -g -O2 -c -o main.o main.c
gcc -Wall -g -O2 -c -o sub.o sub.c

# 2. 链接生成动态链接的可执行文件(默认)
gcc -o test_dyn main.o sub.o

# 3. 链接生成静态链接的可执行文件
gcc -static -o test_static main.o sub.o

# 4. 制作动态库 libsub.so
gcc -fPIC -shared -o libsub.so sub.c

# 5. 使用动态库编译(假设 libsub.so 在当前目录)
gcc -o test_use_dyn main.o -L. -lsub

# 6. 查看生成的文件大小
ls -lh test_* libsub.so

10. 总结速查表

选项 作用 示例
-c 只编译不链接,生成 .o gcc -c main.c
-S 生成汇编代码 .s gcc -S main.c
-E 只预处理,输出到屏幕 gcc -E main.c
-o file 指定输出文件名 gcc -o myapp main.c
-v 显示详细编译过程 gcc -v main.c
-Wall 打开常见警告 gcc -Wall main.c
-g 添加调试信息 gcc -g main.c
-O0, -O1, -O2, -O3 优化级别 gcc -O2 main.c
-llibrary 链接库 liblibrary.a/.so gcc main.c -lm
-static 静态链接 gcc -static main.c
-shared 生成动态库 gcc -shared -fPIC -o lib.so sub.c
-nostartfiles 不链接启动文件 gcc -nostartfiles ...
-nostdlib 不链接启动文件和标准库 gcc -nostdlib ...
-Wl,option 传递选项给链接器 gcc -Wl,-Map=output.map
-Idir 添加头文件搜索路径 gcc -I ./inc main.c
-Ldir 添加库文件搜索路径 gcc -L./lib main.c -lsub
-Bprefix 指定工具前缀 gcc -B/my-tools/ main.c

🎉 结束语:GCC 的选项非常丰富,但本文介绍的这些已经覆盖了日常开发的绝大部分需求。掌握它们,你就能自如地控制编译的每个阶段、优化级别、链接行为以及目录搜索。记住:多动手试,用 -v 看看内部发生了什么,你会理解得更深刻。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐