C++编译器与链接器工作原理 + Link错误

http://blog.csdn.net/qq_20389175/article/details/44159061

VC项目调试基础 --http://blog.csdn.net/phunxm/article/details/5203931

一.Debug版本和Release版本的区别

Debug通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序。Release称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用。

 

一般而言Debug版本会比Release版本多出*.ilk文件和*.pdb文件。

1.*.ilk文件

ilk后缀全称为“Incremental Linking”,意即增量链接。

VC6中,“Project Setting->Link(Category:General)”默认勾选“Link incrementally”;相应VC2005中,“项目属性->配置属性->链接器->常规->启用增量链接”默认选项为“是(/INCREMENTAL)”。

当选定渐增型编译链接时,链接器自动生成ILK文件,记录链接信息,也就是每次重新编译并不编译所有的源文件,只编译改动过的文件。而编译器怎么知道哪些编译过哪些未编译过呢,除了检查修改时间外,这个ilk文件也是很重要的信息。

2.*.pdb文件

pdb后缀全称为“Program Debug Database”,意即程序数据库文件。

“项目属性->配置属性->链接器->调试”,默认“生成调试信息”选项为“是(/DEBUG)”,默认“生成程序数据库文件”处填写“.\Debug\*.pdb”。该选项对应的编译开关为/PDB。

 

符号文件(Symbol Files)是一个数据信息文件,它包含了应用程序二进制文件(比如EXE、DLL等)的调试信息,专门用来作调试之用,最终生成的可执行文件在运行时并不需要这个符号文件,但你的程序中所有的变量信息都记录在这个文件中。所以调试应用程序时,这个文件是非常重要的。用VC和 WinDbg调试程序时都要用到这个文件。


---------------麻将  11.25 断点进不去。 搞了一下午


表现: F11进入地方与目标位置相错2行。 从未遇过:同一个函数内部,前一个断点实心,后一空心如上提示。       解决:代码保留一份,全删除,在copy进来


----

*.ilk文件  :  编译具体编译那个.cpp要靠此文件 <编译器本身会:检查修改时间外>增量链接。
*.pdb文件: 专门用来作调试之用  <所有的变量信息都记录在这个文件中> 程序数据库文件。



.INI:配置文件。
.OBJ:由编译器或汇编工具生成的目标文件,是模块的二进制中间文件。
.PCH:预编译头文件,比较大,由编译器在建立工程时自动生成, 在以后建立工程时不再重新编译这些代码,以便加快整个编译过程的速度。
#include "stdafx.h"  是MFC东西.  手机上是肯定不能用的

VC编译时可以选择incremental linking(渐增式编译),也就是每次重新编译并不编译所有的源文件,只编译改动过的文件。


C++静态库与动态库   ---  http://www.cnblogs.com/skynet/p/3372855.html
---------------------------------------------------------------

编译器编译原理详解

这里并没不是讨论大学课程中所学的《编译原理》,只是写一些我自己对C++编译器及链接器的工作原理的理解和看法吧,以我的水平,还达不到讲解编译原理(这个很复杂,大学时几乎没学明白)。

要明白的几个概念:

    1、编译:编译器对源文件进行编译,就是把源文件中的文本形式存在的源代码翻译成机器语言形式的目标文件的过程,在这个过程中,编译器会进行一系列的语法检查。如果编译通过,就会把对应的CPP转换成OBJ文件。

    2、编译单元:根据C++标准,每一个CPP文件就是一个编译单元。每个编译单元之间是相互独立并且互相不可知。

    3、目标文件:由编译所生成的文件,以机器码的形式包含了编译单元里所有的代码和数据,还有一些期他信息,如未解决符号表,导出符号表和地址重定向表等。目标文件是以二进制的形式存在的。

    根据C++标准,一个编译单元(Translation Unit)是指一个.cpp文件以及这所include的所有.h文件,.h文件里面的代码将会被扩展到包含它的.cpp文件里,然后编译器编译该.cpp文件为一个.obj文件,后者拥有PE(Portable Executable,即Windows可执行文件)文件格式,并且本身包含的就是二进制代码,但是不一定能执行,因为并不能保证其中一定有main函数。当编译器将一个工程里的所有.cpp文件以分离的方式编译完毕后,再由链接器进行链接成为一个.exe或.dll文件。

下面让我们来分析一下编译器的工作过程:

