LLDB 调试器使用指南

1,842 阅读10分钟

LLDB 是 LLVM 项目的重要组成部分,是一个功能强大的现代调试器。作为 Xcode 的默认调试器,它专门用于调试 C、C++、Objective-C 和 Swift 程序,同时也可以在命令行环境中独立使用。

LLDB 提供了全面的调试能力,让你能够:

  • 程序控制:启动、暂停、继续程序执行
  • 断点管理:设置、修改、删除断点,支持条件断点
  • 执行跟踪:单步执行、步入函数、步出函数
  • 状态检查:查看变量值、调用栈、内存内容
  • 动态修改:在运行时修改变量值、内存内容
  • 深度分析:分析程序结构、符号表、寄存器状态

本文将详细介绍 LLDB 的核心概念、基本使用方法、常用命令以及高级调试技巧,帮助你掌握这个强大的调试工具。

实际使用示例

让我们通过一个简单的 C++ 程序来演示 LLDB 的使用。首先创建一个示例程序:

// example.cpp
#include <iostream>
#include <vector>

int calculate_sum(const std::vector<int>& numbers) {
    int sum = 0;
    for (int i = 0; i < numbers.size(); i++) {
        sum += numbers[i];
    }
    return sum;
}

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};
    int result = calculate_sum(numbers);
    std::cout << "Sum is: " << result << std::endl;
    return 0;
}

编译程序(确保包含调试信息):

clang++ -std=c++20 -g example.cpp -o example

现在让我们使用 LLDB 来调试这个程序:

  1. 启动 LLDB
lldb example
  1. 设置断点
# 在 calculate_sum 函数处设置断点
(lldb) b calculate_sum
# 在 main 函数处设置断点
(lldb) b main
  1. 运行程序
(lldb) run
  1. 程序会在 main 函数处停止,查看变量
(lldb) frame variable
# 输出:
# (std::vector<int>) numbers = size=5 {
#   [0] = 1
#   [1] = 2
#   [2] = 3
#   [3] = 4
#   [4] = 5
# }
  1. 单步执行
(lldb) n  # 执行下一行
(lldb) s  # 进入 calculate_sum 函数
  1. 在 calculate_sum 函数中
# 查看局部变量
(lldb) frame variable
# 输出:
# (int) sum = 0
# (const std::vector<int> &) numbers = size=5 {
#   [0] = 1
#   [1] = 2
#   [2] = 3
#   [3] = 4
#   [4] = 5
# }
# (int) i = 0

# 设置条件断点,当 i == 2 时停止
(lldb) b example.cpp:8 -c "i == 2"
  1. 继续执行
(lldb) c
  1. 查看调用栈
(lldb) bt
# 输出:
# * thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
#   * frame #0: 0x0000000100003f50 example`calculate_sum(numbers=size=5) at example.cpp:8
#     frame #1: 0x0000000100003f20 example`main at example.cpp:15
  1. 修改变量值
(lldb) expr sum = 100
  1. 继续执行直到程序结束
(lldb) c

LLDB 核心概念详解

为了更好地理解和使用 LLDB,我们需要深入理解其核心概念。这些概念构成了 LLDB 调试能力的基础。

1. Frame(帧)

Frame 是调用栈中的一个层级,代表程序执行过程中的一个函数调用。每个 frame 包含:

  • 函数信息:当前执行的函数名和参数
  • 局部变量:该函数作用域内的所有变量
  • 程序计数器:当前执行的指令地址
  • 返回地址:函数执行完毕后要返回的地址
# 查看当前帧的变量
(lldb) frame variable
# 简写:fr v

# 查看所有帧(调用栈)
(lldb) bt
# 输出示例:
# * thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
#   * frame #0: 0x0000000100003f50 example`calculate_sum(numbers=size=5) at example.cpp:8
#     frame #1: 0x0000000100003f20 example`main at example.cpp:15

2. Thread(线程)

