LLDB快速入门
最近准备重拾C++,整理一下LLDB的快速入门Tutorial,内容来源LLDB官网。
1. 命令结构
不同于gdb的自由形式的命令,lldb的命令都是高度结构化的。所有的lldb命令都具有以下形式:
<noun> <verb> [-options [option-value]] [argument [argument...]] |
命令行解析是在命令执行之前完成的,因此所有命令的形式都是统一的。基本命令的命令语法是非常简单的,所有的参数,选项和选项值都用空格隔开,如果你的参数中包含空格,那么请用双引号将参数括起来。如果你的参数中有双引号或者反斜杠,那么请在它们之前加一个反斜杠来转义。
你可以在一条命令的任何地方加入一个选项,但如果参数以“-”开头,那么你需要添加一个选项终止符“--”(option termination),来告诉lldb所有的选项已经指定完毕了。比如,你想启动一个线程,并且给process launch
命令指定一个-stop-at-entry
选项,同时你希望给这个线程在启动时传入一些参数-progran_arg value
,那么你需要输入:
(lldb) process launch --stop-at-entry -- -program_arg value |
lldb的设计者希望减少具有特殊目的的参数解析器的个数,这样有时会强制用户在输入命令时必须明确指明他们的意图。比如,在gdb中,你可能会使用以下命令:
(gdb) break foo.c:12 |
在foo.c的第12行设置一个断点,并且通过:
(gdb) break foo |
在foo
函数上设置断点。那么解析器会解释为foo.c::foo
的foo
的foo.c:12
(也就是foo.c文件中的foo
函数),这样非常复杂且匪夷所思。而且特别是在C++中,有时候你无法通过这种方式在函数上设置断点。lldb的命令可能比较啰嗦,但语义更明确,并且支持自动补全。
在lldb中,如果你想在某个文件的某一行设置一个断点,那么你可以使用以下命令:
(lldb) breakpoint set --file foo.c --line 12 |
如果想要在名为foo
的函数上设置断点,你可以输入:
(lldb) breakpoint set --name foo |
同样的,你可以多次使用-name
选项来在一些函数函数设置断点。
(lldb) breakpoint set --name foo --name bar |
在LLDB中,你还可以通过指定方法名来在函数上设置断点。例如,加入你需要在所有名为foo
的方法上设置断点,你可以输入:
(lldb) breakpoint set --method foo |
你还可以通过“-shlib
(lldb) breakpoint set --shlib foo.dylib --name foo |
你同样可以重复-shlib
选项来指定多个共享库。
类似gdb,lldb的命令解释器也提供了很多命令的缩写版本,比如下面两条命令就是等价的:
(lldb) breakpoint set -n "-[SKTGraphicView alignLeftEdges:]" |
lldb还支持对源文件名,符号名,文件名等等进行自动补全。按下TAB
键即可完成自动补全,不同的选项有不同的补全方式。例如,breakpoint
命令中的“–file
lldb中每个命令都有详细的帮助文档。可以使用help
命令获取可用命令的概述,或者获取特定命令的详细信息:
(lldb) help |
还有一个apropos
命令,它可以用来搜索和某个单词相近的所有命令的帮助文档。例如:
(lldb) apropos break |
将搜索与break
相近的命令。
最后,还有一种为常用命令构造别名的机制。例如,如果你不想每次都输入:
(lldb) breakpoint set --file foo.c --line 12 |
那么你可以:
(lldb) command alias bfl breakpoint set -f %1 -l %2 |
lldb的设计者已经添加了一些常用命令的别名,比如step
,next
和continue
等等,但不会事无巨细地为每一条命令都添加别名。因为根据经验,只为基本命令设置长度为1到2个字符的别名是最佳实践。
尽管如此,lldb还是支持用户自定义lldb的命令集。lldb会在每次启动时读取~/.lldbinit
文件,你可以在里面存储你的别名设置。你设置的别名也会记录在帮助命令中,这样可以帮助提醒你已经设置了哪些别名。
需要注意的是,根据一个主流的需求,lldb包括了一个gdb的break
命令的弱仿真器。它不会尝试去做gdb的break
命令所做的一切(例如,它不会处理foo.c::bar
,但它基本上是有效的,这样使得从gdb过渡到lldb更容易)。此外,它的别名为b
。如果你想学习lldb的原生命令集,这意味着它将妨碍其他断点命令。解决方案是,如果你不喜欢预设的别名,你可以运行:
(lldb) command unalias b |
同时运行:
(lldb) command b breakpoint |
这样你就可以通过别名b
来运行原生的llvm断点命令breakpoint
了。
lldb命令解析器还支持”原始“命令,当命令的所有选项指定完毕后,命令字符串的其余部分不经解释就被传递给该命令。这对于那些参数可能是一些复杂表达式的命令很方便,但反斜杠可能会让这些命令很难用。例如,表达式命令是原始命令。命令的帮助输出会告诉你它是否是原始的,这样你就知道该如何使用。必须注意的一件事是,由于原始命令仍然可以有选项,如果你的命令字符串中有破折号,你必须通过将它们放在命令名称之后,命令字符串之前,来表明这些不是选项。
lldb还有一个内置的Python解释器,可以通过script
命令来使用。所有的调试器函数在这个Python解释器中都作为一个类提供,所以对于gdb中的一些复杂命令,你可以在lldb中通过编写Python函数来完成。
在概述了lldb命令语法之后,我们继续介绍标准调试会话的各个阶段。
2. 使用LLDB加载程序
首先我们设置需要调试的程序。你可以指定你想要调试的文件来进入lldb:
lldb /Projects/Sketch/build/Debug/Sketch.app |
或者可以在lldb中通过file
命令:
lldb |
3. 设置断点
我们此前已经讨论过如何设置断点了,你可以使用help breakpoint
来查看breakpoint
命令支持的所有选项。例如,我们可能会:
(lldb) breakpoint set --selector alignLeftEdges: |
你可以查看所有已经设置的断点:
(lldb) breakpoint list |
注意设置断点创建的是逻辑断点,因此可以会找到一个或多个位置。例如,在selector
上设置断点可能会导致程序中所有实现了selector
的地方都命中断点。类似的,如果你的某个文件在可执行文件中多次被内联展开,那么在这个文件的某一行设置断点可能会导致在可执行文件的多个位置都命中断点。
逻辑断点都有一个整数编号,并且它们的具体位置也会产生子编号,子编号用”.“连接。比如例子中的1.1。
逻辑断点会保持实时更新,如果一个共享库被加载进来,而它包括了一个”alignLeftEdges“的selector
实现,那么新的断点位置未被添加到断点1下面,如1.2。
breakpoint list
的另一个作用是告诉我们某个断点是否被定位到了。当位置对应的文件地址加载到正在调试的程序中时,该位置就得到了解析。例如,如果你在共享库中设置了一个断点,然后该共享库被卸载,该断点位置将保留,但将不再解析。
对于gdb用户,还需要注意lldb像gdb那样:
(gdb) set breakpoint pending on |
也就是说,即使找不到任何与所指定相匹配的位置,LLDB也将始终根据你的指定来设置断点。你可以通过检查“断点列表”中的“位置”字段来判断断点表达式是否已解析出对应的位置,lldb会将该断点设置为”等待“状态如果无法解析出断点的位置:
(lldb) breakpoint set --file foo.c --line 12 |
你可以删除、禁用根据逻辑断点生成的任一或所有位置,或设置条件和忽略计数。例如,如果我们想要添加一条命令,作用是在命中断点时打印回溯(backtrace)信息,我们可以:
(lldb) breakpoint command add 1.1 |
默认情况下,breakpoint
命令下的add
命令接收一个lldb命令行中的命令。你通过传入--command
选项来明确这一点。如果你想使用Python脚本来实现你的breakpoint
命令,请使用--script
选项。
这是带来lldb命令帮助的另一个特性的方便之处。执行以下命令:
(lldb) help break command add |
当你看到在语法中使用尖括号(如
(lldb) help <breakpt-id> <breakpt-id> -- Breakpoint ID's consist major and |
4. 断点名称
一个断点包含两组相互「正交(orthogonal)」的信息:一组指出在何处设置断点,另一组则决定了当命中断点时应该执行什么操作。后者又称为断点选项,例如命令(commands),条件(conditions),命中计数(hit-count),自动恢复(auto-continue)等。
有时我们可能向对一些断点都应用一组选项,例如,你可能想要在某些方法的断点命中时,检查是否self==nil
,如果该表达式为真,那么打印回溯信息并继续执行。一个简单的方法是,对于所有的断点指定下列选项:
(lldb) breakpoint modify -c "self == nil" -C bt --auto-continue 1 2 3 |
这是一种解决方案,但每次你设置新的断点时,你都要重复执行一次这条命令,并且如果你想要改变选项,你还必须记住你所有已经使用过的选项。
断点名称(breakpoint names)为这个问题提供了一种更好的解决方案。你可以将你想要设置的那些断点加入一个组中,只需要为它们设置相同的名字即可,如:
(lldb) breakpoint set -N SelfNil |
这样一来,当你设置好所有的断点后,只需要指定它们的名字,即可同时修改这些断点的选项。
(lldb) breakpoint modify -c "self == nil" -C bt --auto-continue SelfNil |
这种方案好些了,但仍存在一些问题,当你添加新的断点时,新的断点不会应用这些修改,并且这些选项只存在于实际断点的上下文中,所以它们很难保存和重用。
一个更好的方法是创建一个完整的配置断点名(configured breakpoint name):
(lldb) breakpoint name configure -c "self == nil" -C bt --auto-continue SelfNil |
名称和断点之间的连接是实时更新的,所以当你更改了名称对应的选项后,所有的断点都会应用这些修改
你可以在你的.lldbinit文件中设置断点名,以达到重用的效果。
你还可以在一个端点的选项中设置一个断点,
(lldb) breakpoint name configure -B 1 SelfNil |
这使得将操作从一个断点复制到一组其他断点很容易。
5. 设置观察点
除了断点之外,你可以使用帮助命令来查看所有的观察点(watchpoint)相关的操作。比如,我们想要观察一个名为global
的变量的写操作,但只有当满足global==5
的条件时调试器才停下来:
(lldb) watch set var global |
6. 启动或连接到你的程序
要启动一个程序我们可以使用process launch
或者内置的别名:
(lldb) process launch |
你也可以通过进程ID或进程名连接到一个进程。当使用进程名连接到进程时,lldb也支持--waitfor
选项,该选项可以让lldb等待直到下一个具有该进程名的进程出现。
(lldb) process attach --pid 123 |
当你启动或连接到一个进程后,你的进程可能毁在某些地方暂停:
(lldb) process attach -p 12345 |
请注意”1 of 3 threads stopped with reasons:“这一行以及它的下一行。在多线程环境中,内核的控制权返回到调试器之前,很可能有多个进程命中你的断点。这种情况下,你可以看到所有的进程是由于什么原因而暂停了。
7. 控制你的程序
程序启动后,我们可以恢复运行直到命中断点。所有用于控制进程的原始命令都位于thread
命令下:
(lldb) thread continue |
目前,你一次只能操作一个线程,但最终的设计将支持在线程1中跳过函数,在线程2中进入函数,然后继续线程3,等等。为了方便起见,所有的步进命令(stepping commands)都有简单的别名,比如thread continue
的别名是c
,等等。
(lldb) thread step-in // The same as gdb's "step" or "s" |
其他的一些步进命令和gdb中的大同小异。你可以使用:
(lldb) thread step-in // The same as gdb's "step" or "s" |
默认情况下,lldb已经为所有的普通的gdb进程控制命令定义了别名(“s”,“step”,“n”,“next”, “finish”)。你可以在你的~/.lldbinit文件中使用command alias
命令来添加更多的别名。
lldb同样支持指令级的步进:
(lldb) thread step-inst // The same as gdb's "stepi" / "si" |
最后,lldb支持程序运行直到退出步进模式:
(lldb) thread until 100 |
这个命令将运行当前帧中的线程,直到它达到这一帧的第100行,如果它离开当前帧就停止。这非常类似于gdb的until
命令。
一个进程在默认情况下会和inferior process共享lldb的终端。在这种模式中,当进程正在运行时,你输入的任何东西都将进入inferior process的标准输入(STDIN)中,就像在gdb中调试那样。你可以输入CTRL+C来中断你的inferior程序。
关于inferior的定义,来自GDB官网:
GDB represents the state of each program execution with an object called an inferior. An inferior typically corresponds to a process, but is more general and applies also to targets that do not have processes. Inferiors may be created before a process runs, and may be retained after a process exits. Inferiors have unique identifiers that are different from process ids. Usually each inferior will also have its own distinct address space, although some embedded targets may have several inferiors running in different parts of a single address space. Each inferior may in turn have multiple threads running in it.
如果你连接到了一个进程,或者使用--no-stdin
选项启动了一个进程,命令解释器始终能够接收输入命令。总是有一个(lldb)提示符可能会让gdb用户感到不安。这样可以允许你随时设置一个端点,而不必在你调试时中断程序:
(lldb) process continue |
有一些命令在程序运行时不可用,命令解释器应该很好地让你知道这种情况。如果你发现任何命令解释器不工作的情况,请提交一个bug。这种操作方式将为我们提供一种称为以线程为中心的调试模式。这种模式将允许我们运行所有线程,只停止处于断点或有异常或信号的线程。
当前支持运行中执行的命令包括中断进程停止执行(process interrupt
),获取进程状态(process status
),断点设置和清除(breakpoint [set|clear|enable|disable|list] ...
),以及内存读写(memory [read|write]
)。
借着在运行时禁用stdio的问题,我们介绍一下如何设置调试器的属性。如果你想始终运行在--no-stdin
的模式下,你可以通过lldb的setting
命令将其设置为通用的进程属性,该命令和gdb的set
命令如出一辙。例如,在上述例子中你可以:
(lldb) settings set target.process.disable-stdio true |
随着时间的推移,gdb的set命令变成了混乱的选项,因此有一些有用的选项,即使是有经验的gdb用户也不知道,因为它们太难找到了。我们试图使用调试器中基本实体的结构来分层组织设置。在大多数情况下,在可以指定通用实体(例如线程)设置的任何地方,也可以将该选项应用于特定实例,这有时也很方便。您可以通过settings list
查看可用的设置,设置命令解释有帮助
8. 检查线程状态
一个线程停止后,LLDB将选择一个当前线程和该线程中的一个当前帧(总是最底部的帧),这个当前线程通常是“出于某种原因”停止的线程。许多检查线程状态的命令都工作与当前线程上。
要检查你的进程的当前状态,你可以通过thread
命令:
(lldb) thread list |
*
表明线程1是当前的线程。要获取该线程的回溯信息,执行:
(lldb) thread backtrace |
你也可以提供一个线程列表来查询回溯信息,或者使用关键词all
来查看所有的线程:
(lldb) thread backtrace all |
你可以选择当前线程,被选择的线程将会默认用于执行所有的命令,选择线程的方法是通过thread select
命令:
(lldb) thread select 2 |
线程的下标与thread list
命令列出的线程保持一致。
9. 检查栈帧状态
检查帧的参数和局部变量的最简便的方法是使用frame variable
命令:
(lldb) frame variable |
如你所见,如果你不指定变量名,那么所有局部变量和参数都会展示出来。如果你给frame variable
传递了局部变量的名字,那么只有你指定的变量会被打印出来。
(lldb) frame variable self |
你也可以传入局部变量的子元素的路径,比如:
(lldb) frame variable self.isa |
frame variable
命令不是一个完整的表达式解析器,但它确实支持一些简单的操作符,比如&
,*
,->
,[]
(未被重载的操作符)。数组括号可以用于指针,将指针解释为数组:
lldb) frame variable *self |
frame variable
命令也可以对变量执行”对象打印“操作(当前只支持Objective-C的对象打印),使用的是对象的”描述“方法。向frame variable
命令传入-o
选项来启用这个功能:
(lldb) frame variable -o self (SKTGraphicView *) self = 0x0000000100208b40 <SKTGraphicView: 0x100208b40> |
你也可以通过传入--relative
或-r
选项来上下移动栈帧。并且lldb内置了两个别名u
和d
来像gdb那样实现这两个功能。