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.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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容