LLDB使用详解

https://blog.csdn.net/u012907783/article/details/53885818

LLDB的Xcode默认的调试器,它与LLVM编译器一起,带给我们更丰富的流程控制和数据检测的调试功能。平时用Xcode运行程序,实际走的都是LLDB。熟练使用LLDB,可以让你debug事半功倍

LLDB基础知识

LLDB控制台

Xcode中内嵌了LLDB控制台,在Xcode中代码的下方,我们可以看到LLDB控制台。


LLDB控制台平时会输出一些log信息。如果我们想输入命令调试,必须让程序进入暂停状态。让程序进入暂停状态的方式主要有2种:

  1. 断点或者watchpoint: 在代码中设置一个断点(watchpoint),当程序运行到断点位置的时候,会进入stop状态
  2. 直接暂停,控制台上方有一个暂停按钮,上图红框已标出,点击即可暂停程序

LLDB语法

在使用LLDB之前,我们来先看看LLDB的语法,了解语法可以帮助我们清晰的使用LLDB:

<command> [<subcommand> [<subcommand>...]] <action> [-options [option-value]] [argument [argument...]]

一眼看上去可能比较迷茫,给大家解释一下:

  1. <command>(命令)和<subcommand>(子命令):LLDB调试命令的名称。命令和子命令按层级结构来排列:一个命令对象为跟随其的子命令对象创建一个上下文,子命令又为其子命令创建一个上下文,依此类推。
  2. <action>:执行命令的操作
  3. <options>:命令选项
  4. <arguement>:命令的参数
  5. []:表示命令是可选的,可以有也可以没有

举个例子,假设我们给main方法设置一个断点,我们使用下面的命令:

breakpoint set -n main

这个命令对应到上面的语法就是:

  1. command: breakpoint 表示断点命令
  2. action: set 表示设置断点
  3. option: -n 表示根据方法name设置断点
  4. arguement: mian 表示方法名为mian

原始(raw)命令

LLDB支持不带命令选项(options)的原始(raw)命令,原始命令会将命令后面的所有东西当做参数(arguement)传递。不过很多原始命令也可以带命令选项,当你使用命令选项的时候,需要在命令选项后面加--区分命令选项和参数。

e.g: 常用的expression就是raw命令,一般情况下我们使用expression打印一个东西是这样的:

 
  1. (lldb) expression count

  2. (int) $2 = 4

当我们想打印一个对象的时候。需要使用-O命令选项,我们应该用--将命令选项和参数区分:

 
  1. (lldb) expression -O -- self

  2. <ViewController: 0x7f9000f17660>

唯一匹配原则

LLDB的命令遵循唯一匹配原则:假如根据前n个字母已经能唯一匹配到某个命令,则只写前n个字母等效于写下完整的命令。
e.g: 前面提到我设置断点的命令,我们可以使用唯一匹配原则简写,下面2条命令等效:

 
  1. breakpoint set -n main

  2. br s -n main

~/.lldbinit

LLDB有了一个启动时加载的文件~/.lldbinit,每次启动都会加载。所以一些初始化的事儿,我们可以写入~/.lldbinit中,比如给命令定义别名等。但是由于这时候程序还没有真正运行,也有部分操作无法在里面玩,比如设置断点。

LLDB命令

expression

expression命令的作用是执行一个表达式,并将表达式返回的结果输出。expression的完整语法是这样的:

expression <cmd-options> -- <expr>
  1. <cmd-options>:命令选项,一般情况下使用默认的即可,不需要特别标明。
  2. --: 命令选项结束符,表示所有的命令选项已经设置完毕,如果没有命令选项,--可以省略
  3. <expr>: 要执行的表达式

expression是LLDB里面最重要的命令都不为过。因为他能实现2个功能。

  • 执行某个表达式。
    我们在代码运行过程中,可以通过执行某个表达式来动态改变程序运行的轨迹。
    假如我们在运行过程中,突然想把self.view颜色改成红色,看看效果。我们不必写下代码,重新run,只需暂停程序,用expression改变颜色,再刷新一下界面,就能看到效果

     
    1. // 改变颜色

    2. (lldb) expression -- self.view.backgroundColor = [UIColor redColor]

    3. // 刷新界面

    4. (lldb) expression -- (void)[CATransaction flush]

  • 将返回值输出。
    也就是说我们可以通过expression来打印东西。
    假如我们想打印self.view:

 
  1. (lldb) expression -- self.view

  2. (UIView *) $1 = 0x00007fe322c18a10

p & print & call

一般情况下,我们直接用expression还是用得比较少的,更多时候我们用的是pprintcall。这三个命令其实都是expression --的别名(--表示不再接受命令选项,详情见前面原始(raw)命令这一节)

  1. print: 打印某个东西,可以是变量和表达式
  2. p: 可以看做是print的简写
  3. call: 调用某个方法。

表面上看起来他们可能有不一样的地方,实际都是执行某个表达式(变量也当做表达式),将执行的结果输出到控制台上。所以你可以用p调用某个方法,也可以用call打印东西
e.g: 下面代码效果相同:

 
  1. (lldb) expression -- self.view

  2. (UIView *) $5 = 0x00007fb2a40344a0

  3. (lldb) p self.view

  4. (UIView *) $6 = 0x00007fb2a40344a0

  5. (lldb) print self.view

  6. (UIView *) $7 = 0x00007fb2a40344a0

  7. (lldb) call self.view

  8. (UIView *) $8 = 0x00007fb2a40344a0

  9. (lldb) e self.view

  10. (UIView *) $9 = 0x00007fb2a40344a0

