静态空间(免费静态空间)

链接是将各个代码和数据片段收集起来合并成一个文件的过程,这个文件通常是一个可执行文件,操作系统可以将这个可执行文件加载到内存中运行。

链接分为静态链接,加载时动态链接,运行时动态链接,链接过程通常由链接器(linker)自动执行,不需要程序员参与。

有了链接器,开发大型的程序(例如操作系统)有了可能,大型的程序可以拆分成N多个小的,更好管理的模块,每个模块各自开发和编译,等到合适的时机通过链接器将这些模块链接成一个可执行文件,以后即使单个模块发生了变化,也不需要所有的模块都重新编译,只需将发生变化的模块重新编译,然后重新链接该模块就可以了。

程序员为什么要学习链接呢,有以下几点好处?

1.理解了链接器,可以帮助你构建大型程序,程序员在构建大型程序时,会经常遇到缺少模块,缺少库,版本不兼容的问题,如果你理解了链接器的工作过程,你不会迷茫慌张,会从容应对。

2.理解了链接器,可以帮助你理解符号引用的解析过程,这样你在开发过程中定义多个重复的全局变量时,会好好想想这样的定义会有什么样的后果,你的程序会更加的健壮。

3.理解了链接器,可以帮助你理解作用域是如果实现的,你会加深理解全局变量和局部变量的区别是什么。

4.理解了链接器,可以帮助你加深对共享库和动态链接的理解,知道为什么使用和怎么使用。

本篇文章基于Linux x86-64操作系统,采用C语言编写程序来阐述链接的整个过程,其它操作系统或者体系结构,原理比较类似,细节各有不同。

基于篇幅比较大,将链接拆分成静态链接和动态链接两篇文章,本篇文章将阐述静态链接。

程序转化为可执行文件的过程

在阐述静态链接前,先介绍下C程序转化为可执行文件的过程,假设有两个C文件main.c和sum.c,如下面所示

main.c

int sum(int*a, int n);extern int mult;int array[2] = {1,2};int global_noinit;int global_noinit2=0;int main(){ static int price = 20; static int local_noinit; static int local_noinit2=0; int val = sum(array,2) * mult * price; return val;}

sum.c

int mult = 10;int sum(int*a, int n){ int i, s = 0; for (i = 0; i < n; i++) { s += a[i]; } return s;}

通过【gcc -Og -o prog main.c sum.c】命令生成可执行文件prog,结果如下:

root@localhost clink]# gcc -Og -o prog main.c sum.c

[root@localhost clink]# ls

main.c prog sum.c

整体翻译过程如下:

静态空间(免费静态空间)

C程序翻译可执行文件的过程

如上图所示,执行gcc命令后,内部其实执行了很多步骤,整体来说分为两大步骤,现在来模拟下这些步骤:

第一步:C程序通过翻译器翻译为可重定位目标文件main.o和sum.o。

翻译器会经过3个子步骤:

a.C程序通过预编译处理器(cpp)生成main.i和sum.i文件,命令如下:

cpp main.c /tmp/main.icpp sum.c /tmp/sum.i

这个子步骤主要进行预编译,将头文件*.h和宏展开包含到*.c文件中,形成了*.i文件。

上述的cpp命令也可以通过【gcc -E main.c -o main.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

这个子步骤通过汇编器将汇编代码翻译成可重定位目标文件,目标文件中包括了没有链接的机器指令和数据等。

第二步:将main.o和sum.o通过链接器链接成一个可执行文件prog。

ld -o prog /tmp/main.o /tmp/sum.o

将多个目标文件进行链接,生成一个可执行文件。

上述是为了演示【gcc -Og -o prog main.c sum.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】:

int sum(int*a, int n);extern int mult;int array[2] = {1,2};int global_noinit;int global_noinit2=0;int main(){ static int price = 20; static int local_noinit; static int local_noinit2=0; int val = sum(array,2) * mult * price; return val;}

全局符号:

上面的代码中array,main,global_noinit,global_noinit2就是全局符号,全局符号就是在当前目标文件定义,可以被其它目标文件引用,它对应于非静态C函数和全局变量。

外部符号:

外部符号是一种特殊的全局符号,上面的代码中sum,mult就是外部符号,外部符号是在当前目标文件中引用,但是定义在其它目标文件中的非静态C函数和全局变量。

局部符号:

上面的代码中price,local_noinit,local_noinit2就是局部符号,局部符号只能在当前目标文件中定义和引用,对于其它目标文件不可见即其它目标文件不能引用,它对应于带static修饰符的函数和全局变量。

