| 这个问题的起因是在讨论别人热更方案的过程中发现用cp替换so会出现coredump的问题,这里转载一部分网上的文章同时自己做一个补充。后续补充服务器热更的一些文章吧(如果有时间的话:)

一、为何cp覆盖进程的动态库(so)会导致coredump?

先说结论:

1.应用程序通过```dlopen`` 函数打开so的时候,kernel通过mmap把so加载到进程地址空间,其对应于虚拟内存空间(vma)里的几个page.

2.在这个so加载过程中loader会把so里面引用的外部符号例如malloc、printf等外部函数解析成真正的虚存地址。

3.当so被cp覆盖时,确切地说是被trunc(trunc标志会先清空文件内容后打开)时,kernel会把so文件在虚拟内存的页清除掉。

4.当运行到so里面的代码时,因为虚拟内存的页已经清除掉了,这时会产生一次缺页中断。

5.缺页中断会导致Kernel从so文件中拷贝对应的页到内存中去,这时候问题出现了:

  • a) 如果so里面依赖了外部符号,但是这时的外部符号并没有经过重新解析,kernel只是简单的将我so文件中对应的页拷贝到内存中,当调用到时就产生segment fault
  • b) 如果需要的文件偏移大于新的so的地址范围,就会产生bus error.

其本质原因是cp后的目标inode并没有被改变。在kernel中,是识别文件的方式是通过inode号而不是文件路径,也就是同一个文件路径下同一个名称的文件,在kernel眼中可能是不一样的inode。

通过```strace`` 命令 我们可以看到cp的实现是这样的:

strace cp new.so old.so 2>&1 | grep open.*test 

下面就a这种情况用代码分析验证下。

//test.c
#include<stdio.h>
void test1(void){
   int j=0;
   printf("test1:j=%dn", j);
   return ;
}
void test2(void){
   int j=1;
   return ;
}

执行下面命令生成so文件

gcc -fPIC -shared -o libtest.so test.c -g
//main.c
#include <stdio.h>
#include <dlfcn.h>
int main()
{
   void *lib_handle;
   void (*fn1)(void);
   void (*fn2)(void);
   char *error;
   //表示要将库装载到内存,准备使用
   lib_handle = dlopen("libtest.so", RTLD_LAZY);
   if (!lib_handle)
   {
       fprintf(stderr, "%sn", dlerror());
       return 1;
   }
   //获得指定函数(symbol)在内存中的位置(指针)
   fn1 = dlsym(lib_handle, "test1");
   if ((error = dlerror()) != NULL)
   {
       fprintf(stderr, "%sn", error);
       return 1;
   }
   printf("fn1:0x%xn", fn1);
   fn1();
   fn2 = dlsym(lib_handle, "test2");
   if ((error = dlerror()) != NULL)
   {
     fprintf(stderr, "%sn", error);
     return 1;
   }
   printf("fn2:0x%xn", fn2);
   fn2();
   dlclose(lib_handle);
   return 0;
}

执行命令:

gcc -o main main.c -ldl -g

首先进行测试1,断点设置在27行,fn1()执行之前

Breakpoint 1, main () at main.c:27
//这时我们在另外一个终端执行下面的命令
//cp libtest.so libtest2.so
//cp libtest2.so libtest.so
27      fn1();
(gdb) s
test1 () at test.c:4
4       int j=0; //没有报错
(gdb) n
5       printf("test1:j=%dn", j);
(gdb) n
//出错,因为引用了printf外部函数,而全局符号表并没有经过重新解析,找不到printf函数
Program received signal SIGSEGV, Segmentation fault.
0x00000396 in ?? ()
(gdb) bt
#0  0x00000396 in ?? ()
#1  0xb7fd84aa in test1 () at test.c:5
#2  0x08048622 in main () at main.c:27

下面进行测试2,断点设置在38行,fn2执行之前。

然后在另一个终端执行和测试1相同的cp操作

Breakpoint 1, main () at main.c:38
38      fn2();
(gdb) s
test2 () at test.c:10
10      int j=1;
(gdb) n
12  }
(gdb) n
main () at main.c:40
40      dlclose(lib_handle);
(gdb) n
42      return 0;
(gdb)
43  }//程序正常结束

从这两个测试例子中,我们可以得到这样的结论:

当用新的so文件去覆盖老的so文件时候:

A)如果so里面依赖了外部符号,程序会core掉

B)如果so里面没有依赖外部符号,so部分代码可以正常运行

二、中为什么动态换bin程序不会core而换so容易core?

  Linux中, 如果一个程序正在运行中,那么要动态替换程序,cp new old, 会发现报“text file busy”。用 strace 查看cp命令输出,会发现报:open old的时候,用了 O_WRONLY|O_TRUNC,open 返回 ETXTBSY (Text file busy)。也就是说,这时候这个文件已经是不可更改的了。如果用 cp -rf 复制,检验下又会发现,其实复制得到的文件的文件虽然还是原来的名字,但是 inode 已经变了。也就是说,cp -rf 其实还是没有真正的覆盖成功。

  这些都是为什么呢?首先不得不说下linux中二进制文件执行的时候的延迟加载。也就是说如果一个bin文件并不会一次性加载进内存,而是按需逐步加载的。为了防止bin文件修改后动态按需load的时候出错,所以内核系统就会把文件锁死,使得不能随便更改。这解释了为什么会“text file busy”。同时也说明了,rm + cp方式动态替换程序的时候,或者动态删除 bin 的时候,“延迟加载”不会导致程序出core。因为文件的inode还没有释放,等于说原文件还存在。

  对于 .so 动态库文件,动态覆盖容易导致出core,是因为系统没有对so作特殊保护,不会”text file busy”之故。

​ 还有一点想说的是编写so文件不应该将内存对象暴露到外部,避免热更的时候发生内存泄漏。通过so热更的第一步就是用install替换so文件,之后要有一个客户端或者是服务器上设置一个哨兵触发程序reload(自己编写的函数,实际上就是重新加载so)so文件。 这样简单的so热更就完成了。

总结

参考

https://www.ruanyifeng.com/blog/2011/12/inode.html 理解inode