gdb动态库延迟断点及线程/进程创建相关事件处理(下)

Posted tsecer

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了gdb动态库延迟断点及线程/进程创建相关事件处理(下)相关的知识,希望对你有一定的参考价值。

一、被调试任务所有so文件如何枚举
在前一篇博客中,大致说明了gdb是通过一个动态库提供的回调函数(_dl_debug_state)处埋伏断点,然后通过约定好的_r_debug全局变量来得到exe程序对应的link_map,然后以该结构为队列头来遍历被调试任务中所有的so文件。当时也说了这个地方比较模糊,只是说了一个思路,所以这里再试图把这个实现相对详细的描述一下。
二、定义被调试任务(debuggee)的link_map地址
同样是在gdb-6.5gdbsolib-svr4.c文件中,其中包含了专门用来定位这个文件位置的函数:
static CORE_ADDR
elf_locate_base (void)
{
  struct bfd_section *dyninfo_sect;
  int dyninfo_sect_size;
  CORE_ADDR dyninfo_addr;
  gdb_byte *buf;
  gdb_byte *bufend;
  int arch_size;

  /* Find the start address of the .dynamic section.  */
  dyninfo_sect = bfd_get_section_by_name (exec_bfd, ".dynamic");通过名字找到被调试程序的动态库节(节名为.dynamic)
  if (dyninfo_sect == NULL)
    return 0;
  dyninfo_addr = bfd_section_vma (exec_bfd, dyninfo_sect);找到该节被加载入内存之后的地址,这是一个动态地址

  /* Read in .dynamic section, silently ignore errors.  */
  dyninfo_sect_size = bfd_section_size (exec_bfd, dyninfo_sect);动态节大小
  buf = alloca (dyninfo_sect_size);
  if (target_read_memory (dyninfo_addr, buf, dyninfo_sect_size))将动态节所有内容读入调试器内存中
    return 0;

  /* Find the DT_DEBUG entry in the the .dynamic section.
     For mips elf we look for DT_MIPS_RLD_MAP, mips elf apparently has
     no DT_DEBUG entries.  */

  arch_size = bfd_get_arch_size (exec_bfd);
  if (arch_size == -1)    /* failure */
    return 0;

  if (arch_size == 32)  32bits系统处理。
    { /* 32-bit elf */
      for (bufend = buf + dyninfo_sect_size;
       buf < bufend;
       buf += sizeof (Elf32_External_Dyn))遍历动态节中的每个tag。
    {
      Elf32_External_Dyn *x_dynp = (Elf32_External_Dyn *) buf;
      long dyn_tag;
      CORE_ADDR dyn_ptr;

      dyn_tag = bfd_h_get_32 (exec_bfd, (bfd_byte *) x_dynp->d_tag);
      if (dyn_tag == DT_NULL)
        break;
      else if (dyn_tag == DT_DEBUG)如果某个tag标识为DT_DEBUG,返回该TAG的值。注意,这个是实现的核心
        {
          dyn_ptr = bfd_h_get_32 (exec_bfd, 
                      (bfd_byte *) x_dynp->d_un.d_ptr);
          return dyn_ptr;
        }

我们随便找个可执行程序来看一下它的动态节
[[email protected] linux-2.6.37.1]$ readelf -d `which cat`

Dynamic section at offset 0xa5e4 contains 24 entries:
  Tag        Type                         Name/Value
 0x00000001 (NEEDED)                     Shared library: [libc.so.6]
 0x0000000c (INIT)                       0x8048cdc
 0x0000000d (FINI)                       0x805066c
 0x6ffffef5 (GNU_HASH)                   0x804818c
 0x00000005 (STRTAB)                     0x8053da4
 0x00000006 (SYMTAB)                     0x80481cc
 0x0000000a (STRSZ)                      795 (bytes)
 0x0000000b (SYMENT)                     16 (bytes)
 0x00000015 (DEBUG)                      0x0   TAG对应内容为零,因为它是在运行时由动态链接器初始化的
 0x00000003 (PLTGOT)                     0x80536dc
三、DT_DEBUG何时初始化
glibc-2.7elf tld.c
static void
dl_main (const ElfW(Phdr) *phdr,
     ElfW(Word) phnum,
     ElfW(Addr) *user_entry)
{
……
  /* Initialize _r_debug.  */
  struct r_debug *= _dl_debug_initialize (GL(dl_rtld_map).l_addr,
                        LM_ID_BASE);
……
  /* 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
……
}
所以此时的方法是调试器在主程序(注意:不是动态链接器)的DT_DEBUG节中填充上程序的_r_debug变量的地址。我们看一下找个结构的定义
glibc-2.7elflink.h
struct r_debug
  {
    int r_version;        /* Version number for this protocol.  */

    struct link_map *r_map;    /* Head of the chain of loaded objects.  */
}
四、动态库布局的一些问题
[[email protected] linux-2.6.37.1]$ sleep 1234 &
[1] 17451
[[email protected] linux-2.6.37.1]$ cat /proc/17451/maps
001e8000-00206000 r-xp 00000000 fd:00 1280       /lib/ld-2.11.2.so
00206000-00207000 r--p 0001d000 fd:00 1280       /lib/ld-2.11.2.so  这里横亘一个只读数据区,比较特殊,从何而来
00207000-00208000 rw-p 0001e000 fd:00 1280       /lib/ld-2.11.2.so
0020a000-0037c000 r-xp 00000000 fd:00 1282       /lib/libc-2.11.2.so
0037c000-0037d000 ---p 00172000 fd:00 1282       /lib/libc-2.11.2.so  这个地方还有一个更惨无人道的不可访问数据区
0037d000-0037f000 r--p 00172000 fd:00 1282       /lib/libc-2.11.2.so
0037f000-00380000 rw-p 00174000 fd:00 1282       /lib/libc-2.11.2.so
00380000-00383000 rw-p 00000000 00:00 0 
00bef000-00bf0000 r-xp 00000000 00:00 0          [vdso]
08048000-0804e000 r-xp 00000000 fd:00 49195      /bin/sleep
0804e000-0804f000 rw-p 00005000 fd:00 49195      /bin/sleep
09d16000-09d37000 rw-p 00000000 00:00 0          [heap]
b7686000-b7886000 r--p 00000000 fd:00 100518     /usr/lib/locale/locale-archive
b7886000-b7887000 rw-p 00000000 00:00 0 
b789c000-b789d000 rw-p 00000000 00:00 0 
bfafc000-bfb11000 rw-p 00000000 00:00 0          [stack]
[[email protected] linux-2.6.37.1]$ readelf -l /lib/ld-2.11.2.so 

Elf file type is DYN (Shared object file)
Entry point 0x1e8850
There are 7 program headers, starting at offset 52

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  LOAD           0x000000 0x001e8000 0x001e8000 0x1d58c 0x1d58c R E 0x1000
  LOAD           0x01dc60 0x00206c60 0x00206c60 0x00bc0 0x00c80 RW  0x1000
  DYNAMIC        0x01defc 0x00206efc 0x00206efc 0x000c8 0x000c8 RW  0x4
  NOTE           0x000114 0x001e8114 0x001e8114 0x00024 0x00024 R   0x4
  GNU_EH_FRAME   0x01aee0 0x00202ee0 0x00202ee0 0x005e4 0x005e4 R   0x4
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x4
  GNU_RELRO      0x01dc60 0x00206c60 0x00206c60 0x003a0 0x003a0 R   0x1

[[email protected] linux-2.6.37.1]$ readelf -l /lib/libc-2.11.2.so 

Elf file type is DYN (Shared object file)
Entry point 0x220d10
There are 10 program headers, starting at offset 52

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  PHDR           0x000034 0x0020a034 0x0020a034 0x00140 0x00140 R E 0x4
  INTERP         0x13fc90 0x00349c90 0x00349c90 0x00013 0x00013 R   0x1
      [Requesting program interpreter: /lib/ld-linux.so.2]
  LOAD           0x000000 0x0020a000 0x0020a000 0x171bcc 0x171bcc R E 0x1000
  LOAD           0x1721c0 0x0037d1c0 0x0037d1c0 0x027bc 0x057a8 RW  0x1000

  DYNAMIC        0x173d7c 0x0037ed7c 0x0037ed7c 0x000f8 0x000f8 RW  0x4
  NOTE           0x000174 0x0020a174 0x0020a174 0x00044 0x00044 R   0x4
  TLS            0x1721c0 0x0037d1c0 0x0037d1c0 0x00008 0x00040 R   0x4
  GNU_EH_FRAME   0x13fca4 0x00349ca4 0x00349ca4 0x06d5c 0x06d5c R   0x4
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x4
  GNU_RELRO      0x1721c0 0x0037d1c0 0x0037d1c0 0x01e40 0x01e40 R   0x1
1、各个内存区属性设置位置
glibc-2.7elfdl-load.c
struct link_map *
_dl_map_object_from_fd (const char *name, int fd, struct filebuf *fbp,
            char *realname, struct link_map *loader, int l_type,
            int mode, void **stack_endp, Lmid_t nsid)
其中有一个循环,就是处理program header中的各个节,其中代码为

    case PT_LOAD:这里使我们最为常见的两个映射,也就是对应上面“r-xp”对应的代码段,rw-p对应的数据段
      /* A load command tells us to map in part of the file.
         We record the load commands and process them all later.  */
……
    case PT_GNU_STACK:
      stack_flags = ph->p_flags;
      break;

    case PT_GNU_RELRO:这里是我们不太常见,但是能够从maps文件中体现出来的RELRO节
      l->l_relro_addr = ph->p_vaddr;
      l->l_relro_size = ph->p_memsz;
      break;
2、不可访问数据区由来
0037c000-0037d000 ---p 00172000 fd:00 1282       /lib/libc-2.11.2.so  这个地方还有一个更惨无人道的不可访问数据区
 
我们看一下glibc的两个DT_LOAD节
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  LOAD           0x000000 0x0020a000 0x0020a000 0x171bcc 0x171bcc R E 0x1000
  LOAD           0x1721c0 0x0037d1c0 0x0037d1c0 0x027bc 0x057a8 RW  0x1000
第一个节结束于0x0020a000 + 0x171bcc=0x37BBCC,第二个节开始于0x0037d1c0 ,前者向上以页面为单位取整(0x1000)为0x37c000,后者向下取整为0x0037d000 ,中间相差了一个页面,然后动态连接器毫不客气的把这个区间设置为了不可访问,对应代码为
/* Determine whether there is a gap between the last segment
         and this one.  */
      if (nloadcmds > 1 && c[-1].mapend != c->mapstart)
        has_holes = true;
……
    if (has_holes)
      /* Change protection on the excess portion to disallow all access;
         the portions we do not remap later will be inaccessible as if
         unallocated.  Then jump into the normal segment-mapping loop to
         handle the portion of the segment past the end of the file
         mapping.  */
      __mprotect ((caddr_t) (l->l_addr + c->mapend),
              loadcmds[nloadcmds - 1].mapstart - c->mapend,
              PROT_NONE);
3、只读数据由来
void internal_function
_dl_protect_relro (struct link_map *l)
{
  ElfW(Addr) start = ((l->l_addr + l->l_relro_addr)
              & ~(GLRO(dl_pagesize) - 1));
  ElfW(Addr) end = ((l->l_addr + l->l_relro_addr + l->l_relro_size)这里的l_relro_addr和l_relro_size同样是之前对DT_RELRO节的读取,对于libc来说,这个值为0x1721c0 0x0037d1c0 0x0037d1c0 0x01e40 0x01e40 R   0x1,即地址为0x0037d1c0  、大小为0x01e40 
            & ~(GLRO(dl_pagesize) - 1));

  if (start != end
      && __mprotect ((void *) start, end - start, PROT_READ) < 0)
    {
      static const char errstring[] = N_("
cannot apply additional memory protection after relocation");
      _dl_signal_error (errno, l->l_name, NULL, errstring);
    }
}
上面的流程处理比较诡异,其实地址和结束地址都是向下取整,所以对于这只读区间,其保护范围为
0x0037d1c0向下取整0x0037d000,结束地址37F000,所以这个只读区大小为两个页面,对应内存为

0037d000-0037f000 r--p 00172000 fd:00 1282       /lib/libc-2.11.2.so
五、和nptl线程库比较
其实这个so的枚举和线程的枚举有很多类似的地方,之前说的对vfork clone之类的跟踪并不能解决线程枚举问题,因为gdb有时候需要在一个程序运行起来之后 attach到一个线程,在attach之后,它只能逐个枚举线程(而不是靠拦截clone系统调用),它有和动态库相似的模式,只是现在的gdb还没有使用,但是线程库操作始终是一个重要问题,大家可以看一下nptl_db文件夹下实现,好像应该对应的文件为pthread_db库,它包含了很多对线程库调试相关的内容。
 
 
 
 
 

以上是关于gdb动态库延迟断点及线程/进程创建相关事件处理(下)的主要内容,如果未能解决你的问题,请参考以下文章

GDB再学习(10):线程调试相关

GDB再学习(10):线程调试相关

python进程相关 - 多线程threading库

gdb多线程调试

gdb - 多线程和共享库

IDA动态调试