• GDB 源码分析系列文章五:动态库延迟断点实现机制


    系列文章:

    GDB 源码分析系列文章一:ptrace 系统调用和事件循环(Event Loop)
    GDB 源码分析系列文章二:gdb 主流程 Event Loop 事件处理逻辑详解
    GDB 源码分析系列文章三:调试信息的处理、符号表的创建和使用
    GDB 源码分析系列文章四:gdb 事件处理异步模式分析 ---- 以 ctrl-c 信号为例
    GDB 源码分析系列文章五:动态库延迟断点实现机制

    延迟断点简介

    如果可执行程序使用动态链接生成,gdb 刚启动时,若断点打在动态库的符号上,因为动态库还未加载,gdb 会提示该符号找不到,并请求是否设置 pending 断点,这种断点即为延迟断点。若该符号在动态库中存在,调试过程中会命中该断点。例如:

    (gdb) b foo
    Function "foo" not defined.
    Make breakpoint pending on future shared library load? (y or [n]) y
    Breakpoint 1 (foo) pending.
    (gdb) r
    Starting program: /home/cambricon/code/sharedlib/a.out 
    
    Breakpoint 1, 0x00007ffff7fc30b0 in foo()@plt () from libfoo.so
    (gdb) 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    本文结合 gdb 源码,分析 gdb 动态库延迟断点的实现机制。

    另外,对于 gdb 的事件循环机制和符号表相关实现机制可以参考往期系列博客,本文提到相关内容时不再赘述。

    延迟断点实现机制

    gdb 之所以要支持动态库延迟断点,是由于动态库延迟加载导致的,也就是说在设置动态库符号的断点时,gdb 还没有读取动态库的符号表和调试信息。gdb 暂时将该断点设置成 pending 状态,在 gdb 读取到动态库的符号表和调试信息后,再真正插入该断点。

    那么 gdb 怎么知道动态库的加载时机呢?当前 gdb 的实现依赖动态链接库的支持。

    延迟断点其实是 gdb 和动态链接库配合实现的。延迟断点真正使能的关键是 gdb 要即使识别动态库加载,并读取其符号表,然后插入断点。gdb 识别动态库加载是通过空函数断点来实现的。gdb 在动态链接器的一个空函数上打上断点,动态库加载时会命中该断点,gdb 就可以识别到动态库的加载。gdb 读取动态库的符号表和调试信息,然后判断 pending 断点是否属于该动态库,如果是则插入该断点。并且 gdb 继续执行,就可以命中用户断点了。

    动态链接库的空函数

    这个空函数就是 _dl_debug_state ,相关代码可以参见 glibc/elf/dl-debug.c

    /* This function exists solely to have a breakpoint set on it by the
       debugger.  The debugger is supposed to find this function's address by
       examining the r_brk member of struct r_debug, but GDB 4.15 in fact looks
       for this particular symbol name in the PT_INTERP file.  */
    void
    _dl_debug_state (void)
    {
    }
    rtld_hidden_def (_dl_debug_state)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    这个空函数就是 gdb 和动态链接库约定好的、专门为调试服务的。gdb 在该函数打上断点,动态库加载时命中该断点,gdb 即识别到该断点。

    gdb 空函数处理

    插入空函数断点

    gdb 使用结构体 solib_break_names 记录了动态链接库的 _dl_debug_state 函数名:

     // gdb/solib-svr4.c
      static const char * const solib_break_names[] =
       {
         "r_debug_state",
         "_r_debug_state",
         "_dl_debug_state",
         "rtld_db_dlactivity",
         "__dl_rtld_db_dlactivity",
         "_rtld_debug_state",
       
         NULL 
       };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    gdb 在 post_create_inferior 的过程中会插入该空函数的断点。具体调用栈如下:
    run_command_1
    post_create_inferior
        solib_create_inferior_hook
          svr4_solib_create_inferior_hook
            enable_break
              solib_bfd_open
              gdb_bfd_lookup_symbol
              svr4_create_solib_event_breakpoints
                svr4_create_probe_breakpoints
                  create_solib_event_breakpoint
                    create_solib_event_breakpoint_1
                      create_internal_breakpoint

    enable_break 首先通过 solib_bfd_open 加载连接器和读取连接器符号表。然后在链接器符号表中查找 _dl_debug_state 符号,找到后,通过 svr4_create_solib_event_breakpoints 插入空函数断点。代码摘取如下:

    // gdb/solib-svr4.c
    static int
    enable_break (struct svr4_info *info, int from_tty)
    {
       // ....
       TRY
            {
    	  tmp_bfd = solib_bfd_open (interp_name);
    	}
    	// ...
    
         /* Now try to set a breakpoint in the dynamic linker.  */
        for (bkpt_namep = solib_break_names; *bkpt_namep != NULL; bkpt_namep++)
    	  {
    	    sym_addr = gdb_bfd_lookup_symbol (tmp_bfd, cmp_name_and_sec_flags,
    					                      *bkpt_namep);
    	    if (sym_addr != 0)
    	      break;
    	  }
    	if (sym_addr != 0)
    	/* Convert 'sym_addr' from a function pointer to an address.
    	   Because we pass tmp_bfd_target instead of the current
    	   target, this will always produce an unrelocated value.  */
    	sym_addr = gdbarch_convert_from_func_ptr_addr (target_gdbarch (),
    						       sym_addr,
    						       tmp_bfd_target);
    
          /* We're done with both the temporary bfd and target.  Closing
             the target closes the underlying bfd, because it holds the
             only remaining reference.  */
          target_close (tmp_bfd_target);
    
          if (sym_addr != 0)
    	{
    	  svr4_create_solib_event_breakpoints (target_gdbarch (),
    					       load_addr + sym_addr);
    	  xfree (interp_name);
    	  return 1;
    	}
       // ....
    
    • 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

    svr4_create_solib_event_breakpoints 函数通过一系列函数调用,最后插入该空函数断点。这里需要注意两点:其一,该空函数的断点是 gdb 的内部断点,也就是说不会让用户感知;其二,该断点的类型为 bp_shlib_event,该断点命中时候,gdb 知道该断点是动态库链接断点。

     static struct breakpoint *
     create_solib_event_breakpoint_1 (struct gdbarch *gdbarch, CORE_ADDR address,
                      enum ugll_insert_mode insert_mode)
     {
       struct breakpoint *b;
     
       b = create_internal_breakpoint (gdbarch, address, bp_shlib_event,
                       &internal_breakpoint_ops);
       update_global_location_list_nothrow (insert_mode);
       return b;
     }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    处理空函数断点

    在空函数命中时,gdb 通过事件循环机制捕获到该 target 事件,即进入该事件的相关处理。

    fetch_inferior_event
    handle_inferior_event
        handle_inferior_event_1
          handle_signal_stop
           bpstat_stop_status
             handle_solib_event
               srv4_handle_solib_event
               solib_add
                 update_solib_list
                 solib_read_symbol
                 breakpoint_re_set
                   breakpoint_re_set_one
                     brkt_re_set
                       breakpoint_re_set_default
                         update_breakpoint_location
           process_event_stop_test
             bpstat_what
             keep_going
               keep_going_pass_signal
                 insert_breakpoint
                 resume

    gdb 在处理 target 事件时,在 handle_signal_stop 中会判断 target 停止的原因,即会调用 bpstat_stop_status 函数判断当前是否停止在一个断点:

     bpstat
     bpstat_stop_status (struct address_space *aspace,
                 CORE_ADDR bp_addr, ptid_t ptid, 
                 const struct target_waitstatus *ws)
     {
       // ....
       /* A bit of special processing for shlib breakpoints.  We need to
          process solib loading here, so that the lists of loaded and
          unloaded libraries are correct before we handle "catch load" and
          "catch unload".  */
       for (bs = bs_head; bs != NULL; bs = bs->next)
         {
           if (bs->breakpoint_at && bs->breakpoint_at->type == bp_shlib_event)
         {
           handle_solib_event ();
           break;
         }
         }
       // .....
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    这里发现断点类型为 bp_shlib_event,则说明命中了动态链接库的空函数 _dl_debug_state。随即进入 handle_solib_event 处理。handle_solib_event 通过调用 solib_add 函数。其中 update_solib_list 负责更新 gdb 动态库链表; solib_read_symbol 读取新加载的动态库符号表;breakpoint_re_set 会判断新加载的符号表中是否包含 pending 断点的符号,若包含,则获取到 pending 断点符号的信息,通过函数 update_breakpoint_location 更新该断点信息。

    然后 handle_signal_stop 函数进入 process_event_stop_test 的处理。

    process_event_stop_test 首先调用 bpstat_what 确定如何处理该断点事件。根据 bptype 决定相应的处理动作。对于该内部断点,gdb 不会通知到用户,直接调用 keep_going 继续执行。

    /* Decide what infrun needs to do with this bpstat.  */
     struct bpstat_what
     bpstat_what (bpstat bs_head)
     {
       //..
         bptype = bs->breakpoint_at->type;
         switch (bptype)
         // ...
            case bp_shlib_event:
           if (bs->stop)
             {
               if (bs->print)
             this_action = BPSTAT_WHAT_STOP_NOISY;
               else
             this_action = BPSTAT_WHAT_STOP_SILENT;
             }
           else
             this_action = BPSTAT_WHAT_SINGLE;
           break;
      // ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    keep_going 会调用到 insert_breakpoint,这里就会将之前的 pending 断点真正插入。然后调用 resume 继续执行目标程序。这样当程序运行到用户设置的动态库的 foo 函数时,目标程序将会停住,并等待用户命令。

    以上就是 gdb 的动态库延迟断点的实现机制,更加详细的实现过程,可以以本文为引导去阅读 gdb 源码。

    至此,本文结束。更多 gdb 源码的分析,敬请期待。

  • 相关阅读:
    视觉SLAM入门 -- 学习笔记 - Part 9 Kitti 的双目视觉里程计
    蓝桥杯:Python组参赛指南 国二选手经验贴 附蓝桥杯历年真题
    通过云服务器对内网穿透实现外网访问群晖NAS
    节日网页HTML代码 学生网页课程设计期末作业下载 清明节大学生网页设计制作成品下载 DW节日网页作业代码下载
    深入理解java虚拟机:虚拟机字节码执行引擎(1)
    【AGC】AGC鉴权认证模式获取clientToken的方法
    如何使用 Terraform 和 Git 分支有效管理多环境?
    第六篇:集合常见面试题
    跟着 Guava 学 Java 之 新集合类型
    【自动化测试】——robotframework实战(三)编写测试用例
  • 原文地址:https://blog.csdn.net/Dong_HFUT/article/details/126069052