美化显示LLDB调试的数据结构
前面的博文 美化显示GDB调试的数据结构介绍了如何美化显示GDB中调试的数据结构,本文将还是以mupdf库为例介绍如何美化显示LLDB中调试的数据结构。
先看一下美化后的效果:
一、加载自定义脚本
与GDB类似,需要添加一个~/.lldbinit
文件,可以在其中添加一些LLDB的命令,笔者的内容为:
1setting set target.x86-disassembly-flavor intel
2command script import C:/Users/admin/lldb/mupdf_printer.py
第一行设置LLDB反汇编格式为intel,第二行则是执行我们自定义的美化显示脚本mupdf_printer.py
,如果是在VSCode中使用codelldb
插件,则可以不用写第二行,而在.vscode/launch.json
中设置调试配置的initCommands
来加载脚本:
1 {
2 "type": "lldb",
3 "request": "launch",
4 "name": "(lldb) 启动",
5 "program": "${workspaceFolder}/build/t.exe",
6 "args": [],
7 "cwd": "${workspaceFolder}",
8 "initCommands": [
9 "command script import ${workspaceFolder}/lldbscripts/mupdf_printer.py",
10 ]
11 },
二、写测试代码
与博文 美化显示GDB调试的数据结构中的测试代码一样。
三、写LLDB的python脚本
1. 向LLDB注册pdf_obj类型
LLDB必须使用__lldb_init_module(debugger : lldb.SBDebugger, internal_dict : dict)
签名的函数来处理类型的注册。
注册分为概要
(summary
类型)和混合器
(synthetic
)类型。概要
类型用于不需要展开即可显示的信息;而混合器
类型用于展开时显示的信息,比如数组、字典等等。
1def __lldb_init_module(debugger : lldb.SBDebugger, internal_dict : dict):
2 debugger.HandleCommand(r'type summary add -x "^pdf_obj.*\*" --python-function mupdf_printer.PDFObjAPISummary')
3 debugger.HandleCommand(r'type synthetic add -x "^pdf_obj.*\*" --python-class mupdf_printer.PDFObjAPIPrinter')
4 print("MuPDF pdf_obj summary and synthetic provider (via API) loaded.")
2.写美化输出代码
根据LLDB的Python协议,概要
类型的处理只能是一个函数,而混合器
类型的处理是一个类。
所以pdf_obj的概要
类型处理函数如下:
1def PDFObjAPISummary(val : lldb.SBValue, internal_dict : dict):
2 try:
3 addr = val.GetValueAsAddress()
4 if not addr:
5 return "<null>"
6
7 ref = ""
8 if call_pdf_api("pdf_is_indirect", val):
9 num = call_pdf_api(f"pdf_to_num", val)
10 #gen = call_pdf_api(f"pdf_to_gen", valobj)
11 ref = f"<Ref {num}> => "
12
13 kind = detect_pdf_obj_kind(val)
14 if kind == "null":
15 return f"{ref}<null>"
16 elif kind == "int":
17 return f"{ref}{call_pdf_api("pdf_to_int", val)}"
18 elif kind == "real":
19 return f"{ref}{call_pdf_api(f"pdf_to_real", val, float)}"
20 elif kind == "bool":
21 v = call_pdf_api(f"pdf_to_bool", val)
22 return f"{ref}{'true' if v else 'false'}"
23 elif kind == "string":
24 return f'{ref}{call_pdf_api("pdf_to_text_string", val, str)}'
25 elif kind == "name":
26 v = call_pdf_api("pdf_to_name", val, str)
27 return f'{ref}/{v.strip('"')}'
28 elif kind == "array":
29 length = call_pdf_api("pdf_array_len", val)
30 return f"{ref}[size]={length}"
31 elif kind == "dict":
32 length = call_pdf_api(f"pdf_dict_len", val)
33 return f"{ref}[pairs]={length}"
34 return f"{ref}{addr}"
35
36 except Exception as e:
37 return f"<error: {e}>"
混合器
类型的处理函数如下:
1class PDFObjAPIPrinter:
2 def __init__(self, val : lldb.SBValue, internal_dict : dict):
3 self.val = val
4 self.kind = detect_pdf_obj_kind(val)
5 self.size = self.num_children()
6
7 def has_children(self):
8 # 只在array/dict类型时允许展开
9 return self.kind in ["array", "dict"]
10
11 def num_children(self):
12 if self.kind == "array":
13 length = call_pdf_api(f"pdf_array_len", self.val)
14 return int(length) if length else 0
15 elif self.kind == "dict":
16 length = call_pdf_api(f"pdf_dict_len", self.val)
17 return int(length) if length else 0
18 return 0
19
20 def get_child_at_index(self, index):
21 try:
22 if index < 0 or index >= self.size:
23 return None
24
25 if self.kind == "array":
26 v = call_pdf_api_1(f"pdf_array_get", self.val, index, object)
27 # 根据索引取到pdf_obj对象了,需要获取其地址
28 addr = v.GetValueAsAddress()
29 # 再构造一个表达式,将这个地址强制转为pdf_obj的指针
30 expr = f"(pdf_obj *){addr}"
31 # 最后根据这个表达式创建一个新的值,LLDB会自动重新根据规则显示这个值
32 return self.val.CreateValueFromExpression(f"[{index}]", expr)
33
34 elif self.kind == "dict":
35 key = call_pdf_api_1("pdf_dict_get_key", self.val, index, object)
36 val = call_pdf_api_1("pdf_dict_get_val", self.val, index, object)
37 # 将pdf_obj中字典的Key一定是一个name,取name的值
38 key_str = call_pdf_api("pdf_to_name", key, str).strip('"')
39 # 将字典的value取地址,构造一个新的表达式
40 addr = val.GetValueAsAddress()
41 expr = f"(pdf_obj *){addr}"
42 # 最后根据这个表达式创建一个新的值,LLDB会自动重新根据规则显示这个值
43 return self.val.CreateValueFromExpression(f"[/{key_str}]", expr)
44 except Exception as e:
45 print(f"Error in get_child_at_index: {e}")
46 return None
代码中同样需要一些辅助函数,就不再一一列举了,直接给出完整代码。
3. 完整代码
mupdf_printer.py
:
1import lldb
2
3pdf_ctx : int | None = None
4
5def get_mupdf_version_from_symbol(target : lldb.SBTarget):
6 try:
7 version = target.EvaluateExpression('(const char*)mupdf_version')
8 return version.GetSummary()
9 except Exception as e:
10 return f"<symbol not found: {e}>"
11
12def call_mupdf_api(func_name : str, val : lldb.SBValue, retType : type, *args):
13 try:
14 target = val.GetTarget()
15 addr = val.GetValueAsAddress() # 使用GetValueAsAddress获取指针值
16 cast = {
17 int: "(int)",
18 float: "(float)",
19 str: "(const char*)", # for functions returning const char*
20 object: "(pdf_obj *)", # for pdf_obj pointers
21 }.get(retType, "(void)")
22 global pdf_ctx
23 if pdf_ctx is None:
24 ver = get_mupdf_version_from_symbol(target)
25 print(f"[LLDB] MuPDF version: {ver}")
26 ctx : lldb.SBValue = target.EvaluateExpression(f"(fz_context*)fz_new_context_imp(0,0,0,{ver})")
27 pdf_ctx = ctx.GetValueAsAddress() # 保存为整数地址
28 if args:
29 args_str = ', '.join([str(arg) for arg in args])
30 expr = f"{cast}{func_name}((fz_context*){pdf_ctx},{addr}, {args_str})"
31 else:
32 expr = f"{cast}{func_name}((fz_context*){pdf_ctx},(pdf_obj*){addr})"
33 result : lldb.SBValue = target.EvaluateExpression(expr)
34 if retType == int:
35 return int(result.GetValue())
36 elif retType == float:
37 return float(result.GetValue())
38 elif retType == str:
39 return result.GetSummary()
40 else:
41 return result
42 except Exception as e:
43 print(f"<error calling {func_name}: {e}>")
44 return f"<error calling {func_name}: {e}>"
45
46def call_pdf_api(func_name : str, val : lldb.SBValue, rettype=int):
47 return call_mupdf_api(func_name, val, rettype)
48
49def call_pdf_api_1(func_name : str, val : lldb.SBValue, arg, rettype=int):
50 return call_mupdf_api(func_name, val, rettype, arg)
51
52# 检测除间隔引用外的数据类型
53def detect_pdf_obj_kind(val : lldb.SBValue):
54 try:
55 if call_pdf_api("pdf_is_null", val):
56 return "null"
57 elif call_pdf_api("pdf_is_int", val):
58 return "int"
59 elif call_pdf_api("pdf_is_real", val):
60 return "real"
61 elif call_pdf_api("pdf_is_bool", val):
62 return "bool"
63 elif call_pdf_api("pdf_is_string", val):
64 return "string"
65 elif call_pdf_api("pdf_is_name", val):
66 return "name"
67 elif call_pdf_api("pdf_is_array", val):
68 return "array"
69 elif call_pdf_api("pdf_is_dict", val):
70 return "dict"
71 return "unknown"
72 except Exception as e:
73 print(f"<error detecting pdf_obj kind: {e}>")
74 return "<error>"
75
76def PDFObjAPISummary(val : lldb.SBValue, internal_dict : dict):
77 try:
78 addr = val.GetValueAsAddress()
79 if not addr:
80 return "<null>"
81
82 ref = ""
83 if call_pdf_api("pdf_is_indirect", val):
84 num = call_pdf_api(f"pdf_to_num", val)
85 #gen = call_pdf_api(f"pdf_to_gen", valobj)
86 ref = f"<Ref {num}> => "
87
88 kind = detect_pdf_obj_kind(val)
89 if kind == "null":
90 return f"{ref}<null>"
91 elif kind == "int":
92 return f"{ref}{call_pdf_api("pdf_to_int", val)}"
93 elif kind == "real":
94 return f"{ref}{call_pdf_api(f"pdf_to_real", val, float)}"
95 elif kind == "bool":
96 v = call_pdf_api(f"pdf_to_bool", val)
97 return f"{ref}{'true' if v else 'false'}"
98 elif kind == "string":
99 return f'{ref}{call_pdf_api("pdf_to_text_string", val, str)}'
100 elif kind == "name":
101 v = call_pdf_api("pdf_to_name", val, str)
102 return f'{ref}/{v.strip('"')}'
103 elif kind == "array":
104 length = call_pdf_api("pdf_array_len", val)
105 return f"{ref}[size]={length}"
106 elif kind == "dict":
107 length = call_pdf_api(f"pdf_dict_len", val)
108 return f"{ref}[pairs]={length}"
109 return f"{ref}{addr}"
110
111 except Exception as e:
112 return f"<error: {e}>"
113class PDFObjAPIPrinter:
114 def __init__(self, val : lldb.SBValue, internal_dict : dict):
115 self.val = val
116 self.kind = detect_pdf_obj_kind(val)
117 self.size = self.num_children()
118
119 def has_children(self):
120 # 只在array/dict类型时允许展开
121 return self.kind in ["array", "dict"]
122
123 def num_children(self):
124 if self.kind == "array":
125 length = call_pdf_api(f"pdf_array_len", self.val)
126 return int(length) if length else 0
127 elif self.kind == "dict":
128 length = call_pdf_api(f"pdf_dict_len", self.val)
129 return int(length) if length else 0
130 return 0
131
132 def get_child_at_index(self, index):
133 try:
134 if index < 0 or index >= self.size:
135 return None
136
137 if self.kind == "array":
138 v = call_pdf_api_1(f"pdf_array_get", self.val, index, object)
139 # 根据索引取到pdf_obj对象了,需要获取其地址
140 addr = v.GetValueAsAddress()
141 # 再构造一个表达式,将这个地址强制转为pdf_obj的指针
142 expr = f"(pdf_obj *){addr}"
143 # 最后根据这个表达式创建一个新的值,LLDB会自动重新根据规则显示这个值
144 return self.val.CreateValueFromExpression(f"[{index}]", expr)
145
146 elif self.kind == "dict":
147 key = call_pdf_api_1("pdf_dict_get_key", self.val, index, object)
148 val = call_pdf_api_1("pdf_dict_get_val", self.val, index, object)
149 # 将pdf_obj中字典的Key一定是一个name,取name的值
150 key_str = call_pdf_api("pdf_to_name", key, str).strip('"')
151 # 将字典的value取地址,构造一个新的表达式
152 addr = val.GetValueAsAddress()
153 expr = f"(pdf_obj *){addr}"
154 # 最后根据这个表达式创建一个新的值,LLDB会自动重新根据规则显示这个值
155 return self.val.CreateValueFromExpression(f"[/{key_str}]", expr)
156 except Exception as e:
157 print(f"Error in get_child_at_index: {e}")
158 return None
159
160def __lldb_init_module(debugger : lldb.SBDebugger, internal_dict : dict):
161 debugger.HandleCommand(r'type summary add -x "^pdf_obj.*\*" --python-function mupdf_printer.PDFObjAPISummary')
162 debugger.HandleCommand(r'type synthetic add -x "^pdf_obj.*\*" --python-class mupdf_printer.PDFObjAPIPrinter')
163 print("MuPDF pdf_obj summary and synthetic provider (via API) loaded.")
四、调试LLDB的python代码
gdb中的python美化显示脚本不能直接使用普通的python调试器进行调试,那是因为GDB中不是使用的常规Python。常规Python不能import gdb包:
而常规Python是可以import lldb包的:
所以lldb中的python脚本是可以使用常规的python调试器调试的,下面就介绍一下在VSCode中如何调试它。
首先,需要在.vscode/launch.json
中添加lldb
调试配置和python
调试配置,需要注意的是python的调试配置是附加到进程的类型:
1{
2 "version": "0.2.0",
3 "configurations": [
4 {
5 "name": "Python 调试程序: 使用进程 ID 附加",
6 "type": "debugpy",
7 "request": "attach",
8 "processId": "${command:pickProcess}"
9 },
10 {
11 "type": "lldb",
12 "request": "launch",
13 "name": "(lldb) 启动",
14 "program": "${workspaceFolder}/build/t.exe",
15 "args": [],
16 "cwd": "${workspaceFolder}",
17 "initCommands": [
18 "command script import ${workspaceFolder}/lldbscripts/mupdf_printer.py",
19 ]
20 }
21 ]
22}
先在C/C++代码中打好断点:
然后启动LLDB调试器,此时会在断点处中断。
再启动Python调试,附加到
codelldb
进程:
附加成功后,在LLDB的python脚本中打的断点就生效了:
此时在LLDB调试器中展开还未获取到值的变量,比如数组、字典需要展开的但从未展开过的变量(展开后有缓存,再次展开不会再触发Python脚本,如果要想再次触发,可以如后面介绍的方法,在调试控制台直接使用p
命令显示)
将调试器切换到Python调试器:
就可以看到触发了Python中的断点了,可以调试Python代码了:
由于调试了Python代码,LLDB可能会获取超时,出现后面的值显示不了的情况:
可以在VSCode的调试控制台中直接输入LLDB命令:p ar
,刷新一下就显示出来了,此时会再次触发Python脚本。
五、总结
细心的读者可能会发现,所有的pdf_obj
变量前面都有一个展开箭头,不管是基本的数据类型,还是数组与字典,展开后都有一个[raw]
项,这是因为pdf_obj
注册了混合器。
如果不注册混合器就不会有展开箭头,但数组与字典也无法展开查看内容,同时pdf_obj的bool值也会有问题:
有解决办法的读者也可以在评论区留言讨论!
笔者可能会持续改进与补充,欲知后续版本,请移步: https://github.com/WittonBell/demo/tree/main/mupdf/lldbscripts
如果本文对你有帮助,欢迎点赞收藏!
- 原文作者:Witton
- 原文链接:https://wittonbell.github.io/posts/2025/2025-06-05-美化显示LLDB调试的数据结构/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议. 进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。