gdb

第十章 gdbshell

程序中除了一目了然的Bug以外都须要必定的调试手段来分析到底错在哪。到目前为止咱们的调试手段只有一种:根据程序执行时的出错现象假设错误缘由,而后在代码中适当的为止插入printf,执行程序并分析打印结果,若是结果和预期的同样,就基本上证实了本身假设的错误缘由,就能够动手修正Bug了,若是结果和预期的不同,就根据结果作进一步的假设和分析。调试工具gdb的基本思想仍然是“分析现象->假设错误缘由->产生新的现象去验证假设”。数组

1.单步执行和跟踪函数调用函数

在编译时要加上-g选项,生成的可执行文件才能用gdb进行调试:工具

 

-g选项的做用是在可执行 文件中加入源代码的信息,好比可执行文件中第几条机器指令对应源代码的第几行,但并非把整个源文件嵌入到可执行文件中,因此在调试时必须保证gdb能找到源文件。gdb提供一个相似shell的命令行环境,上面的(gdb)就是提示符,在这个提示符下输入help能够查看命令的类别,能够进一步查看某一类别中有哪些命令,例如查看files类别下有哪些命令能够用:学习

 

如今试试用list命令从第一行开始列出源代码:操作系统

 

一次只列10行,若是要从11行开始继续列源代码能够输入命令行

 

也能够什么都不输直接敲回车,gdb提供了一个很方便的功能,在提示符下直接敲回车表示用适当的参数重复上一条命令。gdb的不少经常使用命令有简写形式,例如list命令能够写成l,要列一个函数的源代码也能够用函数名作参数:3d

 

如今退出gdb的环境:调试

 

如今把源代码更名或移到别处再用gdb调试,就列不出源代码了:blog

可见gcc的-g选项并非把源代码 嵌入到可执行文件中的,在调试时也须要源文件。如今把源代码恢复原样,咱们继续调试。首先用start命令开始执行程序:

 

这表示停在main函数中变量定义以后的第一条语句处等待咱们发命令,gdb列出这条语句表示它还没执行,而且立刻要执行。咱们能够用next命令(简写为n)控制这些语句一条一条地执行:

 

用n命令依次执行两行赋值语句和一行打印语句,在执行打印语句时结果马上打出来了,而后停留在return语句以前等待咱们发命令。虽然咱们彻底控制了程序的执行,但仍然看不出哪里错了,由于错误不在main函数而在add_range函数,如今用start命令从新来过,此次用step命令(简写为s)进入函数中去执行:

 

此次停在了函数中变量定义以后的第一条语句处。在函数中有几种查看状态的办法,backtrace命令(简写为bt)能够查看函数调用的栈帧:

 

可见当前的add_range函数是被main函数调用的,main传进来的参数是low=1,high=10。main函数的栈帧编号为1,add_range的栈帧编号为0。如今能够用info命令(简写为i)查看add_range局部变量的值:

 

若是想查看main函数当前局部变量的值也能够作到,先用frame命令(简写为f)选择1号栈帧后再查看局部变量:

 

注意到result数组中有不少元素具备杂乱无章的值,咱们知道,未经初始化的局部变量具备不肯定的值。到目前为止一切正常。用s或n往下走几步,而后用print命令(简写为p)打出变量sum的值:

 

第一次循环i是1,第二次循环i是2,加起来是3,没错。这里的$1表示gdb保存着这些中间结果,$后面的编号会自动增加,在命令中能够用$一、$二、$3等编号代替相应的值。因为咱们原本就知道第一次调用的结果是正确的,再往下跟也没意义了,能够用finish命令让程序一直运行到从当前函数返回为止:

 

返回值是55,当前正准备执行赋值操做,用s命令赋值,而后查看result数组:

 

第一个值55确实赋给了result数组的第0个元素。下面用s命令进入第二次add_range掉用,进入以后首先查看参数和局部变量:

 