符号解析的目的是对目标文件中的每个符号引用都能找到它的定义,要想确定一个符号引用对应的定义,需要用到符号表,每个可重定位目标文件都有一个符号表,如下图所示

静态空间(免费静态空间)

符号表

上图通过【readelf -s main.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 -S main.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

void foo(void);int main() { foo(); return 0;}

经过链接后报出如下错误

[root@localhost link]# gcc -Wall -Og -o linkerror linkerror.c/tmp/ccVX5cbO.o: In function `main':linkerror.c:(.text+0x5): undefined reference to `foo'collect2: error: ld returned 1 exit status

对于第二个问题来说,会复杂些,先来简单介绍下强符号和弱符号,强符号是全局的已经初始化的变量如main.c中的array变量,弱符号是全局的未初始化的变量如main.c中的global_noinit。

再来看看解决第二个问题的几个原则:

a.链接过程中涉及的所有目标文件,不允许有同名的强符号,如果有,则报链接错误,举个例子:

foo1.c

int main() { return 0;}

bar1.c

int main() { return 0;}

经过链接后,报出如下错误:

[root@localhost link]# gcc foo1.c bar1.c/tmp/cck3dvgz.o: In function `main':bar1.c:(.text+0x0): multiple definition of `main'/tmp/cc0GOpm3.o:foo1.c:(.text+0x0): first defined herecollect2: error: ld returned 1 exit status

可以看出,main这个强符号重复定义了。

b.链接过程中涉及的所有目标文件,如果有一个强符号与其它的弱符号同名,则使用这个强符号的定义,举个例子

foo2.c

#include<stdio.h>void f(void);int x = 15213;int main() { f(); printf("x = %d\\n", x); return 0;}

bar2.c

int x ;void f(){ x=15212;}

经过链接后,结果如下

[root@localhost link]# gcc -o foobar2 foo2.c bar2.c[root@localhost link]# ./foobar2x = 15212

可以看出,foo2.c中定义的x是强符号,bar2定义的x是弱符号,因此根据规则b,选择了foo2.c中的x作为x的定义,因此f函数执行时,将15212赋值给了强符号x,后续打印的时候发现x被改了,不再是15213。

c.链接过程中涉及的所有目标文件,如果有多个同种类型并且同名的弱符号,则从中任意选择一个弱符号,如果符号同名但类型不同,则选择一个占用空间较大的符号,举个例子

foo3.c

#include<stdio.h>void f(void);int x;int main() { x=15213; f(); printf("x = %d\\n", x); return 0;}

bar3.c

int x ;void f(){ x=15212;}

经过链接后,结果如下:

[root@localhost link]# gcc -o foobar3 foo3.c bar3.c[root@localhost link]# ./foobar3x = 15212

foo3.c和bar3.c中的x都是弱符号,因此规则c,从这两个弱符号中随机选择一个。

符号解析的过程主要是处理这两个问题的过程,当然正常情况还是尽量避免遇到这两个问题,尤其是第二个问题,如果遇到全局符号重复定义的问题,如果不了解链接的过程,往往出现很多让人头大和莫名其妙的问题。

上文说过符号表中的Ndx有个COM的伪节,这个伪节的作用在这里可以揭晓了,当前目标文件中遇到了一个弱符号引用时,它不知道其它目标文件中是否也有同名的弱符号,因此无法确定符号占用的空间大小,不能存储在.bss节,链接器可以查找其它目标文件的符号表,通过符号表找到同名的符号项,通过这个符号项的Ndx的值,就可以知道这个符号是不是弱符号,这样链接器就可以把所有目标文件的弱符号都筛选出来,然后根据第二个问题的规则c,选择出一个弱符号定义,此时符号定义明确了,它占用的空间就确定了,这个时候再放在.bss节中。

第二个任务:重定位:

编译器和汇编器生成的代码节和数据节的地址是从0开始的,这个地址与内存无关,当链接器进行链接时即执行第一个任务后,会给代码节和数据节分配虚拟内存地址,因此代码节和数据节中的变量和函数的地址需要重新定位,链接器就会根据重定位表中重定位项,将重定位表中的每个变量和函数的地址调整为正确的虚拟内存地址。

重定位发生在符号解析完成后,经过符号解析后,每一个符号引用都有一个明确的符号定义,这样就可以正式开始重定位操作了。

重定位的操作分类两步:

第一步:将链接的所有可重定位目标文件进行合并,合并的原则就是相同类型的节进行合并,例如所有的.data节合并成一个新的.data节,所有的节合并后,链接器给每个合并的新节赋予一个虚拟内存地址以及新节中的每个符号都赋予一个虚拟内存地址,这样每条指令和每个变量都有了唯一的运行时虚拟内存地址了。

