前置
1、C++标准中提到,一个编译单元[translation unit]是指一个.cpp文件以及它所include的所有.h文件,.h文件里的代码将会被扩展到包含它的.cpp文件里,然后编译器编译该.cpp文件为一个.obj文件,后者拥有PE[Portable Executable,即windows可执行文件]文件格式,并且本身包含的就已经是二进制码,但是,不一定能够执行,因为并不保证其中一定有main函数。当编译器将一个工程里的所有.cpp文件以分离的方式编译完毕后,再由连接器(linker)进行连接成为一个.exe文件。
特别地,对于模板,模板函数的代码其实并不能直接编译成二进制代码,其中要有一个“具现化”的过程。
2、我们知道,一般的一个C++程序是从main开始执行的,随着main函数的结束而结束。然而,在main函数被调用之前,为了 程序能够顺利执行,要先初始化进程执行环境,比如:堆分配初始化(malloc、free)、线程子系统等。而 c++的全局对象的构造函数也是在这一时期被执行的。C++的全局对象的构造函数在main之前被执行,C++的全局析构函数在main之后被执行。
关于目标文件
程序源代码被编译后主要分为两种段:代码段和数据段(程序指令和程序数据)。代码段属于程序指令,常见名字为.code、.text。数据段属于程序数据,常见名字为.data。
对于数据段,要讲一下.bss。一般未初始化的全局变量和局部静态变量放在.bss段里,由于为这些未初始化的东西在.data分配空间无意义,所以放在这里。.bss段只是为未初始化的全局变量和局部静态变量预留位置,并没有内容,所以它在文件中也不占据空间强符号与弱符号。
对于C/C++语言来说,编译器默认函数和初始化了的全局变量为强符号,未初始化的变量为弱符号。(注意,强符号和弱符号都是针对定义来说的(而非针对引用来说的)。)如果一个模块中有两个或以上的强符号,会报重定义。如果一个符号在某个目标文件中是强符号,在其他文件中都是弱符号,那么选择强符号。如果一个符号在所有目标文件中都是弱符号,那么选择其中占用内存空间最大的一个。
关于COMMON块。了解不多,暂时留白。
经典链接模型
每个 OBJ 文件包含两个符号列表。
提供的符号:这些是 OBJ 包含定义的符号。
需要的符号:这些是 OBJ 想要定义的符号。
对于添加到模块的 OBJ 文件中的每个提供的符号:
如果该符号已在标记为已解析的模块中,则引发错误,抱怨对象具有多个定义。
如果该符号已在标记为未解析的模块中,则将其标记更改为已解析。
否则,符号不在模块中。添加它并将其标记为已解析。
对于添加到模块的 OBJ 文件中的每个所需符号:
如果该符号已在标记为已解析的模块中,则将其标记为已解析。
如果该符号已在标记为未解析的模块中,则将其标记为未解析。
否则,符号不在模块中。添加它并将其标记为未解析。
链接器用来解析符号的算法是这样的:
初始条件:将所有明确提供的 OBJ 文件添加到模块中。
于是,每当有一个未解析的符号时:
查看所有 LIB 以找到第一个提供符号的 OBJ。
如果找到:将该 OBJ 添加到模块中。
如果未找到:引发错误,抱怨未解析的外部问题。 (如果链接器有可用的信息,它可能会提供其他详细信息。)
这就是链接和未解析的外部的全部内容。至少,这就是经典模型的全部内容。
值得注意的事情是,现代编译器引入了很多非经典行为。
例如,在.h中初始化静态变量之类的用法,按经典模型会因多次包含该.h文件而报重定义的错误,但是当原始符号和新符号都标记为 __declspec(selectany)时,不会引发错误。而是任意选择一个并丢弃另一个。否则,还是会报重定义。【__declspec是一个Microsoft Visual C++特定的编译器属性开关。括号中指明的是哪一个属性生效。关于__declspec的其他属性可以使用' __declspec'搜索MSDN进行查看。】再例如,关于没有被引用过的代码的删除,在本任务二周目问题解决中提到过的,链接器优化相关参数(详情搜索/OPT:REF,/Gy,/OPT:ICF相关内容)我有一次任务发现,只是声明一个符号,而没有使用这个符号,那链接器也不会去链这个符号所在的编译单元(不知道是编译器还是链接器的事情,有可能这一步是编译器搞的)。
动态链接
动态链接相比静态链接的优点之一是:保证同一个模块,如果被多个其他模块引用,不需要把它放在内存中两份。在动态链接中,对于共享模块中的地址,按照内部引用/外部引用和指令引用/数据引用,总共有四种情况,使用地址无关代码(PIC)技术实现,四种情况实现方式不同。
这种技术是说链接时,将程序模块中指令共享的部分,与指令不共享的部分分离开来,把不共享的部分和数据部分放在一起。共享的共享,不共享的每个进程拥有一个副本。四种情况分别是:模块内部的函数调用、跳转等;模块内部的数据访问,如模块内部定义的全局变量、静态变量;模块外部的函数调用、跳转等;模块外部的数据访问,如其他模块中定义的全局变量。在动态链接中,针对定义在模块内部的全局变量,由于{当一个模块引用了一个定义在共享对象的全局变量的时候,1、编译器无法判断是否为跨模块间的调用;2、当此引用处于主模块时,由于主模块没有地址无关代码},所以对于模块内部的全局变量有特殊的处理方式,会让他们都引用主模块中的地址。
静态链接
静态库lib可以简单地看成一组目标文件的集合。假如我的有一个源代码为HelloWorld.c。在链接时,它通过文本查找(使用objdump或readelf,再加上文本查找工具如grep),它会在libc.a中找到一些结果,它从结果中发现,ptritf函数被定义在了printf.o中。然后从libc.a中把printf.o解压出来,再把printf.o和hello.c链接在一起就可以了。(这里还有一个递归过程,如果printf.o有引用其他外部符号,会自动继续查找下去并一起链接上去。)
链接时,静态库顺序导致符号未定义问题
· 问题实例(编译安卓时遇到的,PC/IOS端相同代码均无此问题--->这是g++的问题,安卓端使用g++):
已知:Engine.cpp在Common项目内。CreateTaskGraph在Platform项目内定义。
CreateTaskGraph函数在Platform项目中定义,在Engine.cpp内被使用。而编译顺序上,先编译Common项目,后编译Platform项目。在实践中,报了未解析符号CreateTaskGraph的错误。
参考链接:(2条消息) 编译链接的时候静态库顺序导致符号未定义问题详解_zerooffdate的专栏-优快云博客_符号未定义
编译时,会按照顺序依次解析静态库中的符号,会搞一个未解析符号表,每解析一个库,都会把已解决的符号打勾,新的未解决的符号加入该表,并且,不会回头。
所以,编译器在解析时,解析Common后,将未解决符号CreateTaskGraph加入表,但是由于它不会回头,而Platform已经解析过了,所以最终CreateTaskGraph仍是未解决的符号,从而报错。