链接是将各种代码和数据片段收集起来合并成一个文件的过程,这个文件通常是可执行文件,操作系统可以将这个可执行文件加载到内存中运行。
链接可分为静态链接、加载时动态链接和运行时动态链接。链接过程通常由链接器自动执行,无需程序员参与。
有了链接器,就有可能开发大型程序(比如操作系统)。大型程序可以分成n个以上更小更易管理的模块,每个模块单独开发编译。当合适的时候,这些模块通过链接器链接成一个可执行文件。即使将来单个模块发生变化,也不需要重新编译所有模块。只需重新编译更改后的模块,然后重新链接该模块。
程序员为什么要学习链接?有几个好处。
1.知道链接器可以帮助你构建大型程序。程序员在构建大型程序时,经常会遇到缺少模块、缺少库、版本不兼容等问题。了解了链接器的工作流程,就不会迷茫和慌张,反而会从容不迫。
2.'s对链接器的理解可以帮助你理解符号引用的解析过程,这样当你在开发过程中定义多个重复的全局变量时,你会思考这样定义的后果,你的程序会更加健壮。
3.理解链接器,它可以帮助你理解作用域。如果实现了,你会对全局变量和局部变量的区别有更深的理解。
4.了解链接器,它可以帮助你加深对共享库和动态链接的理解,知道为什么以及如何使用它。
本文基于Linuxx86-64操作系统,用C语言编写程序讲解链接的全过程。其他操作系统或架构也有类似的原理和不同的细节。
基于大篇幅,链接分为两篇:静态链接和动态链接。本文将详细阐述静态链接。
main.c
intsum(int*a,intn);externintmultintarray[2]={1,2 };intglobal _ noinitint global _ no init 2=0;int main(){ static intprice=20;staticintlocal _ noinitstatic intlocal _ no init 2=0;intval=sum(array,2)* mult * price;returnval}sum.c
intmult=10intsum(int*a,intn){inti,s=0;for(I=0;在;I){ s=a[I];}退货;}由gcc-Og-oprogmain.csum.c,生成可执行文件prog,结果如下:
root @ localhostclink]# gcc-Og-oprogmain . csum . c[root @ localhostclink]# ls main . cprogsum . c整体翻译过程如下:
用C程序翻译可执行文件的过程
如上图所示,gcc命令执行后,内部其实有很多步骤,整体分为两大步骤。现在让我们模拟这些步骤:
第一步:c程序通过翻译程序翻译成可重定位的目标文件main.o和sum.o。
翻译器会经过3个子步骤:
a. c程序通过预编译处理器(cpp)生成main.i和sum.i文件,命令如下:
CPP main . c/tmp/main . icpsum . c/tmp/sum . I这个子步骤主要是预编译,头文件*。h和宏扩展包含在*中。c文件来形成*。我归档。
上面的cpp命令也可以通过【gcc-emain . c-domain . I】实现。
b.编译器(ccl)将main.i和sum.i文件翻译成汇编文件main.s和sum.s。命令如下:
ccl/tmp/main . I-og-o/tmp/main . sccl/tmp/sum . I-og-o/tmp/sum . s该子步骤将预编译文件编译成汇编文件,汇编文件包含所有汇编代码。
c.汇编程序(as)将main.s和sum.s翻译成main.o和sum.o,命令如下:
as-o/tmp/main . o/tmp/main . SAS-o/tmp/sum . o/tmp/sum . s该子步骤通过汇编器将汇编代码翻译成可重定位的目标文件,目标文件包括机器指令和数据,没有链接。
/p>第二步:将main.o和sum.o通过链接器链接成一个可执行文件prog。
ld-oprog/tmp/main.o/tmp/sum.o
将多个目标文件进行链接,生成一个可执行文件。
上述是为了演示【gcc-Og-oprogmain.csum.c】内部的整个执行过程,实际情况直接用gcc命令即可,不需要单独执行cpp,ccl,as,ld这些命令。
通过静态链接器(上文所述的ld命令)可以将多个可重定位目标文件(*.o)链接成一个完全链接的,可以加载和运行的可执行目标文件(即可执行文件)。
这里可以总结下目标文件这个概念,目标文件包括以下三种类型:
可重定位目标文件(*.o):
可重定位目标文件包括了代码和数据等,这类目标文件中存在【未被链接的符号】(变量或函数),因此可以在链接时,由链接器来确定【未被链接的符号】的地址,这个确定地址的过程就是重定位,下面为可重定位目标文件的格式。
典型的可重定位目标文件
上图为一个可重定位目标文件结构图,可以看出文件由ELF文件头+多个节+节头表组成。
通过ELF文件头可以确定目标文件的类型(可重定位目标文件,可执行目标文件,共享目标文件),文件头的大小,适用的CPU版本,内存布局方式是大端还是小端,节头表的开始位置,节头表的大小,节头表包括的节的个数等。
节头表有多个固定项,每一项描述了节的名字,位置,大小,权限等属性。
ELF文件头和节头表中间的内容就是多个节,每个节的类型和作用不同,下面大致描述下各个节的用途:
.text:包括机器指令。
.rodata:只读数据或者常量数据,比如printf("\d\n",i)中的"\d\n"
.data:已初始化的全局变量和静态变量,局部变量在栈中分配。
.bss:未初始化的全局变量和静态变量,默认值是0,为了减少磁盘空间的占用,在可重定位目标文件中这个节不占用磁盘空间,只在加载到内存时,在内存中分配,初始化值为0。
.symtab:符号表,存放了目标文件中定义和引用的函数符号,全局变量符号,静态变量符号,局部变量不在符号表中。
.rel.text:存储.text节中引用的函数和全局变量的重定位项,每个函数或者全局变量引用分配一个重定位项,当链接器将当前目标文件和其它目标文件链接时,会根据重定位项对引用的地址进行调整即重定位,一般来说,外部函数引用和外部全局变量引用定义在其它目标文件,就会对这些引用进行重定位,对于本地函数引用或者全局变量引用往往不需要修改,对于可执行文件来说不需要.rel.text。
.rel.data:存储.data节中引用的函数和全局变量的重定位项,原理同.rel.text
.debug:调试符号表,定义了局部变量,局部变量类型,全局变量,全局变量类型等。
.line:原始C程序与机器指令的映射关系。
.strtab:一个字符串表,程序中定义的函数名,变量名,debug表中涉及的名称等,都存储在这里。
可执行目标文件:
可执行目标文件又叫可执行文件,这类文件可以直接被操作系统加载到内存中执行。
共享目标文件(*.so):
这是一种特殊类型的可重定位目标文件,这类文件可以在可执行目标文件加载到内存时或者运行过程中被动态链接到内存,共享目标文件中的代码部分在内存中只有一份,其它程序可以共享它的代码部分。
以上就是Linux目标文件的三种类型。
链接的过程主要会执行两个任务:
第一个任务:符号解析
目标文件会定义和引用很多的符号,这些符号有全局符号,外部符号,局部符号三种,举个例子
例子代码【main.c】:
intsum(int*a,intn);externintmult;intarray[2]={1,2};intglobal_noinit;intglobal_noinit2=0;intmain(){staticintprice=20;staticintlocal_noinit;staticintlocal_noinit2=0;intval=sum(array,2)*mult*price;returnval;}
全局符号:
上面的代码中array,main,global_noinit,global_noinit2就是全局符号,全局符号就是在当前目标文件定义,可以被其它目标文件引用,它对应于非静态C函数和全局变量。
外部符号:
外部符号是一种特殊的全局符号,上面的代码中sum,mult就是外部符号,外部符号是在当前目标文件中引用,但是定义在其它目标文件中的非静态C函数和全局变量。
局部符号:
上面的代码中price,local_noinit,local_noinit2就是局部符号,局部符号只能在当前目标文件中定义和引用,对于其它目标文件不可见即其它目标文件不能引用,它对应于带static修饰符的函数和全局变量。
符号解析的目的是对目标文件中的每个符号引用都能找到它的定义,要想确定一个符号引用对应的定义,需要用到符号表,每个可重定位目标文件都有一个符号表,如下图所示
符号表
上图通过【readelf-smain.o】命令可以列出可重定位目标文件【main.o】中的符号表,可以看出【main.o】中总共有17个符号
先来看看符号的一些重要属性:
Size:符号占用的空间大小,如果这个符号不占用空间的话,就为0,通常来说变量和函数符号是需要占用空间的,例如一个整型变量占用4个字节。
Type:符号的类型,NOTYPE表示这个符号不能确定它的类型,通常这个符号定义在其它的目标文件中,OBJECT表示这个符号用于存储数据,例如变量,FUNC表示这个符号是一个函数或者可执行的代码片段,其它的类型不再阐述,与本篇文章无关。
Bind:符号的绑定类型,LOCAL表示局部符号,通常来说一个带有static修饰符的变量或者函数就是局部符号,上文已经阐述过,不再重复阐述,GLOBAL表示全局符号,全局符号可以被当前目标文件和其它目标文件引用。
Ndx:表示当前符号被目标文件中的哪个节引用了,它是节表的索引,通过这个索引可以在节表中找到相应的节,举个例子,如上图符号表中的第14个符号即main函数,它的Ndx等于1,表示main这个符号被节表中索引为1的节引用了,如下图所示,通过readelf-Smain.o查看节表。
节表
如上图所示,节表中【Nr】列就是索引列,查找索引1就可以知道节为【.text】,这个是代码节,main函数这个符号就被代码节引用了。
另外,Ndx还有3个伪节【UND,ABS,COM】,这3个伪节在节表中不存在,它们有特殊的含义,重点介绍下UND和COM。
UND表示当前符号在当前目标文件没有定义,说明这个符号很可能在其它目标文件中定义。
COM表示这是一个全局的未初始化的变量,通常来说全局和静态已初始化的变量(非0值)在.data节中,全局的未初始化的变量在符号表中并且它的Ndx等于COM,其它的静态的未初始化变量,静态的已经初始化变量但是值为0,全局的已经初始化变量但是值为0则在.bss节中,之所以这么设计有它的原因,后面再揭晓。
好了,符号表的符号属性就介绍到这里,我们重点关注【5,6,7,11,12,13,14,15,16】这几个符号,如下图
符号表
依据上面符号表属性的介绍,可以看出
【price.1730,local_noinit2.1732,local_noinit.1731】这几个变量的BIND都是LOCAL,说明它们是局部符号,Type都是OBJECT,说明它们都是变量,【price.1730】变量的Ndx等于3,说明这个符号被.data节引用,【local_noinit2.1732,local_noinit.1731】变量的Ndx等于4,这两个符号被.bss节引用。
【array,global_noinit,global_noinit2】这几个变量BIND都是GLOBAL说明它们都是全局符号,Type都是OBJECT,说明它们都是变量,【array】变量的Ndx等于3,说明这个符号被.data节引用,【global_noinit】变量的Ndx等于COM,说明它是一个全局未初始化变量,COM是个伪节,在节表中是找不到这个节的,因此【global_noinit】只存在于符号表中,【global_noinit2】变量的Ndx等于4,说明这个符号被.bss节引用,这个符号虽然是全局已经初始化的变量,但是它的值是0,因此它被划分到了.bss节。
【main,sum,mult】这几个变量BIND都是GLOBAL说明它们都是全局符号,【main】的Type是FUNC,说明这个符号是个函数,Ndx等于1,说明这个符号被.text节引用,【sum,mult】的Type是NOTYPE,说明这个符号的类型无法确定,Ndx等于UND,说明这个符号没有在当前目标文件中定义,它可能定义在其它目标文件中。
好了,符号表的介绍就到这里了,下面来看看符号解析是怎么实现的。
每个可重定位目标文件都有一个符号表,当一个目标文件遇到的符号引用时,首先去当前目标文件的符号表中查找,对于局部符号来说比较简单,编译器确保每个局部符号都有一个唯一的名字,另外它总是能在当前目标文件的符号表中找到,对于全局符号来说就比较麻烦了。
全局符号解析会遇到两个问题,一个是全局符号的定义不在当前目标文件中,另外一个是多个目标文件中的全局符号可能会重名即重复定义。
对于第一个问题来说,链接器会去其它的目标文件的符号表查找符号的定义,如果所有的目标文件都没有这个符号的定义,那么链接器就会报一个链接错误,举个例子看看
linkerror.c
voidfoo(void);intmain(){foo();return0;}
经过链接后报出如下错误
[root@localhostlink]#gcc-Wall-Og-olinkerrorlinkerror.c/tmp/ccVX5cbO.o:Infunction`main':linkerror.c:(.text+0x5):undefinedreferenceto`foo'collect2:error:ldreturned1exitstatus
对于第二个问题来说,会复杂些,先来简单介绍下强符号和弱符号,强符号是全局的已经初始化的变量如main.c中的array变量,弱符号是全局的未初始化的变量如main.c中的global_noinit。
再来看看解决第二个问题的几个原则:
a.链接过程中涉及的所有目标文件,不允许有同名的强符号,如果有,则报链接错误,举个例子:
foo1.c
intmain(){return0;}
bar1.c
intmain(){return0;}
经过链接后,报出如下错误:
[root@localhostlink]#gccfoo1.cbar1.c/tmp/cck3dvgz.o:Infunction`main':bar1.c:(.text+0x0):multipledefinitionof`main'/tmp/cc0GOpm3.o:foo1.c:(.text+0x0):firstdefinedherecollect2:error:ldreturned1exitstatus
可以看出,main这个强符号重复定义了。
b.链接过程中涉及的所有目标文件,如果有一个强符号与其它的弱符号同名,则使用这个强符号的定义,举个例子
foo2.c
#includevoidf(void);intx=15213;intmain(){f();printf("x=%d\n",x);return0;}
bar2.c
intx;voidf(){x=15212;}
经过链接后,结果如下
[root@localhostlink]#gcc-ofoobar2foo2.cbar2.c[root@localhostlink]#./foobar2x=15212
可以看出,foo2.c中定义的x是强符号,bar2定义的x是弱符号,因此根据规则b,选择了foo2.c中的x作为x的定义,因此f函数执行时,将15212赋值给了强符号x,后续打印的时候发现x被改了,不再是15213。
c.链接过程中涉及的所有目标文件,如果有多个同种类型并且同名的弱符号,则从中任意选择一个弱符号,如果符号同名但类型不同,则选择一个占用空间较大的符号,举个例子
foo3.c
#includevoidf(void);intx;intmain(){x=15213;f();printf("x=%d\n",x);return0;}
bar3.c
intx;voidf(){x=15212;}
经过链接后,结果如下:
[root@localhostlink]#gcc-ofoobar3foo3.cbar3.c[root@localhostlink]#./foobar3x=15212
foo3.c和bar3.c中的x都是弱符号,因此规则c,从这两个弱符号中随机选择一个。
符号解析的过程主要是处理这两个问题的过程,当然正常情况还是尽量避免遇到这两个问题,尤其是第二个问题,如果遇到全局符号重复定义的问题,如果不了解链接的过程,往往出现很多让人头大和莫名其妙的问题。
上文说过符号表中的Ndx有个COM的伪节,这个伪节的作用在这里可以揭晓了,当前目标文件中遇到了一个弱符号引用时,它不知道其它目标文件中是否也有同名的弱符号,因此无法确定符号占用的空间大小,不能存储在.bss节,链接器可以查找其它目标文件的符号表,通过符号表找到同名的符号项,通过这个符号项的Ndx的值,就可以知道这个符号是不是弱符号,这样链接器就可以把所有目标文件的弱符号都筛选出来,然后根据第二个问题的规则c,选择出一个弱符号定义,此时符号定义明确了,它占用的空间就确定了,这个时候再放在.bss节中。
第二个任务:重定位:
编译器和汇编器生成的代码节和数据节的地址是从0开始的,这个地址与内存无关,当链接器进行链接时即执行第一个任务后,会给代码节和数据节分配虚拟内存地址,因此代码节和数据节中的变量和函数的地址需要重新定位,链接器就会根据重定位表中重定位项,将重定位表中的每个变量和函数的地址调整为正确的虚拟内存地址。
重定位发生在符号解析完成后,经过符号解析后,每一个符号引用都有一个明确的符号定义,这样就可以正式开始重定位操作了。
重定位的操作分类两步:
第一步:将链接的所有可重定位目标文件进行合并,合并的原则就是相同类型的节进行合并,例如所有的.data节合并成一个新的.data节,所有的节合并后,链接器给每个合并的新节赋予一个虚拟内存地址以及新节中的每个符号都赋予一个虚拟内存地址,这样每条指令和每个变量都有了唯一的运行时虚拟内存地址了。
第二步:链接器修改代码节和数据节中每个引用的符号的地址,将符号引用调整为正确的运行时地址,要完成这一步就需要用到了重定位表的重定位项了,我们来看看重定位表。
重定位表包括多个重定位项,每个重定位选项包括offset,type,symbol,addend4个属性。
offset:一个节中引用的符号的偏移量即引用的符号的开始位置到节的开始位置之差,这个值是固定的,与内存无关的。
type:重定位的类型,包括32种之多,常用的也是我们重点关注的就两种即R_X86_64_PC32和R_X86_64_32
R_X86_64_PC32表示重定位后的地址是一个相对PC寄存器的偏移值,PC寄存器存储的下一条指令的地址,我们假设偏移值为O,下一条指令的地址为P,我们计算实际的地址就是P+O,所以R_X86_64_PC32也叫做相对地址。
R_X86_64_32表示重定位后的地址就是符号链接时被分配的虚拟内存地址,所以R_X86_64_32也叫做绝地地址。
symbol:符号表的索引,通过这个索引可以从符号表中找到相应的符号,符号表在链接时,每个符号都赋予了虚拟内存地址。
addend:对重定位后的地址进行修正,不同的重定位类型,这个值不同。
通过【objdump-dxmain.o】可以查看main.o目标文件中的所有可重定位项,如下面红色字体部分:
0:55push%rbp
1:4889e5mov%rsp,%rbp
4:4883ec10sub$0x10,%rsp
8:be02000000mov$0x2,%esi
d:bf00000000mov$0x0,%edi
e:R_X86_64_32array
12:e800000000callq17
13:R_X86_64_PC32sum-0x4
17:8b1500000000mov0x0(%rip),%edx#1d
19:R_X86_64_PC32mult-0x4
1d:0fafd0imul%eax,%edx
20:8b0500000000mov0x0(%rip),%eax#26
22:R_X86_64_PC32.data+0x4
26:0fafc2imul%edx,%eax
29:8945fcmov%eax,-0x4(%rbp)
2c:8b45fcmov-0x4(%rbp),%eax
2f:c9leaveq
30:c3retq
上面的黑色字体部分为代码指令,每一行代码一条指令,红色字体部分为汇编器插入的重定位项,重定向项用于修正前一行的指令中引用的符号。
另外每一行都有一个序号从0,1,4,8.....30,这个序号不是虚拟内存地址,而是一个由汇编器分配的从0开始的数字,由于每一行指令大小不同,因此序号都是按照指令的大小递增的,我们把重定位项摘出来,如下面所示:
e:R_X86_64_32array
13:R_X86_64_PC32sum-0x4
19:R_X86_64_PC32mult-0x4
22:R_X86_64_PC32.data+0x4
如上面所示,总共有4个重定位项
第一个重定位项【e:R_X86_64_32array】,从中分析出offset=e,type=R_X86_64_32,symbol定位到符号是array,addend=0。
第二个重定位项【13:R_X86_64_PC32sum-0x4】,从中分析出offset=13,type=R_X86_64_PC32,symbol定位到符号就是sum,addend=-0x4。
第三个重定位项【19:R_X86_64_PC32mult-0x4】,从中分析出offset=19,type=R_X86_64_PC32,symbol定位到符号就是mult,addend=-0x4。
第四个重定位项【22:R_X86_64_PC32.data+0x4】,从中分析出offset=22,type=R_X86_64_PC32,symbol定位到符号就是数据节.data,addend=0x4。
那么我们怎么根据重定位项计算重定位后的地址呢?
对于重定向类型为R_X86_64_32来说,符号引用的重定位地址就等于链接时给符号分配的虚拟地址。
举个例子,如上面的【e:R_X86_64_32array】,假如array被分配的虚拟地址是【0x60102c】那么重定位后地址就是【0x60102c】,那么array符号引用就被替换为这个地址,下面为修正前和修正后的对比
修正前(main.o)
d(序号):bf00000000mov$0x0,%edi
上面红色字体部分是符号引用的偏移量即offset=e,从这个位置开始设置重定位后的地址。
修正后(可执行文件)
4004f9(虚拟地址):bf2c106000mov$0x60102c,%edi
上面红色字体部分为被修改后的指令,序号也变成了虚拟内存地址。
对于重定向类型为R_X86_64_PC32来说,符号引用的重定位地址按照如下算法进行计算:
链接时符号分配的虚拟地址+addend-(符号引用所在节分配的虚拟地址+offset即符号引用的虚拟地址)
举个例子:如上面的重定向项【13:R_X86_64_PC32sum-0x4】
假如链接时符号sum分配的虚拟地址是:0x400518,符号引用所在节分配的虚拟地址是:4004f0,addend=-0x4,offset=13。
重定位地址=0x400518-0x4-(0x4004f0+13)=15,因此修正指令为
修正前(main.o):
12:e800000000callq17
上面红色字体为符号引用的偏移量offset=13,从这个位置开始重定位地址调整为15
修正后(可执行文件)
4004fe:e815000000callq400518
当CPU执行地址4004fe的指令时,这个时候PC寄存器值为4004fe+4=400503,将PC寄存器的值加上15就是实际调用的地址400503+15=400518,这个地址正好是sum函数的地址,下图为可执行文件中sum和main函数的指令分布图
先来看看以下代码
mainlib.c
#include#include"vector.h"intx[2]={1,2};inty[2]={3,4};intz[2];intmain(){addvec(x,y,z,2);printf("z=[%d,%d]\n",z[0],z[1]);return0;}
vector.h
voidaddvec(int*x,int*y,int*z,intn);voidmultvec(int*x,int*y,int*z,intn);
在mainlib.c中引用了addvec这个函数,这个函数的声明在vector.h,而这个函数的定义在addvec.c程序中,如下所示代码
intaddcnt=0;voidaddvec(int*x,int*y,int*z,intn){inti;addcnt++;for(i=0;i
因此要生成一个可执行文件,就需要执行以下命令
【gccmainlib.caddvec.o】这样会生成一个可执行文件
如果minlib.c中又引用了multvec函数,那么就需要执行以下命令
【gccmainlib.caddvec.omultvec.o】这样会生成一个可执行文件
以上是引用了两个目标文件,实际开发环境中,会涉及大量的函数引用,那么引用的列表就会无限扩大,这样既麻烦又容易出错。
那如果把引用的函数放在一个文件中,例如下面的代码
allvec.c
intaddcnt=0;voidaddvec(int*x,int*y,int*z,intn){inti;addcnt++;for(i=0;i
当目标文件要引用这两个函数时,就需要执行以下命令
【gccmainlib.callvec.o】
这样如果有新的函数需要引用,就加入到addvec中,这样生成可执行文件的命令不会变化,一直引用的是allvec.o。
这种方式好吗?
可以说弊端也不少,有以下几个弊端:
1.链接时,mainlib.c将allvec.o中所有的函数都链接到可执行文件中,即使mainlib.c只用到了部分函数,这样会造成不必要的引用,增加可执行文件占用的磁盘空间和内存中使用空间。
2.如果allvec.c中的任意一个函数发生了变化,整个allvec.c中的所有函数都要重新编译,如果allvec.c比较大的话,编译起来也是比较慢的,再说其它的函数也没有变化,编译纯属浪费时间。
因此为了减少链接时引用的目标文件列表,也是为了只链接用到的函数,静态库诞生了。
静态库中每个函数是一个目标文件,多个目标文件打包成一个静态库(*.a),静态库可以作为链接时的引用列表,它只链接用到的函数,没有用到的函数不链接到可执行文件中,这样就解决上述的几个问题。
怎么生成一个静态库呢?可以执行以下命令:
【arrcsallvector.aaddvec.omultvec.o】
上面的命令生成了allvector.a这个静态库。
有了静态库,生成可执行文件时就可以执行以下命令
【gcc-static-omainlibexemainlib.oallvector.a】
上面的命令就可以链接mainlib.o目标文件和allvector.a中的addvec.o,生成可执行文件。
那么,链接器是怎么利用静态库来实现只链接用到的函数呢,这个是由Linux链接器独特的实现方式决定的,首先,这个发生在链接的符号解析阶段,链接器从左到右扫描所有的可重定位目标文件和静态库文件,对所有扫描到的文件执行以下操作:
先假设3个集合:E,U,D。E为可重定位目标文件,U为没有解析的符号集合(在目标文件中引用,但尚未找到定义),D为已经定义的符号集合,刚开始E,U,D都没空,然后开始以下规则:
a.当前解析的文件是可重定位目标文件时,则将这个文件加入到集合E中,然后将文件中的没有解析的符号加入到集合U,将已定义的符号加入到D,如果U中没有解析的符号,在文件中找到了,则从U中删除这个没有解析的符号。
b.当前解析的文件是静态库文件时,遍历静态库中每个可重定位目标文件按照a规则进行处理,当U和D不再发生变化时,遍历结束,此时检查静态库的每个可重定位目标文件,如果检查的文件不再集合E中,则这个文件就被抛弃掉。
按照以上的规则,所有扫描到的文件都执行完成后,如果U是非空的,那就证明有未解析的符号,这个时候链接器就报错,如果U是空的,那么E集合中的所有可重定位文件参与链接,从而生成可执行文件。
了解了静态库的链接规则后,假设一个C程序foo.c依赖了libx.a和liby.a,而libx.a和lib.y又依赖了libz.a,那么gcc的链接命令将按照如下顺序进行链接
【gccfoo.clibx.aliby.alibz.a】可以看出静态库在列表后面,静态库之间按照拓扑依赖顺序排列。
上一篇:杭州哪里可以放烟花?
下一篇:博士后(博士)是什么意思