Featured image of post C 53_C语言GDB调试详解

C 53_C语言GDB调试详解

一、GDB(GNU Debugger)概述

1.1 GDB简述

GDB 全称 GNU symbolic debugger,是一个功能强大的调试器,用于调试和分析计算机程序的执行过程。GDB诞生于GNU开源组织的(同时诞生的还有 GCC、Emacs 等)UNIX 及 UNIX-like 系统下的调试工具,是 Linux 下最常用的程序调试器,可在多个平台上使用,支持多种编程语言,包括 C、C++、Fortran、Go、java、 objective-c、PHP、Ada 等语言。但是在实际应用中,GDB 更常用来调试C和C++程序。虽然说在Linux系统下我们可以借助诸多集成开发工具来完成程序的编写和调试,但实际上,调试C/C++程序一定是直接或者间接使用GDB完成的。所以说GDB调试几乎可以说是Linux程序员必备的基本技能。

GDB 提供了一系列功能,帮助开发人员进行程序调试、故障排除和性能分析。GDB 可以加载和执行可执行文件,并提供一系列命令来控制程序的执行过程。可以逐行执行代码,设置断点,在断点处停止执行,单步执行代码等。解析并利用程序中包含的调试信息是GDB的一个常用功能。调试信息包括源代码行号、变量名称和类型、函数调用关系等。GDB 利用这些信息提供更详细的调试和分析功能。GDB还可以在调试的过程中设置断点,以便在程序执行到指定位置时中断执行。通常情况下可以使用GDB 跟踪查看程序运行时的堆栈信息,以了解函数调用的顺序和参数值。并且 GDB 支持远程调试,在一个计算机上运行 GDB,可以连接到另一个计算机上正在执行的程序。这对于调试嵌入式系统或远程服务器上的程序非常有用。

在类Unix系统中,GDB 使用命令行界面进行交互,一般通过在 GDB 提示符下输入命令来执行调试操作。除了命令行界面外,还有一些基于图形界面(如 Vs Code、Emacs、DDD等)的前后端工具可以与 GDB 集成,提供更直观的调试体验。GDB 是一个强大而复杂的工具,需要一定的学习和经验才能充分利用其功能。可以参考 GDB 的文档和教程,以了解更多关于 GDB 的详细信息和用法。