Thread 是程序执行的基本单位,代表一个独立的执行路径:

  • 单线程程序只有一个主线程
  • 多线程程序有多个线程同时执行
  • 每个线程都有自己的调用栈
# 查看所有线程
(lldb) thread list

# 切换到指定线程
(lldb) thread select 2

3. Breakpoint(断点)

Breakpoint 是程序执行过程中的暂停点:

# 设置断点
(lldb) b calculate_sum

# 条件断点
(lldb) b example.cpp:8 -c "i == 2"

# 查看断点
(lldb) br l

4. Watchpoint(监视点)

Watchpoint 用于监控变量值的变化:

# 当变量值改变时停止
(lldb) watchpoint set variable sum

# 当变量被写入时停止
(lldb) watchpoint set write sum

5. Process(进程)

Process 是正在被调试的程序实例:

# 启动进程
(lldb) run

# 附加到运行中的进程
(lldb) process attach -p <pid>

6. Target(目标)

Target 代表要调试的程序,包含程序的二进制文件和调试信息:

# 创建目标
(lldb) target create example

# 查看目标信息
(lldb) target list

7. Module(模块)

Module 是程序加载的代码单元,通常是动态库或可执行文件:

# 查看所有模块
(lldb) image list

# 查看特定模块的符号
(lldb) image lookup -n function_name

8. Symbol(符号)

Symbol 是程序中的函数名、变量名等标识符:

# 查找符号
(lldb) image lookup -n main

# 查看符号表
(lldb) image lookup -a address

9. Image(镜像)

Image 是 LLDB 中表示可执行文件或动态库的概念,与 Module 概念相关:

# 列出所有加载的镜像
(lldb) image list
# 输出示例:
# [  0] 12345678-1234-1234-1234-123456789ABC 0x0000000100000000 /path/to/example
# [  1] 87654321-4321-4321-4321-CBA987654321 0x00007fff12345678 /usr/lib/libc++.1.dylib

# 查看特定镜像的详细信息
(lldb) image list -o example

# 查看镜像的符号表
(lldb) image lookup -v -n function_name

# 查看镜像的调试信息
(lldb) image lookup -v -a 0x0000000100001234

10. Memory(内存)

Memory 操作允许你直接查看和修改程序的内存内容:

# 读取内存内容
(lldb) memory read address
# 简写:x address

# 读取指定大小的内存
(lldb) memory read -s 4 -c 10 address
# -s 4: 每次读取 4 字节
# -c 10: 读取 10 次

# 以不同格式显示内存
(lldb) memory read -f x address  # 十六进制
(lldb) memory read -f d address  # 十进制
(lldb) memory read -f s address  # 字符串
(lldb) memory read -f i address  # 指令

# 写入内存
(lldb) memory write address value

# 搜索内存中的特定值
(lldb) memory find -s 4 -e 0x12345678 start_address end_address

11. Expression(表达式)

Expression 允许你在调试过程中执行 C/C++ 表达式:

# 执行表达式并显示结果
(lldb) expr variable_name
# 简写:p variable_name

# 执行复杂表达式
(lldb) expr (int)strlen("hello")

# 修改变量值
(lldb) expr variable_name = new_value

# 调用函数
(lldb) expr function_name(arguments)

# 创建临时变量
(lldb) expr int temp = 42

# 执行多行表达式
(lldb) expr {
    int sum = 0;
    for (int i = 0; i < 10; i++) {
        sum += i;
    }
    sum;
}

12. Variable(变量)

Variable 操作专门用于查看和操作变量:

# 查看当前帧的所有变量
(lldb) frame variable
# 简写:fr v

# 查看特定变量
(lldb) frame variable variable_name

# 查看变量的详细信息
(lldb) frame variable -L variable_name

# 查看变量的类型信息
(lldb) frame variable -T variable_name

# 查看全局变量
(lldb) target variable global_variable_name

# 查看静态变量
(lldb) target variable static_variable_name

13. Register(寄存器)

Register 操作允许你查看和修改 CPU 寄存器:

# 查看所有寄存器
(lldb) register read
# 简写:re r