第二步:链接器修改代码节和数据节中每个引用的符号的地址,将符号引用调整为正确的运行时地址,要完成这一步就需要用到了重定位表的重定位项了,我们来看看重定位表。

重定位表包括多个重定位项,每个重定位选项包括offset,type,symbol,addend 4个属性。

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 -dx main.o】可以查看main.o目标文件中的所有可重定位项,如下面红色字体部分:

0: 55 push %rbp

1: 48 89 e5 mov %rsp,%rbp

4: 48 83 ec 10 sub $0x10,%rsp

8: be 02 00 00 00 mov $0x2,%esi

d: bf 00 00 00 00 mov $0x0,%edi

e: R_X86_64_32 array

12: e8 00 00 00 00 callq 17 <main+0x17>

13: R_X86_64_PC32 sum-0x4

17: 8b 15 00 00 00 00 mov 0x0(%rip),%edx # 1d <main+0x1d>

19: R_X86_64_PC32 mult-0x4

1d: 0f af d0 imul %eax,%edx

20: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 26 <main+0x26>

22: R_X86_64_PC32 .data+0x4

26: 0f af c2 imul %edx,%eax

29: 89 45 fc mov %eax,-0x4(%rbp)

2c: 8b 45 fc mov -0x4(%rbp),%eax

2f: c9 leaveq

30: c3 retq

上面的黑色字体部分为代码指令,每一行代码一条指令,红色字体部分为汇编器插入的重定位项,重定向项用于修正前一行的指令中引用的符号。

另外每一行都有一个序号从0,1,4,8…..30,这个序号不是虚拟内存地址,而是一个由汇编器分配的从0开始的数字,由于每一行指令大小不同,因此序号都是按照指令的大小递增的,我们把重定位项摘出来,如下面所示:

e: R_X86_64_32 array

13: R_X86_64_PC32 sum-0x4

19: R_X86_64_PC32 mult-0x4

22: R_X86_64_PC32 .data+0x4

如上面所示,总共有4个重定位项

第一个重定位项【e: R_X86_64_32 array】,从中分析出offset=e,type=R_X86_64_32,symbol定位到符号是array,addend=0。

第二个重定位项【13:R_X86_64_PC32 sum-0x4】,从中分析出offset=13,type=R_X86_64_PC32,symbol定位到符号就是sum,addend=-0x4。

第三个重定位项【19:R_X86_64_PC32 mult-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_32 array】,假如array被分配的虚拟地址是【0x60102c】那么重定位后地址就是【0x60102c】,那么array符号引用就被替换为这个地址,下面为修正前和修正后的对比

修正前(main.o)

d(序号): bf 00 00 00 00 mov $0x0,%edi

上面红色字体部分是符号引用的偏移量即offset=e,从这个位置开始设置重定位后的地址。

修正后(可执行文件)

4004f9(虚拟地址):bf 2c 10 60 00 mov $0x60102c,%edi

上面红色字体部分为被修改后的指令,序号也变成了虚拟内存地址。

对于重定向类型为R_X86_64_PC32 来说,符号引用的重定位地址按照如下算法进行计算:

链接时符号分配的虚拟地址+addend-(符号引用所在节分配的虚拟地址+offset即符号引用的虚拟地址)

举个例子:如上面的重定向项【13:R_X86_64_PC32 sum-0x4】

假如链接时符号sum分配的虚拟地址是:0x400518,符号引用所在节分配的虚拟地址是:4004f0,addend=-0x4,offset=13。

重定位地址=0x400518-0x4-(0x4004f0+13)=15,因此修正指令为

修正前(main.o):

12: e8 00 00 00 00 callq 17 <main+0x17>

上面红色字体为符号引用的偏移量offset=13,从这个位置开始重定位地址调整为15

修正后(可执行文件)

4004fe: e8 15 00 00 00 callq 400518 <sum>

当CPU执行地址4004fe的指令时,这个时候PC寄存器值为4004fe+4=400503,将PC寄存器的值加上15就是实际调用的地址400503+15=400518,这个地址正好是sum函数的地址,下图为可执行文件中sum和main函数的指令分布图

静态空间(免费静态空间)

静态库

先来看看以下代码

mainlib.c

#include <stdio.h>#include "vector.h"int x[2] = {1,2};int y[2] = {3,4};int z[2];int main(){ addvec(x,y,z,2); printf("z = [%d,%d]\\n", z[0], z[1]); return 0;}