GDB: The GNU Project Debugger (https://www.sourceware.org/gdb/)

一般来说,GDB主要帮助程序员完成下面四个方面的功能:

  • 按照自定义的方式启动运行需要调试的程序。
  • 可以使用指定位置和条件表达式的方式来设置断点。
  • 程序暂停时的值的监视。
  • 动态改变程序的执行环境。

1.2 C语言程序的调试(debug)信息

要使用 GDB 调试 C/C++ 的程序,首先在使用 gcc 编译源代码时须加上 -g 参数,以在编译生成的可执行文件中添加程序的 GDB 调试信息。

Tips: gcc 编译时指定 -g 参数选项后做了什么

  • gdb 主要的作用是跟踪程序的执行过程,所以要想用gdb调试程序,首先要把源程序编译为可执行文件。但是,正常使用gcc命令编译出来的可执行文件是无法通过gdb调试的,因为这样编译出来的可执行文件缺少gdb调试所需要的调试信息(比如每一行代码的行号、包含程序中所有符号的符号表等信息)。要想生成带有gdb调试信息的可执行文件,就要在gcc编译的时候添加 -g 参数选项。
  • 当然不加 gcc 的 -g 选项 编译出来的可执行程序也能进入gdb调试,但是进入gdb并不代表就可以调试。因为,不加 -g 编译出来的可执行文件中不包含行号和符号表等调试所需要的信息,所以在gdb调试的过程中,想查看源码、添加断点等操作都是无法实现的。此时,gdb只能执行(run),不能进入函数,也不能查看变量的值等操作。

Tips: 在不特别说明的情况下,本文中的调试试验默认都使用本示例代码(main.c) 及 其生成的二进制可执行程序文件 main_debugmain

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// main.c 
#include <stdio.h>

void Func(int i)
{
    printf("Function running. param: %d\n", i);
}

int main(int argc, char const *argv[])
{
    int i = 0;
    for (i = 1; i < 3; i++){
        Func(i);
    }

    for (i = 0; i < argc; i++){
        printf("Command argv[%d] is %s\n", i, argv[i]);
    }

    int *nP = NULL;
    *nP = 100;

    return 0;
}

main.c 试验程序编译生成可执行文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
[root@master1 ~]# ls -l main.c main main_debug 
ls: 无法访问main: 没有那个文件或目录
ls: 无法访问main_debug: 没有那个文件或目录
-rw-r--r-- 1 root root 298 7月   1 16:03 main.c
[root@master1 ~]# gcc -o main main.c            # gcc 编译生成不带调试信息的 可执行文件 main
[root@master1 ~]# gcc -g -o main_debug main.c   # gcc 编译生成带调试信息的 可执行文件 main_debug
[root@master1 ~]# ls -l main.c main main_debug 
-rwxr-xr-x 1 root root 8392 7月   1 16:11 main
-rw-r--r-- 1 root root  298 7月   1 16:03 main.c
-rwxr-xr-x 1 root root 9632 7月   1 16:11 main_debug
[root@master1 ~]# 

对于一个已经编译好的二进制可执行程序,在不确定编译时是否带有 -g 参数添加上 GDB 调试信息,这个时候可以使用如下的两种方式验证:

  • 方式一: 使用 readelf -S exe_file | grep debug 查看程序是否带有 debug 信息

如上所示 C 语言小程序 main,使用如下命令(在编译时是否添加 -g 参数)分别编译生成的可执行文件,在使用 readelf -S 命令检查可执行文件的debug 符号信息时,结果如下所示:

1
2
3
4
5
6
7
[root@master1 ~]# readelf -S main | grep debug  # 在编译是未添加 `-g` 参数, 则无任何 debug 信息输出
[root@master1 ~]# readelf -S main_debug | grep debug # 在编译是添加 `-g` 参数, 则有类似如下 debug 信息输出
  [27] .debug_aranges  PROGBITS   0000000000000000  00001061
  [28] .debug_info     PROGBITS   0000000000000000  00001091
  [29] .debug_abbrev   PROGBITS   0000000000000000  0000113b
  [30] .debug_line     PROGBITS   0000000000000000  00001197
  [31] .debug_str      PROGBITS   0000000000000000  000011d5
  • 方式二:gdb 载入可执行文件时会提示程序是否有 debug 信息
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
[root@master1 ~]# gdb
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-120.el7
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
(gdb) file main         # gdb模式下使用file 命令载入可执行文件 main 有如下 no debugging symbols found 提示
Reading symbols from /root/main...(no debugging symbols found)...done.
(gdb) file main_debug   # gdb模式下使用file 命令载入可执行文件 main_debug 无 no debugging symbols found 提示
Reading symbols from /root/main_debug...done.
(gdb) 

Tips: 上面代码 中的 (gdb) 为 GDB 内部命令引导符,等待用户输入 GDB 命令 并按回车(Enter)键后提交 GDB 指令运行。

在进入 gdb 时(不管哪种方式进入),总会打印一堆如下声明:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
[root@master1 ~]# gdb
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-120.el7
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
(gdb)   # 此处的 (gdb)是 GDB 内部命令引导符,等待用户输入 GDB 命令 并按回车(Enter)键后提交 GDB 指令运行。

1.3 GDB调试非运行状态程序

GDB调试非运行状态的程序时,根据程序是否带参数可分为两种: 启动调试非运行状态的无参程序启动调试非运行状态的有参程序

  • 启动调试非运行状态的无参程序: 无参程序是指程序在启动时,没有任何参数,如上一小节中的 main 示例程序。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# 方式一: shell 命令行模式下 使用带可执行程序名参数的 gdb 命令 直接载入可执行文件进入 程序调试 模式
[root@master1 ~]# gdb main_debug 
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-120.el7
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /root/main_debug...done.
(gdb) run
Starting program: /root/main_debug 
Function running. param: 1
Function running. param: 2
Commond argv[0] is /root/main_debug
[Inferior 1 (process 3109) exited normally]
Missing separate debuginfos, use: debuginfo-install glibc-2.17-326.el7_9.x86_64
(gdb) quit  # quit 命令退出调试程序,返回 shell 命令行模式
[root@master1 ~]#

# 方式二: shell 命令行模式下 使用 不带参数的 gdb 命令进入 gdb 命令行模式,然后使用 file 命令载入可执行文件进入 程序调试 模式
[root@master1 ~]# gdb
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-120.el7
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
(gdb) file main_debug   # gdb模式下使用 file 命令载入可执行文件 main_debug
Reading symbols from /root/main_debug...done. 
(gdb) run
Starting program: /root/main_debug 
Function running. param: 1
Function running. param: 2
Commond argv[0] is /root/main_debug
[Inferior 1 (process 3109) exited normally]
Missing separate debuginfos, use: debuginfo-install glibc-2.17-326.el7_9.x86_64
(gdb) quit
[root@master1 ~]#

上述两种方式进入 GDB 调试程序 模式后,即可开始调试程序,如 runstart 命令运行程序,break 命令设置断点,next 命令单步调试,print 命令查看变量值等。

  • 启动调试非运行状态的有参程序:有参程序是指程序在启动时有输入参数。

传递运行参数的方式有三种

  1. 使用 gdb --args exe paras 启动 gdb 时指定程序参数(exe表示可执行文件名,paras表示参数),如下:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
[root@master1 ~]# gdb --args main_debug How beautiful!
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-120.el7
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /root/main_debug...done.
(gdb) run   # 运行程序
Starting program: /root/main_debug How beautiful\!
Function running. param: 1
Function running. param: 2
Commond argv[0] is /root/main_debug
Commond argv[1] is How
Commond argv[2] is beautiful!
[Inferior 1 (process 5663) exited normally]
Missing separate debuginfos, use: debuginfo-install glibc-2.17-326.el7_9.x86_64
(gdb) quit
[root@master1 ~]#
  1. 无参数进入程序调试模式后,在运行 runstart 命令时直接输入程序参数,如下:

Tips: 此方式进入调试模式与无参数程序一样,不同之处在于 runstart 命令运行程序时需带上程序参数.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
(gdb) run How beautiful!    # run 命令启动程序运行时,指定程序参数列表 How beautiful! 
Starting program: /root/main_debug How beautiful\!
Function running. param: 1
Function running. param: 2
Commond argv[0] is /root/main_debug
Commond argv[1] is How
Commond argv[2] is beautiful!
[Inferior 1 (process 7939) exited normally]
Missing separate debuginfos, use: debuginfo-install glibc-2.17-326.el7_9.x86_64
(gdb)
  1. 无参数进入程序调试模式后,使用 set args 设置程序参数,然后运行 runstart 命令运行程序,如下:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
(gdb) set args How Beautiful!   # 使用 set args 设置参数列表为 How beautiful! 
(gdb) run                       # 执行 run 命令运行程序
Starting program: /root/main_debug How Beautiful!
Function running. param: 1
Function running. param: 2
Commond argv[0] is /root/main_debug
Commond argv[1] is How
Commond argv[2] is Beautiful!
[Inferior 1 (process 5370) exited normally]
Missing separate debuginfos, use: debuginfo-install glibc-2.17-326.el7_9.x86_64
(gdb) 

1.4 GDB调试运行状态程序(进程)

使用 gdb -p pid(进程号) 命令可以直接指定进程ID(PID),然后 GDB 将连接到该进程并开始调试。这种方式适用于运行中的程序(进程),并且已知要调试的进程的PID。

1
2
3
4
5
6
# 将 GDB 连接到正在运行的进程,并同时指定要调试的可执行文件
gdb <program> 1234
# 通过进程ID直接附加到正在运行的进程
gdb -p 1234
# 用于进入gdb后连接到程序进行调试
gdb attach <pid>

如下示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
[root@master1 ~]# gdb -p 19934
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-120.el7
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Attaching to process 19934
Reading symbols from /usr/bin/bash...Reading symbols from /usr/bin/bash...(no debugging symbols found)...done.
(no debugging symbols found)...done.
Reading symbols from /lib64/libtinfo.so.5...Reading symbols from /lib64/libtinfo.so.5...(no debugging symbols found)...done.
(no debugging symbols found)...done.
Loaded symbols for /lib64/libtinfo.so.5
Reading symbols from /lib64/libdl.so.2...(no debugging symbols found)...done.
Loaded symbols for /lib64/libdl.so.2
Reading symbols from /lib64/libc.so.6...(no debugging symbols found)...done.
Loaded symbols for /lib64/libc.so.6
Reading symbols from /lib64/ld-linux-x86-64.so.2...(no debugging symbols found)...done.
Loaded symbols for /lib64/ld-linux-x86-64.so.2
Reading symbols from /lib64/libnss_files.so.2...(no debugging symbols found)...done.
Loaded symbols for /lib64/libnss_files.so.2
0x00007fb6936a160c in waitpid () from /lib64/libc.so.6
Missing separate debuginfos, use: debuginfo-install bash-4.2.46-34.el7.x86_64
(gdb) 

1.5 GDB调试程序的上下文

GDB 调试器运行环境(上下文)包括环境变量、程序参数、程序运行(工作)目录、程序运行时加载的动态库等。

  • gdb工作目录: 默认情况下,GDB 调试器会把 gdb 启动时所在的目录作为工作目录,有时可能需要根据情况去改变gdb的工作目录,查看 gdb 当前工作目录和改变工作目录的命令 和 shell 下一样,均为 pwdcd 命令。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
[root@master1 ~]# gdb
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-120.el7
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
(gdb) pwd       # 查看当前工作目录
Working directory /root.
(gdb) cd nginx  # 修改工作目录
Working directory /root/nginx.
(gdb)
  • 查看及修改gdb运行环境
  1. 查看程序的运行路径
1
show paths
  1. 设置程序的运行路径
1
path /xxx/xxx/
  1. 查看环境变量
1
show environment

设置环境变量

1
set environment PARA=para
  • 输入输出重定向:
  1. 输入输出重定向: 默认情况下,程序中的输出都是打印在终端上的,通过重定向可以把结果打印到指定位置。比如,可以把程序中的打印结果都打印到某个文件中,如下:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[root@master1 ~]# gdb main_debug 
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-120.el7
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /root/main1_debug...done.
(gdb) run       # 默认情况下,程序中的输出都是打印在终端上的
Starting program: /root/main1_debug 
Hello world.     # 此处显示输出结果
[Inferior 1 (process 3716) exited normally]
Missing separate debuginfos, use: debuginfo-install glibc-2.17-326.el7_9.x86_64
(gdb) run > 1.txt   # 通过重定向可以把结果打印到指定位置(此处为 1.txt 文件中),程序运行结果都被打印到了该文件中。
Starting program: /root/main1_debug > 1.txt
[Inferior 1 (process 3929) exited normally]
(gdb) quit
[root@master1 ~]# cat 1.txt # 通过重定向把结果打印到指定的 1.txt 文件中
Function running. param: 1
Function running. param: 2
Commond argv[0] is /root/main_debug
[root@master1 ~]# 
  1. 选择终端 使用 null 或 终端 tty1,命令如下
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
[root@master1 ~]# gdb main1_debug 
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-120.el7
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /root/main1_debug...done.
(gdb) run    # 默认情况下,程序中的输出都是打印在终端上的
Starting program: /root/main_debug 
Function running. param: 1
Function running. param: 2
Commond argv[0] is /root/main_debug
[Inferior 1 (process 6782) exited normally]
Missing separate debuginfos, use: debuginfo-install glibc-2.17-326.el7_9.x86_64
(gdb) tty /dev/null # 使用 tty 将输出设置为 /dev/null
(gdb) run   # 运行程序,程序中的输出不再打印在终端上
Starting program: /root/main_debug 
[Inferior 1 (process 6978) exited normally]
(gdb) tty /dev/tty1 # 使用 tty 将输出设置为 /dev/tty1
(gdb) run   # 运行程序,程序中的输出不再打印在终端上
Starting program: /root/main1_debug 
[Inferior 1 (process 6433) exited normally]
(gdb) 

1.6 停止调试

  • stop 命令:用于在调试过程中手动停止程序的执行。在 GDB 中执行 stop 命令时,程序会立即停止执行,并停在当前位置,等待您继续执行下一步操作。这个命令通常用于手动控制程序的执行流程,例如在特定位置设置断点或手动观察程序的状态。
  • finish 命令:用于连续执行程序,直到当前函数返回为止。在 GDB 中执行 finish 命令时,程序会自动执行直到当前函数的末尾,并返回到调用该函数的位置。这个命令通常用于快速执行完当前函数的剩余部分,而不需要逐步执行每一行代码。
  • quit 命令:想要退出 GDB 调试器时,可以使用 quitq 命令。这将终止 GDB 的执行并退出到终端或当前的命令行环境。
  • kill 命令:想要停止正在调试的程序,可以使用 kill 命令。它会向正在调试的程序发送一个终止信号(SIGTERM),导致程序停止执行。

二、GDB调试指令

2.1 常用的GDB指令列表

指令 简写 说明
help h help [命令],查看帮助手册,按 q 退出帮助手册, 如:h info
run r run [参数],执行程序并传递参数
start - start [参数],启动程序并在主函数的第一行停下来,等待调试
quit q 退出程序调试
list l 显示函数或行,list 5显示当前断点位置前后5行的代码
set - set variable [变量]=[值],修改变量的值
next n 单步执行下一行代码
nexti ni 执行下一行(以汇编代码为单位)
step s 单步执行,可进入函数内部执行
stepi si 单步执行,可进入函数内部执行(以汇编代码为单位)
until u 执行完当前函数等代码块后暂停,如果是循环则在执行完循环后暂停,常用语跳出循环
break b break [函数名]|[行号]|[文件名:行号]|[文件名:函数名]|[+偏移量]|[-偏移量]|[*地址] 设置断点;break [断点] if [条件],设置条件断点,例如:break fun if node=0
tbreak - 设置只生效一次的断点,用法与 break 一致,但是设置的断点只生效一次,该断点使用一次后自动去除
rbreak - 基于正则匹配设置断点,用于给匹配正则表达式的函数加断点
condition - condition [断点编号] [条件],给指定的断点添加触发条件condition [断点编号],给指定的断点删除触发条件
disable/enable - 禁用/启用指定断点
watch - watch [表达式],表达式发生变化时暂停运行
awatch - awatch [表达式],表达式被访问、改变时暂停运行
rwatch - rwatch [表达式],表达式被访问时暂停
catch - 监视事件,catch enevt 命令监控某一事件 event 的发生,当事件发生时,程序停止
continue c 继续运行程序,遇到断点则暂停。continue [次数] ,指定忽略断点的次数
info i 显示信息
delete d delete [编号],删除指定编号的断点(breakpoint)或观察点(watchpoint)
clear - 删除所有已定义的断点
ignore - ignore [断点编号] [次数],忽略指定编号断点的次数
print p print [变量名]
ptype - 查看变量类型,列如:ptype i
display - 查看某个变量或表达式的值,和 print 命令类似,但是 display 会一直跟踪这个变量或表达式值得变化,每执行一条语句都会打印一次变量或表达式的值,语法和 print 一样
undisplay - 取消跟踪,后面加 Num 编号,删除取消跟踪。也可以使用 del 删除
backtrace bt backtrace [+-n]
x - x $[地址],x/格式 [寄存器],显示内存内容
disassemble disas 反汇编指令
finish - 执行完当前函数后暂停
return - 忽略后面的语句,立即返回,可以指定返回值 return -1
call - 调用某个函数,call func() 调用 func() 函数
edit e 编辑文件或函数
search - search 搜索,reverse-search 反向搜索
generate-core-file - 将调试中的进程保存到coredump文件
attach - attach [pid],attach到指定pid的进程中,之后就可以使用GDB指令调试
detach - detach [inferior],分离指定编号的进程,默认分离当前正在调试的进程
info proc - 查看调试进程的信息
stop - 暂停程序的执行,停在当前位置
commands - commands [断点编号] [命令],定义在断点中断后自动执行的命令
frame f 选择要显示的帧栈
sharedlibrary share 加载共享库的符号
info sharedlibrary info share 查看共享库的状态信息
p - p $[寄存器],p/格式 [寄存器],使用p指令查看寄存器(同print)
info registers info reg 显示当前全部寄存器内容

2.2 GDB 调试命令详解

在 GDB 命令行调试模式下,调试命令 全写 与 缩写 功能效果一样,列如 运行命令 run 与 r 效果一样。

1、h(help) 命令查看gdb帮助手册

使用 help [命令] 查看帮助手册,如果一页显示不完gdb帮助手册,者按 enter 键显示下一页,q + enter 退出帮助手册, 如:h info

2、r (run) 与 start 命令运行程序

  • run 运行程序,如果有断点则停在断点处,如果没有断点会一直执行到程序结束。
  • start 会执行到 main 函数的起始位置,相当于在 main() 函数处加一个断点,然后可以使用 next、step 命令执行。
  • 如果在程序调试或者执行中使用 runstart 都表示从头开始重新执行程序。

Tips: 在 runstart 命令后面加参数可以把参数传入程序并执行(前面已经介绍过了)

3、qquit)退出调试