因为局部变量i和sum没初始化,因此具备不肯定的值,又因为两次调用是挨着的,i和sum正好取了上次调用时的值。i的初值不是0倒不要紧,在for循环中会赋值为0的,但sum若是初值不是0,累加获得的结果就错了。好了,咱们已经找到错误缘由,能够退出gdb修改源代码了。若是咱们不想浪费这一次调试机会,能够在gdb中立刻把sum的初值改成0继续运行,看看这一处改了以后还有没有别的Bug:

 

这样结果就对了。修改变量的值除了用set命令以外也能够用print命令,由于print命令后面跟的是表达式,而咱们知道赋值和函数调用也都是表达式,因此还能够用print来修改变量的值,或者调用函数:

 

printf的返回值表示实际打印的字符数,因此$6的结果是13。

前面用到的gdb的基本命令:

 

2.断点

断点调试实例:

 

这个程序的做用是:首先从键盘读入一串数字存到字符数组input中,而后转换成整型存到sum中,而后打印出来,一直这样循环下去。scanf(“%s”, input);这个调用功能是等待用户输入一个字符串并回车,scanf把其中第一段非空白(非空格、Tab、换行)的字符串放到input数组中,并自动在末尾添加‘\0’。接下来的循环从左到右扫描字符串并把每一个数字累加到结果中,例如输入是“2345”,则循环累加的过程是(((0*10+2)*10+3)*10+4)*10+5=2345。注意字符型的‘2’要减去‘0’的ASCII码才能转换成整数值的2,‘0’的ASCII码是48,而‘\0’的ASCII码是0,两者是不相同的。下面运行程序看问题:

 

又是这种现象,第一次是对的,第二次不对。而这个程序咱们赋了初值,下面调试:

 

可见,若是变量要赋初值,start不会跳过变量定义语句。

用display命令使得每次停下来的时候都显示当前sum值,而后继续往下走:

 

用undisplay能够取消对先前设置的那些变量的跟踪。这个循环应该是没有问题的,由于第一次的结果正确。若是不想一步一步走这个循环,能够用break命令(简写为b)在第9行设一个断点(Breakpoint):

 

break命令的参数也能够是函数名,表示在某一个函数开头设断点。如今用continue命令(简写为c)连续运行而非单步运行,程序到达断点会自动停下来,这样就能够停在下一次循环的开头:

 

而后输入新的字符串准备转换:

 

问题暴露出来了,新的转换应该再次从0开始累加,而sum如今已是123了,缘由在于新的循环没有把sum归零。可见断点有助于快速跳过与问题无关的代码,而后在有问题的代码上慢慢走慢慢分析,“断点加单步”是使用调试器的基本方法。至于应该在哪里设置断点,怎么知道哪些代码能够跳过而哪些代码要慢慢走,也要经过对错误现象的分析和假设来肯定,就像之前分析肯定在哪里插入printf语句同样。一次调试能够设置多个断点,用info命令能够查看已经设置的断点:

 

每一个断点都有一个编号,能够用编号指定删除某个断点:

 

有时候一个断点暂时不想用能够禁用掉而没必要删除,这样之后想用的时候能够直接启用,而没必要从新从代码里找应该在哪一行设断点:

 

gdb的断点功能很是灵活,还能够设置断点在知足某个条件时才激活,例如咱们仍然在循环开头设置断点,可是仅当sum不等于0时才中断,而后用run命令(简写为r)从新从程序开头连续执行:

 

结果是第一次执行scanf以前没有中断,第二次却中断了。

gdb基本命令:

 

3.观察点

断点是当程序执行到某一代码行时中断,而观察点(Watchpoint)是当程序访问某一存储单元时中断,若是咱们不知道某一存储单元时在哪里被改动的,这时候观察点尤为有用。

观察点调试实例:

 

下面善春原来设的断点,从头执行程序,重复上次的输入,用watch命令设置观察点,跟踪input[4]后面那个字节(input[5]):

 

gdb基本命令:

 

4.段错误

在gdb中遇到段错误就会停下来。如scanf输入整型变量就必需要加&,不然就会出段错误,而输出字符串就不要加&。

学习C语言不可能不去了解底层计算机体系结构和操做系统的原理,不了解底层原理一个scanf都用很差,更没办法写出正确的程序。

相关文章
相关标签/搜索