vector.h

void addvec(int *x,int *y,int *z, int n);void multvec(int *x,int *y,int *z, int n);

在mainlib.c中引用了addvec这个函数,这个函数的声明在vector.h,而这个函数的定义在addvec.c程序中,如下所示代码

int addcnt = 0;void addvec(int *x,int *y,int *z, int n){ int i; addcnt++; for(i = 0; i < n; i++){ z[i] = x[i] + y[i]; }}

因此要生成一个可执行文件,就需要执行以下命令

【gcc mainlib.c addvec.o】这样会生成一个可执行文件

如果minlib.c中又引用了multvec函数,那么就需要执行以下命令

【gcc mainlib.c addvec.o multvec.o】这样会生成一个可执行文件

以上是引用了两个目标文件,实际开发环境中,会涉及大量的函数引用,那么引用的列表就会无限扩大,这样既麻烦又容易出错。

那如果把引用的函数放在一个文件中,例如下面的代码

allvec.c

int addcnt = 0;void addvec(int *x,int *y,int *z, int n){ int i; addcnt++; for(i = 0; i < n; i++){ z[i] = x[i] + y[i]; }}int multcnt = 0;void multvec(int *x,int *y,int *z, int n){ int i; multcnt++; for(i = 0; i < n; i++){ z[i] = x[i] * y[i]; }}

当目标文件要引用这两个函数时,就需要执行以下命令

【gcc mainlib.c allvec.o】

这样如果有新的函数需要引用,就加入到addvec中,这样生成可执行文件的命令不会变化,一直引用的是allvec.o。

这种方式好吗?

可以说弊端也不少,有以下几个弊端:

1.链接时,mainlib.c将allvec.o中所有的函数都链接到可执行文件中,即使mainlib.c只用到了部分函数,这样会造成不必要的引用,增加可执行文件占用的磁盘空间和内存中使用空间。

2.如果allvec.c中的任意一个函数发生了变化,整个allvec.c中的所有函数都要重新编译,如果allvec.c比较大的话,编译起来也是比较慢的,再说其它的函数也没有变化,编译纯属浪费时间。

因此为了减少链接时引用的目标文件列表,也是为了只链接用到的函数,静态库诞生了。

静态库中每个函数是一个目标文件,多个目标文件打包成一个静态库(*.a),静态库可以作为链接时的引用列表,它只链接用到的函数,没有用到的函数不链接到可执行文件中,这样就解决上述的几个问题。

怎么生成一个静态库呢?可以执行以下命令:

【ar rcs allvector.a addvec.o multvec.o】

上面的命令生成了allvector.a这个静态库。

有了静态库,生成可执行文件时就可以执行以下命令

【gcc -static -o mainlibexe mainlib.o allvector.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的链接命令将按照如下顺序进行链接

【gcc foo.c libx.a liby.a libz.a】可以看出静态库在列表后面,静态库之间按照拓扑依赖顺序排列。

(0)
海淘直接的头像海淘直接注册用户

