[译文]RMS's gdb Debugger Tutorial

Posted by API Caller on February 20, 2020

RMS’s gdb Debugger Tutorial 原文

RMS’s gdb Debugger Tutorial 译文前五章

最近在看这篇, 找到 _Xie_ 的译文, 后两章未有翻译, 我将尝试接上, 不过水平实在有限, 不免续貂.

“Don’t worry if it doesn’t work right. If everything did, you’d be out of a job.”
--Unknown

1» 如何使用 gdb 调试工具?


当你编译你的程序时,你必须告诉编译器生成程序的同时兼容调试器。调试器需要指定的信息才能正确的运行。为了达到这个目的,你必须使用编译选项 -g 。这一步骤非常重要。如果没有这个选项,调试器无法获得符号信息。这就意味着它不知道函数和变量是如何调用的。当你需要信息时,它也无法理解。

1.1 如何在编译时获得调试符号?

给编译器指定选项 -g
prompt > gcc -g program.c -o programname

NOTE: 如果你的程序包括多个文件,每一个文件都要指定 -g 选项,在链接时,同样也需要设置该选项。

1.2 如何在调试器时运行程序?

在开启调试器时,将你的程序名作为第一个参数。
prompt> gdb programname
接下来使用 run 命令开始执行程序,通过以下方式来设置程序运行需要的参数。
(gdb) run arg1 "arg2" ...

1.3 如何在调试器中重新运行程序?

先使用 kill 命令停止调试程序。接下来使用 run 命令再次进入调试。

1
2
3
(gdb) kill
    Kill the program being debugged? (y or n) y
(gdb) run ...

1.4 如何退出调试器?

使用 quit 命令。
(gdb) quit

NOTE: 当你执行该命令时,可能会提示你是否需要结束当前程序,输入y确认。

1
2
3
(gdb) quit
    The program is running. Exit anyway? (y or n) y
prompt >

1.5 如何在调试时查看帮助?

使用 help 命令,gdb 对每一个命令都有相应的描述,比这篇文章提到的命令要多得多。提供帮助的参数是您想要获得的信息。如果你只是输入 “help” 不带任何参数。你将获得一个类似于以下的帮助主题列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(gdb) help
    List of classes of commands:
    
    aliases -- Aliases of other commands
    breakpoints -- Making program stop at certain points
    data -- Examining data
    files -- Specifying and examining files
    internals -- Maintenance commands
    obscure -- Obscure features
    running -- Running the program
    stack -- Examining the stack
    status -- Status inquiries
    support -- Support facilities
    tracepoints -- Tracing of program execution without stopping the program
    user-defined -- User-defined commands
    
    Type "help" followed by a class name for a list of commands in that class.
    Type "help" followed by command name for full documentation.
    Command name abbreviations are allowed if unambiguous.

2» 如何观察程序的执行?


gdb 的功能有点像程序的解释器,你可以在任何时候给程序发送信号停止来你的程序。通常情况下,中断信号 SIGINT是由 Ctrl - C组合键来完成。在gdb之外,这将终止你的程序。gdb 捕捉这个信号,并停止执行你的程序。使用断点你也可以让程序在任意一行代码或函数调用处停止。一旦你的程序暂停,你可以检查你在代码的那个位置,你可以查看当前在作用域中的变量,以及内存空间和cpu寄存器。你也可以改变变量的值和内存,去看看对你的代码有什么影响。

2.1 如何停止执行?

你可以通过发送UNIX信号(如SIGINT)来停止程序的执行。这是使用 CTRL + C 组合键完成的。在接下来的例子中,我会在 Starting Program... 出现后按下 Ctrl-C。

1
2
3
4
5
6
7
8
(gdb) run
    Starting Program: /home/ug/ryansc/a.out
    
    Program received signal SIGINT, Interrupt.
    0x80483b4 in main(argc=1, argv=0xbffffda4) at loop.c:5
    5   while(1){
    ...
(gdb)

2.2 如何继续执行程序?

使用 continue 命令,在程序停止的时候重新启动程序。

2.3 怎么知道程序停止的位置?

使用 list 命令使 gdb 打印出断点所在位置附近的代码。下面这个例子,断点在第8行。

1
2
3
4
5
6
7
8
9
10
(gdb) list
    3       int main(int argc, char **argv)
    4       {
    5         int x = 30;
    6         int y = 10;
    7       
    8         x = y;
    9       
    10        return 0;
    11      }

2.4 如何一行一行地单步执行代码?

首先发送信号或者使用断点停止你的程序,然后使用 nextstep 命令。

1
2
3
4
    5   while(1){
(gdb) next
    7   }
(gdb)

NOTE: nextstep 命令是不同的。一行代码中包含函数调用时,next 会跳过这个函数的内部执行细节,运行下一行代码,而 step 会跳转到函数的内部中。
next 命令:

1
2
3
4
(gdb)
    11     fun1();
(gdb) next
    12 }

