C语言工具使用,GDB个人学习笔记

gdb

简介

除错器(debugger),可以在一个精准受控的环境下执行另一个程序。例如: 单步执行程序,跟踪程序,查看变数内容,记忆体地址,以及程序中每一条指令指行完毕後CPU暂存器的变化情况,检视程序呼叫堆叠等等。

gdb,全名为gnu debugger,是在GNU软件系统中的标准除错器,介面为互动式的shell,许多类Unix,如:FreeBSD, Linux等作业系统中都能够使用,支援许多语言,包括C, C++等。

详细的gdb使用手册,可以在shell中输入info gdb查阅。

示范程序

以下为范例程序gdb_example.c

#include <stdio.h>

void swap(int *, int*);

int main(void)
{
    int a = 10;
    int b = 20;
    printf("The old values: a = %d, b = %d.\n", a, b);
    swap(&a, &b);
    printf("The new values: a = %d, b = %d.\n", a, b);
    return 0;
}

void swap(int *p1, int *p2)
{
    int *p = p1;
    p1 = p2;
    p2 = p;
}

符号式除错器(symbolic debugger)

gdb是一种符号式除错器(symbolic debugger),所谓的符号式(symbolic),意思是在程序执行的时候,可以使用在源程序中的变数名称或是函式名称。

为了显示和翻译这一些名称,除错器需要程序中的变数型别,函式型别等资讯,以及可执行档中哪一条指令对应到源程序中哪一行code的讯息,而这一些讯息以符号表(symbol table)的形式出现,当使用gcc加上选项'g'进行编译和链结时,会产生出符号表,这个符号表会被嵌入到可执行档中。

因此,当我们需要对一个程序使用gdb进行debug,需要在gcc加上'g'的原因,是因为gdb需要可执行档中的符号表作为变数和函式的判断依据从而进行操作。

在一个可执行档中嵌入符号表,并不会对执行档效能产生影响,只会影响执行档的档案大小。

进行除错

首先,先使用gcc产生出可执行档,并在可执行档中嵌入符号表

$ gcc -g gdb_example.c -o gdb_example

接下来我们执行gdb_example.out,会得到以下结果

The old values: a = 10, b = 20.
The new values: a = 10, b = 20.

显然这不是我们想要的结果,我们呼叫了swap函式,但a和b这两个变数却没有发生交换,我们可以尝试使用gdb来找寻原因,使用以下指令

$ gdb ./gdb_example

得到以下画面

最下方的Reading symbols from ./gdb_example表示gdb成功读取到执行档内的符号表,gdb可以使用。

上方画面表示除错器已经成功执行gdb_example,但在执行gdb_example之前,会等待使用者输入指令。

gdb在执行每一行指令之前,都会输出(gdb),以提醒使用者输入除错指令,输入list(或是简写成l)可以看到执行档的前10行程序码,在输入一次list可看到後续10行的程序码

在开始执行程序之前,我们可以设置中断点(breakpoint),让程序在某一个地方中断执行,当除错器遇到中断点时,除错器会中断正在进行的程序,提供一个检查目前程序状态的机会,检视完毕後,我们可以继续逐步进行程序,一行一行观察程序的状态,或是我们想要追踪的变数状态。

如果要设置中断点,输入break指令,或是简写成b即可达成目的,例如下方的示范,表示在gdb_example.c的第15行设置中断点

(gdb) b 15

之後我们可以输入run(或是简写成r),开始执行程序


遇见中断点时,程序会中断,并等待使用者输入指令。

我们怀疑swap()函式中可能存在错误,使我们无法成功交换两变数的数值,因此我们希望可以一步步执行swap函式,而实现这个目的,gdb提供了next和step这两个指令供我们使用;

两者的差别为next会执行整行程序码,包含所有的函数呼叫,然後在下一行继续中断目前程序的执行

而step则是根据符号表来决定是否进入函式,如果有效,则会呼叫函式,并在函式的第1行的地方中断程序的执行

