5.4.5 加载动态库
加载动态库前,首先需要知道这个可执行程序依赖的动态库,当然也包括这些动态库依赖的动态库,因此,这是一个递归的过程。那么动态链接器是如何知道这些依赖的动态库呢?动态链接器不是一个人在战斗,在编译时,链接器已经为动态链接做了很多铺垫,其中之一就是在ELF文件中创建了一个段".dynamic",保存的全部是与动态链接相关的信息。
我们观察一下可执行程序hello中的段".dynamic":
段".dynamic"中记录了多组与动态库有关的信息,每一组信息都使用如下格式保存:
可见,每组信息使用的是tag/value的形式保存,只不过value有的是个整数值,有的是地址而已。
其中类型(Type)为"NEEDED"的项记录的就是可执行程序依赖的动态库。可以看到,hello依赖动态库libc.so.6和libf1.so。
动态链接器设计了一个数据结构来代表每个加载到内存的动态库(包括可执行程序),定义如下:
这个数据结构中记录了动态库重定位需要的关键两项信息:l_addr和l_ld。l_addr记录的是动态库在进程地址空间中映射的基址,有了这个参照,动态链接器才可以修订符号的运行时地址;l_ld指向动态库的段".dynamic",通过这个参数,动态链接器可以知道一切与动态重定位相关的信息。为了方便,结构体link_map中定义了一个数组l_info,将段".dynamic"中的信息记录在这个数组中,就不必每次使用时再去重新解析".dynamic"了。
当内核将控制权转交给动态链接器时,链接器首先为即将处理的可执行程序创建一个link_map对象,在动态链接器代码中将其命名为main_map。然后,动态链接器找到这个可执行程序依赖的动态库,当然也包括其依赖的动态库也依赖的动态库,依次链接在main_map的后面,形成一个link_map对象链表。动态链接器作为动态库依赖的一个动态库,自然也包含在这个链表中。沿着这个链表,动态链接器将动态库映射进进程地址空间,并进行重定位。
函数dl_main调用_dl_map_object_deps加载可执行程序依赖的所有动态库,代码如下:
函数dl_main给函数_dl_map_object_deps传递了一个参数main_map,前面提到过这个参数,就是可执行程序的link_map对象。函数_dl_map_object_deps遍历可执行程序依赖的所有动态库,对每一个动态库调用函数_dl_map_object将这些动态库全部映射到进程的地址空间,代码如下:
因为动态库可能已经被加载到内存了,所以_dl_map_object首先从已经映射的动态库中寻找。如果没有找到,则调用函数_dl_map_object_from_fd从文件系统加载,代码如下:
_dl_map_object_from_fd调用函数mmap映射文件中的段到进程地址空间,并将映射基址记录到link_map中的l_addr。至于函数mmap,读者应该已经隐隐猜出来了,没错,这个函数就是我们编写应用程序时使用的C库的函数mmap,只不过这是在C库内部调用,所以函数名称略有差别,其实现如下:
_mmap首先将系统调用号__NR_mmap2装入寄存器eax,然后就向内核请求服务。这里请求内核服务时之所以没有使用0x80中断,原因是为了在支持快速系统调用(Fast System Call)指令sysenter/sysexit的CPU上使用这些比0x80中断更优化的系统调用指令。
在映射了程序段后,函数_dl_map_object_from_fd调用了函数mprotect设置段的读、写以及可执行权限,mprotect使用的是内核调用号为__NR_mprotect的服务。
我们看到,动态链接器并没有发明什么新的魔法,它只是使用内核提供的系统调用将动态库映射到进程的地址空间。也就是说,虽然动态库是由动态链接器在用户空间进程映射的,但是本质上的映射动作还是由内核完成的。
最后,_dl_map_object_from_fd将link_map中的成员l_ld指向了段".dynamic"所在的位置:
函数_dl_map_object_from_fd从Program Header Table中取出类型为"DYNAMIC"的段的地址,然后再加上动态库的映射基址。
最后,我们结合图5-31来直观地看一下多个进程是如何共享一个动态库的。
图 5-31 动态库的映射
最初,当共享库映射进内存后,代码段和数据段在物理内存中分别都只有一份副本,并且都是只读的,进程A和进程B共享只读的代码段和数据段。在进程运行过程中,当任一个进程试图修改数据段时,则内核将为这个进程复制一份私有的数据段的副本,而且这个数据段的权限被设置为可读写的。这里使用的策略就是所谓的写时复制(COW,Copy On Write)。但是这个复制动作不会影响进程的地址空间,对进程是透明的,只是同一段地址通过页面表映射到不同的物理页面而已。