文章目录
- 一、C/C++ 编译过程的四个阶段
- 1. 编译之舞的台前幕后
- 2. 舞台布景的准备——预处理
- 3. 舞者的基本训练——编译
- 4. 编舞师的细节调整——汇编
- 5. 合奏的和谐统一——链接
- 二、舞姿的动作细——编译详细模式
- 三、幕后——GCC 的各种选项(Overall Option)
- 1. 预处理选项
- 2. 编译选项
- 3. 汇编选项
- 4. 链接选项
- 5. 其他选项
在现代计算的舞台上,编程语言如同舞者,而编译器则是那幕后默默引导的编舞师,每一次代码的编写都像是一场精心设计的舞蹈。在这个舞台上,C 语言和 GCC(GNU Compiler Collection)是一对经典的搭档。它们共同演绎了一场编译的华美舞蹈,从源代码到可执行文件的华丽蜕变。
本文将探索 C 语言与 GCC 之间的这种美妙协作,并深入了解它们是如何共同创造出那些最终运行在计算机上的程序。
一、C/C++ 编译过程的四个阶段
1. 编译之舞的台前幕后
几乎每个学编程的小伙伴,第一个代码都是输出 Hello World!
(如下代码,做了一些小小的改动)。
#include <stdio.h>int main(int agrc, char *argv[])
{if (argc >= 2)printf("Hello %s!\n", argv[1]);elseprintf("Hello World!\n");return 0;
}
将这段代码编译成可执行的文件,也只需要简简单单的一条命令:
gcc hello.c -o hello
使用 gcc
命令,配合上 -o
选项,即可生成 hello
这个可执行文件。然而整个编译过程并没有我们看到的那样简单。
一个或多个 C/C++ 文件要经过预处理(preprocessing)、编译(compilation)、汇编(assembly) 和链接(linking) 四个阶段才能变成可执行文件。
如上图所示,扩展名为 .c
的源代码文件,经过预处理之后可以生成扩展名为 .i
的临时文件。而 .i
文件再经过编译后,可以生成扩展名为 .s
的汇编文件。而扩展名为 .s
的汇编文件再经过汇编之后,可生成扩展名为 .o
的二进制文件。最后,这些二进制文件通过链接,合并成一个可执行文件。
接下来,我们将使用 GCC,将这段代码编译成机器能够理解的指令。
2. 舞台布景的准备——预处理
在 C/C++ 源文件中,以 #
开头的命令被称为预处理命令,如包含命令 #include
、宏定义命令 #define
、条件编译命令 #if
、#ifndef
等。预处理就是将要包含(include)的文件插入原文件中、将宏定义展开、根据条件编译命令选择要使用的代码,最后将这些东西输出到一个扩展名为 .i
的临时文件中,等待下一步处理。
以前面的 hello.c
为例,可以使用 -E
选项让 GCC 仅进行预处理,并输出结果:
gcc -E hello.c -o hello.i
用 vim
打开 hello.i
文件,会发现这是一个将 hello.c
的头文件完全展开的文件:
源代码只有 11 行,被展开后多达 803 行,不过源代码的主题内容还能在 hello.i
的结尾处看到。
3. 舞者的基本训练——编译
我们日常所说的 “编译”,其实涵盖了这个四个阶段,并不是特指这四个阶段中的 “编译”。当然,这里还是要特指一下,这里的 “编译”,就是预处理后的下一步动作。编译阶段就是把扩展名为 .i
的临时文件,“翻译” 成汇编代码,所用到的工具为 cc1
(不要怀疑,这个工具的名字就是 cc1
。不同架构的芯片有自己的 cc1
,x86 架构有自己的 cc1
工具,ARM 架构也有自己的 cc1
工具。)。
一步骤可以通过 -S
选项实现:
gcc -S hello.i -o hello.s
同样,可以通过 vim
来打开 hello.s
文件:
由上图可见,此时生成的 hello.s
文件是汇编代码,描述了程序的基本操作。这些指令仍然是人类可读的,但距离计算机执行还需进一步的转换。
4. 编舞师的细节调整——汇编
汇编阶段就是将汇编代码翻译成符合一定格式的机器代码,在 Linux 系统上一般表现为 ELF 目标文件(OBJ 文件),用到的工具为 as
。
使用 -c
选项,GCC 会将汇编代码转换为目标代码(机器码):
gcc -c hello.s -o hello.o
此时,再用 vim
打开 hello.o
文件,就是一堆完全开不懂得东西了:
这里我们可以用另一个命令行工具来打开 hello.o
文件,那就是用于显示文件的内容为十六进制(hexadecimal)形式的 hexdump
。输入如下命令:
hexdump -C hello.o
如下图所示,第一行就是文件的格式。整个文件包含了程序的机器指令,但尚未完成最终的链接。
5. 合奏的和谐统一——链接
链接阶段是编译过程的最后一步,就是将 OBJ 文件和系统库的 OBJ 文件、库文件链接起来,最终生成了可以在特定平台运行的可执行文件,用到的工具为 ld
或 collect2
。输入如下命令,生成最终的可执行文件:
gcc hello.o -o hello
此时,一个完整的可执行文件 hello
就生成了。当然,这个可执行文件,与 gcc hello.c -o hello
生成的可执行文件,没有任何区别。
二、舞姿的动作细——编译详细模式
在 GCC 编译过程中,使用 -v
选项可以启用详细模式,显示编译的每个步骤和所调用的各个工具的详细信息。这对于调试和理解编译过程非常有用。具体命令如下:
gcc hello.c -o hello -v
执行这个命令会产出一堆信息,不过,我们也可以从中找到一些关键的信息,来验证我们前面提到的一些内容。
在这段信息最前面的就是编译器配置信息,我们把其中的主要信息摘出来:
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/7/lto-wrapper
OFFLOAD_TARGET_NAMES=nvptx-none
OFFLOAD_TARGET_DEFAULT=1
Target: x86_64-linux-gnu
Configured with: ../src/configure -v ...... # 此处省略不重要的内容
Thread model: posix
gcc version 7.5.0 (Ubuntu 7.5.0-3ubuntu1~18.04)
具体说明如下:
Using built-in specs.
:表示使用内置的编译器规格。COLLECT_GCC=gcc
:表明主编译器是gcc
。Target: x86_64-linux-gnu
:目标架构是 64 位的 x86 架构,运行在 GNU/Linux 系统上。Configured with
:显示了编译器的配置选项。Thread model: posix
:使用 POSIX 线程模型。gcc version 7.5.0
:编译器版本为 7.5.0。
接下来按前面所提的四个步骤,应该先进行预处理,不过从输出的信息可以看出,GCC 会把预处理和编译两个阶段一起做了。其中也用到了 cc1
工具:
提取出关键的信息(如下),这里就是用 cc1
工具,将 hello.c
文件编译成临时的 /tmp/ccEwwHut.s
,这是个汇编文件。
COLLECT_GCC_OPTIONS='-o' 'hello' '-v' '-mtune=generic' '-march=x86-64'
/usr/lib/gcc/x86_64-linux-gnu/7/cc1 hello.c -o /tmp/ccEwwHut.s # 去掉了很多参数
[!NOTE]
启用
cc1
工具时使用了很多选项,这些选项对初学者来说,理解起来还是有难度的。不过,为了方便已经入行的小伙伴,这里给出一些选项的介绍,方便大家学习:
/usr/lib/gcc/x86_64-linux-gnu/7/cc1
:调用了cc1
,这是 GCC 的前端,负责预处理、词法分析和语法分析。-quiet
:表示减少输出信息。-imultiarch x86_64-linux-gnu
:指定了目标平台。-dumpbase hello.c
:指定源文件名。-mtune=generic
:优化针对通用架构。-march=x86-64
:目标架构为 x86-64。-fstack-protector-strong
:启用强堆栈保护。-Wformat
:开启格式字符串警告。-Wformat-security
:开启格式字符串安全检查。
这还多了一步前面没提到,就是搜索路径,信息如下:
#include "..." search starts here:
#include <...> search starts here:/usr/lib/gcc/x86_64-linux-gnu/7/include/usr/local/include/usr/lib/gcc/x86_64-linux-gnu/7/include-fixed/usr/include/x86_64-linux-gnu/usr/include
End of search list.
其实这个步骤只是为后面的链接阶段做准备,提前找出加载的库。
到了汇编阶段,可以看到是调用 as
汇编器进行汇编,其中的 -v
选项就是详细模式,--64
表示生成 64 位的目标代码,最后生成指定输出对象文件 /tmp/cc8dsQ4D.o
,这也是个临时文件。
最后到了链接阶段,从下图中可以看出,调用 collect2
链接器。链接器后面的参数中,除了一些相关的库之外,最关键的 /tmp/cc8dsQ4D.o
也包含在其中。从中也可以看到有 -o hello
的选项和参数,也就是最终会生成 hello
的可执行文件。
[!NOTE]
同样有很多选项比较难,稍微解释一下作为提升内容:
-plugin
:使用 LTO 插件进行链接时优化。-plugin-opt
:传递给插件的选项。-m elf_x86_64
:指定 ELF 格式为 64 位。-dynamic-linker /lib64/ld-linux-x86-64.so.2
:指定动态链接器。-pie
:生成位置无关的可执行文件。-z now
:使某些符号立即可用。-z relro
:创建只读重定位段。-lgcc
,-lgcc_s
,-lc
:链接必要的库。
三、幕后——GCC 的各种选项(Overall Option)
GCC (GNU Compiler Collection) 提供了许多选项来控制编译器的行为。这些选项可以大致分为几个类别,包括预处理选项、编译选项、汇编选项和链接选项等。下面对相对重要的选项进行解释:
1. 预处理选项
-E
:只进行预处理阶段,然后停止。输出是经过预处理的源代码。-P
:不输出行控制信息(例如#line
指令)。-C
:保留所有注释。-M
:输出依赖性列表。-MM
:输出依赖性列表,并忽略标准头文件。
2. 编译选项
-c
:只编译并汇编,但不链接。生成一个目标文件。-S
:只编译,生成汇编代码。-E
:只预处理,不编译。-g
:生成调试信息。-O
:设置优化等级。-O0
表示无优化,-O1
至-O3
分别代表不同的优化级别,-O3
是最高级别的优化。-Os
:优化以减小代码尺寸。-Og
:优化同时保持调试信息的可用性。-Wall
:打开所有警告。-Wextra
:打开额外的警告。-Werror
:将所有警告视为错误。-pedantic
:启用所有 ISO C 和 ISO C++ 标准所禁止的语言扩展。-pedantic-errors
:如同-pedantic
但是将扩展视为错误。-std=standard
:指定要遵循的标准(如-std=c99
或-std=c++11
)。-fPIC
:生成位置无关代码(Position Independent Code),用于共享库。
3. 汇编选项
-Wa,option
:将 option 传递给汇编器。-masm=att
:选择 AT&T 汇编风格。-masm=intel
:选择 Intel 汇编风格。
4. 链接选项
-Ldir
:添加目录 dir 到链接器的搜索路径。-lfoo
:链接名为 libfoo 的库。-static
:产生静态链接的可执行文件。-shared
:生成共享库。-pie
:生成位置无关的可执行文件。-fPIC
:与-pie
类似,用于生成位置无关代码,通常用于共享库。-Wl,option
:传递选项给链接器。-T
:指定链接器脚本。-nostartfiles
:不使用任何启动文件。-nostdlib
:不使用标准库。
5. 其他选项
-v
:显示编译器版本信息和编译过程中的详细信息。-V
:显示编译器版本。-Bprefix
:指定前缀路径 prefix 来查找编译器相关的工具。-print-file-name=filename
:打印指定文件的完整路径。-print-prog-name=program
:打印指定程序的完整路径。-print-libgcc-file-name
:打印 libgcc 的路径。-dumpversion
:打印版本号。