分类: LINUX
2009-02-07 14:02:16
梁肇新在其《编程高手箴言》一书第342页中说:没有 太多人 “玩” Linux就是因为它的工具太原始了。实际上,它的工具原始化的水平就跟很早以前的DOS DEBUG差不多,不过Linux后来也能列出哪行源代码出错了。但是所有的单步等都是通过输入命令,例如输入T就弹出寄存器的状态,这种方式还是非常原 始。在这上面开发一个软件很困难。如果没有集成调试环境工具,一个像样的软件就很难开发。因为你要做很多工作的确认,要调试。如果没有集成工具,只能做个 PRINT让它把情况和状态打印出来,或者做个OUT把它写到一个文件里面测这种方法坐垫小程序还可以,如果生成大的程序就很难了。
我想,已流于世的众多优秀自由软件,也是梁所诋毁的Linux程序开发工具的产品,这足以证实梁信誓旦旦所下的结论是完全错误的。有时也真难以想 象,具有这种软件观和世界观的程序员究竟如何做到“我就是程序,程序就是我”的境界的。当初,我从Windows逃离,正是因为我对M$所提供的开发工具 极度失望,而对奇怪的Linux程序及其奇怪的开发工具极度好奇所使然。本文打算详细介绍一下这些奇怪的软件开发工具之一,就是GDB,我们来看看它是不 是真的如梁所断言的那样不堪,那样原始!而且在写这篇文章之时,我也是初次使用GDB。所以,这应该是一篇适合和我一样的初哥阅读的教程。但愿我能把它写 好。多给些支持与鼓励呗?
要让程序可调试,程序执行文件中必须包含调试器必需的调试信息。不过默认情况下,GCC在编译时不会将调试信息插入到程序二进制代码中,因为这样会增加可执行文件的大小。
在编译时生成调试信息,可使用GCC的-g或者-ggdb选项,其中-g选项可以生成标准的调试信息,而-ggdb选项仅生成支持gdb调试器的调 试信息。GCC在使用-g和-ggdb选项产生调试信息时,采用了分级的思路。开发人员可以通过在-g选项后附加数字1、2或3来指定在代码中加入调试信 息的多少。默认的级别是2(-g2),产生的调试信息包括扩展的符号表、行号、局部或外部变量信息。级别3(-g3)包含级别2中的所有调试信息,以及源 代码中定义的宏。级别1(-g1)不包含局部变量和与行号有关的调试信息,因此只能够用于回溯跟踪和堆栈转储之用。回溯跟踪指的是监视程序在运行过程中的 函数调用历史,堆栈转储则是一种以原始的十六进制格式保存程序执行环境的方法,两者都是经常用到的调试手段,并在后面会详细讲述。
如果你还不会使用gcc,可以参考gcc入门。
需要注意的是,使用任何一个调试选项都会使最终生成的二进制文件的大小急剧增加,同时增加程序在执行时的开销,因此调试选项通常仅在软件的开发和调 试阶段使用。但事实上Linux中的许多软件在测试版本甚至最终发行版本中仍然使用了调试选项来进行编译,这样做的目的是鼓励用户在发现问题时自己进行调 试解决,如果你不堪程序调试版本的庞大笨重,可以对其所附源代码重新优化编译。这是Linux的一个显著特色。而在Windows下,这样的特色几乎是不 可能出现的,因为软件开发商认为用户对其销售的软件进行反汇编或调试是偷窃行为。天底下怎么会有这样一种逻辑呢?我买了一样东西,结果厂家说我把它打开是 要触犯法律的。
Linux下最常用的调试器是gdb(好像是gnu debug的简写)。梁肇新说的没错,gdb是采用命令行交互方式(CUI)进行程序调试的,但它并不原始。用gdb调试程序,首先要做的是将被调试程序 置入gdb为其创建的”沙箱“中。这个沙箱允许被调试程序访问内存、寄存器、和输入/输出设备,不过这些操作都在gdb的控制之下,并且调试器能够提供信 息来显示程序如何访问及何时访问这些设备。
在程序执行过程的任何位置,gdb都能够停止程序并且指出源代码中相应的停止位置。当然,为了实现这一功能,gdb必须了解被调试程序的源代码,以 及从哪些源代码行生成了哪些计算机指令码。因此,gdb需要被调试程序提供一些额外信息以便识别这些元素,将其与源代码进行位置匹配,这也就是为什么要在 编译程序的时候使用产生调试信息选项的原因了。
程序在执行过程中停止时,gdb能够显示与被调试程序相关的任何内存区域或寄存器值,这使得程序员在被调试程序运行时能够了解程序内部运行状况,对 程序进行排错。另外,gdb向程序员提供了在程序运行时改变程序变量值的途径,这使得程序员在程序运行时改动程序并且查看这些改动影响程序的输出,从而节 省了源代码修改、编译到运行——这个过程的时间。
gdb能够高效完成现代C/C++调试器所能完成的任何任务,并且已经在各种项目中广为使用,因此任何人没有任何理由可以指责它原始,或者无法胜任大项目的调试工作。
在程序莫明其妙地当掉之时,操作系统就会把程序当掉时的内存内容 dump 出来(现在通常是写在一个叫 core 的 file 里面),以让我们或是 debugger 做为参考,这种行为名曰core dump。很多人将其翻译为“内核转储”,我觉得不通,应该叫“内存转储”,下文我将以“内存转储文件”这个名词来取代程序崩溃文件。
可能我们用的这个Linux发行版为了节约硬盘空间,默认状态下会将内存转储功能关闭了,若将其打开,需要在shell配置文件(如bash的.bash_profile)中添加:
ulimit -c unlimited |
默认状态下,程序崩溃时,生成的内存转储文件位于该程序所在目录下,且名为"core.pid",pid时崩溃程序的进程号。我们可以通过修改位于 /proc/sys/kernel目录下的core_uses_pid和corecore_pattern文件,来控制内存转储文件的存放位置和命名方 式。
我建议的设置时去掉内存转储文件的pid,因为它只会增加字符输入工作,别无它用。另外,为了便于管理,建议将内存转储文件(去掉pid后,名为 core)放在一个指定目录(譬如corefiles)下。这样在使用gdb进行调试时,只需要“gdb 程序名 corefiles/core"即可。也许你觉得将内存转储文件名设置与你的程序名一致会更好一些。那么,怎样才可以实现呢,如下:
上面,第一句是设置pid无效,如果将"0"设置为"1"就是让它起效。第二句中,/tmep/corefiles是内存转储文件所在目录,这里要 特别注意必须首先创建这样一个目录,然后才可以在core_pattern中进行设置,不然总是无法成功的。%e是将崩溃程序名作为内存转储文件名。最 后,也许你会觉得每次在使用内存转储文件都要输入其所在目录,这样很繁琐,你可以为该目录做一个环境变量,譬如$core,可以降低输入量。shell配 置文件设置完毕,在shell下,"source 配置文件名",即可让其起效。
如果,想再多了解一下内存转储文件的知识,可参考(如果该链接无效,可以google关键字"Controlling core files“)。
内存转储功能比较有用的是使用命令”gdb 被调试程序 内存转储文件”, 进入gdb后键入bt或where命令, 就可以显示出出程序是在哪一行当掉的, 还有在当掉时在哪个函数里, 这个函数是被哪个函数所调用的, 而这个调用函数又是被哪个函数所调用的.... 一直到 main()。根据这个信息, 可以找出五六成的 bug,不过也有例外的情况,比如本文所举的例子中有一个就属于例外。
gdb的基本语法是:
gdb [选项] [被调试程序文件 [内存转储文件]] |
如果在gdb命令中指定了程序崩溃文件,可以实现不需要调试,就可以基本上分析出程序崩溃的原因了。
下面为了演示一下内存转储功能,我们先做一个临时的小程序,这个程序故意调用了abort()函数,让程序意外中止,从而可生成内存转储文件。现在实例感受一下吧!
下面是这个程序与gdb的交互过程:
$ gcc -g tmp_prog.c -o tmp_prog |
上面过程中,gdb的-q选项可以在运行gdb时禁止gdb输出长长的软件许可说明之类的信息,如果你想阅读一下这些信息,去掉-q选项即可。另 外,还有一个警告:Can't read pathname for load map,我没有弄明白是甚么问题,如果你知道,告诉我吧。
gdb运行后,我们键入了bt命令,其全称是backtrace,因为gdb支持独一无二的命令缩写模式,所以可以直接用bt即可。这个命令可以输
出堆栈中跟踪的函数调用记录,就是按最近调用的函数在前,顺序排列的函数列表。请注意,上面的bt输出结果显示有三次recurse调用,看看我们的源代
码就明白了,因为recurse函数本身就是递归三次的。自上而下,其第一次调用就是程序运行中最后一次调用。每个函数调用在堆栈中成为一帧。
(gdb) frame 3 |
如果想查看当前帧所对应的源代码,可以使用list命令,如下:
(gdb) frame 3 |
初次执行list命令后,如果继续回车,则会继续显示其后续代码。这是因为gdb将回车键视为是上一次命令的重复。这样做的好处是降低你重复输入命令的疲累。
gdb的退出命令是quit,由于其支持缩写模式,输入q,即可退出。