Linux下GDB调试一篇入魂(GDB调试详解)

2023-11-22  本文已影响0人  Feel_狗焕

目录:
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程序的时候需要注意以下几点:

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调试的文本用户界面,可以方便地显示源代码、汇编和寄存器文本窗口。源代码窗口和汇编窗口会高亮显示程序运行位置并以'>'符号标记。有两个特殊标记用于标识断点,第一个标记用于标识断点类型:

2)、查看源代码:

3)、设置断点与观察断点:

设置断点:两可以使用“行号”“函数名称”“执行地址”等方式指定断点位置。其中在函数名称前面加“*”符号表示将断点设置在“由编译器生成的prolog代码处”。如果不了解汇编,可以不予理会此用法;
删除断点:d: Delete breakpoint的简写,删除指定编号的某个断点,或删除所有断点。断点编号从1开始递增;
启停断点:通过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的时候进行单步跟着进入这类操作。

5)、查看运行时数据:

可以查看代码执行时候的一些变量值、类型、线程、堆栈以及可以为指定变量赋值等等
【print(p)】:查看运行时的变量输出或者修改指定变量的值。当程序中包含多个作用域不同但名称相同的变量或表达式时,可以借助"::"运算符明确指定要查看的目标变量或表达式。(p 变量名称,可查看代码执行后该变量的值);

backtrace:简写命令 bt,用来打印执行的代码过程和堆栈信息,用法为如下:(gdb) backtrace [-full] [n]

【diaplay】:
和 print 命令一样,display 命令也用于调试阶段查看某个变量或表达式的值,它们的区别是,使用 display 命令查看变量或表达式的值,每当程序暂停执行(例如单步执行)时,GDB 调试器都会自动帮我们打印出来,而 print 命令则不会。格式如下"display expr"、"display/fmt expr";

【info】:

6)、设置监视点

要想找到变量在何处被改变,可以使用watch命令设置监视点watchpoint

7)、设置代码执行时环境变量:

1.2、命令使用导图:

gdb-2.jpg

2、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”命令"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
上一篇下一篇

猜你喜欢

热点阅读