zoukankan      html  css  js  c++  java
  • 为何cp覆盖进程的动态库(so)会导致coredump

    >>转载请注明来源:飘零的代码 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

  • 相关阅读:
    android studio中timber的配置
    bufferknife框架的正确使用方式 -终于他么知道了
    开源的图像滤镜库----for Android
    HSV做通道分离是出现的Vector内存越界错误
    HSV与RGB的相互转换的公式
    Windows下python的第三方库的安装
    Python出现"Non-ASCII character 'xe6' in file"错误解决方法
    Verilog的IDE Quartus II
    Java-ZipUtil工具类
    Java-JDBCUtil工具类
  • 原文地址:https://www.cnblogs.com/cnland/p/2969346.html
Copyright © 2011-2022 走看看