# 查看特定寄存器
(lldb) register read rax

# 查看寄存器组
(lldb) register read --all

# 修改寄存器值
(lldb) register write rax 0x12345678

# 查看浮点寄存器
(lldb) register read --all --fp

概念之间的关系

Process (进程)
├── Thread (线程)
│   ├── Frame #0 (当前函数)
│   │   ├── Variable (局部变量)
│   │   └── Register (寄存器状态)
│   ├── Frame #1 (调用者)
│   └── ...
├── Module/Image (模块/镜像)
│   ├── Symbol (符号)
│   └── Memory (内存区域)
├── Breakpoint/Watchpoint (断点/监视点)
└── Expression (表达式执行环境)

常用命令

1. 启动调试

lldb program_name

2. 断点操作

# 设置断点
breakpoint set -f file.cpp -l 10
# 简写形式
b file.cpp:10

# 列出所有断点
breakpoint list
# 简写形式
br l

# 删除断点
breakpoint delete 1
# 简写形式
br del 1

# 条件断点
breakpoint set -f file.cpp -l 10 -c "variable > 5"

# 使用正则表达式设置断点*
breakpoint set -r "function_name"

# 保存断点配置
breakpoint write -f breakpoints.txt

3. 程序控制

# 运行程序
run
# 简写形式
r

# 继续执行
continue
# 简写形式
c

# 单步执行(不进入函数)
next
# 简写形式
n

# 单步执行(进入函数)
step
# 简写形式
s

# 退出当前函数
finish

4. 查看变量和内存

# 打印变量值
print variable_name
# 简写形式
p variable_name

# 查看变量类型
frame variable -L
# 简写形式
fr v -L

# 查看内存
memory read address
# 简写形式
x address

# 使用 watchpoint 监控变量变化
watchpoint set variable variable_name

5. 调用栈操作

# 查看调用栈
backtrace
# 简写形式
bt

# 切换到指定帧
frame select 2
# 简写形式
f 2

LLDB 脚本编程

LLDB 支持多种脚本语言,让你能够自动化调试过程、创建自定义命令和扩展调试功能。LLDB 内置了 Python 脚本支持,下面以 python 脚本为例,讲述如何使用脚本。

内联脚本

你也可以直接在 LLDB 命令行中执行 Python 代码:

# 执行单行 Python 代码
(lldb) script print("Hello from Python!")

# 执行多行 Python 代码
(lldb) script
>>> target = lldb.debugger.GetSelectedTarget()
>>> process = target.GetProcess()
>>> print(f"Process ID: {process.GetProcessID()}")
>>> DONE

# 使用 Python 表达式
(lldb) script print(lldb.debugger.GetSelectedTarget().GetExecutable().GetFilename())

自定义命令

1. 创建自定义命令

# custom_commands.py
import lldb

def print_call_stack(debugger, command, result, internal_dict):
    """自定义命令:美化显示调用栈"""
    target = debugger.GetSelectedTarget()
    process = target.GetProcess()
    thread = process.GetSelectedThread()
    
    print("📞 Call Stack:")
    for i in range(thread.GetNumFrames()):
        frame = thread.GetFrameAtIndex(i)
        function = frame.GetFunction()
        line_entry = frame.GetLineEntry()
        
        if function.IsValid():
            func_name = function.GetName()
        else:
            func_name = "<unknown>"
        
        if line_entry.IsValid():
            file_spec = line_entry.GetFileSpec()
            line_num = line_entry.GetLine()
            print(f"  {i:2d}: {func_name} at {file_spec.GetFilename()}:{line_num}")
        else:
            print(f"  {i:2d}: {func_name}")

def analyze_variables(debugger, command, result, internal_dict):
    """自定义命令:分析变量类型和值"""
    target = debugger.GetSelectedTarget()
    process = target.GetProcess()
    thread = process.GetSelectedThread()
    frame = thread.GetSelectedFrame()
    
    variables = frame.GetVariables(True, True, True, True)
    print("🔍 Variable Analysis:")
    for var in variables:
        name = var.GetName()
        value = var.GetValue()
        type_name = var.GetType().GetName()
        print(f"  {name} ({type_name}): {value}")

