how to debug with some tips or tools

调试-linux程序设计

clip_image001

错误查找及纠正

有几种原因会造成程序的缺陷,针对每种原因,都有一些建议的方法来查找和纠正。比如功能定义错误,设计规划错误和代码编写错误

clip_image003

程序调试的5个阶段

clip_image005

使用printf来排错

       相信这个方法是N多人使用的方法

使用-Wall选项来排错

       在使用gcc编译的时候加上-Wall选项,可以提前发现很多的warnings

注释法

       将怀疑有错的程序块注释起来,查看一下结果,如此反复即可。

使用内置宏__FILE__

       灵活使用内置宏__LINE__,__FILE__,__DATE____TIME可以很快速地定位问题所在。

使用assert断言来预防错误的发生

       诸如分母不能为0的情况。

专业级别的gdb调试

       关于gdb的调试,参考:

clip_image006 clip_image008 clip_image010

静态分析调试工具

ctags

       参考:http://ctags.sourceforge.net/

       相信使用过vim的童鞋都晓得这个工具,ctagsGenerate tag files for source code)是vim下方便代码阅读的工具。尽管ctags也可以支持其它编辑器,但是它正式支持的只有VIM。并且VIM中已经默认安装了Ctags,它可以帮助程序员很容易地浏览源代码。

ctags 最先是用来生成C代码的tags文件,后来扩展成可以生成各类语言的tags, 有些语言也有专有的tags生成工具(比如javajtags, python ptags)

clip_image012

cxref

参考:http://www.gedanken.demon.co.uk/cxref/

Cxref是一款在LaTeXHTMLRTFSGML中将C代码生成文件的程序,包括从C程序源代码的交叉引用。它用于ANSI C,包含大多数gcc扩展,可以从代码中的注释进行生成适当格式的程序文档,交叉引用来自于代码本身并不需要额外的工作。

clip_image014

cflow

参考:http://www.gnu.org/software/cflow/

GNU cflow analyzes a collection of C source files and prints a graph, charting control flow within the program.

GNU cflow is able to produce both direct and inverted flowgraphs for C sources. Optionally a cross-reference listing can be generated. Two output formats are implemented: POSIX and GNU (extended).

Input files can optionally be preprocessed before analyzing.

The package also provides Emacs major mode for examining the produced flowcharts in Emacs.

clip_image016

动态分析调试工具

gprof

gprofGNU profiler工具。可以显示程序运行的“flat profile”,包括每个函数的调用次数,每个函数消耗的处理器时间。也可以显示调用图,包括函数的调用关系,每个函数调用花费了多少时间。还可以显示注释的源代码,是程序源代码的一个复本,标记有程序中每行代码的执行次数。

gprof编译程序


在编译或链接源程序的时候在编译器的命令行参数中加入“-pg”选项,编译时编译器会自动在目标代码中插入用于性能测试的代码片断,这些代码在程序在运行时采集并记录函数的调用关系和调用次数,以及采集并记录函数自身执行时间和子函数的调用时间,程序运行结束后,会在程序退出的路径下生成一个gmon.out文件。这个文件就是记录并保存下来的监控数据。可以通过命令行方式的gprof或图形化的Kprof来解读这些数据并对程序的性能进行分析。另外,如果想查看库函数的profiling,需要在编译是再加入“-lc_p”编译参数代替“-lc”编译参数,这样程序会链接libc_p.a库,才可以产生库函数的profiling信息。如果想执行一行一行的profiling,还需要加入“-g”编译参数。
例如如下命令行:
gcc -Wall -g -pg -lc_p example.c -o example

clip_image018

工具lint

lint是最著名的C语言工具之一,是由贝尔实验室SteveJohnson1979PCC(PortableC Compiler)基础上开发的静态代码分析,一般由UNIX系统提供。与大多数C语言编译器相比,lint可以对程序进行更加广泛的错误分析,是一种更加严密的编译工具。最初,lint这个工具用来扫描C源文件并对源程序中不可移植的代码提出警告。但是现在大多数lint实用程序已经变得更加严密,它不但可以检查出可移植性问题,而且可以检查出那些虽然可移植并且完全合乎语法但却很可能是错误的特性。

随着历史的推移,Lint后来形成了一系列的工具,包括PC-Lint/FlexeLint(Gimpel),LintPlus(Cleanscape)以及Splint

clip_image019

内存调试工具ElectricFence

clip_image020

参考:http://directory.fsf.org/wiki/Electric_Fence

       实际运行环境中经常比较困扰的问题是内存产生轻微越界,但是并没有立刻产生问题, 给调查带来很大难度。

如果程序问题调查没有头绪,可以借助 Electric Fence (简称 Efence )工具检查内存问题并产生 CORE 文件,比较精确的定位问题场所。

       在发生如下问题的情况下此工具会转储 CORE

