
我叫GDB是一个调试器程序员通过我可以调试他们编写的软件分析其中的bug。作为一个调试器调试分析是我的看家本领像是给目标进程设置断点或者让它单步执行又或是查看进程中的变量、内存数据、CPU的寄存等等操作我都手到擒来。你只要输入对应的命令我就能帮助你调试你的程序。我之所以有这些本事都得归功于一个强大的系统函数它的名字叫ptrace。不管是开始调试进程还是下断点、读写进程数据、读写寄存器我都是通过这个函数来进行要是没了它我可就废了。它的第一个参数是一个枚举型的变量表示要执行的操作我支持的调试命令很多都是靠它来实现的你可以通过我来启动一个新的进程调试我会使用fork创建出一个新的子进程然后在子进程中通过execv来执行你指定的程序。不过在执行你的程序之前我会在子进程中调用ptrace函数然后指定第一个参数为PTRACE_TRACEME这样一来我就能监控子进程中发生的事情了也才能对你指定的程序进行调试。你也可以让我attach到一个已经运行的进程分析这样的话我直接调用ptrace函数并且指定第一个参数为PTRACE_ATTACH就可以了然后我就会变成那个进程的父进程。具体要选择哪种方式来调试这就看你的需要了。不过不管哪种方式最终我都会“接管”被调试的进程它里面发生的各种信号事件我都能得到通知方便我对它进行调试操作。软件断点作为一个调试器最常用的功能就是给程序下断点了。你可以通过break命令告诉我你要在程序的哪个位置添加断点。当我收到你的命令之后我会偷偷把被调试进程中那个位置的指令修改为一个0xCC这是一条特殊指令的CPU机器码——int 3是x86架构CPU专门用来支持调试的指令。我的这个修改是偷偷进行的你如果通过我来查看被调试进程的内存数据或者在反汇编窗口查看那里的指令会发现跟之前一样这其实是我使的障眼法让你看起来还是原来的数据实际上已经被我修改过了你要是不信你可以另外写个程序来查看那里的数据内容看看我说的是不是真的。一旦被调试的进程运行到那个位置CPU执行这条特殊的指令时会陷入内核态然后取出中断描述符表IDT中的3号表项中的处理函数来执行。IDT中的内容操作系统一启动早就安排好了所以系统内核会拿到CPU的执行权随后内核会发送一个SIGTRAP信号给到被调试的进程。而因为我的存在这个信号会被我截获我收到以后会检查一下是不是程序员之前下的断点如果是的话就会显示断点触发了然后等待程序员的下一步指示。在没有下一步指示之前被调试的进程都不会进入就绪队列被调度执行。直到你使用continue命令告诉我继续我再偷偷把替换成int 3的指令恢复然后我再次调用ptrace函数告诉操作系统让它继续运行。这就是我给程序下断点的秘密。不知道你有没有发现一个问题当我把替换的指令恢复后让它继续运行以后就再也不会中断在这里了可程序员并没有撤销这个断点而是希望每次执行到这里都能中断这可怎么办呢我有一个非常巧妙的办法就是让它单步执行只执行一条指令然后又会中断到我这里但这时候我并不会通知程序员而仅仅是把刚才恢复的断点又给打上替换指令然后就继续运行。这一切都发生的神不知鬼不觉程序员根本察觉不到。单步调试说到单步执行应该算是程序员调试程序的时候除了下断点之外最常见的操作了每一次只让被调试的进程运行一条指令这样方便跟踪排查问题。你可能很好奇我是如何让它单步执行的呢单步执行的实现可比下断点简单多了我不用去修改被调试进程内存中的指令只需要调用ptrace函数传递一个PTRACE_SINGLESTEP参数就行了操作系统会自动把它设置为单步执行的模式。我也很好奇操作系统是怎么办到的就去打听了一下。原来x86架构CPU有一个标志寄存器名叫eflags它里面不止包含了程序运行的一些状态还有一些工作模式的设定。其中就有一个TF标记用来告诉CPU进入单步执行模式只要把这个标记为设置为1CPU每执行一条指令就会触发一次调试异常调试异常的向量号是1所以触发的时候都会取出IDT中的1号表项中的处理函数来执行。接下来的事情就跟命中断点差不多了我会截获到内核发给被调试进程的SIGTRAP信号然后等待程序员的下一步指令。如果你继续进行单步调试那我便继续重复这个过程。如果你有程序的源代码你还可以进行源码级别的单步调试不过这里的单步就指的是源代码中的一行了。这种情况下要稍微麻烦一点我还要分析出每一行代码对应的指令有哪些然后用上面说的单步执行指令的方法一条条指令快速掠过直到这一行代码对应的指令都执行完成。内存断点有的时候直接给程序中代码的位置下断点并不能包治百病。比如程序员发现某个内存地址的内容老是莫名其妙被修改想知道到底是哪个函数干的这时候连地址都没有根本没法下断点。单步执行也不行那么多条指令得执行到猴年马月去才能找到不用担心我可以帮你解决这个烦恼。你可以通过watch命令告诉我让我监视被调试进程中某个内存地址的数据变化一旦发现被修改我都会把它给停下来报告给你。猜猜我是如何做到的呢我可以用单步执行的方式每执行一步就检查一下内容有没有没修改一旦发现就停下来通知你们程序员。不过这种方式实在是太麻烦了会严重拖垮被调试进程的性能。好在x86架构的CPU提供了硬件断点的能力帮我解决了大问题。在x86架构CPU的内部内置了一组调试寄存器从DR0到DR7总共8个。通过在DR0-DR3中设置要监控的内存地址然后在DR7中设置要监控的模式是读还是写剩下的交给CPU就好了。CPU执行的时候一旦发现有符合调试寄存器中设置的情况发生时就会产生调试异常然后取出IDT中的1号表项中的处理函数来执行接下来的事情就跟单步调试产生的异常差不多了。CPU内部依靠硬件电路来完成监控可比我们软件一条一条的检查快多了现在你不止可以使用watch命令来监控内存被修改还可以使用rwatch、awatch命令