为了以TUI模式运行GDB,可以在调用GDB时在命令行上指定-tui选项,或者处于非TUI模式时在GDB中使用Ctrl+X+A组合键。如果当前处于TUI模式,后一种命令方式就会使你离开TUI模式。
在TUI模式中,GDB窗口划分为两个子窗口——一个用于输入GDB命令,而另一个用于查看源代码。
例如:
源代码为ins.c
#include <stdio.h> int x[10], y[10], num_inputs, num_y = 0; void get_args(int ac,char **av){ int i; num_inputs = ac - 1; for(i = 0;i < num_inputs;++i) x[i] = atoi(av[i + 1]); } void scoot_over(int jj){ int k; for(k = num_y;k > jj;--k) y[k] = y[k - 1]; } void insert(int new_y){ int j; // if(num_y==0){ y[0] = new_y; return; } for(j = 0;j < num_y;++j){ if(new_y < y[j]){ scoot_over(j); y[j] = new_y; return; } } y[num_y]=new_y; } void process_data(){ for(num_y = 0;num_y < num_inputs;++num_y) insert(x[num_y]); } void print_results(){ int i; for(i = 0;i < num_inputs;++i) printf("%d\n",y[i]); } int main(int argc,char **argv){ get_args(argc,argv); process_data(); print_results(); }
编译后:
gcc -g3 -Wall -o insert_sort ins.c
注意,在GCC中可以用-g选项让编译器将符号表(即对应于程序的变量和代码行的内存地址列表)保存在生成的可执行文件(这里是insert_sort)中。这是一个绝对必要的步骤,这样才能在调试会话过程中引用源代码中的变量名和行号。
使用GDB调试insert_sort
如果正在使用GDB但没有使用TUI模式,则位于下方的子窗口确切地显示你将看到的内容。此处,该子窗口显示如下内容。
1)发出一条break命令,在当前源文件第12行处设置断点。
2)执行run命令运行程序,并且向该程序传递命令行参数12、5、6.在此之后,调试器在指定的断点处停止执行。GDB会提醒用户断点位于ins.c的第12行,并且通知该源代码行的机器代码驻留在内存地址0xbffff484中。
3)发出next命令,步进到下一个代码行,即第13行。
退出GDB:quit或者Ctrl+d
执行程序:run
2.1 单步调试源代码
安排程序的执行在某个地方暂停,以便检查变量的值,从而得到关于程序错误所在位置的线索。
调试工具会在指定断点处暂停程序的执行。在GDB中是通过break命令及其行号完成的。
普通断点和条件断点
(gdb) break 30
Breakpoint 1 at 0x80483fc: file ins.c,line 30.
(gdb) condition 1 num_y==1
第一个命令在第30行放置一个断点。这里的第二个命令condition 1 num_y==1使得该断点称为有条件的断点:只有当满足条件num_y==1时,GDB才会暂停程序的执行。
注意,与接受行号(或函数名)的break命令不同,condition接受断点号。总是可以用命令info break来查询要查找的断点的编号。
用break if可以将break和condition命令组合成一个步骤,如下所示:
(gdb) break 30 if num_y==1
前面提到过,在GDB中next命令会让GDB执行下一行,然后暂停。step命令的作用与此类型,只是函数调用时step命令会进入函数,而next导致程序执行的暂停出现在下次调用函数时。
在GDB中,continue命令通知调试器恢复执行并继续,直到遇到断点为止。
在GDB中,tbreak命令与break相似,但是这一命令设置的断点的有效期限只到首次到达指定行时为止。
2.2 检查变量
(gdb) print j
$1=1
对GDB的这一查询的输出表明j的值为1.$1标签意味着这是你要求GDB输出的第一个值。($1、$2、$3等表示的值统称为调试会话的值历史。)
2.3 在GDB中设置监视点以应对变量值的改变
监视点结合了断点和变量检查的概念。最基本形式的监视点通知调试器,每当指定变量的值发生变化时都暂停程序的执行。
(gdb) watch z
当运行程序时,每当z的值发生变化,GDB都会暂停执行。
更好的方法是,可以基于条件表达式来设置监视点。例如,查找执行期间z 的值大于28的第一个位置
(gdb) watch(z>28)
2.4 上下移动调用栈
在函数调用期间,与调用关联的运行时信息存储在称为栈帧的内存区域中。帧中包含函数的局部变量的值、其形参,以及调用该函数的记录。每次发生函数调用时,都会创建一个新帧,并将其推导一个系统维护的栈上;栈最上方的帧表示当前正在执行的函数,当函数退出时,这个帧被弹出栈,并且被释放。
在GDB中可用用如下命令查看以前的帧:
(gdb) frame 1
当执行GDB的frame命令时,当前正在执行的函数的帧被编号为0,其父帧(即该函数的调用者的栈帧)被编号为1,父帧的父帧被编号为2,以此类推。GDB的up命令将你带到调用栈中的下一个父帧(例如,从帧0到帧1),down则引向相反方向。
显示整个栈:backtrace
浏览以前的GDB命令:上一个Ctrl+P、下一个Ctrl+N
在GDB中,可以通过help命令访问文档。例如:
(gdb) help breakpoints
在重新编译代码时,最好不要退出GDB。这样,你的断点和建立的其他各种动作都会保留。要是退出GDB,就不得不再次重复键入这些内存。
然而,在完成调试前可能需要退出GDB。如果你要离开一段时间,而且不能保持登录在计算机中,则需要退出GDB。为了不丢失它们,可以将断点和设置的其他命令放在一个GDB启动文件中,然后每次启动GDB时都会自动加载它们。
GDB的启动文件默认名为.gdbinit。
在调用GDB时可以指定启动文件。例如,
$ gdb -command=z x
表示要在可执行文件x上运行GDB,首先要从文件z中读取命令。
有3种方式可以通知GDB暂停程序的执行。
1)断点:通知GDB在程序中的特定位置暂停执行。
2)监视点:通知GDB当特定内存位置的值发生变化时暂停执行
3)捕获点:通知GDB当特定事件发生时暂停执行。
GDB中使用delete命令删除断点:
(gdb) help delete
5.1 断点概述
GDB中关于断点“位置”的含义非常灵活,它可以指各种源代码行、代码地址、源代码文件中的行号或者函数的入口等。
例如:
break 35
这里指GDB执行到第34行,但是第35行还没有执行。断点显示的是将要执行的代码行。
5.2 跟踪断点
程序员创建的每个断点(包括断点、监视点和捕获点)都被标识为从1开始的唯一整数标识符。这个标识符用来执行该断点上的各种操作。
5.2.1 GDB中的断点列表
当创建断点时,GDB会告知你分配给该断点的编号。例如,
(gdb) break main
Breakpoint 1 at 0x8048569: file ins.c, line 52.
被分配的编号是1.如果忘记了分配给哪个断点的编号是什么可以使用info breakpoints命令来提示。
(gdb) info breakpoints
Num Type Disp Enb Address What
1 breakpoint keep y 0x08048569 in main at ins.c:52
2 breakpoint keep y 0x0804847e in insert at ins.c:25
3 breakpoint keep y 0x08048491 in insert at ins.c:30
如果想要删除断点,可以通过delete命令以及断点标识符,例如
(gdb) delete 1 2 3
5.3 设置断点
5.3.1 在GDB中设置断点
1)break function
在函数function()的入口处设置断点。
(gdb) break main
在main函数的入口处设置断点
2)break line_number
在当前活动源代码文件的line_number处设置断点。
(gdb) break 35
在当前显示的源文件的35行设置了一个断点。
3)break filename:line_number
在源代码文件filename的line_number处设置断点。如果filename不在当前工作目录中,则可以给出相对路径名或者完全路径名来帮助GDB查找该文件,例如:
(gdb) break source/bed.c:35
4)break filename:function
在文件filename中的函数function()的入口处设置断点。重载函数或者使用同名静态函数的程序可能需要使用这种形式,例如:
(gdb) break bed.c:parseArguments
正如我们看到的,当设置一个断点时,该断点的有效性会持续到删除、禁用或者退出GDB时。然而,临时断点时首次到达后就会被自动删除的断点。临时断点使用tbreak命令设置。
C++允许重载函数,使用break function会在所有具有相同名称的函数上设置断点。如果要在函数的某个特定实例上设置断点,需要没有歧义,则使用源文件中的行号。
int main(void) { int i; i = 3; return 0; }
如果我们尝试在函数main入口处设置断点,断点实际会被设置在第4行。因为GDB会认为第三行的机器码对我们的调试目的来说没有用处。
5.4 多文件中的断点设置
例如:
main.c
#include<stdio.h> void swap(int *a,int *b); int main() { int i=3; int j=5; printf("i:%d,j:%d\n",i,j); swap(&i,&j); printf("i:%d,j:%d\n",i,j); return 0; }
swap.c
void swap(int *a,int *b) { int c=*a; *a=*b; *b=c; }
在main上设置断点:
(gdb) break main
Breakpoint 1 at 0x80483cd: file main.c, line 6.
在swap上设置断点的方法:
(gdb) break swapper.c:1
Breakpoint 2 at 0x804843a: file swapper.c, line 1.
(gdb) break swapper.c:swap
Note: breakpoint 2 also set at pc 0x804843a.
Breakpoint 3 at 0x804843a: file swapper.c, line 3.
(gdb) break swap
Note: breakpoints 2 and 3 also set at pc 0x804843a.
Breakpoint 4 at 0x804843a: file swapper.c, line 3.
每个GDB都有一个焦点,可以将它看作当前“活动”文件。这意味着除非对命令做了限定,否则都是在具有GDB的焦点的文件上执行命令。默认情况下,具有GDB的初始焦点的文件是包含main()函数的文件,但是当发生如下任一动作时,焦点会转移到不同的文件上。
1)向不同的源文件应用list命令
2)进入位于不同的源代码文件中的代码
3)当在不同的源代码文件中执行代码时GDB遇到断点
例如:
(gdb) break 6
Note: breakpoint 1 also set at pc 0x80483cd.
Breakpoint 5 at 0x80483cd: file main.c, line 6.
当前焦点是main.c,所以在main.c中设置。
(gdb) list swap
(gdb) break 6
Breakpoint 6 at 0x8048454: file swapper.c, line 6.
现在的焦点是swapper.c。
(gdb) info breakpoints
Num Type Disp Enb Address What
1 breakpoint keep y 0x080483cd in main at main.c:6
2 breakpoint keep y 0x0804843a in swap at swapper.c:1
3 breakpoint keep y 0x0804843a in swap at swapper.c:3
4 breakpoint keep y 0x0804843a in swap at swapper.c:3
5 breakpoint keep y 0x080483cd in main at main.c:6
6 breakpoint keep y 0x08048454 in swap at swapper.c:6
5.5 断点的持久性
如果在修改和重新编译代码时没有退出GDB,那么在下次执行GDB的run命令时,GDB会感知到代码已修改,并自动重新加载新版本。
5.6 删除和禁用断点
在调试会话期间,有时会发现有的断点不再使用。如果确认不再需要断点,可以删除它。也许你不想删除它,而是打算将它虚置起来,这称为禁用断点。如果以后再次需要,可以重新启用断点。
2.6.1 在GDB中删除断点
如果确认不再需要当前断点,那么可以删除该断点。
delete命令用来基于标识符删除断点,clear命令使用和创建断点的语法删除相同。
1)delete breakpointer_list
删除断点使用数值标识符。断点可以是一个数字,比如delete 2 删除第2个断点;也可以是数字列表,不然delete 2 4 删除第二个和第四个断点。
2)delete
删除所有断点。
3)clear
清除GDB将执行的下一个指令处的断点。这种方法适用于要删除GDB已经到达的断点额情况。
4)clear function、clear filename:function、clear line_number和clear filename:line_number
2.6.2 在GDB中禁用断点
每个断点都可以禁用和启用。只有遇到启用的断点时,才会暂停程序的执行;它会忽略禁用的断点。
为什么要禁用断点呢?在调试会话期间,会遇到大量断点。对于经常重复的循环结构或函数,这种情况使得调试极不方便。如果要保留断点以便以后使用,暂时又不希望GDB停止执行,可以禁用它们,在以后需要时再启用。
使用disable breakpoint-list命令禁用断点,使用enable breakpoint-list命令启用断点。
例如,
(gdb) disable 3
将禁用第三个断点
(gdb) enable 1 5
将启用第一个和第五个断点。
不带任何参数地执行disable命令将禁用所有现有断点。类似的,不带任何参数的enable命令将启用所有断点。
还有一个enable once命令,在得到下次引起GDB暂停执行后被禁用。语法为:
enable once breakpoint-list
例如,enable once 3 会使得断点3 在下次导致GDB停止程序的执行后被禁用。这个命令与tbreak命令非常类似,但是当遇到断点时,它是禁用断点,而不是删除断点。
2.6.3 浏览断点属性
info breakpoints命令(简写 i b)来获得设置的所有断点的清单,以及它们的属性。
例如:
(gdb) info breakpoints
Num Type Disp Enb Address What
1 breakpoint keep y 0x080483cd in main at main.c:6
2 breakpoint keep y 0x0804843a in swap at swapper.c:1
3 breakpoint keep y 0x0804843a in swap at swapper.c:3
4 breakpoint keep y 0x0804843a in swap at swapper.c:3
5 breakpoint keep y 0x080483cd in main at main.c:6
6 breakpoint keep y 0x08048454 in swap at swapper.c:6
7 hw watchpoint keep y counter
让我们分析info breakpoints的这一输出:
1)标识符(num):断点的唯一标识符
2)类型(type):这个字段指出该断点是断点、监视点还是捕获点
3)部署(disp):每个断点都有一个部署,指示断点下次引起GDB暂停程序的执行后该断点上会发生什么事情。
保持(keep),下次到达断点后不改变断点
删除(del),下次到达断点后删除断点,临时断点(tbreak设置)
禁用(dis),下次到达后会禁用断点,使用enable once命令设置的
4)启用状态(enb):这个字段说明断点当前是启用还是禁用的
5)地址(Address):这是内存中设置断点的位置。
6)位置(what):what字段显示了断点所在的位置的行号和文件名
恢复执行的方法有3类。第一类是使用step和next“单步”调试程序,仅执行代码的下一行然后再次暂停。第二类由使用continue组成,使GDB无条件地恢复程序的执行,直到遇到另一个断点或程序结束。最后一类方法涉及条件:用finish或until命令恢复。在这种情况下,GDB会恢复执行;程序继续运行直到遇到某个预先确定的条件(比如,到达函数的末尾),到达另一个断点,或者程序完成。
6.1 使用step和next单步调试
一旦GDB在断点处停止,可以使用next(简写n)和step(简写s)命令来单步调试代码。
这两个命令的不同之处在于它们如何处理函数调用:next执行函数,不会在其中暂停,然后在调用之后的第一条语句处暂停。而step在函数中的第一个语句处暂停。step命令会进入调用的函数,这称为单步进入函数。而next永远不会离开main()。这是两个命令的主要区别。next将函数调用看做一行代码,并在一个操作中执行整个函数,这称为单步越过函数。
然而,似乎next越过调用的函数主体,但是它并未真的单步“越过”任何内容。GDB安静地执行调用函数的每一行,不向我们展示细节。
6.2 使用continue恢复程序执行
第二种恢复执行的方法是使用continue命令,简写为c。这个命令使GDB恢复程序的执行,直到触发断点或者程序结束。
continue命令可以接受一个可选的整数参数n。这个数字要求GDB忽略下面n个断点。例如,continue 3让GDB恢复程序执行,并忽略接下来的3个断点。
6.3 使用finish恢复程序执行
一旦触发了断点,就使用next和step命令逐行执行程序。有时这是一个痛苦的过程。
有时使用step进入的调用的函数,查看了几个变量的信息,如果没有兴趣单步调试其余部分,想返回到单步进入被调用函数之前GDB所在的调用函数。然而,如果要做的只是跳过函数的其余部分,那么再设置一个无关断点并使用continue似乎比较浪费。这是可以使用finish命令。
finish命令(简写为fin)指示GDB恢复执行,直到恰好在当前栈帧完成之后为止。也就是说,这意味着如果你在一个不是main()的函数中,finish命令会导致GDB恢复执行,直到恰好在函数返回之后为止。
虽然可以键入next 3 而不是finish,但是后者更容易。
finish的另一个常见用途是当不小心单步进入原本希望单步越过的函数时(换言之,当需要使用next时使用了step)。在这种情况下,使用finish可以讲你正好放回到使用next会位于的位置。
如果在一个递归函数中,finish只会将你带到递归的上一层。
6.4 使用until恢复程序执行
finish命令在不进一步在函数中暂停(除了中间断点)的情况想完成当前函数的执行。类似地,until命令(简写为u)通常用来在不进一步在循环中暂停(除了循环中的中间断点)的情况下完成正在执行的循环。
当i很大是,使用next需要多次。而使用until会执行循环的其余部分,让GDB在循环后面的第一行代码处暂停。当然,如果GDB在离开循环前遇到一个断点,它就会在那里暂停。
只要启用了断点,调试器就总是在该断点处停止。然而,有时有必要告诉调试器只有当符合某种添条件时才在断点处停止。
7.1 设置条件断点
break break-args if (condition)
其中brea-args是可以传递给break以指定断点位置的任何参数。括着condition的圆括号是可选的。
例如:
break main if argc>1
例如,在循环中,满足一定次数之后发生中断:
break if (i==7000)
条件中断中的condition可以包含如下形式,但是必须是布尔值:
可以对正常断点设置条件以将它转变为条件断点。例如,如果设置了断点3为无条件断点,但是希望添加添加i==3,只有键入:
(gdb) cond 3 i==3
如果以后要删除条件,但是保持该断点,只要键入:
(gdb) cond 3
当GDB遇到断点时,几乎总是要查看某个变量。如果反复遇到同一个断点,将反复查看相同的变量。让GDB在每次到达某个断点时自动执行一组命令,从而自动完成这一过程。
事实上,使用“断点命令列表”就可以做这件事。
使用commands命令设置命令列表。
其中breakpoint-number是要将命令添加到其上的断点的标识符,commands是用行分隔的任何有效GDB命令列表。逐条输入命令,然后键入end表示输入命令完毕。从那以后,每当GDB在这个断点处中断时,它都会执行输入的任何命令。
例如:
fibonacci.c
#include<stdio.h> int fibonacci(int n); int main(void) { printf("Fibonacci(3) is %d\n",fibonacci(3)); return 0; } int fibonacci(int n) { if(n<=0||n==1) return 1; else return fibonacci(n-1)+fibonacci(n-2); }
gdb调试:
如果觉得输出太冗长了,可以使用silent命令使GDB更安静地触发断点。
现在输出结果不错,但是每次要键入continue,可以修改如下:
也可以使用define定义宏来代替:
监视点是一种特殊类型的断点,它类似于正常断点,是要求GDB暂停程序执行的指令。监视点是指示GDB每当某个表达式改变了值就暂停执行的指令。
(gdb) watch i
它会使得每当i改变值时GDB就暂停。
9.1 设置监视点
当变量var存在且在作用域中时,可以通过使用如下命令来设置监视点。
watch var
该命令会导致每当var改变值时GDB都中断。
例如:
#include<stdio.h> int i=0; int main() { i=3; printf("i is %d.\n",i); i=5; printf("i is %d.\n",i); return 0; }
我们每当i大于4时得到通知。因此在main()的入口处放一个断点,以便让i在作用域中,并设置一个监视点以指出i何时大于4.不能在i上设置监视点,因为在程序运行之前,i不存在。因此必须现在main()上设置断点,然后在i上设置监视点。
既然i已经在作用域中了,现在设置监视点并通知GDB继续执行程序。
比如声明数组:
int x[25];
方法是通过键入:
(gdb) p x
但是,如果是动态创建的数组会是什么样呢?比如:
int *x
...
x=(int *)malloc(25*sizeof(int));
如果要在GDB中输出数组,就不能输入:
(gdb) p x
可以简单打印数组地址。或者键入:
(gdb) p *x
这样只会输出数组的一个元素——x[0]。仍然可以像在命令 p x[5]中那样输入单个元素,但是不能简单地在x上使用print命令输出整个数组。
1)在GDB的解决方案
在GDB中,可以通过创建一个人工数组来解决这个问题。如下:
#include<stdio.h> #include<stdlib.h> int *x; void main() { x=(int*)malloc(25*sizeof(int)); x[3]=12; }
然后执行:
我们可以看到,一般形式为:
*pointer@number_of_elements
GDB还允许在适当的时候使用类型强制转换,比如:
(gdb) p (int [25])*x
$2={0,0,0,12,0 <repeats 21 times>}