天天看点

一个C/C++程序的一生:从源程序到可执行程序再到进程

        当你在IDE里创建了一个工程,写好了你的各个头文件和源文件,按下运行按钮时,发生了什么?

        我们以C/C++程序为例,假如现在有一个工程test.pro,该工程中有源文件main.cpp、MyClass.cpp、MyClass2.cpp,头文件MyClass.h、MyClass2.h。

一个C/C++程序的一生:从源程序到可执行程序再到进程

        这是一个仅为了举例使用的简单工程,MyClass和MyClass2是两个什么功能都没写的类,主函数在main.cpp中,主函数中调用标准库函数printf输出字符串"Hello World!"。

#include <stdio.h>
#include "MyClass.h"
#include "MyClass2.h"

int main(int argc, char *argv[])
{
    const char *p="Hello World!";
    printf("%s",p);
    return 0;
}
           

        点击运行时,首先,你的源程序们会变成一个可执行程序,这个过程又可细分为四个阶段:预处理、编译、汇编、链接。

一个C/C++程序的一生:从源程序到可执行程序再到进程

预处理

        C/C++的宏替换和文件包含的工作,不归入编译器的范围,而是交给独立的预处理器。主要处理源代码文件中的以“#”开头的预编译指令。处理规则为:

  • 1、删除所有的#define,展开所有的宏定义。

    (注:通过这一点我们可以看到#define和const定义常量的区别,#define所做的是在预处理阶段简单地做字符串替换,不会进行任何安全检查,而const定义常量,是编译器来处理,编译器会检查是否有语法错误,比较而言,const定义常量更加安全。)

  • 2、处理所有的条件预编译指令,如“#if”、“#endif”、“#ifdef”。
  • 3、处理“#include”预编译指令,将文件内容替换到它的位置。比如我们这个例子中的#include <stdio.h>命令,预处理器会读取系统头文件stdio.h的内容,并把它直接插入到程序文本中。
  • 4、删除所有的注释,“//”和“”。
  • 5、保留所有的#pragma编译器指令,编译器需要用到他们,如:#pragma once 是为了防止有文件被重复引用。
  • 6、添加行号和文件标识,便于编译时编译器产生调试用的行号信息,编译时产生编译错误或警告能够显示行号。

.c和.cpp文件经过预处理后就变成了.i和.ii文件。

一个C/C++程序的一生:从源程序到可执行程序再到进程

编译

        把预处理之后生成的.i(C)或.ii(C++)文件,进行一系列词法分析、语法分析、语义分析及优化后,生成相应的汇编代码文件。

一个C/C++程序的一生:从源程序到可执行程序再到进程

汇编

        汇编器根据汇编指令和机器指令的对照表,将汇编代码一 一翻译成机器码。经汇编之后,产生可重定位的目标文件(与可执行文件格式几乎一样)xxx.o(Windows下)、xxx.obj(Linux下)。

一个C/C++程序的一生:从源程序到可执行程序再到进程

        我们来看看可重定位的目标文件格式,Windows使用的是PE(Portable Executable)格式,Linux和Unix系统使用的是ELF(Executable and Linkable Format)格式,不管是哪种格式,基本的概念是相似的。下图是ELF可重定位目标文件的格式,一个典型的ELF可重定位目标文件包含下面几个节:

一个C/C++程序的一生:从源程序到可执行程序再到进程

链接(这里讲的链接,严格说应该叫静态链接。)

        链接就是将工程中的多个可重定位目标文件组合成单一可执行文件的过程。在Windows系统下可执行文件的后缀是.exe,在Unix系统下是.out,由于我的计算机使用的Windows系统,最后生成的可执行文件是test.exe。

一个C/C++程序的一生:从源程序到可执行程序再到进程
一个C/C++程序的一生:从源程序到可执行程序再到进程

链接器必须完成两个任务:

  • 1、 符号解析:目标文件.o定义和引用符号,每个符号对应于一个函数、一个全局变量或一个静态变量,符号解析的目的是将每个符号引用和定义关联起来。
  • 2、 重定位:汇编器生成地址从0开始的可重定位目标文件,链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使得它们指向这个内存位置。

        对每个可重定位目标文件而言,它们的代码、只读数据、全局变量、静态变量等的地址都是相对于本文件而言的。在例子中的main.o、MyClass.o、MyClass2.o,它们文件中的符号(函数、全局变量、静态变量)的地址都在本文件中定好了,链接器所做的工作就是把这些文件组合成一个文件,在组合的过程中,要把每个文件中的这些符号的地址修改一下,修改的规则如下图,EIF可重定位目标文件的每个节会被映射到可执行目标文件对应的节中。

一个C/C++程序的一生:从源程序到可执行程序再到进程

        源程序们经过预处理、编译、汇编、链接最终生成了一个可执行程序。其中,预处理器、编译器、汇编器、链接器一起构成了编译系统(简称编译器)。常见的编译系统有GCC、MSVC。

        可执行程序是一个静态的概念,我们可以打开QQ安装目录看看QQ的可执行程序(应用程序),这个程序和我们电脑上运行着的QQ有什么关系呢?

        程序保存在磁盘上,是静态的,你不去删除它它就一直都存在。运行着的QQ是一个进程,而进程运行在内存上,是动态的,你把运行着的QQ关了,那么这个进程就被操作系统回收了,这个进程就消亡了。

一个C/C++程序的一生:从源程序到可执行程序再到进程

       一个工程中的源程序在点击运行后,会经历预处理、编译、汇编、链接生成可执行程序,可执行程序中的某些节再被加载到内存中,最终成为进程。

继续阅读