添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
GDB 调试入门

GDB 调试入门

0. 序

调试程序是开发过程中必不可少的一环,在 Windows 或 MacOS 上开发时,可以使用 VS 和 CLion 等 IDE 上自带的调试功能来打断点或查看变量和堆栈,但 Linux 并没有图形化的操作界面,而如果只通过打 log 的方式来查找问题的话效率将会非常低下,此时我们可以利用 GDB 来提升我们的开发效率。

GDB 是 GNU Debugger 的简写,是 GNU 软件系统中的标准 调试器 。GDB 具备各种调试功能,包括但不限于打断点、单步执行、打印变量、查看寄存器、查看函数调用堆栈等,能够有效地针对函数的运行进行追踪和警告;使用 GDB 调试时,可以监督和修改程序的变量,并且这些修改是独立于主程序之外的。GDB 主要用于调试编译型语言,对 C,C++,Go,Fortran 等语言有内置的支持,但它不支持解释型语言。

1. 环境搭建

1.1 编写程序

为了进行调试,我们需要准备一个简单的 C++ 程序:

 $ cat test.cpp
 #include <iostream>
 void Func(const char *s) {
     int *p = nullptr;
     int &r = static_cast<int&>(*p);
     int num = std::atoi(s);
     r = num;
     printf("%d\n", r);
 int main (int argc, char *argv[]) {
     if (argc != 2) {
         printf("test [int]\n");
         return -1;
     Func(argv[1]);
     return 0;

1.2 编译

对于 C/C++ 程序,在使用 gcc/clang 编译的时候需要加上参数 -g,才能生成完整的调试信息并在 GDB 中调试:

 $ clang++ -g -std=c++11 -m64 -o test test.cpp

2. 调试示例

GDB 有非常多的功能,当我们忘记如何使用这些功能时,可以在 GDB 交互界面里输入 help help all 来查看指令:

 (gdb) help
 List of classes of commands:
 ...

2.1 启动

我们可以使用 gdb [executable file] 来启动调试:

 $ gdb test.cpp
 Reading symbols from test...done.
 (gdb)

也可以直接使用 gdb 来进入交互界面,再使用 file 来指定要调试的程序:

 $ gdb
 (gdb) file test
 Reading symbols from test...done.

2.2 运行

对于带参数的程序,我们可以使用 set args [arg] ... 设置参数,再使用 run 来运行:

 (gdb) set args 1
 (gdb) run
 Starting program: test 1
 ...

当然也可以直接在 run 后加上参数来运行:

 (gdb) run 1
 Starting program: test 1
 Program received signal SIGSEGV, Segmentation fault.
 0x0000000000400749 in Func (s=0x7fffffffe167 "test") at test.cpp:8
 8           r = num;

我们发现程序出现了 Segmentation fault,此时可以通过打断点来调试程序。

2.3 断点

通过错误信息我们可以看到是函数 Func 中,test.cpp 的第 8 行出现了错误,于是我们可以打上两个断点,一个在进入 Func 函数时,一个在 test.cpp 文件的第 8 行,并使用 info breakpoints info b 来查看我们设置了哪些断点:

 (gdb) b test.cpp:8
 Breakpoint 1 at 0x400742: file test.cpp, line 8.
 (gdb) b Func
 Breakpoint 2 at 0x40071c: file test.cpp, line 4.
 (gdb) info b
 Num     Type           Disp Enb Address            What
 1       breakpoint     keep y   0x0000000000400742 in Func(char const*) at test.cpp:8
 2       breakpoint     keep y   0x000000000040071c in Func(char const*) at test.cpp:4

2.4 调试

设置好断点之后就可以使用 run 开始运行了:

 (gdb) run
 Starting program: test 1
 Breakpoint 2, Func (s=0x7fffffffe167 "test") at test.cpp:4
 4           int *p = nullptr;

因为我们在 Func 函数的开头设置了一个断点,所以程序在进入 Func 函数时暂停了;我们可以使用 list l [function name] 来查看函数的上下文:

 (gdb) l Func
 1       #include <iostream>
 3       void Func(const char *s) {
 4           int *p = nullptr;
 5           int &r = static_cast<int&>(*p);
 7           int num = std::atoi(s);
 8           r = num;
 9           printf("%d\n", r);
 10      }

此时我们可以使用 print p 来查看变量:

 (gdb) p s
 $1 = 0x7fffffffe187 "1"
 (gdb) p *s
 $2 = 49 '1'

到目前为止还没有出现问题,我们可以使用 continue c 走到下一个断点处:

 (gdb) c
 Continuing.
 Breakpoint 2, Func (s=0x7fffffffe187 "1") at test.cpp:8
 8           r = num;

查看一下当前作用域的一些变量:

 (gdb) p s
 $8 = 0x7fffffffe187 "1"
 (gdb) p p
 $9 = (int *) 0x0
 (gdb) p *p
 Cannot access memory at address 0x0
 (gdb) p r
 $10 = (int &) @0x0: <error reading variable>
 (gdb) p &r
 $11 = (int *) 0x0
 (gdb) p num
 $12 = 1

会发现 r 这个 int& 绑定到了一个被解引用的空指针上,所以 &r == nullptr,因此不能读取 r 的值,我们可以用 next n 来进行单步执行,看看接下来会发生什么:

 (gdb) n
 Program received signal SIGSEGV, Segmentation fault.
 0x0000000000400749 in Func (s=0x7fffffffe187 "1") at test.cpp:8
 8           r = num;

果然发生了 Segmentation fault。

3. GDB 命令

根据刚才的示例程序,可以整理出一些常用的 GDB 命令。

3.1 启动

  1. 将 GDB 链接到一个可执行文件并启动
    gdb [executable file]

    gdb
    (gdb) file [executable file]
  2. 将 GDB 链接到一个正在运行的进程并启动
    gdb
    (gdb) attach [PID]

3.2 断点

  1. 增加断点
  • b [function name]
  • b [file:line]


  1. 查看断点
  • info b


  1. 删除断点
    delete [breakpoint number]
    d [breakpoint number]
    clear # 删除当前行的断点
    clear [function name] # 删除某个函数处的断点
    clear [file:line] # 删除某一行的断点
  2. 禁用和启用断点
    disable # 禁用所有断点
    disable [breakpoint number] # 禁用某个断点
    enable # 启用所有断点
    enable [breakpoint number] # 启用某个断点

3.3 运行

  1. 从头开始运行
  • run


  1. 单步执行
  • next n


  1. 进入函数内部
  • step s
  • stepi 执行一条机器指令


3.4 查看

  1. 查看代码

    list # 从头开始依次打印,默认每次打印 10 行
    l # 同上
    l [function name] # 从函数定义开始打印
    l [file:line] # 从某一行开始打印
    set listsize 20 # 修改每次打印的行数
  2. 查看变量
    print [expression] # 打印表达式
    p [expression] # 同上
    ptype [expression] # 打印表达式的类型
    info args # 打印函数参数
    info locals # 打印局部变量
    info registers # 打印寄存器信息

3.5 修改

  1. 修改变量
    set variable i = 10 # 将变量 i 设置为 10
    set var i = 10 # 同上
    p i = 10 # 将变量 i 设置为 10,并打印

3.6 调用信息

  1. 查看函数调用栈信息
  • backtrace bt
  • where


  1. 查看当前栈帧
  • frame f


4. corefile

core dump / crash dump / memory dump / system dump 都是指一个程序在特定时间崩溃(crash)时的内存记录,它包含了很多关键信息,比如寄存器(包括程序计数器和堆栈指针),内存管理信息,操作系统标志信息等。corefile 就是转储(dump)时的快照,corefile可以被重新执行用以调试错误信息。

4.1 生成

为了让系统能够生成 corefile,需要先检查配置:

 $ ulimit -c
 unlimited

如果结果是 0 则说明系统禁止了 corefile 的生成,需要执行 ulimit -c unlimited 来让 corefile 能够正常生成。以刚才的示例程序为例,先执行 test 文件,生成一个 corefile:

 $ ./test 1
 段错误 (core dumped)
 $ ll /data/corefile
 -rw------- 1 root root      450560 4月  22 17:07 core_test_1587546447.28284

能够看到系统在特定的目录下(可以修改)生成了一个 corefile 叫做 core_test_1587546447.28284,我们使用 gdb 来对这个 corefile 进行调试。

4.2 调试

要执行 corefile 还需要准备对应的可执行文件,运行 gdb [executable file] [corefile] 就能开始调试了:

 $ gdb test /data/corefile/core_test_1587546447.28284
 Core was generated by `./test 1'.
 Program terminated with signal 11, Segmentation fault.
 #0  0x0000000000400749 in Func (s=0x7ffed098c19e "1") at test.cpp:8
 8           r = num;

因为示例程序比较简单,因此函数调用栈也比较少,我们可以先使用 bt 打印函数调用栈信息:

 (gdb) bt
 #0  0x0000000000400749 in Func (s=0x7ffed098c19e "1") at test.cpp:8
 #1  0x00000000004007c0 in main (argc=2, argv=0x7ffed098afb8) at test.cpp:17

我们通过 frame 0 f 0 进入程序崩溃的栈帧来查看相关信息:

 (gdb) f 0
 #0  0x0000000000400749 in Func (s=0x7ffed098c19e "1") at test.cpp:8
 8           r = num;
 (gdb) info args
 s = 0x7ffed098c19e "1"
 (gdb) info locals
 p = 0x0
 r = @0x0: <error reading variable>
 num = 1
 (gdb) ptype r
 type = int &
 (gdb) ptype p
 type = int *

查看一下当前栈帧的汇编代码:

 (gdb) disas
 Dump of assembler code for function Func(char const*):
    0x0000000000400710 <+0>:     push   %rbp
    0x0000000000400711 <+1>:     mov    %rsp,%rbp
    0x0000000000400714 <+4>:     sub    $0x20,%rsp
    0x0000000000400718 <+8>:     mov    %rdi,-0x8(%rbp)
    0x000000000040071c <+12>:    movq   $0x0,-0x10(%rbp)
    0x0000000000400724 <+20>:    mov    -0x10(%rbp),%rdi
    0x0000000000400728 <+24>:    mov    %rdi,-0x18(%rbp)
    0x000000000040072c <+28>:    mov    -0x8(%rbp),%rdi
    0x0000000000400730 <+32>:    callq  0x4005a0 <atoi@plt>
    0x0000000000400735 <+37>:    movabs $0x400860,%rdi
    0x000000000040073f <+47>:    mov    %eax,-0x1c(%rbp)
    0x0000000000400742 <+50>:    mov    -0x1c(%rbp),%eax
    0x0000000000400745 <+53>:    mov    -0x18(%rbp),%rcx
 => 0x0000000000400749 <+57>:    mov    %eax,(%rcx)
    0x000000000040074b <+59>:    mov    -0x18(%rbp),%rcx
    0x000000000040074f <+63>:    mov    (%rcx),%esi
    0x0000000000400751 <+65>:    mov    $0x0,%al
    0x0000000000400753 <+67>:    callq  0x400550 <printf@plt>
    0x0000000000400758 <+72>:    mov    %eax,-0x20(%rbp)
    0x000000000040075b <+75>:    add    $0x20,%rsp
    0x000000000040075f <+79>:    pop    %rbp
    0x0000000000400760 <+80>:    retq
 End of assembler dump.

查看寄存器状态:

 (gdb) i r
 rax            0x1      1
 rbx            0x0      0
 rcx            0x0      0
 rdx            0xa      10
 rsi            0x0      0
 rdi            0x400860 4196448
 rbp            0x7ffed098aea0   0x7ffed098aea0
 rsp            0x7ffed098ae80   0x7ffed098ae80
 r8             0x7f9c88bbf060   140310285643872
 r9             0x7ffed098c19f   140732398092703
 r10            0x1      1
 r11            0x0      0
 r12            0x40061c 4195868
 r13            0x7ffed098afb0   140732398088112
 r14            0x0      0
 r15            0x0      0
 rip            0x400749 0x400749 <Func(char const*)+57>
 eflags         0x10206  [ PF IF RF ]