详解 “预处理、编译(compile)、 代码优化、 汇编(Assembly)、 链接 (link)”的具体过程

来源:互联网 发布:java snmp4j 编辑:程序博客网 时间:2024/06/15 18:18

我们在用vc写好代码后,在进行编译和连接的时候,经常会遇到各种问题:

如在连接时遇到的问题:(1)error LNK2019: 无法解析的外部符号*** ,该符号在函数***中被引用
   (2)error LNK2001: 无法解析的外部符号 ***

等等,出现了这种问题,我们就去百度,谷歌,看到解决方法就去试,试好了,就过了,但是,这到底是怎么回事呢,如果我们能对编译和连接的原理理解的话,这种问题就比较好解决了。下面是在网上看到的对编译和连接的比较好的解释,以便参考。

-------------------------------------预处理、编译、 优化、 汇编、 链接-------------------------------------------------------

编译程序读取源程序(字符流),对之进行词法和语法的分析,将高级语言指令转换为功能等效的汇编代码,再由汇编程序转换为机器语言(目标文件),并且按照操作系统对可执行文件格式的要求链接生成可执行程序。
C源程序头文件-->预编译处理(cpp)-->编译程序本身-->优化程序-->汇编程序-->链接程序-->可执行文件

1.编译预处理读取c源程序,对其中的伪指令(以#开头的指令)和特殊符号进行处理伪指令主要包括以下四个方面
(1)宏定义指 令,如#define Name TokenString,#undef等。对于前一个伪指令,预编译所要做的是将程序中的所有Name用TokenString替换,但作为字符串常量的 Name则不被替换。对于后者,则将取消对某个宏的定义,使以后该串的出现不再被替换。
(2)条件编译指令,如#ifdef,#ifndef,#else,#elif,#endif,等等。这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉
(3) 头文件包含指令,如#include "FileName"或者#include 等。在头文件中一般用伪指令#define定义了大量的宏(最常见的是字符常量),同时包含有各种外部符号的声明。采用头文件的目的主要是为了使某些定义可以供多个不同的C源程序使用。因为在需要用到这些定义的C源程序中,只需加上一条#include语句即可,而不必再 在此文件中将这些定义重复一遍。预编译程序将把头文件中的定义统统都加入到它所产生的输出文件中,以供编译程序对之进行处理。 包含到c源 程序中的头文件可以是系统提供的,这些头文件一般被放在/usr/include目录下。在程序中#include它们要使用尖括号(< >)。另外开发人员也可以定义自己的头文件,这些文件一般与c源程序放在同一目录下,此时在#include中要用双引号("")。
(4)特殊符号,预编译程序可以识别一些特殊的符号。例如在源程序中出现的LINE标识将被解释为当前行号(十进制数),FILE则被解释为当前被编译的C源程序的名称。预编译程序对于在源程序中出现的这些串将用合适的值进行替换。 预编译程序所完成的基本上是对源程序的“替代”工作。经过此种替代,生成一个没有宏定义、没有条件编译指令、没有特殊符号的输出文件。这个文件的含义同没有经过预处理的源文件是相同的,但内容有所不同。下一步,此输出文件将作为编译程序的输出而被翻译成为机器指令。

2.编译阶段
经过预编译得到的输出文件中,将只有常量。如数字、字符串、变量的定义,以及C语言的关键字,如main,if,else,for,while,{,}, +,-,*,\,等等。预编译程序所要作得工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代 码。

3.优化阶段
优化处理是编译系统中一项比较艰深的技术。它涉及到的问题不仅同编译技术本身有关,而且同机器的硬件环境也有很大的关系。优化一部分是对中间代码的优化。 这种优化不依赖于具体的计算机。另一种优化则主要针对目标代码的生成而进行的。上图中,我们将优化阶段放在编译程序的后面,这是一种比较笼统的表示。 对于前一种优化,主要的工作是删除公共表达式、循环优化(代码外提、强度削弱、变换循环控制条件、已知量的合并等)、复写传播,以及无用赋值的删除,等等。 后 一种类型的优化同机器的硬件结构密切相关,最主要的是考虑是如何充分利用机器的各个硬件寄存器存放的有关变量的值,以减少对于内存的访问次数。另外,如何 根据机器硬件执行指令的特点(如流水线、RISC、CISC、VLIW等)而对指令进行一些调整使目标代码比较短,执行的效率比较高,也是一个重要的研究课题。 经过优化得到的汇编代码必须经过汇编程序的汇编转换成相应的机器指令,方可能被机器执行。

4.汇编过程
汇编过程实际上指把汇编语言代码翻译成目标机器指令的过程。对于被翻译系统处理的每一个C语言源程序,都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。 目标文件由段组成。通常一个目标文件中至少有两个段:
代码段  该段中所包含的主要是程序的指令。该段一般是可读和可执行的,但一般却不可写。
数据段  主要存放程序中要用到的各种全局变量或静态的数据。一般数据段都是可读,可写,可执行的。
UNIX环境下主要有三种类型的目标文件:
(1)可重定位文件  其中包含有适合于其它目标文件链接来创建一个可执行的或者共享的目标文件的代码和数据。
(2)共享的目标文件  这种文件存放了适合于在两种上下文里链接的代码和数据。第一种事链接程序可把它与其它可重定位文件及共享的目标文件一起处理来创建另一个目标文件;第二种是动态链接程序将它与另一个可执行文件及其它的共享目标文件结合到一起,创建一个进程映象。
(3)可执行文件   它包含了一个可以被操作系统创建一个进程来执行之的文件。 汇编程序生成的实际上是第一种类型的目标文件。对于后两种还需要其他的一些处理方能得到,这个就是链接程序的工作了。

5.链接程序
由汇编程序生成的目标文件并不能立即就被执行,其中可能还有许多没有解决的问题。例如,某个源文件中的函数可能引用了另一个 源文件中定义的某个符号(如变量或者函数调用等);在程序中可能调用了某个库文件中的函数,等等。所有的这些问题,都需要经链接程序的处理方能得以解决。 链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够诶操作系统装入执行的统一整体。
根据开发人员指定的同库函数的链接方式的不同,链接处理可分为两种:
(1)静态链接 在这种链接方式下,函数的代码将从其所在地静态链接库中被拷贝到最终的可执行程序中。这样该程序在被执行时这些代码将被装入到该进程的虚拟地址空间中。静态链接库实际上是一个目标文件的集合,其中的每个文件含有库中的一个或者一组相关函数的代码。

(2) 动态链接  在此种方式下,函数的代码被放到称作是动态链接库或共享对象的某个目标文件中。链接程序此时所作的只是在最终的可执行程序中记录下共享对象的 名字以及其它少量的登记信息。在此可执行文件被执行时,动态链接库的全部内容将被映射到运行时相应进程的虚地址空间。动态链接程序将根据可执行程序中记录的信息找到相应的函数代码。 对于可执行文件中的函数调用,可分别采用动态链接或静态链接的方法。使用动态链接能够使最终的可执行文件比较 短小,并且当共享对象被多个进程使用时能节约一些内存,因为在内存中只需要保存一份此共享对象的代码。但并不是使用动态链接就一定比使用静态链接要优越。在某些情况下动态链接可能带来一些性能上损害。

来自:http://hi.baidu.com/weiyuanke/item/339dc8e92ba1d00f64db0076

------------------------------------------参考2----------------------------------------------

以c语言经典的“Hello World”为例

#include <stdio.h>

int main()

{

printf("Hello World]n");

return 0;

}

在Linux下, 使用GCC编译只需简单命令

$gcc Hello.c

$./a.out

Hello Word

使用Bulid过程可以分解为4个步骤

预处理(Prepressing), 编译(Compilation)、汇编(Assembly)、链接(Linking). 如图

预编译    

预编译过程主要处理那些源文件中的以“#”开始的预编译指令,主要处理规则有:

1) 将所有的“#define”删除,并展开所用的宏定义