根据唯一匹配原则,如果你没有自己添加特殊的命令别名。e也可以表示expression的意思。原始命令默认没有命令选项,所以e也能带给你同样的效果

po

我们知道,OC里所有的对象都是用指针表示的,所以一般打印的时候,打印出来的是对象的指针,而不是对象本身。如果我们想打印对象。我们需要使用命令选项:-O。为了更方便的使用,LLDB为expression -O --定义了一个别名:po

 
  1. (lldb) expression -- self.view

  2. (UIView *) $13 = 0x00007fb2a40344a0

  3. (lldb) expression -O -- self.view

  4. <UIView: 0x7fb2a40344a0; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x7fb2a4018c80>>

  5. (lldb) po self.view

  6. <UIView: 0x7fb2a40344a0; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x7fb2a4018c80>>

还有其他很多命令选项,不过我们一般用得比较少,所以我就不具体的一一介绍了,如果想了解,在LLDB控制台上输入:help expression即可查到expression所有的信息

thread

thread backtrace & bt

有时候我们想要了解线程堆栈信息,可以使用thread backtrace
thread backtrace作用是将线程的堆栈打印出来。我们来看看他的语法

thread backtrace [-c <count>] [-s <frame-index>] [-e <boolean>]

thread backtrace后面跟的都是命令选项:

-c:设置打印堆栈的帧数(frame)
-s:设置从哪个帧(frame)开始打印
-e:是否显示额外的回溯
实际上这些命令选项我们一般不需要使用。
e.g: 当发生crash的时候,我们可以使用thread backtrace查看堆栈调用

 
  1. (lldb) thread backtrace

  2. * thread #1: tid = 0xdd42, 0x000000010afb380b libobjc.A.dylib`objc_msgSend + 11, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=EXC_I386_GPFLT)

  3. frame #0: 0x000000010afb380b libobjc.A.dylib`objc_msgSend + 11

  4. * frame #1: 0x000000010aa9f75e TLLDB`-[ViewController viewDidLoad](self=0x00007fa270e1f440, _cmd="viewDidLoad") + 174 at ViewController.m:23

  5. frame #2: 0x000000010ba67f98 UIKit`-[UIViewController loadViewIfRequired] + 1198

  6. frame #3: 0x000000010ba682e7 UIKit`-[UIViewController view] + 27

  7. frame #4: 0x000000010b93eab0 UIKit`-[UIWindow addRootViewControllerViewIfPossible] + 61

  8. frame #5: 0x000000010b93f199 UIKit`-[UIWindow _setHidden:forced:] + 282

  9. frame #6: 0x000000010b950c2e UIKit`-[UIWindow makeKeyAndVisible] + 42

我们可以看到crash发生在-[ViewController viewDidLoad]中的第23行,只需检查这行代码是不是干了什么非法的事儿就可以了。

LLDB还为backtrace专门定义了一个别名:bt,他的效果与thread backtrace相同,如果你不想写那么长一串字母,直接写下bt即可:

(lldb) bt

thread return

Debug的时候,也许会因为各种原因,我们不想让代码执行某个方法,或者要直接返回一个想要的值。这时候就该thread return上场了。

thread return [<expr>]

thread return可以接受一个表达式,调用命令之后直接从当前的frame返回表达式的值。

e.g: 我们有一个someMethod方法,默认情况下是返回YES。我们想要让他返回NO

我们只需在方法的开始位置加一个断点,当程序中断的时候,输入命令即可:

(lldb) thread return NO

效果相当于在断点位置直接调用return NO;,不会执行断点后面的代码

c & n & s & finish

一般在调试程序的时候,我们经常用到下面这4个按钮:

用触摸板的孩子们可能会觉得点击这4个按钮比较费劲。其实LLDB命令也可以完成上面的操作,而且如果不输入命令,直接按Enter键,LLDB会自动执行上次的命令。按一下Enter就能达到我们想要的效果,有木有顿时感觉逼格满满的!!!
我们来看看对应这4个按钮的LLDB命令:

  1. c/ continue/ thread continue: 这三个命令效果都等同于上图中第一个按钮的。表示程序继续运行
  2. n/ next/ thread step-over: 这三个命令效果等同于上图第二个按钮。表示单步运行
  3. s/ step/ thread step-in: 这三个命令效果等同于上图第三个按钮。表示进入某个方法
  4. finish/ step-out: 这两个命令效果等同于第四个按钮。表示直接走完当前方法,返回到上层frame

thread其他不常用的命令

thread 相关的还有其他一些不常用的命令,这里就简单介绍一下即可,如果需要了解更多,可以使用命令help thread查阅

  1. thread jump: 直接让程序跳到某一行。由于ARC下编译器实际插入了不少retain,release命令。跳过一些代码不执行很可能会造成对象内存混乱发生crash。
  2. thread list: 列出所有的线程
  3. thread select: 选择某个线程
  4. thread until: 传入一个line的参数,让程序执行到这行的时候暂停
  5. thread info: 输出当前线程的信息

frame

前面我们提到过很多次frame(帧)。可能有的朋友对frame这个概念还不太了解。随便打个断点

我们在控制台上输入命令bt,可以打印出来所有的frame。如果仔细观察,这些frame和左边红框里的堆栈是一致的。平时我们看到的左边的堆栈就是frame。

frame variable

