最近准备重拾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::foofoofoo.c:12(也就是foo.c文件中的foo函数),这样非常复杂且匪夷所思。而且特别是在C++中,有时候你无法通过这种方式在函数上设置断点。lldb的命令可能比较啰嗦,但语义更明确,并且支持自动补全。

在lldb中,如果你想在某个文件的某一行设置一个断点,那么你可以使用以下命令:

(lldb) breakpoint set --file foo.c --line 12
(lldb) breakpoint set -f foo.c -l 12

如果想要在名为foo的函数上设置断点,你可以输入:

(lldb) breakpoint set --name foo
(lldb) breakpoint set -n foo

同样的,你可以多次使用-name选项来在一些函数函数设置断点。

(lldb) breakpoint set --name foo --name bar

在LLDB中,你还可以通过指定方法名来在函数上设置断点。例如,加入你需要在所有名为foo的方法上设置断点,你可以输入:

(lldb) breakpoint set --method foo
(lldb) breakpoint set -M foo

你还可以通过“-shlib ”(或该选项的缩写“-s ”)选项来指定共享库,从而限制断点的位置:

(lldb) breakpoint set --shlib foo.dylib --name foo
(lldb) breakpoint set -s foo.dylib -n foo

你同样可以重复-shlib选项来指定多个共享库。

类似gdb,lldb的命令解释器也提供了很多命令的缩写版本,比如下面两条命令就是等价的:

(lldb) breakpoint set -n "-[SKTGraphicView alignLeftEdges:]"
(lldb) br s -n "-[SKTGraphicView alignLeftEdges:]"

lldb还支持对源文件名,符号名,文件名等等进行自动补全。按下TAB键即可完成自动补全,不同的选项有不同的补全方式。例如,breakpoint命令中的“–file ”选项会补全源文件的文件名,而“–shlib ”选项会加载共享库等等。lldb甚至还支持限制范围的补全,如果你指定了”shlib “选项,并且正在使用”-file “的自动补全,那么lldb将会只列出通过”shlib “选项指定的共享库中的源文件。

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) bfl foo.c 12

lldb的设计者已经添加了一些常用命令的别名,比如stepnextcontinue等等,但不会事无巨细地为每一条命令都添加别名。因为根据经验,只为基本命令设置长度为1到2个字符的别名是最佳实践。

尽管如此,lldb还是支持用户自定义lldb的命令集。lldb会在每次启动时读取~/.lldbinit文件,你可以在里面存储你的别名设置。你设置的别名也会记录在帮助命令中,这样可以帮助提醒你已经设置了哪些别名。

需要注意的是,根据一个主流的需求,lldb包括了一个gdb的break命令的弱仿真器。它不会尝试去做gdb的break命令所做的一切(例如,它不会处理foo.c::bar,但它基本上是有效的,这样使得从gdb过渡到lldb更容易)。此外,它的别名为b。如果你想学习lldb的原生命令集,这意味着它将妨碍其他断点命令。解决方案是,如果你不喜欢预设的别名,你可以运行:

(lldb) command unalias b

同时运行:

(lldb) command alias b breakpoint

这样你就可以通过别名b来运行原生的llvm断点命令breakpoint了。

lldb命令解析器还支持”原始“命令,当命令的所有选项指定完毕后,命令字符串的其余部分不经解释就被传递给该命令。这对于那些参数可能是一些复杂表达式的命令很方便,但反斜杠可能会让这些命令很难用。例如,表达式命令是原始命令。命令的帮助输出会告诉你它是否是原始的,这样你就知道该如何使用。必须注意的一件事是,由于原始命令仍然可以有选项,如果你的命令字符串中有破折号,你必须通过将它们放在命令名称之后,命令字符串之前,来表明这些不是选项。

lldb还有一个内置的Python解释器,可以通过script命令来使用。所有的调试器函数在这个Python解释器中都作为一个类提供,所以对于gdb中的一些复杂命令,你可以在lldb中通过编写Python函数来完成。

在概述了lldb命令语法之后,我们继续介绍标准调试会话的各个阶段。

