linux下的gdb调试工具--断点调试
到目前为止我们的调试手段只有一种:
根据程序执行时的出错现象假设错误原因,然后在代码中适当的位置插入printf
,执行程序并分析打印结果,如果结果和预期的一样,就基本上证明了自己假设的错误原因,就可以动手修正Bug了。
如果结果和预期的不一样,就根据结果做进一步的假设和分析。
这里,我们介绍一种很强大的调试工具gdb
,可以完全操控程序的运行,使得程序就像你手里的玩具一样,叫它走就走,叫它停就停,并且随时可以查看程序中所有的内部状态,比如各变量的值、传给函数的参数、当前执行的代码行等。
有了gdb以后,即使调试手段丰富了,调试的基本思想仍然是“分析现象->假设错误原因->产生新的现象去验证假设”这样一个循环,根据现象如何假设错误原因,以及如何设计新的现象去验证假设,这都需要非常严密的分析和思考,如果因为手里有了强大的工具就滥用而忽略了分析过程,往往会治标不治本地修正Bug,导致一个错误现象消失了但Bug仍然存在,甚至是把程序越改越错。
怎么开始使用gdb
看下面的程序代码:
- #include <stdio.h>
- int add_range(int low, int high)
- {
- int i, sum;
- for (i = low; i <= high; i++)
- sum = sum + i;
- return sum;
- }
- int main(void)
- {
- int result[100];
- result[0] = add_range(1, 10);
- result[1] = add_range(1, 100);
- printf("result[0]=%d\nresult[1]=%d\n", result[0], result[1]);
- return 0;
- }
add_range
函数从low
加到high
,在main
函数中首先从1加到10,把结果保存下来,然后从1加到100,再把结果保存下来,最后打印的两个结果。程序运行之后,发现结果是不正确的。
现在开始调试:
在编译时要加上-g
选项,生成的可执行文件才能用gdb
进行源码级调试:
- $ gcc -g main.c -o main
- $ gdb main
- GNU gdb 6.8-debian
- Copyright (C) 2008 Free Software Foundation, Inc.
- License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
- This is free software: you are free to change and redistribute it.
- There is NO WARRANTY, to the extent permitted by law. Type "show copying"
- and "show warranty" for details.
- This GDB was configured as "i486-linux-gnu"...
- (gdb)
-g
选项的作用是在可执行文件中加入源代码的信息,比如可执行文件中第几条机器指令对应源代码的第几行,但并不是把整个源文件嵌入到可执行文件中,所以在调试时必须保证gdb
能找到源文件。gdb
提供一个类似Shell的命令行环境,上面的(gdb)
就是提示符,在这个提示符下输入help
可以查看命令的类别:
- (gdb) help
- List of classes of commands:
- aliases -- Aliases of other commands
- breakpoints -- Making program stop at certain points
- data -- Examining data
- files -- Specifying and examining files
- internals -- Maintenance commands
- obscure -- Obscure features
- running -- Running the program
- stack -- Examining the stack
- status -- Status inquiries
- support -- Support facilities
- tracepoints -- Tracing of program execution without stopping the program
- user-defined -- User-defined commands
- Type "help" followed by a class name for a list of commands in that class.
- Type "help all" for the list of all commands.
- Type "help" followed by command name for full documentation.
- Type "apropos word" to search for commands related to "word".
- Command name abbreviations are allowed if unambiguous.
也可以进一步查看某一类别中有哪些命令,例如查看files
类别下有哪些命令可用:
- (gdb) help files
- Specifying and examining files.
- List of commands:
- add-shared-symbol-files -- Load the symbols from shared objects in the dynamic linker's link map
- add-symbol-file -- Load symbols from FILE
- add-symbol-file-from-memory -- Load the symbols out of memory from a dynamically loaded object file
- cd -- Set working directory to DIR for debugger and program being debugged
- core-file -- Use FILE as core dump for examining memory and registers
- directory -- Add directory DIR to beginning of search path for source files
- edit -- Edit specified file or function
- exec-file -- Use FILE as program for getting contents of pure memory
- file -- Use FILE as program to be debugged
- forward-search -- Search for regular expression (see regex(3)) from last line listed
- generate-core-file -- Save a core file with the current state of the debugged process
- list -- List specified function or line
- ...
现在试试用list
命令从第一行开始列出源代码:
list的作用是列出具体的函数或者行,list仅能列出10行代码,简写为l。
- (gdb) list 1
- 1 #include <stdio.h>
- 2
- 3 int add_range(int low, int high)
- 4 {
- 5 int i, sum;
- 6 for (i = low; i <= high; i++)
- 7 sum = sum + i;
- 8 return sum;
- 9 }
- 10
一次只列10行,如果要从第11行开始继续列源代码可以输入list或者输入回车(代表重复前一个指令)。
也可以什么都不输直接敲回车,gdb
提供了一个很方便的功能,在提示符下直接敲回车表示重复上一条命令。
- (gdb) (直接回车)
- 11 int main(void)
- 12 {
- 13 int result[100];
- 14 result[0] = add_range(1, 10);
- 15 result[1] = add_range(1, 100);
- 16 printf("result[0]=%d\nresult[1]=%d\n", result[0], result[1]);
- 17 return 0;
- 18
gdb
的很多常用命令有简写形式,例如list
命令可以写成l
,要列一个函数的源代码也可以用函数名做参数:
- (gdb) l add_range
- 1 #include <stdio.h>
- 2
- 3 int add_range(int low, int high)
- 4 {
- 5 int i, sum;
- 6 for (i = low; i <= high; i++)
- 7 sum = sum + i;
- 8 return sum;
- 9 }
- 10
现在退出gdb
的环境:
- (gdb) quit
我们做一个实验,把源代码改名或移到别处再用gdb
调试,这样就列不出源代码了:
- $ mv main.c mian.c
- $ gdb main
- ...
- (gdb) list 1
- 1 main.c: No such file or directory.
可见gcc
的-g
选项并不是把源代码嵌入到可执行文件中的,在调试时也需要源文件。现在把源代码恢复原样,我们继续调试。首先用start
命令开始执行程序。
单步执行调试
执行单步调试命令start,程序停止在从main函数开始,第一个可以设置为断点的语句。
- $ gdb main
- ...
- (gdb) start
- Breakpoint 1 at 0x80483ad: file main.c, line 14.
- Starting program: /home/akaedu/main
- main () at main.c:14
- 14 result[0] = add_range(1, 10);
- (gdb)
gdb
停在main
函数中变量定义之后的第一条语句处等待我们发命令,gdb
列出的这条语句是即将执行的下一条语句。我们可以用next
命令(简写为n
)控制这些语句一条一条地执行:
- (gdb) n
- 15 result[1] = add_range(1, 100);
- (gdb) (直接回车)
- 16 printf("result[0]=%d\nresult[1]=%d\n", result[0], result[1]);
- (gdb) (直接回车)
- result[0]=55
- result[1]=5105
- 17 return 0;
注意:
- 打印出来的result[0]和result[1]是不正确的,里面的值是一个乱的数,因为sum并没有初始化。
- 题目中给出的值只是这些所有乱数中的一个,也就是sum第一次调用的初始值为0,然后增加到了55,然后第二次调用,没有重新初始化,所以第二次的累加是在55的基础上累加的。这个过程涉及到函数栈的新建-->销毁-->新建三个过程。
用n
命令依次执行两行赋值语句和一行打印语句,在执行打印语句时结果立刻打出来了,然后停在return
语句之前等待我们发命令。虽然我们完全控制了程序的执行,但仍然看不出哪里错了,因为错误不在main
函数中而在add_range
函数中,现在用start
命令重新来过,这次用step
命令(简写为s
)钻进add_range
函数中去跟踪执行:
- (gdb) start
- The program being debugged has been started already.
- Start it from the beginning? (y or n) y
- Breakpoint 2 at 0x80483ad: file main.c, line 14.
- Starting program: /home/akaedu/main
- main () at main.c:14
- 14 result[0] = add_range(1, 10);
- (gdb) s
- add_range (low=1, high=10) at main.c:6
- 6 for (i = low; i <= high; i++)
这次停在了add_range
函数中变量定义之后的第一条语句处。在函数中有几种查看状态的办法,backtrace
命令(简写为bt
)可以查看函数调用的栈帧:
- (gdb) bt
- #0 add_range (low=1, high=10) at main.c:6
- #1 0x080483c1 in main () at main.c:14
可见当前的add_range
函数是被main
函数调用的,main
传进来的参数是low=1, high=10
。main
函数的栈帧编号为1,add_range
的栈帧编号为0。
现在可以用info
命令(简写为i
)查看add_range
函数局部变量的值:
- (gdb) i locals
- i = 0
- sum = 0
如果想查看main
函数当前局部变量的值也可以做到,先用frame
命令(简写为f
)选择1号栈帧然后再查看局部变量:
- (gdb) f 1
- #1 0x080483c1 in main () at main.c:14
- 14 result[0] = add_range(1, 10);
- (gdb) i locals
- result = {0, 0, 0, 0, 0, 0, 134513196, 225011984, -1208685768, -1081160480,
- ...
- -1208623680}
注意到result
数组中有很多元素具有杂乱无章的值,我们知道未经初始化的局部变量具有不确定的值。到目前为止一切正常。用s
或n
往下走几步,然后用print
命令(简写为p
)打印出变量sum
的值:
- (gdb) s
- 7 sum = sum + i;
- (gdb) (直接回车)
- 6 for (i = low; i <= high; i++)
- (gdb) (直接回车)
- 7 sum = sum + i;
- (gdb) (直接回车)
- 6 for (i = low; i <= high; i++)
- (gdb) p sum
- $1 = 3
第一次循环i
是1,第二次循环i
是2,加起来是3,没错。这里的$1
表示gdb
保存着这些中间结果,$后面的编号会自动增长,在命令中可以用$1
、$2
、$3
等编号代替相应的值。由于我们本来就知道第一次调用的结果是正确的,再往下跟也没意义了,可以用finish
命令让程序一直运行到从当前函数返回为止:
- (gdb) finish
- Run till exit from #0 add_range (low=1, high=10) at main.c:6
- 0x080483c1 in main () at main.c:14
- 14 result[0] = add_range(1, 10);
- Value returned is $2 = 55
返回值是55,当前正准备执行赋值操作,用s
命令赋值,然后查看result
数组:
- (gdb) s
- 15 result[1] = add_range(1, 100);
- (gdb) p result
- $3 = {55, 0, 0, 0, 0, 0, 134513196, 225011984, -1208685768, -1081160480,
- ...
- -1208623680}
第一个值55确实赋给了result
数组的第0个元素。下面用s
命令进入第二次add_range
调用,进入之后首先查看参数和局部变量:
- (gdb) s
- add_range (low=1, high=100) at main.c:6
- 6 for (i = low; i <= high; i++)
- (gdb) bt
- #0 add_range (low=1, high=100) at main.c:6
- #1 0x080483db in main () at main.c:15
- (gdb) i locals
- i = 11
- sum = 55
由于局部变量i
和sum
没初始化,所以具有不确定的值,又由于两次调用是挨着的,i
和sum
正好取了上次调用时的值。i
的值有没有初始化倒没关系,在for
循环中会赋值为low的值,但sum
如果初值不是0,累加得到的结果就错了。
好了,我们已经找到错误原因,可以退出gdb
修改源代码了。如果我们不想浪费这次调试机会,可以在gdb
中马上把sum
的初值改为0继续运行,看看这一处改了之后还有没有别的Bug:
- (gdb) set var sum=0
- (gdb) finish
- Run till exit from #0 add_range (low=1, high=100) at main.c:6
- 0x080483db in main () at main.c:15
- 15 result[1] = add_range(1, 100);
- Value returned is $4 = 5050
- (gdb) n
- 16 printf("result[0]=%d\nresult[1]=%d\n", result[0], result[1]);
- (gdb) (直接回车)
- result[0]=55
- result[1]=5050
- 17 return 0;
这样结果就对了。修改变量的值除了用set
命令之外也可以用print
命令,因为print
命令后面跟的是表达式,而我们知道赋值和函数调用也都是表达式,所以也可以用print
命令修改变量的值或者调用函数:
- (gdb) p result[2]=33
- $5 = 33
- (gdb) p printf("result[2]=%d\n", result[2])
- result[2]=33
- $6 = 13
总结一下gdb的基本命令
断点调试
看下面的代码:
- #include <stdio.h>
- int main(void)
- {
- int sum = 0, i = 0;
- char input[5];
- while (1)
- {
- scanf("%s", input);
- for (i = 0; input[i] != '\0'; i++)
- sum = sum*10 + input[i] - '0';
- printf("input=%d\n", sum);
- }
- return 0;
- }
这个程序的作用是:首先从键盘读入一串数字存到字符数组input
中,然后转换成整型存到sum
中,然后打印出来,一直这样循环下去。
注意字符型的'2'
要减去'0'
的ASCII码才能转换成整数值2。下面编译运行程序看看有什么问题:
- $ gcc main.c -g -o main
- $ ./main
- 123
- input=123
- 234
- input=123234
- (Ctrl-C退出程序)
- $
第一次是对的,第二次就不对。先试试只看代码能不能看出错误原因。下面来调试:
- $ gdb main
- ...
- (gdb) start
- Breakpoint 1 at 0x80483b5: file main.c, line 5.
- Starting program: /home/akaedu/main
- main () at main.c:5
- 5 int sum = 0, i = 0;
有了上一次的经验,sum
被列为重点怀疑对象,我们可以用display
命令使得每次停下来的时候都显示当前sum
的值,然后继续往下走:
- (gdb) display sum
- 1: sum = -1208103488
- (gdb) n
- 9 scanf("%s", input);
- 1: sum = 0
- (gdb)
- 123
- 10 for (i = 0; input[i] != '\0'; i++)
- 1: sum = 0
undisplay
命令可以取消跟踪显示,变量sum
的编号是1,可以用undisplay 1
命令取消它的跟踪显示。这个循环应该没有问题,因为上面第一次输入时打印的结果是正确的。如果不想一步一步走这个循环,可以用break
命令(简写为b
)在第9行设一个断点(Breakpoint):
- (gdb) l
- 5 int sum = 0, i;
- 6 char input[5];
- 7
- 8 while (1) {
- 9 scanf("%s", input);
- 10 for (i = 0; input[i] != '\0'; i++)
- 11 sum = sum*10 + input[i] - '0';
- 12 printf("input=%d\n", sum);
- 13 }
- 14 return 0;
- (gdb) b 9
- Breakpoint 2 at 0x80483bc: file main.c, line 9.
break
命令的参数也可以是函数名,表示在某个函数开头设断点。现在用continue
命令(简写为c
)连续运行而非单步运行,程序到达断点会自动停下来,这样就可以停在下一次循环的开头:
- (gdb) c
- Continuing.
- input=123
- Breakpoint 2, main () at main.c:9
- 9 scanf("%s", input);
- 1: sum = 123
然后输入新的字符串准备转换:
- (gdb) n
- 234
- 10 for (i = 0; input[i] != '\0'; i++)
- 1: sum = 123
问题暴露出来了,新的转换应该再次从0开始累加,而sum
现在已经是123了,原因在于新的循环没有把sum
归零。可见断点有助于快速跳过没有问题的代码,然后在有问题的代码上慢慢走慢慢分析,“断点加单步”是使用调试器的基本方法。至于应该在哪里设置断点,怎么知道哪些代码可以跳过而哪些代码要慢慢走,也要通过对错误现象的分析和假设来确定,以前我们用printf
打印中间结果时也要分析应该在哪里插入printf
,打印哪些中间结果,调试的基本思路是一样的。一次调试可以设置多个断点,用info
命令可以查看已经设置的断点:
- (gdb) b 12
- Breakpoint 3 at 0x8048411: file main.c, line 12.
- (gdb) i breakpoints
- Num Type Disp Enb Address What
- 2 breakpoint keep y 0x080483c3 in main at main.c:9
- breakpoint already hit 1 time
- 3 breakpoint keep y 0x08048411 in main at main.c:12
每个断点都有一个编号,可以用编号指定删除某个断点:
- (gdb) delete breakpoints 2
- (gdb) i breakpoints
- Num Type Disp Enb Address What
- 3 breakpoint keep y 0x08048411 in main at main.c:12
有时候一个断点暂时不用可以禁用掉而不必删除,这样以后想用的时候可以直接启用,而不必重新从代码里找应该在哪一行设断点:
- (gdb) disable breakpoints 3
- (gdb) i breakpoints
- Num Type Disp Enb Address What
- 3 breakpoint keep n 0x08048411 in main at main.c:12
- (gdb) enable 3
- (gdb) i breakpoints
- Num Type Disp Enb Address What
- 3 breakpoint keep y 0x08048411 in main at main.c:12
- (gdb) delete breakpoints
- Delete all breakpoints? (y or n) y
- (gdb) i breakpoints
- No breakpoints or watchpoints.
gdb
的断点功能非常灵活,还可以设置断点在满足某个条件时才激活,例如我们仍然在循环开头设置断点,但是仅当sum
不等于0时才中断,然后用run
命令(简写为r
)重新从程序开头连续运行:
- (gdb) break 9 if sum != 0
- Breakpoint 5 at 0x80483c3: file main.c, line 9.
- (gdb) i breakpoints
- Num Type Disp Enb Address What
- 5 breakpoint keep y 0x080483c3 in main at main.c:9
- stop only if sum != 0
- (gdb) r
- The program being debugged has been started already.
- Start it from the beginning? (y or n) y
- Starting program: /home/akaedu/main
- 123
- input=123
- Breakpoint 5, main () at main.c:9
- 9 scanf("%s", input);
- 1: sum = 123
结果是第一次执行scanf
之前没有中断,第二次却中断了。总结一下本节用到的gdb
命令:
linux下的gdb调试工具--断点调试的更多相关文章
- linux下的gdb调试工具--内存调试
接着上一节的代码,在while(1)的循环里面增加代码:sum=0 #include <stdio.h> int main(void) { int sum = 0, i = 0; char ...
- linux下使用gdb对php源码调试
title: linux下使用gdb对php源码调试 date: 2018-02-11 17:59:08 tags: --- linux下使用gdb进行php调试 调试了一些php的漏洞,记录一下大概 ...
- Linux下使用GDB进行调试
Linux下使用GDB进行调试的常用命令记于此. $ sudo su # g++ -g test.cpp -o test -pthread # gdb test <------- ...
- Linux下使用GDB调试程序
问题描述: Linux下使用GDB调试程序 问题解决: (1)生成调试文件 注: 使用命令 gdb IOStream.c -o IOStre ...
- linux下如何产生core,调试core
linux下如何产生core,调试core 摘自:http://blog.163.com/redhumor@126/blog/static/19554784201131791239753/ 在程序不寻 ...
- Linux下的串口调试工具——Xgcom
Linux下的串口调试工具——Xgcom xgcom的下载网址:https://code.google.com/archive/p/xgcom/downloads (1)安装必须的库 apt-get ...
- Lua中如何实现类似gdb的断点调试--01最小实现
说到Lua代码调试,最常用的方法应该就是加一堆print进行打印.print大法虽好,但其缺点也是显而易见的.比如效率低下,需要修改原有函数内部代码,在每个需要的地方添加print语句,运行一次只能获 ...
- Linux下编辑、编译、调试命令总结——gcc和gdb描述
GCC gcc是linux系统集成的编译器.在linux环境下编辑程序,首先需要克服的便是没有集成开发环境的一键式操作所带来的麻烦.这其中涉及命令行操作.编译选项的设定.文件依赖关系的书写(makef ...
- Linux下交叉编译gdb,gdbserver+gdb的使用以及通过gdb调试core文件
交叉编译gdb和gdbserver 1.下载gdb:下载地址为:http://ftp.gnu.org/gnu/gdb/按照一般的想法,最新版本越好,因此下载7.2这个版本.当然,凡事无绝对.我们以gd ...
随机推荐
- ng-class用法
在angular中为我们提供了3种方案处理class: 1:scope变量绑定.这种方案不推荐,因为scope里最好处理业务逻辑,不去管渲染的事.2:字符串数组形式.3:对象key/value处理. ...
- HTML5新增的主体元素和新增的非主体结构元素
HTML5新增的主体元素 article元素 article元素表示文档.页面或应用程序中独立的.完整的.可以独自被外部引用的内容.它可以是一篇博客或者报刊中的文章,一篇论坛帖子.一段用户评论或独立的 ...
- 获得创建临时表的session id
通过sql server的default trace和tempdb中的sys.objects视图,你能够获得创建临时表的session id,下面是相应的sql语句: DECLARE @FileNam ...
- HDU 5724 - Chess
题意: 一个n行20列的棋盘. 每一行有若干个棋子. 两人轮流操作, 每人每次可以将一个棋子向右移动一个位置, 如果它右边有一个棋子, 就跳过这个棋子, 如果有若干个棋子, 就将这若干个 ...
- mysql单表大小的限制
mysql单表大小的限制一.MySQL数据库的MyISAM存储 引擎单表大小限制已经不是有MySQL数据库本身来决定(限制扩大到64pb),而是由所在主机的OS上面的文件系统来决定了.在mysql5. ...
- 【Android & iOS】应用升级实现
在移动应用中,都会有的一个功能就是应用版本升级,怎么实现这个功能呢? 基本的思路就是:对比当前使用的应用版本和最新的版本号,如果版本号不一致,就可以提示用户升级啦. Android中,可以通过一下方式 ...
- iOS中判断设备系统版本
@import url(http://i.cnblogs.com/Load.ashx?type=style&file=SyntaxHighlighter.css);@import url(/c ...
- EasyUI 使用心得
最近项目中用到EasyUI,总结了一下 注:EasyUI中所有的控件不能重名,否则会出现意向不到的后果.这是EasyUI框架决定的. ① EasyUI 获取文本框中的值 //日期 $('#beginD ...
- shell基础——字符串连接
#!/bin/sh str1="hello" str2="world" echo str1=$str1, str2=$str2 strconn1=$str1$s ...
- Dynamics CRM 2013 初体验(3):新增加的功能
新系统除了修补系统历史漏洞外当然还会添加些比较有意思的新功能,至于这些新功能是否好用那就得看它是否能经过咱们这些使用者的考验了.Dynamics CRM 2013系统将不再支持Dynamics CRM ...