gdb 调试工具学习

1.兹声明,此文章为本人原创,文责自负。
2.如需转载,麻烦您联系作者,并取得作者的明示同意。感谢。

工作中遇到一个c++ 进程crash的问题,在分析解决的过程的中用到了gdb,结合搜索的相关资料和例子,对gdb做下简单总结,完整的gdb 文档请参看 The GNU Project Debugger, 文中例子运行的机器为 Fedora 31.

1. 什么是 gdb ?

gdb 是一个程序调试工具!
(1). 一个可以让你清晰的知道程序在整个执行过程中每一步做了什么的工具!!!
(2). 多用于c/c++项目的调试,当然也支持一些别的语言。

2. gdb 能做什么?

(1) 在执行/运行一个可执行文件/程序后,正常情况下,我们能够看到的仅仅是标准输出/或者执行结果,对于程序的每一步执行情况,我们只能够通过源码去猜测实际中执行的样子,显然有的时候结果并不一定是理想结果,那我们此时一定好奇该程序这个时候做了什么呢?
(2) 我想单步调试自己的程序,就像在学c语言的时候用visual studio 6.0去单步调试和加断点。
(3) 自己的一个程序crash了,我想知道为啥crash?

当你在实际的工作中,你一定面对过这样的需求/疑问,好吧,gdb 就是用来解决以上痛点的!
简而言之,gdb 可以帮助你的事情有:
1). 加载程序后,以单步/断点/条件等各种方式运行,并可实时的查看运行过程中各种调用关系及各变量状态
2). 分析coredump,并打印crash时的堆栈及各状态,找到程序crash的原因

3. gdb 怎么用?

在bash中,执行 gdb, 如果安装了gdb tool,正常会进入到一个gdb的界面上。按一下help里面列出了很多命令可以使用。
还是从前面的两个用途处着手,看下常用的gdb 是怎样执行的。

1). 调试程序:

源文件例子:

#include <iostream>                                                              
#include <assert.h>                                                              
                                                                                 
                                                                                 
static int sum (int numberOne, int numberTwo);                                   
static int divide(int numberOne, int numberTwo);                                 
                                                                                 
int main() {                                                                     
//assert(false);                                                                 
  int a = 0;                                                                     
  int b = 5;                                                                     
  int c = sum(a, b);                                                             
  int d = divide(a, b);                                                          
  return 0;                                                                      
}                                                                                
                                                                                 
static int sum (int numberOne, int numberTwo) {                                  
  return numberOne + numberTwo;                                                  
}                                                                                
                                                                                 
static int divide(int numberOne, int numberTwo) {                                
  return numberOne / numberTwo;                                                  
}

(1). 在使用gdb 去调试程序的时候,首先你得先编译出这一个程序。在编译的时候,请一定注意要使用 -g 的编译参数(将程序指定于可供调试)。例如 g++ -g main.cpp -o test 去编译一个 test的程序。
(2). 先加载程序到gdb中,命令gdb <program> or 先 gdb 进入到gdb的控制台,然后file <program>。请将<program>改为你要调试的程序名。例: 运行 gdb test
(3). 这个时候程序加载到gdb中了,请运行下 run来让程序跑起来吧。
(4). 运行run后,程序就会从头开始执行,如果没有问题这个程序就会正常退出,exit 0.这个过程中,也不会有什么输出。那么如果我想要让程序不一下执行而是在某些地方停下呢。请使用断点命令 break <source file>:<line number>。例如 break main.cpp:3,不仅可以对具体的行加断点还可以在某个函数处加断点。例如break sum,这个时候重新run下,可以看到程序会先停在断点main.cpp的第三行, 如下

Starting program: /home/tli/docs/test 

Breakpoint 1, main () at main.cpp:10
Breakpoint 1, main () at main.cpp:10
10    int a = 0;
(gdb)

continue 可以直接跳到下一个断点,step 可以让你跳到下一行。另外直接按 enter, 什么都不输入,将重复上一个命令。按 continue 后,如下所示。

Breakpoint 3, sum (numberOne=0, numberTwo=5) at main.cpp:18
18    return numberOne + numberTwo;
(gdb) 

直接跳到了我们加的sum 断点处,其实我们可以看到入参是0和 5,继续按step, step, 跳两步就看到程序跑到了

(gdb) step
19  }
(gdb) step
main () at main.cpp:13
13    int d = divide(a, b);

你也可以使用 print 命令去打印相关变量的值,比如看下这个时候a, b的值。print a print b

(gdb) print a
$11 = 0
(gdb) print b
$12 = 5

这个print 还是蛮强大的,还可以打印对象。在工作中遇到的例子中可以使用 print *this 来打印当前的对象的信息,还可以print map对象里面的内容。其余的命令再慢慢探索吧。
到这里,对调试程序的过程应该有了初步的了解了。

2). 分析core dump

