gdb这个debug工具,应该无人不知吧。服务器开发和客户端开发人员应该都或多或少的通过这个工具排查问题。但有没有想过它的实现原理,为什么打断点能够在断点处停止运行?为什么能查看cpu寄存器?等等。我们就带着这些疑问接着往下看。
知识铺垫
我们都听说过系统调用,这里简单的解释下系统调用的含义。操作系统提供了一种标准的服务来让程序员实现对底层的硬件和服务的控制,比如申请、释放内存,打开、关闭文件等等,这就叫做系统调用(system calls)。
系统调用的过程比较复杂,大致流程是这样的:将相关参数放进系统调用的相关寄存器中,然后调用软中断(0x80),这个中断作用就是让一个程序从用户态陷入到内核态执行,程序将参数和系统调用号交给内核,内核来执行。
看完这个知识铺垫,估计心里猜测跟系统调用有关系了。确实不是特别高大上的技术,只是利用了linux提供的非常优雅的方式:ptrace系统调用,用man查看以下这个系统调用。ptrace可以让父进程观察和控制其子进程的检查、执行,改变其寄存器和内存的内容。主要的作用就是大家常用的打断点的功能了,还有一个功能是打印系统调用的轨迹信息。
我们先来看下这个ptrace函数的原型:
#include <sys/ptrace.h> long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);
第一个参数决定了ptrace的行为与其他参数的使用方法。可取的值有:
PTRACE_ME PTRACE_PEEKTEXT PTRACE_PEEKDATA PTRACE_PEEKUSER PTRACE_POKETEXT PTRACE_POKEDATA PTRACE_POKEUSER PTRACE_GETREGS PTRACE_GETFPREGS, PTRACE_SETREGS PTRACE_SETFPREGS PTRACE_CONT PTRACE_SYSCALL, PTRACE_SINGLESTEP PTRACE_DETACH
下面对常用的几种模式进行说明。
详细可参考《http://man7.org/linux/man-pages/man2/ptrace.2.html》
gdb三种调试方式
看完ptrace使用方法,再来看看gdb的三种调试方式。
断点的实现过程
再回到刚文章开头的问题,断点是怎样实现的呢?我从上面文字中知道gdb调试的实现都是建立在信号的基础上的。在使用参数为PTRACE_TRACEME或PTRACE_ATTACH的ptrace系统调用建立调试关系后,交付给目标程序的任何信号首先都会被gdb截获。
因此gdb可以先行对信号进行相应处理,并根据信号的属性决定是否要将信号交付给目标程序。
当用breakpoint 设置一个断点后,gdb会在=找到该位置对应的具体地址,然后向该地址写入断点指令INT3,即0xCC。
目标程序运行到这条指令时,就会触发SIGTRAP信号,gdb会首先捕获到这个信号。然后根据目标程序当前停止的位置在gdb维护的断点链表中查询,若存在,则可判定为命中断点。