>>转载请注明来源:飘零的代码 piao2010 ’s blog,谢谢!^_^
>>本文链接地址:为何cp覆盖进程的动态库(so)会导致coredump
接上一篇博客《 Linux共享库(so)动态加载和升级》留下的问题:为何cp覆盖进程(运行中的程序)的动态库(so)会导致coredump ?
之前的分析只是定位到cp覆盖so文件的时候由于不会改变inode号所以引发了悲剧,但本质原因并没有找到。于是开始查找学习Linux下动态链接的实现,以及神器 gdb的常用操作。恰好在搜索相关信息的时候发现了一篇关键的文章,在作者思路的引导下发现可以利用gdb做相应的测试。
简单修改一下代码,文件test.c
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include<stdio.h> void test1(void){ int j=0; printf("test1:j=%d\n", j); return ; } void test2(void){ int j=1; printf("test2:j=%d\n", j); return ; } |
执行gcc -fPIC -shared -o libtest.so test.c -g 生成共享库文件 (注意这回我们带上了-g调试信息)
稍微修改一下主进程文件main.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
#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, "%s\n", dlerror()); return 1; } fn1 = dlsym(lib_handle, "test1"); if ((error = dlerror()) != NULL) { fprintf(stderr, "%s\n", error); return 1; } printf("fn1:0x%x\n", fn1); fn1(); fn2 = dlsym(lib_handle, "test2"); if ((error = dlerror()) != NULL) { fprintf(stderr, "%s\n", error); return 1; } printf("fn2:0x%x\n", fn2); fn2(); dlclose(lib_handle); return 0; } |
执行gcc -o main main.c -ldl -g 生成二进制文件main,同样也带上调试信息。
然后用gdb加载main进行调试,
gdb -q main Reading symbols from /root/so/main...done. (gdb) b 27 //在main.c第27行设置断点 Breakpoint 1 at 0x80485fc: file main.c, line 27. (gdb) l 27 //显示代码 22 return 1; 23 } 24 25 printf("fn1:0x%x\n", fn1); 26 27 fn1(); 28 29 fn2 = dlsym(lib_handle, "test2"); 30 if ((error = dlerror()) != NULL) 31 { (gdb) r //运行程序 Starting program: /root/so/main fn1:0x2c1450 Breakpoint 1, main () at main.c:27 //中断在我们预设的27行 27 fn1(); Missing separate debuginfos, use: debuginfo-install glibc-2.12-1.7.el6.i686 //在另外一个终端里用cp将libtest2.so(仅仅是libtest.so的拷贝而已)覆盖libtest.so (gdb) s //单步跟入函数fn1()的实现 test1 () at test.c:4 4 int j=0; (gdb) s 5 printf("test1:j=%d\n", j); //执行test.c第4行 int j=0; 并没有问题,因为没有引入未外部符号。 (gdb) s Program received signal SIGSEGV, Segmentation fault. //执行到test.c第5行printf("test1:j=%d\n", j);出现问题,因为printf是外部符号 0x0000035a in ?? () (gdb) bt //打印堆栈信息 #0 0x0000035a in ?? () #1 0x002c147e in test1 () at test.c:5 //test.c第5行是printf("test1:j=%d\n", j); #2 0x08048602 in main () at main.c:27 (gdb) |
为了作对比,可以把test.c的第5行给注释,另外main.c从29行到39行之间的也注释掉( so覆盖之后如果执行dlsym这个函数也会出现coredump,所以后面fn2相关操作要先注释),然后重新编译后测试发现这次不会出现 coredump 了,说明确实是printf这个外部符号导致的问题。
另外如果把断点设置在dlsym这个函数,可以看一下效果。
gdb -q main Reading symbols from /root/so/main...done. (gdb) b dlsym Breakpoint 1 at 0x8048434 (gdb) r Starting program: /root/so/main Breakpoint 1, 0x00131d26 in dlsym () from /lib/libdl.so.2 Missing separate debuginfos, use: debuginfo-install glibc-2.12-1.7.el6.i686 //在另外一个终端里用cp将libtest2.so(仅仅是libtest.so的拷贝而已)覆盖libtest.so (gdb) s Single stepping until exit from function dlsym, which has no line number information. Program received signal SIGSEGV, Segmentation fault. 0x00119531 in check_match.8616 () from /lib/ld-linux.so.2 (gdb) bt #0 0x00119531 in check_match.8616 () from /lib/ld-linux.so.2 #1 0x00119d64 in do_lookup_x () from /lib/ld-linux.so.2 #2 0x00119f5a in _dl_lookup_symbol_x () from /lib/ld-linux.so.2 #3 0x00252560 in do_sym () from /lib/libc.so.6 #4 0x0025295a in _dl_sym () from /lib/libc.so.6 #5 0x00131de8 in dlsym_doit () from /lib/libdl.so.2 #6 0x0011e966 in _dl_catch_error () from /lib/ld-linux.so.2 #7 0x0013203c in _dlerror_run () from /lib/libdl.so.2 #8 0x00131d7c in dlsym () from /lib/libdl.so.2 #9 0x080485ab in main () at main.c:18 |
下载glibc源码查看相关函数do_lookup_x:dl-lookup.c文件
129 130 131 132 133 134 135 136 137 138 139 140 |
/* Nested routine to check whether the symbol matches. */ const ElfW(Sym) * __attribute_noinline__ check_match (const ElfW(Sym) *sym) { unsigned int stt = ELFW(ST_TYPE) (sym->st_info); assert (ELF_RTYPE_CLASS_PLT == 1); if (__builtin_expect ((sym->st_value == 0 /* No value. */ && stt != STT_TLS) || (type_class & (sym->st_shndx == SHN_UNDEF)), 0)) return NULL; |
查看相关函数check_match:tst-rxspencer.c文件
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 |
static int check_match (regmatch_t *rm, int idx, const char *string, const char *match, const char *fail) { if (match[0] == '-' && match[1] == '\0') { if (rm[idx].rm_so == -1 && rm[idx].rm_eo == -1) return 0; printf ("%s rm[%d] unexpectedly matched\n", fail, idx); return 1; } if (rm[idx].rm_so == -1 || rm[idx].rm_eo == -1) { printf ("%s rm[%d] unexpectedly did not match\n", fail, idx); return 1; } if (match[0] == '@') { if (rm[idx].rm_so != rm[idx].rm_eo) { printf ("%s rm[%d] not empty\n", fail, idx); return 1; } if (strncmp (string + rm[idx].rm_so, match + 1, strlen (match + 1) ?: 1)) { printf ("%s rm[%d] not matching %s\n", fail, idx, match); return 1; } return 0; } if (rm[idx].rm_eo - rm[idx].rm_so != strlen (match) || strncmp (string + rm[idx].rm_so, match, rm[idx].rm_eo - rm[idx].rm_so)) { printf ("%s rm[%d] not matching %s\n", fail, idx, match); return 1; } return 0; } |
实验的结果和作者结论是一致的,所以我直接引用过来吧:
1.应用程序通过dlopen打开so的时候,kernel通过mmap把so加载到进程地址空间,对应于vma里的几个page.
2.在这个过程中loader会把so里面引用的外部符号例如malloc printf等解析成真正的虚存地址。
3.当so被cp覆盖时,确切地说是被trunc时,kernel会把so文件在虚拟内的页purge 掉。
4.当运行到so里面的代码时,因为物理内存中不再有实际的数据(仅存在于虚存空间内),会产生一次缺页中断。
5.Kernel从so文件中copy一份到内存中去,a)但是这时的全局符号表并没有经过解析,当调用到时就产生segment fault , b)如果需要的文件偏移大于新的so的地址范围,就会产生bus error.
所以,如果用相同的so去覆盖
A) 如果so 里面依赖了外部符号,coredump
B) 如果so里面没有依赖外部符号,运气不错,不会coredump
参考资料:
http://blog.sina.com.cn/s/blog_622a99700100pjv3.html
http://www.ibm.com/developerworks/cn/linux/l-dynlink/index.html