C++ GDB / LLDB 调试器脚本编写:自动化复杂调试任务

好的,各位观众老爷,欢迎来到“GDB/LLDB调试器脚本编写:自动化复杂调试任务”专场。今天咱们不讲那些虚头巴脑的理论,直接上干货,教大家怎么用脚本武装你的调试器,让复杂的调试任务变成小菜一碟。

开场白:调试,苦逼程序员的日常

话说程序员这行,一半的时间在写代码,另一半的时间就在Debug。有时候,代码就像个调皮的孩子,你越想找到它出错的地方,它就越跟你捉迷藏。尤其遇到复杂的Bug,那简直就是一场噩梦,让人抓耳挠腮,恨不得把电脑砸了。

但是,别急着砸电脑!咱们还有调试器这个神器。GDB和LLDB就是调试器界的两大扛把子,一个在Linux世界称王称霸,一个在苹果生态如鱼得水。今天,咱们就聊聊怎么用脚本来驯服它们,让它们为你所用,自动化那些繁琐的调试任务。

第一幕:脚本的魅力——解放你的双手

你可能会问:直接用GDB/LLDB命令不香吗?为什么要费劲写脚本?

嗯,直接用命令当然可以,但那就像用计算器算加减乘除,简单是简单,但遇到复杂的公式,你还不是得敲到手抽筋?

脚本的优势在于:

  • 自动化: 一次编写,多次使用。把常用的调试流程写成脚本,以后遇到类似的问题,直接运行脚本,省时省力。
  • 可重复性: 保证每次调试过程的一致性,避免人为操作带来的误差。
  • 可扩展性: 根据实际需要,灵活修改脚本,定制自己的调试工具。
  • 复杂逻辑: 脚本可以包含复杂的条件判断、循环等逻辑,完成更高级的调试任务。

总之,脚本就像一个自动化的流水线,把繁琐的调试步骤串起来,让你从重复性的劳动中解放出来,有更多的时间去思考问题,而不是跟调试器死磕。

第二幕:GDB脚本入门——Python来帮忙

GDB原生支持Tcl脚本,但说实话,Tcl这门语言有点老旧,用起来不太顺手。好消息是,GDB还支持Python脚本!Python语法简洁易懂,功能强大,简直是脚本界的最佳选择。

2.1 GDB Python脚本的基本结构

一个简单的GDB Python脚本通常包含以下几个部分:

# 导入gdb模块
import gdb

# 定义一个GDB命令
class MyCommand(gdb.Command):
    """这是一个自定义的GDB命令,用于打印变量的值。"""

    def __init__(self):
        # 命令名称
        super().__init__("myprint", gdb.COMMAND_USER)

    def invoke(self, arg, from_tty):
        # 执行命令时的逻辑
        try:
            val = gdb.parse_and_eval(arg)
            print(f"{arg} = {val}")
        except gdb.error as e:
            print(f"Error: {e}")

# 注册命令
MyCommand()

这个脚本定义了一个名为myprint的GDB命令,它的作用是打印指定变量的值。

  • import gdb:导入GDB模块,才能使用GDB提供的API。
  • class MyCommand(gdb.Command):定义一个继承自gdb.Command的类,表示这是一个GDB命令。
  • __init__:构造函数,用于初始化命令。super().__init__("myprint", gdb.COMMAND_USER)表示命令名称是myprint,类型是用户自定义命令。
  • invoke:执行命令时的逻辑。arg是命令的参数,from_tty表示命令是否从终端输入。
  • gdb.parse_and_eval(arg):解析并计算表达式arg的值。
  • MyCommand():注册命令,让GDB知道有这个命令。

2.2 加载和运行GDB Python脚本

把上面的代码保存为myprint.py,然后在GDB中输入以下命令加载脚本:

source myprint.py

然后就可以使用自定义的myprint命令了:

(gdb) myprint my_variable

2.3 常用GDB Python API

API 描述 示例
gdb.execute(command) 执行GDB命令。 gdb.execute("break main")
gdb.parse_and_eval(expr) 解析并计算表达式的值。 val = gdb.parse_and_eval("my_variable")
gdb.breakpoints() 获取所有断点。 for bp in gdb.breakpoints(): print(bp.location)
gdb.Breakpoint(location) 创建断点。 gdb.Breakpoint("main")
gdb.delete_breakpoints() 删除所有断点。 gdb.delete_breakpoints()
gdb.selected_frame() 获取当前帧。 frame = gdb.selected_frame()
gdb.Frame.name() 获取帧的名称。 frame_name = gdb.selected_frame().name()
gdb.Frame.read_var(name) 读取帧中变量的值。 val = gdb.selected_frame().read_var("my_variable")
gdb.inferiors() 获取所有inferior。 Inferior可以理解为程序的一个实例。 inferior = gdb.inferiors()[0] 获取第一个inferior.
gdb.Inferior.pid 获取inferior的pid。 pid = gdb.inferiors()[0].pid
gdb.events.stop 停止事件。 当程序停止时会触发。 gdb.events.stop.connect(my_stop_handler) 连接一个函数my_stop_handler到停止事件。

