系列文章:
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(¤t->sigmask_lock);
signr = dequeue_signal(¤t->blocked, &info);
spin_unlock_irq(¤t->sigmask_lock);
if ((current->ptrace & PT_PTRACED) && signr != SIGKILL) {
current->exit_code = signr;
current->state = TASK_STOPPED;
notify_parent(current, SIGCHLD);
schedule();
...
}
}
}
总结一下子进程在被被追踪模式时,调用 exec() 后主要做了哪些事情:
- 将 tracee 设置为被追踪模式
- 向 tracee 发送 SIGTRAP 信号
- 处理 SIGTRAP 信号,将 tracee 暂停
- 向 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)