关于通过cp覆盖so导致coredump
| 这个问题的起因是在讨论别人热更方案的过程中发现用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热更就完成了。