一、LLDB 是什么?
LLDB是Mac OS X上Xcode的默认调试器,支持在桌面和iOS设备和模拟器上调试C ,Objective-C和C++。它是新一代高性能调试器,它可以高效利用LLVM项目中的现有库,例如Clang表达式解析器和LLVM反汇编程序。
随着Xcode 5的发布,LLDB调试器已经取代了GDB,成为了Xcode工程中默认的调试器。它与LLVM编译器一起,带给我们更丰富的流程控制和数据检测的调试功能。LLDB为Xcode提供了底层调试环境,其中包括内嵌在Xcode IDE中的位于调试区域的控制面板。
Chisel是facebook下一个开源LLDB命令集合,由于我这里调试的是Xcode自带的LLDB命令,如果想探究Chisel的请移步到文章最下方查看文章相关链接。
与此同时,让我们以在调试器中打印变量来开始我们的旅程吧。
二、基础命令
这是一个简单加了断点的程序,程序会在这一行停止运行,并且控制台会被打开,允许我们和调试器交互。这时候我们应该打些什么命令呢?
1、帮助 help
最简单命令是help
,它会列举出所有的命令。如果你忘记了一个命令是做什么的,或者想知道更多的话,你可以通过help <command-name>
来了解更多细节,例如help print
或者help thread
。只需要在控制台上图lldb字样的地方键入help
即可。
2、打印对象 print 和 po
打印值很简单;只要试试print
命令:
LLDB 实际上会作前缀匹配。所以你也可以使用prin
,pri
,或者p
。但你不能使用pr
,因为 LLDB 不能消除process
的歧义 (幸运的是p
并没有歧义)。而print
则是expression --
的简写方式。
你可能还注意到了,结果中有个$0
。实际上你可以使用它来指向这个结果。试试print $0 + 7
,你会看到 130。任何 $ 符开头的东西都是存在于 LLDB 的命名空间的,它们是为了帮助你进行调试而存在的。
打印复杂对象时,print
可能显得力不从心 ,我们想看的是对象的 description 方法的结果,这时可以使用po
,po
其实是e -o --
的别名。
甚至可以给print 指定不同的打印格式。它们都是以print/<fmt>
或者简化的p/<fmt>
格式书写。
//默认的格式
(lldb) p 16
(int) $3 = 16
//十六进制:
(lldb) p/x 16
(int) $4 = 0x00000010
//二进制 (t 代表 two):
(lldb) p/t 16
(int) $5 = 0b00000000000000000000000000010000
(lldb) p/t (char)16
(char) $6 = 0b00010000
3、修改对象 expression
如果想改变一个值怎么办?我们要用到的是expression
这个方便的命令。
上图中修改了num的值,断点步进后可以看到NSLog的对应值已经发生了变化。
三、变量
现在你已经可以打印对象和简单类型,并且知道如何使用 expression 命令在调试器中修改它们了。现在让我们使用一些变量来减少输入量。就像你可以在 C 语言中用 int a = 0 来声明一个变量一样,你也可以在 LLDB 中做同样的事情。不过为了能使用声明的变量,变量必须以$符开头。
(lldb) e int $a = 2
(lldb) p $a * 19
(int) $1 = 38
(lldb) e NSArray *$array = @[ @"Saturday", @"Sunday", @"Monday" ]
(lldb) p [$array count]
(NSUInteger) $2 = 3
(lldb) po [[$array objectAtIndex:0] uppercaseString]
SATURDAY
(lldb) p (char)[[$array objectAtIndex:$a] characterAtIndex:0]
(char) $4 = 'M'
四、UI调试
因为全局变量是可访问的,可以像这样打印整个视图层级:
(lldb) po [[[UIApplication sharedApplication] keyWindow] recursiveDescription]
<UIWindow: 0x7fb3fbf05b70; frame = (0 0; 375 667); gestureRecognizers = <NSArray: 0x60000165f1b0>; layer = <UIWindowLayer: 0x600001809a80>>
| <UITransitionView: 0x7fb3fbc19e30; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x60000182f320>>
| | <UIDropShadowView: 0x7fb3fbc1a570; frame = (0 0; 375 667); clipsToBounds = YES; autoresize = W+H; layer = <CALayer: 0x60000182f3e0>>
| | | <UIView: 0x7fb3ff116c30; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x600001802e00>>
| | | | <UIView: 0x7fb3fbe0a090; frame = (87 384; 240 128); autoresize = RM+BM; layer = <CALayer: 0x600001835e00>>
更新UI
就像上文变量中提到那样,我们可以拿到这个view:
(lldb) expression id $myView = (id)0x7fb3fbe0a090
尝试做一些修改:
(lldb) expression (void)[$myView setBackgroundColor:[UIColor redColor]]
但是只有程序继续运行之后才会看到界面的变化。因为改变的内容必须被发送到渲染服务中,然后显示才会被更新。
渲染服务实际上是一个另外的进程 (被称作 backboardd)。这就是说即使我们正在调试的内容所在的进程被打断了,backboardd 也还是继续运行着的。
这意味着你可以运行下面的命令,而不用继续运行程序:
(lldb) expression (void)[CATransaction flush]
这个时候就能看到背景颜色的改变了。
五、流程控制
通过xcode加断点调试时,调试条上回出现四个可以控制程序执行流程的按钮:
从左到右分别是 continue program execution 、 step over 、 step into 和 step out。
1、 continue program execution 按钮,会取消程序的暂停,允许程序正常执行 (要么一直执行下去,要么到达下一个断点)。
在 LLDB 中,你可以使用process continue
或者thread continue
命令来达到同样的效果。
2、 step over 按钮,会以黑盒的方式执行一行代码。如果所在这行代码是一个函数调用,那么就不会跳进这个函数,而是会执行这个函数,然后继续。
LLDB 则可以使用 thread step-over
,next
,或者 n
命令。
3、 step in 按钮,可以跳进一个函数调用来调试或者检查程序的执行情况。
在LLDB中使用 thread step-in
,step
,或者 s
命令。注意,当前行不是函数调用时,next
和 step
效果是一样的。
4、step out 按钮 ,如果你曾经不小心跳进一个函数,但实际上你想跳过它,常见的反应是重复的运行 n 直到函数返回。其实这种情况,step out 按钮是你的救世主。它会继续执行到下一个返回语句 (直到一个堆栈帧结束) 然后再次停止。
在LLDB中使用 thread step-out
命令。
六、断点
Xcode在断点导航中提供了一系列工具创建和管理断点,我们可以来看LLDB中等价的命令,主要是breakpoint
命令。
1、查看 启用/禁用
上图是xcode查看断点的地方,点击断点会开启或关闭断点。对应的LLDB如下:
//查看断点 命令输出列表显示每个逻辑断点都有一个整数标识
//输出列表中另一个信息是断点位置是否是已解析的(resolved)。这个标识表示当与之相关的文件地址被加载到程序进行调试时,其位置是已解析的。
//例如,如果在共享库中设置的断点之后被卸载了,则断点的位置还会保留,但其不能再被解析。
(lldb) breakpoint list
Current breakpoints:
1: file = '/Users/qdd7/Documents/OC-Demo/Demo29-master/Demo29-master/main.m', line = 23, exact_match = 0, locations = 1, resolved = 1, hit count = 1
1.1: where = Demo29-master`main + 248 at main.m:23:9, address = 0x0000000109a10728, resolved, hit count = 1
//禁用断点
(lldb) breakpoint disable 1
1 breakpoints disabled.
//启用断点
(lldb) breakpoint enable 1
1 breakpoints enabled.
2、 创建/删除
在Xcode创建断点的方式一种是 直接在代码左边的行数出点击 即可创建断点。对应的LLDB如下:
//在main.m的第24行创建断点
(lldb) breakpoint set -f main.m -l 24
Breakpoint 2: where = Demo29-master`main + 264 at main.m:28:5, address = 0x0000000109a10738
//删除刚才的断点
(lldb) breakpoint delete 2
1 breakpoints deleted; 0 breakpoint locations disabled.
七、查看 线程/调用栈 状态
在进程停止后,LLDB会选择一个当前线程和线程中当前帧(frame)。很多检测状态的命令可以用于这个线程或帧。
为了检测进程的当前状态,可以从以下命令thread list
开始:
(lldb) thread list
Process 13180 stopped
* thread #1: tid = 0x3cd30e, 0x00007fff5182322a libsystem_kernel.dylib`mach_msg_trap + 10, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP
thread #2: tid = 0x3cd499, 0x00007fff51824bfe libsystem_kernel.dylib`__workq_kernreturn + 10
thread #5: tid = 0x3cd49a, 0x00007fff51824bfe libsystem_kernel.dylib`__workq_kernreturn + 10
...
星号(*)表示thread #1为当前线程。为了获取线程的跟踪栈,可以使用以下命令thread backtrace
:
//默认为当前线程 也可以指定线程 : thread backtrace 2
(lldb) thread backtrace
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP
* frame #0: 0x00007fff5182322a libsystem_kernel.dylib`mach_msg_trap + 10
frame #1: 0x00007fff5182376c libsystem_kernel.dylib`mach_msg + 60
frame #2: 0x00007fff23b0caf5 CoreFoundation`__CFRunLoopServiceMachPort + 197
...
如果想查看所有线程的调用栈,则可以使用以下命令:thread backtrace all
检查帧参数和本地变量的最简便的方式是使用frame variable
命令:
(lldb) frame variable
(ViewController *) self = 0x00007fb7e180a5f0
(SEL) _cmd = "viewDidLoad"
(NSUInteger) num = 123
(__NSCFConstantString *) str = 0x000000010c6590b0 @"learning LLDB"
(__NSArrayI *) arr = 0x00006000036fede0 @"2 elements"
如果没有指定任何变量名,则会显示所有参数和本地变量。如果指定参数名或变量名,则只打印指定的值。如:
(lldb) frame variable self
(ViewController *) self = 0x00007fb7e180a5f0
image
image指令是target module指令的缩写,借助它我们能够查看当前的Binary Images相关的信息。日常开发我们主要利用它寻址。image
命令的用法也挺多,首先可以用它来查看工程中使用的库,如下所示:
(lldb) image list
[ 0] 4EA7C9AA-212E-3B57-9A3D-B78FB0DBC3E7 0x000000010c656000 /Users/qdd7/Library/Developer/Xcode/DerivedData/Demo29-master-bsvyynbgkgcozefqlgdaouuznpzi/Build/Products/Debug-iphonesimulator/Demo29-master.app/Demo29-master
[ 1] CE635DB2-D47E-3C05-A0A3-6BD982E7E750 0x0000000114088000 /usr/lib/dyld
[ 2] 32684ACA-A9FF-35E2-BB46-62CFF84251FE 0x000000010c662000 /Users/qdd7/Documents/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib/dyld_sim
...
我们还可以用它来查找可执行文件或共享库的原始地址,这一点还是很有用的,当我们的程序崩溃时,我们可以使用这条命令来查找崩溃所在的具体位置,如下所示:
NSArray *array = @[@1,@2];
NSLog(@"array 3 : %@",array[2]);
// 以下是异常代码
2019-10-17 15:54:53.153364+0800 Demo29-master[13218:3994766] *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayI objectAtIndexedSubscript:]: index 2 beyond bounds [0 .. 1]'
*** First throw call stack:
(
0 CoreFoundation 0x00007fff23baa1ee __exceptionPreprocess + 350
1 libobjc.A.dylib 0x00007fff50864b20 objc_exception_throw + 48
2 CoreFoundation 0x00007fff23c3cb71 _CFThrowFormattedException + 194
3 CoreFoundation 0x00007fff23c1b30d -[__NSArrayI objectAtIndexedSubscript:] + 93
4 Demo29-master 0x0000000100a16121 -[ViewController viewDidLoad] + 273
5 UIKitCore 0x00007fff46f03d96 -[UIViewController _sendViewDidLoadWithAppearanceProxyObjectTaggingEnabled] + 83
6 UIKitCore 0x00007fff46f08cef -[UIViewController loadViewIfRequired] + 1084
...
根据以上信息,我们可以判断崩溃位置是在ViewController中,要想知道具体在哪一行,可以使用以下命令image lookup --address
:
(lldb) image lookup --address 0x0000000100a16121
Address: Demo29-master[0x0000000100001121] (Demo29-master.__TEXT.__text + 273)
Summary: Demo29-master`-[ViewController viewDidLoad] + 273 at ViewController.m:32
可以看到,最后定位到了ViewController.m:32行,正是我们代码所在的位置。