C++实现自定义的调试器扩展:利用Python/Lua脚本定制GDB/LLDB的功能

C++实现自定义的调试器扩展:利用Python/Lua脚本定制GDB/LLDB的功能

大家好!今天我们来聊聊如何利用Python/Lua脚本定制GDB/LLDB的功能,也就是如何为这两个强大的调试器编写自定义的扩展。这能极大地提升调试效率,简化复杂任务,并允许我们根据特定项目或需求定制调试器的行为。

为什么要定制调试器?

默认的GDB/LLDB功能已经很强大,但有时我们需要更进一步。以下是一些定制调试器的常见理由:

  • 自动化重复任务: 比如,每次断点命中时自动记录某些变量的值,或执行一系列命令。
  • 定义自定义命令: 创建更符合项目语境的命令,例如,专门用于操作特定数据结构的命令。
  • 增强可视化: 格式化输出,以便更清晰地呈现复杂数据。
  • 动态分析: 在运行时修改程序行为,注入错误,或模拟特定情况。
  • 桥接不同工具: 将调试器与外部工具集成,例如,性能分析器或静态分析器。

GDB扩展:Python脚本

GDB支持使用Python脚本进行扩展。 Python是一种灵活且功能强大的脚本语言,拥有丰富的库和工具,非常适合编写调试器扩展。

GDB Python API

GDB Python API提供了一组对象和函数,用于与GDB交互。一些核心类和函数包括:

  • gdb.Command: 创建自定义命令。
  • gdb.Breakpoint: 管理断点。
  • gdb.execute(command): 执行GDB命令。
  • gdb.parse_and_eval(expression): 解析并计算表达式。
  • gdb.Value: 表示程序中的值。
  • gdb.Type: 表示类型信息。
  • gdb.selected_frame(): 获取当前选定的帧。
  • gdb.InferiorThread: 表示程序中的线程。

编写一个简单的GDB Python扩展

让我们创建一个简单的GDB扩展,该扩展定义一个名为print_array的命令,该命令打印指定地址开始的数组的元素。

# array_printer.py
import gdb

class PrintArray(gdb.Command):
    """
    Prints the elements of an array.

    Usage: print_array <address> <length> <type>
    """
    def __init__(self):
        super(PrintArray, self).__init__("print_array", gdb.COMMAND_USER)

    def invoke(self, arg, from_tty):
        args = arg.split()
        if len(args) != 3:
            print("Usage: print_array <address> <length> <type>")
            return

        try:
            address = int(args[0], 16)  # Convert address from hex to int
            length = int(args[1])
            type_name = args[2]
        except ValueError:
            print("Invalid arguments. Address should be a hexadecimal number, length should be an integer.")
            return

        try:
            type = gdb.lookup_type(type_name)
        except gdb.error:
            print(f"Type '{type_name}' not found.")
            return

        for i in range(length):
            try:
                val = gdb.Value(address + i * type.sizeof).cast(type.pointer()).dereference()
                print(f"[{i}]: {val}")
            except gdb.error as e:
                print(f"Error accessing element {i}: {e}")
                return

PrintArray()

代码解释:

  1. PrintArray(gdb.Command): 定义一个继承自gdb.Command的类。
  2. __init__: 构造函数,注册命令print_array,并将其分类为用户自定义命令(gdb.COMMAND_USER).
  3. invoke: 命令执行的入口点。它接收命令的参数(arg)和一个布尔值(from_tty),指示命令是否从终端执行。
  4. 参数解析: 解析arg字符串,提取地址、长度和类型名称。将地址从十六进制字符串转换为整数。
  5. 类型查找: 使用gdb.lookup_type查找指定类型的gdb.Type对象。
  6. 循环打印: 循环遍历数组,计算每个元素的地址,并使用gdb.Value读取该地址的值。 cast(type.pointer()).dereference()会将原始地址转换为对应类型的指针,然后解引用获取实际值。
  7. 错误处理: 使用try...except块捕获可能发生的错误,例如无效的地址或类型。

加载GDB Python扩展

要加载此扩展,请在GDB中执行以下命令:

source array_printer.py

或者,你可以将source array_printer.py添加到你的.gdbinit文件中,以便每次启动GDB时自动加载该扩展。

使用自定义命令

现在,你可以使用print_array命令了。例如,假设你的程序中有一个名为my_arrayint数组,其地址为0x7fffffffe000,长度为10。你可以使用以下命令打印该数组的元素:

print_array 0x7fffffffe000 10 int

GDB将打印数组的每个元素,如下所示:

[0]: 1
[1]: 2
[2]: 3
[3]: 4
[4]: 5
[5]: 6
[6]: 7
[7]: 8
[8]: 9
[9]: 10

进阶:使用断点

我们可以将Python脚本与断点结合使用,以实现更复杂的行为。例如,我们可以创建一个扩展,该扩展在特定函数被调用时自动打印该函数的参数。

# print_args.py
import gdb

