文章目录
1. 编译过程
GCC(GNU Compiler Collection)编译器是一个强大的编译工具链,能够将C/C++源代码转换为可执行的机器码。编译过程分为四个主要步骤:预处理(preprocessing)、编译(compilation)、汇编(assembly)和链接(linking)。
预处理(Preprocessing)
在C/C++编译过程中,预处理是第一步。预处理器的主要任务是处理以#
开头的命令,包括:
#include
:插入头文件。预处理器将头文件的内容插入到包含它的源文件中。#define
:定义宏。预处理器将宏替换为其定义的值或代码片段。#if
/#ifdef
/#endif
:条件编译。根据条件选择性地编译代码。
预处理器将这些命令处理完后,生成一个带有扩展名为.i
的中间文件,该文件包含展开后的完整源代码,准备进入下一步的编译阶段。
编译(Compilation)
在编译阶段,编译器(如gcc
)将预处理后的源代码(.i
文件)进行词法分析和语法分析,生成汇编代码。这个阶段的主要任务是:
- 词法分析:将源代码分解成有意义的词法单元(tokens)。
- 语法分析:根据语法规则检查词法单元的结构。
编译器会根据源代码的逻辑生成对应的汇编代码,扩展名为.s
。
汇编(Assembly)
在汇编阶段,汇编器(如as
)将汇编代码(.s
文件)转换成目标代码(机器代码),生成扩展名为.o
的目标文件。这个阶段的主要任务是:
- 指令转换:将汇编指令转换为机器码。
- 符号解析:解析汇编代码中的符号并生成对应的机器地址。
目标文件包含了可执行代码的机器指令,但还不能单独运行,需要经过链接阶段生成最终的可执行文件。
链接(Linking)
在链接阶段,链接器(如ld
)将一个或多个目标文件(.o
文件)合并,并解析所有外部符号引用,生成最终的可执行文件。这个阶段的主要任务是:
- 符号解析:解析和处理目标文件中的符号引用,确保每个符号都有定义。
- 地址重定位:调整目标文件中的地址引用,使得它们在最终的可执行文件中正确指向。
链接器将所有目标文件中的代码和数据整合到一个可执行文件中,完成整个编译过程。
整个gcc编译过程包括以下四个步骤:
- 预处理:处理
#
开头的预处理命令,生成.i
文件。 - 编译:对预处理后的源代码进行词法和语法分析,生成
.s
汇编代码文件。 - 汇编:对汇编代码进行优化,生成
.o
目标代码文件。 - 链接:解析目标代码中的外部引用,将多个目标代码文件连接为一个可执行文件。
通过这些步骤,源代码被逐步转换为可执行的机器代码,最终生成可以在目标硬件上运行的可执行文件。
2. 源文件种类
编译器利用上面4个步骤中的一个或多个来处理输入文件,源文件的后缀名表示源文件所用的语言,后缀名控制着编译器的缺省动作。
GCC编译器可以处理多种类型的源文件,每种源文件都有特定的后缀名,这些后缀名表示文件所用的编程语言以及编译器将如何处理它们。以下是各种后缀名及其对应的语言和处理步骤:
后缀名 | 语言种类 | 后期操作 |
---|---|---|
.c | C源程序 | 预处理、编译、汇编 |
.C | C++源程序 | 预处理、编译、汇编 |
.cc | C++源程序 | 预处理、编译、汇编 |
.cxx | C++源程序 | 预处理、编译、汇编 |
.m | Objective-C源程序 | 预处理、编译、汇编 |
.mm | Objective-C++源程序 | 预处理、编译、汇编 |
.i | 预处理后的C文件 | 编译、汇编 |
.ii | 预处理后的C++文件 | 编译、汇编 |
.s | 汇编语言源程序 | 汇编 |
.S | 汇编语言源程序 | 预处理、汇编 |
.h | 预处理器文件 | 通常不会单独出现 |
连接器处理其他后缀名文件
编译完成后,生成的中间文件或目标文件会被传递给链接器进行最终的链接操作,通常包括以下类型的文件:
- .o:目标文件(Object file)
- .a:归档库文件(Archive file)
这些文件包含编译后的代码和数据,链接器将它们组合起来,生成最终的可执行文件。
编译过程中的选项
在编译过程中,除非使用了 -c
、-S
或 -E
选项(或者编译错误阻止了整个过程),否则最后的步骤总是链接。在链接阶段中,所有对应于源程序的 .o
文件、-l
选项指定的库文件、无法识别的文件名(包括指定的 .o
目标文件和 .a
库文件)按照命令行中的顺序传递给链接器。
以下是这些选项的具体功能:
- -c:仅编译到目标文件,不进行链接。
- -S:将源代码编译到汇编代码,生成
.s
文件,不进行汇编和链接。 - -E:仅进行预处理,不进行编译、汇编和链接。
3. gcc命令
3.1 GCC命令格式
gcc [选项] 文件列表
- 选项:用于定制GCC的行为,例如控制编译过程的各个阶段、优化级别、调试信息等。
- 文件列表:指定要编译的源文件。
GCC命令用于实现C程序编译的全过程。文件列表参数指定了GCC的输入文件,选项用于定制GCC的行为。GCC根据选项的规则将输入文件编译生成适当的输出文件。GCC的选项非常多,常用的选项大致可以分为几类。
示例代码
假设一个C语言源文件 main.c
:
// main.c
#include <stdio.h>
#define HUNDRED 100
int main() {
printf("%d abc\n", HUNDRED);
return 0;
}
3.2 预处理选项(-E)
在GCC编译过程中,预处理(preprocessing)是非常重要的一步。预处理是指在正式编译代码之前,先对源代码中的一些指令进行处理,生成中间文件。这些指令包括头文件的包含(#include
)、宏定义(#define
)、条件编译指令(#if
、#ifdef
、#endif
)等。通过预处理,所有的宏定义都会被展开,头文件的内容也会被插入到源文件中。
使用以下命令来进行预处理:
gcc -E main.c -o main.i
-E
:表示只进行预处理,不进行编译、汇编和链接。main.c
:源文件。-o main.i
:将预处理后的内容输出到main.i
文件中。
执行上述命令后,可以得到 main.i
文件的内容。这里仅截取部分关键代码进行解释:
extern int ftrylockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
# 942 "/usr/include/stdio.h" 3 4
# 2 "main.c" 2
# 5 "main.c"
int main() {
printf("%d ask\n", 100);
return 0;
}
-
头文件展开:
- 预处理器将
#include <stdio.h>
指令展开,插入了标准输入输出库stdio.h
的内容。
- 预处理器将
-
宏定义展开:
- 预处理器将所有的宏定义展开,比如
#define HUNDRED 100
被替换为相应的值。在printf
函数中,原来的HUNDRED
被替换为100
。
- 预处理器将所有的宏定义展开,比如
预处理阶段的任务是将所有预处理指令(以 #
开头的命令)进行处理,将头文件的内容插入到包含它们的源文件中,将宏定义展开,根据条件编译指令选择性的保留或删除代码。预处理的结果是一个纯C代码的中间文件(.i
文件),它不包含任何预处理指令,方便后续的编译阶段进行处理。
通过预处理,我们可以更好地理解代码的结构和内容,也可以用来调试和优化代码。这一步非常重要,因为它是编译器生成目标代码的基础。如果预处理阶段出现问题,后续的编译、汇编和链接阶段也无法顺利进行。
3.3 编译选项(-S)
在GCC编译过程中,编译(compilation)是将经过预处理的C/C++代码(如 .i
文件)翻译成汇编代码( .s
文件)。汇编代码是机器码的文本表示形式,每一条汇编指令对应一条机器码指令。通过编译生成汇编代码,可以帮助我们理解编译器生成的指令以及优化代码的效果。
使用以下命令来进行编译:
gcc -S main.c -o main.s
-S
:表示只进行编译,不进行汇编和链接。main.c
:源文件。-o main.s
:将编译后的汇编代码输出到main.s
文件中。
执行上述命令后,我们可以看到 main.s
文件的内容。以下是生成的汇编代码:
.file "main.c"
.text
.section .rodata
.LC0:
.string "%d ask\n"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $100, %esi
leaq .LC0(%rip), %rdi
movl $0, %eax
call printf@PLT
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0"
.section .note.GNU-stack,"",@progbits
-
文件头信息:
.file "main.c"
:指明了源文件名。.ident "GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0"
:GCC编译器的版本信息。
-
文本段:
.text
:定义代码段,存放程序的指令。.section .rodata
:定义只读数据段,存放常量和字符串。.LC0:
:标识符,表示一个字符串常量的位置。.string "%d ask\n"
:定义字符串常量%d ask\n
。
-
全局变量与函数:
.globl main
:声明main
函数为全局符号。.type main, @function
:定义main
为函数类型。main:
:函数入口。
-
汇编指令:
.cfi_startproc
和.cfi_endproc
:用于调试信息的指令,标记函数的开始和结束。pushq %rbp
和popq %rbp
:保存和恢复基指针寄存器。movq %rsp, %rbp
:将栈指针寄存器的值移动到基指针寄存器。movl $100, %esi
:将立即数100
移动到esi
寄存器。leaq .LC0(%rip), %rdi
:将字符串常量地址加载到rdi
寄存器。movl $0, %eax
:将0
移动到eax
寄存器。call printf@PLT
:调用printf
函数。ret
:函数返回。
-
其他信息:
.size main, .-main
:定义main
函数的大小。
通过生成汇编代码,我们可以清晰地看到C语言代码被翻译成汇编语言的过程。汇编代码提供了更底层的视角,帮助我们理解程序的运行机制和性能优化。
3.4 汇编选项(-c)
在GCC编译过程中,汇编(assembly)是将经过编译生成的汇编代码( .s
文件)翻译成符合一定格式的机器码。在Linux系统上,这些机器码通常会被打包成目标文件(Object file,OBJ文件),例如 .o
文件。目标文件包含了机器代码和数据,准备链接成可执行程序。
使用以下命令来进行汇编:
gcc -c main.c -o main.o
-c
:表示只进行编译和汇编,不进行链接。main.c
:源文件。-o main.o
:将编译和汇编后的目标文件输出到main.o
文件中。
执行上述命令后生成了 main.o
文件。目标文件 main.o
包含了机器代码,可以被链接器(linker)用来生成最终的可执行文件。
通过汇编选项 -c
,我们只进行前两个步骤,不进行最后的链接步骤。因此,输出的 main.o
文件是一个中间文件,包含了机器代码和数据,但还不能独立运行。
3.5 链接步骤(Linking)
链接(linking)是编译过程的最后一步,它将多个目标文件( .o
文件)和库文件结合在一起,生成一个可执行的二进制文件。链接的主要任务是解析符号引用,合并代码和数据段,并处理重定位信息。
假设我们有多个目标文件,需要链接成一个可执行文件。使用以下命令:
gcc main.o -o main
main.o
:目标文件,包含编译和汇编后的机器代码。-o main
:指定输出文件的名称,这里生成可执行文件main
。
如果有多个目标文件:
gcc file1.o file2.o -o program
file1.o
和file2.o
:多个目标文件。-o program
:生成的可执行文件名称为program
。
链接过程
- 符号解析:链接器会检查所有目标文件和库文件中的符号(变量和函数)引用,确保每个符号都有定义。
- 重定位:链接器根据符号表和重定位信息,修正代码中的地址引用,使它们指向正确的内存位置。
- 合并段:将不同目标文件的代码段、数据段和其他段合并到一起,形成一个完整的可执行文件。
- 库文件处理:链接器会搜索并包含必要的库文件(例如标准库),以满足所有外部符号引用。
通过链接步骤,将独立编译的目标文件和库文件结合起来,生成一个可以在目标平台上运行的完整程序。这一步至关重要,确保所有的符号引用都能被正确解析,所有的代码和数据都能正确定位和访问。
3.6 运行可执行文件
$ ls
main.c main
$ ./main
100 abc
- 使用
ls
命令列出当前目录中的文件,可以看到main.c
源文件和生成的main
可执行文件。 - 运行生成的
main
文件,可以看到输出 abc。
如果不使用 -o
选项,编译器会生成默认的输出文件名。各编译阶段有各自的默认文件名。可执行文件的默认名为 a.out
。使用示例如下:
$ gcc main.c
$ ls
a.out main.c
$ ./a.out
100 abc
- 使用
ls
命令列出当前目录中的文件,可以看到main.c
源文件和生成的a.out
可执行文件。 - 运行生成的
a.out
文件,可以看到输出100 ask
。
-
使用
-o
选项:- 当我们编译一个 C 源文件时,可以通过
-o
选项指定生成的输出文件名。 - 示例中,
gcc main.c -o main
命令告诉编译器生成一个名为main
的可执行文件。 - 使用
ls
命令,我们可以看到源文件main.c
和生成的可执行文件main
。 - 执行生成的
main
文件后,程序输出 abc。
- 当我们编译一个 C 源文件时,可以通过
-
默认输出文件名:
- 如果不使用
-o
选项,编译器会使用默认的输出文件名a.out
。 - 示例中,
gcc main.c
命令告诉编译器编译main.c
文件并生成一个默认名为a.out
的可执行文件。 - 使用
ls
命令,我们可以看到源文件main.c
和生成的默认可执行文件a.out
。 - 执行生成的
a.out
文件后,程序输出 abc。
- 如果不使用
通过这两个示例,可以看到使用 -o
选项和不使用 -o
选项时编译器的行为差异。指定输出文件名可以帮助我们更好地管理生成的可执行文件,避免默认文件名 a.out
带来的混淆。