2. 使用LLDB加载程序

首先我们设置需要调试的程序。你可以指定你想要调试的文件来进入lldb:

$ lldb /Projects/Sketch/build/Debug/Sketch.app
Current executable set to '/Projects/Sketch/build/Debug/Sketch.app' (x86_64).

或者可以在lldb中通过file命令:

$ lldb
(lldb) file /Projects/Sketch/build/Debug/Sketch.app
Current executable set to '/Projects/Sketch/build/Debug/Sketch.app' (x86_64).

3. 设置断点

我们此前已经讨论过如何设置断点了,你可以使用help breakpoint来查看breakpoint命令支持的所有选项。例如,我们可能会:

(lldb) breakpoint set --selector alignLeftEdges:
Breakpoint created: 1: name = 'alignLeftEdges:', locations = 1, resolved = 1

你可以查看所有已经设置的断点:

(lldb) breakpoint list
Current breakpoints:
1: name = 'alignLeftEdges:', locations = 1, resolved = 1
1.1: where = Sketch`-[SKTGraphicView alignLeftEdges:] + 33 at /Projects/Sketch/SKTGraphicView.m:1405, address = 0x0000000100010d5b, resolved, hit count = 0

注意设置断点创建的是逻辑断点,因此可以会找到一个或多个位置。例如,在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
Breakpoint created: 2: file ='foo.c', line = 12, locations = 0 (pending)
WARNING: Unable to resolve breakpoint to any actual locations.

你可以删除、禁用根据逻辑断点生成的任一或所有位置,或设置条件和忽略计数。例如,如果我们想要添加一条命令,作用是在命中断点时打印回溯(backtrace)信息,我们可以:

(lldb) breakpoint command add 1.1
Enter your debugger command(s). Type 'DONE' to end.
> bt
> DONE

默认情况下,breakpoint命令下的add命令接收一个lldb命令行中的命令。你通过传入--command选项来明确这一点。如果你想使用Python脚本来实现你的breakpoint命令,请使用--script选项。

这是带来lldb命令帮助的另一个特性的方便之处。执行以下命令:

(lldb) help break command add
Add a set of commands to a breakpoint, to be executed whenever the breakpoint is hit.

Syntax: breakpoint command add <cmd-options> <breakpt-id>
etc...

当你看到在语法中使用尖括号(如)指定的命令参数时,表明这是一种常见的参数类型,你可以从命令系统获得进一步的帮助。

(lldb) help <breakpt-id> <breakpt-id> -- Breakpoint ID's consist major and
minor numbers; the major etc...

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
Watchpoint created: Watchpoint 1: addr = 0x100001018 size = 4 state = enabled type = w
declare @ '/Volumes/data/lldb/svn/ToT/test/functionalities/watchpoint/watchpoint_commands/condition/main.cpp:12'
(lldb) watch modify -c '(global==5)'
(lldb) watch list
Current watchpoints:
Watchpoint 1: addr = 0x100001018 size = 4 state = enabled type = w
declare @ '/Volumes/data/lldb/svn/ToT/test/functionalities/watchpoint/watchpoint_commands/condition/main.cpp:12'
condition = '(global==5)'
(lldb) c
Process 15562 resuming
(lldb) about to write to 'global'...
Process 15562 stopped and was programmatically restarted.
Process 15562 stopped and was programmatically restarted.
Process 15562 stopped and was programmatically restarted.
Process 15562 stopped and was programmatically restarted.
Process 15562 stopped
* thread #1: tid = 0x1c03, 0x0000000100000ef5 a.out`modify + 21 at main.cpp:16, stop reason = watchpoint 1
frame #0: 0x0000000100000ef5 a.out`modify + 21 at main.cpp:16
13
14 static void modify(int32_t &var) {
15 ++var;
-> 16 }
17
18 int main(int argc, char** argv) {
19 int local = 0;
(lldb) bt
* thread #1: tid = 0x1c03, 0x0000000100000ef5 a.out`modify + 21 at main.cpp:16, stop reason = watchpoint 1
frame #0: 0x0000000100000ef5 a.out`modify + 21 at main.cpp:16
frame #1: 0x0000000100000eac a.out`main + 108 at main.cpp:25
frame #2: 0x00007fff8ac9c7e1 libdyld.dylib`start + 1
(lldb) frame var global
(int32_t) global = 5
(lldb) watch list -v
Current watchpoints:
Watchpoint 1: addr = 0x100001018 size = 4 state = enabled type = w
declare @ '/Volumes/data/lldb/svn/ToT/test/functionalities/watchpoint/watchpoint_commands/condition/main.cpp:12'
condition = '(global==5)'
hw_index = 0 hit_count = 5 ignore_count = 0
(lldb)

6. 启动或连接到你的程序

要启动一个程序我们可以使用process launch或者内置的别名:

(lldb) process launch
(lldb) run
(lldb) r

你也可以通过进程ID或进程名连接到一个进程。当使用进程名连接到进程时,lldb也支持--waitfor选项,该选项可以让lldb等待直到下一个具有该进程名的进程出现。

(lldb) process attach --pid 123
(lldb) process attach --name Sketch
(lldb) process attach --name Sketch --waitfor

当你启动或连接到一个进程后,你的进程可能毁在某些地方暂停:

(lldb) process attach -p 12345
Process 46915 Attaching
Process 46915 Stopped
1 of 3 threads stopped with reasons:
* thread #1: tid = 0x2c03, 0x00007fff85cac76a, where = libSystem.B.dylib`__getdirentries64 + 10, stop reason = signal = SIGSTOP, queue = com.apple.main-thread

请注意”1 of 3 threads stopped with reasons:“这一行以及它的下一行。在多线程环境中,内核的控制权返回到调试器之前,很可能有多个进程命中你的断点。这种情况下,你可以看到所有的进程是由于什么原因而暂停了。

7. 控制你的程序

程序启动后,我们可以恢复运行直到命中断点。所有用于控制进程的原始命令都位于thread命令下:

(lldb) thread continue
Resuming thread 0x2c03 in process 46915
Resuming process 46915
(lldb)

目前,你一次只能操作一个线程,但最终的设计将支持在线程1中跳过函数,在线程2中进入函数,然后继续线程3,等等。为了方便起见,所有的步进命令(stepping commands)都有简单的别名,比如thread continue的别名是c,等等。

(lldb) thread step-in    // The same as gdb's "step" or "s"
(lldb) thread step-over // The same as gdb's "next" or "n"
(lldb) thread step-out // The same as gdb's "finish" or "f"

其他的一些步进命令和gdb中的大同小异。你可以使用:

(lldb) thread step-in    // The same as gdb's "step" or "s"
(lldb) thread step-over // The same as gdb's "next" or "n"
(lldb) thread step-out // The same as gdb's "finish" or "f"

默认情况下,lldb已经为所有的普通的gdb进程控制命令定义了别名(“s”,“step”,“n”,“next”, “finish”)。你可以在你的~/.lldbinit文件中使用command alias命令来添加更多的别名。

lldb同样支持指令级的步进:

(lldb) thread step-inst       // The same as gdb's "stepi" / "si"
(lldb) thread step-over-inst // The same as gdb's "nexti" / "ni"

最后,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
(lldb) breakpoint set --name stop_here

有一些命令在程序运行时不可用,命令解释器应该很好地让你知道这种情况。如果你发现任何命令解释器不工作的情况,请提交一个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
Process 46915 state is Stopped
* thread #1: tid = 0x2c03, 0x00007fff85cac76a, where = libSystem.B.dylib`__getdirentries64 + 10, stop reason = signal = SIGSTOP, queue = com.apple.main-thread
thread #2: tid = 0x2e03, 0x00007fff85cbb08a, where = libSystem.B.dylib`kevent + 10, queue = com.apple.libdispatch-manager
thread #3: tid = 0x2f03, 0x00007fff85cbbeaa, where = libSystem.B.dylib`__workq_kernreturn + 10

*表明线程1是当前的线程。要获取该线程的回溯信息,执行:

(lldb) thread backtrace
thread #1: tid = 0x2c03, stop reason = breakpoint 1.1, queue = com.apple.main-thread
frame #0: 0x0000000100010d5b, where = Sketch`-[SKTGraphicView alignLeftEdges:] + 33 at /Projects/Sketch/SKTGraphicView.m:1405
frame #1: 0x00007fff8602d152, where = AppKit`-[NSApplication sendAction:to:from:] + 95
frame #2: 0x00007fff860516be, where = AppKit`-[NSMenuItem _corePerformAction] + 365
frame #3: 0x00007fff86051428, where = AppKit`-[NSCarbonMenuImpl performActionWithHighlightingForItemAtIndex:] + 121
frame #4: 0x00007fff860370c1, where = AppKit`-[NSMenu performKeyEquivalent:] + 272
frame #5: 0x00007fff86035e69, where = AppKit`-[NSApplication _handleKeyEquivalent:] + 559
frame #6: 0x00007fff85f06aa1, where = AppKit`-[NSApplication sendEvent:] + 3630
frame #7: 0x00007fff85e9d922, where = AppKit`-[NSApplication run] + 474
frame #8: 0x00007fff85e965f8, where = AppKit`NSApplicationMain + 364
frame #9: 0x0000000100015ae3, where = Sketch`main + 33 at /Projects/Sketch/SKTMain.m:11
frame #10: 0x0000000100000f20, where = Sketch`start + 52

你也可以提供一个线程列表来查询回溯信息,或者使用关键词all来查看所有的线程:

(lldb) thread backtrace all

你可以选择当前线程,被选择的线程将会默认用于执行所有的命令,选择线程的方法是通过thread select命令:

(lldb) thread select 2

线程的下标与thread list命令列出的线程保持一致。

9. 检查栈帧状态

检查帧的参数和局部变量的最简便的方法是使用frame variable命令:

(lldb) frame variable
self = (SKTGraphicView *) 0x0000000100208b40
_cmd = (struct objc_selector *) 0x000000010001bae1
sender = (id) 0x00000001001264e0
selection = (NSArray *) 0x00000001001264e0
i = (NSUInteger) 0x00000001001264e0
c = (NSUInteger) 0x00000001001253b0

如你所见,如果你不指定变量名,那么所有局部变量和参数都会展示出来。如果你给frame variable传递了局部变量的名字,那么只有你指定的变量会被打印出来。

(lldb) frame variable self
(SKTGraphicView *) self = 0x0000000100208b40

你也可以传入局部变量的子元素的路径,比如:

(lldb) frame variable self.isa
(struct objc_class *) self.isa = 0x0000000100023730

frame variable命令不是一个完整的表达式解析器,但它确实支持一些简单的操作符,比如&*->[](未被重载的操作符)。数组括号可以用于指针,将指针解释为数组:

lldb) frame variable *self
(SKTGraphicView *) self = 0x0000000100208b40
(NSView) NSView = {
(NSResponder) NSResponder = {
...

(lldb) frame variable &self
(SKTGraphicView **) &self = 0x0000000100304ab

(lldb) frame variable argv[0]
(char const *) argv[0] = 0x00007fff5fbffaf8 "/Projects/Sketch/build/Debug/Sketch.app/Contents/MacOS/Sketch"

frame variable命令也可以对变量执行”对象打印“操作(当前只支持Objective-C的对象打印),使用的是对象的”描述“方法。向frame variable命令传入-o选项来启用这个功能:

(lldb) frame variable -o self (SKTGraphicView *) self = 0x0000000100208b40 <SKTGraphicView: 0x100208b40>
You can select another frame to view with the "frame select" command

(lldb) frame select 9
frame #9: 0x0000000100015ae3, where = Sketch`function1 + 33 at /Projects/Sketch/SKTFunctions.m:11

你也可以通过传入--relative-r选项来上下移动栈帧。并且lldb内置了两个别名ud来像gdb那样实现这两个功能。