GDB 源码分析系列文章二:gdb 主流程 Event Loop 事件处理逻辑详解

系列文章:

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

GDB 源码分析系列文章二:gdb 主流程 Event Loop 事件处理逻辑详解

上一篇 探寻 GDB 内部实现系列文章一:ptrace 系统调用和事件循环(Event Loop) 介绍了 gdb 内部实现关键技术 ptrace 系统调用和 Event Loop 事件循环机制。今天将接着上文,详细介绍 gdb 主流程 Event Loop 事件处理机制。

上文介绍了 gdb 主进程(tracer)创建子进程(inferior/tracee)后,子进程先进入被追踪模式,然后调用 exec 接口执行被调试程序时,inferior 会向主进程发送 SIGCHLD 信号,并将自己停住。我们探寻一下这个过程的原理。

tracee 的被追踪模式发生了什么

gdb 创建 tracee,tracee 将自己设置为被追踪模式,然后执行 exec。

void do_tracee( void )
{
	printf( "tracee process %ld\n", (long)getpid() );

	if (ptrace( PTRACE_TRACEME, 0, NULL, NULL ))
	{
		perror( "tracee error!" );
		return;
	}

	execve( "test", NULL, NULL); // test 是一个可执行程序
}


int main()
{
	pid_t child;
	child = fork();
	if (child == 0)  // 子进程
		do_tracee();
    ...
}

ptrac() 会调用内核 sys_ptrace() 函数,sys_ptrace() 将自己标记为追踪模式,X86 CPU 的 sys_ptrace() 代码如下:

asmlinkage int sys_ptrace(long request, long pid, long addr, long data)
{
    ...
    if (request == PTRACE_TRACEME) {
        if (current->ptrace & PT_PTRACED)
            goto out;
        current->ptrace |= PT_PTRACED;
        ret = 0;
        goto out;
    }
    ...
}

再看 exec() 函数执行过程中发生了什么? exec() 函数调用过程如下:exec->sys_execve() -> do_execve() -> load_elf_binary(),在 load_elf_binary() 函数中会向当前进程发送 SIGTRAP 信号。

static int load_elf_binary(struct linux_binprm * bprm, struct pt_regs * regs)
{
    ...
    if (current->ptrace & PT_PTRACED)
        send_sig(SIGTRAP, current, 0);
    ...
}

当前进程接收到 SIGTRAP 时通过 do_signal() 进行处理:如果自己被标记为被追逐状态,则使当前进程暂停,并向主进程发送 SIGCHLD 信号。

int do_signal(struct pt_regs *regs, sigset_t *oldset) 
{
    for (;;) {
        unsigned long signr;

        spin_lock_irq(&current->sigmask_lock);
        signr = dequeue_signal(&current->blocked, &info);
        spin_unlock_irq(&current->sigmask_lock);

        if ((current->ptrace & PT_PTRACED) && signr != SIGKILL) {
            current->exit_code = signr;
            current->state = TASK_STOPPED;
            notify_parent(current, SIGCHLD);
            schedule();
            ...
        }
    }
}

总结一下子进程在被被追踪模式时,调用 exec() 后主要做了哪些事情:

  1. 将 tracee 设置为被追踪模式
  2. 向 tracee 发送 SIGTRAP 信号
  3. 处理 SIGTRAP 信号,将 tracee 暂停
  4. 向 tracer 发送 SIGCHLD 信号

gdb 断点机制简介

gdb 断点通常使用 INT3 指令来实现,INT3 是一个字节的指令,它的操作码是 0xcc。gdb 设置断点其实就是在断点位置,将第一个字节保存,并替换成 0xcc。当程序运行到断点位置将产生 SIGTRAP 信号。gdb 既接收到子进程发送的 SIGCHLD 信号,也能接收到子进程的 SIGTRAP 信号,然后 gdb 可以通过 ptrace 接口查看被调试程序状态。然后 gdb 再将断点处指令恢复,并将 pc 指针返回上一条指令,然后重新执行即可。

gdb 调试模拟程序

下面给出一段程序来模拟 gdb 实现的过程。

首先,给一段被调试程序:

#include <stdio.h>

int main () {
    int a = 3;
    printf("before breakpoint pos!\n");
    a = 4;
    printf("after breakpoint pos!\n");
    return 0;
}

我们将在第二个 printf 之前设置断点,为此我们使用 objdump -d 观测反汇编:

