目录

​​前言​​

​​范例​​

​​预处理(Preprocessing)​​

​​作用​​

​​宏定义指令​​

​​条件编译指令​​

​​头文件包含指令​​

​​特殊符号​​

​​删除注释​​

​​范例​​

​​总结​​

​​编译(Compilation)​​

​​作用​​

​​编译优化​​

​​范例​​

​​汇编(Assemble)​​

​​作用​​

​​范例​​

​​链接(Linking)​​

​​作用​​

​​范例​​

​​其他​​

​​GCC识别的主要文件扩展名​​


前言

我们写出的C语言代码(.c文件),若要在机器上运行,需要经过一个编译过程,主要分为如下四个阶段;
1.预处理(Preprocessing);
2.编译(Compilation);
3.汇编(Assemble);
4.链接(Linking)

linux C gcc 编译过程_目标文件

1) 预处理:即完成宏定义和include 文件展开等工作;生成.i文件。GCC命令为:·gcc -E
2) 编译:根据编译参数进行不同程序的优化,编译成汇编代码;生成.s文件。GCC命令为:·gcc -S
3)汇编:用汇编器把上一阶段生成的汇编代码进一步生成目标代码;生成.o文件。GCC命令为:·gcc -c
4)链接:用链接器把上一阶段生成的目标代码、其他一些相关的系统提供的目标代码(如crtx.o)和系统或用户提供的库链接起来,生成最终的执行代码。生成可执行文件。GCC命令为:·gcc

gcc --help 可以查看gcc的选项,部分选项如下所示:

linux C gcc 编译过程_头文件_02

范例

假设我们自己定义了一个头文件​​mymath.h​​​,实现一些自己的数学函数,并把具体实现放在​​mymath.c​​​当中。然后写一个​​test.c​​程序使用这些函数。程序目录结构如下:

├── test.c
└── inc
├── mymath.h
└── mymath.c

程序代码如下:

// test.c
#include <stdio.h>
#include "mymath.h"// 自定义头文件
int main(){
int a = 2;
int b = 3;
int sum = add(a, b);
printf("a=%d, b=%d, a+b=%d\n", a, b, sum);
}

头文件定义:

// mymath.h
#ifndef MYMATH_H
#define MYMATH_H
int add(int a, int b);
int sum(int a, int b);
#endif

头文件实现:

// mymath.c
int add(int a, int b){
return a+b;
}
int sub(int a, int b){
return a-b;
}

预处理(Preprocessing)

作用

预处理用于将所有的#include头文件以及宏定义替换成其真正的内容,预处理之后得到的仍然是文本文件,但文件体积会大很多。

宏定义指令

如#define Pi 3.1415,预处理阶段会将程序中所有的Pi用3.1415代替。与之对应的#undef 则会取消对某个宏的定义,使之后面出现时不再被替换。

条件编译指令

如#ifdef、#ifndef、#else、#elif、#endif等伪指令的引入,可以使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理,即预处理阶段将有关的文件中不必要的代码过滤掉。

头文件包含指令

如#include,头文件中一般通过#define定义了一些宏(如字符常量),同时也包含了各种外部符号的声明。采用头文件可以使一些定义在多个不同的C源程序中使用,而不必在文件中重新定义。预处理阶段会将头文件中的定义加入到引用它的代码中。

特殊符号

如在源程序中出现的FUNCTION会被解释为当前被编译的C源程序中的函数名称。预处理阶段会对源程序中出现的这些特殊符号用合适的值进行替换。

删除注释

 

范例

gcc -E -I./inc test.c -o test.i

上述命令中-E是让编译器在预处理之后就退出,不进行后续编译过程;-I指定头文件目录,这里指定的是我们自定义的头文件目录;-o指定输出文件名。

总结

预处理阶段主要是完成对源程序的替换工作。经过替换后,会生成一个没有宏定义、没有条件编译指令、没有特殊符号的输出文件.i。

编译(Compilation)

作用

编译阶段所有做的工作就是通过词法分析和语法分析,在确认所有指令都符合语法规则之后,将其翻译成等价的中间代码或者是汇编代码。

这里的编译不是指程序从源文件到二进制程序的全部过程,而是指将经过预处理之后的程序转换成特定汇编代码(assembly code)的过程。

编译优化

编译阶段会对代码进行优化处理,不仅涉及到编译技术本身,还涉及到机器的硬件环境。

优化分为两部分:

1)不依赖于具体计算机的优化。

主要是删除公共表达式、循环优化(代码外提、强度削弱、变换循环控制、已知量的合并等)、无用赋值的删除等

2)同机器硬件结构相关的优化。

主要考虑如何充分利用机器的硬件寄存器存放的有关变量的值以减少内存的访问次数;根据机器硬件执行指令的特点对指令进行调整使目标代码比较短,执行效率更高等。

范例

gcc -S -I./inc test.c -o test.s 

上述命令中​​-S​​​让编译器在编译之后停止,不进行后续过程。编译过程完成后,将生成程序的汇编代码​​test.s​​,这也是文本文件;

// test.c汇编之后的结果test.s
.file "test.c"
.section .rodata
.LC0:
.string "a=%d, b=%d, a+b=%d\n"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
andl $-16, %esp
subl $32, %esp
movl $2, 20(%esp)
movl $3, 24(%esp)
movl 24(%esp), %eax
movl %eax, 4(%esp)
movl 20(%esp), %eax
movl %eax, (%esp)
call add
movl %eax, 28(%esp)
movl 28(%esp), %eax
movl %eax, 12(%esp)
movl 24(%esp), %eax
movl %eax, 8(%esp)
movl 20(%esp), %eax
movl %eax, 4(%esp)
movl $.LC0, (%esp)
call printf
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 4.8.2-19ubuntu1) 4.8.2"
.section .note.GNU-stack,"",@progbits

汇编(Assemble)

作用

汇编过程将上一步的汇编代码转换成机器码(machine code),这一步产生的文件叫做目标文件,是二进制格式。

目标文件由段组成,通常一个目标文件中至少有两个段:

代码段:主要包含程序的指令。该段一般是可读和可执行的,一般不可写。
数据段:主要存放程序中用到的各种全局变量和静态的数据。一般数据段是可读、可写、可执行的。

范例

gcc -c test.s -o test.o

这一步会为每一个源文件产生一个目标文件。因此​​mymath.c​​​也需要产生一个​​mymath.o​​文件;

链接(Linking)

作用

链接阶段的主要工作是将有关的目标文件相链接,即将一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有这些目标文件成为一个能够被操作系统执行的统一整体。

链接过程将多个目标文以及所需的库文件(.so等)链接成最终的可执行文件(executable file)。

举例如下:

某个源文件的函数引用了另一个源文件中定义的变量和函数,因此需要链接阶段将这些变量和函数连接在一起。

 

范例

ld -o test.out test.o inc/mymath.o ...libraries...

其他

GCC识别的主要文件扩展名

文件扩展名

文件类型

.c

C语言代码

.C、.cc

C++语言代码

.i

预处理后的C语言代码

.s、.S

汇编语言代码

.o

目标代码

.a

静态链接库(程序编译时使用)

.so

动态链接库(程序运行时使用)