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

链接是将各个代码和数据片段收集起来合并成一个文件的过程,这个文件通常是一个可执行文件,操作系统可以将这个可执行文件加载到内存中运行。链接分为静态链接,加载时动态链接,运行时动态链接,链接过程通常由链接器(linker)自动执行,不需要程序员参与。有了链接器,开发大型的程序(例如操作系统)有了可能,大型的程序可以拆分成N多个小的,更好管理的模块,每个模块各自开发和编译,等到合适的时机通过链接器将这些

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

链接分为静态链接,加载时动态链接,运行时动态链接,链接过程通常由链接器(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.i
cpp 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.s
ccl /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.s
as -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 here
collect2: 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]# ./foobar2
x = 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]# ./foobar3
x = 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】可以看出静态库在列表后面,静态库之间按照拓扑依赖顺序排列。

版权声明:本文图片和内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送联系客服 举报,一经查实,本站将立刻删除,转转请注明出处:https://www.soutaowang.com/17052.html

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注