LoopJump's Blog

gdb调试技巧(live doc)

2017-02-10

Gdb是linux下C/C++代码调试神器,本文综合了一些比较好的国内外博客,加上我自己的一些使用经验,希望能对读者有用。

使用gdb前,代码要用gcc -g选项编译以带上符号表。

调试C++程序的基本环境设置

1
2
3
4
5
6
7
set print pretty on
set print object on
set print static-members off
set print vtbl on
set print demangle on
set demangle-style gnu-v3
set print sevenbit-strings off

在gdb里面临时执行一些shell命令,可以用gdb的shell命令

1
2
3
4
5
gdb> shell pwd
或者先进入shell
gdb> shell
$ pwd
$ exit // 退出shell,回到gdb

查看当前堆栈

1
2
3
4
5
gdb> bt // backtrace
也可以先切换到其他线程,看其他线程的当前堆栈
gdb> info thread // 查看所有线程
gdb> thread 12 // 切换到12号线程
gdb> bt

断点的几个技巧

条件断点:

1
2
3
4
5
gdb> break foo if x>0
commands
printf "x is %dn",x
continue
end

在x>0时,停止并执行一系列commands。

打印变量或内存print命令

1
2
3
gdb> p *arr@23  // 将arr视为长度为23的数组打印出来
gdb> p/x my_var // 以16进制打印my_var,除x外,还有c/d等
gdb> x mem_addr // 查看mem_addr地址对应的内存内容

打印格式的调整:

1
2
3
gdb> set print pretty // 打印struct/class时,gdb会带缩进
gdb> set print static-member off // 打印对象时,不打印对象的static变量
gdb> set print element 23 // 打印数组时,最多打印几个对象,0为不限

打印函数局部static __thread变量

1
2
3
4
5
6
7
C/C++ code
void foo() {
  static __thread my_var = 0;
}

gdb> thread 12
gdb> p ‘foo::my_var’

不过这个命令在某些环境下不能生效,不知道为什么。

打印真实类型

1
2
set print object on
print base_class_ptr

disassemble

除了查看汇编之外,这个命令在某些情况下还可以用来查看某个地址上存放的是什么对象。因为C++对象布局的原因,假设可以确认某个地址(例如内存分配器的一块内存)存放了一个C++对象(假设有虚表指针),那么直接disassemble这个对应到虚表指针的地址,可以看到一个mangled符号名字,基本就可以确定上面存放的是什么对象了。

我之前在排查一个内存泄漏的问题时用过这个技巧,内存分配器实现时可以检测是否有泄漏,并且能够知道哪些内存块被泄漏了,但是因为只能在分配器对象析构时才能确定内存泄漏,所以并不方便知道是哪个使用者泄漏了(把使用者id传进来太麻烦)。通过这种办法能够先确定是哪个使用者泄漏了,进而缩小问题范围。

dump memory把一段内存内容转储到文件中

我之前在排查某个问题时,需要在coredump中将一个哈希表的内容打印出来。假设哈希表实现为大数组(数组内的元素只存放指针),拉链解决冲突。那么,就可以用gdb脚本(我们后面会给出一个详细的例子)将所有的元素打印出来。不过这里有个问题:gdb是解释执行的,非常慢,导致打印一个大的哈希表耗时可能达到几天之久。

假设哈希表数组大小为一千万,但是实际有效元素只有几万,并且数组中无效元素都是NULL指针。那么就可以先将数组dump memory到文件,用shell脚本先过滤掉NULL元素,只遍历剩下的有效指针,在我遇到的这个场景下,遍历耗时从7天降到了10分钟。

不过还遇到一个麻烦,gdb不支持将shell命令结果存入gdb变量中,所以只能拼凑一个临时的gdb脚本,里面有shell脚本生成的诸如set my_gdb_var = ‘pwd’等内容,然后在gdb环境下source这个临时脚本,曲线地将内容加载到gdb变量中。

gdb脚本示例

gdb脚本是一个非常好用的利器,比如在coredump文件中,你可能想知道某个链表/哈希表等数据结构里面都有什么元素,可以写脚本遍历打印出来。这里给一个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
using namespace std;

struct Node
{
int val_;
Node *next_;
Node(int v) : val_(v), next_(nullptr) {}
};

int main()
{
Node *head = new Node(0);
Node *curr = head;
for (int i = 1; i < 10; i++) {
curr->next_ = new Node(i);
curr = curr->next_;
}
abort(); // abort for gdb core dump file.
return 0;
}

gdb脚本代码print_my_list_script.gdb

1
2
3
4
5
6
7
8
define print_my_list
set $curr = $arg0
while $curr
printf "%d ", $curr->val_
set $curr = $curr->next_
end
printf "\n"
end

执行脚本

1
2
3
4
(gdb)source print_my_list_script.gdb
(gdb)print_my_list head
0 1 2 3 4 5 6 7 8 9

gdb打印STL库

大部分内置STL库对应的gdb脚本,都可以在这里下载 http://www.yolinux.com/TUTORIALS/src/dbinit_stl_views-1.03.txt

不过这里没有unordered_map,这里我写了一个例子,读者如果需要,可以自己写一个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <unordered_map>
using namespace std;
struct Student
{
int val_1_;
int val_2_;
};
int main()
{
unordered_map<int, Student> m;
for (int i = 0; i < 10; i++) {
Student s;
s.val_1_ = i;
s.val_2_ = i;
m.insert({i, s});
}
abort(); // abort to get a coredump file
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
define dump_map_new
set $map = $arg0
set $head_node = $map._M_h._M_before_begin._M_nxt
set $curr_node = $head_node
while $curr_node
set $iter_node = (std::__detail::_Hash_node<std::pair<int const, Student>, false> *)($curr_node)
set $addr = &($iter_node->_M_storage._M_storage)
set $iter_pair = (std::pair<int const, Student> *)($addr)
printf "key=%d, value={%d, %d}\n", $iter_pair->first, $iter_pair->second.val_1_, $iter_pair->second.val_2_
set $curr_node = $curr_node->_M_nxt
end

注意:$curr_node的类型,可以在gdb下print 这个map看下(可能需要先disable pretty-printer)。

1
2
3
4
5
6
7
8
9
10
11
12
(gdb) source dump_map_new.gdb
(gdb) dump_map_new m
key=9, value={9, 9}
key=8, value={8, 8}
key=7, value={7, 7}
key=6, value={6, 6}
key=5, value={5, 5}
key=4, value={4, 4}
key=0, value={0, 0}
key=1, value={1, 1}
key=2, value={2, 2}
key=3, value={3, 3}

扫描二维码,分享此文章