我们目前已经进入了函式,并且得到了p1和p2的记忆体地址,分别为
p1 = 0x55555555526d, p2 = 0x7fffffffdf96,输入s後来到第17行,并执行int * p = p1,这时候我们可以检查变数的值是否正确,我们可以使用print来检视变数内容

我们可以确定p1和p2中的数值是正确的,接着我们可以继续执行程序,使用n(next),除错器会分别执行18, 19, 20这三行程序码,此时我们还在swap函式中,尚未离开函式

gdb会显示目前执行到的行数,以及程序码的内容,此时我们可以使用print检视p1和p2的内容

可以看到p1和p2的数值确实发生了交换,但,这时候我们需要确认交换的是不是传入的a和b,前面我们已经知道了a和b的记忆体地址,这里我们可以试着查看p1和p2的记忆体地址确认这一件事情

这里我们发现到了一件事情,理想上经过交换之後,p1应该是代表b,p2应该是要代表a,也因此p1的记忆体地址应该为0x7fffffffdf96,p2应该为0x55555555526d,因此我们确定了一件事情,p1指标和p2指标确实交换了数值,但不是由* p1和* p2记忆体地址去进行交换的,而这就是swap()函式所发生的错误,是要交换* p1和* p2所指向的整数,而不是储存p1和p2变数的记忆体地址。我们可以使用quit指令,结束gdb,接着去修改程序。

修改後的swap如下

void swap(int *p1, int *p2)
{
    int tmp = *p1;
    *p1 = *p2;
    *p2 = temp;
}

执行结果如下:

p.s: 我们在启动gdb时,会有许多的版权宣告或是其他无关的讯息等等,我们可以透过以下参数让gdb不显示这一些讯息并启动

$ gdb -silent

启动之後我们可以再利用file指令去启动我们想要进行除错的程序

指令选项

gdb的指令选项大多数可以分为长选项和短选项这两种格式。下面列出几个常用的选项。
长选项,例如"-tty device",这个选项需要额外的参数才能够运作,我们可以使用空格或是'='进行分割,例如'-tty=/dev/tty6',选项前可以加上连字符号'-',例如'-quiet'和'--quiet',这两者的功能是相同的。

举例:

  1. --version, -v 显示gdb的版本和版权宣告,然後退出gdb
  2. --quiet, --silent 在不显示版本和版权讯息的情况下启动gdb
  3. --help, -h 显示gdb的指令语法,然後退出

gdb指令

  1. help ( h ):显示指令简短说明。例:help breakpoint
  2. file:开启档案。等同於 gdb filename
  3. run ( r ):执行程序,或是从头再执行程序。
  4. kill:中止程序的执行。
  5. backtrace ( bt ):显示程序呼叫的堆叠(stack)。。会显示出上层所有的 frame 的简略资讯。
  6. print ( p ):印出变数内容。例:print i,印出变数 i 的内容。
  7. list ( l ):印出程序码。若在编译时没有加上 -g 参数,list 指令将无作用。
  8. whatis:印出变数的型态。例: whatis i,印出变数 i 的型态。
  9. breakpoint (b, bre, break):设定中断点
    使用 info breakpoint (info b) 来查看已设定了哪些中断点。
    在程序被中断後,使用 info line 来查看正停在哪一行。
  10. continue (c, cont):继续执行。和 breakpoint 搭配使用。
  11. frame:显示正在执行的行数、副程序名称、及其所传送的参数等等 frame 资讯。
     frame 2:看到 #2,也就是上上一层的 frame 的资讯。
  12. next ( n ):单步执行,但遇到函式时会将呼叫的函式作为一个语句执行。
  13. step ( s ):单步执行。但遇到函式时会进入呼叫的函式执行。
  14. up:直接回到上一层的 frame,并显示其 stack 资讯,如进入点及传入的参数等。
  15. up 2:直接回到上三层的 frame,并显示其 stack 资讯。
  16. down:直接跳到下一层的 frame,并显示其 stack 资讯。
     必须使用 up 回到上层的 frame 後,才能用 down 回到该层来。
  17. info:显示一些特定的资讯。如: info break,显示中断点,
    info share,显示共享函式库资讯。
  18. disable:暂时关闭某个 breakpoint 或 display 之功能。
  19. enable:将被 disable 暂时关闭的功能再启用。
  20. clear/delete:删除某个 breakpoint。
  21. attach PID:载入已执行中的程序以进行除错。其中的 PID 可由 ps 指令取得。
  22. detach PID:释放已 attach 的程序。
  23. shell:执行 Shell 指令。如:shell ls,呼叫 sh 以执行 ls 指令。
  24. quit:离开 gdb。或是按下 Ctrl+C 也行。
  25. 按下Enter:直接执行上个指令