第三幕:LLDB脚本入门——也是Python的天下

LLDB和GDB一样,也支持Python脚本。而且LLDB的Python API更加现代化,用起来也更舒服。

3.1 LLDB Python脚本的基本结构

LLDB Python脚本的结构和GDB类似:

import lldb

def __lldb_init_module(debugger, internal_dict):
    # 创建一个命令
    debugger.HandleCommand('command script add -f my_command.my_lldb_command mycommand')

def my_lldb_command(debugger, command, exe_ctx, result, internal_dict):
    """这是一个自定义的LLDB命令,用于打印变量的值。"""

    target = debugger.GetSelectedTarget()
    process = target.GetProcess()

    # 使用SBCommandReturnObject来返回结果
    return_obj = lldb.SBCommandReturnObject()

    try:
        val = exe_ctx.frame.EvaluateExpression(command).GetValue()
        return_obj.SetStatus(lldb.eReturnStatusSuccess)
        return_obj.SetOutputString(f"{command} = {val}")
    except Exception as e:
        return_obj.SetStatus(lldb.eReturnStatusFailed)
        return_obj.SetErrorString(f"Error: {e}")

    result.AppendMessage(return_obj.GetOutput())
    result.SetStatus(return_obj.GetStatus())

这个脚本定义了一个名为mycommand的LLDB命令,它的作用也是打印指定变量的值。

  • import lldb:导入LLDB模块。
  • __lldb_init_module:LLDB加载脚本时会调用这个函数,用于注册命令。
  • debugger.HandleCommand('command script add -f my_command.my_lldb_command mycommand'):注册命令。-f参数指定命令的处理函数。
  • my_lldb_command:命令的处理函数。
  • exe_ctx.frame.EvaluateExpression(command):计算表达式command的值。
  • lldb.SBCommandReturnObject:用于返回命令执行结果。

3.2 加载和运行LLDB Python脚本

把上面的代码保存为my_command.py,然后在LLDB中输入以下命令加载脚本:

(lldb) command source my_command.py

然后就可以使用自定义的mycommand命令了:

(lldb) mycommand my_variable

3.3 常用LLDB Python API

API 描述 示例
lldb.debugger 获取当前调试器对象。 debugger = lldb.debugger.GetInstance()
lldb.SBTarget 表示被调试的目标程序。 target = debugger.GetSelectedTarget()
lldb.SBProcess 表示目标程序的进程。 process = target.GetProcess()
lldb.SBThread 表示进程中的线程。 thread = process.GetThreadAtIndex(0)
lldb.SBFrame 表示线程中的栈帧。 frame = thread.GetFrameAtIndex(0)
lldb.SBValue 表示变量的值。 value = frame.EvaluateExpression("my_variable")
lldb.SBBreakpoint 表示断点。 breakpoint = target.BreakpointCreateByLocation("main.c", 10)
lldb.SBCommandInterpreter 允许执行LLDB命令。 interpreter = debugger.GetCommandInterpreter() result = lldb.SBCommandReturnObject() interpreter.HandleCommand("breakpoint set -n main", result)
lldb.SBCommandReturnObject 用于返回命令执行结果。 return_obj = lldb.SBCommandReturnObject() return_obj.SetStatus(lldb.eReturnStatusSuccess) return_obj.SetOutputString("Command executed successfully")
lldb.SBExecutionContext 上下文对象,包含了执行命令时的各种信息,例如目标、进程、线程、栈帧等。 frame = exe_ctx.frame value = frame.EvaluateExpression("my_variable")
lldb.SBError 表示错误信息。 error = lldb.SBError() target.BreakpointCreateByLocation("main.c", 10, error) if error.Fail(): print(error.GetCString())

第四幕:实战演练——自动化调试的正确姿势

光说不练假把式,咱们来几个实际的例子,看看怎么用脚本自动化调试。

4.1 GDB脚本:自动打印函数参数

有时候,我们需要在函数调用时自动打印参数的值,以便分析函数的行为。可以用下面的GDB Python脚本实现:

import gdb

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

    def stop(self):
        frame = gdb.selected_frame()
        args = frame.args()
        print(f"Entering {self.function_name}:")
        for arg in args:
            print(f"  {arg.name} = {arg.value}")
        return False  # 继续执行

PrintArgs("my_function") #把my_function替换成你想要调试的函数

这个脚本定义了一个PrintArgs断点类,当程序执行到my_function时,会自动打印函数的参数值。

4.2 LLDB脚本:自动检测内存泄漏

内存泄漏是C/C++程序中常见的问题。可以用下面的LLDB Python脚本自动检测内存泄漏:

import lldb
import os

def __lldb_init_module(debugger, internal_dict):
    debugger.HandleCommand('command script add -f memcheck.memcheck memcheck')