相关推荐

  • 黎姓女孩名字-2024年1月1日出生怎么起名寓意健康平安

    黎姓女孩名字2024年1月1日出生的生辰八字五行查询公历:2024年1月1日(星期一)农历:2024年1月1日八字:癸卯甲子甲子五行:水木木水木水方位:北东东北东北五行缺什么:金火土生肖:兔五行分析:五行【木

    2023年12月26日
  • 实习个人鉴定

    各位网友们好,相信很多人对实习个人鉴定都不是特别的了解,因此呢,今天就来为大家分享下关于实习个人鉴定以及的问题知识,还望可以帮助大家,解决大家的一些困惑,下面一起来看看吧!

    2023年6月8日
  • 网页游戏能开挂吗

    亲爱的玩家:您好,是一款辅助软件,这个是虚源在官方禁止使用的哦,建议您是不要使用的,现在流返岩传的一些第三方辅助软件都是不稳定的,都会自动操作玩家的游戏,令玩家的游戏数据异常,导致物品消失的哦,为了您的游戏正常运行,也会有很多辅助软件下

    2023年11月18日
  • psv10003g版和wifi版有啥区别,哪个更值得购买

    如果你正在考虑购买PSV游戏机,你可能会被PSV10003G版和WiFi版之间的选择所困扰。这两个版本的主要区别是什么?哪个更值得购买?在本文中,我们将详细介绍这两个版本之间的区别,以便您可以做出明智的购买决策。外观和设计首先,让我们看

    2023年12月1日
  • 一生必去的新疆旅游攻略(旅游攻略 新疆)

    本文内容概括:1、新疆必去的十大景点2、新疆旅游攻略:必玩景点推荐3、新疆最佳旅游路线及攻略4、新疆旅游必去的十大景点推荐5、新疆旅游攻略:精选几日游路线新疆必去的十大景点新疆必去的十大景点分别是:喀纳斯、赛里木湖、喀什老城、天山天池、那拉提旅游风景区、白沙湖、葡萄沟、火焰山、可可托海景区

    2023年12月22日
  • 拼多多的AB面

    又是一年财报季,不少企业纷纷发布了自己的三季报,要说*的惊喜,无疑是拼多多。今年的三季报发完,可以说是见证了历史:市值1850亿美金,直逼阿里1950亿美金。具体数据来看:拼多多集团今年第三季度收入为688.4亿元,同比增长 93.9%,归属于拼多多普通股股东的净利润为155.371亿元,同比增长4

    2023年11月30日
  • 纪芷颜陆瑾州的小说 纪芷颜陆瑾州全文免费阅读

    纪芷颜陆瑾州是著名作者佚名小说作品里面的男女主角,这本小说以巧思支撑的短篇小说,内容很是有趣,简练生动,极富韵味。一起来看看小说简介吧!天黑陆瑾州不知道自己怎么参加完纪芷颜的葬礼,也不记得自己怎么回的家,回过神时,他才发现自己站在漆黑的客厅里。浅吸口气,空气中仿佛还残存着一丝属于她的药草淡香。打开灯,一切都没变,可好像一切都变了……“瑾州。”陆瑾州愣了愣,转身望去,见是司令,本能地敬了个礼。

    美文 2023年6月13日
  • present的用法总结大全(present用法总结)

    各位网友们好,相信很多人对present的用法总结大全都不是特别的了解,因此呢,今天就来为大家分享下关于present的用法总结大全以及present用法总结的问题知识,还望可以帮助大家,解决大家的一些困惑,下面一起来看看吧!

    2023年8月15日
  • 误解(误解作文600字初中)

    各位网友们好,相信很多人对误解都不是特别的了解,因此呢,今天就来为大家分享下关于误解以及误解作文600字初中的问题知识,还望可以帮助大家,解决大家的一些困惑,下面一起来看看吧!

    2023年6月2日
  • 最后一百天(《最后一百天》两章)

    各位网友们好,相信很多人对最后一百天都不是特别的了解,因此呢,今天就来为大家分享下关于最后一百天以及《最后一百天》两章的问题知识,还望可以帮助大家,解决大家的一些困惑,下面一起来看看吧!

    2023年8月26日
  • 福州的特产有哪些

    福州特产有寿山石、脱胎漆器、连江鲍鱼、福州橄榄、福桔、软木画、福州茉莉花茶、罗源秀珍菇、漳港海蚌、青红酒、永泰芙蓉李干、鱼露、福州肉松、佛跳墙等。1、寿山石寿山石是福州十分稀有的名贵石材,也是福州市晋安区的特产,其色泽晶莹,色彩多样,可

    2023年12月17日
  • 二年级红歌串词(红歌联唱串词报幕词)

    各位网友们好,相信很多人对二年级红歌串词都不是特别的了解,因此呢,今天就来为大家分享下关于二年级红歌串词以及红歌联唱串词报幕词的问题知识,还望可以帮助大家,解决大家的一些困惑,下面一起来看看吧!

    2023年6月10日
  • 李梦先(对于李梦先简单介绍)

    小伙伴们,你们好,今天云生来聊聊一篇关于李梦先,对于李梦先简单介绍的文章,网友们对这件事情都比较关注,那么现在就为大家来简单介绍下,希望对各位小伙伴们有所帮助。1、李梦先,字维康,原名李梦轩。2、医药科普作家,主管中药师,中国民间疗法研究

    2023年10月2日
  • 大义凛然是什么意思(大义凛然的意思及成语解释)

    大义凛然,读音是dà yì lǐn rán,意思是由于胸怀正义而神态庄严,令人敬畏。 大义:正义;凛然:严肃、或敬畏的样子。由于胸怀正义而神态庄严,令人敬畏。 【出自】:宋·曹辅《…

    美文 2022年5月21日
  • 夏天成年人的较量,从2000元一把水枪开始

    夏天成年人的较量,从2000元一把水枪开始,尽管电动水枪快速崛起,但水枪依然是一个技术门槛不高的季节性品类。当电动水枪的价格从2000多元被重新“卷”回200元,新玩具的网红面纱,也会有被摘下的那一天。

    2023年8月4日 创投