问题描述
我正在阅读这篇精彩的文章:https://www.technovelty.org/linux/plt-and-got-the-key-to-code-sharing-and-dynamic-libraries.html 关于动态和静态链接。
阅读完后,有 2 个问题仍未得到解答或不够清晰,我无法理解。
1)
这不适用于共享库 (.so)。整个点 共享库是应用程序随机选择 库的排列来实现他们想要的。如果您的共享 库被构建为仅在加载到一个特定地址时工作 一切可能都很好——直到另一个图书馆出现 也使用该地址构建。问题其实有点 易于处理——你可以枚举每个共享库 系统并为它们分配所有唯一的地址范围,确保 无论加载什么库组合,它们都不会重叠。这 本质上是预链接的作用(尽管这是一个提示,而不是 而不是固定的、必需的地址库)。除了保养 噩梦,对于 32 位系统,您很快就会开始耗尽 地址空间,如果你试图给每个可能的库一个唯一的 地点。因此,当您检查共享库时,它们不会指定 要加载的特定基地址
那么动态链接是如何解决这个问题的呢?一方面,write 提到我们不能使用相同的地址,另一方面他说使用多个地址会导致可用内存不足。我听到了一个矛盾(注意:我知道什么是虚拟地址)。
2)
这是处理数据,但是函数调用呢?使用的间接 这里称为过程链接表或 plt。代码不调用 直接外部函数,但只能通过 plt 存根。让我们检查一下 这个:
我没明白,为什么数据的处理与功能不同?像我们以前处理普通变量那样在 GOT 中保存函数地址有什么问题?
解决方法
一方面,文章提到我们不能使用相同的地址,另一方面他说使用多个地址会导致可用内存不足。
在大约 15-20 年前切换到 ELF 之前的 Linux 上,所有共享库都必须在全球范围内进行协调。这是一场维护噩梦,因为一个系统可能有数百个共享库。您用完了地址空间为每个库分配唯一地址,即使其中一些库从未一起加载(但地址空间范围的分配者不知道哪些库从未一起加载,因此可以加载到相同的范围内)。
动态加载器通过在加载库时将它们放入任意地址范围来解决这个问题,并重新定位它们,以便它们在刚刚加载的地址正确执行。
这里的优点是您不需要提前对地址空间进行分区。
为什么数据处理与功能不同?
这是不同的,因为当您访问数据时,不涉及链接器。第一次访问必须有效,并且必须在库可用之前重新定位数据。没有可以为延迟动态链接挂钩的函数调用。
但是对于函数调用,链接器可以参与。程序调用PLT“存根”函数foo@plt
。在第一次调用该存根时,它执行工作以解析一个指向实际 foo()
定义的指针,并保存 那个指针。在后续调用中,foo@plt
只是使用已经保存的指针直接跳转到 foo()
的定义。
这被称为延迟重定位,如果程序永远不会访问它具有调用站点的许多库函数,它可以节省大量的工作。 (例如,一个计算数学表达式并可以调用任何 libm.so.6
函数的程序,但对于普通的简单输入,或者使用 --help
,只能调用一对。)
您可以通过运行包含大量共享库的大型程序来观察延迟重定位的效果,无论是否使用 LD_BIND_NOW
环境变量(禁用了延迟重定位)。
或者使用 gcc -fno-plt
(https://gcc.gnu.org/ml/gcc-patches/2015-05/msg00225.html),GCC 将通过 GOT 内联调用,这意味着在一次调用而不是两次调用中到达库函数。 (一些 x86-64 Linux 发行版为他们的二进制包启用了这个功能。)这需要提前绑定,但稍微降低了每次调用的成本,因此对长时间运行的程序很有好处。 (PLT + 早期绑定是两者中最糟糕的,除了在解析所有内容时具有缓存位置。)