def memcheck(debugger, command, exe_ctx, result, internal_dict):
    """
    使用Address Sanitizer (ASan) 检测内存泄漏。
    需要在编译时启用ASan: clang -fsanitize=address
    """
    target = debugger.GetSelectedTarget()
    process = target.GetProcess()

    if not process:
        result.SetError("请先启动程序。")
        return

    # 检查ASan是否启用
    asan_enabled = False
    for lib in process.modules:
        if "libclang_rt.asan" in lib.file.GetFilename():
            asan_enabled = True
            break

    if not asan_enabled:
        result.SetError("请使用-fsanitize=address编译并运行程序。")
        return

    # 执行 continue 命令并等待程序停止
    process.Continue()
    state = process.GetState()
    if state == lldb.eStateStopped:
        # 检查是否有ASan报告
        for thread in process:
            for frame in thread:
                if "AddressSanitizer" in frame.GetFunctionName():
                    result.SetOutputString(f"发现ASan报告:n{frame}")
                    return

        result.SetOutputString("没有发现内存泄漏。")
    elif state == lldb.eStateExited:
        result.SetOutputString("程序已退出。")
    else:
        result.SetError("程序运行出错。")

这个脚本定义了一个memcheck命令,它会检查程序是否使用了Address Sanitizer (ASan),然后运行程序,如果发现内存泄漏,就会打印ASan的报告。 需要注意的是,这个脚本需要在编译时启用ASan (clang -fsanitize=address)。

4.3 GDB 脚本:调用自定义函数

这个例子展示了如何在调试过程中调用程序中已定义的函数。

import gdb

class CallFunction(gdb.Command):
  """Calls a function in the inferior process."""

  def __init__(self):
    super(CallFunction, self).__init__("call_func", gdb.COMMAND_USER)

  def invoke(self, arg, from_tty):
    args = arg.split()
    if len(args) < 1:
      print("Usage: call_func <function_name> [arg1] [arg2] ...")
      return

    function_name = args[0]
    function_args = args[1:]

    try:
      # Construct the argument list.  We need to parse each argument as an
      # expression so that GDB can figure out its type.
      arg_values = [gdb.parse_and_eval(a) for a in function_args]

      # Call the function.
      result = gdb.parse_and_eval(function_name + "(" + ", ".join(a.format_string() for a in arg_values) + ")")

      print(f"Result: {result}")

    except gdb.error as e:
      print(f"Error: {e}")

CallFunction()

使用方法:call_func my_function 1 2 "hello" 会调用my_function(1, 2, "hello")

第五幕:高级技巧——让脚本更上一层楼

掌握了基本用法,咱们再来几个高级技巧,让你的脚本更加强大。

5.1 GDB/LLDB事件处理

GDB和LLDB都提供了事件处理机制,可以让你在程序执行过程中对特定事件做出响应。例如,可以在断点命中时执行一段代码,或者在程序退出时进行清理。

  • GDB事件:

    • gdb.events.stop:程序停止时触发。
    • gdb.events.exited:程序退出时触发。
    • gdb.events.new_objfile:加载新的目标文件时触发。
  • LLDB事件:

    • SBProcess.GetListener():获取进程的监听器。
    • SBListener.WaitForEvent():等待事件发生。
    • SBEvent.GetType():获取事件类型。

5.2 GDB/LLDB Pretty Printer

Pretty Printer可以让你自定义复杂数据类型的显示方式,让调试信息更加清晰易懂。例如,可以自定义显示STL容器的内容。

  • GDB Pretty Printer: 需要编写Python类,实现to_string()方法,用于返回自定义的显示字符串。
  • LLDB Data Formatters: LLDB的Data Formatters 功能更强大,支持多种语言(Python, Objective-C, C++)。

5.3 GDB/LLDB 远程调试

GDB和LLDB都支持远程调试,可以在一台机器上运行调试器,在另一台机器上运行被调试的程序。这对于调试嵌入式系统或者远程服务器上的程序非常有用。

第六幕:总结——脚本在手,天下我有

今天咱们聊了GDB/LLDB脚本的基本用法、常用API和高级技巧,相信大家已经对调试器脚本有了初步的了解。

调试器脚本就像一把瑞士军刀,可以帮助你解决各种复杂的调试问题。只要善于利用脚本,就能大大提高调试效率,让你的Debug之路更加顺畅。

记住,熟练掌握调试器脚本,你就能:

  • 节省时间: 自动化重复性任务。
  • 提高效率: 更快地找到Bug。
  • 减少痛苦: Debug不再是噩梦。

所以,不要再犹豫了,赶紧拿起你的键盘,开始编写你的第一个调试器脚本吧!

最后的彩蛋:一些调试小技巧

  • 善用日志: 在脚本中添加日志输出,方便调试脚本本身。
  • 逐步调试: 可以使用gdb.execute("next")或者process.StepOver()等命令,单步执行脚本,观察变量的值。
  • 查阅文档: GDB和LLDB的官方文档非常详细,遇到问题可以查阅文档。

祝大家Debug顺利,早日成为调试大师!

发表回复

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