C语言编译过程浅析

前言

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ɪŋ/

C语言编译过程浅析_第1张图片

示例

为了下面步骤讲解的方便,需要一个稍微复杂一点的例子,假设我们自己定义了一个头文件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、预处理(Preprocessing)

预处理产生编译器的输出,它实现以下的功能:

(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指定输出文件名

经过预处理之后代码体积会大很多:

注意:预处理之后的程序还是文本,可以用文本编辑器打开。 

C语言编译过程浅析_第2张图片

X 文件名  文件大小 代码行数
预处理前 test.cpp 490B 26
预处理后 test.i 420304B 17582

2、编译、优化(Compilation)

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

g++ -S test.i -o test.s -std=c++11

g++ -S mymath.i -o mymath.s

上述命令中-S让编译器在编译之后停止,不进行后续过程。编译过程完成后,生成程序的汇编代码test.s,内容如下所示(截取部分):

C语言编译过程浅析_第3张图片

3、汇编(Assemble)

汇编过程将汇编代码(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)源文件产生一个目标文件。

4、链接(Linking)

汇编程序生成的目标文件,即.o文件,并不会立即执行,因为可能会出现:.cpp文件中的函数引用了另一个.cpp文件中定义的符号或者调用了某个库文件中的函数。那链接的目的就是将这些文件对应的目标文件连接成一个整体,从而生成可执行的程序.exe文件。

命令大致如下:

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

链接分为两种:

  • 静态链接:代码从其所在的静态链接库中拷贝到最终的可执行程序中,在该程序被执行时,这些代码会被装入到该进程的虚拟地址空间中。

C语言编译过程浅析_第4张图片

 

  • 动态链接:代码被放到动态链接库或共享对象的某个文件中,链接程序只是在最终的可执行程序中记录了共享对象的名字等一些信息。在程序执行时,动态链接库的全部内容会被映射到运行时相应的虚拟地址的空间。

C语言编译过程浅析_第5张图片

 两者的优缺点:

  • 静态链接:浪费空间,每个可执行程序都会有目标文件的一个副本,这样如果目标文件进行了更新操作,就需要重新进行编译链接生成可执行程序(更新困难);优点就是执行的时候运行速度快,因为可执行程序具备了程序运行的所有内容。
  • 动态链接:节省内存、更新方便,但是动态链接是在程序运行时,每次执行都需要链接,相比静态链接会有一定的性能损失。

你可能感兴趣的:(C基础,c语言,开发语言,后端,编译器,gcc/gdb编译调试)