我们跳过语法分析,直接来到目标文件的生成,假设我们有一个A.cpp文件,如下定义:

    int n = 1;

    void FunA()

    {

        ++n;

    }

    它编译出来的目标文件A.obj就会有一个区域(或者说是段),包含以上的数据和函数,其中就有n、FunA,以文件偏移量形式给出可能就是下面这种情况:

    偏移量    内容    长度

    0x0000    n       4

    0x0004    FunA    ??

    注意:这只是说明,与实际目标文件的布局可能不一样,??表示长度未知,目标文件的各个数据可能不是连续的,也不一定是从0x0000开始。

    FunA函数的内容可能如下:

    0x0004 inc DWORD PTR[0x0000]

    0x00?? ret

    这时++n已经被翻译成inc DWORD PTR[0x0000],也就是说把本单元0x0000位置的一个DWORD(4字节)加1。

    有另外一个B.cpp文件,定义如下:

    extern int n;

    void FunB()

    {

        ++n;

    }

    它对应的B.obj的二进制应该是:

    偏移量    内容    长度

    0x0000    FunB    ??

    这里为什么没有n的空间呢,因为n被声明为extern,这个extern关键字就是告诉编译器n已经在别的编译单元里定义了,在这个单元里就不要定义了。由于编译单元之间是互不相关的,所以编译器就不知道n究竟在哪里,所以在函数FunB就没有办法生成n的地址,那么函数FunB中就是这样的:

    0x0000 inc DWORD PTR[????]

    0x00?? ret

    那怎么办呢?这个工作就只能由链接器来完成了。

    为了能让链接器知道哪些地方的地址没有填好(也就是还????),那么目标文件中就要有一个表来告诉链接器,这个表就是“未解决符号表”,也就是unresolved symbol table。同样,提供n的目标文件也要提供一个“导出符号表”也就是exprot symbol table,来告诉链接器自己可以提供哪些地址。

    好,到这里我们就已经知道,一个目标文件不仅要提供数据和二进制代码外,还至少要提供两个表:未解决符号表和导出符号表,来告诉链接器自己需要什么和自己能提供些什么。那么这两个表是怎么建立对应关系的呢?这里就有一个新的概念:符号。在C/C++中,每一个变量及函数都会有自己的符号,如变量n的符号就是n,函数的符号会更加复杂,假设FunA的符号就是_FunA(根据编译器不同而不同)。

    所以,

    A.obj的导出符号表为

    符号    地址

    n       0x0000

    _FunA   0x0004

    未解决符号为空(因为他没有引用别的编译单元里的东西)。

    B.obj的导出符号表为

    符号    地址

    _FunB   0x0000

    未解决符号表为

    符号    地址

    n       0x0001

    这个表告诉链接器,在本编译单元0x0001位置有一个地址,该地址不明,但符号是n。

    在链接的时候,链接在B.obj中发现了未解决符号,就会在所有的编译单元中的导出符号表去查找与这个未解决符号相匹配的符号名,如果找到,就把这个符号的地址填到B.obj的未解决符号的地址处。如果没有找到,就会报链接错误。在此例中,在A.obj中会找到符号n,就会把n的地址填到B.obj的0x0001处。

    但是,这里还会有一个问题,如果是这样的话,B.obj的函数FunB的内容就会变成inc DWORD PTR[0x000](因为n在A.obj中的地址是0x0000),由于每个编译单元的地址都是从0x0000开始,那么最终多个目标文件链接时就会导致地址重复。所以链接器在链接时就会对每个目标文件的地址进行调整。在这个例子中,假如B.obj的0x0000被定位到可执行文件的0x00001000上,而A.obj的0x0000被定位到可执行文件的0x00002000上,那么实现上对链接器来说,A.obj的导出符号地地址都会加上0x00002000,B.obj所有的符号地址也会加上0x00001000。这样就可以保证地址不会重复。

    既然n的地址会加上0x00002000,那么FunA中的inc DWORD PTR[0x0000]就是错误的,所以目标文件还要提供一个表,叫地址重定向表,address redirect table。

    总结一下:

    目标文件至少要提供三个表:未解决符号表,导出符号表和地址重定向表。

    未解决符号表:列出了本单元里有引用但是不在本单元定义的符号及其出现的地址。

    导出符号表:提供了本编译单元具有定义,并且可以提供给其他编译单元使用的符号及其在本单元中的地址。

    地址重定向表:提供了本编译单元所有对自身地址的引用记录。

    链接器的工作顺序:

    当链接器进行链接的时候,首先决定各个目标文件在最终可执行文件里的位置。然后访问所有目标文件的地址重定义表,对其中记录的地址进行重定向(加上一个偏移量,即该编译单元在可执行文件上的起始地址)。然后遍历所有目标文件的未解决符号表,并且在所有的导出符号表里查找匹配的符号,并在未解决符号表中所记录的位置上填写实现地址。最后把所有的目标文件的内容写在各自的位置上,再作一些另的工作,就生成一个可执行文件。

    说明:实现链接的时候会更加复杂,一般实现的目标文件都会把数据,代码分成好向个区,重定向按区进行,但原理都是一样的。

    明白了编译器与链接器的工作原理后,对于一些链接错误就容易解决了。

    下面再看一看C/C++中提供的一些特性:

    extern:这就是告诉编译器,这个变量或函数在别的编译单元里定义了,也就是要把这个符号放到未解决符号表里面去(外部链接)。

    static:如果该关键字位于全局函数或者变量的声明前面,表明该编译单元不导出这个函数或变量,因些这个符号不能在别的编译单元中使用(内部链接)。如果是static局部变量,则该变量的存储方式和全局变量一样,但是仍然不导出符号。

    默认链接属性:对于函数和变量,默认链接是外部链接,对于const变量,默认内部链接。

外部链接的利弊:外部链接的符号在整个程序范围内都是可以使用的,这就要求其他编译单元不能导出相同的符号(不然就会报duplicated external symbols)。

内部链接的利弊:内部链接的符号不能在别的编译单元中使用。但不同的编译单元可以拥有同样的名称的符号。

    为什么头文件里一般只可以有声明不能有定义:头文件可以被多个编译单元包含,如果头文件里面有定义的话,那么每个包含这头文件的编译单元都会对同一个符号进行定义,如果该符号为外部链接,则会导致duplicated external symbols链接错误。

    为什么公共使用的内联函数要定义于头文件里:因为编译时编译单元之间互不知道,如果内联被定义于.cpp文件中,编译其他使用该函数的编译单元的时候没有办法找到函数的定义,因些无法对函数进行展开。所以如果内联函数定义于.cpp里,那么就只有这个.cpp文件能使用它。


-----------------------

函数库分为静态库和动态库两种。

  静态库在程序编译时会被连接到目标代码中,程序运行时将不再需要该静态库。

  动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入,因此在程序运行时还需要动态库存在。

例子:  没有重新编译, 如果编译了 怎么会再去Link


猜你喜欢

转载自blog.csdn.net/u013321328/article/details/52413285