class PrintArgs(gdb.Breakpoint):
    def __init__(self, function_name):
        super(PrintArgs, self).__init__(function_name)
        self.function_name = function_name

    def stop(self):
        frame = gdb.selected_frame()
        args = frame.args()
        print(f"Function {self.function_name} called with arguments:")
        for arg in args:
            print(f"  {arg.name} = {arg}")
        return False  # Continue execution

PrintArgs("my_function") #假设my_function是你的程序中你想查看参数的函数

代码解释:

  1. PrintArgs(gdb.Breakpoint): 定义一个继承自gdb.Breakpoint的类。
  2. __init__: 构造函数,设置断点在指定的函数名称处。
  3. stop: 当断点命中时执行的函数。它获取当前帧,然后获取该帧的参数。它打印函数名和参数名和值。
  4. return False: 指示GDB继续执行程序。 如果返回True,则GDB会像普通的断点一样停止。

要使用此扩展,请在GDB中加载它:

source print_args.py

现在,每当my_function被调用时,GDB将打印其参数。

LLDB扩展:Python脚本

LLDB也支持使用Python脚本进行扩展,其API与GDB的API非常相似,但也有一些差异。

LLDB Python API

一些核心类和函数包括:

  • lldb.SBCommandInterpreter: 用于执行LLDB命令。
  • lldb.SBCommandReturnObject: 表示命令执行的结果。
  • lldb.SBBreakpoint: 管理断点。
  • lldb.SBTarget: 表示被调试的目标程序。
  • lldb.SBProcess: 表示被调试的进程。
  • lldb.SBThread: 表示进程中的线程。
  • lldb.SBFrame: 表示调用栈中的帧。
  • lldb.SBValue: 表示程序中的值。
  • lldb.SBType: 表示类型信息。

编写一个简单的LLDB Python扩展

让我们创建一个与GDB示例类似的LLDB扩展,该扩展定义一个名为print_array的命令,该命令打印指定地址开始的数组的元素。

# array_printer_lldb.py
import lldb

def print_array_command(debugger, command, result, internal_dict):
    args = command.split()
    if len(args) != 3:
        result.SetError("Usage: print_array <address> <length> <type>")
        return

    try:
        address = int(args[0], 16)  # Convert address from hex to int
        length = int(args[1])
        type_name = args[2]
    except ValueError:
        result.SetError("Invalid arguments. Address should be a hexadecimal number, length should be an integer.")
        return

    target = debugger.GetSelectedTarget()
    if not target:
        result.SetError("No target selected.")
        return

    process = target.GetProcess()
    if not process:
        result.SetError("No process selected.")
        return

    error = lldb.SBError()
    type = target.FindTypes(type_name).GetTypeAtIndex(0) # 获取第一个匹配的类型
    if not type:
        result.SetError(f"Type '{type_name}' not found.")
        return

    for i in range(length):
        element_address = address + i * type.GetByteSize()
        val = process.ReadMemory(element_address, type.GetByteSize(), error)
        if error.Fail():
            result.SetError(f"Error accessing element {i}: {error}")
            return

        # 将读取的内存转换为对应类型的值。  注意这里需要手动转换
        if type_name == "int":
            element_value = int.from_bytes(bytes.fromhex(val.hex()), byteorder='little', signed=True)  # 假设是小端序
        elif type_name == "float":
             import struct
             element_value = struct.unpack('f', bytes.fromhex(val.hex()))[0]
        else:
            element_value = f"Raw bytes: {val}" #如果类型未知,则打印原始字节

        result.AppendMessage(f"[{i}]: {element_value}")

def __lldb_init_module(debugger, internal_dict):
    debugger.HandleCommand('command script add -f array_printer_lldb.print_array_command print_array')
    print("The 'print_array' command has been installed, type 'help print_array' for more information.")

代码解释:

  1. print_array_command(debugger, command, result, internal_dict): LLDB扩展的入口点。
    • debugger: lldb.SBDebugger 对象,提供与调试器的交互。
    • command: 用户输入的命令字符串。
    • result: lldb.SBCommandReturnObject 对象,用于将结果返回给调试器。
    • internal_dict: 内部字典,供扩展使用。
  2. 参数解析: 与GDB示例类似,解析命令字符串,提取地址、长度和类型名称。
  3. 获取Target和Process: 获取当前调试的目标程序(lldb.SBTarget)和进程(lldb.SBProcess)。
  4. 类型查找: 使用target.FindTypes(type_name).GetTypeAtIndex(0)查找类型。
  5. 循环读取内存: 循环遍历数组,计算每个元素的地址,并使用process.ReadMemory从该地址读取内存。
  6. 类型转换: 将读取的内存转换为对应类型的值。 这部分比GDB复杂,因为需要手动处理类型转换。 这里简单地处理了intfloat类型。
  7. 结果输出: 使用result.AppendMessage将结果输出到LLDB控制台。
  8. __lldb_init_module(debugger, internal_dict): LLDB模块初始化函数。
    • debugger.HandleCommand('command script add -f array_printer_lldb.print_array_command print_array'): 注册命令。
    • command script add: LLDB命令,用于添加自定义命令。
    • -f array_printer_lldb.print_array_command: 指定命令处理函数的名称。
    • print_array: 用户可见的命令名称。

加载LLDB Python扩展

