6

linux学习6,gdb工具的使用

 3 years ago
source link: https://blog.popkx.com/linux-learning-6-the-use-of-gdb-tools/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本节介绍一下 linux 下的单步调试工具,方便我们后续调试 linux 内核。

8ecc94f0c93712f28a0de735a4a9fa85.png

linux 下程序开发不可避免遇到 bug

在程序开发中,调试和修复 bug 通常会占相当比例的时间,应该没有程序员能够一次性写出绝对完美的代码,除非程序只需要打印 hello world。在《C语言入门第12节,看书都懂,真让写代码却不知道如何入手咋办》一节,我们介绍过“增量式”开发,使用这种模式开发过程中,如果发现了错误,能够在很小的代码范围内查找并解决。然而不幸的是,并不是所有情况下都是“增量式”开发,比如接手别人的有 bug 的代码时,或者帮程序媛妹子们查找 bug 时,bug 可不是一下子就能找到的。

有些 bug 固然一眼就能看出,例如手误写错了初值。但是,有些 bug 可不容易看出,即使最终发现原来辛辛苦苦找到的 bug 原来如此低级。

程序员可能花了一周时间才发现,造成程序产生巨大错误的原因是,自己把应该是 a<=b 语句写成 a<b 了。

有时候找 bug 我们会希望,如果程序能够每执行一行,就停下来一次就好了,这样我们就能够知道这一行代码的执行结果究竟对不对了。程序员们通常称这种调试方法为“单步调试”。事实上,这并不难做到,相信习惯使用 IDE 开发的朋友们都应该使用过单步调试。

e61c933b35342da837d7982ba137e88e.png

很多程序员从 windows 开发转向 linux 开发,最大的不适应就是惯用的 IDE 没法使用了,甚至连图形界面都没了,鼠标居然也用不上了。于是又去找能够在 linux 下使用的 IDE,最好还能和 windows 下的 IDE 功能一样。其实,在哪种环境下开发,只是习惯问题,如果我们一开始就是在 linux 这种没有界面,不用鼠标的环境下做程序开发,突然让在 windows 下开发,肯定也是不适应的。既然坚定了自己的嵌入式开发生涯,就没必要一直迁就于 windows。何况,并不是所有的 linux 环境都允许我们安装 IDE 的。

linux 下开发也能单步调试吗

linux 没有界面,鼠标也几乎没用,也能单步调试吗?答案显然是肯定的,GDB 工具的调试功能绝对不亚于各种 IDE。

gdb 就是一个调试工具,一般来说,GDB主要帮助你完成下面四个方面的功能:

  • 启动程序,可以按照我们自定义的要求随心所欲的运行程序。
  • 可让被调试的程序在指定的调置的断点处停住。(断点可以是条件表达式)
  • 当程序被停住时,可以检查此时程序中所发生的事。
  • 可以改变程序的某些行为,将一个 bug 产生的影响修正从而测试其他 bug。
ffc2fc9ce77b94cd5130269d4f1947f6.png

gdb 工具的使用

我们还是从实例出发,介绍 GDB 工具的使用。现在需要开发一个程序:

要求先定义一个 square 函数用于求整数的平方值,然后在 main 函数中打印从 0 到 9 的平方值

这个要求是容易实现的,请看下面的 C 语言程序:

#include <stdio.h>

int square(int i)
{
    int s;
    s = i*i;
    return s;
}
int main()
{
    int j, k;
    for(j=1; j<10; j++){        //不小心把j=0写成j=1了
        k = square(j);
        printf("square(%d) = %d\n", j, k);
    }
    return 0;
}

因为手误,程序员小明不小心把j=0写成j=1了。现在,我们使用 GDB 工具来单步调试这个程序,首先,编译这个程序:

# gcc t.c -g

-g 参数可以在编译过程中保留调试信息(主要是各种符号),便于调试。使用 GDB 的方式是简单的:

# gdb a.out

这样,我们就进入了 gdb 的环境中:

a9175e0198eab84299009f7d704c1631.png

输入 run,也可以输入缩写 r,按回车:
(gdb) run
Starting program: /lccRoot/C/tmp/a.out 
square(1) = 1
square(2) = 4
square(3) = 9
square(4) = 16
square(5) = 25
square(6) = 36
square(7) = 49
square(8) = 64
square(9) = 81
[Inferior 1 (process 20996) exited normally]
(gdb)

可以看出,gdb 的 run 命令和它的字面意思一样,就是让程序跑起来。可以看出,程序少输出了一条内容。现在使用“单步调试”法来定位错误。首先,需要下断点(程序运行时,遇到断点会停下来),因为假设我们并不知道错误在哪里,所以断点就下在程序入口,也即 main 函数处:

(gdb) b main
Breakpoint 1 at 0x40054b: file t.c, line 14.
(gdb) r
Starting program: /lccRoot/C/tmp/a.out 