比较step和next,与backtrace的使用

范例程序

#include <stdio.h>

int func();
int main(void)
{
        printf("Hello World");
}

int func()
{
        printf("in func");
}
  • 使用next进行逐步执行结果

    • 我们使用bt去查看next的呼叫结果
  • 使用step进行逐步执行结果

    • 我们使用bt去追踪step的呼叫结果

我们发现使用step或是next最终的输出皆是Hello Worldin func,两者的差别即是在函式中,如果碰到了printf这种函式,step会直接输出printf的结果,以这个范例为例,如果是在main函式中碰到printf函式,便会直接印出Hello World。

但如果是step,则会进入到printf函式,并去追踪printf的呼叫,我们使用bt这个指令发现到这一个现象,step去追踪printf,发现printf呼叫了_vfprintf_internal,继续使用step,接着追踪,发现_vfprintf_internal呼叫了_IO_new_file_xsputn,不断的追踪下去

我们在上方有使用到backtrace(bt)指令,去追踪程序呼叫的堆叠,我们也可以使用frame这个指令去检视目前呼叫的堆叠,frame後方加上数字表示显示第几层堆叠的资讯

info这个指令可以显示出任何我们想要的资讯,例如输入info breakpoint可以显示我们所下的所有中断点

检查点(watchpoint)

在gdb中,可以透过设定检查点(watchpoint)来监测变数的读写操作,以及变化。检查点类似中断点,但差别在於检查点并没有绑定源程序码中某一行程序码,如果针对某一个变数设置检查点,当该变数发生变化时,gdb就会中断程序,和中断点的差别为不会在是碰到某一行被中断点标记的程序码停下,而是标记的变数发生变化时。

检查点不仅可以用来观察变数,也可以用来观察表达式中的变数值。可以使用watch, rwatch, awatch指令来设定不同的检查点

  1. watch expression 当expression的值发生改变时,程序中断进行
  2. rwatch expression 当程序读取与计算与expression相关的任何对象时,程序中断进行
  3. awatch expression 当程序读取或修改与计算expression相关的任何对象时,程序中段进行

检查点最常见的用途为检查程序'何时'修改某一个变数,当一个变数改变时,gdb会显示被检查的变数旧的数值和新的数值,以及下一行要执行的程序。以下方的程序作为范例

#include <stdio.h>

int main(void)
{
        int a = 10;
        int b = 20;
        int *iPtr = &a;

        ++*iPtr;
        puts("This is the statement following ++*iPtr.");

        printf("a = %d; bb = %d.\n", a, b);
        return 0;
}

我们先将中断点设置在第9行,之後开始执行程序

我们对变数a设置检查点,指令为watch a

之後使用continue继续执行程序,直到a发生了改变,程序中断

因为第9行的iPtr指向变数a,当第9行执行++*iPtr时,a发生了变化,因此程序中断,并显示下一行准备被执行的程序码。

我们继续执行这一个例子,我们为b设定一个读取检查点,指令为rwatch b,这个检查点会被包含在中断点列表中,使用info breakpoints指令可以确认是否在其中

我们可以发现目前有三个中断点,分别为在源程序码第9行的中断点,变数a的检查点,变数b的检查点,接着继续执行程序

发现在程序码第12行的地方会对变数b进行读取,因此程序进行中断,接着继续执行

当程序离开一个程序区块(被大括号包住的区块),该区域内变数的检查点会被自动删除

继续上方这个范例程序码,我们可以看看对整个表达式设置检查点会发生甚麽事情,
指令为rwatch a + b

