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