Breakpoint 1, main () at t.c:14
14      for(j=1; j<10; j++){
(gdb) l
9   
10  int main()
11  {
12      int j, k;
13  
14      for(j=1; j<10; j++){
15          k = square(j);
16          printf("square(%d) = %d\n", j, k);
17      }
18  
(gdb)
b57f34b3d954b991058d73b8957e29d0.png

下好断点后,输入 r 让程序跑起来。能够发现,这次程序并没有直接运行到最后,而是停在了第 14 行,输入 l(list),可以把第 14 行附近的代码打印出来。接着,我们输入 n(next),就可以一行一行的执行代码了。

(gdb) n
15          k = square(j);
(gdb) n
16          printf("square(%d) = %d\n", j, k);
(gdb) n
square(1) = 1
14      for(j=1; j<10; j++){
(gdb)

到这里,发现程序输出的是 square(1) = 1,这显然是不对的,对照一下代码,错误的位置就很明显了。gdb 的单步调试不仅能发现错误,还能帮助我们理解代码:

(gdb) n
15          k = square(j);
(gdb) s
square (i=2) at t.c:6
6       s = i*i;
(gdb) p s
$4 = 32767
(gdb) p i
$5 = 2
(gdb) n
7       return s;
(gdb) p s
$6 = 4

执行到 k=square(j); 时,输入 s(step into)就可以进入 square 函数的代码块,这时程序停在了 s=i* i; 处,输入 p s,可以把此时内存里 s 的值打印出来。发现 s 居然等于 32767,打印出 i,等于 2,这是为什么呢?因为此时 s=i* i; 还没有执行,s 作为局部变量没有定义初值,所以此时它的值是未知的。输入 n,程序执行完 s=i* i;,发现 s 等于 4,正常了。

721f8a1e9987ec6ea71e0948293ca151.png

关于 C 语言的局部变量,可参考《三分钟弄清楚C语言为何函数退出就不能使用局部变量了,不初始化局部变量会出错吗》一节。

输入 bt:

(gdb) bt
#0  square (i=2) at t.c:7
#1  0x000000000040055e in main () at t.c:15
(gdb)

可以看出,此时程序是从 main 函数进入 square 函数的。gdb 的功能显然还不止于此,本节仅作为抛砖引玉的作用。以后我们调试 linux 内核时,会继续使用 GDB 的。

GDB 的常用命令

gdb 全速运行

  • (gdb)run,也可只输入 r,程序会开始运行,停在第一个断点处。
  • (gdb)continue,也可只输入 c,程序继续运行,停在下一个断点处。

gdb 添加搜索代码源文件路径

  • (gdb)directory [path],也可输入 dir [path],将 path 添加到搜索目录中。

gdb 打印代码

  • gdb + 可执行程序即可进入 gdb 调试:
    输入 l 可以打印出代码,如果想输出指定行号的代码,可以加上数字,例如 l 5。

gdb 断点

下断点有两种方式:
* (gdb)break(也可只输入b) + 函数名
* (gdb)break + 行号
* (gdb)break + 条件。这个命令必须在变量i被定义之后才会成功运行,为了解决这个问题,首先在变量 i 被定义的后一行设置中断,然后使用run命令运行程序,程序暂停后就可以使用watch i==99设置断点了。例如 break 7 if i==99, watch i==99

  • 显示当前gdb的断点信息:(gdb) info break
  • 删除指定的某个断点:(gdb) delete breakpoint 1
  • 该命令将会删除编号为1的断点,如果不带编号参数,将删除所有的断点:(gdb) delete breakpoint
  • 禁止使用某个断点:(gdb) disable breakpoint 1,该命令将禁止断点1,同时断点信息的 (Enb)域将变为 n
  • 允许使用某个断点:(gdb) enable breakpoint 1,该命令将允许断点1,同时断点信息的 (Enb)域将变为 y
  • 清除源文件中某一代码行上的所有断点:(gdb)clear number
    >注:number 为源文件的某个代码行的行号
ea3f82d6756071c35d35d9c26aa206b2.png

变量检查赋值编辑

  • whatis:识别数组或变量的类型
  • ptype:比whatis的功能更强,他可以提供一个结构的定义
  • set variable = value:将值赋予变量
  • print variable = value or p variable = value : 除了显示一个变量的值外,还可以用来赋值

单步执行编辑

  • next 不进入的单步执行
  • step 进入的单步执行如果已经进入了某函数,而想退出该函数返回到它的调用函数中,可使用命令finish

函数调用编辑

  • call name 调用和执行一个函数
    (gdb) call gen_and_sork(1234,1,0)
    (gdb) call printf(“abcd”)
    =4
  • finish 结束执行当前函数,显示其返回值(如果有的话)

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK