*unix下使用gdb调试代码

这篇博文实际上是Washington University课程CS 342的中文翻译

源代码

下面的例子所使用的源代码是

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
// main.cc
// Andrew Gilpin
// agg1@cec.wustl.edu

// This file contains the example program used in the gdb debugging
// tutorial. The tutorial can be found on the web at
// http://students.cec.wustl.edu/~agg1/tutorial/

#include <iostream.h>

int number_instantiated = 0;

template <class T>
class Node {
public:
Node (const T &value, Node<T> *next = 0) : value_(value), next_(next) {
cout << "Creating Node, "
<< ++number_instantiated
<< " are in existence right now" << endl;
}
~Node () {
cout << "Destroying Node, "
<< --number_instantiated
<< " are in existence right now" << endl;
next_ = 0;
}

Node<T>* next () const { return next_; }
void next (Node<T> *new_next) { next_ = new_next; };
const T& value () const { return value_; }
void value (const T &value) { value_ = value; }

private:
Node ();
T value_;
Node<T> *next_;
};

template <class T>
class LinkedList {
public:
LinkedList () : head_(0) {};
~LinkedList () { delete_nodes (); };

// returns 0 on success, -1 on failure
int insert (const T &new_item) {
return ((head_ = new Node<T>(new_item, head_)) != 0) ? 0 : -1;
}

// returns 0 on success, -1 on failure
int remove (const T &item_to_remove) {
Node<T> *marker = head_;
Node<T> *temp = 0; // temp points to one behind as we iterate

while (marker != 0) {
if (marker->value() == item_to_remove) {
if (temp == 0) { // marker is the first element in the list
if (marker->next() == 0) {
head_ = 0;
delete marker; // marker is the only element in the list
marker = 0;
} else {
head_ = new Node<T>(marker->value(), marker->next());
delete marker;
marker = 0;
}
return 0;
} else {
temp->next (marker->next());
delete temp;
temp = 0;
return 0;
}
}
marker = 0; // reset the marker
temp = marker;
marker = marker->next();
}

return -1; // failure
}

void print (void) {
Node<T> *marker = head_;
while (marker != 0) {
cout << marker->value() << endl;
marker = marker->next();
}
}

private:
void delete_nodes (void) {
Node<T> *marker = head_;
while (marker != 0) {
Node<T> *temp = marker;
delete marker;
marker = temp->next();
}
}

Node<T> *head_;
};

int main (int argc, char **argv) {
LinkedList<int> *list = new LinkedList<int> ();

list->insert (1);
list->insert (2);
list->insert (3);
list->insert (4);

cout << "The fully created list is:" << endl;
list->print ();

cout << endl << "Now removing elements:" << endl;
list->remove (4);
list->print ();
cout << endl;

list->remove (1);
list->print ();
cout << endl;

list->remove (2);
list->print ();
cout << endl;

list->remove (3);
list->print ();

delete list;

return 0;
}

另外还配有一个简单的Makefile

1
2
3
4
5
6
7
8
CXX = g++
FLAGS = -ggdb -Wall

main: main.cc
${CXX} ${FLAGS} -o main main.cc

clean:
rm -f main

这份代码中定义了两个类,一个节点类以及一个链表类。同时实现了一个简单的测试。

准备

调试记号

gdb可以使用g++生成的调试记号 当gdb调试有与其对应的记号的程序时是最高效的,通过给编译命令加上-g参数可以实现给可执行程序绑定记号

调试

什么时候需要调试工具

首先需要知道的是,调试工具是无可避免的。每一个程序员都无可厚非的曾经在职业生涯的某一刻调试过一段代码。调试有非常多的方式,从在屏幕上打印一些消息,到使用专业的调试工具,或者仅仅是通过思考程序的行为然后做一些猜想来使得程序正确。

在一个bug被修复之前,这个bug的源头需要被正确的定位。例如,段错误发生时,我们需要知道哪一行代码引发了段的错误。当一行有问题的代码被找到的时候,我们还需要知道在这个函数中变量的值是什么,又是谁调用了这个函数,以及为什么这个错误会发生。使用调试工具可以很简单的解答上述的问题。

编译运行源代码中的程序,它会打印一些信息,然后它会表示收到了一个段错误,接着程序就崩溃了。我们就要开始调试这个程序。

载入一个程序

通过编译运行后,现在有了一个可执行文件(下面用main来表示)并且你需要去调试它。首先必须启动调试工具gdb,同时可以告诉它你需要调试哪个文件,比如输入gdb main执行的时候,它就会是下面这个样子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
root@a852a26669ff:/bustub# gdb main
GNU gdb (Ubuntu 8.1.1-0ubuntu1) 8.1.1
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 "aarch64-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 main...done.
(gdb)

gdb现在在等待用户的进一步输入,我们需要运行这个程序因此调试工具可以帮助我们了解程序崩溃的时候发生了什么,在gdb交互程序中输入run,会有如下的消息打出

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) run
Starting program: /bustub/main
warning: Error disabling address space randomization: Operation not permitted
Creating Node, 1 are in existence right now
Creating Node, 2 are in existence right now
Creating Node, 3 are in existence right now
Creating Node, 4 are in existence right now
The fully created list is:
4
3
2
1

Now removing elements:
Creating Node, 5 are in existence right now
Destroying Node, 4 are in existence right now
4
3
2
1


Program received signal SIGSEGV, Segmentation fault.
0x0000aaaac4c31304 in Node<int>::next (this=0x0) at main.cc:29
29 Node<T>* next () const { return next_; }

程序崩溃了,我们来看看发生了什么

检查崩溃

我们现在已经可以知道程序在main.cc的28行发生了崩溃,this指针指向了0,并且我们可以知道这一行执行的代码是什么。但是我们同样也想知道是谁条约9那个了这个额函数,并且我们希望能够测试函数中的变量的值。通过gdb交互,输入backtrace可以得到以下的输出:

1
2
3
4
5
6
(gdb) backtrace
#0 Node<int>::next (this=0x0) at main.cc:28
#1 0x2a16c in LinkedList<int>::remove (this=0x40160,
item_to_remove=@0xffbef014) at main.cc:77
#2 0x1ad10 in main (argc=1, argv=0xffbef0a4) at main.cc:111
(gdb)

我们知道了调用的函数以及局部变量,同时也知道了是哪个函数调用了这个函数并且它的调用参数是什么。例如,我们知道了是通过LinkedList<int>::remove()执行的调用,并且参数item_to_remove0xffbef014这个地址。通过item_to_remove的值可以帮助我们理解我们的bug,因此我们想要看看地址为0xffbef014的值。这个可以通过使用x命令完成,当运行这个命令时命令行给出了下面的结果:

1
2
(gdb) x 0xffbef014
0xffbef014: 0x00000001

因此当使用参数1运行LinkedList<int>::remove时程序会发生崩溃。我们现在将问题集中到了解决一个具体的参数值上。

条件断点

现在我们知道了何时在何处发生了段错误,我们想要观察程序在发生错误之前是否正确的执行。可行的一个方法是一步一步执行,直到我们达到了想要的地方。

如果你曾经使用过调试工具那么一定对断点不陌生。最基础的,断点是源代码中的一行,调试工具会在这行停止运行。在我们的例子中,我们想要去观察LinkedList<int>::remove()中的代码,所以我们可以设置一个在52行的断点。有的时候你可能不知道确切的行号,所以你也可以告诉调试工具你需要停在哪个函数中,例如:

1
2
3
(gdb) break LinkedList<int>::remove
Breakpoint 1 at 0x29fa0: file main.cc, line 52.
(gdb)

因此现在main.cc中的52行设置好了断点1(断点都有一个编号的原因在于我们稍后可以通过这个编号指向断点,例如想要删除它的时候)。当程序执行时,每当跑到52行的时候它会将控制权交还给调试工具。当这个方法会被调用很多次的时候断点可能就没有那么有效了,因此条件断电可以帮助我们解决问题。例如,我们知道程序会在LinkedList<int>::remove()以参数1调用的时候发狠恶搞崩溃,因此我们可能想告诉调试器仅仅在item_to_remove为1的时候才在52行停下来,这可以通过以下的命令实现

1
2
(gdb) condition 1 item_to_remove==1
(gdb)

步进

继续上述的例子,我们设置了一个条件断点,现在希望可以一次一步的执行这个函数尝试定位错误的源头,这个可以通过使用step命令实现。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
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y

Starting program: /home/cec/s/a/agg1/.www-docs/tutorial/main
Creating Node, 1 are in existence right now
Creating Node, 2 are in existence right now
Creating Node, 3 are in existence right now
Creating Node, 4 are in existence right now
The fully created list is:
4
3
2
1

Now removing elements:
Creating Node, 5 are in existence right now
Destroying Node, 4 are in existence right now
4
3
2
1


Breakpoint 1, LinkedList<int>::remove (this=0x40160,
item_to_remove=@0xffbef014) at main.cc:52
52 Node<T> *marker = head_;
(gdb) step
53 Node<T> *temp = 0; // temp points to one behind as we iterate
(gdb)
55 while (marker != 0) {
(gdb)
56 if (marker->value() == item_to_remove) {
(gdb)
Node<int>::value (this=0x401b0) at main.cc:30
30 const T& value () const { return value_; }
(gdb)
LinkedList<int>::remove (this=0x40160, item_to_remove=@0xffbef014)
at main.cc:75
75 marker = 0; // reset the marker
(gdb)
76 temp = marker;
(gdb)
77 marker = marker->next();
(gdb)
Node<int>::next (this=0x0) at main.cc:28
28 Node<T>* next () const { return next_; }
(gdb)

Program received signal SIGSEGV, Segmentation fault.
Node<int>::next (this=0x0) at main.cc:28
28 Node<T>* next () const { return next_; }
(gdb)

在输入run之后,gdb会询问我们是否想要重新运行程序,我们重新运行后,程序会在既定的地方停下来,接着我们输入step然后回车以步进程序,注意调试工具会进入调用的函数,如果你不希望这样,你可以使用next

程序中的错误已经非常明显了,75行marker被设置成0,但是77行maker的成员被访问,因为程序不能够访问内存地址为0的对象的成员,段错误就发生了,在这个例子中,简单的删除75行即可避免错误。

更多信息

这个文档仅仅包含了最简单和必要的命令以开始使用gdb,有关于gdb的更多信息可以查看man gdb的手册页面或者浏览一个非常长的说明。也可以通过在运行gdb的时候输入help来获取在线的帮助。