1 )动态分配内存边界外的读写操作

2 )访问已经 free 的内存

3 free 的地址不是 malloc() 分配的

Valgrind

       参考:http://valgrind.org/

Valgrind是一款用于内存调试、内存泄漏检测以及性能分析的软件开发工具。

clip_image021

using GDB/DDD/Eclipse for other languages

对其他语言使用GDB/DDD/Eclipse

       人们一般都知道GDBDDDC/C++程序的调试器,但是他们也可以用于其他语言的开发。Eclipse最初是为Java开发设计的。

       不管是CC++JavaPythonPerl还是其他可以使用这些工具的语言或调试器,如果能够使用相同的调试界面,那将是相当棒的。DDD就适用于所有这次语言

       这些工具的多语言功能是如何实现的:

l  虽然最初GDB是为了C/c++的调试器创建的,但是后来使用GNU的人也提供了一款Java的编译器GCJ

l  DDD本身不是调试器,而是GUI可以通过它来想底层调试器发布命令,对于C/c++,该底层调试器通常是GDB,然后,DDD经常可用来作为其他语言特有的调试器的前端;

l  Eclipse也只是前端,各种语言的插件赋予了它管理用那些语言编写代码的开发与调试能力。

 

DDD可以直接与Java Development KitJDB调试器结合起来使用,例如:

$ddd –jdb test.java

       Perl有它自己的内置调试器,可以通过-d选项调用:

$perl –d test.pl

       Python的基本调试器时PDB,这是一个基于文本的工具,它的有用性通过使用DDD作为GUI前端而得到大大增强。也可以通过ddd –pydb来使用。

调试SWIG代码

       SWIGSimplified Wrapper and Interface Generator)是一种流行的开源工具,用来将JavaPerlPython和若干其他解释语言与C/C++结合。大部分Linux分布式系统都包括SWIG,它允许使用解释语言编写应用程序的大部分代码,并与程序员用C/C++编写的特定部分结合,从而增强性能

汇编语言

       GDBDDD在调试汇编语言代码时也机器有用。

other tools

其他工具

       精通调试代码并不是说学会使用GDB这样的调试器就行了这只是开端。

       为了增强调试技能,初学者程序员最好学会其中的集中调试工具,了解那种工具适合于调试那种程序错误,并识别发生程序错误时使用其中那个工具可以节省时间和精力。

充分利用文本编辑器

       最好的调试方法是一开始就不要有编程错误,而简单地充分利用一种支持编程的编辑器就可以达到这个效果。因为:

l  精通强大的编辑器可以缩短编写代码所需的时就爱你,具有自动缩排、单词补充和全文代码查询等特殊功能的编辑器对程序员是非常有利的;

l  优秀的编辑器确实可以帮到编码者在编写代码时捕获某些类型的程序错误。

 

MarkMakefile中可以使用patsubst关键字来进行文本查找和替换命令。

 

       vim中调用make非常方便,不需要退出编辑器,直接:make 即可,同时vim可以捕获编译器发出的所有消息,编辑器理解GCC输出内容的语法,知道何时发生编辑器警告或错误。发生错误时可以直接定位到第一个错误,然后使用:cnext或者:cprevious来显示下一个或上一个错误或者警告。

       Vim中可以使用K来查询man页面中的函数

充分利用编译器

       如果说编辑器是对抗程序错误的第一个武器,那么编译器就是第二个武器了,所有编译器都有能力扫描代码并查找常见错误,但是通常必须通过调用适当的选项来启用这种错误检查。

       对于GCC而言,如果不适用-Wall,就几乎没有必要使用GCC了,所以任何编译最好都加上-Wall选项。

C语言中的错误报告

       C语言中的错误报告是使用名为errno的老式机制完成的。系统与库调用的失败通常是由于设置了名为errno的全局定义整数变量,在大多数GNU/Linux系统上,error是在/usr/include/errno.h上声明的,因此,只要包含了这个头文件,就不必在源代码中声明extern int errno

       当一个系统或库调用失败时,它将errno设置为一个指示失败类型的值。检查errno的值,并采取适当的动作可以判断错误类型。比如可以使用函数perror或者strerror

       errno可以通过任何库函数或系统调用设置,无论它是成功还是失败。因为即便成功的函数调用能够设置errno,让人不能依赖errno告诉你是否发生了错误。因此使用errno最安全的方式为:

l  执行对库或者系统函数的调用;

l  使用函数的返回值判断时候发生了某个错误;

l  如果发生了某个错误,使用errno确定为什么发生这个错误。

库函数和系统调用的区别

       库函数是更高级别的,完全在用户空间里运行,并未程序员提供了更方便的做实际工作的函数接口。

       系统调用代表用户以内核模式工作,由操作系统本身的内核提供。

       库函数printf看上去类似于一般输出函数,但是它实际上只是格式化你提供给字符串的数据,并用低级系统调用write编写字符串数据,然后将数据发送到一个与终端的标准输出关联的文件中。

