我知道共享库被加载到内存中,并被各种程序使用。程序如何知道库在内存中的位置?
当使用共享库时,链接过程有两个部分。 在编译时,链接程序。ld
在Linux中,针对共享库进行链接,以便了解哪些符号是由它定义的。 然而,共享库中的代码或数据初始化器实际上都不包含在最终的 a.out
文件。 而不是。ld
只是记录了哪些动态库是被链接的,这些信息会被放入到 a.out
文件。
第二阶段在执行时,在 main
被调用。 内核会加载一个小的辅助程序。ld.so
,进入地址空间,这就会被执行。 因此,程序的起始地址不是 main
甚至 _start
(如果你听说过它)。 相反,它实际上是动态库加载器的起始地址。
在Linux中,内核将动态库加载器的 ld.so
loader代码到预处理地址空间的一个方便的地方,并设置堆栈,使所需共享库的列表(和其他必要的信息)存在。 动态加载器通过查看一连串的目录来找到每个所需的库,这些目录通常指向 LD_LIBRARY_PATH
环境变量。 还有一个预定义的列表,它被硬编码到了 ld.so
额外的搜索地点可以硬编码到 a.out
在链接时间内)。) 对于每一个库,动态加载器读取它的头,然后使用 mmap
来创建库的内存区域。
现在是有趣的部分。
由于在链接时并不知道实际在运行时用于满足要求的库,我们需要想办法访问共享库中定义的函数和由共享库导出的全局变量(这种做法已经被废弃,因为导出全局变量并不是线程安全的,但我们还是会尽量处理)。
全局变量在链接时被分配一个静态地址,然后通过绝对内存地址来访问。
对于库导出的函数,库的用户要发出一系列的 call
汇编指令,它引用的是一个绝对的内存地址。 但是,在链接时并不知道引用函数的具体绝对内存地址。 我们如何处理这个问题?
好吧,链接器会创建一个称为 程序链接表,这是一系列的 jmp
装配跳转)指令。 跳转的目标是在运行时填写的。
现在,当处理代码中的动态部分(即 .o
编译的文件。-fpic
),没有任何绝对的内存引用。 为了访问全局变量,而这些全局变量对代码的静态部分也是可见的,另一个叫做 全局偏移表 被使用。 这个表是一个指针数组。 在链接时,由于全局变量的绝对内存地址是已知的,所以链接器会填充这个表。 然后,在运行时,动态代码能够通过首先找到全局偏移表,然后从表中适当的槽位加载正确的变量地址,最后再去引用指针来访问全局变量。