0000000000400526 <main>:
  400526:	55                   	push   %rbp
  400527:	48 89 e5             	mov    %rsp,%rbp
  40052a:	48 83 ec 10          	sub    $0x10,%rsp
  40052e:	c7 45 fc 03 00 00 00 	movl   $0x3,-0x4(%rbp)
  400535:	bf e4 05 40 00       	mov    $0x4005e4,%edi
  40053a:	e8 c1 fe ff ff       	callq  400400 <puts@plt>
  40053f:	c7 45 fc 04 00 00 00 	movl   $0x4,-0x4(%rbp)
  400546:	bf fb 05 40 00       	mov    $0x4005fb,%edi
  40054b:	e8 b0 fe ff ff       	callq  400400 <puts@plt>
  400550:	b8 00 00 00 00       	mov    $0x0,%eax
  400555:	c9                   	leaveq 
  400556:	c3                   	retq   
  400557:	66 0f 1f 84 00 00 00 	nopw   0x0(%rax,%rax,1)
  40055e:	00 00

断点位置设置在 0x400546 位置。我们的模拟程序如下:

#include <stdio.h>
#include <signal.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/user.h>
#include <time.h>

void sigchld_handler( int sig )
{
    printf( "Process %ld received signal %d\n", (long)getpid(), sig );
}

void do_debugger( pid_t child )
{
    int status = 0;
    long data;
    long orig_data;
    unsigned long addr;

    struct user_regs_struct regs;

    printf( "debugger process %ld\n", (long)getpid() );

    if (signal( SIGCHLD, sigchld_handler ) == SIG_ERR)
    {
        perror( "signal" );
        exit( -1 );
    }

    // Waiting for child process to stop...
    wait( &status );
    
    // Placing breakpoint...
    addr = 0x400546;

    data = ptrace( PTRACE_PEEKTEXT, child, (void *)addr, NULL );
    orig_data = data;
    data = (data & ~0xff) | 0xcc;
    ptrace( PTRACE_POKETEXT, child, (void *)addr, data );

    // Breakpoint is ready. Telling child to continue running...
    printf( "resume debuggee!\n"); 
    ptrace( PTRACE_CONT, child, NULL, NULL );
    child = wait( &status );
    
    // Restoring original data...
    ptrace( PTRACE_POKETEXT, child, (void *)addr, orig_data );
    
    // Changing RIP register so that it will point to the right address...
    memset( &regs, 0, sizeof( regs ) );
    ptrace( PTRACE_GETREGS, child, NULL, &regs );
    printf( "RIP before resuming child is %lx\n", regs.rip );
    regs.rip = addr;
    ptrace( PTRACE_SETREGS, child, NULL, &regs );
    sleep( 2 );
    printf( "debugger can watch debuggee here!\n");
    sleep( 2 );
    printf( "resume debuggee!\n");
    ptrace( PTRACE_CONT, child, NULL, NULL );

    child = wait( &status );
    if (WIFSTOPPED( status ))
        printf( "Debuggie stopped %d\n", WSTOPSIG( status ) );
    if (WIFEXITED( status ))
        printf( "Debuggie exited...\n" );

    printf( "Debugger exiting...\n" );
}

void do_debuggee( void )
{
    char* argv[] = { NULL };
    char* envp[] = { NULL };

    printf( "In debuggie process %ld\n", (long)getpid() );

    if (ptrace( PTRACE_TRACEME, 0, NULL, NULL ))
    {
        perror( "ptrace" );
        return;
    }

    execve( "test", argv, envp );
}

int main()
{
    pid_t child;

    child = fork();
    if (child == 0)
        do_debuggee();
    else if (child > 0)
        do_debugger( child );
    else
    {
        perror( "fork" );
        return -1;
    }

    return 0;
}

运行程序,输出如下:

debugger process 68107
In debuggie process 68108
Process 68107 received signal 17
resume debuggee!
before breakpoint pos!
Process 68107 received signal 17
RIP before resuming child is 400547
debugger can watch debuggee here!
resume debuggee!
after breakpoint pos!
Process 68107 received signal 17
Debuggie exited...
Debugger exiting...