要加载此扩展,请将以下行添加到你的~/.lldbinit文件中:

command script import array_printer_lldb.py

使用自定义命令

现在,你可以使用print_array命令了,与GDB示例类似。

print_array 0x7fffffffe000 10 int

LLDB将打印数组的每个元素。

进阶:使用断点

LLDB中可以使用lldb.SBBreakpoint类来创建断点,与GDB类似。

# print_args_lldb.py
import lldb

def print_args_callback(frame, bp_loc, dict):
    function_name = frame.GetFunctionName()
    print(f"Function {function_name} called with arguments:")
    for i in range(frame.GetNumArguments()):
        arg = frame.GetArgumentAtIndex(i)
        print(f"  {arg.GetName()} = {arg.GetValue()}")
    return False  # Continue execution

def __lldb_init_module(debugger, internal_dict):
    target = debugger.GetSelectedTarget()
    if not target:
        print("No target selected.")
        return

    breakpoint = target.BreakpointCreateByName("my_function") # 假设my_function是你的程序中你想查看参数的函数
    if breakpoint.IsValid():
        breakpoint.SetScriptCallbackFunction("print_args_lldb.print_args_callback")
        print("Breakpoint set at my_function, will print arguments when hit.")
    else:
        print("Failed to set breakpoint at my_function.")

代码解释:

  1. print_args_callback(frame, bp_loc, dict): 断点回调函数。 当断点命中时,此函数将被调用。
    • frame: lldb.SBFrame 对象,表示当前帧。
    • bp_loc: lldb.SBBreakpointLocation 对象,表示断点位置。
    • dict: 一个字典,供回调函数使用。
  2. 获取函数参数: 获取当前帧的函数名称和参数,并打印它们。
  3. __lldb_init_module(debugger, internal_dict): LLDB模块初始化函数。
    • target.BreakpointCreateByName("my_function"): 创建一个名称为my_function的断点。
    • breakpoint.SetScriptCallbackFunction("print_args_lldb.print_args_callback"): 将回调函数设置为断点命中时执行的函数。

要使用此扩展,请将以下行添加到你的~/.lldbinit文件中:

command script import print_args_lldb.py

GDB扩展:Lua脚本

GDB还支持使用Lua脚本进行扩展。 Lua是一种轻量级的脚本语言,嵌入式应用广泛。

GDB Lua API

GDB Lua API提供了一组函数,用于与GDB交互。一些核心函数包括:

  • gdb.execute(command): 执行GDB命令。
  • gdb.parse_and_eval(expression): 解析并计算表达式。
  • gdb.Breakpoint(location, options): 创建断点。
  • gdb.Command(name, command_class, options): 创建自定义命令。
  • gdb.FrameDecorator(frame): 装饰帧对象以添加自定义属性和方法。

编写一个简单的GDB Lua扩展

让我们创建一个简单的GDB扩展,该扩展定义一个名为print_array_lua的命令,该命令打印指定地址开始的数组的元素。

-- array_printer.lua
gdb.Command("print_array_lua", gdb.COMMAND_USER, {
    help = "Prints the elements of an array.nnUsage: print_array_lua <address> <length> <type>",
    invoke = function(argv, from_tty)
        if #argv ~= 3 then
            print("Usage: print_array_lua <address> <length> <type>")
            return
        end

        local address = tonumber(argv[1], 16)  -- Convert address from hex to int
        local length = tonumber(argv[2])
        local type_name = argv[3]

        local type = gdb.lookup_type(type_name)
        if not type then
            print("Type '" .. type_name .. "' not found.")
            return
        end

        for i = 0, length - 1 do
            local element_address = address + i * type.sizeof
            local value = gdb.parse_and_eval("*((" .. type_name .. "*)" .. element_address .. ")")
            print("[" .. i .. "]: " .. value)
        end
    end
})

代码解释:

  1. gdb.Command(name, command_class, options): 定义一个自定义命令。
    • name: 命令名称 (print_array_lua).
    • command_class: 命令类别 (gdb.COMMAND_USER).
    • options: 一个表,包含命令的选项,例如 helpinvoke
  2. invoke: 命令执行的入口点。
  3. 参数解析: 与Python示例类似,解析命令参数。
  4. 类型查找: 使用gdb.lookup_type查找类型。
  5. 循环打印: 循环遍历数组,计算每个元素的地址,并使用gdb.parse_and_eval读取该地址的值。 使用字符串连接构造表达式,然后使用gdb.parse_and_eval计算表达式。

加载GDB Lua扩展

要加载此扩展,请在GDB中执行以下命令:

source array_printer.lua

或者,你可以将source array_printer.lua添加到你的.gdbinit文件中。

使用自定义命令

现在,你可以使用print_array_lua命令了。

print_array_lua 0x7fffffffe000 10 int

总结:定制调试器,提升开发效率

我们探讨了如何使用Python和Lua脚本定制GDB和LLDB的功能,编写自定义命令、断点处理程序,以及更有效地调试C++代码。掌握这些技巧,能够显著提高调试效率,并根据项目需求定制调试器。

更多IT精英技术系列讲座,到智猿学院

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注