step 命令:

1
2
3
4
5
6
(gdb)
    11     fun1();
(gdb) step
    fun1 () at loop.c:5
    5    return 0;
(gdb)

2.5 如何检查变量的值?

使用变量名作为 print 的参数。比如,如果你程序中有 int xchar *s:

1
2
3
4
5
(gdb) print x
    $1 = 900
(gdb) print s
    $3 = 0x8048470 "Hello World!\n"
(gdb)

NOTE: print 命令的输出总是 $## = (value) 格式。$##是只是一个记数器,可以跟踪你检查过的变量。

2.6 如何更改变量的值?

使用 set 命令,C语言中的赋值语句作为其参数。比如,改变 x 的值为3:

1
2
3
(gdb) set x = 3
(gdb) print x
    $4 = 3

NOTE: 在 gdb 的新版本中,使用 set var 是必要的,这里就应该是 set var x = 3

2.7 如何调用链接到我的程序中的函数?

在调试器命令行中,你可以使用 call 命令来调用任意函数链接到你的程序中去。这包括你自己的代码和标准库函数。比如,如果你希望你的程序 dump core
(gdb) call abort()

2.8 如何从一个函数中返回?

使用 finish 命令,结束当前函数的执行并返回到该函数的调用者处。这条命令也会显示函数的返回值。

1
2
3
4
5
(gdb) finish
    Run till exit from #0  fun1 () at test.c:5
    main (argc=1, argv=0xbffffaf4) at test.c:17
    17        return 0;
    Value returned is $1 = 1

3» 如何使用调用栈?


调用堆栈是我们找到控制程序流的堆栈帧。当一个函数被调用时,它会创建一个栈帧,告诉计算机在函数结束执行之后如何将控制权返回给调用者。栈帧也是局部变量和函数参数存储的地方。我们可以观察栈帧来推断程序是如何运行的。找到当前帧下面的栈帧列表称为回溯 (backtrace)。

3.1 如何获得 backtrace?

使用 backtrace 命令,在下面的backtrace中,我们可以看到当前在 func2() 是被 func1() 调用的,func1() 又是被 main() 调用的。

1
2
3
4
5
6
(gdb) backtrace
    #0  func2 (x=30) at test.c:5
    #1  0x80483e6 in func1 (a=30) at test.c:10
    #2  0x8048414 in main (argc=1, argv=0xbffffaf4) at test.c:19
    #3  0x40037f5c in __libc_start_main () from /lib/libc.so.6
(gdb) 

3.2 如何改变栈帧?

使用 frame 命令,注意上面的每一个栈帧都有编号。这个编号作为使用 frame 命令的参数。

1
2
3
4
(gdb) frame 2
    #2  0x8048414 in main (argc=1, argv=0xbffffaf4) at test.c:19
    19        x = func1(x);
(gdb) 

3.3 如何检查栈帧?

有3个有用的命令可以用来获得当前帧的内容。info frame 显示当前栈帧的信息。info locals 显示当前栈帧的局部变量的列表和它们的值。info args显示参数列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(gdb) info frame
    Stack level 2, frame at 0xbffffa8c:
     eip = 0x8048414 in main (test.c:19); saved eip 0x40037f5c
     called by frame at 0xbffffac8, caller of frame at 0xbffffa5c
     source language c.
     Arglist at 0xbffffa8c, args: argc=1, argv=0xbffffaf4
     Locals at 0xbffffa8c, Previous frame's sp is 0x0
     Saved registers:
      ebp at 0xbffffa8c, eip at 0xbffffa90
(gdb) info locals
    x = 30
    s = 0x8048484 "Hello World!\n"
(gdb) info args
    argc = 1
    argv = (char **) 0xbffffaf4

4» 如何使用断点?


断点是告诉调试器你想运行到程序代码的固定行的方法。你也可以在你的程序调用指定的函数处停止运行。一旦你的程序停止,你可以在内存中查看所有变量的值,检查栈,并单步执行程序。

4.1 如何在某一行上设置断点?

设置断点的命令是 break。如果你只有一个源文件,你可以像这样设置一个断点:

1
2
(gdb) break 19
    Breakpoint 1 at 0x80483f8: file test.c, line 19

如果不只一个文件,你必须给break命令提供一个文件名:

1
2
(gdb) break test.c:19
    Breakpoint 2 at 0x80483f8: file test.c, line 19  

4.2 如何在一个C函数上设置一个断点?

在一个C函数上设置断点,要传递一个函数名给break

1
2
(gdb) break func1
    Breakpoint 3 at 0x80483ca: file test.c, line 10  

4.3 如何在一个C++函数上设置一个断点?

方法类似于在C函数设置断点。然而C++是多态的,因此你必须在设置断点时指定函数的版本(甚至只有一个函数也需要)。要做到这一点,你可以告诉它参数类型的列表。

1
2
(gdb) break TestClass::testFunc(int) 
    Breakpoint 1 at 0x80485b2: file cpptest.cpp, line 16.

4.4 如何设置一个临时断点?

使用tbreak取代break命令。一个临时断点只在断点处停止一次,然后会被移除。

4.5 如何获取断点列表?

使用info breakpoints命令:

1
2
3
4
(gdb) info breakpoints
    Num Type           Disp Enb Address    What
    2   breakpoint     keep y   0x080483c3 in func2 at test.c:5
    3   breakpoint     keep y   0x080483da in func1 at test.c:10

4.6 如何禁用断点?

使用disable命令,并指定一个参数,这个参数为你希望禁用的断点的序号。你可以在断点列表查找断点序号,具体方法在上面提过。在下面的例子中,我们可以看到断点2被关闭(Enb列有一个n标识)

1
2
3
4
5
(gdb) disable 2
(gdb) info breakpoints
    Num Type           Disp Enb Address    What
    2   breakpoint     keep n   0x080483c3 in func2 at test.c:5
    3   breakpoint     keep y   0x080483da in func1 at test.c:10

4.7 如何跳过一个断点?

想跳过一个断点固定的次数,我们可以使用ignore命令,这个命令带有两个参数:待跳过的断点序号,跳过该断点的次数。

1
2
(gdb) ignore 2 5
    Will ignore next 5 crossings of breakpoint 2.

5» 如何使用监测点(watchpoints)?


监测点与断点类似。然而监测点不是为函数或是某一行代码准备的。监测点是为了变量准备的。当那些变量被读写时,监测点会被触发,程序会停止执行。
通过以上的描述理解 watchpoints 会有点困难,因此接下来的示例程序会被作为命令使用例子。

1
2
3
4
5
6
7
8
#include <stdio.h>
int main(int argc, char **argv)
{
  int x = 30;
  int y = 10;
  x = y;
  return 0;
}

5.1 如何为一个变量设置一个 write watchpoint

使用 watch 命令。watch 命令的参数是一个被评估的表达式。这意味着你想要设置一个 watchpoint 的变量必须在当前范围内。因此,要在非全局变量上设置一个 watchpoint ,你必须设置一个断点,当变量处于作用域时,它将停止你的程序。在程序中断之后,设置监视点。

NOTE: 在下面的示例中,你可能会注意到,打印的代码行与更改变量x的行不匹配,这是因为设置 watchpoint 的存储指令是执行“x=y”任务所需的最后一个序列。所以调试器已经进入下一行代码了。在示例中,在’main’函数中设置了一个断点,并被触发以停止该程序。 ``` (gdb) watch x Hardware watchpoint 4: x (gdb) c Continuing. Hardware watchpoint 4: x

1
2
3
4
Old value = -1073743192
New value = 11
main (argc=1, argv=0xbffffaf4) at test.c:10
10      return 0; ```  

5.2 如何为一个变量设置一个read watchpoint

使用rwatch命令,用法与watch命令相同

1
2
3
4
5
6
7
8
9
(gdb) rwatch y 
    Hardware read watchpoint 4: y
(gdb) continue
    Continuing.
    Hardware read watchpoint 4: y
    
    Value = 1073792976
    main (argc=1, argv=0xbffffaf4) at test.c:8
    8         x = y;

5.3 如何为变量设置一个read/write 监测点?

使用 awatch 命令,用法与 watch 命令相同

5.4 如何关闭监测点?

激活的监测点显示在断点列表。使用 info breakpoints 命令来获得这个列表。然后使用 disable 命令关闭一个watchpoint,就像禁用断点一样。

