Victo

我的网络笔记本,用于收藏和总结一些知识。

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: :: :: 管理 ::

1 几个基本概念

编译:编译器对源文件的编译过程,就是将源文件中的文本形式代码翻译为机器语言形式的目标文件的过程,此过程中会有一系列语法检查、指令优化等,生成目标(OBJ)文件。

编译单元:每一个CPP文件就是一个编译单元,每个单元之间是互相独立且不可知的。

目标文件:编译步骤产生的文件,包含了编译单元内所有代码和数据,以二进制形式存在。请注意三个关键表:未解决符号表、导出符号表、地址重定向表

链接:当编译器将工程中所有CPP文件以分离形式编译成各个对应目标(OBJ)文件之后,再由链接器进行链接生成一个EXE或者DLL文件。

2 一个例子

用一个例子来理解下编译器与链接器的工作:

file1.cpp

int gVal = 123;

void func1()
{
    ++gVal;
}

这个文件编译出来的目标文件file1.obj 就会有一个段来包含上面的数据和函数,内容大致如下(只是示意,并非完全一样):

偏移量    内容    长度

0x0000   gVal    4

0x0004   func1  ??

这里的??表示长度未知,实际目标文件的各个数据可能不是连续的,也不一定从0x0000开始。比如这里的func1可能是这样:

0x0004 inc DWORD PTR[0x0000]

0x00?? ret

这里把++gVal翻译为inc语句,也就是把本单元的0x0000地址的一个DWORD(4字节)进行加一。

file2.cpp

extern int gVal;

void func2()
{
    ++gVal;
}

对应的file2.obj文件内容应该是:

偏移量    内容    长度

0x0000   func2  ??

可以看到这里并没有gVal,原因是extern关键字声明这是个外部引用符号,已经在别的单元里面定义了。由于单元之间是隔离的,所以这里的func2代码就没有办法填写地址,大致是这样:

0x0004 inc DWORD PTR[????]

0x00?? ret

这个????就表示当前无法拿到有效地址,需要链接器来完成,如何告诉链接器?需要一个未解决符号表(unresolved symbol table),同样提供gVal符号的目标文件也要提供一个导出符号表(export symbol table)来告知链接器可以提供的符号内容。

这两个表之间依靠符号来进行关联,在C/C++中每一个变量和函数都有自己的符号名,函数名略复杂(依据编译器不同而有差异),假设这里函数func1的符号为_func1,那么file1.obj文件的导出符号为:

导出符号      地址

gVal             0x0000

_func1         0x0004

而对应的未解决符号表则为空,因为没有依赖其他单元的内容。另一个file2.cpp文件的导出符号表为:

导出符号      地址

_func2         0x0000

而对应的未解决符号表为(下表的含义是说0x0001位置有一个地址不明,符号叫gVal):

未决符号      地址

gVal             0x0001

链接器会针对未解决符号表中的在所有导出符号表中进行匹配,如果找到则匹配填写进来,如果找不到就会报链接错误。但这里可以发现一个问题,就是gVal的符号地址是0x0000,如果直接将解析的地址替换未解决符表中的????就会生成一个冲突的地址0x0000,与本单元的地址重复了。为了解决这个问题,还需要针对每一个编译单元引入的一个地址偏移。例如file1.obj的地址从0x00001000开始,file2.obj的地址从0x00002000开始,这样符号地址叠加后就不会重复了。记录这样的地址偏移量的表称之为地址重定向表(address redirect table)

3 链接器工作顺序

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

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

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

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

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

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

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

 

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

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

posted on 2019-10-12 10:39  VictoKu  阅读(1658)  评论(0编辑  收藏  举报