quit 退出 gdb 调试,回到 shell。

4、llsit)查看代码

  • 默认(不带参数)情况下,list 命令一次显示10行代码
  • 如果要查看更多行代码,可以使用 list [行号] 命令,如:list 20 查看第 20-5 到 20+4 行(共10行)
  • 使用 list n1,n2 命令可查看第 n1 到 n2 行代码,如:list 6, 23 查看 第 6 到 23 行代码
  • 查看其它文件代码,用于包含多个源文件的情况,比如可执行文件 test 由 test1.c 和 test2.c 编译而成,可以通过指定文件名来查看 test1.c 或 test2.c 的源代码。查看 test1.c 的代码1到10行 命令:list test1.c:1,10

5、set 设置变量或调试环境值

set 命令可以用于设置 GDB 环境变量(convenience 变量)的值(show convenience,以$开头) 、寄存器(info registers,以$开头的标准名称)值 或 被调试程序的变量值(程序参数值、变量值、函数参数值等),如:set variable i=10 设置变量 i 的值为 10。set 设置的与 set子命令 相同的变量时,使用 set variable var-name=value 命令。

set命令有如下几种用法:

  • 设置变量:set [变量]=[值] or set var [变量]=[值]
  • set variable [变量]=[值] 修改变量的值,如:set variable i=10
  • set args [para-list] 设置程序运行时参数, 如:set args para1 para2 ... paraN
  • set print pretty 设置打印变量时使用更加美观的格式,如:set print pretty on

6、nnext)执行下一条语句,遇到不进入函数内部

nnext)单步执行代码,一条语句一条语句的执行,如果遇到函数不会进入函数内部,也可以在后面加数字 N 表示执行 N 行。

ninexti)执行下一行(以汇编代码为单位)。

7、sstep)执行下一条语句,遇到函数进入函数内部

用法基本与 next 相同,区别在于 step 在遇到函数的时候会进入函数内部(像 printf 等这种标准库 或 第三方 .so、.a 库函数不会进入)。同样,step 也可以在后面加数字 N 表示一次执行 N 行。

si (stepi) 单步执行,可进入函数内部执行(以汇编代码为单位).

8、uuntil)命令

  • 跳出循环体:在遇到循环体时,如果在循环体尾部(最后一行代码) until 命令会直接执行完整个循环体,并停在循环体外。
  • 跳转至某一行:until num 直接跳至第 num 行执行并停在这一行。
  • 在其它时候,功能和 next 一样,都是单步执行。

9、bbreak)设置断点以及打断点的六种方式

断点(BreakPoint)可以让程序执行到断点处并停在这里,加断点应该是调试的时候最常用的一种方法,加断点的方式有很多种,下面将逐一介绍:

  • b num(直接加行号)在第num行添加断点
  • b file.c:num 在 file.c 文件的第 num 行加断点,如果不加文件名 file.c 则默认是在含main函数的那个文件第 num 行加断点
  • b function(直接加函数名)在某个函数 function 处添加断点
  • b file.c:function 在 file.c 文件中名为 function 的函数处加断点。
  • b ±num 通过偏移地址设置断点,+ 表示从当前程序运行行开始,往下数 num 行并设置断点;- 表示当前程序运行行开始,往上数 num 行并设置断点。
  • break *0x8048400 在内存地址 0x8048400 设置一个断点。
  • b (上面五种方式指定断点位置) if expression 当满足表达式 expression 的时候打断点,也就是说只有当这个表达式为真的时候,这个断点才会生效。例如:b 12 if i==2 表示当i==2的时候在第12行加断点;b Func if i<3 表示当 i<3 的时候在函数 Func 处加断点;

10、tbreak 设置只生效一次的断点

tbreak 命令的格式和用法与 break 相同,但是设置的断点只生效一次,该断点使用一次后自动去除。

11、rbreak 基于正则匹配设置断点

该命令用于给函数加断点,rbreak regex 给所有满足表达式 REGEX 的函数加断点,设置的断点和 break 设置的断点一样。这个命令在C++调试的时候,用于给所有重载函数加断点非常方便。也可以加文件名来限制为特定文件中的所有满足表达式的函数加断点 rbreak file.c:regex

12、disableenable 命令禁用和激活断点

disableenable 命令用于禁用和激活断点(普通断点、捕捉点、观察点、display的变量),通过断点号来指定要禁用或激活的断点(通过 info 查看断点号),可以通过 help 手册查看用法,被 disable 禁用的断点将会暂时失效,使用 enable 激活后会再度恢复正常使用。

enable 可以激活多个断点,并且可以指定被激活的断点起作用的次数。

13、watch 设置观察点

watch 设置观察点,如果在执行过程中变量发生变化,就把他打印出来,并停止运行。

这里要注意,如果你用指针(或地址)来设置观察点,一定要解引用,* 指针才是对指针所指向的变量进行观察如果不解引用,那就是对指针变量本身(地址)进行观察。另外,如果你观察一个临时变量或表达式,当它的生命周期结束的时候,对应的观察点也就失效了。 观察点有软件观察点和硬件观察点,这里不再详细介绍。

14、rwatch 只要程序中出现读取目标变量或表达式的值的操作,程序就会停止运行。(读)

15、awatch 只要程序中出现读取目标变量或表达式的值或者改变值的操作,程序就会停止运行。(读写)

16、catch 监视事件

catch enevt 命令监控某一事件 event 的发生,当事件发生时,程序停止

这个 event 可以是下面的情况:

  • C++中 throw 抛出的异常或 catch 捕捉到的异常;
  • load 命令或 unload 命令,在动态库加载或卸载时程序停止执行;
  • fork、vfork、exec 系统调用时,程序停止运行;

17、ccontinue)执行到下一个断点处, 继续执行程序,一直执行到下一个断点处,或者直到程序结束

18、info 查看

  • info breakpoints 查看所有断点的信息
    • Num:断点编号
    • Type:断点类型
    • Enb:激活状态,y表示已激活,n表示未激活
  • info breakpoints num 查询 num 号断点的信息
  • info variables 查询当前全局变量信息
  • info watchpoints 查看观察点信息
  • info registers 查看寄存器
  • info locals 查看当前函数内部临时变量的值
  • info args 查看当前函数参数的值

info 的更多用法,请查看帮助手册

19、deldelete)删除

