什么是 LLDB?
LLDB(Low Level Debugger) is a next generation, high-performance debugger. It is built as a set of reusable components which highly leverage existing libraries in the larger LLVM Project, such as the Clang expression parser and LLVM disassembler.
LLDB is the default debugger in Xcode on Mac OS X and supports debugging C, Objective-C and C++ on the desktop and iOS devices and simulator.
All of the code in the LLDB project is available under the standard LLVM License, an open source "BSD-style" license.
基础
LLDB 是一个有着 REPL 的特性的调试器,这说明他和Linux数据库有一样的"套路"。当一般不是手动把help
命令禁止掉的时候,就可以通过help
查找所有想要使用的命令。
现在iOS
的xcode
,安卓
的android studio
都有LLDB模块支持。学好LLDB,各种难搞的问题都能找到一些可以原由。
你可能还注意到了,结果中有个 $3。 实际上你可以使用它来指向这个结果。试试 print $3 + 1
,你会看到 100。任何以美元符开头的东西都是存在于 LLDB 的命名空间的,它们是为了帮助你进行调试而存在的。
expression
如果想改变一个值怎么办?你或许会猜 modify。其实这时候我们要用到的是 expression 这个方便的命令。
这不仅会改变调试器中的值,实际上它改变了程序中的值。从现在开始,我们将会偷懒分别以p
和e
来代替print
和expression
。
什么是 print 命令
考虑一个有意思的表达式:p count = 18。如果我们运行这条命令,然后打印 count 的内容。我们将看到它的结果与 expression count = 18 一样。
和 expression 不同的是,print 命令不需要参数。比如 e -h +17 中,你很难区分到底是以 -h 为标识,仅仅执行 +17 呢,还是要计算 17 和 h 的差值。连字符号确实很让人困惑,你或许得不到自己想要的结果。
幸运的是,解决方案很简单。用 -- 来表征标识的结束,以及输入的开始。如果想要 -h 作为标识,就用 e -h -- +17,如果想计算它们的差值,就使用 e -- -h +17。因为一般来说不使用标识的情况比较多,所以 e -- 就有了一个简写的方式,那就是 print。
输入 help print
'print' is an abbreviation for 'expression --'.
(print是 `expression --` 的缩写)
打印对象
(lldb) p @[ @"foo", @"bar" ]
(NSArray *) $8 = 0x00007fdb9b71b3e0 @"2 objects"
实际上,我们想看的是对象的 description 方法的结果。我么需要使用 -O (字母 O,而不是数字 0) 标志告诉 expression 命令以 对象 (Object) 的方式来打印结果。
(lldb) e -O -- $8
<__NSArrayI 0x7fdb9b71b3e0>(
foo,
bar
)
幸运的是,e -o -- 有也有个别名,那就是 po (print object 的缩写),我们可以使用它来进行简化:
(lldb) po $8
<__NSArrayI 0x7fdb9b71b3e0>(
foo,
bar
)
(lldb) po @"lunar"
lunar
(lldb) p @"lunar"
(NSString *) $13 = 0x00007fdb9d0003b0 @"lunar"
Thread Return
调试时,还有一个很棒的函数可以用来控制程序流程:thread return 。它有一个可选参数,在执行时它会把可选参数加载进返回寄存器里,然后立刻执行返回命令,跳出当前栈帧。这意味这函数剩余的部分不会被执行。这会给 ARC 的引用计数造成一些问题,或者会使函数内的清理部分失效。但是在函数的开头执行这个命令,是个非常好的隔离这个函数,伪造返回值的方式 。
断点
我们都把断点作为一个停止程序运行,检查当前状态,追踪 bug 的方式。但是如果我们改变和断点交互的方式,很多事情都变成可能。
想象把断点放在函数的开头,然后用 thread return 命令重写函数的行为,然后继续。想象一下让这个过程自动化,听起来不错,不是吗?
创建断点
在上面的例子中,我们通过在源码页面器的滚槽 16 上点击来创建断点。你可以通过把断点拖拽出滚槽,然后释放鼠标来删除断点 (消失时会有一个非常可爱的噗的一下的动画)。你也可以在断点导航页选择断点,然后按下删除键删除。
要在调试器中创建断点,可以使用breakpoint set
命令。
(lldb) breakpoint set -f main.m -l 16
Breakpoint 1: where = DebuggerDance`main + 27 at main.m:16, address = 0x000000010a3f6cab
也可以在一个符号 (C 语言函数) 上创建断点,而完全不用指定哪一行
(lldb) b isEven
Breakpoint 3: where = DebuggerDance`isEven + 16 at main.m:4, address = 0x000000010a3f6d00
(lldb) br s -F isEven
Breakpoint 4: where = DebuggerDance`isEven + 16 at main.m:4, address = 0x000000010a3f6d00
如果你 Xcode 的 UI 上右击任意断点,然后选择 "Edit Breakpoint" 的话,会有一些非常诱人的选择。
你也可以添加多个行为,可以是调试器命令,shell 命令,也可以是更直接的打印:
UI操作
更新UI
(lldb) e id $myView = (id)0x7f82b1d01fd0
然后在调试器中改变它的背景色:
(lldb) e (void)[$myView setBackgroundColor:[UIColor blueColor]]
但是只有程序继续运行之后才会看到界面的变化。因为改变的内容必须被发送到渲染服务中,然后显示才会被更新。
渲染服务实际上是一个另外的进程 (被称作 backboardd)。这就是说即使我们正在调试的内容所在的进程被打断了,backboardd 也还是继续运行着的。
这意味着你可以运行下面的命令,而不用继续运行程序:
(lldb) e (void)[CATransaction flush]
Push 一个 View Controller
(lldb) e id $nvc = [[[UIApplication sharedApplication] keyWindow] rootViewController]
(lldb) e id $vc = [UIViewController new]
(lldb) e (void)[[$vc view] setBackgroundColor:[UIColor yellowColor]]
(lldb) e (void)[$vc setTitle:@"Yay!"]
(lldb) e (void)[$nvc pushViewContoller:$vc animated:YES]
(lldb) caflush // e (void)[CATransaction flush]
查找按钮的 target
(lldb) po [$myButton allTargets]
{(
<MagicEventListener: 0x7fb58bd2e240>
)}
(lldb) po [$myButton actionsForTarget:(id)0x7fb58bd2e240 forControlEvent:0]
<__NSArrayM 0x7fb58bd2aa40>(
_handleTap:
)
观察实例变量的变化
假设你有一个 UIView,不知道为什么它的 _layer 实例变量被重写了 (糟糕)。因为有可能并不涉及到方法,我们不能使用符号断点。相反的,我们想监视什么时候这个地址被写入。
首先,我们需要找到 _layer 这个变量在对象上的相对位置:
(lldb) p (ptrdiff_t)ivar_getOffset((struct Ivar *)class_getInstanceVariable([MyView class], "_layer"))
(ptrdiff_t) $0 = 8
现在我们知道 ($myView + 8) 是被写入的内存地址:
(lldb) watchpoint set expression -- (int *)$myView + 8
Watchpoint created: Watchpoint 3: addr = 0x7fa554231340 size = 8 state = enabled type = w
new value: 0x0000000000000000
这被以 wivar $myView _layer 加入到 Chisel 中。
非重写方法的符号断点
假设你想知道 -[MyViewController viewDidAppear:] 什么时候被调用。如果这个方法并没有在MyViewController 中实现,而是在其父类中实现的,该怎么办呢?试着设置一个断点,会出现以下结果
(lldb) b -[MyViewController viewDidAppear:]
Breakpoint 1: no locations (pending).
WARNING: Unable to resolve breakpoint to any actual locations.
因为 LLDB 会查找一个符号,但是实际在这个类上却找不到,所以断点也永远不会触发。你需要做的是为断点设置一个条件 [self isKindOfClass:[MyViewController class]],然后把断点放在 UIViewController 上。正常情况下这样设置一个条件可以正常工作。但是这里不会,因为我们没有父类的实现。
viewDidAppear: 是苹果实现的方法,因此没有它的符号;在方法内没有 self 。如果想在符号断点上使用 self,你必须知道它在哪里 (它可能在寄存器上,也可能在栈上;在 x86 上,你可以在 $esp+4 找到它)。但是这是很痛苦的,因为现在你必须至少知道四种体系结构 (x86,x86-64,armv7,armv64)。想象你需要花多少时间去学习命令集以及它们每一个的调用约定,然后正确的写一个在你的超类上设置断点并且条件正确的命令。幸运的是,这个在 Chisel 被解决了。这被成为 bmessage:
(lldb) bmessage -[MyViewController viewDidAppear:]
Setting a breakpoint at -[UIViewController viewDidAppear:] with condition (void*)object_getClass((id)$rdi) == 0x000000010e2f4d28
Breakpoint 1: where = UIKit`-[UIViewController viewDidAppear:], address = 0x000000010e11533c
LLDB 和 Python
LLDB 有内建的,完整的 Python 支持。在LLDB中输入 script,会打开一个 Python REPL。你也可以输入一行 python 语句作为 script 命令 的参数,这可以运行 python 语句而不进入REPL:
(lldb) script import os
(lldb) script os.system("open http://www.objc.io/")
这样就允许你创造各种酷的命令。把下面的语句放到文件 ~/myCommands.py 中:
def caflushCommand(debugger, command, result, internal_dict):
debugger.HandleCommand("e (void)[CATransaction flush]")
然后再 LLDB 中运行:
command script import ~/myCommands.py
常用命令
frame variable
查看当前栈帧里帧参数和本地变量
register read
查看寄存器的值
register read $arg1 $arg2
查看两个变量
memory read
查看内存地址的命令
(lldb) memory read --size 8 --format A --count 10 0x0000000100004630
memory write
内存值的修改
up down
stack frame 跳栈帧
bt
显示所有的调用栈帧。该命令可用来显示函数的调用顺序。
disassemble --frame
disassemble相关指令用于输出某段程序的汇编代码 汇编当前栈帧的代码。
使用disassemble -b则会输出带字节信息的汇编代码: