注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

Tsecer的回音岛

Tsecer的博客

 
 
 

日志

 
 

gdb对attach和core文件的处理方式  

2013-10-24 23:49:02|  分类: gdb源代码分析 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
一、符号读取
这个问题我突然就想到我之前有两篇日志,一个是关于gdb对延迟断点的处理方式,另一个是说明linux下core文件的格式,两篇综合一下其实就应该是这篇文章的基础。但是之前的两篇日志都没有联系起来,都是基础性的东西,在遇到这个问题的时候,我还是纠结了一段时间,对于一些现象并不能解释原因。
引入这个问题的原因在于我们通过gdb -p 来attach进程,或者是-c来调试一个core文件的时候,此时gdb会人来疯的把这个进程用到的所有的so文件逐个加载,并且一边读取一边打印(大家脑部下一边走一边唱的欢乐画面):例如下面的打印
Reading symbols from /usr/lib/libstdc++.so.6...(no debugging symbols found)...done.
Loaded symbols for /usr/lib/libstdc++.so.6
Reading symbols from /lib/libm.so.6...(no debugging symbols found)...done.
Loaded symbols for /lib/libm.so.6
Reading symbols from /lib/libgcc_s.so.1...(no debugging symbols found)...done.
Loaded symbols for /lib/libgcc_s.so.1
通常情况下,这个机制工作的很好,所以通常没有人会注意到这个(就像gdb也没有注意有没有注意这个信息一样),但是如果有一天,这个你觉得天经地义的事情没有人帮你来做的时候,你就会意识到它的存在及意义。下面是我构造的一个现象
Attaching to process 11717
Reading symbols from /home/tsecer/CodeTest/OpSo/toucher.exe...done.
Error while mapping shared library sections:
./libtoucher.so: No such file or directory.
Symbol file not found for ./libtoucher.so
Reading symbols from /usr/lib/libstdc++.so.6...(no debugging symbols found)...done.

由于最近遇到过这种问题,就是gdb提示无法加载一些符号的信息(就像《鹿鼎记》的韦小宝有幸在机缘巧合之下见到过陈近南一面一样),这个现象就使我再次回想了之前两篇日志的内容,发现综合起来并不能解释这个现象。
二、gdb如何从特殊目标中找到符号信息
特点地,以core文件为例,因为core文件的生成代码在内核里是完全可见的,而且代码并不复杂。在内核的core文件生成的过程中,我们可以看到在生成的core文件中并没有包含每个VMA是否是一个文件映射,跟不用说这个VMA使用的映射文件的名字,core文件对内存区域的dump只是保存了每个VMA的开始和结束逻辑地址,而一个可执行文件及SO文件的调试符号信息又根本不会被加载入内存,此时如何从core文件中找到这个core文件在生前加载了哪些so文件,而gdb是如何知道这些信息的呢?同样的问题对于attach一个进程也是存在的,因为可以认为core文件就是对正在执行文件的一个快照保存。
1、从DT_DEBUG开始
问题总有一个切入点,当切入点找到之后,剩下的一切问题都会简单许多。符号加载来说,ELF文件下的定义了一个特殊的动态符号标签,DT_DEBUG,当我们读取一个可执行文件的时候,可以看到这个标签
[root@Harry OpSo]# readelf toucher.exe -d

Dynamic section at offset 0x75c contains 24 entries:
  Tag        Type                         Name/Value
 0x00000001 (NEEDED)                     Shared library: [./libtoucher.so]
 0x00000001 (NEEDED)                     Shared library: [libstdc++.so.6]
 0x00000001 (NEEDED)                     Shared library: [libm.so.6]
 0x00000001 (NEEDED)                     Shared library: [libgcc_s.so.1]
 0x00000001 (NEEDED)                     Shared library: [libc.so.6]
 0x0000000c (INIT)                       0x804840c
 0x0000000d (FINI)                       0x804863c
 0x6ffffef5 (GNU_HASH)                   0x804818c
 0x00000005 (STRTAB)                     0x804829c
 0x00000006 (SYMTAB)                     0x80481cc
 0x0000000a (STRSZ)                      230 (bytes)
 0x0000000b (SYMENT)                     16 (bytes)
 0x00000015 (DEBUG)                      0x0
在linux下的动态加载器ld.so为一个可执行文件执行动态链接时,它会在动态链接完成之后,将可执行文件这个位置填充上整个进程的全局的一个debug信息的位置,动态连接器中相关代码为 glibc-2.11.2\elf\rtld.c
  /* Set up debugging before the debugger is notified for the first time.  */
#ifdef ELF_MACHINE_DEBUG_SETUP
  /* Some machines (e.g. MIPS) don't use DT_DEBUG in this way.  */
  ELF_MACHINE_DEBUG_SETUP (main_map, r);
  ELF_MACHINE_DEBUG_SETUP (&GL(dl_rtld_map), r);
#else
  if (main_map->l_info[DT_DEBUG] != NULL)
    /* There is a DT_DEBUG entry in the dynamic section.  Fill it in
       with the run-time address of the r_debug structure
  */
    main_map->l_info[DT_DEBUG]->d_un.d_ptr = (ElfW(Addr)) r;

  /* Fill in the pointer in the dynamic linker's own dynamic section, in
     case you run gdb on the dynamic linker directly.  */
  if (GL(dl_rtld_map).l_info[DT_DEBUG] != NULL)
    GL(dl_rtld_map).l_info[DT_DEBUG]->d_un.d_ptr = (ElfW(Addr)) r;
#endif
这里填充的r结构的类型为
/* Rendezvous structure used by the run-time dynamic linker to communicate
   details of shared object loading to the debugger
If the executable's
   dynamic section has
a DT_DEBUG element, the run-time linker sets that
   element's value to the address where this structure can be found
.  */

struct r_debug
  {
    int r_version;        /* Version number for this protocol.  */

    struct link_map *r_map;    /* Head of the chain of loaded objects.  */

    /* This is the address of a function internal to the run-time linker,
       that will always be called when the linker begins to map in a
       library or unmap it, and again when the mapping change is complete.
       The debugger can set a breakpoint at this address if it wants to
       notice shared object mapping changes.  */
    ElfW(Addr) r_brk;
    enum
      {
    /* This state value describes the mapping change taking place when
       the `r_brk' address is called.  */
    RT_CONSISTENT,        /* Mapping change is complete.  */
    RT_ADD,            /* Beginning to add a new object.  */
    RT_DELETE        /* Beginning to remove an object mapping.  */
      } r_state;

    ElfW(Addr) r_ldbase;    /* Base address the linker is loaded at.  */
  };
这个指针存储位于可执行文件中,而指向的r_debug结构则位于ld.so的地址空间中。
2、gdb对于该信息的读取
gdb-7.2\gdb\solib-svr4.c
elf_locate_base (void)
{
……
  /* Find DT_DEBUG.  */
  if (scan_dyntag (DT_DEBUG, exec_bfd, &dyn_ptr)
      || scan_dyntag_auxv (DT_DEBUG, &dyn_ptr))
    return dyn_ptr;

  /* This may be a static executable.  Look for the symbol
     conventionally named _r_debug, as a last resort.  */
  msymbol = lookup_minimal_symbol ("_r_debug", NULL, symfile_objfile);
  if (msymbol != NULL)
    return SYMBOL_VALUE_ADDRESS (msymbol);

  /* DT_DEBUG entry not found.  */
}
这里的代码首先从DT_DEBUG中读取,如果没有则搜索_r_debug符号,这个符号也是我们在rtld.c中赋值给DT_DEBUG的符号地址,这里这么做应该是一种容错的处理方法。
事实上gdb做了更多的事情,例如可执行文件中声明的符号位置是否和进程运行时地址一致的问题。这一点在core文件生成时,内核会把进程真正的基地址也保存在在core文件中,代码在内核binfmt_elf.c:elf_core_dump()
i = 0;
    do
        i += 2;
    while (auxv[i - 2] != AT_NULL);
    fill_note(&notes[numnote++], "CORE", NT_AUXV,
          i * sizeof(elf_addr_t), auxv);