如果使用 quit 退出调试,然后再次启动 gdb 的话,之前设置的所有类型的断点(包括观察点、捕捉点)都会消失。通过 delete 可以在当前调试中删除断点。在使用 delete 删除断点的时候,要先用 info 命令查看断点信息,在显示信息的第一列会有断点的编号,然后再根据编号删除断点即可(删除观察点、捕捉点方法与删除断点一致)。

如果直接使用 delete 命令,不加断点号的话,会删除当前所有断点。

20、clear 清除

clear 删除断点,后面加行号或函数名,(delete是按照断点号删除)

21、ignore忽略断点

忽视断点 ignore num count 忽视编号为 num 的断点 count 次

22、pprint)打印

  • 打印变量的值, 例如: p val 打印变量 val 的值; p &val 打印变量 val 的地址;
  • 指定打印变量值的进制,比如 /x 表示按16进制打印进制表如下:
    命令 进制
    /t 二进制
    /d 十进制有符号
    /u 十进制无符号数
    /x 十六进制
    /o 八进制
    /f 浮点型
    /c 字符型
  • 打印表达式结果,例如: print sizeof(i)
  • 修改并打印变量的值,例如: print i = 5

23、ptype 查看变量类型

查看一个变量的数据类型,列如: ptype i

24、display 跟踪变化

查看某个变量或表达式的值,和 print 命令类似,但是 display 会一直跟踪这个变量或表达式值得变化,每执行一条语句都会打印一次变量或表达式的值。

display 也可以按格式打印,语法和 print 一样,请参照上表(print)。

display 跟踪得变量或表达式也会放入一张表中,使用 info 命令可以查看信息

display 命令输出结果中 Num表示编号,Enb表示是否激活,Expression表示被跟踪的表达式。

25、undisplay 取消跟踪

后面加 Num 编号,删除取消跟踪。也可以使用 del 删除。

26、btbacktrace)查看栈信息

在一个程序的执行过程中,如果遇到函数调用,每次调用都会产生一系列与函数上下文相关的信息:比如函数调用的位置、函数参数、函数内部的临时变量等。这些信息会被存放在一块称为栈帧的内存空间中,并且每一个函数调用都对应一个栈帧(main 函数也有自己的栈帧,称为初始帧)。这些所有的栈帧都存放在内存中的栈区。通过命令 info frame 可以查看当前使用的栈帧所存储的信息,这里面包含了栈帧编号、栈帧地址、调用者、源码编程语言等信息。通过命令 frame num 、up 、down 可以选的改变栈帧。

backtrace 命令查看当前所有栈帧。

27、x 查看内存中的值

变量内存地址可以使用 print &i 查看。

x 查看内存中的值可以指定按什么格式查看, 例如 x/d 。

28、disas (disassemble)反汇编

disas Func 查看函数 Func() 函数的反汇编代码,使用命令 q 退出。

29、finish

跳出当前所在的函数。

30、return

忽略后面的语句,立即返回,可以指定返回值 return -1

31、call

调用某个函数,call func() 调用 func() 函数。

32、edit

进入编辑模式,可对代码进行编辑。

33、search

search 搜索,reverse-search 反向搜索。

三、GDB 调试详解

3.1 查看源码

在使用 GDB 调试程序的过程中,经常需要查看程序源码,查看代码的命令是 list,缩写形式为 l。用法如下:

  • list 查看将要运行代码行前后 5 行代码,默认是 5 行;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