更好地使用straceltrace

       strace跟踪系统调用而ltrace跟踪库调用,这两个使用程序,有时比深度调试代码还快。

静态代码检查器:lint与其衍生

       静态代码检查器:扫描代码的工具,不编译代码,仅仅警告错误、可能的错误和与严格C语言编码标准的差距。

       衍生版本为splint:该软件的目标是帮助编写大部分有防御性、安全和尽可能少出错的程序,当然,改程序对组成有效代码的内容非常挑剔。

       很多程序员将splint没有报告警告看做一种极大的荣誉。当出现这种情况是,代码被声明为无lint的。

调试动态分配的内存

       动态分配的内存(Dynamically Allocated Memory, DAM)是程序用malloccalloc这样的函数从堆中请求的内存。

       查找DAM问题分麻烦,分为以下几类:

l  没有释放动态分配的内存;内存泄漏

l  malloc的调用失败;

l  DAM段之外的地址执行读写操作;访问错误

l  释放DAM段之后对DAM区域中的内存进行读写操作;访问错误

l  对动态内存的同一段调用两次free重复释放double free

Electric Fence

       Bruce Perens1988年写的,当EFence链接到代码中时,导致程序在发生下列情况之一时立即发生段错误并转出核心:

l  DAM边界之外执行读写操作;

l  对已经释放的DAM执行读写操作;

l  对没有指向malloc分配的DAM的指针执行free,包括重复释放的特殊情况。

比如,

gcc –g3 –Wall –std=c99 test.c –o test_with_efence outOfBound_with_efence  –lefence

gcc –g3 –Wall –std=c99 test.c –o test_with_efence outOfBound_without_efence  -lefence

可以对一个内存泄露但是没有编译失败的程序做测试。

GNU C库工具调试DAM问题

l  在调用任何与堆有关的函数钱调用函数mcheck

l  使用mtrace函数捕获内存泄露和重复释放;

程序崩溃处理

程序崩溃处理

       有人说C语言是低级语言,这有一部分原因是因为应用程序的内存管理大部分需要由程序员来实现。虽然这种方法非常有用,但是也给程序员添加了很多的麻烦。

       也有人说,C语言是相对较小且容易学习的语言,然而,只有不考虑标准C语言库的典型实现时,C语言才比较小,这个库相当庞大,很多程序员认为C语言是易用语言,那是因为他们还没有遇到指针。

       一般而言,程序错误会导致下面两件事情的发生:

l  导致程序做一些程序员没有打算做的事情;

l  导致程序崩溃

 

相信很多调试过程序的兄弟都碰到过段错误即segmentation fault,则合格主要原因是试图在未经允许的情况下访问一个内存单元。硬件会感知这件事并执行对操作系统的跳转。

堆区域

       调用malloc函数分配的内存;

栈区域

       用来动态分配数据的空间,函数调用的数据(包括参数、局部变量和返回地址)都存储在栈上。

查看程序在Linux上的精确内存布局情况

       可以通过使用info proc mappings来详细查看该程序在Linux上的精确内存布局情况,例如:

clip_image002

此时我们还可以看到这个进程号为14455,所以我们还可以通过文件/proc/14455/maps来查看该信息。通过这些信息,我们有可能看到文本和数据区域,以及堆和栈。

分配页策略

       操作系统不会将不完整的页分配给程序,例如,如果要运行的程序总共大约有10000字节,如果完全加载,会占用3个内存页(一个页占4096个字节),它不会仅占用2.5个页,因为页是虚拟内存系统能够操作的最小内存单元,这是调试时要着重了解的情况,这也导致了程序的一些错误内存访问不会触发段错误,换言之,在调试会话期间,没有引起段错误并不能直接说明代码是没有问题的。

页的角色细节

       当程序执行时,它会连续访问程序中的各个区域,导致硬件按照以下几种情况所示处理页表:

l  每次程序使用全局变量时,需要具有对数据区域的读写访问权限;

l  每次程序访问局部变量时,程序会访问栈,需要对栈区域具有读写访问权限;

l  每次程序进入或离开函数时,对该栈进行一次或多次访问,需要对栈区具有读写访问权限;

l  每次程序访问通过调用malloc或者new创建的存储器时,都会发生堆访问,也需要读写访问权限;

l  程序执行的每个机器指令是从文本区域取出的,因此需要具有读和执行文件;

信号

       这里需要注意的是,进程抛出的信号,实际上没有任何内容发送给进程。所发生的事情只不过是操作系统将信号记录到进程表中,以便下次进程接收信号时得到CPU上的时间片,执行恰当的信号处理程序。

