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) 

本文结合 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)

这个空函数就是 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 
   };

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;
	}
   // ....

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;
 }

处理空函数断点

在空函数命中时,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;
     }
     }
   // .....
 }

这里发现断点类型为 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;
  // ...
}

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

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

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

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值