2)处理所有条件预编译指令,比如“#if”、“#ifdef”、 “#elif”、“#endif”

3)处理“#include”预编译指令,将被包含的文件插入到该编译指令的位置,注:此过程是递归进行的

4)删除所有注释

5) 添加行号和文件名标识,以便于编译时编译器产生调试用的行号信息以及用于编译时产生编译错误或警告时可显示行号

6)保留所有的#pragma编译器指令。

编译

编译过程就是把预处理完的文件进行一系列的词法分析、语法分析、语义分析及优化后生成相应的汇编代码文件。这个过程是整个程序构建的核心部分。

b编译的过程一般可以分为6步:扫描、词法分析、语义分析、源代码优化、代码生成、和目标代码优化

词法分析

首先源代码程序被输入奥扫描器中,扫描器用一种类似于有限状态机(Finite State Machine)的算法可以轻易地将源代码分割成一系列记号(Token)。词法分析的生成的记号一本分为:关键字、标识符、字面量(数字,字符串等)和特殊符号(加号等号等),在识别记号的同时,扫描器还完成其他工作,比如将标识符存放在符号表中,将数字、字符串常量存放在文字表中等……

语法分析

将有扫描器生成的记号进行语法分析,从而生成语法树,整个分析过程采用了上下文无关语法(Context-free Grammar)的分析技术。

