系列文章:
GDB 源码分析系列文章一:ptrace 系统调用和事件循环(Event Loop)
GDB 源码分析系列文章二:gdb 主流程 Event Loop 事件处理逻辑详解
GDB 源码分析系列文章三:调试信息的处理、符号表的创建和使用
GDB 源码分析系列文章四:gdb 事件处理异步模式分析 ---- 以 ctrl-c 信号为例
GDB 源码分析系列文章五:动态库延迟断点实现机制
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.cstaticconstchar*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.cstaticintenable_break(structsvr4_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);return1;}// ....
svr4_create_solib_event_breakpoints
函数通过一系列函数调用,最后插入该空函数断点。这里需要注意两点:其一,该空函数的断点是 gdb 的内部断点,也就是说不会让用户感知;其二,该断点的类型为
bp_shlib_event
,该断点命中时候,gdb 知道该断点是动态库链接断点。
staticstructbreakpoint*create_solib_event_breakpoint_1(structgdbarch*gdbarch, CORE_ADDR address,enumugll_insert_mode insert_mode){structbreakpoint*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(structaddress_space*aspace,
CORE_ADDR bp_addr,ptid_t ptid,conststructtarget_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. */structbpstat_whatbpstat_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 源码的分析,敬请期待。
版权归原作者 loongknown 所有, 如有侵权,请联系我们删除。