GDB是一个基于传统命令行形式实现的控制台程序(其界面如图2.3所示)。虽然在GDB的长期进化中,GDB接受了大量优秀的图形用户界面(GUI)(如图2.4和图2.5所示),但Turbo Debugger(TD)风格之类的交互调试目前在UNIX世界中依然不流行。这通常都是从Windows平台移植程序的人的致命错误,这些人的思维方式不可避免地贴上了“M$”标签。这里简单的模拟并不适用:如果TD是长凳工具,那么GDB就是具有编程控制的车床。你迟早都会喜欢它的。
为了在源代码级进行调试,程序必须在编译时包含调试信息。在GCC中,使用命令行选项 –g 就能实现这一点。如果没有调试信息,GDB将在反汇编级上进行调试。
被调试文件的文件名通常都是通过命令行来传递的,其格式如下:gdb文件名。如果要调试一个活动的进程,请在命令行中指定其进程标志。如果要调试内核转储,请使用命令行选项 –core==内核名。这三个参数可以同时使用,还可以使用命令行命令target来在它们之间进行切换。命令行命令target exec切换到被调试的文件,target child切换到附加的进程,target core切换到内核转储。可选用的命令行参数-q用于禁止显示版权信息。
图2.3 传统的GDB界面
将应用程序加载到调试器之后,必须设置断点。请使用break命令来设置断点(其简称是b)。例如,命令b main将断点设在C语言的main函数上,而b _start将断点设在可执行和链接格式(ELF)文件的入口点(有的文件的入口点具有不同的名称)。也可以将断点设置在任何地址上,比如,b *0x8048424或者b *$eax。寄存器的名称要用小写字母,并以美元符号为前缀。GDB理解两种“跨平台”的寄存器:$pc用于命令指针,而$sp用于堆栈指针。但是请记住,程序被装载到调试器之后并不能立即看到寄存器。只有用命令run(r)启动被调试的进程后,寄存器才会显示出来。
调试器执行决定应该设置软件断点还是硬件断点。最好不要干涉这一过程。并非所有的调试器版本都支持强制设置硬件断点的的命令hbreak。例如,我所用的版本就不支持该命令。用于数据的断点在GDB中称为观察点。当地址addr处的数据被改变时,命令watch addr将调用调试器。当读或写操作访问地址addr时,命令awatch addr将调用调试器。当读操作访问地址addr时,命令rwatch addr将调用调试器,但并非所有版本的调试器都支持这一命令。使用命令info break可以查看所有已设置的断点和观察点。命令clear删除所有的断点,而命令clear addr删除所有设置在给定的函数、地址或者行号上的断点。诸如enable和disable之类的命令用于暂时地允许和禁止断点。还有一种常规命令的高级语法用于处理断点,可以在文档中找到该语法的详细描述。命令continue(c)用于继续执行被命令b所中断的程序,即从断点继续执行。
图2.4 调试器DDD给GDB提供的图形用户界面
图2.5 加在GDB上的另一种图形用户界面
命令next N(n N)执行下面的N行代码,会越过嵌套的函数,而命令step N(s N)会进入嵌套的函数。如果没有指定N,则只执行一行代码。命令nexti和stepi的功能与它们相似。不同的是,这两条命令用于机器指令,而不是源代码的行数。它们通常与命令display/i $pc(x/i $pc)一起使用,这条命令要求调试器显示当前的机器指令。每一次会话只需调用一次该命令就足够了。
命令jump addr将控制转到程序的任意位置,而命令call addr/fname调用参数fname所指定的函数。即使是SoftIce也没有提供这一命令!它是非常有用的,也是用户常常要求提供的命令。另外几条很有用的命令包括finish,它用于在退出当前函数之前中断执行(它等效于SoftIce的命令P RET),以及命令until addr(u addr),它用于继续执行直到到达指定的位置为止。如果启动命令时没带参数,则当下一条命令到达时停止执行(这对循环特别重要)。命令return立即取消函数的执行。
命令print expression(p expression)用于输出指定表达式(例如p 1+2)的值、指定变量的内容(p my_var)、指定寄存器的内容(p *$eax)或者内存单元的内容(p *0x8048424,p *$eax)。如果需要输出几个单元,就使用命令x/Nh addr,其中N是要输出的单元的数量。此时并不需要在地址前面加上星号。
命令info register(i r)输出所有可用寄存器的值。修改内存单元和寄存器的内容可用命令set来实现。例如,set $eax = 0将0写入寄存器eax。命令set var may_var = $ecx将寄存器ecx的值赋给变量my_var,而命令set {unsigned char*} 0x8048424 = 0xCC将数字0xCC写入字节地址。命令disassemble _addr_from _addr_to以反汇编清单的形式输出内存的内容,表示的格式由命令set disassembly-flavor确定。
命令info frame,info args和info local分别显示堆栈帧,函数参数和局部变量的内容。为了切换到父函数的帧,使用命令frame N。命令backtrace(bt)所做的事情与Windows调试器的命令call stack相同。当研究内核转储时,这是不可或缺的。
用GDB进行工作的一个完整过程大致如下:将程序装入调试器,执行命令b main(如果不成功,则使用b _start),然后执行命令r。用命令n或者s一步一步地调试程序。如果需要,指定x/i $pc使GDB显示当前所执行的代码。执行命令quit(q)以退出调试器。可在文档中找到所有其他命令的详细描述。希望这里列出的GDB命令简明指南能对你有所帮助,而不至于迷失在信息丛林中。
比较UNIX和Windows的调试器,马上就能发现:后者比前者落后许多,而且能够证明Windows调试器并不是针对专业人士的。三维显示的按钮、可以缩放的图标、弹出式菜单和其他过于花哨的东西,确实很好看。然而,需要不停地按<F10>键,你不觉得厌烦吗?在GDB中,通过编写宏(或者使用已有的宏)可以使事情变得方便许多,因为可以编程实现的每一件事在此都已经实现编程,而且可以免费使用。
UNIX中的调试工具功能强大而且使用方便。除了GDB外,还有一些其他的产品。惟一缺少的东西是可以基于没有符号信息和源代码的二进制文件工作的高质量系统级内核调试器。有一段时期UNIX存在多个平台,而且需要进行不同平台之间的移植,这在UNIX中留下了一个忧郁记号,以及实现可移植性和跨平台支持的愿望。在这种条件下,破解当然是很困难的!不过,源程序的可用性可以减少一些困难。
2.1.4 追踪系统调用
追踪系统调用打开了一扇真实窗口,透过它可以了解正被研究的程序的内幕。它可以显示正被调用的函数的名称,它们的参数和返回值。对于编程新手来说,有一个共同的问题是常常忘记“额外的”错误检查,而调试器不是查找这类错误的最佳工具。可以使用truss或者ktrace之类的标准工具,或者使用商业代码分析程序的自由软件版本。
清单2.6显示了使用truss所获得的日志。该程序试图打开名称为my_good_file的文件,但是找不到该文件,从而导致程序出错。这是最简单的案例。然而,一个著名的规则认为全部开发时间的99%花在寻找错误上,而这些错误都是不值得查找的!
清单2.6 使用truss查找错误
2.1.5 相关链接
r GDB内幕(http://gnuarm.org/pdf/gdbint.pdf):一本关于GDB内幕的极好的指南。当需要改善源程序时,它非常有用。
r 用ptrace追踪进程(http://linuxgazette.net/issue81/sandeep.html):一篇关于在Linux中使用最简单的追踪程序中的例子来进行追踪的论文(在FreeBSD中的情形完全不同)。
r 在源程序中修正漏洞(http://www.linux-mag.com/2004-04/code_01.html):一篇关于利用源代码分析进行早期查错的论文。
r 使用CTrace库(http://ctrace.sourceforge.net):一篇关于使用该库来调试多线程应用程序的论文。
r 内核和用户空间调试技术(德语文章)(http://www.unfug.org/files/debugging.pdf):专门论述调试,描述少为人知的GDB结构细节的论文集。
r ELF/INTEL系统的逆向工程(法语文章)(http://www.sstic.org/SSTIC03/arcticles/ SSTIC03-Vanegue_roy-Reverse_Intel_ELF.pdf):关于在没有源程序时在i386平台上研究和调试ELF文件的论文。