1. 为什么需要自定义GDB脚本
第一次用GDB调试段错误时,我盯着那一堆寄存器值和十六进制地址发呆了半小时。后来才知道,GDB其实支持用Python或内置命令语言编写自定义脚本,这就像给调试器装上了"外挂"。现在每次遇到复杂的内存错误,我都会先写个脚本把关键信息自动提取出来。
GDB脚本主要解决三类问题:
- 自动化重复操作(比如每次断点都要打印10个变量)
- 扩展调试功能(比如自动检测内存越界)
- 定制化显示(比如用更友好的格式显示结构体)
2. GDB脚本的两种实现方式
2.1 内置命令脚本
这种脚本文件后缀通常是.gdb,语法就是GDB命令的集合。比如下面这个检查链表完整性的脚本:
gdb复制# check_list.gdb
define check_list
set $node = (ListNode*)list_head
while $node != 0
printf "Node at %p, data=%d\n", $node, $node->data
if $node->next == $node
echo Error: self-loop detected!\n
end
set $node = $node->next
end
end
使用时直接source check_list.gdb,然后就能像普通GDB命令一样使用check_list。
经验:在脚本开头加上
set pagination off可以避免长输出被分页打断
2.2 Python脚本
从GDB 7.0开始支持Python API,功能更强大。比如下面这个自动检测内存泄漏的脚本:
python复制import gdb
class MemLeakChecker(gdb.Command):
def __init__(self):
super().__init__("check_leaks", gdb.COMMAND_USER)
def invoke(self, arg, from_tty):
# 获取malloc/free调用记录
mallocs = gdb.execute("info break malloc", to_string=True)
frees = gdb.execute("info break free", to_string=True)
# 对比未配对的malloc
# ...具体分析逻辑...
gdb.execute("set python print-stack full")
MemLeakChecker()
保存为leakcheck.py后,在GDB中用source -p leakcheck.py加载。
3. 实用脚本编写技巧
3.1 条件断点的进阶用法
常规条件断点:
gdb复制break test.c:100 if param==5
但条件复杂时更适合用脚本:
gdb复制break test.c:100
commands
silent # 不打印断点信息
if param % 2 == 0
printf "Even param: %d\n", param
continue # 自动继续执行
else
print param
end
end
3.2 自动化数据收集
调试网络协议时,我常用这个脚本记录所有收到的数据包:
python复制class PacketLogger:
def __init__(self):
self.packets = []
gdb.events.stop.connect(self.on_stop)
def on_stop(self, event):
if isinstance(event, gdb.BreakpointEvent):
buf = gdb.parse_and_eval("buffer")
size = int(gdb.parse_and_eval("size"))
self.packets.append(bytes(buf.string()[0:size]))
3.3 自定义pretty printer
让GDB漂亮地显示你的数据结构:
python复制class MyStructPrinter:
def __init__(self, val):
self.val = val
def to_string(self):
return f"MyStruct(x={self.val['x']}, y={self.val['y']})"
def lookup_type(val):
if str(val.type) == "MyStruct":
return MyStructPrinter(val)
return None
gdb.pretty_printers.append(lookup_type)
4. 调试实战:内存损坏排查
假设遇到随机内存损坏问题,可以编写这样的排查脚本:
gdb复制define watch_memory
# 在可疑内存范围设置观察点
watch *(char[0x1000]*)0x12340000
# 当内存被修改时
commands
backtrace
x/32xw 0x12340000
printf "Modified by: %s\n", $_siginfo._sifields._sigfault.si_addr
continue
end
end
配合Python脚本自动分析修改模式:
python复制class MemCorruptionAnalyzer:
def __init__(self):
self.patterns = defaultdict(int)
def record_access(self, addr):
# 记录访问模式
key = (addr & 0xFFFF0000) >> 16
self.patterns[key] += 1
if self.patterns[key] > 10:
print(f"Hot memory region: 0x{key:04x}0000")
5. 性能调优脚本示例
分析函数调用耗时:
gdb复制define profile
set $func = $arg0
break *$func
commands
silent
set $start = $_siginfo._sifields._sigfault.ticks
continue
end
break *($func + 1)
commands
silent
set $end = $_siginfo._sifields._sigfault.ticks
printf "%s: %d cycles\n", "$func", $end - $start
continue
end
end
6. 常见问题解决
问题1:脚本加载失败
- 检查GDB版本是否支持Python(
gdb --version) - Python脚本需要GDB编译时启用Python支持
问题2:脚本执行报错
- 用
set verbose on查看详细错误 - Python脚本可以加
try-catch打印完整堆栈
问题3:性能影响
- 复杂脚本可能显著降低调试速度
- 在不需要时用
disable临时关闭脚本断点
问题4:兼容性问题
- 不同架构的寄存器名称可能不同
- 使用
info registers确认当前环境
7. 我的调试工具箱
这几个脚本我几乎每次调试都会用到:
- crash_analyzer.gdb:自动分析段错误时的关键信息
gdb复制define analyze_crash
printf "Fault address: 0x%lx\n", $_siginfo._sifields._sigfault.si_addr
info registers
x/10i $pc
thread apply all bt
end
- watch_struct.py:监控结构体字段变化
python复制class StructWatcher:
def __init__(self, struct_name, field_name):
self.last_value = None
self.breakpoint = gdb.Breakpoint(f"*{struct_name}::{field_name}")
def on_break(self, event):
new_val = gdb.parse_and_eval(f"{struct_name}->{field_name}")
if self.last_value != new_val:
print(f"Value changed from {self.last_value} to {new_val}")
- call_tree.gdb:打印函数调用树
gdb复制define calltree
set logging file calltree.txt
set logging on
set max-depth 10
bt full
set logging off
end
调试复杂的内存问题时,我会先用crash_analyzer定位大致位置,然后用watch_struct监控关键数据结构,最后用call_tree理清调用关系。这套组合拳能解决90%的疑难杂症。
