在主程序运行时替换共享对象(.so文件)

21

我有一个共享对象gateway.so(在Linux/C中)。并且a.out应用程序正在使用它。

问题A

我猜测:当进程a.out启动时,加载器会加载gateway.so(我没有使用像dlopen这样的dl函数)。因此,所有对gateway.so的运行时符号解析都将在内存中发生。它不需要再从磁盘访问gateway.so了。

我是正确的吗?

那么,在a.out正在运行时,我不能替换gateway.so为更新版本,对吗?

问题B

另一个相关的问题:一次当我用过时的版本替换了gateway.so文件后,我收到了以下消息:

"a.out: can't resolve symbol 'Test_OpenGateway'"

哪个程序组件(加载器/链接器等...)发送了这个输出?该组件作为同一进程上下文的一部分执行吗?


@所有回答者,如果可能的话,请提供一些关于您的答案的证据... - Jeegar Patel
4个回答

40

问题 A

如果你以正确的方式进行,可以在应用程序使用库的同时替换它。

在此之前,让我们先看一下主程序二进制文件。以下是一个示例程序:

#include <unistd.h>

void justsit(void) {
  for (;;) {
    sleep(1);
  }
}

int main(int argc, char **argv) {
  printf("My PID is %d\n", getpid());
  justsit();
  return 0;
}

编译并启动它:

$ gcc -Wall -o example example.c
$ ./example
My PID is 4339

