linux的调试包括了用户层的调试和内核的调试,主要使用的工具就包括gdb和kgdb。
该篇着重介绍linux编程环境中,调试器的工作原理,关键技术,平台依赖部分的区别,较少着墨用法。如果时间充分,会touch一下multi—thread的调试
+++++++++++++++++++++++++++++++++++++++++++++++++++++++
一,debugger 工作基础
看似神秘的debugger其实工作在一些简单的技术上,虽然不同的平台会有一些差异,但其基本原理还是类似的,下面还是以linux为例作一下备忘录(从一个朋友那了解到windows也有一个开源系统在进行,很强大,但真担心生不逢时啊.....)吧啊:
1)ptrace。ptrace提供了一种使父进程得以监视和控制其它进程的方式,它还能够改变子进程中的寄存器和内核映像,因而可以实现断点调试和系统调用的跟踪。使用ptrace,你可以在用户层拦截和修改系统调用(sys call)。
2)ELF format。ELF文件格式提供了很多对debug有用的信息。
3)DWARF format。这是调试信息存放在二进制文件中的一种格式,和ELF不是一个层次的。当我们在编译的时候使用-g参数后,编译器会从源文件中收集大量的信息,例如变量 名、变量类型、变量所在行号、函数名、函数参数、函数的地址范围、行号和地址的对应关系等等,然后按照一种特定的格式写入到编译后的文件中。调试的时候, 调试器便从文件中读取并解析这些信息,以产生人们可读性比较强的信息。简单的说,调试信息就是在机器码和对应的源代码之间建立一座桥梁,大大方便和提高了 调试程序的能力。DWARF的全称是"Debugging With Attributed Record Formats",遵从GNU FDL授权。
4)/proc。linux的proc文件系统中会包含很多关于运行程序的信息。
添加一点对系统调用的解释,也算是完成debug所必需的:操作系统提供了一种标准的服务来让程序员实现对底层硬件和服务的控制(比如文件系统),叫做系统调用(system calls)。当一个程序需要作系统调用的时候,它将相关参数放进系统调用相关的寄存器,然后调用软中断0x80,这个中断就像一个让程序得以接触到内核模式的窗口,程序将参数和系统调用号交给内核,内核来完成系统调用的执行。
二,debugger如何工作
在使用参数为PTRACE_TRACEME或PTRACE_ATTACH的ptrace系统调用建立调试关系之后,交付给目标程序的任何信号(除SIGKILL之外)都将被gdb先行截获,或在远程调试中被gdbserver截获并通知gdb。
gdb中对于信号的处理可以通过handle signal keywords... 来进行行为设置,具体包括:
nostop/stop, noprint/print, noignore/ignor。
gdb因此有机会对信号进行相应处理,并根据信号的属性决定在继续目标程序运行时是否将之前截获的信号实际交付给目标程序或如何处理。
对于gdb中的信号,这里有两点注意:
1),可以使用signal命令,产生一个信号给被调试的程序,但这里和在shell用kill想程序发信号不同,kill发的信号是被gdb截获的,而signal命令发的信号是直接发给被调试程序的。
2),特别注意的是,如果一个任务处于TRACING状态后,当这个进程的接受到任何信号(除了SIGKILL),都会使其停止,哪怕你设置了nostop,这儿从字面来看特别容易产生误会,这也是有时候一个有很多信号交互的程序,在gdb中运行时会变慢。
下面以gdb为例简单介绍一下debugger几个基本behaviour的实现(很多内容来源于刘东(雨丝风片)的文章):
1)run
当我们在gdb中执行run的时候,gdb将完成以下操作:
1,通过fork系统调用创建一个新进程
2,在新创建的子进程中通过ptrace执行TRACEME操作
3,在进程成中通过execv系统调用加载用户指定的可执行文件。
0,命令响应gdb的工作模式是由外部事件来激励的。共有两个外部事件源,一个是标准输入(用户输入的调试命令),一个是远程连接上报的事件。
2) gdbsever
大部分时候我们都要在目标机上进行远程调试,这时候就需要在tagert上使用到gdbsever,在host上我们继续使用gdb连接gdbsever,进行我们的调试。 1,gdb和gdbsever之间的通信数据格式由gdb远程串行协议(remote serial protocol)定义。 2,gdbsever的启动方式也是相当于在target上运行一个进程,并TRACE它,再使用exec执行指定的程序。3) breakpoint
在gdb中,大部分操作都是使用signal来驱动的。信号也是实现断点功能的基础,譬如在X86平台上,向某个地址打入断点,实际上就是往该地址写入断点指令INT3。目标程序运行到这条指令之后就会触发SIGTRAP信号,gdb捕获到这个信号,根据目标程序当前停止位置查询gdb维护的断点链表,若发现在该地址确实存在断点,则可判定为断点命中。
4) 单步
指令级单步了解step之前要了解指令级单步。所谓指令级单步就是gdb控制目标程序只运行一条指令后立即停止。指令级单步是next step等运行类调试命令的基础。
指令级单步分为硬件单步和软件单步两种。硬件单步指CPU架构本身直接支持指令级单步,目标程序可以在运行一条指令之后自动停止。软件单步指CPU不支持指令级单步,需要gdb用软件方法来实现指令级单步。
X86,PPC都支持硬件单步。在该模式中,CPU每执行一条指令,就会产生一个单步异常,通知gdb进行处理。
arm和mips不支持硬件单步。在这种情况下,gdb采用临时的软件断点来模拟单步的方法。在需要执行指令的下一条指令处临时插入一个断点,然后让目标程序继续运行,它会在执行完当前指令之后遇到下一条指令处的临时断点,于是目标程序停止,通知gdb命中断点,gdb再将此断点删除,由此来完成指令级单步的过程。
C代码级单步next命令实现C代码级的单步,其实现机制中引入了step_range,step_range_start和step_range_end的概念。执行next命令时,gdb会计算出当前停止位置的C语句的第一条指令的地址作为step_range_start,然后计算出当前停止位置下一条的C语句的第一条指令的地址作为step_rang_end,然后控制目标程序从当前停止位置开始走指令级单步,直至PC超出step_range为止。
step命令和next命令一样,也是实现C代码级的单步,对于简单语句,step完全等同于next。不同的是,若单步过程中遇到函数调用,step命令将停止在子函数的起始处,而不是将其跨越(无调试信息的子函数除外)。
三,关键技术本来还想做个介绍debug依赖的技术,但网上资源已经很丰富了,而且这些关键技术要做到备忘,需要的篇幅也太长了,这里仅列出参考资源,上面有些内容也来自这些参考资源,感谢这些无私的作者。
ptrace http://blogold.chinaunix.net/u/19651/showart_362901.html
dwarf http://hi.baidu.com/delovery/blog/item/5827f124e46a853b8744f933.html
ELF http://cs.mipt.ru/docs/comp/eng/os/linux/howto/howto_english/elf/elf-howto.html#toc1
我还保存了一个非常好的关于ELF文档,我会将它放到上让大家共享。
multi 的内容还不知道怎么去写抓住关键点,稍后补充吧。