skaiuijing

前言

gdb是一个非常强大的调试工具,不过很多人都觉得它不是很好用,大家都更偏向图形界面的调试。其实gdb只是上手难受,但是习惯后你就会被它强大灵活的调试功能惊讶到。

gdb与vim一样,让人觉得非常简陋,不方便。当然,最劝退人的一点可能就是命令式的调试了,而且实际调试情况千变万化,一行行输出代码也不是很方便。像很多ide,就会自动”尾随“代码并显示对应变量值,这显得gdb不够”智能“。

其实,gdb也是可以做到很“智能”的。

为了应对复杂的调试情况,gdb社区在经过深思熟虑后,从gdb7版本开始,引入了脚本语言扩展,也就是python API。

python API

现在笔者面对一个问题:在使用gdb调试时,笔者经常需要查看某些变量的值,但是每一次都要输入大量的命令,有没有办法解决?

为了方便,我们可能会把频繁使用的命令放在旁边的一个文件夹里,然后一行行复制粘贴,但是,这样效率实在是太低了。

如果我们可以单独创建一系列命令,可以一次性执行这些命令并显示,那么我们的效率会大大提高。接下来,笔者将展示如何使用python API逐步解决常见的一些问题。

从打印开始

对于某些打印情况,gdb就非常不方便,例如:

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

int main() {
std::vector<int> vec = {7, 5, 16, 8};
std::cout << "Size: " << vec.size() << std::endl;
return 0;
}

如果直接使用gdb:
(gdb) print vec
那么结果可能会是:
$1 = {
_M_impl = {
_M_start = 0x55555576e010,
_M_finish = 0x55555576e018,
_M_end_of_storage = 0x55555576e018
}
}

这是为什么呢?

原因是gdb无法直接解读和显示 std::vector 类型的数据结构,GDB 默认的输出格式对于 std::vector 这样的复杂 C++ 容器类型并不友好。

解决办法:我们可以下载 GDB Pretty Printers,比如:git clone https://github.com/gcc-mirror/gcc.git

然后在gdb命令行里面导入路径:(gdb) source /path/to/gcc/libstdc++-v3/python/libstdcxx/v6/printers.py

这样gdb就可以正确识别std::vector容器类型了。

我们可以通过source命令导入.py文件,那么,这也意味着我们可以导入我们自己写好的脚本文件。

打印结构体变量

让我们先看看如下代码:

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
struct DAT{
list_node node;
};

struct list_node{
list_node *prev_node;
int value;
};

list_node node1;
list_node node2;

void fun(DAT *DAT1)
{
DAT1->node = node1;
node1.value = 403;
node1.prev_node = node2;
node2.value = 404;
}

int main( ){
DAT *DAT1 = malloc(sizeof(struct DAT));

fun(&node2);
while(1){

}
}

假设现在你并不知道node2的value的值是404,现在程序执行到了while(1),要打印这个值,gdb使用如下:

(gdb) print DAT1->node.prev_node.value

如果你又要打印node1的value的值,gdb使用如下:

(gdb) print DAT1->node.value

咋一看好像觉得没什么问题,但是有没有觉得,我们做了太多无用的工作?如果指针的嵌套再深一点,那么我们又该如何打印?如果这两个value的值时时刻刻都在改变而且需要被观察,那么我们需要不断重复。

复制粘贴确实不复杂,但问题是如果需要观察数十个value呢?一行行输入命令太枯燥了,而且,你能分辨到底是哪个value吗?

我们需要一次性完成全部打印的方式,而且还要显示嵌套层次。

使用python API进行打印

先看看如何使用Python API,现在,让我们编写这样一个py文件:

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
import gdb

class PrintDATStructure(gdb.Command):

def __init__(self):
super(PrintDATStructure, self).__init__("print-dat", gdb.COMMAND_USER)

def invoke(self, arg, from_tty):
try:
dat_ptr = gdb.parse_and_eval(arg) # 解析指针

# 脚本有问题时打印
if not dat_ptr:
print("DAT pointer is NULL.")
return