现在它只会停留在那里,因此打开一个新的终端来执行以下操作:
$ gcc -Wall -o example-updated example.c
$ cp example-updated example
cp: cannot create regular file `example': Text file busy

现在发生了什么?内核拒绝更改文件example,因为有一个正在运行该文件的进程。

现在让我们尝试删除它:

$ rm example

什么?那个有效?为什么文件可以被删除,但无法替换?实际上,文件并没有真正被删除,只是“名称”被删除,内核告诉文件系统保留文件的内容。当没有任何东西再打开这个文件时,文件的内容也会被删除。(dentry立即被删除,但是inode在没有用户时才被释放,就像文件系统人员所说的那样)

这在/proc中可以看到:(这就是为什么程序打印它的PID,以便您可以轻松检查此操作)

$ readlink /proc/4339/exe
/tmp/t/example (deleted)

无论如何。它的工作方式意味着我们可以安全地通过移除旧的二进制文件并将新的文件放在相同的位置来升级程序。有一个处理此过程的程序:install(1)。

好的,回到您的问题 - 共享对象。

让我们将示例分为两部分:main.c和shared.c:

/* main.c */
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

void justsit(void);

int main(int argc, char **argv) {
  printf("My PID is %d\n", getpid());
  justsit();
  return 0;
}

并且

/* shared.c */
#include <stdio.h>
#include <unistd.h>

void justsit(void) {
  for (;;) {
    sleep(1);
  }
}

按照以下方式进行编译:

$ gcc -Wall --shared -o libshared.so shared.c 
$ gcc -Wall -L. -o main main.c -lshared

现在,希望如果我们尝试替换libshared.so,会出现类似的“文本文件忙”错误?让我们看看。首先启动主程序 - 当前目录不在lib搜索路径中,因此告诉动态链接器在那里搜索:

$ LD_LIBRARY_PATH=. ./main 
My PID is 5697

前往另一个终端,用明显损坏的库替换它:

$ echo "junk" > libshared.so 
$

首先 - 它不像替换程序二进制文件那样被拒绝。 在另一个终端中,出现了有趣的事情,该程序停止运行,并显示以下错误消息:

Segmentation fault
$

因此,在使用程序中更换库文件是不被禁止的!但正如上面的例子所示,这可能会带来灾难性后果。

幸运的是,用于替换正在运行二进制文件的“技巧”也可以用于替换正在使用的库文件。重新启动主程序(不要忘记重新编译 libshared.so,因为它已被替换为垃圾),然后查看如何安全地对库文件进行删除。可以检查 /proc/PID/maps 以查看进程正在使用哪些共享对象:

$ cat /proc/5733/maps  | grep libshared.so
008a8000-008a9000 r-xp 00000000 08:01 2097292    /tmp/t/libshared.so
008a9000-008aa000 r--p 00000000 08:01 2097292    /tmp/t/libshared.so
008aa000-008ab000 rw-p 00001000 08:01 2097292    /tmp/t/libshared.so
$ rm libshared.so 
$ cat /proc/5733/maps  | grep libshared.so
008a8000-008a9000 r-xp 00000000 08:01 2097292    /tmp/t/libshared.so (deleted)
008a9000-008aa000 r--p 00000000 08:01 2097292    /tmp/t/libshared.so (deleted)
008aa000-008ab000 rw-p 00001000 08:01 2097292    /tmp/t/libshared.so (deleted)

主程序继续正常运行,这是因为只有名称(dentry)从磁盘中删除了,而不是实际内容(inode)。在移除后,可以安全地创建一个名为libshared.so的新文件,而不影响正在运行的程序。
因此,总结一下 - 只需使用install命令安装程序和二进制文件。
问题B:
是的,这是由用户空间中的动态链接器打印的。
#include <stdio.h>
#include <unistd.h>

int main(int argc, char **argv) {
    execl("./main", "main", NULL);
    printf("exec failed?\n");
    return 0;
}

使用gcc -Wall -o execit execit.c来编译。请记住,execl会用指定的命令替换当前进程。

$ ./execit 
main: error while loading shared libraries: libshared.so: cannot open shared object file: No such file or directory
$ rm main
$ ./execit 
exec failed?

发生了什么?这告诉我们什么?首先,出现了“error while loading shared libraries”而没有“exec failed?”。没有“exec failed”意味着进程成功替换。这意味着内核将控制权传递给动态链接器,但它失败了。在“main”被删除后,它很快就会失败,进程也无法被替换。

我只想评论一下,这是一个非常棒的答案,因为它解释了屏幕背后正在发生的事情。但我想进一步澄清:所以rm从dentry中取消链接文件,但不是inode。文件何时完全删除?当应用程序停止时(即/proc/<PID>/...被删除)吗?还是每次使用rm时我们开始在文件系统中使用更多的空间 - 我正在尝试弄清楚文件最终何时完全删除。谢谢! - code_fodder
我认为现代Unix文件系统进行引用计数。当进程退出时,它是文件的最后一个参考,因此文件被释放。 - Franklin Yu
现代内核版本出于某种原因不再发出“文本文件忙”错误。 - Hadi Brais
我认为你替换 .so 文件后出现段错误的原因是因为它还没有被加载。如果已经加载,那么在磁盘上更改文件不会影响已经加载它的任何进程。 - Hadi Brais

12

不是的,一旦运行时链接器(ld.so)将文件映射到进程的地址空间中,文件可能仍然需要从磁盘读取。这种映射发生的方式是通过mmap(2)系统调用,并使用标志PROT_EXEC允许执行。

映射并不会在整个文件映射到内存后才完成,而是创建一个内存区域,如果所请求的内存块尚未复制,则会按需调用页面故障。该页面故障由内核空间处理,通过在文件的适当偏移量处进行读取。

对于第二个问题,是运行时链接器(ld.so)在抱怨。加载ld.so的代码是由编译时链接器(ld)作为程序启动代码生成的,因此它在用户空间中执行,在main被调用之前执行。


如果可能,请提供一些有关此主题的证明/文档。 - Jeegar Patel
最好的证明是在程序执行时尝试删除gateway.so文件。由于它被mmap映射,内核应该已经锁定它并且不允许删除。 - Blagovest Buyukliev
1
我能够在主程序运行时删除gateway.so,而a.out继续正常运行。但是下次我启动a.out时,会出现“无法加载库”的错误。 - Lunar Mushrooms
1
(上述评论:通过“删除 gateway.so”这一术语,我实际上是将 gateway.so 重命名了。并没有删除) - Lunar Mushrooms
吹毛求疵:对于ELF格式,需要ld.so的事实在头文件中被记录下来,然后内核加载它并将控制权传递给它。对于旧的a.out格式,在FreeBSD 2.x/3.x上至少是这样描述的,ld.so也是这样引入的。效果基本相同。 - jilles
显示剩余6条评论

2

对于A: 没错,一旦共享库映射到内存中,就无法再替换它了。甚至可能系统已经为其他进程加载了先前版本的库,并检测到该so已经映射到内存中并将其重新映射为启动过程的一部分。这就是为什么在关键更新后(即使在*nix上),您始终需要重新启动的原因 ;)

对于B: 可执行文件使用的符号记录在二进制文件中的符号表中。系统加载器扫描此表并尝试解析所需函数的地址。如果找不到,则会出现此错误。所以答案是,消息是由动态链接加载器产生的。


0

a. 对的。在这种情况下,您必须使用dl_*()函数并尽快关闭文件。

b. 如果您替换了该文件,但它不包含所需的符号,则加载失败并出现上述错误。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接