我们让程序继续进行,会发现每一次只要a+b的值发生读取或是计算,程序便会中断



在第9行尝试读取a的值,中断一次,改变a的值,再中断一次
第12行a+b被读取了两次,分别先读取a,再读取b

总共中断4次

核心档案(core file)

核心档案(core file),也被称作为核心转存档案(core dump),当程序在执行的过程中发生了异常的中止或是非法存取记忆体,作业系统会将当时的记忆体状态记录下来,这个状态包括暂存器状态,程序堆叠等等,然後把这一些资讯储存成一个档案,而这个档案被称为核心转存档案(core dump),这个档案可以有效的协助我们对程序进行除错。

举例来说,如果我们非法存取记忆体时,程序会回报Segmentation fault这行字,但这样的资讯是无法帮助我们除错的,我们必须知道是哪一行程序码触发了这个错误,因此,我们必须使用core file来除错。

通过gdb来分析核心档案时,与一般的除错不同,因为程序已经被中断,我们无法通过bt, next, step这些指令来除错,假设我们执行一个已经经过'-g'选项编译过的档案,执行core_dump这个程序
以下为core_dump.c

#include <stdio.h>

int main(void)
{
    int *b;
    
    
    scanf("%d", b);
    return 0;
}

使用为初始化的指标,造成Segmentation fault,我们执行core_dump的执行档看看

./core_dump


产生了错误,此时在该执行档所在的资料夹会产生一个名称为core的档案,这个档案即为刚刚发生Segmentation fault所产生的,也就是核心转存档案,我们使用gdb来检视他

gdb ./core_dump core

执行画面如下

里头告诉我们Core是由执行core_dump这个执行档所产生,下面则有一些堆叠呼叫的资讯,我们得知在呼叫_vfscanf_internal时发生了错误,但不知道是由哪一个函式呼叫了__vfscanf_internal,我们可以使用bt来检视这个程序的函式堆叠

我们可以看到在程序第8行呼叫了__isoc99_scanf这个函式,而这个函式在第30行的地方呼叫了__vfscanf_internal这个函式,结果产生了Segmentation fault,__isoc99_scanf和vfscanf_internal皆为C的标准函式库,因此发生错误的地方是在程序码第8行scanf的地方,因为存取为初始化的指标而发生的错误,我们可以使用print b把b给印出来便会发现。

有了core file这种除错技巧,当我们遇到Segmentation fault这种状况时,便可以得到有用的资讯并进行除错了。

p.s 如果程序进行完毕後没有产生core file,我们需要执行以下指令

ulimit -c unlimited

让系统产生出core file。

新手发文,请多指教~

参考资料

https://b8807053.pixnet.net/blog/post/336154079-%5B%E8%BD%89%E8%B2%BC%5Dgdb-%E4%BB%8B%E7%B4%B9
https://www.cnblogs.com/J1ac/p/9113669.html
https://www.cyut.edu.tw/~ckhung/b/c/gdb.php
C语言核心技术第二版


<<:  Day 27 (Js)

>>:  战略层次(Levels of Strategy)

让Python GUI 画面在最前方显示/ 最小化显示/ 隐藏桌面图示的方法

很多时候,刚程序开启後有可能因为使用者操作的因素就让正在跑的程序被盖住了。尤其是在使用键盘滑鼠精灵控...

Day 3 - 新人报到前的准备与莫名的焦虑感

确定了offer也确定了报到时间後,距离到职日大概还有两周多的时间,因为自己是北漂青年因此开始寻找後...

Log Agent - Fluent Bit 简介

前几篇讲这麽多, 来介绍一个服务Fleunt Bit Fleunt Bit 它是一个开源的数据收集器...

30天学会C语言: Day 22-阵列处理

阵列比较 C 语言没办法透过比较运算检查两个阵列(或字串)是否相等,必须要透过阵列对每个圆一个一个做...

为了转生而点技能-javascript,day4(初探型别

动态型别 定义:变数会因为值性质的不同,而在***执行阶段***才会赋予确立型别有不同的型别;同一个...