转载自:知乎:C语言的编译过程详解
- 当我们编译C程序时会发生什么?
- 编译过程中的组件有哪些,编译执行过程是什么样的?
什么是编译
C语言的编译过程就是把我们可以理解的高级语言代码转换为计算机可以理解的机器代码的过程,其实就是一个翻译的过程。
C语言的编译过程包括四个步骤:
1.预处理
2.编译
3.汇编
4.链接
下面两张图就是C程序编译的完整过程
接下来我们看看编译过程不同阶段在做什么。
1. 预处理
编译过程的第一步就是预处理,与处理结束后会产生一个后缀位(.i)的临时文件,这一步由预处理器完成。预处理器主要完成以下任务:
- 删除所有的注释
- 宏扩展
- 文件包含
预处理器会在编译过程中删除所有注释,因为注释不属于程序代码,它们对程序的运行没有特别作用。
宏是使用 #define 指令定义的一些常量值或表达式。宏调用会导致宏扩展。预处理器创建一个中间文件,其中一些预先编写的汇编级指令替换定义的表达式或常量(基本上是匹配的标记)。为了区分原始指令和宏扩展产生的程序集指令,
在每个宏展开语句中添加了一个“+”号。
文件包含
C语言中的文件包含是在预处理期间将另一个包含一些预写代码的文件添加到我们的C程序中。它是使用 #include指令完成的。在预处理期间包含文件会导致在源代码中添加文件名的全部内容,从而替换 #include<文件名> 指令,从而创建新的中间文件。
2. 编译
C中的编译阶段使用内置编译器软件将(.i)临时文件转换为具有汇编级指令(低级代码)的汇编文件(.s)。为了提高程序性能,程序的性能,编译器将中间文件转换为程序集文件。
汇编代码是低级语言,用于编写低级指令(在微控制器程序中,我们使用汇编语言)。编译过程可分为6步:扫描(词法分析)、语法分析、语义分析、源代码优化、代码生成、目标代码优化。
- 词法分析:扫描器(Scanner)将源代的字符序列分割成一系列的记号(Token)。lex工具可实现词法扫描。
- 语法分析:语法分析器将记号(Token)产生语法树(Syntax Tree)。yacc工具可实现语法分析(yacc: Yet
Another Compiler Compiler)。 - 语义分析:静态语义(在编译器可以确定的语义)、动态语义(只能在运行期才能确定的语义)。
- 源代码优化:源代码优化器(Source Code Optimizer),将整个语法书转化为中间代码(Intermediate
Code)(中间代码是与目标机器和运行环境无关的)。中间代码使得编译器被分为前端和后端。编译器前端负责产生机器无关的中间代码;编译器后端将中间代码转化为目标机器代码。 - 目标代码生成:代码生成器(Code Generator).
- 目标代码优化:目标代码优化器(Target Code Optimizer)。
3.汇编
使用汇编程序将程序集代码(.s文件)转换为机器可理解的代码(二进制/十六进制形式)。汇编程序是一个预先编写的程序,它将汇编代码转换为机器代码。它从程序集代码文件中获取基本指令,并将其转换为特定于计算机类型(成为目标代码)的二进制/十六进制代码。
生成的文件与程序集文件同名,在DOS中称为扩展名为.obj的对象文件,在UNIX操作系统中扩展名为.o。
4.链接
由汇编程序生成的目标文件并不能立即就被执行,其中可能还有许多没有解决的问题。
例如,某个源文件中的函数可能引用了另一个源文件中定义的某个符号(如变量或者函数调用等);在程序中可能调用了某个库文件中的函数,等等。所有的这些问题,都需要经链接程序的处理方能得以解决。
链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。链接过程会生成一个可执行文件,其扩展名为.exe,在UNIX操作系统中为.out。
根据开发人员指定的同库函数的链接方式的不同,链接处理可分为两种:
- 静态链接
在这种链接方式下,函数的代码将从其所在的静态链接库中被拷贝到最终的可执行程序中。这样该程序在被执行时这些代码将被装入到该进程的虚拟地址空间中。静态链接库实际上是一个目标文件的集合,其中的每个文件含有库中的一个或者一组相关函数的代码。 - 动态链接
在此种方式下,函数的代码被放到称作是动态链接库或共享对象的某个目标文件中。链接程序此时所作的只是在最终的可执行程序中记录下共享对象的名字以及其它少量的登记信息。在此可执行文件被执行时,动态链接库的全部内容将被映射到运行时相应进程的虚地址空间。动态链接程序将根据可执行程序中记录的信息找到相应的函数代码。
对于可执行文件中的函数调用,可分别采用动态链接或静态链接的方法。使用动态链接能够使最终的可执行文件比较短小,并且当共享对象被多个进程使用时能节约一些内存,因为在内存中只需要保存一份此共享对象的代码。但并不是使用动态链接就一定比使用静态链接要优越。在某些情况下动态链接可能带来一些性能上损害。
举例
接下来,我们通过一个例子详细看看C编译过程中涉及的所有步骤。第一步先写一个简单的C程序并保存为hello.c
// Simple Hello World program in C
#include<stdio.h>
int main()
{
// printf() is a output function which prints
// the passed string in the output console
printf("Hello World!");
return 0;
}
接着我们执行编译命令对hello.c进行编译:
gcc -save-temps hello.c -o compilation
-save-temps 选项会保留所有编译过程中产生的中间文件,总共会生成四个文件。
- hello.i 预处理器产生的文件
- hello.s 编译器编译后产生的文件
- hello.o 汇编程序翻译后的目标文件
- hello.exe 可执行文件(Linux系统会产生hello.out文件)
首先,我们的C程序的预处理开始,注释从程序中删除,因为该程序中没有宏指令,因此宏扩展不会发生,我们还包含了一个stdio.h头文件,并且在预处理期间,标准输入/输出函数(如printf(),scanf()等)的声明被添加到我们的C程序中。
打开预处理阶段产生的hello.i 文件就可以看到类似下面这样的代码。
# 1 "hello.c"
# 1 ""
# 1 ""
# 1 "hello.c"
# 1 "C:/Program Files (x86)/CodeBlocks/MinGW/include/stdio.h" 1 3
# 293 "C:/Program Files (x86)/CodeBlocks/MinGW/include/stdio.h" 3
int __attribute__((__cdecl__)) __attribute__ ((__nothrow__)) fprintf (FILE*, const char*, ...);
int __attribute__((__cdecl__)) __attribute__ ((__nothrow__)) printf (const char*, ...);
int __attribute__((__cdecl__)) __attribute__ ((__nothrow__)) sprintf (char*, const char*, ...);
int __attribute__((__cdecl__)) __attribute__ ((__nothrow__)) scanf (const char*, ...);
int __attribute__((__cdecl__)) __attribute__ ((__nothrow__)) sscanf (const char*, const char*, ...);
...
...
...
int __attribute__((__cdecl__)) __attribute__ ((__nothrow__)) putw (int, FILE*);
# 3 "hello.c" 2
int main()
{
printf("Hello World!");
return 0;
}
从上面的代码中可以看到,预编译过后,所有的注释没有了,#include <stdio.h>也被它的头文件内容代替了。
接下来就是编译阶段,编译器接收到hello.i 文件将它转化为汇编代码hello.s文件。这过程中发生了以下几件事:
- 编译器检查语法错误
- 将源代码翻译中间代码,例如汇编代码
- 对代码进行优化
编译结束后产生的hello.s 文件长这个样子:
.file "hello.c"
.def ___main; .scl 2; .type 32; .endef
.section .rdata,"dr"
LC0:
.ascii "Hello World!\0"
.text
.globl _main
.def _main; .scl 2; .type 32; .endef
_main:
LFB12:
.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 $16, %esp
call ___main
movl $LC0, (%esp)
call _printf
movl $0, %eax
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
LFE12:
.ident "GCC: (MinGW.org GCC-6.3.0-1) 6.3.0"
.def _printf; .scl 2; .type 32; .endef
接下来,汇编程序把hello.s 文件转换为二进制代码,并在Windows环境中生成对象文件 hello.obj,在 Linux系统中生成 hello.o文件。
接着,链接器使用库文件将所需的定义添加到对象文件中,并在Windows环境中生成一个可执行文件 hello.exe,在 Linux操作系统中生成 hello.out文件。
当我们运行 hello.exe/hello.out 时,我们会在屏幕上 输出 Hello World!。
程序流程图
结论
- C中的编译过程也称为将人类可理解代码(C程序)转换为机器可理解代码(二进制代码)的过程。
- C语言的编译过程包括四个步骤:预处理、编译、汇编和链接。 预处理器执行删除注释、宏扩展、文件包含。这些命令在编译过程的第一步执行。
- 编译器可以提高程序的性能,并将中间文件转换为汇编文件。 汇编程序有助于将汇编文件转换为包含机器代码的对象文件。
- 链接器用于将库文件与对象文件链接。这是编译中生成可执行文件的最后一步。