对面前的程序简单做一个修改,来对gdb 如何分析core dump进行说明。
程序例子:

  1 #include <iostream>                                                              
  2 #include <assert.h>                                                              
  3                                                                                  
  4                                                                                  
  5 static int sum (int numberOne, int numberTwo);                                   
  6 static int divide(int numberOne, int numberTwo);                                 
  7                                                                                  
  8 int main() {                                                                     
  9 //assert(false);                                                                 
 10   int a = 0;                                                                     
 11   int b = 5;                                                                     
 12   int c = sum(a, b);                                                             
 13   int d = divide(b, a);                                                          
 14   return 0;                                                                      
 15 }                                                                                
 16                                                                                  
 17 static int sum (int numberOne, int numberTwo) {                                  
 18   return numberOne + numberTwo;                                                  
 19 }                                                                                
 20                                                                                  
 21 static int divide(int numberOne, int numberTwo) {                                
 22   return numberOne / numberTwo;                                                  
 23 }

这个时候 g++ -g main.cpp -o test1,编译出可执行文件test1。我们在 shell 中运行一下我们的程序 ./test1

[tli@localhost docs]$ ./test1 
Floating point exception (core dumped)

这里告诉了我们是一个Floating point exception。我们暂且当作没看到它,仍然用解析core dump的方式去分析。因为很多情况下,我们只拿到的是一个dump文件/程序没有预兆的crash了。那么此例子中./test1程序产生的core dump文件去哪里了呢? linux下有一个文件/proc/sys/kernel/core_pattern来说明了产生的core dump文件的名字格式。执行下cat /proc/sys/kernel/core_pattern,可以看到是使用ststemd-coredump去指定的。如下示

[tli@localhost docs]$ cat /proc/sys/kernel/core_pattern
|/usr/lib/systemd/systemd-coredump %P %u %g %s %t %c %h

其实我们可以覆盖此文件,来指定输出的coredump文件目录和格式。那么使用systemd-coredump产生的目录是在 /var/lib/systemd/coredump中。如下我们看到我们的文件

[liyang@localhost coredump]$ ls -al
total 80
drwxr-xr-x. 2 root root  4096 Jan 15 15:19 .
drwxr-xr-x. 6 root root  4096 Nov 13 19:59 ..
-rw-r-----+ 1 root root 68558 Jan 15 15:07 core.test1.1000.e283c0ef6c8a489f82332660910c539c.44694.1579072044000000000000.lz4
[tli@localhost coredump]$ pwd
/var/lib/systemd/coredump

这个core.test1.* 就是我们要找的test1 产生的core dump文件了,这个格式是lz4压缩格式的,直接将它作为core dump文件传入 gdb, 会有一个 is not a core dump: File format not recognized 的报错。gdb 是不认这个格式的,解压缩它。

sudo lz4 core.test1.1000.e283c0ef6c8a489f82332660910c539c.44694.1579072044000000000000.lz4

这个解压缩的文件 core.test1.1000.e283c0ef6c8a489f82332660910c539c.44694.1579072044000000000000,就是我们要的core dump文件了。
有了coredump文件后,开始解析它。

  1. gdb <program> <core dump>
gdb test1 core.test1.1000.e283c0ef6c8a489f82332660910c539c.44694.1579072044000000000000
  1. 然后我们就可以看到程序直接是停在了的如下图所示的divide函数中。
Core was generated by `./test1'.
Program terminated with signal SIGFPE, Arithmetic exception.
#0  0x00000000004011a9 in divide (numberOne=5, numberTwo=0) at main.cpp:22
22    return numberOne / numberTwo;
(gdb) 

显然分析,我们就知道了numberTwo不应该为0。

  1. 可以查看完整的栈关系 bt or bt full, 如下图所示,整个调用的过程更加清晰了。
Core was generated by `./test1'.
Program terminated with signal SIGFPE, Arithmetic exception.
#0  0x00000000004011a9 in divide (numberOne=5, numberTwo=0) at main.cpp:22
22    return numberOne / numberTwo;
(gdb) bt full
#0  0x00000000004011a9 in divide (numberOne=5, numberTwo=0) at main.cpp:22
No locals.
#1  0x000000000040117d in main () at main.cpp:13
        a = 0
        b = 5
        c = 5
        d = 1255048768
(gdb)

在此过程中,请结合print使用更佳。
借用gdb 来分析 coredump的过程结束。

本文参考资料链接:

  1. https://www.cs.umd.edu/~srhuang/teaching/cmsc212/gdb-tutorial-handout.pdf
  2. https://sourceware.org/gdb/download/onlinedocs/
  3. https://stackoverflow.com/questions/2065912/core-dumped-but-core-file-is-not-in-the-current-directory

推荐阅读更多精彩内容

  • 什么是coredump Coredump叫做核心转储,它是进程运行时在突然崩溃的那一刻的一个内存快照。操作系统在程...
    java菜阅读 2,783评论 0 4
  • 程序调试的基本思想是“分析现象->假设错误原因->产生新的现象去验证假设”这样一个循环过程,根据现象如何假设错误原...
    Manfred_Zone阅读 13,904评论 0 26
  • gdb是gcc的调试工具,在Linux环境下开发C/C++程序必不可少的工具之一,它还可以让你在汇编层面深入的了解...
    HugiFish阅读 5,701评论 0 3
  • 调试前准备 获取进程的内核转储(core dump) why:最大好处是,其保存了问题发生时的状态。记录进程当前状...
    Gitlusen阅读 375评论 0 2
  • 刚回家又大吃一顿... 吃的啥呢?剩饭。 本来说好的晚上减肥,一口都不吃的。结果坚持了没几天,现在不光是回家吃水果...
    霁粹阅读 66评论 0 0