Skip to content

Files

Latest commit

Aug 31, 2016
8687251 · Aug 31, 2016

History

History
134 lines (78 loc) · 10.5 KB

Compiler.md

File metadata and controls

134 lines (78 loc) · 10.5 KB

C++ 编译器工作原理

简单地说,一个编译器就是一个程序,它可以阅读以某一种语言(源语言)编写的程序,并把该程序翻译成一个等价的、用另一种语言(目标语言)编写的程序。

C/C++编译系统将一个程序转化为可执行程序的过程包含:

  • 预处理(preprocessing):根据已放置的文件中的预处理指令来修改源文件的内容。
  • 编译(compilation):通过词法分析和语法分析,在确认所有指令都是符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。
  • 汇编(assembly):把汇编语言代码翻译成目标机器指令的过程。
  • 链接(linking):找到所有用到的函数所在的目标文件,并把它们链接在一起合成为可执行文件(executable file)。

预处理

预处理器是在程序源文件被编译之前根据预处理指令对程序源文件进行处理的程序。预处理器指令以#号开头标识,末尾不包含分号。预处理命令不是C/C++语言本身的组成部分,不能直接对它们进行编译和链接。C/C++语言的一个重要功能是可以使用预处理指令和具有预处理的功能。C/C++提供的预处理功能主要有文件包含、宏替换、条件编译等。

文件包含

预处理指令 #include 用于包含头文件,有两种形式:#include <xxx.h>,#include "xxx.h"。

尖括号形式表示被包含的文件在系统目录中。如果被包含的文件不一定在系统目录中,应该用双引号形式。在双引号形式中可以指出文件路径和文件名。如果在双引号中没有给出绝对路径,则默认为用户当前目录中的文件,此时系统首先在用户当前目录中寻找要包含的文件,若找不到再在系统目录中查找。

对于用户自己编写的头文件,宜用双引号形式。对于系统提供的头文件,既可以用尖括号形式,也可以用双引号形式,都能找到被包含的文件,但显然用尖括号形式更直截了当,效率更高。

宏替换

宏定义:一般用一个短的名字代表一个长的代码序列。宏定义包括无参数宏定义和带参数宏定义两类。宏名和宏参数所代表的代码序列可以是任何意义的内容,如类型、常量、变量、操作符、表达式、语句、函数、代码块等。

宏定义在源文件中必须单独另起一行,换行符是宏定义的结束标志,因此宏定义以换行结束,不需要分号等符号作分隔符。如果一个宏定义中代码序列太长,一行不够时,可采用续行的方法。续行是在键入回车符之前先键入符号\,注意回车要紧接在符号\之后,中间不能插入其它符号,当然代码序列最后一行结束时不能有\。

预处理器在处理宏定义时,会对宏进行展开(即宏替换)。宏替换首先将源文件中在宏定义随后所有出现的宏名均用其所代表的代码序列替换之,如果是带参数宏则接着将代码序列中的宏形参名替换为宏实参名。宏替换只作代码字符序列的替换工作,不作任何语法的检查,也不作任何的中间计算,一切其它操作都要在替换完后才能进行。如果宏定义不当,错误要到预处理之后的编译阶段才能发现。

条件编译

一般情况下,在进行编译时对源程序中的每一行都要编译,但是有时希望程序中某一部分内容只在满足一定条件时才进行编译,如果不满足这个条件,就不编译这部分内容,这就是条件编译

条件编译主要是进行编译时进行有选择的挑选,注释掉一些指定的代码,以达到多个版本控制、防止对文件重复包含的功能。if, #ifndef, #ifdef, #else, #elif, #endif是比较常见条件编译预处理指令,可根据表达式的值或某个特定宏是否被定义来确定编译条件。

此外,还有 #pragma 指令,它的作用是设定编译器的状态或指示编译器完成一些特定的动作。

编译

编译过程的第一个步骤称为词法分析(lexical analysis)或扫描(scanning),词法分析器读入组成源程序的字符流,并且将它们组织成有意义的词素的序列,对于每个词素,词法分析器产生一个词法单元(token),传给下一个步骤:语法分析。

语法分析(syntax analysis)或解析(parsing)是编译的第二个步骤,使用词法单元来创建树形的中间表示,该中间表示给出了词法分析产生的词法单元流的语法结构。一个常用的表示方法是语法树(syntax tree),树中每个内部结点表示一个运算,而该结点的子结点表示该运算的分量。