平时Debug的时候我们经常做的事就是查看变量的值,通过frame variable命令,可以打印出当前frame的所有变量

 
  1. (lldb) frame variable

  2. (ViewController *) self = 0x00007fa158526e60

  3. (SEL) _cmd = "text:"

  4. (BOOL) ret = YES

  5. (int) a = 3

可以看到,他将self,_cmd,ret,a等本地变量都打印了出来

如果我们要需要打印指定变量,也可以给frame variable传入参数:

 
  1. (lldb) frame variable self->_string

  2. (NSString *) self->_string = nil

不过frame variable只接受变量作为参数,不接受表达式,也就是说我们无法使用frame variable self.string,因为self.string是调用stringgetter方法。所以一般打印指定变量,我更喜欢用p或者po

其他不常用命令

一般frame variable打印所有变量用得比较多,frame还有2个不怎么常用的命令:

frame info: 查看当前frame的信息

 
  1. (lldb) frame info

  2. frame #0: 0x0000000101bf87d5 TLLDB`-[ViewController text:](self=0x00007fa158526e60, _cmd="text:", ret=YES) + 37 at ViewController.m:38

frame select: 选择某个frame

 
  1. (lldb) frame select 1

  2. frame #1: 0x0000000101bf872e TLLDB`-[ViewController viewDidLoad](self=0x00007fa158526e60, _cmd="viewDidLoad") + 78 at ViewController.m:23

  3. 20

  4. 21 - (void)viewDidLoad {

  5. 22 [super viewDidLoad];

  6. -> 23 [self text:YES];

  7. 24 NSLog(@"1");

  8. 25 NSLog(@"2");

  9. 26 NSLog(@"3");

当我们选择frame 1的时候,他会把frame1的信息和代码打印出来。不过一般我都是直接在Xcode左边点击某个frame,这样更方便

breakpoint

调试过程中,我们用得最多的可能就是断点了。LLDB中的断点命令也非常强大

breakpoint set

breakpoint set命令用于设置断点,LLDB提供了很多种设置断点的方式:

使用-n根据方法名设置断点:

e.g: 我们想给所有类中的viewWillAppear:设置一个断点:

 
  1. (lldb) breakpoint set -n viewWillAppear:

  2. Breakpoint 13: 33 locations.

使用-f指定文件

e.g: 我们只需要给ViewController.m文件中的viewDidLoad设置断点:

 
  1. (lldb) breakpoint set -f ViewController.m -n viewDidLoad

  2. Breakpoint 22: where = TLLDB`-[ViewController viewDidLoad] + 20 at ViewController.m:22, address = 0x000000010272a6f4

这里需要注意,如果方法未写在文件中(比如写在category文件中,或者父类文件中),指定文件之后,将无法给这个方法设置断点。

使用-l指定文件某一行设置断点

e.g: 我们想给ViewController.m第38行设置断点

 
  1. (lldb) breakpoint set -f ViewController.m -l 38

  2. Breakpoint 23: where = TLLDB`-[ViewController text:] + 37 at ViewController.m:38, address = 0x000000010272a7d5

使用-c设置条件断点

e.g: text:方法接受一个ret的参数,我们想让ret == YES的时候程序中断:

 
  1. (lldb) breakpoint set -n text: -c ret == YES

  2. Breakpoint 7: where = TLLDB`-[ViewController text:] + 30 at ViewController.m:37, address = 0x0000000105ef37ce

使用-o设置单次断点

e.g: 如果刚刚那个断点我们只想让他中断一次:

 
  1. (lldb) breakpoint set -n text: -o

  2. 'breakpoint 3': where = TLLDB`-[ViewController text:] + 30 at ViewController.m:37, address = 0x000000010b6f97ce

breakpoint command

有的时候我们可能需要给断点添加一些命令,比如每次走到这个断点的时候,我们都需要打印self对象。我们只需要给断点添加一个po self命令,就不用每次执行断点再自己输入po self

breakpoint command add

breakpoint command add命令就是给断点添加命令的命令。

e.g: 假设我们需要在ViewControllerviewDidLoad中查看self.view的值
我们首先给-[ViewController viewDidLoad]添加一个断点

 
  1. (lldb) breakpoint set -n "-[ViewController viewDidLoad]"

  2. 'breakpoint 3': where = TLLDB`-[ViewController viewDidLoad] + 20 at ViewController.m:23, address = 0x00000001055e6004

可以看到添加成功之后,这个breakpoint的id为3,然后我们给他增加一个命令:po self.view

(lldb) breakpoint command add -o "po self.view" 3

-o完整写法是--one-liner,表示增加一条命令。3表示对id为3breakpoint增加命令。
添加完命令之后,每次程序执行到这个断点就可以自动打印出self.view的值了

如果我们一下子想增加多条命令,比如我想在viewDidLoad中打印当前frame的所有变量,但是我们不想让他中断,也就是在打印完成之后,需要继续执行。我们可以这样玩:

 
  1. (lldb) breakpoint command add 3

  2. Enter your debugger command(s). Type 'DONE' to end.

  3. > frame variable

  4. > continue

  5. > DONE

输入breakpoint command add 3对断点3增加命令。他会让你输入增加哪些命令,输入'DONE'表示结束。这时候你就可以输入多条命令了

多次对同一个断点添加命令,后面命令会将前面命令覆盖

breakpoint command list

如果想查看某个断点已有的命令,可以使用breakpoint command list
e.g: 我们查看一下刚刚的断点3已有的命令

 
  1. (lldb) breakpoint command list 3

  2. 'breakpoint 3':

  3. Breakpoint commands:

  4. frame variable

  5. continue

可以看到一共有2条命令,分别为frame variablecontinue

breakpoint command delete

有增加就有删除,breakpoint command delete可以让我们删除某个断点的命令
e.g: 我们将断点3中的命令删除:

 
  1. (lldb) breakpoint command delete 3

  2. (lldb) breakpoint command list 3

  3. Breakpoint 3 does not have an associated command.

可以看到删除之后,断点3就没有命令了

breakpoint list

如果我们想查看已经设置了哪些断点,可以使用breakpoint list
e.g:

 
  1. (lldb) breakpoint list

  2. Current breakpoints:

  3. 4: name = '-[ViewController viewDidLoad]', locations = 1, resolved = 1, hit count = 0

  4. 4.1: where = TLLDB`-[ViewController viewDidLoad] + 20 at ViewController.m:23, address = 0x00000001055e6004, resolved, hit count = 0

我们可以看到当前只有一个断点,打在-[ViewController viewDidLoad]上,id是4

breakpoint disable/enable

有的时候我们可能暂时不想要某个断点,可以使用breakpoint disable让某个断点暂时失效
e.g: 我们来让刚刚的断点4失效

 
  1. (lldb) breakpoint disable 4

  2. 1 breakpoints disabled.

输入完命令之后,显示断点已经失效

当我们又需要这个断点的时候,可以使用breakpoint enable再次让他生效
e.g: 重新启用断点4

 
  1. (lldb) breakpoint enable 4

  2. 1 breakpoints enabled.

breakpoint delete

如果我们觉得这个断点以后再也用不上了,可以用breakpoint delete直接删除断点.
e.g: 删除断点4

 
  1. (lldb) breakpoint delete 4

  2. 1 breakpoints deleted; 0 breakpoint locations disabled.

如果我们想删除所有断点,只需要不指定breakpoint delete参数即可

 
  1. (lldb) breakpoint delete

  2. About to delete all breakpoints, do you want to do that?: [Y/n] y

  3. All breakpoints removed. (1 breakpoint)

删除的时候他会提示你,是不是真的想删除所有断点,需要你再次输入Y确认。如果想直接删除,不需要他的提示,使用-f命令选项即可

 
  1. (lldb) breakpoint delete -f

  2. All breakpoints removed. (1 breakpoint)

实际平时我们真正使用breakpoint命令反而比较少,因为Xcode已经内置了断点工具。我们可以直接在代码上打断点,可以在断点工具栏里面查看编辑断点,这比使用LLDB命令方便很多。不过了解LLDB相关命令可以让我们对断点理解更深刻。
如果你想了解怎么使用Xcode设置断点,可以阅读这篇文章《Xcode中断点的威力》

watchpoint

breakpoint有一个孪生兄弟watchpoint。如果说breakpoint是对方法生效的断点,watchpoint就是对地址生效的断点

如果我们想要知道某个属性什么时候被篡改了,我们该怎么办呢?有人可能会说对setter方法打个断点不就行了么?但是如果更改的时候没调用setter方法呢?
这时候最好的办法就是用watchpoint。我们可以用他观察这个属性的地址。如果地址里面的东西改变了,就让程序中断

watchpoint set

watchpoint set命令用于添加一个watchpoint。只要这个地址中的内容变化了,程序就会中断。

watchpoint set variable

一般情况下,要观察变量或者属性,使用watchpoint set variable命令即可
e.g: 观察self->_string

 
  1. (lldb) watchpoint set variable self->_string

  2. Watchpoint created: Watchpoint 1: addr = 0x7fcf3959c418 size = 8 state = enabled type = w

  3. watchpoint spec = 'self->_string'

  4. new value: 0x0000000000000000

watchpoint set variable传入的是变量名。需要注意的是,这里不接受方法,所以不能使用watchpoint set variable self.string,因为self.string调用的是string的getter方法

watchpoint set expression

如果我们想直接观察某个地址,可以使用watchpoint set expression
e.g: 我们先拿到_model的地址,然后对地址设置一个watchpoint

 
  1. (lldb) p &_model

  2. (Modek **) $3 = 0x00007fe0dbf23280

  3. (lldb) watchpoint set expression 0x00007fe0dbf23280

  4. Watchpoint created: Watchpoint 1: addr = 0x7fe0dbf23280 size = 8 state = enabled type = w

  5. new value: 0

watchpoint command

跟breakpoint类似,在watchpoint中也可以添加命令

watchpoint command add

我们来看看怎么给watchpoint添加命令:

首先,我们设置一个watchpoint:

 
  1. (lldb) watchpoint set variable _string

  2. Watchpoint created: Watchpoint 1: addr = 0x7fe4e1444760 size = 8 state = enabled type = w

  3. watchpoint spec = '_string'

  4. new value: 0x0000000000000000

可以看到这个watchpoint的id是1。我们可以用watchpoint command add -o添加单条命令

watchpoint command add -o 'bt' 1

我们在watchpoint停下来的时候,打印了他的线程信息。

我们也可以一次添加多条命令:

 
  1. (lldb) watchpoint command add 1

  2. Enter your debugger command(s). Type 'DONE' to end.

  3. > bt

  4. > continue

  5. > DONE

可以看到watchpoint的使用方法跟breakpoint几乎一模一样。

watchpoint command list

我们可以用watchpoint command list列出某个watchpoint所有的command

 
  1. (lldb) watchpoint command list 1

  2. Watchpoint 1:

  3. watchpoint commands:

  4. bt

  5. continue

watchpoint command delete

我们也可以用watchpoint command delete删除某个watchpoint所有的command

 
  1. (lldb) watchpoint command delete 1

  2. (lldb) watchpoint command list 1

  3. Watchpoint 1 does not have an associated command.

watchpoint list

如果我们想看当前所有watchpoint,可以使用watchpoint list:

 
  1. (lldb) watchpoint list

  2. Number of supported hardware watchpoints: 4

  3. Current watchpoints:

  4. Watchpoint 1: addr = 0x7fe9f9f28e30 size = 8 state = enabled type = w

  5. watchpoint spec = '_string'

  6. old value: 0x0000000000000000

  7. new value: 0x000000010128e0d0

可以看到,只有一个watchpoint。

watchpoint disable

当我们不想让某个watchpoint生效的时候,可以用watchpoint disable:

 
  1. (lldb) watchpoint disable 1

  2. 1 watchpoints disabled.

再次查看这个watchpoint,可以看到他的state已经变为了disabled

 
  1. (lldb) watchpoint list

  2. Number of supported hardware watchpoints: 4

  3. Current watchpoints:

  4. Watchpoint 1: addr = 0x7fe9f9f28e30 size = 8 state = disabled type = w

  5. watchpoint spec = '_string'

  6. old value: 0x0000000000000000

  7. new value: 0x000000010128e0d0

watchpoint enable

过了一会,我们又要用这个watchpoint了,这时候可以使用watchpoint enable:

 
  1. (lldb) watchpoint enable 1

  2. 1 watchpoints enabled.

watchpoint delete

如果我们觉得再也用不着这个watchpoint了,可以用watchpoint delete将他删除:

 
  1. (lldb) watchpoint delete 1

  2. 1 watchpoints deleted.

  3. (lldb) watchpoint list

  4. Number of supported hardware watchpoints: 4

  5. No watchpoints currently set.

删除之后,我们可以看到watchpoint list里面已经没有watchpoint1

如果有很多个watchpoint,我们想全都干掉,只需要不指定具体哪个watchpoint即可:

 
  1. (lldb) watchpoint delete

  2. About to delete all watchpoints, do you want to do that?: [Y/n] y

  3. All watchpoints removed. (2 watchpoints)

target

target modules lookup(image lookup)

对于target这个命令,我们用得最多的可能就是target modules lookup。由于LLDB给target modules取了个别名image,所以这个命令我们又可以写成image lookup

image lookup --address

当我们有一个地址,想查找这个地址具体对应的文件位置,可以使用image lookup --address,简写为image lookup -a
e.g: 当我们发生一个crash

 
  1. 2015-12-17 14:51:06.301 TLLDB[25086:246169] *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArray0 objectAtIndex:]: index 1 beyond bounds for empty NSArray'

  2. *** First throw call stack:

  3. (

  4. 0 CoreFoundation 0x000000010accde65 __exceptionPreprocess + 165

  5. 1 libobjc.A.dylib 0x000000010a746deb objc_exception_throw + 48

  6. 2 CoreFoundation 0x000000010ac7c395 -[__NSArray0 objectAtIndex:] + 101

  7. 3 TLLDB 0x000000010a1c3e36 -[ViewController viewDidLoad] + 86

  8. 4 UIKit 0x000000010b210f98 -[UIViewController loadViewIfRequired] + 1198

  9. 5 UIKit 0x000000010b2112e7 -[UIViewController view] + 27

我们可以看到是由于-[__NSArray0 objectAtIndex:]:超出边界而导致的crash,但是objectAtIndex:的代码到底在哪儿呢?

 
  1. (lldb) image lookup -a 0x000000010a1c3e36

  2. Address: TLLDB[0x0000000100000e36] (TLLDB.__TEXT.__text + 246)

  3. Summary: TLLDB`-[ViewController viewDidLoad] + 86 at ViewController.m:32

根据0x000000010a1c3e36 -[ViewController viewDidLoad]里面的地址,使用image lookup --address查找,我们可以看到代码位置在ViewController.m里面的32行

image lookup --name

当我们想查找一个方法或者符号的信息,比如所在文件位置等。我们可以使用image lookup --name,简写为image lookup -n

e.g: 刚刚遇到的真问题,某个第三方SDK用了一个我们项目里原有的第三方库,库里面对NSDictionary添加了category。也就是有2个class对NSDictionary添加了名字相同的category,项目中调用自己的category的地方实际走到了第三方SDK里面去了。最大的问题是,这2个同名category方法行为并不一致,导致出现bug
现在问题来了,怎么寻找到底是哪个第三方SDK?方法完全包在.a里面。
其实只需使用image lookup -n即可:

 
  1. (lldb) image lookup -n dictionaryWithXMLString:

  2. 2 matches found in /Users/jiangliancheng/Library/Developer/Xcode/DerivedData/VideoIphone-aivsnqmlwjhxapdlvmdmrubbdxpq/Build/Products/Debug-iphoneos/BaiduIphoneVideo.app/BaiduIphoneVideo:

  3. Address: BaiduIphoneVideo[0x00533a7c] (BaiduIphoneVideo.__TEXT.__text + 5414908)

  4. Summary: BaiduIphoneVideo`+[NSDictionary(SAPIXmlDictionary) dictionaryWithXMLString:] at XmlDictionary.m

  5. Module: file = "/Users/jiangliancheng/Library/Developer/Xcode/DerivedData/VideoIphone-aivsnqmlwjhxapdlvmdmrubbdxpq/Build/Products/Debug-iphoneos/BaiduIphoneVideo.app/BaiduIphoneVideo", arch = "armv7"

  6. CompileUnit: id = {0x00000000}, file = "/Users/jiangliancheng/Development/Work/iOS_ShareLib/SharedLib/Srvcs/BDPassport4iOS/BDPassport4iOS/SAPI/Extensive/ThirdParty/XMLDictionary/XmlDictionary.m", language = "Objective-C"

  7. Function: id = {0x23500000756}, name = "+[NSDictionary(SAPIXmlDictionary) dictionaryWithXMLString:]", range = [0x005a6a7c-0x005a6b02)

  8. FuncType: id = {0x23500000756}, decl = XmlDictionary.m:189, clang_type = "NSDictionary *(NSString *)"

  9. Blocks: id = {0x23500000756}, range = [0x005a6a7c-0x005a6b02)

  10. LineEntry: [0x005a6a7c-0x005a6a98): /Users/jiangliancheng/Development/Work/iOS_ShareLib/SharedLib/Srvcs/BDPassport4iOS/BDPassport4iOS/SAPI/Extensive/ThirdParty/XMLDictionary/XmlDictionary.m

  11. Symbol: id = {0x0000f2d5}, range = [0x005a6a7c-0x005a6b04), name="+[NSDictionary(SAPIXmlDictionary) dictionaryWithXMLString:]"

  12. Variable: id = {0x23500000771}, name = "self", type = "Class", location = [sp+32], decl =

  13. Variable: id = {0x2350000077e}, name = "_cmd", type = "SEL", location = [sp+28], decl =

  14. Variable: id = {0x2350000078b}, name = "string", type = "NSString *", location = [sp+24], decl = XmlDictionary.m:189

  15. Variable: id = {0x23500000799}, name = "data", type = "NSData *", location = [sp+20], decl = XmlDictionary.m:192

  16. Address: BaiduIphoneVideo[0x012ee160] (BaiduIphoneVideo.__TEXT.__text + 19810016)

  17. Summary: BaiduIphoneVideo`+[NSDictionary(XMLDictionary) dictionaryWithXMLString:] at XMLDictionary.m

  18. Module: file = "/Users/jiangliancheng/Library/Developer/Xcode/DerivedData/VideoIphone-aivsnqmlwjhxapdlvmdmrubbdxpq/Build/Products/Debug-iphoneos/BaiduIphoneVideo.app/BaiduIphoneVideo", arch = "armv7"

  19. CompileUnit: id = {0x00000000}, file = "/Users/wingle/Workspace/qqlive4iphone/iphone_4.0_fabu_20150601/Common_Proj/mobileTAD/VIDEO/Library/Third Party/XMLDictionary/XMLDictionary.m", language = "Objective-C"

  20. Function: id = {0x79900000b02}, name = "+[NSDictionary(XMLDictionary) dictionaryWithXMLString:]", range = [0x01361160-0x0136119a)

  21. FuncType: id = {0x79900000b02}, decl = XMLDictionary.m:325, clang_type = "NSDictionary *(NSString *)"

  22. Blocks: id = {0x79900000b02}, range = [0x01361160-0x0136119a)

  23. LineEntry: [0x01361160-0x01361164): /Users/wingle/Workspace/qqlive4iphone/iphone_4.0_fabu_20150601/Common_Proj/mobileTAD/VIDEO/Library/Third Party/XMLDictionary/XMLDictionary.m

  24. Symbol: id = {0x0003a1e9}, range = [0x01361160-0x0136119c), name="+[NSDictionary(XMLDictionary) dictionaryWithXMLString:]"

  25. Variable: id = {0x79900000b1e}, name = "self", type = "Class", location = r0, decl =

  26. Variable: id = {0x79900000b2c}, name = "_cmd", type = "SEL", location = r1, decl =

  27. Variable: id = {0x79900000b3a}, name = "string", type = "NSString *", location = r2, decl = XMLDictionary.m:325

  28. Variable: id = {0x79900000b4a}, name = "data", type = "NSData *", location = r2, decl = XMLDictionary.m:327

东西有点多,我们只需关注里面的file这一行:

 
  1. CompileUnit: id = {0x00000000}, file = "/Users/jiangliancheng/Development/Work/iOS_ShareLib/SharedLib/Srvcs/BDPassport4iOS/BDPassport4iOS/SAPI/Extensive/ThirdParty/XMLDictionary/XmlDictionary.m", language = "Objective-C"

  2. CompileUnit: id = {0x00000000}, file = "/Users/wingle/Workspace/qqlive4iphone/iphone_4.0_fabu_20150601/Common_Proj/mobileTAD/VIDEO/Library/Third Party/XMLDictionary/XMLDictionary.m", language = "Objective-C"

可以清晰的看到,LLDB给我们找出来了这个方法的位置。
当然这个命令也可以找到方法的其他相关信息,比如参数等.

image lookup --type

当我们想查看一个类型的时候,可以使用image lookup --type,简写为image lookup -t:
e.g: 我们来看看Model的类型:

 
  1. (lldb) image lookup -t Model

  2. Best match found in /Users/jiangliancheng/Library/Developer/Xcode/DerivedData/TLLDB-beqoowskwzbttrejseahdoaivpgq/Build/Products/Debug-iphonesimulator/TLLDB.app/TLLDB:

  3. id = {0x30000002f}, name = "Model", byte-size = 32, decl = Modek.h:11, clang_type = "@interface Model : NSObject{

  4. NSString * _bb;

  5. NSString * _cc;

  6. NSString * _name;

  7. }

  8. @property ( getter = name,setter = setName:,readwrite,nonatomic ) NSString * name;

  9. @end

  10. "

可以看到,LLDB把Model这个class的所有属性和成员变量都打印了出来,当我们想了解某个类的时候,直接使用image lookup -t即可

target stop-hook

我们知道,用LLDB debug,大多数时候需要让程序stop,不管用breakpoint还是用watchpoint
target stop-hook命令就是让你可以在每次stop的时候去执行一些命令

target stop-hook只对breakpointwatchpoint的程序stop生效,直接点击Xcode上的pause或者debug view hierarchy不会生效

target stop-hook add & display

假如我们想在每次程序stop的时候,都用命令打印当前frame的所有变量。我们可以添加一个stop-hook:

 
  1. (lldb) target stop-hook add -o "frame variable"

  2. Stop hook #4 added.

target stop-hook add表示添加stop-hook,-o的全称是--one-liner,表示添加一条命令。
我们看一下,当执行到一个断点的时候会发生什么?

 
  1. - Hook 1 (frame variable)

  2. (ViewController *) self = 0x00007fd55b12e380

  3. (SEL) _cmd = "viewDidLoad"

  4. (NSMutableURLRequest *) request = 0x00007fd55b1010c0

在程序stop的时候,他会自动执行frame variable,打印出了所有的变量。

大多情况下,我们在stop的时候可能想要做的是打印一个东西。正常情况我们需要用target stop-hook add -o "p xxx",LLDB提供了一个更简便的命令display
e.g: 下面2行代码效果相同

 
  1. (lldb) target stop-hook add -o "p self.view"

  2. (lldb) display self.view

也可以用display来执行某一个命令。p,e,expression是等效的。

target stop-hook list

当添加完stop-hook之后,我们想看当前所有的stop-hook怎么办呢?使用stop-hook list

 
  1. (lldb) target stop-hook list

  2. Hook: 4

  3. State: enabled

  4. Commands:

  5. frame variable

  6.  
  7. Hook: 5

  8. State: enabled

  9. Commands:

  10. expression self.view

  11.  
  12. Hook: 6

  13. State: enabled

  14. Commands:

  15. expr -- self.view

我们可以看到,我们添加了4个stop-hook,每个stop-hook都有一个id,他们分别是4,5,6

target stop-hook delete & undisplay

有添加的命令,当然也就有删除的命令。使用target stop-hook delete可以删除stop-hook,如果你觉得这个命令有点长,懒得敲。你也可以用undisplay

 
  1. (lldb) target stop-hook delete 4

  2. (lldb) undisplay 5

我们用target stop-hook deleteundisplay分别删除了id为4和5的stop-hook

target stop-hook disable/enable

当我们暂时想让某个stop-hook失效的时候,可以使用target stop-hook disable

(lldb) target stop-hook disable 8

如果我们想让所有的stop-hook失效,只需不传入stop-hookid即可:

(lldb) target stop-hook disable

disable就有enable,我们又想让stop-hook生效了。可以使用target stop-hook enable

(lldb) target stop-hook enable 8

同理,不传入参数表示让所有stop-hook生效

(lldb) target stop-hook enable

Extension

前几天@兔be南玻1在微博上给出一个小技巧。LLDB中@import UIKit即可打印frame等变量(默认情况下打不出来)微博链接

 
  1. (lldb) p self.view.frame

  2. error: property 'frame' not found on object of type 'UIView *'

  3. error: 1 errors parsing expression

  4. (lldb) e @import UIKit

  5. (lldb) p self.view.frame

  6. (CGRect) $0 = (origin = (x = 0, y = 0), size = (width = 375, height = 667))

由于每次run Xcode,LLDB的东西都会被清空。所以每次run你都需要在LLDB中输入e @import UIKit才能使用这个方便的功能,有点麻烦呀!

之后有人提出了比较方便的一个办法。给UIApplicationMain设置一个断点,在断点中添加执行e @import UIKit


这种方法非常方便,不用自己输入了,但是断点我们可能会误删,而且断点是对应工程的。换一个工程又得重新打一个这样的断点。还是有点麻烦。有没有更简便的方法呢?

我们首先想到的是LLDB在每次启动的时候都会load '~/.lldbinit'文件。在这里面执行e @import UIKit不就行了么?不会被误删,对每个工程都有效!

然而想法是美好的,现实却是残酷的!因为UIKit这个库是在target中。而load '~/.lldbinit'的时候target还没创建。所以无法import UIKitstackoverflow详细解释

这时候我们又想到,可不可以在'~/.lldbinit'中给UIApplicationMain设置一个断点,在断点中添加执行e @import UIKit呢?
答案是不行。原因跟前面一样,load '~/.lldbinit'执行时间太早。断点是依赖target的,target还未创建,断点加不上去。好事多磨,道路坎坷呀~~~

后来我们又想到用stop-hook行不行呢?stop-hook不依赖target。一般我们p frame的时候,都需要先stop,理论上是可行的
事实证明stop-hook的方法完全ok。只需要在'~/.lldbinit'中添加这2条命令即可:

 
  1. display @import UIKit

  2. target stop-hook add -o "target stop-hook disable"

  • 命令1:使用display表示在stop的时候执行@import UIKit
  • 命令2:由于我们只需要执行一次@import UIKit,所以执行完成之后,执行target stop-hook disable,使原有的所有stop-hook失效

这个命令有个缺陷,直接点击Xcode上的pausedebug view hierarchystop-hook不会生效。正在探索有没有更好的办法完成@import UIKit,如果你想到了,可以联系我~

target symbols add(add-dsym)

说这个命令之前,先简单解释一下dSYM文件。程序运行的时候,都会编译成二进制文件。因为计算机只识别二进制文件,那为什么我们还能在代码上打断点?
这主要是因为在编译的时候Xcode会生成dSYM文件,dSYM文件记录了哪行代码对应着哪些二进制,这样我们对代码打断点就会对应到二进制上。dSYM详细资料

当Xcode找不着dSYM文件的时候,我们就无法对代码打断点,进行调试。target symbols add命令的作用就是让我们可以手动的将dSYM文件添加上去。LLBD对这个命令起了一个别名: add-dsym

e.g: 当我们对接framework的时候,如果只有framework代码,没有工程代码,能不能debug呢?其实我们只需要拿到工程的ipa和dSYM文件,就可以debug了,通过Attach to Process,使用命令add-dsym将dSYM文件加入target,即可只debug framework,不需要工程的代码

add-dsym ~/.../XXX.dSYM

详细细节可以查看iOS中framework的联调

help & apropos

LLDB的命令其实还有很多,很多命令我也没玩过。就算玩过的命令,我们也非常容易忘记,下次可能就不记得是怎么用的了。还好LLDB给我们提供了2个查找命令的命令:help & apropos

help

直接在LLDB中输入help。可以查看所有的LLDB命令

 
  1. (lldb) help

  2. Debugger commands:

  3.  
  4. apropos -- Find a list of debugger commands related to a particular

  5. word/subject.

  6. breakpoint -- A set of commands for operating on breakpoints. Also see

  7. _regexp-break.

  8. help -- Show a list of all debugger commands, or give details

  9. about specific commands.

  10. script -- Pass an expression to the script interpreter for

  11. evaluation and return the results. Drop into the

  12. interactive interpreter if no expression is given.

  13. settings -- A set of commands for manipulating internal settable

  14. debugger variables.

  15. source -- A set of commands for accessing source file information

  16. target -- A set of commands for operating on debugger targets.

  17. thread -- A set of commands for operating on one or more threads

  18. within a running process.

  19. type -- A set of commands for operating on the type system

  20. version -- Show version of LLDB debugger.

  21. watchpoint -- A set of commands for operating on watchpoints.

  22. ....(东西太多,只截取了一部分)

如果我们想看具体某一个命令的详细用法,可以使用help <command-name>
e.g: 我们查看watchpoint命令

 
  1. (lldb) help watchpoint

  2. The following subcommands are supported:

  3.  
  4. command -- A set of commands for adding, removing and examining bits of

  5. code to be executed when the watchpoint is hit (watchpoint

  6. 'commmands').

  7. delete -- Delete the specified watchpoint(s). If no watchpoints are

  8. specified, delete them all.

  9. disable -- Disable the specified watchpoint(s) without removing it/them.

  10. If no watchpoints are specified, disable them all.

  11. enable -- Enable the specified disabled watchpoint(s). If no watchpoints

  12. are specified, enable all of them.

  13. ignore -- Set ignore count on the specified watchpoint(s). If no

  14. watchpoints are specified, set them all.

  15. list -- List all watchpoints at configurable levels of detail.

  16. modify -- Modify the options on a watchpoint or set of watchpoints in

  17. the executable. If no watchpoint is specified, act on the

  18. last created watchpoint. Passing an empty argument clears the

  19. modification.

  20. set -- A set of commands for setting a watchpoint.

apropos

有的时候,我们可能并不能完全记得某个命令,如果只记得命令中的某个关键字。这时候我们可以使用apropos搜索相关命令信息。
e.g: 我们想使用stop-hook的命令,但是已经不记得stop-hook命令是啥样了

 
  1. (lldb) apropos stop-hook

  2. The following built-in commands may relate to 'stop-hook':

  3. _regexp-display -- Add an expression evaluation stop-hook.

  4. _regexp-undisplay -- Remove an expression evaluation stop-hook.

  5. target stop-hook -- A set of commands for operating on debugger

  6. target stop-hooks.

  7. target stop-hook add -- Add a hook to be executed when the target stops.

  8. target stop-hook delete -- Delete a stop-hook.

  9. target stop-hook disable -- Disable a stop-hook.

  10. target stop-hook enable -- Enable a stop-hook.

  11. target stop-hook list -- List all stop-hooks.

可以看到使用apropos stop-hook搜索一下,即可将所有stop-hook相关命令搜索出来

常用的Debug快捷键

debug的时候,使用快捷键是一个很好的习惯,我简单列举了几个debug的快捷键

功能 命令
暂停/继续 cmd + ctrl + Y
断点失效/生效 cmd + Y
控制台显示/隐藏 cmd + shift + Y
光标切换到控制台 cmd + shift + C
清空控制台 cmd + K
step over F6
step into F7
step out F8

End

东西有点多,感谢大家耐心看完这篇文章。LLDB命令非常多,有很多LLDB命令我也没玩过。这些命令我们不一定要完全记住,只要有个印象LLDB可以实现哪些功能就可以了。具体用的时候再用help或者apropos查找

猜你喜欢

转载自blog.csdn.net/chenyijun/article/details/86212595