假设我们有一个使用共享库的 C 程序。
如果您对共享库进行更改并重建它,则使用该库的所有程序将在下次运行时自动接收这些更改。对于静态库,只有在使用新版本的库重新编译程序后,更改才可见。
如果我们改变一些共享库函数的代码(不改变方法签名)、添加新函数等,函数的地址就会改变。
使用我们共享库的程序如何在不重新编译和重新链接的情况下再次找到这些函数?如果他们的地址改变了。
正如我在下面解释的,您不应该只是重新编译库并将其用于已经运行的二进制文件。但我认为你可以使用
dlopen(2)
系统调用和其他方法来手动操作所需的共享库,所以一旦你重新编译该库,你只需要同步你已经运行的代码(就像最简单的 RCU 所做的那样)和新的共享库。
通常(特别是在 Linux 上)您的程序由动态加载器运行(在 Linux 上,对于 x86-64 架构,它通常放置到
/lib64/ld-linux-x86-64.so.2
,对于 32 位版本,它通常放置到 /lib/ld-linux-<something>
)。
一旦您通过调用(shell 将为您执行此操作)
exec*
函数系列(假设您使用 ELF - 可执行和可链接格式,通常您这样做)来运行程序,内核将在某个时候 - Linux 的 load_elf_binary - 读取名为 .interp
的 ELF 部分(该部分包含动态加载器的路径 - 在我的例子中为 /lib/ld-linux-x86-64.so.2
)并将链接器加载到内存。之后,内核将准备auxv
(辅助向量)值(特别是AT_ENTRY
将被设置为程序的入口点,另见getauxval(2)
),动态加载器将通过调用AT_ENTRY
读取getauxval(2)
值获取程序的入口点,以便稍后接管对程序的控制。
在动态加载器能够接管控制之前,它必须检查您的程序是否依赖于某些共享库。然后加载器也必须加载它们,否则你的程序将失败,因为它不知道函数在哪里。通常该过程是通过调用
mmap(2)
、mprotect(2)
等系统调用来完成的,以确保这些库将被共享并且不会再次加载它们。
现在是最有趣的时刻。因为动态加载器通过调用
mmap(2)
来加载这些库,所以它几乎没有选项 - 延迟加载(由 LD_BIND_NOW
环境变量控制)或 立即填充它。第一个意味着内核只会为该库准备VMA,因此一旦调用特定库中的函数(访问库的代码/数据),就会加载物理页。第二种情况意味着必须立即加载所有物理页(这种方法的优点是程序可以更快,因为它不需要在执行时等待每个页——程序一旦加载就不会出现页错误正在运行,但成本是启动时间,该时间将会增加,因为页面将在程序启动时填充)。
这意味着共享库的代码可能会加载到内存中,也可能不会加载到内存中。这导致我们假设我们可以在将库加载到内存之前对其进行修改。不,我们不能,因为ELF特定的头已经被内核读取,所以一旦你重新编译了库,你的程序很可能会失败,因为.text
,
.data
和其他段可以改变它的偏移量文件会导致错误加载页面,从而导致系统出现非常不良的行为。