接下来是语义分析(semantic analyzer),使用语法树和符号表中的信息来检测源程序是否和语言定义的语义一致。

在源程序的语法分析和语义分析之后,生成一个明确的低级的或者类机器语言的中间表示。接下来一般会有一个机器无关的代码优化步骤,试图改进中间代码,以便生成更好的目标代码。

汇编

对于被翻译系统处理的每一个C/C++语言源程序,都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标机器语言代码。目标文件由段组成,通常一个目标文件中至少有两个段:代码段和数据段。

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

链接

链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够按操作系统装入执行的统一整体。主要有静态链接和动态链接两种方式:

  • 静态链接:在链接阶段,会将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中,程序运行的时候不再需要静态库文件。
  • 动态链接:把调用的函数所在文件模块(DLL)和调用函数在文件中的位置等信息链接进目标程序,程序运行的时候再从DLL中寻找相应函数代码,因此需要相应DLL文件的支持。

这里的库是写好的现有的,成熟的,可以复用的代码。现实中每个程序都要依赖很多基础的底层库,不可能每个人的代码都从零开始,因此库的存在意义非同寻常。本质上来说库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。库有两种:静态库(.a、.lib)和动态库(.so、.dll),所谓静态、动态是指链接方式的不同。

静态链接库与动态链接库都是共享代码的方式。如果采用静态链接库,程序在运行时与函数库再无瓜葛,移植方便。但是会浪费空间和资源,因为所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件。此外,静态库对程序的更新、部署和发布也会带来麻烦。如果静态库更新了,所有使用它的应用程序都需要重新编译、发布给用户。

动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入。不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。动态库在程序运行是才被载入,也解决了静态库对程序的更新、部署和发布页会带来麻烦。用户只需要更新动态库即可,增量更新。

此外,还要注意静态链接库中不能再包含其他的动态链接库或者静态库,而在动态链接库中还可以再包含其他的动态或静态链接库。

简单的例子

下面是一个保存在文件 helloworld.cpp 中一个简单的 C++ 程序的代码:

/* helloworld.cpp */
#include <iostream>
int main(int argc,char *argv[])
{
    std::cout << "hello, world" << std::endl;
    return 0;
}

用下面命令编译:

$ g++ helloworld.cpp

编译器 g++ 通过检查命令行中指定的文件的后缀名可识别其为 C++ 源代码文件。编译器默认的动作:编译源代码文件生成对象文件(object file),链接对象文件和 libstd c++ 库中的函数得到可执行程序,然后删除对象文件。由于命令行中未指定可执行程序的文件名,编译器采用默认的 a.out。

预处理

选项 -E 使 g++ 将源代码用编译预处理器处理后不再执行其他动作。下面的命令预处理源码文件 helloworld.cpp,并将结果保存在 .ii 文件中:

➜  ~  g++ -E helloworld.cpp -o helloworld.ii
➜  ~  ls | grep helloworld
helloworld.cpp
helloworld.ii
➜  ~  wc -l helloworld.ii
   38126 helloworld.ii

helloworld.cpp 的源代码,仅仅有六行,而且该程序除了显示一行文字外什么都不做,但是,预处理后的版本将超过3万行。这主要是因为头文件 iostream 被包含进来,而且它又包含了其他的头文件,除此之外,还有若干个处理输入和输出的类的定义。

编译

选项 -S 指示编译器将程序编译成汇编代码,输出汇编语言代码而后结束。下面的命令将由 C++ 源码文件生成汇编语言文件 helloworld.s,生成的汇编语言依赖于编译器的目标平台。

g++ -S helloworld.cpp

汇编

选项 -c 用来告诉编译器将汇编代码(.s文件,或者直接对源代码)转换为目标文件,但不要执行链接。输出结果为对象文件,文件默认名与源码文件名相同,只是将其后缀变为 .o。

➜  ~  g++ -c helloworld.s
➜  ~  ls |grep helloworld.o
helloworld.o

链接

加载相应的库,执行链接操作,将对象文件(.o,也可以直接将原文件)转化成可执行程序。

➜  ~  g++ helloworld.o -o helloworld.o
➜  ~  ./helloworld.o
hello, world

更多阅读

详解C/C++预处理器
Compiling Cpp
C++静态库与动态库
高级语言的编译:链接及装载过程介绍
编译原理 (预处理>编译>汇编>链接)