动态连接和静态连接

一个库可以通过两种方式链接到程序中

1、静态链接:使用链接器ld在编译链接的过程中直接将所有符号链接到2进制可执行文件中,在链接的过程中完成符号的重定位。

2、动态链接:使用动态连接器(linux下叫做ld.x.x.x.so),在程序运行时动态加载所需要的符号,然后进行重定位,或者使用dlopen等函数进行显式运行时链接,这里有两种方式,即在程序运行时动态链接,或者使用dlopen进行实时的动态链接

那么、动态库和静态库有什么本质的区别?为什么静态库不能被动态链接呢?

其实,静态库从理论上来说也可以被动态链接到程序中去,只是静态库对于符号的描述中没有将符号描述为动态地址,操作系统的机制就是这样。

动态库也有两种实现方式

(1) 可重定位代码(relocatable code):Windows DLL 以及不使用 -fPIC 的 Linux SO。

生成动态库时假定它被加载在地址 0 处。加载时它会被加载到一个地址(base),这时要进行一次重定位(relocation),把代码、数据段中所有的地址加上这个 base 的值。这样代码运行时就能使用正确的地址了。

(2) 位置无关代码(position independent code):使用 -fPIC 的 Linux SO。

这样的代码本身就能被放到线性地址空间的任意位置,无需修改就能正确执行。通常的方法是获取指令指针(如 IA32 的 EIP 寄存器)的值,加上一个偏移得到全局变量/函数的地址。

PIC vs. relocatable:

(1) PIC 的缺点主要就是代码有可能长一些。例如 IA32,由于不能直接使用 [EIP+constant] 这样的寻址方式,甚至不能直接将 EIP 的值交给其他寄存器,要用到 GOT(global offset table)来定位全局变量和函数。这样导致代码的效率略低。

(2) PIC 的加载速度稍快,因为不需要做重定位。

(3) 多个进程引用同一个 PIC 动态库时,可以共用内存。这一个库在不同进程中的虚拟地址不同,但操作系统显然会把它们映射到同一块物理内存上。对于可重定位代码,则必须为每个库都在物理内存中复制一份副本,因为需要修改其中的地址。当然,主流现代操作系统都启用了分页内存机制,这使得重定位时可以使用 COW(copy on write)来节省内存(32 位 Windows 就是这样做的);然而,页面的粒度还是比较大的(例如 IA32 上是 4KiB),至少对于代码段来说能节省的相当有限。

也就是说,地址无关代码能够真正做到共享代码段,而重定向的方式是无法做到共享代码段的,因为需要重定位代码,使用写时复制的特性可以做到某些情况下共享代码段

在 Linux 下制作动态链接库,“标准” 的做法是编译成位置无关代码(Position Independent Code,PIC),然后链接成一个动态链接库。经常遇到的一个问题是 -fPIC 是不是必需,因为好像不加经常也能正常运行,只是创建 .so 的时候会有一个警告。

(1) 通常的建议是始终加上 -fPIC 生成位置无关代码;

(2) AMD64 下,必须使用位置无关代码,否则连接失败:

relocation R_X86_64_32S against `a local symbol’ can not be used when making a shared object; recompile with -fPIC

(3) IA32 下,连接成功,但有警告:

warning: creating a DT_TEXTREL in object.

如果加上-fPIC那么就使用地址无关代码来实现动态链接,否则使用重定向的方式来实现