Debugging(调试)是指创建并使用分析框架来隔离因果路径并检验假设。debugging最重要的工具是debugger(调试器),它可以帮助你了解程序在运行时的行为,而无需修改代码。
LLDB 为 Apple平台上的开发人员提供了底层调试环境。你可以从 Terminal窗口 或 Xcode源代码编辑器 中使用它,以查找和消除Swift,C,C++ 和 Objective-C 代码中的问题。
debugger具有两个主要功能:控制执行流 和 访问状态。
你主要通过在代码中的不同位置设置breakpoints来控制程序的执行。每当程序达到设置的breakpoint时,debugger就会暂时停止执行该程序。在执行停止后,你可以使用debugger检查或修改不同变量的当前状态,step over、into或退出下一条语句,然后根据需要继续执行。
LLDB 命令语法
通过在调试会话中输入命令来与 LLDB 进行交互。每个 LLDB command 由零个或多个subcommand组成,并且可以以下形式另外指定一个或多个选项或参数:
1 | <command> [<subcommand>...] [--<option> [<option-value>]]... [argument]... |
subcommand 子命令
Subcommands 是用空格分隔的标记,用于组织相关的操作。通常,command的最后subcommand是指示要执行的操作的谓词。例如,与断点管理相关的命令以breakpoint开头,例如 breakpoint list和breakpoint disable命令,它们分别列出和禁用断点。
Arguments 参数
command可能需要一个或多个arguments。arguments 是用空格分隔的标记,用于指示要执行的操作。例如,breakpoint disable命令需要一个参数来指定要禁用的断点,例如breakpoint disable 1,它会禁用ID等于1的断点。
用单引号(‘’)或双引号(“”)括起来来指定包含空格的参数。在单引号或双引号中,可以使用反斜杠字符()来转义非定界的引号,例如
"some \"quoted\" string"。
Options 选项
command可能还包含一个或多个options。options是以双破折号(--)开头的以空格分隔的标记,可以在不同的组合中使用,以修改要执行的操作。某些options还提供了使用单破折号(-)的简写形式。例如,当breakpoint set命令指定--one-shot (-o)选项时,如breakpoint set —one--shot,该breakpoint将在第一次导致程序停止时被删除。
某些options指定单个空格分隔的值作命令的命名参数。例如,breakpoint set命令可以通过传递--name选项,并将函数名称作为选项值来设置特定函数的断点。
某些命令可能需要某些组合的options。例如,breakpoint set命令可以通过传递--file和--line选项以及相应的文件名和行号来在代码中的特定位置设置断点。
接受
options和 自由格式参数的命令(如expression命令),必须在最后一个option和第一个参数之间放置一个以空格分隔的双破折号(--)。这样可以确保以破折号(-)开头的option的参数被解释为参数。
命令表单
等效的 LLDB 命令可以以各种不同的形式表示。例如,以下3个命令执行相同的操作:
- Canonical form 规范形式
expression --object-description -- someVariable
命令的canonical form充当要执行的操作的描述性表示。
- Abbreviated form 缩写形式
e -O -- someVariable
命令的abbreviated form使用命令和子命令的缩写形式(例如e表示expression)以及options的缩写形式(例如-O表示--object-description)。
- Alias 别名
po someVariable
可以为任何命令子序列创建一个alias,为执行常见操作提供方便的快捷方式。例如po用来计算并打印对象表达式。
为了清楚起见,本文档主要以canonical form引用命令,后跟括号中的任何缩写形式或别名。
使用命令行帮助
LLDB 通过 help 命令在debugger会话中提供了大量文档。
调用不带任何参数的 help 命令将列出所有可用的 调试器命令 以及 现有的命令别名。例如:(lldb) help
通过将特定命令或子命令作为 help 命令的参数传递,可以获得有关特定命令或子命令的用法的信息。例如:(lldb) help breakpoint set
help命令适用于任何可用形式的命令,包括别名。例如:(lldb) help po
Breakpoints 断点
breakpoint在执行的指定点中断程序的执行。breakpoint是开发人员使用debugger开始与程序进行交互的主要方法。
设置 Breakpoint
你可以使用 breakpoint set 命令设置 breakpoint:
breakpoint set -n 函数名或绝对路径添加断点
1 | (lldb) breakpoint set -n sayHello |
breakpoint set -f 文件名 -l 行数在某个源文件某一行设置断点1
2
3
4
5
6(lldb) breakpoint set -f OneViewController.swift -l 47
Breakpoint 7: where = test01`test01.OneViewController.sayHello() -> () + 20 at OneViewController.swift:48:15, address = 0x000000010f50c4f4
(lldb) breakpoint list
Current breakpoints:
7: file = 'OneViewController.swift', line = 47, exact_match = 0, locations = 1, resolved = 1, hit count = 0
7.1: where = test01`test01.OneViewController.sayHello() -> () + 20 at OneViewController.swift:48:15, address = 0x000000010f50c4f4, resolved, hit count = 0
3、将breakpoint设置在随时发生 Swift错误 或 抛出Objective-C异常发生时,通过在 --language-exception (-E) 选项后传递 Swift或objc值
1 | # 将断点配置为仅在抛出特定类型的错误或引发异常时停止 |
1 | (lldb) breakpoint set -E objc |
修改 Breakpoint
通过使用 breakpoint ID或location ID 作为参数以及以下任何配置的选项,可以使用breakpoint Modify命令来修改逻辑断点或个别位置:
--condition (-c)指定一个表达式,该表达式必须计算为true才能使断点停止--ignore-count (-i)指定在停止之前跳过断点的次数--one-shot (-o)第一次停止时删除断点--queue-name (-q)指定断点在其上停止的队列的名称--thread-name (-T)指定断点在其上停止的线程的名称--thread-id (-t)指定断点在其上停止的线程的ID(TID)--thread-index (-x)指定断点在其上停止的线程的索引
例如,下面的代码片段显示了如何修改第一个断点,使其在第一次停在其任何位置时被删除:
1 | (lldb) breakpoint modify --one-shot 1 |
在断点处运行命令
当达到设定的breakpoint时,它将停止程序的执行并允许运行LLDB命令。你还可以通过使用breakpoint command add命令来指定每次到达breakpoint时要运行的命令,该命令将 breakpoint ID 或 location ID作为参数。
例如,要将命令添加到第一个断点的第一个位置:
1 | (lldb) breakpoint command add 1.1 |
默认情况下,breakpoint command add命令使用LLDB command interpreter(LLDB命令解释器),并打开一个交互式提示,该提示的行以直角括号(>)开头。每行输入一个命令。输入命令完成后,键入DONE退出交互式提示。
如果你输入process Continue命令作为最后一个断点命令,则debugger器将在执行所有前面的命令后自动继续运行程序。这对于记录有关程序状态的信息而又不中断用户与程序的交互性特别方便。
1 | (lldb) breakpoint command add 1.1 |
要指定要在指定的断点内联而不是在交互式提示中执行的命令,请将--one-liner (-o)选项(带有单引号的值作为引号括起来)传递给breakpoint command add命令
1 | (lldb) breakpoint command add 1.1 -o "bt" |
禁用和启用断点
- 禁用
logical breakpoint时,它不会在任何位置停止。要禁用断点而不删除它,请使用breakpoint disable命令。
1、通过传递breakpoint ID作为参数来禁用 logical breakpoint:
1 | (lldb) breakpoint disable 1 |
2、传递location ID 禁用单个断点位置
1 | (lldb) breakpoint disable 2.1 |
- 使用
breakpoint enable命令启用logical breakpoint或breakpoint location,并传递breakpoint ID或location ID。
1 | (lldb) breakpoint enable 1 |
- 要仅禁用
logical breakpoint的某些位置,请使用breakpoint disable命令,传递断点ID,后跟点分隔的通配符(*)。
1 | (lldb) breakpoint disable 1.* |
删除 Breakpoint
删除断点将禁用它,并防止重新启用它。使用breakpoint delete + breakpoint ID命令:
1 | (lldb) breakpoint delete 1 |
Watchpoints
watchpoint(监视点)是你在地址或变量上设置的一种breakpoint,可在访问值时随时停止,而不是在执行时进行设置。
Watchpoints受运行调试程序的硬件上的寄存器数限制。
可以使用 watchpoint 来隔离执行过程中何时更改变量,这对于调试在代码中多个组件之间共享的状态特别有用。一旦知道了变量的更改位置和更改方式,便可以在要调查的执行点上创建breakpoints,然后删除watchpoint。
设置 Watchpoint
使用 watchpoint set variable 变量名 命令在变量上设置watchpoint,并使用 watchpoint set expression 命令在表达式中的地址上设置watchpoint。
1 | # 设置当前断点上下文的变量 |
默认情况下,watchpoint监视对变量或地址的写访问。通过将--watch (-w)选项传递给值read,write或read_write,指定要监视的访问类型。
1 | watchpoint set variable -w [read | write | read_write] 变量名 |
默认情况下,监视点使用目标的指针字节大小监视读写。通过传递值为 1 | 2 | 4 | 8 的 --size (-s) 选项来更改用于监视区域的字节数。
列出 Watchpoints
与breakpoints一样,您可以使用 watchpoint list 命令显示所有已设置的watchpoints。
1 | (lldb) watchpoint list |
像breakpoint一样,watchpoints也具有分配的整数ID。与断点不同,watchpoints不会解析到任何特定位置,因为它会监视变量或地址的任何更改。
修改 Watchpoint
使用watchpoint modify命令通过将watchpoint ID作为参数以及--condition (-c)选项和表达式作为其值,来传递设置watchpoint停止的时间。
1 | (lldb) watchpoint modify --condition !places.isEmpty |
向 Watchpoints 添加命令
要添加在击中监视点时运行的命令,请使用 watchpoint command add 命令,并将 watchpoint ID 作为参数传递。
1 | (lldb) watchpoint command add 1 |
删除 Watchpoint
由于watchpoints受硬件限制,因此在不需要它们时将其删除很重要。可以使用watchpoint delete命令删除watchpoint,并将 watchpoint ID 作为参数传递。
1 | (lldb) watchpoint delete 1 |
控制程序执行
一旦程序在breakpoint处停止,它将控制权转移到debugger。然后,开发人员可以与debugger进行交互,以逐步完成指令并继续执行或退出。
thread step-over\next\n:单步运行,把子函数当作整体一步执行。thread step-in\step\s:单步运行,遇到子函数会进入函数内。thread step-out\finish:直接执行完当前函数所有代码,返回到上一个函数。process continue\continue\c:在完成对程序当前断点处的检查后,使用此命令恢复执行程序。
汇编指令级别控制
可以使用 Xcode -> Debug -> Debug Workflow -> Always Show Disassembly 切换源码级别和汇编指令级别调试面板
thread step-inst-over\nexti\ni:和n类似thread step-inst\stepi\si:和s类似
检查调用 Stack
程序运行时,它将有关正在执行的操作的信息存储在称为call stack(调用栈)的数据结构中。每次调用一个方法时,程序都会在call stack的顶部pushes一个新的stack frame(栈帧),其中包含以下内容:传递给该方法的参数(如果有),该方法的局部变量(如果有的话)以及地址 方法调用完成后返回。
当程序在breakpoint处停止时,你可以与debugger进行交互以检查当前stack frame的状态。这使你可以对方法的行为以及它与程序其他部分的交互方式进行推理。
除了获取有关当前stack frame的信息之外,你还可以与debugger进行交互,以检查当前线程以及程序使用的其他线程的整个call stack。
获取有关当前帧的信息
通过输入frame info命令,可以获得代码中当前frame(帧)的位置,包括源文件和行号。
1 | (lldb) frame info |
检查变量
- 使用
frame variable (fr v)命令获取stack frame中所有变量的列表。
1 | (lldb) frame variable |
- 要获取有关单个变量的信息,请使用
frame variable命令,并将变量名称作为参数传递。
1 | (lldb) frame variable name |
- 显示当前帧的局部变量
1 | (lldb) fr v -a |
- 显示格式为十六进制的局部变量栏的内容
1 | (lldb) fr v -f x num |
执行 Expressions
LLDB最强大的功能之一是能够从调试会话中evaluate expressions(计算表达式)。
expression (e)命令将传递的参数作为目标语言中的表达式进行计算。例如,在调试Swift程序时,可以将 Swift 代码当作当前stack frame上下文中的read-eval-print (REPL) loop来执行。这是一种在执行过程中在不同点内省变量的强大方法。
1 | (lldb) expression -- self.view.backgroundColor = .red |
打印模式
在调试会话期间查看值时,重要的是要了解frame variable和 expression命令之间的差异。
frame variable (f v) |
expression -- (p) |
expression -O -- (po) |
|---|---|---|
| 不运行代码 | 运行你的代码 | 运行你的代码 |
| 使用LLDB格式化程序 | 使用LLDB格式化程序 | 添加代码以格式化对象 |
使用frame variable (f v)命令不会运行任何代码,因此不会产生任何副作用。访问属性或计算调用方法的结果通常会改变程序的状态,从而掩盖你试图调试的问题,因此,使用 frame variable (f v) 命令是进行粗略调查的最安全选择。
expression 命令是 p 和 po 的别名,这是调试时经常使用的操作。两者之间的区别在于,p 使用内置的 LLDB 数据格式化程序,而 po 调用由开发人员提供的创建该对象表示形式的代码,例如Swift中的debugDescription方法。 如果没有自定义表示形式,则po命令将退回到p命令提供的表示形式上。
如果要使用默认的
LLDB格式,请使用p;如果要使用类型的实现来控制表示,请使用po。
获取 Backtrace
backtrace(回溯) 是当前活动的函数调用的列表。通过使用 thread backtrace (bt) 命令,你可以更清楚地推断导致程序处于当前状态的事件链。
调用不带参数的thread backtrace命令会生成当前线程的backtrace。
1 | (lldb) thread backtrace |
可以将整数作为参数传递给thread backtrace命令,以限制显示的frames(帧数)。
或者,以all作为参数调用thread backtrace命令会产生所有线程的完整backtrace。
线程清单
程序通常跨多个线程执行代码。要获取进程中所有当前线程的列表,请使用thread list命令。
学习博客
FrizzleFur/DailyLearning/开发工具Tool🔧/