GNU 调试器中的 Python 解释器¶
摘要¶
GNU 调试器 (GDB) 是开发者用于排查代码错误的最强大的调试工具。但是,对于初学者来说,GDB 难以学习,这就是许多程序员更喜欢插入 print
来检查运行时状态的原因。幸运的是,GDB 文本用户界面 (TUI) 为开发者提供了一种同时查看源代码和调试的方法。更令人兴奋的是,在 GDB 7 中,Python 解释器被内置到 GDB 中。此功能通过 Python 库提供了更简单的方法来自定义 GDB 打印机和命令。本文通过讨论示例,试图探索通过 Python 使用高级调试技术来开发 GDB 工具包。
引言¶
排查软件错误对于开发者来说是一个巨大的挑战。虽然 GDB 提供了许多“调试命令”来检查程序的运行时状态,但其不直观的用法阻碍了程序员使用它来解决问题。的确,掌握 GDB 是一个长期的过程。但是,快速入门并不复杂;你必须像尤达大师一样忘掉你所学过的知识。为了更好地理解如何在 GDB 中使用 Python,本文将重点讨论 GDB 中的 Python 解释器。
定义命令¶
GDB 支持使用 define
来自定义命令。它可以同时运行一批命令来进行排查。例如,开发者可以通过定义 sf
命令来显示当前帧信息。
# define in .gdbinit
define sf
where # find out where the program is
info args # show arguments
info locals # show local variables
end
但是,由于 API 有限,编写用户定义的命令可能不方便。幸运的是,通过与 GDB 中的 Python 解释器交互,开发者可以利用 Python 库轻松地建立他们的调试工具包。以下部分展示了如何使用 Python 简化调试过程。
内存转储¶
检查进程的内存信息是排查内存问题的有效方法。开发者可以通过 info proc mappings
和 dump memory
获取内存内容。为了简化这些步骤,定义一个自定义命令很有用。但是,使用纯 GDB 语法实现并不简单。即使 GDB 支持条件,处理输出也不直观。为了解决这个问题,使用 GDB 中的 Python API 会很有帮助,因为 Python 包含许多用于处理字符串的有用操作。
# mem.py
import gdb
import time
import re
class DumpMemory(gdb.Command):
"""Dump memory info into a file."""
def __init__(self):
super().__init__("dm", gdb.COMMAND_USER)
def get_addr(self, p, tty):
"""Get memory addresses."""
cmd = "info proc mappings"
out = gdb.execute(cmd, tty, True)
addrs = []
for l in out.split("\n"):
if re.match(f".*{p}*", l):
s, e, *_ = l.split()
addrs.append((s, e))
return addrs
def dump(self, addrs):
"""Dump memory result."""
if not addrs:
return
for s, e in addrs:
f = int(time.time() * 1000)
gdb.execute(f"dump memory {f}.bin {s} {e}")
def invoke(self, args, tty):
try:
# cat /proc/self/maps
addrs = self.get_addr(args, tty)
# dump memory
self.dump(addrs)
except Exception as e:
print("Usage: dm [pattern]")
DumpMemory()
运行 dm
命令将调用 DumpMemory.invoke
。通过在 .gdbinit 中加载或实现 Python 脚本,开发者可以使用用户定义的命令在程序运行时跟踪错误。例如,以下步骤展示了如何在 GDB 中调用 DumpMemory
。
(gdb) start
...
(gdb) source mem.py # source commands
(gdb) dm stack # dump stack to ${timestamp}.bin
(gdb) shell ls # ls current dir
1577283091687.bin a.cpp a.out mem.py
JSON 转储¶
当开发者在正在运行的程序中检查 JSON 字符串时,解析 JSON 会很有帮助。GDB 可以通过 gdb.parse_and_eval
解析 std::string
并将其作为 gdb.Value
返回。通过处理 gdb.Value
,开发者可以将 JSON 字符串传递到 Python json
API 中,并以漂亮的格式打印出来。
# dj.py
import gdb
import re
import json
class DumpJson(gdb.Command):
"""Dump std::string as a styled JSON."""
def __init__(self):
super().__init__("dj", gdb.COMMAND_USER)
def get_json(self, args):
"""Parse std::string to JSON string."""
ret = gdb.parse_and_eval(args)
typ = str(ret.type)
if re.match("^std::.*::string", typ):
return json.loads(str(ret))
return None
def invoke(self, args, tty):
try:
# string to json string
s = self.get_json(args)
# json string to object
o = json.loads(s)
print(json.dumps(o, indent=2))
except Exception as e:
print(f"Parse json error! {args}")
DumpJson()
命令 dj
在 GDB 中显示更易读的 JSON 格式。此命令有助于在 JSON 字符串较大的情况下提高视觉识别能力。此外,通过使用此命令,可以检测或监视 std::string
是否为 JSON。
(gdb) start
(gdb) list
1 #include <string>
2
3 int main(int argc, char *argv[])
4 {
5 std::string json = R"({"foo": "FOO","bar": "BAR"})";
6 return 0;
7 }
...
(gdb) ptype json
type = std::string
(gdb) p json
$1 = "{\"foo\": \"FOO\",\"bar\": \"BAR\"}"
(gdb) source dj.py
(gdb) dj json
{
"foo": "FOO",
"bar": "BAR"
}
语法高亮¶
语法高亮对于开发者跟踪源代码或排查问题很有用。通过使用 Pygments,可以轻松地为源代码应用颜色,而无需手动定义 ANSI 转义代码。以下示例展示了如何为 list
命令输出应用颜色。
import gdb
from pygments import highlight
from pygments.lexers import CLexer
from pygments.formatters import TerminalFormatter
class PrettyList(gdb.Command):
"""Print source code with color."""
def __init__(self):
super().__init__("pl", gdb.COMMAND_USER)
self.lex = CLexer()
self.fmt = TerminalFormatter()
def invoke(self, args, tty):
try:
out = gdb.execute(f"l {args}", tty, True)
print(highlight(out, self.lex, self.fmt))
except Exception as e:
print(e)
PrettyList()
跟踪点¶
尽管开发者可以插入 printf
、std::cout
或 syslog
来检查函数,但当项目庞大时,打印消息并不是一种有效的调试方法。开发者可能会浪费时间构建源代码,并且可能获得很少的信息。更糟糕的是,输出可能变得太多,难以检测问题。实际上,检查函数或变量不需要在代码中嵌入打印函数。通过使用 GDB API 编写 Python 脚本,开发者可以自定义观察点以在运行时动态地跟踪问题。例如,通过实现 gdb.Breakpoint
和 gdb.Command
,对于开发者获取必要信息(如参数、调用栈或内存使用情况)很有用。
# tp.py
import gdb
tp = {}
class Tracepoint(gdb.Breakpoint):
def __init__(self, *args):
super().__init__(*args)
self.silent = True
self.count = 0
def stop(self):
self.count += 1
frame = gdb.newest_frame()
block = frame.block()
sym_and_line = frame.find_sal()
framename = frame.name()
filename = sym_and_line.symtab.filename
line = sym_and_line.line
# show tracepoint info
print(f"{framename} @ {filename}:{line}")
# show args and vars
for s in block:
if not s.is_argument and not s.is_variable:
continue
typ = s.type
val = s.value(frame)
size = typ.sizeof
name = s.name
print(f"\t{name}({typ}: {val}) [{size}]")
# do not stop at tracepoint
return False
class SetTracepoint(gdb.Command):
def __init__(self):
super().__init__("tp", gdb.COMMAND_USER)
def invoke(self, args, tty):
try:
global tp
tp[args] = Tracepoint(args)
except Exception as e:
print(e)
def finish(event):
for t, p in tp.items():
c = p.count
print(f"Tracepoint '{t}' Count: {c}")
gdb.events.exited.connect(finish)
SetTracepoint()
与其在函数开头插入 std::cout
,不如在函数的入口点设置跟踪点,以便提供有用的信息来检查参数、变量和栈。例如,通过在 fib
上设置跟踪点,可以帮助检查内存使用情况、栈和调用次数。
int fib(int n)
{
if (n < 2) {
return 1;
}
return fib(n-1) + fib(n-2);
}
int main(int argc, char *argv[])
{
fib(3);
return 0;
}
以下输出显示了检查函数 fib
的结果。在这种情况下,跟踪点显示了开发者需要的所有信息,包括参数的值、递归流程和变量的大小。与 std::cout
相比,使用跟踪点可以获取更有用的信息。
(gdb) source tp.py
(gdb) tp main
Breakpoint 1 at 0x647: file a.cpp, line 12.
(gdb) tp fib
Breakpoint 2 at 0x606: file a.cpp, line 3.
(gdb) r
Starting program: /root/a.out
main @ a.cpp:12
argc(int: 1) [4]
argv(char **: 0x7fffffffe788) [8]
fib @ a.cpp:3
n(int: 3) [4]
fib @ a.cpp:3
n(int: 2) [4]
fib @ a.cpp:3
n(int: 1) [4]
fib @ a.cpp:3
n(int: 0) [4]
fib @ a.cpp:3
n(int: 1) [4]
[Inferior 1 (process 5409) exited normally]
Tracepoint 'main' Count: 1
Tracepoint 'fib' Count: 5
性能分析¶
无需插入时间戳,仍然可以通过跟踪点进行性能分析。通过在 gdb.Breakpoint
之后使用 gdb.FinishBreakpoint
,GDB 会在帧的返回地址处设置一个临时断点,以便开发者获取当前时间戳并计算时间差。请注意,通过 GDB 进行性能分析并不精确。其他工具(如 Linux perf 或 Valgrind)提供了更有用、更准确的信息来跟踪性能问题。
import gdb
import time
class EndPoint(gdb.FinishBreakpoint):
def __init__(self, breakpoint, *a, **kw):
super().__init__(*a, **kw)
self.silent = True
self.breakpoint = breakpoint
def stop(self):
# normal finish
end = time.time()
start, out = self.breakpoint.stack.pop()
diff = end - start
print(out.strip())
print(f"\tCost: {diff}")
return False
class StartPoint(gdb.Breakpoint):
def __init__(self, *a, **kw):
super().__init__(*a, **kw)
self.silent = True
self.stack = []
def stop(self):
start = time.time()
# start, end, diff
frame = gdb.newest_frame()
sym_and_line = frame.find_sal()
func = frame.function().name
filename = sym_and_line.symtab.filename
line = sym_and_line.line
block = frame.block()
args = []
for s in block:
if not s.is_argument:
continue
name = s.name
typ = s.type
val = s.value(frame)
args.append(f"{name}: {val} [{typ}]")
# format
out = ""
out += f"{func} @ {filename}:{line}\n"
for a in args:
out += f"\t{a}\n"
# append current status to a breakpoint stack
self.stack.append((start, out))
EndPoint(self, internal=True)
return False
class Profile(gdb.Command):
def __init__(self):
super().__init__("prof", gdb.COMMAND_USER)
def invoke(self, args, tty):
try:
StartPoint(args)
except Exception as e:
print(e)
Profile()
以下输出显示了在函数 fib
上设置跟踪点后的性能分析结果。可以方便地同时检查函数的性能和栈。
(gdb) source prof.py
(gdb) prof fib
Breakpoint 1 at 0x606: file a.cpp, line 3.
(gdb) r
Starting program: /root/a.out
fib(int) @ a.cpp:3
n: 1 [int]
Cost: 0.0007786750793457031
fib(int) @ a.cpp:3
n: 0 [int]
Cost: 0.002572298049926758
fib(int) @ a.cpp:3
n: 2 [int]
Cost: 0.008517265319824219
fib(int) @ a.cpp:3
n: 1 [int]
Cost: 0.0014069080352783203
fib(int) @ a.cpp:3
n: 3 [int]
Cost: 0.01870584487915039
漂亮打印¶
虽然 GDB 中的 set print pretty on
提供了一种更好的格式来检查变量,但开发者可能需要解析变量的值以提高可读性。以系统调用 stat
为例。虽然它提供了有用的信息来检查文件属性,但输出值(如权限)可能对于调试来说不可读。通过实现用户定义的漂亮打印,开发者可以解析 struct stat
并以易读的格式输出信息。
import gdb
import pwd
import grp
import stat
import time
from datetime import datetime
class StatPrint:
def __init__(self, val):
self.val = val
def get_filetype(self, st_mode):
if stat.S_ISDIR(st_mode):
return "directory"
if stat.S_ISCHR(st_mode):
return "character device"
if stat.S_ISBLK(st_mode):
return "block device"
if stat.S_ISREG:
return "regular file"
if stat.S_ISFIFO(st_mode):
return "FIFO"
if stat.S_ISLNK(st_mode):
return "symbolic link"
if stat.S_ISSOCK(st_mode):
return "socket"
return "unknown"
def get_access(self, st_mode):
out = "-"
info = ("r", "w", "x")
perm = [
(stat.S_IRUSR, stat.S_IWUSR, stat.S_IXUSR),
(stat.S_IRGRP, stat.S_IRWXG, stat.S_IXGRP),
(stat.S_IROTH, stat.S_IWOTH, stat.S_IXOTH),
]
for pm in perm:
for c, p in zip(pm, info):
out += p if st_mode & c else "-"
return out
def get_time(self, st_time):
tv_sec = int(st_time["tv_sec"])
return datetime.fromtimestamp(tv_sec).isoformat()
def to_string(self):
st = self.val
st_ino = int(st["st_ino"])
st_mode = int(st["st_mode"])
st_uid = int(st["st_uid"])
st_gid = int(st["st_gid"])
st_size = int(st["st_size"])
st_blksize = int(st["st_blksize"])
st_blocks = int(st["st_blocks"])
st_atim = st["st_atim"]
st_mtim = st["st_mtim"]
st_ctim = st["st_ctim"]
out = "{\n"
out += f"Size: {st_size}\n"
out += f"Blocks: {st_blocks}\n"
out += f"IO Block: {st_blksize}\n"
out += f"Inode: {st_ino}\n"
out += f"Access: {self.get_access(st_mode)}\n"
out += f"File Type: {self.get_filetype(st_mode)}\n"
out += f"Uid: ({st_uid}/{pwd.getpwuid(st_uid).pw_name})\n"
out += f"Gid: ({st_gid}/{grp.getgrgid(st_gid).gr_name})\n"
out += f"Access: {self.get_time(st_atim)}\n"
out += f"Modify: {self.get_time(st_mtim)}\n"
out += f"Change: {self.get_time(st_ctim)}\n"
out += "}"
return out
p = gdb.printing.RegexpCollectionPrettyPrinter("sp")
p.add_printer("stat", "^stat$", StatPrint)
o = gdb.current_objfile()
gdb.printing.register_pretty_printer(o, p)
通过加载前面的 Python 脚本,PrettyPrinter
可以识别 struct stat
并输出易读的格式,以便开发者检查文件属性。无需插入函数来解析和打印 struct stat
,这是从 Python API 获取更好输出的一种更便捷的方式。
(gdb) list 15
10 struct stat st;
11
12 if ((rc = stat("./a.cpp", &st)) < 0) {
13 perror("stat failed.");
14 goto end;
15 }
16
17 rc = 0;
18 end:
19 return rc;
(gdb) source st.py
(gdb) b 17
Breakpoint 1 at 0x762: file a.cpp, line 17.
(gdb) r
Starting program: /root/a.out
Breakpoint 1, main (argc=1, argv=0x7fffffffe788) at a.cpp:17
17 rc = 0;
(gdb) p st
$1 = {
Size: 298
Blocks: 8
IO Block: 4096
Inode: 1322071
Access: -rw-rw-r--
File Type: regular file
Uid: (0/root)
Gid: (0/root)
Access: 2019-12-28T15:53:17
Modify: 2019-12-28T15:53:01
Change: 2019-12-28T15:53:01
}
请注意,开发者可以通过 disable
命令禁用用户定义的漂亮打印。例如,前面的 Python 脚本在全局漂亮打印机下注册了一个漂亮打印机。通过调用 disable pretty-print
,打印机 sp
将被禁用。
(gdb) disable pretty-print global sp
1 printer disabled
1 of 2 printers enabled
(gdb) i pretty-print
global pretty-printers:
builtin
mpx_bound128
sp [disabled]
stat
此外,如果不再需要,开发者可以在当前 GDB 调试会话中排除打印机。以下代码片段展示了如何通过 gdb.pretty_printers.remove
删除 sp
打印机。
(gdb) python
>import gdb
>for p in gdb.pretty_printers:
> if p.name == "sp":
> gdb.pretty_printers.remove(p)
>end
(gdb) i pretty-print
global pretty-printers:
builtin
mpx_bound128
结论¶
将 Python 解释器集成到 GDB 中提供了许多灵活的方法来排查问题。虽然许多集成开发环境 (IDE) 可能会嵌入 GDB 以进行可视化调试,但 GDB 允许开发者在运行时实现自己的命令并解析变量的输出。通过使用调试脚本,开发者可以在不修改代码的情况下监视和记录必要的信息。老实说,插入或启用调试代码块可能会改变程序的行为,开发者应该改掉这个坏习惯。此外,当问题重现时,GDB 可以附加该进程并检查其状态,而无需停止它。显然,如果出现具有挑战性的问题,则无法避免通过 GDB 进行调试。感谢将 Python 集成到 GDB 中,开发调试脚本变得更容易,这使得开发者能够以多种方式建立自己的调试方法。