语法树实际上是以表达式为节点的树。

语法分析仅仅完成了表达式的语法层面的分析,并不了解这个语句是否是真正有意义的。

语义分析

编译器所能分析的予以是静态语义(Static Semantic),所谓静态语义是指在编译期间可以确定的语义。与之相对应的是动态语义。

静态语义通常包括声明和类型的匹配,类型的转换。

经过语义分析阶段后,整个语法树的表达式都被标识了类型,如有些类型需要做隐式类型转换,语义分析程序就会在语法树种插入相应节点。

中间语言生成:

现代编译器往往会有很多层次的优化,往往在源代码级会有一个优化过程。具体这里不详细介绍

目标生成代码与优化


汇编

汇编器是将汇编代码转化成机器可以执行的指令,每一条汇编语句几乎都是一条机器指令。经过编译、链接、汇编输出的文件成为目标文件(Object File)

链接

程序设计模块化是人们一直追求的目标,因为当一个系统十分复杂的时候,我们不得不吧程序划分都小的模块而各个击破。一个复杂的软件也是如此,人们把源代码模块独立编译,然后按照需求将他们组装起来,这个组装模块的过程就是链接。链接的主要内容就是把各个模块直接爱你相互引用的部分处理好,使各个模块可以正确的拼接。

链接的主要过程包块 地址和空间的分配(Address and Storage Allocation)、符号决议(Symbol Resolution)和重定位(Relocation)等步骤。

每个模块的源代码文件经过编译器编译成目标文件(Object File 一般扩展名为.o和.obj),目标文件和库(Library)一起链接形成最终的可执行文件。

在链接过程中,对其他定义在目标文件中的函数调用的指令必须重新调整,对使用其他定义在目标文件中的变量,也存在同样的问题。 假设我们在目标文件A有一个全局变量var,我们在目标文件B中要访问var。比如说B中有这么一条指令

movl $0x2a, var

这条指令是为var变量赋值0x2a,由于在编译目标文件B的时候,编译器并不知道变量var的目标地址,所以编译器在没法确定地址的情况下,将这条mov指令的目标地址设置为0,等到链接器在将目标文件A和B链接的时候再将其修正。这个地址修正的过程也叫做重定位。每个要被修正的地方叫一个重定位入口。重定位所作的就是给程序中每一个这样的绝对地址的位置“打补丁”,使其指向正确的地址。

来自:http://blog.csdn.net/guokefly/article/details/11564447
-------------------------------------------------------------------------------------------------
最后,说明下前面两个常见错误的解决方法:
(1)“error LNK2019: 无法解析的外部符号*** ,该符号在函数***中被引用”
解决方法:造成这种问题的原因比较多,主要是找不到某些符号或函数的定义造成,最多的原因是我们没有加入相应的静态库文件(.lib)
加入lib文件的方法有3中:
    (a)项目属性\链接器\输入\附加依赖项 增加***.lib
 (b)加入预编译指令#pragma comment(lib,”*.lib”)     ,具体参考:http://blog.csdn.net/bzhxuexi/article/details/11604123
(2)"error LNK2001: 无法解析的外部符号 ***"
分析:首先看看这个错误文件是不是我们自己定义的,如果是,看看定义时是否有错误;如果不是,就是我们不熟悉的文件或系统文件,那就是看看是不是没有加入相应的静态链接库文件(.lib),加入即可。还有很多原因造成这个问题,具体参考:http://blog.csdn.net/bzhxuexi/article/details/17758213

0 0
原创粉丝点击