gdb调试技巧-持续更新
gdb的一些调试技巧,包括打印格式、断点、gdb脚本编写和一些经验。
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 |
(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} |