前面的博文 美化显示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

如果本文对你有帮助,欢迎点赞收藏!