静态和动态链接 (二)

2014-11-24 03:18:22 · 作者: · 浏览: 1
libc.a位于/usr/lib/i386-linux-gnu/中。你可以认为标准库就是把一些常用的函数的目标文件打包在一起,用命令ar -t libc.a可以查看libc.a中的内容,其中你就可以发现printf.o这个目标文件。在链接的时候,我们需要告诉链接器需要链接的目标文件和库文件(默认gcc会把标准库作为链接器输入的一部分)。链接器会根据输入的目标文件从库文件中提取需要目标文件。比如,链接器发现main.o会需要printf这个函数,在处理标准库文件的时候,链接器就会把printf.o从库文件中提取处理。当然printf.o依赖的目标文件也很被一起提取出来。库中其他目标文件就被舍弃掉,从而减小了最终生成的可执行文件的大小。


知道了这些信息后,链接器就可以开始工作了,分为两个步骤:1)合并相似段,把所有需要链接的目标文件的相似段放在可执行文件的对应段中。2)重定位符号使得目标文件能正确调用到其他目标文件提供的函数。
用命令gcc -static -o helloworld.static main.c来编译并做静态链接,生成可执行文件helloworld.static。因为可执行文件helloworld.static已经是链接好了的,所以里面就不会有重定位表了。命令 readelf -S helloworld.static | grep .rel.text将不会有任何输出(注:-S是打印出ELF文件中的Sections)。经过静态链接生成的可执行文件,只要装载到了内存中,就可以开始运行了。

动态链接
静态链接看起来很简单,但是有些不足。其中之一就对磁盘空间和内存空间的浪费。标准库中那些函数会被放到每个静态链接的可执行文件中,在运行的时候,这些重复的内容也会被不同的可执行文件加载到内存中去。同时,如果静态库有更新的话,所有可执行文件都得重新链接才能用上新的静态库。动态链接就是为了解决这个问题而出现的。所谓动态链接就是在运行的时候再去链接。理解动态链接需要从两个角度来看,一是从动态库的角度,而是从使用动态库的可执行文件的角度。


从动态库的角度来看,动态库像普通的可执行文件一样,有其代码段和数据段。为了使得动态库在内存中只有一份,需要做到不管动态库装载到什么位置,都不需要修改动态库中代码段的内容,从而实现动态库中代码段的共享。而数据段中的内容需要做到进程间的隔离,因此必须是私有的,也就是每个进程都有一份。因此,动态库的做法是把代码段中变化的部分放到数据段中去,这样代码段中剩下的就是不变的内容,就可以装载到虚拟内存的任何位置。那代码段中变化的内容是什么,主要包括了对外部函数和变量的引用。
我们来看一个简单的例子吧,假设我们要把下面的代码做成一个动态库:
[plain
1 #include
2 extern int shared;
3 extern void bar();
4 void foo(int i)
5 {
6 printf("Printing from Lib.so %d\n", i);
7 printf("Printing from Lib.so, shared %d\n", shared);
8
9 bar();
10 sleep(-1);
11 }

1 #include
2 extern int shared;
3 extern void bar();
4 void foo(int i)
5 {
6 printf("Printing from Lib.so %d\n", i);
7 printf("Printing from Lib.so, shared %d\n", shared);
8
9 bar();
10 sleep(-1);
11 }
用命令gcc -shared -fPIC -o Lib.so Lib.c将生成一个动态库Lib.so(-shared是生成共享对象,-fPIC是生成地址无关的代码)。该动态库提供(导出)一个函数foo,依赖(导入)一个函数bar,和一个变量shared。
这里我们需要解决的问题是如何让foo这个函数能正确地引用到外部的函数bar和shared变量?程序装载有个特性,代码段和数据段的相对位置是固定的,因此我们把这些外部函数和外部变量的地址放到数据段的某个位置,这样代码就能根据其当前的地址从数据段中找到对应外部函数的地址(前提是谁能帮忙在数据段中填上这个外部函数的正确地址,下面会讲)。动态库中外部变量的地址是放在.got(global offset table)中,外部函数的地址是放在了.got.plt段中。
如果你用命令readelf -S Lib.so | grep got将会看到Lib.so中有这样两个Section。他们就是分别存放外部变量和函数地址的地方。

[plain]
[20] .got PROGBITS 00001fe4 000fe4 000010 04 WA 0 0 4
[21] .got.plt PROGBITS 00001ff4 000ff4 000020 04 WA 0 0 4

[20] .got PROGBITS 00001fe4 000fe4 000010 04 WA 0 0 4
[21] .got.plt PROGBITS 00001ff4 000ff4 000020 04 WA 0 0 4
到此为止,我们知道了动态库是把地址相关的内容放到了数据段中来实现地址无关的代码,从而使得动态库能被多个进程共享。那么接着的问题就谁来帮助动态库来修正.got和.got.plt中的地址。


那么我们就从动态链接器的角度来看看吧!


静态链接的可执行文件在装载进入内存后就可以开始运行了,因为所有的外部函数都已经包含在可执行文件中。而动态链接的可执行文件中对外部函数的引用地址在生成可执行文件的时候是未知的,所以在这些地址被修正前是动态链接生成的可执行文件是不能运行的。因此,动态链接生成的可执行文件运行前,系统会首先将动态链接库加载到内存中,动态链接器所在的路径在可执行文件可以查到的。


还是以前面的helloworld为例,用命令gcc -o helloworld.dyn main.c来以动态链接的方式生成可执行文件。然后用命令readelf -l helloworld.dyn | grep interpreter可以看到动态链接器在系统中的路径。
[plain]
[Requesting program interpreter: /lib/ld-linux.so.2]

[Requesting program interpreter: /lib/ld-linux.so.2]
当动态链接器被加载进来后,它首先做的事情就是先找到该可执行文件依赖的动态库,这部分信息也是在可执行文件中可以查到的。用命令readelf -d helloworld.dyn,可以看到如下输出:
[plain]
Dynamic section at offset 0xf28 contains 20 entries:
Tag Type Name/Value
0x00000001 (NEEDED) Shared libr