[root@master1 ~]# gdb main_debug 
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-120.el7
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /root/main_debug...done.
(gdb) list
4	void Func(int i)
5	{
6	    printf("Function running. param: %d\n", i);
7	    int *nP = NULL;
8	    *nP = 100;
9	}
10	
11	int main(int argc, char const *argv[])
12	{
13	    int i = 0;
(gdb) start
Temporary breakpoint 1 at 0x40056f: file main.c, line 13.
Starting program: /root/main_debug 

Temporary breakpoint 1, main (argc=1, argv=0x7fffffffe498) at main.c:13
13	    int i = 0;
(gdb) list      #  查看将要运行代码行前后 5 行代码,默认是 5 行
8	    *nP = 100;
9	}
10	
11	int main(int argc, char const *argv[])
12	{
13	    int i = 0;
14	
15	    for (i = 0; i < argc; i++){
16	        printf("Commond argv[%d] is %s\n", i, argv[i]);
17	    }
(gdb) 
  • list first,last 指定行号查看代码
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
2	#include <stdio.h>
3	
4	void Func(int i)
5	{
6	    printf("Function running. param: %d\n", i);
7	    int *nP = NULL;
8	    *nP = 100;
9	}
10	
11	int main(int argc, char const *argv[])
12	{
(gdb) 
  • list [函数名] 列出指定函数名前后的源码
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
(gdb) list Func
1	// main.c 
2	#include <stdio.h>
3	
4	void Func(int i)
5	{
6	    printf("Function running. param: %d\n", i);
7	    int *nP = NULL;
8	    *nP = 100;
9	}
10	
(gdb)
  • list [文件名]:[行号或函数名] 列出指定文件的源码
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
(gdb) list main.c:main
7	    int *nP = NULL;
8	    *nP = 100;
9	}
10	
11	int main(int argc, char const *argv[])
12	{
13	    int i = 0;
14	
15	    for (i = 0; i < argc; i++){
16	        printf("Commond argv[%d] is %s\n", i, argv[i]);
(gdb) list main.c:12  
7	    int *nP = NULL;
8	    *nP = 100;
9	}
10	
11	int main(int argc, char const *argv[])
12	{
13	    int i = 0;
14	
15	    for (i = 0; i < argc; i++){
16	        printf("Commond argv[%d] is %s\n", i, argv[i]);
(gdb) list main.c:12,18
12	{
13	    int i = 0;
14	
15	    for (i = 0; i < argc; i++){
16	        printf("Commond argv[%d] is %s\n", i, argv[i]);
17	    }
18	
(gdb) 

3.2 断点设置与使用

在使用 GDB 调试程序的过程中,经常需要查看变量内容、堆栈情况等等,这时就需要指定断点,程序执行到断点处会暂停执行。

break 就是命令用来设置断点,缩写形式为 b,设置断点后,以便可以更详细的跟踪断点附近程序的执行情况。

  • 设置断点: 设置断点有很多方式,在 第二节 中已经有 基础的介绍,下面将介绍一些高级的设置断点的方法:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
(gdb) list 0 
1	// main.c 
2	#include <stdio.h>
3	
4	void Func(int i)
5	{
6	    printf("Function running. param: %d\n", i);
7	    int *nP = NULL;
8	    *nP = 100;
9	}
10	
(gdb) 
11	int main(int argc, char const *argv[])
12	{
13	    int i = 0;
14	
15	    for (i = 0; i < argc; i++){
16	        printf("Commond argv[%d] is %s\n", i, argv[i]);
17	    }
18	
19	    for (i = 1; i < 3; i++){
20	        Func(i);
(gdb) 
21	    }
22	
23	    return 0;
24	}
(gdb) break 13      # 当前文件(main函数所在文件)第 13行 设置断点
Breakpoint 1 at 0x40056f: file main.c, line 13.
(gdb) break main.c:14         # main.c 文件第 14行 设置断点
Breakpoint 2 at 0x400576: file main.c, line 14.
(gdb) break main     # 当前文件(main函数所在文件)main函数处 设置断点
Breakpoint 3 at 0x40056f: file main.c, line 13.
(gdb) break main.c:Func     # main.c 文件 Func 函数处 设置断点
Breakpoint 4 at 0x400538: file main.c, line 6.
(gdb) break -17 # 当前程序运行行开始,往上数 17 行并设置断点
Breakpoint 6 at 0x400554: file main.c, line 8.
(gdb) break +4  # 从当前程序运行行开始,往下数 4 行并设置断点
Breakpoint 8 at 0x4005b6: file main.c, line 17.
(gdb) info add Func # 查看函数地址
Symbol "Func" is a function at address 0x40052d.
(gdb) b *0x40052d   # 通过函数地址设置断点
Breakpoint 9 at 0x40052d: file main.c, line 5.
(gdb) b 6 if i == 2 # 设置条件断点,当 i == 2 时,才会断点
Note: breakpoint 4 also set at pc 0x400538.
Breakpoint 10 at 0x400538: file main.c, line 6.
(gdb) 
  • 查看断点: 可以使用 info breakinfo breakpoints 查看断点的情况。命令将显示设置了那些断点,断点被命中的次数等信息。示例如下:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
(gdb) info  break
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x000000000040056f in main at main.c:13
	breakpoint already hit 1 time
2       breakpoint     keep y   0x0000000000400576 in main at main.c:14
3       breakpoint     keep y   0x000000000040056f in main at main.c:13
	breakpoint already hit 1 time
4       breakpoint     keep y   0x0000000000400538 in Func at main.c:6
8       breakpoint     keep y   0x00000000004005b6 in main at main.c:17
9       breakpoint     keep y   0x000000000040052d in Func at main.c:5
10      breakpoint     keep y   0x0000000000400538 in Func at main.c:6
	stop only if i == 2
11      breakpoint     keep y   0x0000000000400538 in Func at main.c:6
	stop only if i == 2
12      breakpoint     keep y   0x0000000000400538 in Func at main.c:6
	stop only if i == 2
(gdb) 

该命令 将列出所有已设置的断点,每一个断点都有一个标号,用来代表这个断点。

  • 删除断点:对于无用的断点我们可以删除。删除的命令格式为 delete breakpoint 断点编号。info breakpoint 命令显示结果中的num列就是编号。删除断点的示例如下:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
(gdb) delete 2
(gdb) info  break
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x000000000040056f in main at main.c:13
	breakpoint already hit 1 time
3       breakpoint     keep y   0x000000000040056f in main at main.c:13
	breakpoint already hit 1 time
4       breakpoint     keep y   0x0000000000400538 in Func at main.c:6
8       breakpoint     keep y   0x00000000004005b6 in main at main.c:17
9       breakpoint     keep y   0x000000000040052d in Func at main.c:5
10      breakpoint     keep y   0x0000000000400538 in Func at main.c:6
	stop only if i == 2
11      breakpoint     keep y   0x0000000000400538 in Func at main.c:6
	stop only if i == 2
12      breakpoint     keep y   0x0000000000400538 in Func at main.c:6
	stop only if i == 2
(gdb) 

清除断点:清除断点的命令为 clear 断点编号。清除所有断点命令为 clear 或者 clear all。清除断点示例如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# 设置断点的几种方式
(gdb) break function    # 在function函数入口设置断点
(gdb) break main.c:18   # 在main.c的第18行处设置断点,文件名有时可省略
(gdb) info breakpoints  # 查看断点列表

(gdb) tbreak 18        # 设置的断点仅生效一次,之后被删除
(gdb) rbreak regex     # 在所有符合正则表达式的函数处设置断点
(gdb) break 10 if i==18  # 设置条件断点,仅当条件满足时触发中断
(gdb) ignore 2 5       # 接下来5次命中编号为2的断点时不会中断

# 删除/启用/禁用断点(适用于breakpoint、watchpoint、catchpoint)
(gdb) clear            # 删除所有断点
(gdb) clear function   # 删除所有位于function内的断点
(gdb) delete num       # 删除指定编号的断点
(gdb) enable num       # 启用指定编号的断点
(gdb) disable num      # 禁用指定编号的断点

# 保存和导入断点
(gdb) save breakpoints file  # 保存断点信息到指定文件
(gdb) source file      # 导入文件中保存的断点信息
  • watchpoint 用法:当变量或表达式的值发生变化时触发中断,又称数据断点。

分为硬件实现、软件实现。前者需要硬件系统的支持;后者的原理就是每步执行后都检查变量的值是否改变。GDB在新建数据断点时会优先尝试硬件方式,如果失败再尝试软件实现。

1
2
3
4
5
6
(gdb) watch variable    # 设置变量数据断点
(gdb) watch var1 + var2 # 设置表达式数据断点
(gdb) rwatch variable   # 设置读断点,仅支持硬件实现
(gdb) awatch variable   # 设置读写断点,仅支持硬件实现

(gdb) info watchpoints  # 查看数据断点列表

使用数据断点时,需要注意:

当监控变量为局部变量时,一旦局部变量失效,数据断点也会失效 如果监控的是指针变量p,则watch *p监控的是p所指内存数据的变化情况,而watch p监控的是p指针本身有没有改变指向 最常见的数据断点应用场景:定位堆上的结构体内部成员何时被修改。由于指针一般为局部变量,为了解决断点失效,一般有两种方法。

1
2
3
4
5
(gdb) print &variable         # 先查看变量的内存地址
(gdb) watch *(type *)address  # 通过内存地址间接设置断点

(gdb) watch -l variable       # 指定location参数
(gdb) watch variable thread 1 # 仅编号为1的线程修改变量var值时会中断
  • catchpoint

当程序发生某种事件时中断,这里的事件如:抛出异常、加载动态库、某次系统调用等。

1
2
3
(gdb) catch fork      # 程序调用fork时中断
(gdb) tcatch fork     # 设置的断点只触发一次,之后被自动删除
(gdb) catch syscall ptrace   # 为ptrace系统调用设置断点

Tips: 在command命令后加断点编号,可以定义断点触发后想要执行的操作。在一些高级的自动化调试场景中可能会用到。

3.3 单步调试

单步调试 是指一条一条语句的去执行程序,可以随时查看执行后的结果。单步执行有两个命令,分别是 stepnext。在程序中可能打了多处断点,或者断点打在循环内,这个时候,可以使用 continue 命令。这三个命令的区别在于:

  • next 命令(可简写为n)用于在程序断点暂停处住后,继续执行下一条语句。

  • step 命令(可简写为s),它可以单步跟踪到函数内部。

  • continue命令(可简写为c)或者 fg,它会继续执行程序,直到再次遇到断点处。

  • 单步执行 - next 命令(可简写为n)命令用于让程序在断点暂停后一条语句一条语句的执行,它不会跟踪到调用函数内部,使用next命令只会在本函数中单步执行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
(gdb) start
Temporary breakpoint 1 at 0x40056f: file main.c, line 13.
Starting program: /root/main_debug 

Temporary breakpoint 1, main (argc=1, argv=0x7fffffffe498) at main.c:13
13	    int i = 0;
Missing separate debuginfos, use: debuginfo-install glibc-2.17-326.el7_9.x86_64
(gdb) n
15	    for (i = 0; i < argc; i++){
(gdb) 
16	        printf("Commond argv[%d] is %s\n", i, argv[i]);
(gdb) 
Commond argv[0] is /root/main_debug
15	    for (i = 0; i < argc; i++){
(gdb) 
19	    for (i = 1; i < 3; i++){
(gdb) 
20	        Func(i);    # 只会在本函数(main)中单步执行, 不会进入调用函数(Func)中单步执行
(gdb) 
Function running. param: 1

Program received signal SIGSEGV, Segmentation fault.
0x0000000000400558 in Func (i=1) at main.c:8
8	    *nP = 100;
(gdb) 
  • 单步进入 - step 命令用于让程序在断点暂停后一条语句一条语句的执行,它可以单步跟踪到调用函数内部。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(gdb) start
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Temporary breakpoint 7 at 0x40056f: file main.c, line 13.
Starting program: /root/main_debug 

Temporary breakpoint 7, main (argc=1, argv=0x7fffffffe498) at main.c:13
13	    int i = 0;
(gdb) s
15	    for (i = 0; i < argc; i++){
(gdb) 
16	        printf("Commond argv[%d] is %s\n", i, argv[i]);
(gdb) 
Commond argv[0] is /root/main_debug
15	    for (i = 0; i < argc; i++){
(gdb) 
19	    for (i = 1; i < 3; i++){
(gdb) 
20	        Func(i);    # 会进入调用函数(Func)中单步执行
(gdb) 
Func (i=1) at main.c:6
6	    printf("Function running. param: %d\n", i);
(gdb) 

使用 step 命令将一行一行的单步执行程序。遇到函数也将进入函数单步执行函数语句,但是如果没有该函数源码,需要跳过该函数执行,可使用finish命令,继续后面的执行。

  • 继续执行到下一个断点 - continue

在调试程序时,可能打了多处断点,或者断点打在循环内,这个时候,想跳过这个断点,甚至跳过多次断点继续执行该怎么做呢?可以使用continue命令。它的作用就是从暂停处继续执行。命令的简写形式为c。继续执行过程中遇到断点或者观察点变化依然会暂停。示例代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
(gdb) b 13
Breakpoint 2 at 0x40056f: file main.c, line 13.
(gdb) b 19
Breakpoint 3 at 0x4005b6: file main.c, line 19.
(gdb) run
Starting program: /root/main_debug 

Breakpoint 2, main (argc=1, argv=0x7fffffffe498) at main.c:13
13	    int i = 0;
(gdb) c     #   继续执行到下一个断点
Continuing.
Commond argv[0] is /root/main_debug

Breakpoint 3, main (argc=1, argv=0x7fffffffe498) at main.c:19
19	    for (i = 1; i < 3; i++){
(gdb) 
  • 跳过执行 – skip

使用skip之后,将不会进入judge_sd函数。好处就是skip可以在step时跳过一些不想关注的函数或者某个文件。如果想删除skip,使用 skip delete [num]

3.4 变量的观察与设置

  • 查看变量print(可简写为 p)打印变量内容。
1
print [变量名]

多个函数或者多个文件会有同一个变量名,这个时候可以在前面加上文件名或者函数名来区分:

1
2
(gdb) p 'testfile.c'::i
(gdb) p 'sum'::i

打印指针:

1
2
(gdb) print *pt     # 使用*只能打印第一个值
(gdb) print *buf@i   # 如果要打印多个值,后面跟上@并加上要打印的长度。或者@后面跟上变量值:

$ 符号可表示上一个变量,在调试链表时时经常会用到的,它有 next 成员代表下一个节点,则可使用下面方式不断打印链表内容,举例:

1
2
(gdb) p *linkNode  #这里显示linkNode节点内容
(gdb) p *$.next #这里显示linkNode节点下一个节点的内容
  • 设置变量

使用print命令查看了变量的值,如果感觉这个值不符合预期,想修改下这个值,再看下执行效果。这种情况下可以 使用 set 修改代码变量值,再重新执行代码。set命令可以直接修改变量的值。

  • 设置观察点

设置观察点的作用就是:当被观察的变量发生变化后,程序就会暂停执行,并把变量的原值(Old)和新值(New)都会显示出来。设置观察点的命令是watch。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
(gdb) start
Temporary breakpoint 1 at 0x40056f: file main.c, line 13.
Starting program: /root/main_debug 

Temporary breakpoint 1, main (argc=1, argv=0x7fffffffe498) at main.c:13
13	    int i = 0;
Missing separate debuginfos, use: debuginfo-install glibc-2.17-326.el7_9.x86_64
(gdb) watch i   # 设置观察点
Hardware watchpoint 2: i
(gdb) c     # 让程序继续运行,如果num的值发生变化,则会打印相关内容
Continuing.
Commond argv[0] is /root/main_debug
Hardware watchpoint 2: i

Old value = 0
New value = 1
0x00000000004005ae in main (argc=1, argv=0x7fffffffe498) at main.c:15
15	    for (i = 0; i < argc; i++){
(gdb) 

3.5 常用的info组合指令

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 查看当前已设置的断点信息
(gdb) info breakpoints
# 查看当前所有线程的信息,包括每个线程pid、当前执行的函数、当前源文件和行号等信息
(gdb) info threads
# 查看当前帧(frame)的信息,包括函数名、源文件、行号、参数和局部变量等
(gdb) info frame
# 查看当前帧内的局部变量的值
(gdb) info locals
# 查看当前函数传递的参数的值
(gdb) info args
# 查看加载共享库(shared library)的信息
(gdb) info sharedlibrary
# 查看有关可执行文件和核心转储文件的信息,常用来查看coredump文件的生成时间等信息
(gdb) info files
  • 寄存器/地址查看技巧

在调试过程中,可在任意位置通过断点暂停执行程序,然后通过寄存器/地址查看技巧自由的显示任意变量或地址,通过对比预期值,以确认是否存在bug。可以使用info reg 查看当前程序的全部寄存器信息。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
(gdb) info reg
rax            0x400560	4195680
rbx            0x0	0
rcx            0x4005e0	4195808
rdx            0x7fffffffe4a8	140737488348328
rsi            0x7fffffffe498	140737488348312
rdi            0x1	1
rbp            0x7fffffffe3b0	0x7fffffffe3b0
rsp            0x7fffffffe390	0x7fffffffe390
r8             0x7ffff7dd5e80	140737351868032
r9             0x0	0
r10            0x7fffffffdee0	140737488346848
r11            0x7ffff7a2f460	140737348039776
r12            0x400440	4195392
r13            0x7fffffffe490	140737488348304
r14            0x0	0
r15            0x0	0
rip            0x400576	0x400576 <main+22>
eflags         0x206	[ PF IF ]
cs             0x33	51
ss             0x2b	43
ds             0x0	0
es             0x0	0
fs             0x0	0
gs             0x0	0
(gdb) 

使用 print 指令在寄存器前面加 $ ,即可显示寄存器的内容。

1
2
3
4
5
(gdb) p $rax
$1 = 4195680
(gdb) p $pc 
$2 = (void (*)()) 0x400576 <main+22>
(gdb) 

显示寄存器内容时可以指定格式。

1
2
3
4
(gdb) p/x $rax
$11 = 0x555555555269
(gdb) p/x $pc
$13 = 0x555555555269

显示寄存器可用格式:

格式 说明
x 显示为十六进制数
d 显示为十进制数
u 显示为无符号十进制数
o 显示为八进制数
t 显示为二进制数,t的由来是two
a 地址
c 显示为字符(ASCII)
f 显示为浮点小数
s 显示为字符串
i 显示为汇编指令(仅在显示内存的x命令中可用)
  • 使用 x 指令可以显示内存的内容

x 是 eXamining 的缩写。

1
2
3
4
5
(gdb) x $pc
0x400576 <main+22>:	0x00fc45c7
(gdb) x/i $pc
=> 0x400576 <main+22>:	movl   $0x0,-0x4(%rbp)
(gdb) 

i格式表示显示为汇编指令语言,仅在x命令中可用,我们可以使用i格式来查看pc指针上的汇编指令。下面示例中显示从pc所指地址开始的5条汇编指令

1
2
3
4
5
6
7
(gdb) x/5i $pc
=> 0x400576 <main+22>:	movl   $0x0,-0x4(%rbp)
   0x40057d <main+29>:	jmp    0x4005ae <main+78>
   0x40057f <main+31>:	mov    -0x4(%rbp),%eax
   0x400582 <main+34>:	cltq   
   0x400584 <main+36>:	lea    0x0(,%rax,8),%rdx
(gdb) 

一般在使用x命令时,格式为 x/NFU ADDR。此处ADDR为希望显示的地址,N为重复次数,F为显示格式(x,d,u,o,t,a,c,f,s,i),U为下表中所示的单位。

U代表的单位:

单位 说明
b 字节
h 半字(2字节)
w 字(4字节)默认值
g 双字(8字节)
  • disassemble指令为反汇编指令

可以简写为disas,用disas可以查看当前函数的汇编代码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
(gdb) disas       
Dump of assembler code for function main:
   0x0000000000400560 <+0>:	push   %rbp
   0x0000000000400561 <+1>:	mov    %rsp,%rbp
   0x0000000000400564 <+4>:	sub    $0x20,%rsp
   0x0000000000400568 <+8>:	mov    %edi,-0x14(%rbp)
   0x000000000040056b <+11>:	mov    %rsi,-0x20(%rbp)
   0x000000000040056f <+15>:	movl   $0x0,-0x4(%rbp)
=> 0x0000000000400576 <+22>:	movl   $0x0,-0x4(%rbp)
   0x000000000040057d <+29>:	jmp    0x4005ae <main+78>
   0x000000000040057f <+31>:	mov    -0x4(%rbp),%eax
   0x0000000000400582 <+34>:	cltq   
   0x0000000000400584 <+36>:	lea    0x0(,%rax,8),%rdx
   0x000000000040058c <+44>:	mov    -0x20(%rbp),%rax
   0x0000000000400590 <+48>:	add    %rdx,%rax
   0x0000000000400593 <+51>:	mov    (%rax),%rdx
   0x0000000000400596 <+54>:	mov    -0x4(%rbp),%eax
   0x0000000000400599 <+57>:	mov    %eax,%esi
   0x000000000040059b <+59>:	mov    $0x40068d,%edi
   0x00000000004005a0 <+64>:	mov    $0x0,%eax
   0x00000000004005a5 <+69>:	callq  0x400410 <printf@plt>
   0x00000000004005aa <+74>:	addl   $0x1,-0x4(%rbp)
   0x00000000004005ae <+78>:	mov    -0x4(%rbp),%eax
   0x00000000004005b1 <+81>:	cmp    -0x14(%rbp),%eax
   0x00000000004005b4 <+84>:	jl     0x40057f <main+31>
   0x00000000004005b6 <+86>:	movl   $0x1,-0x4(%rbp)
   0x00000000004005bd <+93>:	jmp    0x4005cd <main+109>
   0x00000000004005bf <+95>:	mov    -0x4(%rbp),%eax
   0x00000000004005c2 <+98>:	mov    %eax,%edi
   0x00000000004005c4 <+100>:	callq  0x40052d <Func>
   0x00000000004005c9 <+105>:	addl   $0x1,-0x4(%rbp)
   0x00000000004005cd <+109>:	cmpl   $0x2,-0x4(%rbp)
   0x00000000004005d1 <+113>:	jle    0x4005bf <main+95>
   0x00000000004005d3 <+115>:	mov    $0x0,%eax
   0x00000000004005d8 <+120>:	leaveq 
   0x00000000004005d9 <+121>:	retq   
End of assembler dump.
(gdb) 

disas <程序计数器> 可以显示指定程序计数器所在函数的汇编代码。

disas <开始地址> <结束地址> 反汇编指定范围地址上的程序。

3.6 GDB检索历史记录

  • 值的历史

使用print命令查看过的值会记录在内部的值的历史中,这些值可以在其他表达式中使用。查看值的历史一般有两种方法,如下所示:

1
2
3
4
5
6
7
(gdb) p $   # 最后访问的值可以使用$查看
$3 = (void (*)()) 0x400576 <main+22>
(gdb) show values   # show values命令可以显示历史中的最后10个值
$1 = 4195680
$2 = (void (*)()) 0x400576 <main+22>
$3 = (void (*)()) 0x400576 <main+22>
(gdb) 

访问值的历史的变量选项如下表所示:

变量 说明
$ 值历史的最后一个值
$n 值历史的第n个值
$$ 值历史的倒数第2个值
$$n 值历史的倒数第n个值
$_ x命令显示过的最后的地址
$__ x命令显示过的最后的地址的值
$_exitcode 调试中的程序的返回代码
$bpnum 最后设置的断点编号
  • 命令历史

输入过的命令也会被保存在文件中,可以在其它调试中重复利用这些命令(通过箭头键查找历史命令)。命令历史默认保存在 ./.gdb_history 文件中。

1
2
3
4
5
6
(gdb) show history 
expansion:  History expansion on command input is off.
filename:  The filename in which to record the command history is "/root/.gdb_history".
save:  Saving of the history record on exit is off.
size:  The size of the command history is 1000.
(gdb) 

默认情况下,历史扩展(history expansion)是禁用的,这意味着我们无法使用类似于 Bash shell 中的历史扩展功能(例如使用 ! 来重复先前的命令)。我们可以通过启用 GDB 的历史扩展功能来实现类似的效果。还有更多设置如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# 启用历史扩展
(gdb) set history expansion
(gdb) show history expansion
History expansion on command input is on.
# 设置历史命令保存文件
(gdb) set history filename
(gdb) show history filename
The filename in which to record the command history is "/root/.gdb_history".
# 启用保存历史命令
(gdb) set history save
(gdb) show history save
Saving of the history record on exit is on.
# 设置保存历史命令的个数
(gdb) set history size 1000
(gdb) show history size
The size of the command history is 1000.

3.7 backtrace

在GDB调试过程中,使用 backtrace 命令来显示当前函数调用链的信息,backtrace显示的内容主要包括函数名称、参数和调用位置等信息。backtrace可以帮助我们追踪程序的执行路径,从而达到定位问题的源头目的。

backtrace 命令的输出通常以栈帧(stack frame)的形式显示,每个栈帧对应一个函数调用。栈帧按从上到下的顺序列出,最上面的栈帧是当前正在执行的函数,下面的栈帧是它的调用者,以此类推,如下案例所示。

1
2
3
4
(gdb) backtrace
#0  Func (i=1) at main.c:6
#1  0x00000000004005c9 in main (argc=1, argv=0x7fffffffe498) at main.c:20
(gdb) 

对于每个栈帧,backtrace 提供以下信息:

  • 栈帧编号(Frame number):以#开头的数字,表示栈帧的序号,从0开始递增。0号栈帧是当前正在执行的函数。
  • 函数名称(Function name):显示栈帧对应的函数名称。
  • 参数(Arguments):显示函数调用时传递的参数的值。
  • 文件名和行号(File and line):显示函数所在的源文件名和调用发生的行号。

通过分析 backtrace 命令的输出,我们可以了解程序的函数调用路径,以及每个函数在调用时的参数和调用位置。这有助于定位问题的源头,特别是在出现崩溃、异常或错误时。

  • backtrace无法正确显示

既然 backtrace 命令显示的是栈帧(stack frame),那么当程序的栈因为某些原因遭到破坏(例如:由于栈溢出或其他内存错误)之后,就无法打印出正确的栈帧信息。这会导致我们追溯问题发生的调用路径时变得异常困难。由此来看,系统中存在栈被破坏的可能,因此backtrace并非万能的。

如果遇到 backtrace 无法正确显示的情况,就需要使用其他调试技术和工具来定位问题,例如检查程序的日志、使用内存检测工具(如Valgrind)、查看info locals(显示局部变量)、info registers(显示寄存器状态)和 x(查看内存内容)等其他信息,以深入分析问题并获取更多的上下文信息。

下面将通过案例来了解backtrace无法正确显示的情况,以及对于这种问题的应对方法。虽然案例中以极端的方式破坏堆栈,但实际工作环境中也会遇到类似的问题。在这种情况下,我们无法信任GDB调试过程中看到的backtrace信息,可信任的backtrace只有在栈没有被破坏的前提下才能出现。 参考示例

四、核心转储(Core Dump)概述 及 调试方法

4.1 核心转储(Core Dump)概述

  • 核心转储(Core Dump)

核心转储(Core Dump) 是指在计算机系统遇到严重错误或异常情况时,将当前内存中的程序(进程)数据和状态信息保存到磁盘上的文件中,它是一种诊断工具,用于在程序崩溃或异常终止时提供有关问题的详细信息。它包含了程序在崩溃时的内存映像,包括堆栈跟踪、寄存器状态、变量值等。这些信息对于调试和分析问题非常有价值,可以帮助开发人员确定程序崩溃的原因和位置。

Tips: 在计算中,核心转储( core dump)、内存转储(memory dump)、故障转储(crash dump)、存储转储(storage dump)、系统转储(system dump)或异常结束转储( ABEND dump)由计算机程序在特定时间(通常是在程序崩溃或以其他方式异常终止时)记录的工作内存状态组成 。

核心转储文件 通常以操作系统特定的格式存储,并且可以很大,具体取决于系统内存的大小。它们可以通过调试工具GDB进行分析,以还原程序崩溃时的上下文环境,并帮助开发人员找到错误的根本原因。

对于开发人员和系统管理员来说,核心转储是一种重要的工具,用于调试和排查应用程序的故障。通过分析核心转储文件,可以识别和修复代码中的错误、内存泄漏、资源冲突等问题。

Tips: 为什么会发生Coredump

  • Core就是内存的意思,这个词源自很早以前制造内存的材料,一直延用到现在,当程序运行过程中检测到异常程序异常退出时, 系统把程序当前的内存状况存储在一个core文件中, 叫core dumped,也就信息转储,操作系统检测到当前进程异常时将通过信号的方式通知目标进程相应的错误信息,常见的信号有SIGSEGV,SIGBUS等,默认情况下进程接收到相应的信号都有相应的处理机制。

Tips: 需要注意的是,核心转储文件可能包含敏感信息,如内存中的数据和变量值。因此,在共享或传输核心转储文件时,应注意确保数据的安全性和机密性

  • Segmentation fault(段错误) C 中的分段错误(也称为段错误)是当程序尝试访问不允许访问的内存时发生的错误。

在 C 编程语言中,当程序尝试读取或写入尚未分配给它的内存位置时,就会发生 C 分段错误。

在C语言中,内存是由程序员手动管理的,内存管理中的错误可能会导致C语言中的分段错误。 例如,如果程序尝试访问尚未初始化或已释放的指针,则可能会发生分段错误。

分段错误也可能由于缓冲区溢出(程序将数据写入超出分配的内存块的范围)或堆栈溢出(程序用尽调用堆栈上的所有可用空间)而发生。

当 C 中发生分段错误时,程序通常会崩溃并生成核心转储,该文件包含有关崩溃时程序状态的信息。

该信息可用于调试程序并确定分段错误的原因。

造成程序 core dump 的原因有很多,主要是内存访问越界、使用线程不安全的函数、使用空指针、堆栈溢出等等

4.2 启用coredump

程序崩溃时生成 coredump 文件的首要条件是操作系统的 ulimit(用户限制)设置,可以使用 ulimit -aulimit -c 来查看当前的 ulimit 设置。确保 “core file size"(核心文件大小)的限制不是 “0",而是一个大于零的值。如果 “core file size” 设置为 “0”,则说明core dump并未启用,需要修改相关配置文件来更改该值。如下所示:

1
2
[root@master1 ~]# ulimit -c
0

设置 ulimit -c 的值为 unlimited 即可开启内核转储,unlimited 表示不限制 core dump 的大小,设置为不限制后,发生问题时进程的内存就可以全部转储到 coredump 文件中。在调试大量消耗内存的进程时,可能希望设置 coredump 文件的上限,这时就需要直接再参数中指定其大小, 如下所示:

1
2
3
4
5
6
[root@master1 ~]# ulimit -c unlimited   # 设置 core dump 文件大小上限为不限制
[root@master1 ~]# ulimit -c                                                          
unlimited
[root@master1 ~]# ulimit -c 1073741824  # 设置core dump 文件大小上限为1GB
[root@master1 ~]# ulimit -c                                                          
1073741824

上述命令用于设置产生coredump文件的大小(系统重启后该设置失效)。

在 配置/etc/security/limits.conf 中添加 以下行(永久开启 coredump 功能):

1
2
3
# 在文件中添加以下行
*               soft    core            unlimited
*               hard    core            unlimited

4.3 配置 core dump 文件保存位置和文件名格式

core dump的配置方法随不同操作系统可能有差异,更多配置可以查看其相关文档或书籍。

Tips: 在Linux系统中,如果进程崩溃了,系统内核会捕获到进程崩溃信息,然后将进程的coredump 信息写入到文件中,这个文件名默认是core 。存储位置与对应的可执行程序(进程)的工作目录相同,文件名是core。

Linux 下,在 /proc/sys/kernel/core_pattern 文件中存放当前使用的 core dump 文件保存位置和文件名格式 配置方式如下:

  • 创建 core dump 文件统一存放路径,如:/data/corefile

  • 修改配置 /proc/sys/kernel/core_pattern 文件 设置 core dump 文件存放路径 及 命名模式, 如下:

1
2
3
4
5
# 将core文件统一生成到 /data/coredump 目录下,产生的文件名为core-命令名-pid-时间戳
echo "/data/corefile/core-%e-%p-%t" > /proc/sys/kernel/core_pattern

# 或者:
sysctl -w kernel.core_pattern=/corefile/core-%e-%p-%t

Core_pattern的格式:

  • %p: 转储过程的PID
  • %u: (数字)转储进程的实际UID
  • %g: (数字)转储过程的实际GID
  • %s: 引起转储的信号数值
  • %t: 转储时间,表示为自1970年1月1日00:00:00 +0000(UTC)以来的秒数
  • %h: 主机名(与uname(2)返回的节点名相同)
  • %e: 可执行文件名(无路径前缀)
  • %E: 可执行文件的路径名,用斜杠(’/’)替换为感叹号(’!’)。
  • %C: 崩溃过程的核心文件大小软资源限制(自Linux 2.6.24开始)

Tips: 了解更多 core 详细信息可以参考 https://linux.die.net/man/5/core

4.4 配置 core dump 文件清理规则

由于进程异常崩溃所产生的 coredump 文件一般都很大,若长期不清理的话,时间长了会将硬盘占满;所以在开启保存 core dump 文件功能后需要设置该类文件的自动清理操作。

通常可以使用 crontab 工具设置定时清理任务。

另外还可在 配置文件/etc/rc.local中最后面添加命令(机器重启时会自动加载该命令):判断磁盘空间是否超过 80%,如果超过则执行 rm -rf /data/corefile/* 删除所有 coredump 文件。

4.5 手动生成coredump

常用的手动生成coredump的方法有三种:

  • gdb 程序调试模式下 使用 generate-core-file 命令对运行的程序生成 core dump 文件
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[root@master1 ~]# gdb main_debug 
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-120.el7
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /root/main_debug...done.
(gdb) b 8   # 设置断点
Breakpoint 1 at 0x40055d: file main.c, line 8.
(gdb) run   # 启动程序运行
Starting program: /root/main_debug 
Breakpoint 1, main (argc=1, argv=0x7fffffffe498) at main.c:11   # 程序运行到断点处,此时程序会暂停
11||int i = 0;
Missing separate debuginfos, use: debuginfo-install glibc-2.17-326.el7_9.x86_64
(gdb) generate-core-file    # 手动生成 core dump 文件
Saved corefile core.10313
(gdb) quit
[root@master1 ~]# ls -lh core.10313  # 查看生成的core dump 文件
-rw-r--r-- 1 root root 369K 6月  29 21:48 core.10313
[root@master1 ~]# 
  • shell 下使用 gcore pid(进程号) 命令生成 core dump 文件
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[root@master1 ~]# sleep 30&     # 使用 sleep 命令启动一个 进程 
[1] 14731       # 进程 pid 为 14731
[root@master1 ~]# gcore 14731   # 使用 gcore 命令生成 core dump 文件
0x00007fa8775609e0 in __nanosleep_nocancel () from /lib64/libc.so.6
warning: target file /proc/14731/cmdline contained unexpected null characters
Saved corefile core.14731
[Inferior 1 (process 14731) detached]
[root@master1 ~]# ls -lh core.14731     # 查看生成的core dump 文件
-rw-r--r-- 1 root root 361K 6月  29 21:57 core.14731
[root@master1 ~]# 
  • shell 下使用 kill -s signal(信号名) pid(进程号)kill -NO(信号值) pid(进程号) 命令生成 core dump 文件
1
2
3
kill -s SIGQUIT pid(进程号)  或者 kill -3 pid(进程号)
kill -s SIGABRT pid(进程号)  或者 kill -6 pid(进程号)
kill -s SIGSEGV pid(进程号)  或者 kill -11 pid(进程号)

以下列表为 可产生 coredump 文件的信号

Tips: Linux 下查看 信号列表 ,可以使用 kill -l 命令,查看信号详细信息 可以使用 man 7 signal 命令

4.6 readelf 查看core dump文件信息

从 Type 类型,可知是 Core file 文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
[root@master1 ~]# readelf -h core-main-20107-1719655316 
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              CORE (Core file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          64 (bytes into file)
  Start of section headers:          0 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         18
  Size of section headers:           0 (bytes)
  Number of section headers:         0
  Section header string table index: 0
[root@master1 ~]# 

4.7 gdb查看core文件

使用 gdb 查看 core 文件的内容,以定位文件中引发core dump的行。

要在 GDB 中加载一个 core 文件进行调试,可以按照以下方法步骤进行操作:

  • 方式一 使用 gdb core文件gdb -c core文件 可以直接打开core file进行查看,但是缺少对应的符号表,很多函数都是 ?? 的形形式
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
[root@master1 ~]# gdb core-main_debug-3906-1719891242 
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-120.el7
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
[New LWP 3906]
Missing separate debuginfo for the main executable file
Try: yum --enablerepo='*debug*' install /usr/lib/debug/.build-id/90/373eda36e2597dd1d6c2de3d4098d883e9a5cc
Core was generated by `./main_debug'.
Program terminated with signal 11, Segmentation fault.
#0  0x00000000004005cd in ?? ()    // 缺少对应的符号表,函数都是 `??` 的形形式
(gdb) 
  • 方式二 使用 gdb exe(可执行文件名) core文件gdb ./exe(可执行文件名) core文件 可以导入原程序,增加符号表
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
[root@master1 ~]# gdb main_debug core-main_debug-3906-1719891242 
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-120.el7
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /root/main_debug...done.   # 从程序中导入符号表Reading symbols from /root/main_debug...
[New LWP 3906]
Core was generated by `./main_debug'.
Program terminated with signal 11, Segmentation fault.
#0  0x00000000004005cd in main (argc=1, argv=0x7ffcf62a1648) at main.c:21
21	    *nP = 100;
  • 方式二:进入程序调试模式后使用 core-file 命令载入 core 文件,如下
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
[root@master1 ~]# gdb main_debug 
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-120.el7
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /root/main_debug...done.
(gdb) core-file core-main_debug-3906-1719891242 
[New LWP 3906]
Core was generated by `./main_debug'.
Program terminated with signal 11, Segmentation fault.
#0  0x00000000004005cd in main (argc=1, argv=0x7ffcf62a1648) at main.c:21
21	    *nP = 100;
(gdb) 

载入 core 文件之后就可以,通过 bt(backtrace) 命令(或者 where )可以看到函数的调用栈情况,info frame 查看当前栈帧信息。

正常发布环境是不会带上调试信息的,因为调试信息通常会占用比较大的存储空间,一般都会在编译的时候把-g选项去掉。没有调试信息的情况下找 core 文件的处理:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
[root@master1 ~]# gdb main core-main-27813-1719893987 
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-120.el7
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /root/main...(no debugging symbols found)...done.  # 五调试信息
[New LWP 27813]
Core was generated by `./main'.
Program terminated with signal 11, Segmentation fault.
#0  0x0000000000400558 in Func ()
(gdb) bt                            # 打印函数调用信息
#0  0x0000000000400558 in Func ()
#1  0x00000000004005c9 in main ()
(gdb) f 0                           # 进入指定函数调用堆栈
#0  0x0000000000400558 in Func ()
(gdb) disassemble                   # 反汇编
Dump of assembler code for function Func:
   0x000000000040052d <+0>:	push   %rbp
   0x000000000040052e <+1>:	mov    %rsp,%rbp
   0x0000000000400531 <+4>:	sub    $0x20,%rsp
   0x0000000000400535 <+8>:	mov    %edi,-0x14(%rbp)
   0x0000000000400538 <+11>:	mov    -0x14(%rbp),%eax
   0x000000000040053b <+14>:	mov    %eax,%esi
   0x000000000040053d <+16>:	mov    $0x400670,%edi
   0x0000000000400542 <+21>:	mov    $0x0,%eax
   0x0000000000400547 <+26>:	callq  0x400410 <printf@plt>
   0x000000000040054c <+31>:	movq   $0x0,-0x8(%rbp)
   0x0000000000400554 <+39>:	mov    -0x8(%rbp),%rax
=> 0x0000000000400558 <+43>:	movl   $0x64,(%rax)
   0x000000000040055e <+49>:	leaveq 
   0x000000000040055f <+50>:	retq   
End of assembler dump.
(gdb) 

没有调试信息的情况下,打开coredump堆栈,并不会直接显示core的代码行。此时,frame addr(帧数)或者简写如上,f 0 跳转到core堆栈的第0帧。因为第0帧是函数调用处。

disassemble 打开该帧函数的反汇编代码。如上箭头位置表示coredump时该函数调用所在的位置

当然,现实环境中,coredump的场景肯定远比这个复杂,都是逻辑都是一样的,我们需要先找到coredump的位置,再结合代码以及core文件推测coredump的原因。

查看其它相关信息:

1
2
3
4
5
6
7
8
# 在 GDB 提示符下,使用以下命令查看共享库信息
(gdb) info sharedlibrary
# 如果想查看特定共享库的详细信息,可以使用以下命令
(gdb) info sharedlibrary <共享库名称>
# 可以使用以下格式设置搜索路径,多个路径之间使用冒号分隔
(gdb) set solib-search-path <路径1>:<路径2>:...
# 可以使用以下格式设置系统根目录
(gdb) set sysroot <目录路径>

查看每个线程的堆栈信息

  • 首先,info threads 查看所有线程正在运行的指令信息
  • 其次,thread apply all bt 打开所有线程的堆栈信息
  • 最后,threadapply threadID bt 查看指定线程堆栈信息,如:thread apply 5 bt 查看 5 号线程信息

进入指定线程栈空间

  • thread threadID 可进入指定线程栈空间,如:
  • info frame 查看当前栈帧信息

没有调试信息的情况下找core的代码行

gdb调试coredump,大部分时候还是只能从core文件找出core的直观原因,但是更根本的原因一般还是需要结合代码一起分析当时进程的运行上下文场景,才能推测出程序代码问题所在。

因此gdb调试coredump也是需要经验的积累,只有有一定的功底和对于基础知识的掌握才能在一堆二进制符号的core文件中找出问题的所在。