def __lldb_init_module(debugger, internal_dict):
    debugger.HandleCommand('command script add -f custom_commands.print_call_stack pcs')
    debugger.HandleCommand('command script add -f custom_commands.analyze_variables pva')

2. 使用自定义命令

# 加载自定义命令
(lldb) command script import custom_commands.py

# 使用美化调用栈命令
(lldb) pcs

# 使用变量分析命令
(lldb) pva

使用 lldb -s 命令执行脚本

lldb -s 命令允许你在启动 LLDB 时自动执行脚本文件,这是自动化调试的强大工具。

基本用法

# 启动 LLDB 并执行脚本文件
lldb -s script_file.lldb program_name

# 或者使用简写形式
lldb -S script_file.lldb program_name

脚本文件格式

LLDB 脚本文件使用 LLDB 命令语法,每行一个命令:

# debug_session.lldb
# 设置断点
breakpoint set -n main
breakpoint set -n calculate_sum

# 运行程序
run

# 查看变量
frame variable

# 单步执行
next

# 查看调用栈
bt

# 继续执行
continue

结合 Python 脚本

你可以在 LLDB 脚本中加载和执行 Python 脚本:

# debug_with_python.lldb
# 加载 Python 脚本
command script import debug_utils.py

# 设置断点
breakpoint set -n main

# 运行程序
run

# 使用 Python 脚本中的自定义命令
pvars
pcs

# 继续调试
continue

对应的 Python 脚本 debug_utils.py

# debug_utils.py
import lldb

def print_variables(debugger, command, result, internal_dict):
    """打印所有变量"""
    target = debugger.GetSelectedTarget()
    process = target.GetProcess()
    thread = process.GetSelectedThread()
    frame = thread.GetSelectedFrame()
    
    variables = frame.GetVariables(True, True, True, True)
    print("📋 Variables:")
    for var in variables:
        print(f"  {var.GetName()}: {var.GetValue()}")

def print_call_stack(debugger, command, result, internal_dict):
    """美化显示调用栈"""
    target = debugger.GetSelectedTarget()
    process = target.GetProcess()
    thread = process.GetSelectedThread()
    
    print("📞 Call Stack:")
    for i in range(thread.GetNumFrames()):
        frame = thread.GetFrameAtIndex(i)
        function = frame.GetFunction()
        if function.IsValid():
            print(f"  {i}: {function.GetName()}")
        else:
            print(f"  {i}: <unknown>")

def __lldb_init_module(debugger, internal_dict):
    debugger.HandleCommand('command script add -f debug_utils.print_variables pvars')
    debugger.HandleCommand('command script add -f debug_utils.print_call_stack pcs')

在 VSCode 中调试 C/C++ 代码

使用 CodeLLDB 在 VSCode 中调试程序

VSCode 提供了强大的 LLDB 调试支持,通过 CodeLLDB 扩展可以方便地进行 C/C++ 程序调试。以下是详细设置步骤:

  1. 安装 CodeLLDB 插件
  2. 创建 launch.json 文件,并配置:
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "CodeLLDB Debug",
            "type": "lldb",
            "request": "launch",
            "program": "executable file", // 必须用 -g 生成调试信息
        }
    ]
}

使用 LLDB DAP 在 vscode 中 调试 C++

lldb-dap 是 LLDB 的调试适配器协议(Debug Adapter Protocol)实现,它允许你在支持 DAP 的编辑器中使用 LLDB 进行调试。以下是详细的使用指南:

  1. 安装 lldb-dap
  2. 配置 VSCode
  • 创建 .vscode/launch.json 文件:
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "lldb-dap Debug",
            "type": "lldb-dap",
            "request": "launch",
            "program": "executable file",
        }
    ]
}
  • 配置 settings.json
{
    "lldb-dap.executable": "/path/to/lldb-dap",
}