总结一下模拟程序的流程:

  1. 主进程 debugger 创建子进程 debuggee。
  2. debuggee 设置自己为被追踪模式,并执行目标程序。
  3. debugger 中通过 signal 机制监控来自子进程的 SIGCHLD 信号,并使用回调函数 sigchld_handler 记录日志。
  4. debugger 接收到 SIGCHLD 信号,并且通过 wait 接口接收到 debuggee 停止状态信号。
  5. debugger 读取 test 断点位置指令并保存,然后修改为 0xcc INT3 指令。接着发送 PTRACE_CONT 信号让 test 继续执行。
  6. debugger 接收到 SIGCHLD 信号,并且通过 wait 接口接收到 debuggee 停止状态信号。说明 test 停在了我们设置的断点位置。这个时候就可以观测 test 程序的状态了。接着读取 pc 指针,并替换成前一条指令,并且恢复断点位置的指令。发送PTRACE_CONT 信号让 test 继续执行。
  7. debugger SIGCHLD 信号,并且通过 wait 接口接收到 debuggee 退出信号。此事,目标程序已执行完成。debugger 退出。

gdb 主流程 Event Loop 详解

上面一节 gdb 调试的模拟程序已经在很大程度上体现了 gdb 调试程序的流程。下面,我将结合 gdb 源码(7.12)分析和梳理下 gdb 处理主流程。

从上一篇文章的介绍我们知道,gdb 主流程采用 Event Loop 循环机制,Event Loop 循环处理两个事件:用户输入(stdin event)事件和目标程序事件(target event)。

用户事件是将标准输入(fd=0)作为事件源加入到 gdb_nitifier。当用户输入命令后,Event Loop 中通过 poll/select 捕捉到事件源发生改变,随即进入 stdin event 回调函数处理用户命令。

目标程序事件是使用 linux_nat_event_pipe[2] = {-1, -1} 作为事件源。linux_nat_event_pipe 地一个元素作为 fd,第二个元素用于事件变化。通过 linux_async_pipe 创建和销毁 target event pipe:

// gdb/linux-nat.c
 4528 static int
 4529 linux_async_pipe (int enable)
 4530 {
 4531   int previous = linux_is_async_p ();
 4532  
 4533   if (previous != enable)
 4534     {
 4535       sigset_t prev_mask;
 4536  
 4537       /* Block child signals while we create/destroy the pipe, as
 4538      their handler writes to it.  */
 4539       block_child_signals (&prev_mask);
 4540  
 4541       if (enable)
 4542     {
 4543       if (gdb_pipe_cloexec (linux_nat_event_pipe) == -1)
 4544         internal_error (__FILE__, __LINE__,
 4545                 "creating event pipe failed.");
 4546 
 4547       fcntl (linux_nat_event_pipe[0], F_SETFL, O_NONBLOCK);
 4548       fcntl (linux_nat_event_pipe[1], F_SETFL, O_NONBLOCK);
 4549     }
 4550       else
 4551     {
 4552       close (linux_nat_event_pipe[0]);
 4553       close (linux_nat_event_pipe[1]);
 4554       linux_nat_event_pipe[0] = -1;
 4555       linux_nat_event_pipe[1] = -1;
 4556     }
 4557 
 4558       restore_child_signals_mask (&prev_mask);
 4559     }
 4560 
 4561   return previous;
 4562 }

通过 async_file_mark 接口改变事件事件源,也即向 Event Loop 插入 target event。

69 /* Put something (anything, doesn't matter what, or how much) in event 
  270    pipe, so that the select/poll in the event-loop realizes we have 
  271    something to process.  */                                     
  272                                                 
  273 static void                        
  274 async_file_mark (void)                              
  275 {        
  276   int ret;   
  277      
  278   /* It doesn't really matter what the pipe contains, as long we end 
  279      up with something in it.  Might as well flush the previous 
  280      left-overs.  */                            
  281   async_file_flush ();                     
  282                                                                      
  283   do                        
  284     {                                          
  285       ret = write (linux_nat_event_pipe[1], "+", 1);
  286     }                                                              
  287   while (ret == -1 && errno == EINTR);
  288      
  289   /* Ignore EAGAIN.  If the pipe is full, the event loop will already 
  290      be awakened anyway.  */ 
  291 }    
  292      

因此,向 Event Loop 插入 stdin event,只需要通过 add_file_handler接口 向 gdb_notifier 插入事件源和注册回调函数,用户输入命令,即可被 Event Loop 的 poll/select 接口捕捉,并进入回调函数处理用户命令。

向 Event Loop 插入 target event,则需要通过 linux_async_pipe 先创建 async_pipe,再通过 add_file_handler 接口向 gdb_notifier 插入事件源和回调函数,并且通过 async_file_mark 接口使 target event ready,即可被 Event Loop 的 poll/select 接口捕捉,并进入回调函数处理目标程序事件。

回顾了 Event Loop 的机制后,我们在分析下 gdb 主流程。

一、Event Loop 前的准备工作:

启动 gdb 进入 gdb 主函数 captured_main。captured_main 在进入 Event Loop 之前主要是一些初始化动作,这里主要介绍两个跟事件循环强相关动作:

SIGCHLD 信号回调函数注册:

在 captured_main->gdb_init->initialize_all_files->_initialize_linux_nat:

 void
 4970 _initialize_linux_nat (void)
 4971 {
 4972   // ...
 4990   /* Save this mask as the default.  */
 4991   sigprocmask (SIG_SETMASK, NULL, &normal_mask);
 4992 
 4993   /* Install a SIGCHLD handler.  */
 4994   sigchld_action.sa_handler = sigchld_handler;
 4995   sigemptyset (&sigchld_action.sa_mask);
 4996   sigchld_action.sa_flags = SA_RESTART;
 4997 
 4998   /* Make it the default.  */
 4999   sigaction (SIGCHLD, &sigchld_action, NULL);
 5000 
 5001   /* Make sure we don't block SIGCHLD during a sigsuspend.  */
 5002   sigprocmask (SIG_SETMASK, NULL, &suspend_mask);
 5003   sigdelset (&suspend_mask, SIGCHLD);
 5004 
 5005   sigemptyset (&blocked_mask);
 5006   // ....
      }

通过 sigaction 监听 SIGCHLD 信号,并注册回调函数:sigchld_handler。

 4496 /* SIGCHLD handler that serves two purposes: In non-stop/async mode,
 4497    so we notice when any child changes state, and notify the
 4498    event-loop; it allows us to use sigsuspend in linux_nat_wait_1
 4499    above to wait for the arrival of a SIGCHLD.  */
 4500 
 4501 static void
 4502 sigchld_handler (int signo)
 4503 {
 4504   int old_errno = errno;
 4505 
 4506   if (debug_linux_nat)
 4507     ui_file_write_async_safe (gdb_stdlog,
 4508                   "sigchld\n", sizeof ("sigchld\n") - 1);
 4509 
 4510   if (signo == SIGCHLD
 4511       && linux_nat_event_pipe[0] != -1)
 4512     async_file_mark (); /* Let the event loop know that there are
 4513                events to handle.  */
 4514 
 4515   errno = old_errno;
 4516 }

sigchld_handler 即为子进程(tracee/inferior)发送的 SIGCHLD 信号后的回调函数。sigchld_handler 通过 async_file_mark 接口时 target event 就绪,Event Loop 即可捕捉。但这里有个入口条件 linux_nat_event_pipe[0] != -1 ,这个需要linux_async_pipe 接口将 event_pipe 创建后,sigchld_handler 才能向 Event Loop 插入 target event,因此 sigchld_handler 暂时还无法向 Event Loop 插入事件。

插入 stdin_event:

captured_main->set_top_level_interpreter->interp_set->tui_resume->gdb_setup_readline->ui_register_input_event_handler:

  563 void
  564 ui_register_input_event_handler (struct ui *ui)
  565 {
  566   add_file_handler (ui->input_fd, stdin_event_handler, ui);
  567 }

通过 add_file_handler 将 stdin event 加入到 gdb_notifier,并注册回调函数 stdin_event_handler。

二、 Event_Loop 处理

启动 gdb 完成初始化操作,即通过 start_event_loop 进入 while 死循环,通过 poll/select 监听 stdin event 和 target event。在初始化阶段已经将 stdin event 加入到 poll/select 的事件源中。这个时候只要用户输入命令,即可进入 Event Loop 事件处理。

一般地,用户会在此时设置断点,然后执行 run 命令。

Event Loop 捕捉到用户输入,进入 stdin event 回调函数 stdin_event_handler。stdin_event_handler 首先处理断点命令(这里就不展开了),然后处理 run 命令(run_command_1),这里介绍一下主要处理逻辑:

创建子进程 fork inferior

这里主要就是调用 fork 接口创建 inferior,调用 exec 接口执行被调试程序。

启动子进程 startup_inferior:

接着调用 startup_inferior->target_wait->delegate_wait->linux_nat_wait->linux_nat_wait_1->my_waitpid:

  79 int
  80 my_waitpid (int pid, int *status, int flags)
  81 {
  82   int ret, out_errno;
  83 
  84   linux_debug ("my_waitpid (%d, 0x%x)\n", pid, flags);
  85 
  86   do
  87     {
  88       ret = waitpid (pid, status, flags);
  89     }
  90   while (ret == -1 && errno == EINTR);
  91   out_errno = errno;
  92 
  93   linux_debug ("my_waitpid (%d, 0x%x): status(%x), %d\n",
  94            pid, flags, (ret > 0 && status != NULL) ? *status : -1, ret);
  95 
  96   errno = out_errno;
  97   return ret;
  98 }