进程运行时加载地址存放在
#define AT_ENTRY  9    /* entry point of program */
中,根据可执行文件中地址和这个运行时地址可以获得两者的差异。
3、动态连接器(rtld=RunTime Loader)及gdb对链表的处理
从glibc的注释中可以知道,每个被加载入内存的so文件都会在内存中有一个link_map结构,并且整个进程地址空间中所有的这些map结构由r_debug为头组成一个链表,在知道了这个链表的起始地址之后,我们就可以遍历整个链表,得到可执行文件所有的加载so文件,我们看glibc中对于该结构的定义
struct link_map
  {
    /* These first few members are part of the protocol with the debugger.
       This is the same format used in SVR4.  */

    ElfW(Addr) l_addr;        /* Base address shared object is loaded at.  */
    char *l_name;        /* Absolute file name object was found in.  */
    ElfW(Dyn) *l_ld;        /* Dynamic section of the shared object.  */
    struct link_map *l_next, *l_prev; /* Chain of loaded objects.  */
……
}
gdb对于该链表的读取通过svr4_current_sos函数完成,根据已经获得的头地址,加上一个对link_map各个字段的描述结构LM_NEXT函数获得下一个link_map的地址,从而建立一个so_list链表,这个描述结构如此定义,指明了link_map中各个字段相对于结构开始位置的偏移量,solib-svr4.h
/* Critical offsets and sizes which describe struct r_debug and
   struct link_map on SVR4-like targets.  All offsets and sizes are
   in bytes unless otherwise specified.  */

struct link_map_offsets
  {
    /* Offset and size of r_debug.r_version.  */
    int r_version_offset, r_version_size;

    /* Offset of r_debug.r_map.  */
    int r_map_offset;

    /* Offset of r_debug.r_brk.  */
    int r_brk_offset;

    /* Offset of r_debug.r_ldsomap.  */
    int r_ldsomap_offset;

    /* Size of struct link_map (or equivalent), or at least enough of it
       to be able to obtain the fields below.  */
    int link_map_size;

    /* Offset to l_addr field in struct link_map.  */
    int l_addr_offset;

    /* Offset to l_ld field in struct link_map.  */
    int l_ld_offset;

    /* Offset to l_next field in struct link_map.  */
    int l_next_offset;

    /* Offset to l_prev field in struct link_map.  */
    int l_prev_offset;

    /* Offset to l_name field in struct link_map.  */
    int l_name_offset;
  };
对于我们常见的386系统,这个结构由函数下面函数填充,其中r_开始的时r_debug结构的内部便宜,后面l_开始的则为link_map结构的内存布局结构,对于之前截取的部分link_map结构,大家可以校验下其正确性。found in.  */
struct link_map_offsets *
svr4_ilp32_fetch_link_map_offsets (void)
{
  static struct link_map_offsets lmo;
  static struct link_map_offsets *lmp = NULL;

  if (lmp == NULL)
    {
      lmp = &lmo;

      lmo.r_version_offset = 0;
      lmo.r_version_size = 4;
      lmo.r_map_offset = 4;
      lmo.r_brk_offset = 8;
      lmo.r_ldsomap_offset = 20;

      /* Everything we need is in the first 20 bytes.  */
      lmo.link_map_size = 20;
      lmo.l_addr_offset = 0;
      lmo.l_name_offset = 4;
      lmo.l_ld_offset = 8;
      lmo.l_next_offset = 12;
      lmo.l_prev_offset = 16;
    }

  return lmp;
}
这里注意一点的时,其中使用的so文件名字是 lmo.l_name_offset = 4字段,这个地方link_map中的注释是
char *l_name;        /* Absolute file name object was
4、动态加载器对绝对路径的初始化
在前一节,说明了这里是绝对文件路径,同样在link_map中,还有一个lib文件名结构struct libname_list *l_libname;,这个并不是我们关心的,不要混淆。
这个绝对路径的初始化方法
__dlopen--->>>dlopen_doit--->>_dl_open--->>>dl_open_worker--->>_dl_map_object
  if (strchr (name, '/') == NULL)
    {
      /* Search for NAME in several places.  */