dat = dat_ptr.dereference() # dereference()是解引用
print("DAT Structure:")
print(" DAT address: {}".format(dat_ptr)) # 字符串格式化并打印

node = dat["node"]
if node:
print(" Node Address: {}".format(node))
node = node.dereference()
print(" Node Value: {}".format(node["value"])) # 获取value

prev_node = node["prev_node"]
if prev_node:
prev_node = prev_node.dereference()
print(" Previous Node:")
print(" Address: {}".format(node["prev_node"]))
# 也可以通过指针访问值,写法如下:self.val["node"]["prev_node"]["value"]
print(" Value: {}".format(prev_node["value"]))

else:
print(" Previous Node: None")
else:
print(" Node: None")

except gdb.error as e:
print("Error: {}".format(e))


PrintDATStructure()

在gdb下使用(gdb) source /path/to/xxx.py命令导入脚本文件,然后让gdb运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(gdb) source /pathto/xxx.py
(gdb) b main
(gdb) r
(gdb) 使用n,c或s等命令运行到想调试的那一行
想调试时,使用print-dat DAT1命令
打印如下:
(gdb) print-dat DAT1
DAT Structure:
DAT address: 0x602010
Node Address: 0x601050 <node1>
Node Value: 403
Previous Node:
Address: 0x601060 <node2>
Value: 404

这样,层次结构就很清晰了。

python脚本写法

基本写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import gdb

class 类(gdb.Command):
# 注册
def __init__(self):
super(类, self).__init__("自定义xxx命令", gdb.命令类型)
self.commands = []

# invoke 方法定义了当用户在 GDB 中执行 print-dat 命令时会发生的操作
def invoke(self, arg, from_tty):
code.....

# 添加实例化的类执行方法,例如
类()
# 也可以定义方法,可以在gdb中手动执行
自定义方法():
code....

有多种命令类型:

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
gdb.COMMAND_NONE:

无类型,不属于特定类别的命令。

gdb.COMMAND_RUNNING:

与程序运行相关的命令,例如启动、停止和继续程序。

gdb.COMMAND_DATA:

与数据检查和操作相关的命令,例如打印变量值或修改内存。

gdb.COMMAND_STACK:

与堆栈检查相关的命令,例如查看调用堆栈和堆栈帧。

gdb.COMMAND_FILES:

与文件操作相关的命令,例如加载和卸载目标文件。

gdb.COMMAND_SUPPORT:

支持命令,例如显示帮助信息和调整 GDB 设置。

gdb.COMMAND_USER:

用户定义的命令,允许用户创建和使用自定义命令。

gdb.COMMAND_OBSCURE:

稀有或特殊用途的命令。

使用脚本记录命令

了解gdb的python脚本的基本写法后,让我们尝试解决实际问题,例如:我们可能并不想一次性把命令写死,而是想选择性的添加执行命令,那么,脚本如下:

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

import gdb

class CommandRecorder(gdb.Command):
# 注册命令
def __init__(self):
super(CommandRecorder, self).__init__("record_command", gdb.COMMAND_USER)
self.commands = []
# record_command的执行方法,也就是添加命令
def invoke(self, arg, from_tty):
if arg:
self.commands.append(arg)
print("Command recorded: {}".format(arg))
else:
print("No command to record. Usage: record_command <command>")

# 手动执行方法,遍历数组全部命令并执行
def execute_commands(self):
for cmd in self.commands:
print("Executing: {}".format(cmd))
gdb.execute(cmd)

# 实例化
recorder = CommandRecorder()

def execute_recorded():
recorder.execute_commands()


在gdb中,使用如下:

1
2
3
4
5
(gdb)record_command print xxx
# record_command可以自己定义,不必写得这么长,敲得难受
(gdb)record_command print xxxxx
# 执行记录过的命令
(gdb)python execute_recorded()

总结

介绍了gdb的python脚本扩展,以及引入脚本的便利性。通过简单的脚本文件示例,介绍python API的编写模板,然后根据模板编写特定需求的脚本文件。

本文参考:The GDB Python API | Red Hat Developer