首先,我们从编译原理开始
一、编译原理概念:
1、编译:编译器对源文件进行编译,就是把源文件中的文本形式存在的源代码翻译成机器语言形式的目标文件的过程,在这个过程中,编译器会进行一系列的语法检查。如果编译通过,就会把对应的CPP转换成OBJ文件。
2、编译单元:根据C++标准,每一个CPP文件就是一个编译单元。每个编译单元之间是互相独立互相不可知。
3、目标文件:由编译所生成的文件,以机器码的形式包含了编译单元里所有的代码和数据,还有一些其他信息,如未解决符号表,导出符号表和地址重定向表等。目标文件是以二进制的形式存在。
根据C++标准,一个编译单元(Translation Unit)是一个.cpp文件以及使用include包含的所有.h文件,.h文件里面的代码将都会扩展到它的.cpp文件中,然后编译器编译该.cpp文件为一个.obj文件,后者拥有PE(Protable Executable,即Windows可执行文件)文件格式,并且本身包含的就是二进制代码,但是不一定能执行,因为并不能保证其中一定有main()函数。当编译器将一个工程里的所有.cpp文件以分离的方式编译完成后,再以连接器链接称为一个.exe或.dll文件。
二、编译器分析
下面通过两个例子来分析编译器的工作过程:
eg A:
我们跳过语法分析,直接来到目标文件的生成,假设我们有一个A.cpp文件,如下定义:
eg B:
到现在为止,我们已经知道,一个目标文件不仅要提供数据和二进制代码外,还至少要提供两个表:未解决符号表和导出符号表,来告诉连接器自己需要什么和自己能提供什么。
那么这两个表示怎么建立对应关系的呢?这里就有一个全新的概念:符号。在C/C++中,每一个变量及函数都会有自己的符号,如变量n的符号就是n,函数的符号就会更加复杂,假设FunA()的符号就是_FunA()根据编译器不同而不同。
所以,
在链接的时候,链接在B.obj中发现了未解决符号,就会在所有的编译单元中的导出符号表去查找与这个未解决符号相匹配的符号名,如果找到,就把这个符号的地址填到B.obj的未解决符号的地址处。如果没有找到,就会报链接错误。在此例中,在A.obj中会找到符号n,就会把n的地址填到B.obj的0x0001处。
但是,这里还会有一个问题,如果是这样的话,B.obj的函数FunB的内容就会变成inc DWORDPTR[0x000](因为n在A.obj中的地址是0x0000),由于每个编译单元的地址都是从0x0000开始,那么最终多个目标文件链接时就会导致地址重复。所以链接器在链接时就会对每个目标文件的地址进行调整。在这个例子中,假如B.obj的0x0000被定位到可执行文件的0x00001000上,而A.obj的0x0000被定位到可执行文件的0x00002000上,那么实现上对链接器来说,A.obj的导出符号地地址都会加上0x00002000,B.obj所有的符号地址也会加上0x00001000。这样就可以保证地址不会重复。
既然n的地址会加上0x00002000,那么FunA中的inc DWORDPTR[0x0000]就是错误的,所以目标文件还要提供一个表,叫地址重定向表,address redirect table。
总结一下:
目标文件至少要提供三个表:未解决符号表,导出符号表和地址重定向表。
为解决符号表:列出了本单元里有引号但是不在本单元定义的符号以及出现的地址。
符号导出表:提供了本编辑单元具有定义,并且可以提供给其他编译单元使用的符号及其在本单元中的地址。
地址重定向表:提供了本编译单元所有对自身地址的引用记录。
链接器的工作顺序:
当连接器进行链接的时候,
(a)首先决定各个文件在最终可执行文件里的位置。
(b)、然后访问所在目标文件的地址重定义表,对其记录的地址进行重定向(加上一个偏移量,即该编译单元在可执行文件上的起始地址)
(c)、然后遍历所有目标文件的未解决符号表,并且在所有的导出符号表里查找匹配的符号,并在未解决符号表中所记录的位置上填写实现地址
(d)、最后把所有的目标内容卸载各自的位置上,再做一些其他的工作,就生成一个可执行文件。
下面再看一下C/C++特工的一些特性:
extern:这就是告诉编译器,这个变量或函数在别的编译单元里定义了,也就是要把这个符号放到未解决符号表里面去(外部链接)。
static:如果该关键字位于全局函数或者变量的声明前面,表明改变编译单元不导出这个函数或变量,因此这个符号不能在别的编译单元中使用(内部链接)。如果是static局部变量,则该变量的存储方式和全局变量一样,但是仍然不导出符号。
默认链接属性:对于函数和变量,默认链接是外部链接,对于const变量,默认是内部链接。
外部链接的利弊:
外部链接的符号在整个程序范围内都是可以使用的,这就要求其他编译单元不能导出相同的符号(不然会报duplicated exteranl symbols)
内部连接的利弊:
内部链接的符号不能再别的编译单元使用。但不同的编译单元可以拥有同样的名称的符号。
为什么头文件里一般只可以有声明不能有定义?
头文件可以被多个编译单元包含,如果头文件里面有定义的话,那么每个包含这头文件的编译单元都会对同一个符号进行定义,如果该符号为外部链接,则会导致duplicatedexternal symbols链接错误。
为什么公共使用的内联函数要定义于头文件里?
因为编译时编译单元之间互不知道,如果内联被定义于.cpp文件中,编译其他使用该函数的编译单元的时候没有办法找到函数的定义,因些无法对函数进行展开。所以如果内联函数定义于.cpp里,那么就只有这个.cpp文件能使用它。