……
     /* Finally, try the default path.  */
      if (fd == -1
      && ((l = loader ?: GL(dl_ns)[nsid]._ns_loaded) == NULL
          || __builtin_expect (!(l->l_flags_1 & DF_1_NODEFLIB), 1))
      && rtld_search_dirs.dirs != (void *) -1)
    fd = open_path (name, namelen, preloaded, &rtld_search_dirs,
            &realname, &fb, l, LA_SER_DEFAULT, &found_other_class);
……
}
  else
    {
      /* The path may contain dynamic string tokens.  */
      realname = (loader
          ? expand_dynamic_string_token (loader, name)
          : local_strdup (name));
      if (realname == NULL)
    fd = -1;
      else
    {
      fd = open_verify (realname, &fb,
                loader ?: GL(dl_ns)[nsid]._ns_loaded, 0,
                &found_other_class, true);
      if (__builtin_expect (fd, 0) == -1)
        free (realname);
    }
    }
……
  return _dl_map_object_from_fd (name, fd, &fb, realname, loader, type, mode,
                 &stack_end, nsid);
}
_dl_map_object_from_fd --->>_dl_new_object
{
  new->l_libname = newname
    = (struct libname_list *) (new->l_symbolic_searchlist.r_list + 1);
  newname->name = (char *) memcpy (newname + 1, libname, libname_len);
  /* newname->next = NULL;    We use calloc therefore not necessary.  */
  newname->dont_free = 1;
……
  new->l_name = realname;

}
可以看到,在加载一个so文件时,如果so文件包含有路径分隔符(无论是绝对路径还是相对路径),此时动态加载器并不会改变这个路径的名称,而是直接把这个字符串传递给link_map保存,这样在gdb读取的时候,读取到的同样是原始的so文件字符串
三、对一些现象的解释
1、使用相对路径
同样是前一篇日志中的工程,演示下效果
[root@Harry OpSo]# cat Makefile
all:
    g++ -shared Toucher.cpp -o libtoucher.so -g
    g++ -g main.cpp  ./libtoucher.so  -o toucher.exe