这里调用 wait_pid 同步接口等待子进程信号(这时候sigchld_handler异步信号处理还没有真正开启)。

接收到子进程信号(SIGTRAP)后,继续执行子进程:startup_inferior->target_resume->delegate_resume->linux_nat_resume:

linux_nat_resume 一方面调用 linux_resume_one_lwp->linux_resume_one_lwp_throw->inf_ptrace_resume 通过 ptrace 接口给子进程发生 CONTINUE 信号让子进程继续运行:

322 /* Resume execution of thread PTID, or all threads if PTID is -1.  If
 323    STEP is nonzero, single-step it.  If SIGNAL is nonzero, give it
 324    that signal.  */
 325 
 326 static void
 327 inf_ptrace_resume (struct target_ops *ops,
 328            ptid_t ptid, int step, enum gdb_signal signal)
 329 {
 330   pid_t pid;
 331   int request;
 332 
 333   if (ptid_equal (minus_one_ptid, ptid))
 334     /* Resume all threads.  Traditionally ptrace() only supports
 335        single-threaded processes, so simply resume the inferior.  */
 336     pid = ptid_get_pid (inferior_ptid);
 337   else
 338     pid = get_ptrace_pid (ptid);
 339 
 340   if (catch_syscall_enabled () > 0)
 341     request = PT_SYSCALL;
 342   else
 343     request = PT_CONTINUE;
 344 
 345   if (step)
 346     {
 347       /* If this system does not support PT_STEP, a higher level
 348          function will have called single_step() to transmute the step
 349          request into a continue request (by setting breakpoints on
 350          all possible successor instructions), so we don't have to
 351          worry about that here.  */
 352       request = PT_STEP;
 353     }
 354 
 355   /* An address of (PTRACE_TYPE_ARG3)1 tells ptrace to continue from
 356      where it was.  If GDB wanted it to start some other way, we have
 357      already written a new program counter value to the child.  */
 358   errno = 0;
 359   ptrace (request, pid, (PTRACE_TYPE_ARG3)1, gdb_signal_to_host (signal));
 360   if (errno != 0)
 361     perror_with_name (("ptrace"));
 362 }
 363 

linux_nat_resume 另一方面通过 target_async->delegate_async->linux_nat_async:

6 static void
 4567 linux_nat_async (struct target_ops *ops, int enable)
 4568 {
 4569   if (enable)
 4570     {
 4571       if (!linux_async_pipe (1))
 4572     {
 4573       add_file_handler (linux_nat_event_pipe[0],
 4574                 handle_target_event, NULL);
 4575       /* There may be pending events to handle.  Tell the event loop
 4576          to poll them.  */
 4577       async_file_mark ();
 4578     }
 4579     }
 4580   else
 4581     {
 4582       delete_file_handler (linux_nat_event_pipe[0]);
 4583       linux_async_pipe (0);
 4584     }
 4585   return;
 4586 }

linux_async_pipe 创建 async_pipe,并调用 add_file_handler 和 async_file_mark 接口向 Event_loop 插入 target event。也就是说这时候向 Event Loop 插入 target event 是执行 run 命令后,在 resume 子进程后主动做的。在此之后就是通过 sigchld_handler 异步方式来向 Event_loop 插入target event 事件。

startup_inferior 使用 while 死循环调用 target_wait,多次进入 linux_nat_wait 和 linux_nat_resume(这里为什么进入多次,可以详细阅读源码) 后退出 while 循环。这时候回到 Event Loop 循环。

断点命中:

当子进程停在断点处后,进入 sigchld_handler 处理,向 Event Loop 插入 target event。Event Loop 中 poll/select 捕捉后进入该事件处理。Event Loop 采用 round-robin 模式捕捉事件,有多种方式相应 target event(感兴趣可以查看源码)。Event Loop 响应 target event,进入 linux_nat_wait 中等待接收和处理子进程发送的 SIGTRAP 等信号。处理完 target event 后调用 add_file_handler 向 Event Loop 插入用户事件,等待用户命令。

如此,通过 Event Loop 机制循环处理用户事件和目标程序事件,直到程序退出,删除事件源,Event Loop 也随之退出。

这次就介绍到这里,更多 gdb 内部实现的介绍尽请期待!

reference

  • [1] http://www.alexonlinux.com/how-debugger-works
  • 15
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值