C语言程序从源程序到二进制都经历了那些过程?本文以Linux下C语言的编译过程为例,讲解C语言程序的编译过程。
编写hello.c程序:
// hello.c
#include
int main(int argc, char *argv[])
{
printf("hello world!\n");
return 0;
}
编译过程只需:
$ gcc hello.c #编译
$ ./a.out #执行
这个过程看似很简单,其实经历了以下四步操作:
(1)、预处理(Preprocessing) /priːˈprəʊsesɪŋ/
(2)、编译(Compilation) /ˌkɒmpɪˈleɪʃn/
(3)、汇编(Assemble) /əˈsemb(ə)l/
(4)、链接(Linking) /lɪŋkɪŋ/
为了下面步骤讲解的方便,需要一个稍微复杂一点的例子,假设我们自己定义了一个头文件mymath.h,实现了一些自己的数学函数,并把具体实现放在mymath.cpp当中。然后写一个test.c程序使用这些函数。程序目录结构如下所示:
├── test.cpp
└── inc
├── mymath.h
└── mymath.cpp
test.cpp源文件如下:
// test.cpp
#include
#include "mymath.h"// head file
#define PI 3.14
#define SWITCH(a,b) do { \
decltype(a) temp = a;\
a = b; \
b = temp; \
} while(0)
using namespace std;
int main(int argc, char *argv[])
{
int a = 1;
int b = 2;
int sum = add(a, b);
cout << "a = " << a << endl
<< "b = " << b << endl
<< "sum = " << sum << endl;
cout << "PI = " << PI << endl;
SWITCH(a,b);
cout << "a = " << a << endl
<< "b = " << b << endl;
return 0;
}
mymath.h源文件如下:
// mymath.h
#ifndef MYMATH_H
#define MYMATH_H
int add(int a, int b);
int sub(int a, int b);
#endif
mymath.cpp源文件如下:
// mymath.cpp
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
预处理产生编译器的输出,它实现以下的功能:
(1)、文件包含
可以把源程序中的#include扩展为正文,即把包含的.h文件找到并展开到#include所在处。
(2)、条件编译
预处理器根据#if和#ifdef等编译命令及其后的条件,将源程序中的某部分包含进来或排除在外,通常把排除在外的语句转换成空行。
(3)、宏展开
预处理器将源程序文件中出现的对应宏展开成相应的宏定义,即本文所说的#define功能,由预处理器来完成。经过预处理器处理的源程序与之前的源程序有所不同,在这个阶段所进行的工作只是纯粹的替换与展开,没有任何计算功能,所以在学习#define命令时只要能真正理解这一点,这样才不会对此命令引起误解并误用。
gcc的预处理是由预处理器cpp来完成的,可以通过如下命令对test.c进行预处理:
g++ -E -I ./inc test.cpp -o test.i
g++ -E mymath.cpp -o mymath.i
或者直接调用cpp命令:
cpp test.cpp -I ./inc -o test.i
上述命令中-E是让编译器在预处理之后就退出,不进行后续编译过程;-I指定头文件目录,这里指的是我们自定义mymath.h头文件目录;-o指定输出文件名。
经过预处理之后代码体积会大很多:
注意:预处理之后的程序还是文本,可以用文本编辑器打开。
X | 文件名 | 文件大小 | 代码行数 |
预处理前 | test.cpp | 490B | 26 |
预处理后 | test.i | 420304B | 17582 |
这里的编译不是指程序从源文件到二进制文件的全部过程,而是指将经过预处理之后的程序转换成特定汇编代码(assembly code)的过程,编译的指令如下:
g++ -S test.i -o test.s -std=c++11
g++ -S mymath.i -o mymath.s
上述命令中-S让编译器在编译之后停止,不进行后续过程。编译过程完成后,生成程序的汇编代码test.s,内容如下所示(截取部分):
汇编过程将汇编代码(assembly code)转换成机器码(machint code),这一步产生的文件叫做目标文件。gcc的汇编过程通过as命令来完成的,可以通过如下命令对test.c进行预处理:
g++ -C test.s ./inc/mymath.s -o test.o
或者直接调用as命令:
as test.s ./inc/mymath.s -o test.o
这一步会为每一个(.c或.cpp)源文件产生一个目标文件。
汇编程序生成的目标文件,即.o文件,并不会立即执行,因为可能会出现:.cpp文件中的函数引用了另一个.cpp文件中定义的符号或者调用了某个库文件中的函数。那链接的目的就是将这些文件对应的目标文件连接成一个整体,从而生成可执行的程序.exe文件。
命令大致如下:
ld -o test.out test.o inc/mymath.o ... libraried...
链接分为两种:
两者的优缺点: