动态库加载的底层原理
动态库加载的底层原理
1. 动态库加载策略
动态库有两种绑定策略,延迟绑定(Lazy Binding)和立即绑定(Eager Binding)。这两种绑定方式在程序编译时确定,决定程序在运行时如何加载动态库。
1.1 延迟绑定
延迟绑定为程序运行过程中,首次调用库函数时再解析符号,进行动态库加载。延迟绑定是 gcc 的默认动态库绑定策略:
gcc -o main main.c -Wl,-z,lazy
-Wl
表示将选项传递给链接器,-z,lazy
表示选择延迟绑定策略。
1.2 立即绑定
立即绑定为在程序启动时,完成所有符号解析,即在程序启动时加载动态库。
gcc -o main main.c -Wl,-z,now
-Wl
表示将选项传递给链接器,-z,now
表示选择立即绑定策略。
1.3 绑定选择策略
立即绑定更适合在程序处于调试阶段中使用,立即绑定在程序启动时对所有符号解析,可以快速检查出库和符号错误,即时修改 BUG。由于一个程序不一定会用到其包含的所有动态库,如果 release 程序也使用立即绑定,程序的启动速度可能就会很慢。所以会使用延迟绑定让库按需加载来提升程序运行速度。
2. 动态库的底层
当进程被加载到内存时,其依赖的动态库并不会立即加载进内存,而是由操作系统的动态链接器(如 Linux 下的 ld.so)按需加载。当进程首次调用动态库中的函数或访问其中的符号时,动态链接器会将动态库映射到进程的虚拟地址空间中。动态库的加载主要通过内存映射(mmap)机制实现,这样可以在需要时加载动态库的内容,节省内存资源。
在进程的虚拟地址空间中,动态库的代码和数据被映射到特定区域。动态库的代码段通常是只读且可执行的,因此多个进程可以共享相同的物理内存页。而数据段则是私有的,每个进程有自己独立的数据副本。相比之下,静态库在编译时被直接链接进可执行文件,成为程序代码的一部分,加载时与程序一起进入内存。
当其他进程需要使用已加载的动态库时,操作系统会通过内存映射技术共享动态库的物理页,避免重复加载。内核在映射动态库时会检测是否已有相同的文件映射,若存在,则直接共享已加载的物理页,进一步优化内存使用。
由于动态库可以被加载到虚拟地址空间的任意位置,为了保证代码的可执行性,动态库通常被编译为位置无关代码(PIC,Position Independent Code)。这样,无论动态库被加载到虚拟地址空间的哪个位置,都能正确访问其内部的函数和数据。
PIC 通过相对寻址方式访问数据和函数,主要依赖全局偏移表(GOT,Global Offset Table)和过程链接表(PLT,Procedure Linkage Table)来实现符号解析和函数调用。