自定义信号的复杂性

       使用GDB/DDD/Eclipse调试时,自定义信号处理程序可能会使程序变得复杂,无论是直接使用还是通过DDD GUI,每当发出任何信号时,GDB都会停止进程,所以,有可能意味着GDB会因为与调试无关的工作而频繁的停止,此时可以使用handle命令告诉GDB在某些信号发生时不要停止。

总线错误的原因

l  访问不存在的物理地址;

l  在很多架构上,要求访问32位量的机器指令要求字对齐,而导致视图在奇数号地址上访问具有4字节的数的指针错误可能引起总线错误。

总线错误是处理器层的异常,导致在Unix系统上发出SIGBUS信号,默认情况下,SIGBUS会导致转储内存并终止。

核心文件

       有些信号表示让某个进程继续是不妥当的,甚至是不可能的,在这些情况中,默认动作是提前终止进程,并编写一个名为核心文件core file文件,俗称转储核心

       核心文件包含程序崩溃时对程序状态的详细描述:栈的内容、CPU寄存器的内容、程序的静态分配变量的值。

       我们可以通过file命令来查看文件的详细信息。

为什么需要核心文件

l  只有在运行了一段长时间后才发生段错误,所以在调试器中无法重新创建该错误;

l  程序的行为取决于随机的环境事件,因此再次运行程序可能不会再现段错误;

l  当新手用户运行程序时发生的段错误,需要发送核心文件给开发人员。

重载功能

       GDB注意到重新编译了程序后,它会自动加载新的可执行文件,因此不需要退出和重启GDB

调试设计的内容

l  确认原则;

l  使用核心文件进行崩溃进程的“死后”分析;

l  纠正、编译并重新运行程序后,甚至不需要退出GDB

l  Printf()风格调试的不足之处;

l  利用你的智慧,这是无可替代的;

l  如果你过去使用printf风格调试的,就会发现使用printf跟踪这些程序错误中的部分错误原来有多难,虽然在调试中使用printf诊断代码有一定的好处,但是作为一种通用目的的工具,它远远不足以用来跟踪实际代码中发生的大部分程序错误。

 

停下来环顾程序

停下来环顾程序

调试器不仅能够运行程序,还可以通知它暂停程序的运行,暂停以后,调试器提供了检查变量、跟踪执行路径的机会。

暂停机制

3中方式可以通过GDB暂停程序的执行:

l  断点:通知GDB在程序中的特定位置暂停执行;

l  监视点:通知GDB当特定内存位置(或者设计一个或多个位置的表达式)的值发生变化时暂停执行;

l  捕获点:通知GDB当特定事件发生时暂停执行;

GDB中删除断点

l  delete breakpoint_list

l  delete

l  clear

l  clear functionclear filenamefunctionclear line_numberclear filenameline_number

2.7.2 GDB中禁用断点 51

       在调试会话期间,会遇到大量断点,对于经常重复的循环结构或函数,这种情况使得调试极不方便。如果要保留断点以便以后使用,暂时又不希望GDB停止执行,可以禁用它们,在以后需要时再启用。此时我们可以使用disable/enable breakpoint_list来禁用和启用断点。

DDD

l  可以直接拖拽断点,很方便;

l  还有一个优秀的功能Undo/Redo

GDB中恢复执行的方法

l  使用stepnext单步调试程序;

l  使用continue使GDB无条件地恢复程序的执行,知道它遇到另一个断点或者程序结束;

l  finishuntil命令恢复。

nextstep的区别

       next执行函数,不会在其中暂停,然后在调用之后的第一条语句处暂停;

       step在函数中的第一个语句处暂停;

step的单步进入函数

clip_image002

next单步越过函数

clip_image004

使用continue恢复程序执行

       continue与执行一行代码的stepnext相反,这个命令使GDB恢复程序的执行,直到触发断点或者程序结束。

       continue命令可以接受一个可选的整数参数n,这个数字要求GDB忽略下面n个断点。

使用finish恢复程序执行

       命令finish指示GDB恢复执行,直到恰好在当前栈帧完成之后位置,这意味着如果你在一个不是main的函数中,finish命令会导致GDB恢复执行,直到恰好在函数返回之后为止,例如:

clip_image006

       如果在一个递归函数中,finish只会将你带到递归的上一层,这是因为每次调用都被看做在它自己权限内的函数调用,因为每个函数都有自己的栈帧,如果要在递归层次较高时完全退出递归函数,那么更适合使用临时断点及continue,或者使用until命令

使用until恢复程序执行

       命令until执行程序,直到到达当前循环体外的下一行源代码。

clip_image008

设置条件断点的方法

       break break-args if (condition)

 

监视点

       监视点是一种特殊类型的断点,它类似于正常断点,是要求GDB暂停程序执行的指令。区别在于监视点是没有住在某一行源码中,取而代之的是,监视点是指示GDB每当某个表达式改变了值就暂停执行的指令。