[root@Harry OpSo]# make
g++ -shared Toucher.cpp -o libtoucher.so -g
g++ -g main.cpp  ./libtoucher.so  -o toucher.exe
[root@Harry OpSo]# ./toucher.exe &
[2] 12098
[root@Harry OpSo]# kill -SIGSEGV 12098
[2]+  Segmentation fault      (core dumped) ./toucher.exe
[root@Harry OpSo]#
手动发送一个段错误信号,让进程生成coredump文件
[root@Harry OpSo]# cd ..
[root@Harry CodeTest]# gdb -c OpSo/
11696_11_0         a.out              Makefile           toucher.exe
12098_11_0         libtoucher.so      test.test          usetoucher.exe
9022 7 0           libtoucher.so.bak  Toucher.c         
9917_7_0           main.cpp           Toucher.cpp       
[root@Harry CodeTest]# gdb -c OpSo/12098_11_0 OpSo/toucher.exe
GNU gdb (GDB) Fedora (7.0-3.fc12)
Copyright (C) 2009 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "i686-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /home/tsecer/CodeTest/OpSo/toucher.exe...done.
Missing separate debuginfo for ./libtoucher.so
Try: yum --enablerepo='*-debuginfo' install /usr/lib/debug/.build-id/df/5cf8dedba4556846ab28b23e84ac4c04d8e900
Error while mapping shared library sections:
./libtoucher.so: No such file or directory.
更换当前目录后gdb附加,gdb无法知道相对路径下so。
2、不指定可执行文件调试core文件
[root@Harry CodeTest]# gdb -c OpSo/12098_11_0
GNU gdb (GDB) Fedora (7.0-3.fc12)
Copyright (C) 2009 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "i686-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Missing separate debuginfo for the main executable file
Try: yum --enablerepo='*-debuginfo' install /usr/lib/debug/.build-id/90/0454e950c6997c6a0ebd384849fcdc80c71871
Core was generated by `./toucher.exe'.
Program terminated with signal 11, Segmentation fault.
#0  0x001d4424 in __kernel_vsyscall ()
(gdb)
由于没有指定可执行文件,所以gdb无法从可执行文件中找到DT_DEBUG标签的位置,进而无法找到link_map结构的头部位置,没有任何so文件加载。
3、对于没有指定路径的符号
这些大量存在于我们常见的系统so文件,例如libstdc++,libc等so文件,它们在加载时realname从系统指定文件找到so绝对路径赋值,所以core文件中可以看到它们绝对位置信息
[root@Harry CodeTest]# strings OpSo/12098_11_0 | grep -e .so
linux-gate.so.1
ld-linux.so.2
linux-gate.so.1
__gxx_personality_v0
libstdc++.so.6
libm.so.6
libgcc_s.so.1
libc.so.6
__gxx_personality_v0
libstdc++.so.6
libm.so.6
libgcc_s.so.1
libc.so.6
/lib/ld-linux.so.2
./libtoucher.so
libstdc++.so.6
__gxx_personality_v0
libm.so.6
libgcc_s.so.1
libc.so.6
/lib/ld-linux.so.2
./libtoucher.so
libstdc++.so.6
__gxx_personality_v0
libm.so.6
libgcc_s.so.1
libc.so.6
libc.so.6
libc.so.6
./libtoucher.so
./libtoucher.so
libtoucher.so
/usr/lib/libstdc++.so.6
libstdc++.so.6
libstdc++.so.6
/lib/libm.so.6
libm.so.6
libm.so.6
/lib/libgcc_s.so.1
libgcc_s.so.1
libgcc_s.so.1
/lib/libc.so.6
4、从可执行文件中看
[root@Harry CodeTest]# readelf -d OpSo/toucher.exe

Dynamic section at offset 0x75c contains 24 entries:
  Tag        Type                         Name/Value
 0x00000001 (NEEDED)                     Shared library: [./libtoucher.so]
 0x00000001 (NEEDED)                     Shared library: [libstdc++.so.6]
 0x00000001 (NEEDED)                     Shared library: [libm.so.6]
 0x00000001 (NEEDED)                     Shared library: [libgcc_s.so.1]
 0x00000001 (NEEDED)                     Shared library: [libc.so.6]
 0x0000000c (INIT)                       0x804840c
5、-l指定文件和绝对路径指明so文件的不同
通常so在命令行都是通过 -L libpath -lname的形式指定so文件作为连接输入,但是如果使用了绝对路径作为连接的输出,那么此时这个绝对路径会被保存在可执行文件中,从而导致可以执行较差。
[root@Harry OpSo]# cat Makefile
all:
    g++ -shared Toucher.cpp -o libtoucher.so -g
    g++ -g main.cpp  `pwd`/libtoucher.so  -o toucher.exe
[root@Harry OpSo]# make
g++ -shared Toucher.cpp -o libtoucher.so -g
g++ -g main.cpp  `pwd`/libtoucher.so  -o toucher.exe
[root@Harry OpSo]# readelf -d toucher.exe |head

Dynamic section at offset 0x77c contains 24 entries:
  Tag        Type                         Name/Value
 0x00000001 (NEEDED)                     Shared library: [/home/tsecer/CodeTest/OpSo/libtoucher.so]
 0x00000001 (NEEDED)                     Shared library: [libstdc++.so.6]
 0x00000001 (NEEDED)                     Shared library: [libm.so.6]
 0x00000001 (NEEDED)                     Shared library: [libgcc_s.so.1]
 0x00000001 (NEEDED)                     Shared library: [libc.so.6]
 0x0000000c (INIT)                       0x8048428
 0x0000000d (FINI)                       0x804865c
[root@Harry OpSo]#
6、动态加载
由于dlopen和rtld执行的底层操作相同,所以这里说明的情况对于动态加载同样适用。
7、补充说明
其实这篇日志的核心就是动态链接器在link_map填充lname时候会根据so名字是否包含路径分隔符而做特殊处理,但是这里写了不少,拷贝的代码也很多,并不是为了凑字数,一方面是为了备份查找过程,另一方面避免信口开河。海明码的发明者(当然就是海明本人了)说过一句话:"计算的目的不在于数据,而在于洞察事务"。同样,代码的目的不在字数,而在于思路。
  评论这张
 
阅读(1416)| 评论(0)
推荐 转载

历史上的今天

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017