LLDB探究

一、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的请移步到文章最下方查看文章相关链接。

与此同时,让我们以在调试器中打印变量来开始我们的旅程吧。

二、基础命令

image.png

这是一个简单加了断点的程序,程序会在这一行停止运行,并且控制台会被打开,允许我们和调试器交互。这时候我们应该打些什么命令呢?

1、帮助 help

最简单命令是help,它会列举出所有的命令。如果你忘记了一个命令是做什么的,或者想知道更多的话,你可以通过help 来了解更多细节,例如help print或者help thread。只需要在控制台上图lldb字样的地方键入help即可。

image.png
2、打印对象 print 和 po

打印值很简单;只要试试print命令:

image.png

LLDB 实际上会作前缀匹配。所以你也可以使用prinpri,或者p。但你不能使用pr,因为 LLDB 不能消除process的歧义 (幸运的是p并没有歧义)。而print则是expression --的简写方式。

你可能还注意到了,结果中有个$0。实际上你可以使用它来指向这个结果。试试print $0 + 7,你会看到 130。任何 $ 符开头的东西都是存在于 LLDB 的命名空间的,它们是为了帮助你进行调试而存在的。

打印复杂对象时,print可能显得力不从心 ,我们想看的是对象的 description 方法的结果,这时可以使用popo其实是e -o --的别名。

甚至可以给print 指定不同的打印格式。它们都是以print/或者简化的p/格式书写。

//默认的格式
(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这个方便的命令。

image.png

上图中修改了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]
; layer = >
   | >
   |    | >
   |    |    | >
   |    |    |    | >
更新UI

就像上文变量中提到那样,我们可以拿到这个view:

(lldb) expression id $myView = (id)0x7fb3fbe0a090

尝试做一些修改:

(lldb) expression (void)[$myView setBackgroundColor:[UIColor redColor]]

但是只有程序继续运行之后才会看到界面的变化。因为改变的内容必须被发送到渲染服务中,然后显示才会被更新。

渲染服务实际上是一个另外的进程 (被称作 backboardd)。这就是说即使我们正在调试的内容所在的进程被打断了,backboardd 也还是继续运行着的。

这意味着你可以运行下面的命令,而不用继续运行程序:

(lldb) expression (void)[CATransaction flush]

这个时候就能看到背景颜色的改变了。

五、流程控制

通过xcode加断点调试时,调试条上回出现四个可以控制程序执行流程的按钮:

image.png

从左到右分别是 continue program execution 、 step over 、 step into 和 step out。

1、 continue program execution 按钮,会取消程序的暂停,允许程序正常执行 (要么一直执行下去,要么到达下一个断点)。

在 LLDB 中,你可以使用process continue或者thread continue命令来达到同样的效果。

2、 step over 按钮,会以黑盒的方式执行一行代码。如果所在这行代码是一个函数调用,那么就不会跳进这个函数,而是会执行这个函数,然后继续。

LLDB 则可以使用 thread step-overnext,或者 n 命令。

3、 step in 按钮,可以跳进一个函数调用来调试或者检查程序的执行情况。

在LLDB中使用 thread step-instep,或者 s 命令。注意,当前行不是函数调用时,nextstep 效果是一样的。

4、step out 按钮 ,如果你曾经不小心跳进一个函数,但实际上你想跳过它,常见的反应是重复的运行 n 直到函数返回。其实这种情况,step out 按钮是你的救世主。它会继续执行到下一个返回语句 (直到一个堆栈帧结束) 然后再次停止。

在LLDB中使用 thread step-out 命令。

六、断点

Xcode在断点导航中提供了一系列工具创建和管理断点,我们可以来看LLDB中等价的命令,主要是breakpoint命令。

1、查看 启用/禁用
image.png

上图是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行,正是我们代码所在的位置。

文章相关链接

深入了解GDB和LLDB
Chisel的初步使用

你可能感兴趣的:(LLDB探究)