Linux下GDB调试一篇入魂(GDB调试详解)
目录:
1、使用
1.1、常用命令
1.2、命令使用导图
2、GDB调试方式
2.1、GDB的动态调试启动方法
2.2、core文件调试
3、使用示例
【简介】:
GDB是 Linux 下常用的程序调试器。发展至今,GDB 已经迭代了诸多个版本,当下的 GDB 支持调试多种编程语言编写的程序,包括 C、C++、Go、Objective-C、OpenCL、Ada等。实际场景中,GDB 更常用来调试 C 和 C++程序。
总的来说,借助 GDB调试器可以实现以下几个功能:
程序启动时,可以按照我们自定义的要求运行程序,例如设置参数和环境变量;
可使被调试程序在指定代码处暂停运行,并查看当前程序的运行状态(例如当前变量的值,函数的执行结果等),即支持断点调试;
程序执行过程中,可以改变某个变量的值,还可以改变代码的执行顺序,从而尝试修改程序中出现的逻辑错误。
安装
$ yum -y install gdb
或
$ apt-get install gdb
注意:目前支持调试Go程序的GDB版本必须大于7.1。编译Go程序的时候需要注意以下几点:
- 传递参数-ldflags "-s",忽略debug的打印信息;
- 传递-gcflags "-N -l" 参数,这样可以忽略Go内部做的一些优化,聚合变量和函数等优化,这样对于GDB调试来说非常困难,所以在编译的时候加入这两个参数避免这些优化;
1、使用:
GDB 的主要功能就是监控程序的执行流程。这也就意味着,只有当源程序文件编译为可执行文件并执行时,并且该文件中必须包含必要的调试信息(比如各行代码所在的行号、包含程序中所有变量名称的列表(又称为符号表)等),GDB才会派上用场。所以在编译时需要使用 gcc/g++ -g 选项编译源文件,才可生成满足 GDB 要求的可执行文件;
1.1、常用命令:
pwd: 查看gdb当前的工作路径;
cd : 改变gdb当前的工作路径;
info terminal:显示gdb当前所使用的终端的类型信息;
1)、运行程序:
file <文件名>:加载被调试的可执行程序文件。因为一般都在被调试程序所在目录下执行GDB。
run(r)运行程序,如果要加参数,则是run arg1 arg2 ...
start:如果需要断点在main()处,直接执行start就可以
可以直接使用gdb 加文件进行调试,或者启用tui用户界面来调试,TUI(TextUserInterface)为GDB调试的文本用户界面,可以方便地显示源代码、汇编和寄存器文本窗口。源代码窗口和汇编窗口会高亮显示程序运行位置并以'>'符号标记。有两个特殊标记用于标识断点,第一个标记用于标识断点类型:
- B:程序至少有一次运行到了该断点
- b:程序没有运行到过该断点
- H:程序至少有一次运行到了该硬件断点
- h:程序没有运行到过该硬件断点
2)、查看源代码:
- list查看原代码(list-n,从第n行开始查看代码。list+ 函数名:查看具体函数),简写l:
- list numb:指定查看行的代码,未指定行数则默认查看最近十行源码;
- list fun:查看fun函数源代码;
- list file:fun:查看flie文件中的fun函数源代码,如list test.c:5,10显示源文件test.c第5行到第10行的代码,一般用于调试含多个源文件的程序;
3)、设置断点与观察断点:
设置断点:两可以使用“行号”“函数名称”“执行地址”等方式指定断点位置。其中在函数名称前面加“*”符号表示将断点设置在“由编译器生成的prolog代码处”。如果不了解汇编,可以不予理会此用法;
删除断点:d: Delete breakpoint的简写,删除指定编号的某个断点,或删除所有断点。断点编号从1开始递增;
启停断点:通过disable/enable临时停用启用设置的断点;
- b <行号>:在指定行设置断点,如:(gdb) b 8;
- b <函数名称>/*<函数名称>:给指定函数设置断点,如:(gdb) b main/(gdb) b *main
- b <代码地址>:在代码指定的内存地址上设置断点,如:(gdb) b *0x804835c;
- break if<condition>:条件成立时程序设置断点,如:break sum if value==9,判断sum函数当输入的value为9的时候才会断住;
- info break(缩写:i b):查看所有设置的断点;
- watch expr:一旦expr值发生改变,程序设置断点;
- delete n:删除断点,如删除所有断点:(gdb) d;
- disable/enable 断点编号:临时停用/启用指定编号的断点,这样就会忽略该断点;
断点高级功能设置:
#include <stdio.h>
int total = 0;
int square(int i)
{
int result=0;
result = i*i;
return result;
}
int main(int argc, char **argv)
{
int i;
for(i=0; i<10; i++)
{
total += square(i);
}
return 0;
}
比如需要对如上程序square参数i为5的时候断点,并在此时打印栈、局部变量以及total的值,编写gdb.init,然后在gdb中加载,内容如下:
set logging on overwrite gdb.log #---将显示log保存到gdb.log中
b square if i == 5 #----在函数square接收到的变量i为5时候为其设置断点,这里设置上的是if条件语句格式为if...else...end
commands #---断点后,执行如下命令
bt full #---打印代码执行堆栈信息和全部变量
i locals #--打印当前变量信息
p total #--打印total变量值
print "Hit break when i == 5"
end
4)、单步调试:
调试类似在ide中debug的时候进行单步跟着进入这类操作。
- continue(c):运行至下一个断点;
- step(s):单步跟踪,进入函数,类似于ide中的step in;
- next(n):单步跟踪,不进入函数,类似于ide中的step out;
- finish:finish 命令和 return命令的区别是,finish命令会执行函数到正常退出;而 return 命令是立即结束执行当前函数并返回,也就是说,如果当前函数还有剩余的代码未执行完毕,也不会执行了;
- until(u):until 命令并非任何情况下都会发挥这个作用,只有当执行至循环体尾部(最后一行代码)时,until命令才会发生此作用;反之,until命令和 next 命令的功能一样,只是单步执行程序;
5)、查看运行时数据:
可以查看代码执行时候的一些变量值、类型、线程、堆栈以及可以为指定变量赋值等等
【print(p)】:查看运行时的变量输出或者修改指定变量的值。当程序中包含多个作用域不同但名称相同的变量或表达式时,可以借助"::"运算符明确指定要查看的目标变量或表达式。(p 变量名称,可查看代码执行后该变量的值);
- print file::variable:其中file用于指定具体的文件名,variable表示要查看的目标变量或表达式,即表达要在那个文件下输出指定的变量值;
- print function::variable:funciton 用于指定具体所在函数的函数名,variable表示要查看的目标变量或表达式。即表达要在那个函数下输出指定的变量值;
- ptype:查看类型,ptype 变量名称,可用于查看变量的类型;
- print array:打印数组所有元素;
- print *array@len:查看动态内存,len是查看数组array的元素个数;
- print x=5:改变x变量运行时数据;
- set x=5:该命令用来改变运行过程中的变量值,格式如:set variable <var>=<value>;
backtrace:简写命令 bt,用来打印执行的代码过程和堆栈信息,用法为如下:(gdb) backtrace [-full] [n]
- bt -full用于打印帧栈信息的同时,打印局部变量的值;
- n:一个整数值,当为正整数时,表示打印最里层的 n 个栈帧的信息。n为负整数时,那么表示打印最外层n个栈帧的信息;
注意,当调试多线程程序时,该命令仅用于打印当前线程中所有栈帧的信息。如果想要打印所有线程的栈帧信息,应执行"thread apply all backtrace"命令。
【diaplay】:
和 print 命令一样,display 命令也用于调试阶段查看某个变量或表达式的值,它们的区别是,使用 display 命令查看变量或表达式的值,每当程序暂停执行(例如单步执行)时,GDB 调试器都会自动帮我们打印出来,而 print 命令则不会。格式如下"display expr"、"display/fmt expr";
- display 变量名:实时跟踪并输出该变量的变化信息;
- undisplay:取消display的设置;
【info】:
- info命令后面跟各种子命令可以查看对应的子命令的详细信息,如:
- info break:可以查看所有断点信息;
- info inferiors:查看当前进程信息;
- info threads:查看当前线程,然后可以使用"thread 编号"切换到指定的线程中;
6)、设置监视点
要想找到变量在何处被改变,可以使用watch命令设置监视点watchpoint
- watch <表达式>:表达式发生变化时暂停运行;
- awatch<表达式>:表达式被访问、改变是暂停执行;
- rwatch<表达式>:表达式被访问时暂停执行;
7)、设置代码执行时环境变量:
- show environment/env [VARNAME]:显示程序的环境变量VARNAME的值;如果不指明环境变量名,那么该命令将显示所有环境变量的值;
- set environment/env VARNAME [=] VALUE:设置程序的某个环境变量VARNAME的值;不过,只对你所调试的程序有效,对gdb本身不起作用;
- unset environment/env VARNAME:删除程序的某个环境变量VARNAME;
1.2、命令使用导图:
gdb-2.jpg2、GDB调试方式:
2.1、GDB的动态调试启动方法:
动态调试就是在不终止正在运行的进程的情况下来对这个正在运行的进程进行调试;其启动方式有两种:
方式一:
gdb <可执行程序名> <进程ID>:
比如: gdb <可执行程序名> 1234
这条命令会把进程ID为1234的进程与gdb联系起来,也就是说,这条命令会把进程ID为1234的进程的地址空间附着在gdb的地址空间中,然后使这个进程在gdb的环境下运行,这样的话,gdb就可以清楚地了解该进程的执行情况、函数堆栈、内存使用情况,等等;
方式二:
直接在gdb中把一个正在运行的进程连接到gdb中,以便于进行动态调试;使用attach命令,attach <进程ID>:
当使用attach命令时,你应该先使用file命令来指定进程所联系的程序源代码和符号表;当gdb接到attach命令后的第一件事情就是停止进程的运行,你可以使用所有gdb的命令来调试一个已"连接"到gdb的进程,这就像你使用run/r命令在gdb中启动它一样,如果你要进程继续运行,那么可以使用continue/c命令就可以了;
detach:
当你调试结束之后,可以使用该命令断开进程与gdb的连接(结束gdb对进程的控制),在这个命令执行之后,你所调试的那个进程将继续运行;如果你在使用attach命令把一个正在运行的进程连接到gdb之后又退出了gdb,或者是使用run/r命令执行了另外一个进程,那么刚才那个被连接到gdb的进程将会因为收到一个kill命令而退出;
如果要使用attach命令,你的操作系统环境就必须支持进程;另外,你还需要有向进程发送信号的权力;使用attach命令的例子:
(gdb) file <可执行程序名> #---指定进程所关联的程序源代码和符号表
(gdb) attach <进程ID>
.....
使用gdb的命令进行调试;
.....
(gdb) detach #----调试结束,解除进程与gdb的连接,使进程继续运行
2.2、core文件调试:
在程序崩溃时,一般会生成一个文件叫core文件。core文件记录的是程序崩溃时的内存映像,并加入调试信息,core文件生成过程叫做core dump(核心已转储)。系统默认不会生成该文件。
1)、设置生成core文件:
ulimit -c:查看core-dump状态。
ulimit -c xxxx:设置core文件的大小。
ulimit -c unlimited:core文件无限制大小。
2)、gdb调试core文件:
当设置完ulimit -c xxxx后,再次运行程序发生错误,此时就会生成一个core文件,使用gdb core调试core文件,使用bt命令打印栈回溯信息。
3、使用示例:
3.1、c语言调试示例
1)、编译c代码:
$ gcc gdb-sample.c -o gdb-sample -g
/*
gdb-sample
*/
#include <stdio.h>
int nGlobalVar = 0;
int tempFunction(int a, int b)
{
printf("tempFunction is called, a = %d, b = %d \n", a, b);
return (a + b);
}
int main()
{
int n;
n = 1;
n++;
n--;
nGlobalVar += 100;
nGlobalVar -= 12;
printf("n = %d, nGlobalVar = %d \n", n, nGlobalVar);
n = tempFunction(1, 2);
printf("n = %d", n);
return 0;
}
2)、启动gdb,加载要调试的应用程序:
(gdb) file gdb-sample
下面使用“r”命令执行(Run)被调试文件,因为尚未设置任何断点,将直接执行到程序结束:
(gdb) r
Starting program: /root/gdb-sample
n = 1, nGlobalVar = 88
tempFunction is called, a = 1, b = 2
n = 3
Program exited normally.
下面使用“b”命令在 main 函数开头设置一个断点(Breakpoint):
(gdb) b main
Breakpoint 1 at 0x804835c: file gdb-sample.c, line 19.
上面最后一行提示已经成功设置断点,并给出了该断点信息:在源文件 gdb-sample.c 第19行处设置断点;这是本程序的第一个断点(序号为1);断点处的代码地址为 0x804835c(此值可能仅在本次调试过程中有效)。回过头去看源代码,第19行中的代码为“n = 1”,恰好是 main 函数中的第一个可执行语句(前面的“int n;”为变量定义语句,并非可执行语句)。
再次使用“r”命令执行(Run)被调试程序:
(gdb) r
Starting program: /root/gdb-sample
Breakpoint 1, main () at gdb-sample.c:19
19 n = 1;
程序中断在gdb-sample.c第19行处,即main函数是第一个可执行语句处。上面最后一行信息为:下一条将要执行的源代码为“n = 1;”,它是源代码文件gdb-sample.c中的第19行。
下面使用“s”命令(Step)执行下一行代码(即第19行“n = 1;”):
(gdb) step
20 n++;
上面的信息表示已经执行完“n = 1;”,并显示下一条要执行的代码为第20行的“n++;”。既然已经执行了“n = 1;”,即给变量 n 赋值为 1,那我们用“p”命令(Print)看一下变量 n 的值是不是 1 :
(gdb) print n
$1 = 1
果然是 1。(1 表示该变量 表示该变量所在存储区的名称",再次执行“p n”将显示“$2 = 1”此信息应该没有什么用处。)
下面我们分别在第26行、tempFunction 函数开头各设置一个断点(分别使用命令“b 26”“b tempFunction”):
(gdb) b 26
Breakpoint 2 at 0x804837b: file gdb-sample.c, line 26.
(gdb) b tempFunction
Breakpoint 3 at 0x804832e: file gdb-sample.c, line 12.
使用“c”命令继续(Continue)执行被调试程序,程序将中断在第二个断点(26行),此时全局变量 nGlobalVar 的值应该是 88;再一次执行“c”命令,程序将中断于第三个断点(12行,tempFunction 函数开头处),此时tempFunction 函数的两个参数 a、b 的值应分别是 1 和 2:
(gdb) c
Continuing.
Breakpoint 2, main () at gdb-sample.c:26
26 printf("n = %d, nGlobalVar = %d /n", n, nGlobalVar);
(gdb) p nGlobalVar
$2 = 88
(gdb) c
Continuing.
n = 1, nGlobalVar = 88
Breakpoint 3, tempFunction (a=1, b=2) at gdb-sample.c:12
12 printf("tempFunction is called, a = %d, b = %d /n", a, b);
(gdb) p a
$3 = 1
(gdb) p b
$4 = 2