1
2
3
4
5
6
7
(gdb) info breakpoints
    Num Type           Disp Enb Address    What
    1   breakpoint     keep y   0x080483c6 in main at test.c:5
            breakpoint already hit 1 time
    4   hw watchpoint  keep y   x
            breakpoint already hit 1 time
(gdb) disable 4

6» gdb 高级用法


6.1 如何查看内存?

x 命令查看内存, 命令格式为 x/nfu addr1.

  • n: 输出单元的个数
  • f: 输出格式(x: 16进制; o: 8进制)
  • u: 一个单元的长度(b: byte; h: halfword; w: word; g: giantword)
  • addr: 可以是地址也可以是符号

还有很多, help x 可以看到所有情况.

假设被调试的代码是 char *s = "Hello World\n"

  • 查看字符串
1
2
(gdb) x/s s
    0x8048434 <_IO_stdin_used+4>:    "Hello World\n"
  • 查看字符
1
2
(gdb) x/c s
    0x8048434 <_IO_stdin_used+4>:   72 'H'
  • 查看四个字符
1
2
(gdb) x/4c s
    0x8048434 <_IO_stdin_used+4>:   72 'H'  101 'e' 108 'l' 108 'l'

6.2 如何查看寄存器?

info registers 命令, 会根据硬件平台输出, 以 Intel 平台为例:

1
2
3
4
5
6
7
8
9
(gdb) info registers
    eax            0x40123460       1074934880
    ecx            0x1      1
    edx            0x80483c0        134513600
    ebx            0x40124bf4       1074940916
    esp            0xbffffa74       0xbffffa74
    ebp            0xbffffa8c       0xbffffa8c
    esi            0x400165e4       1073833444
    ...

6.3 如何调试 core 文件?

不想用转储这个词.

程序出现段错误(segfault) 时会留下 core dump 文件, 可以用 gdb 查看以了解程序崩溃时的状况. 用 core 命令加载 core 文件.

1
2
3
4
5
6
7
prompt > myprogram
    Segmentation fault (core dumped)
prompt > gdb myprogram
...
(gdb) core core
...

6.4 如何逐指令地单步执行代码?

nextistepi, 与 nextstep 对应.

6.5 如何查看被调试程序的汇编代码?

disassemble 命令. 参数可以是地址也可以是函数名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(gdb) disassemble main
    Dump of assembler code for function main:
    0x80483c0 <main>:       push   %ebp
    0x80483c1 <main+1>:     mov    %esp,%ebp
    0x80483c3 <main+3>:     sub    $0x18,%esp
    0x80483c6 <main+6>:     movl   $0x0,0xfffffffc(%ebp)
    0x80483cd <main+13>:    mov    0xfffffffc(%ebp),%eax
    0x80483d0 <main+16>:    movb   $0x7,(%eax)
    0x80483d3 <main+19>:    xor    %eax,%eax
    0x80483d5 <main+21>:    jmp    0x80483d7 <main+23>
    0x80483d7 <main+23>:    leave  
    0x80483d8 <main+24>:    ret    
    End of assembler dump.

7» 调试举例


7.1 无限循环

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1 : #include <stdio.h>
2 : #include <ctype.h>
3 : 
4 : int main(int argc, char **argv)
5 : {
6 :   char c;
7 :
8 :   c = fgetc(stdin);
9 :   while(c != EOF){
10:
11:	    if(isalnum(c))
12:	      printf("%c", c);
13:     else
14:	      c = fgetc(stdin);
15:   }
16:
17:   return 1;
18: }

先编译:

prompt> gcc -g inf.c -static

对于新的 gcc 来说, 此处为静态编译比较适合复现原文的执行和调试过程

运行可知程序会在获得输入后进入死循环输出, 试着来找出原因:

加载 gdb:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
prompt> gdb a.out
GNU gdb (Ubuntu 8.1-0ubuntu3.2) 8.1.0.20180409-git
Copyright (C) 2018 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-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from a.out...done.
(gdb) 

启动这个循环, 然后立刻按下 Ctrl + C 也就是往程序发送 SIGINT

1
2
3
4
5
6
7
8
9
10
11
12
(gdb) run
    Starting program: /home/x/a.out 
    moo
    mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm
    mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm
    ....
    mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm
    mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm
    Program received signal SIGINT, Interrupt.
    0x0000000000449711 in write ()
(gdb) 

backtrace 查看调用栈:

1
2
3
4
5
6
7
8
(gdb) backtrace 
    #0  0x0000000000449711 in write ()
    #1  0x000000000041349d in _IO_new_file_write ()
    #2  0x00000000004146a1 in _IO_new_do_write ()
    #3  0x000000000041544b in _IO_new_file_overflow ()
    #4  0x00000000004103f5 in putchar ()
    #5  0x0000000000400b9b in main (argc=1, argv=0x7fffffffdfe8) at inf.c:12

真正想看的是 main 里面的, 切换到对应栈帧:

1
2
3
4
(gdb) frame 5
    #5  0x0000000000400b9b in main (argc=1, argv=0x7fffffffdfe8) at inf.c:12
    12	      printf("%c", c);

看一下 c 的值:

1
2
(gdb) print c
    $1 = 109 'm'

可见目前停在了 write 中, 用 next 回到 main():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(gdb) next
    Single stepping until exit from function _IO_new_file_write,
    which has no line number information.
    0x00000000004146a1 in _IO_new_do_write ()
(gdb) next
    Single stepping until exit from function _IO_new_do_write,
    which has no line number information.
    0x000000000041544b in _IO_new_file_overflow ()
(gdb) next
    Single stepping until exit from function _IO_new_file_overflow,
    which has no line number information.
    0x00000000004103f5 in putchar ()
(gdb) next
    Single stepping until exit from function putchar,
    which has no line number information.
    main (argc=1, argv=0x7fffffffdfe8) at inf.c:9
    9	  while(c != EOF){

main()next 观察执行情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
(gdb) n
    11	    if(isalnum(c))
(gdb) n
    12	      printf("%c", c);
(gdb) n
    9	  while(c != EOF){
(gdb) n
    11	    if(isalnum(c))
(gdb) n
    12	      printf("%c", c);
(gdb) n
    9	  while(c != EOF){

可以发现代码中的 else 分支从未被执行, 删除 else 即可修复错误.

7.2 段错误 (Segmentation Fault)

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1 : #include <stdio.h>
2 : #include <stdlib.h>

3 : int main(int argc, char **argv)
4 : {
5 :   char *buf;
6 :
7 :   buf = malloc(1<<31);
8 :
9 :   fgets(buf, 1024, stdin);
10:   printf("%s\n", buf);
11:
12:   return 1;
13: }

运行可复现:

1
2
3
prompt> ./a.out 
1
Segmentation fault (core dumped)
1
2
3
4
5
6
7
(gdb) run
    Starting program: /home/x/a.out 
    1

    Program received signal SIGSEGV, Segmentation fault.
    0x0000000000410494 in _IO_getline ()

1
2
3
4
5
(gdb) backtrace 
    #0  0x0000000000410494 in _IO_getline ()
    #1  0x000000000041006d in fgets ()
    #2  0x0000000000400b84 in main (argc=1, argv=0x7fffffffdfe8) at segfault.c:7

1
2
3
4
(gdb) frame 2
    #2  0x0000000000400b84 in main (argc=1, argv=0x7fffffffdfe8) at segfault.c:7
    7	  fgets(buf, 1024, stdin);

可见在调用 fgets 的时候崩溃了, 查看 buf

1
2
(gdb) print buf
    $1 = 0x0

buf 是 NULL 显然就是崩溃的原因, 去看分配 buf 的地方:

1
2
3
4
5
6
7
8
9
10
11
(gdb) list
4	{
5	  char *buf;
6	
7	  buf = malloc(1<<31);
8	
9	  fgets(buf, 1024, stdin);
10	  printf("%s\n", buf);
11	
12	  return 1;
13	}

kill 掉在第 7 行下断然后运行起来断下:

1
2
3
4
5
6
7
8
9
10
(gdb) kill
    Kill the program being debugged? (y or n) y
(gdb) b segfault.c:7
    Breakpoint 1 at 0x400b5c: file segfault.c, line 7.
(gdb) run
    Starting program: /home/x/a.out 

    Breakpoint 1, main (argc=1, argv=0x7fffffffdfe8) at segfault.c:7
    7	  buf = malloc(1<<31);

看看 malloc 前的 buf:

1
2
(gdb) print buf
    $1 = 0x6b9018 "0?D"

未初始化, 所以是无意义的内容, 执行完 malloc 再查看:

1
2
3
4
(gdb) next
    9	  fgets(buf, 1024, stdin);
(gdb) print buf
    $2 = 0x0

又是 NULL, 可见 malloc 返回了 NULL, 检查 malloc 的参数可知 1<<31 可能过大, 调小一些重新编译运行, 一切正常, 调试结束.

RMS’s gdb Debugger Tutorial