iOS调试之LLDB

iOS调试之LLDB

Xcode内嵌了LLDB控制台,在Xcode代码编辑区的下方。shift + cmd + y可以显示隐藏LLDB控制台。

LLDB语法

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

  1. <command>subcommand:LLDB调试命令的名称。命令和子命令按 层级结构来排列:一个命令对象为跟随其的子命令创建一个上下文,子命令又为其子命令创建一个上下文,以此类推。

  2. action:执行命令的操作。

  3. option:命令选项。

  4. argument:命令的参数。

  5. []:表示命令是可选的,可以有也可也没有。

    breakpoint set -n main

  6. <command>:breakpoint 。没有<subcommand>

  7. action:set 。

  8. option: -n 表示根据方法name设置断点。

  9. argument: main 表示方法名为main 。

原始(Raw)命令

LLDB支持不带命令选项(options)的原始(raw)命令,原始命令会将命令后面的所有东西当做参数(arguement)传递。不过很多原始命令也可以带命令选项,当你使用命令选项的时候,需要在命令选项后面加--区分命令选项和参数。
e.g: 常用的expression就是raw命令,一般情况下我们使用expression打印一个东西是这样的:
(lldb) expression count
(int) $2 = 4
当我们想打印一个对象的时候。需要使用-O命令选项,我们应该用--将命令选项和参数区分:
(lldb) expression -O -- self
<ViewController: 0x7f9000f17660>

唯一匹配原则

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

~/.lldbinit

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

LLDB命令
expresssion

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

expression <cmd-options> -- <expr>

  1. <cmd-options>:命令选项,一般情况下使用默认的即可,不需要特别标明。
  2. --: 命令选项结束符,表示所有的命令选项已经设置完毕,如果没有命令选项,--可以省略。
  3. <expr>: 要执行的表达式。
    expression能够实现两个主要功能。
  • 执行某个表达式。
    我们在代码运行过程中,可以通过执行某个表达式来动态改变程序运行的轨迹。
    假如我们在运行过程中,突然想把self.view颜色改成红色,看看效果。我们不必写下代码,重新run,只需暂停程序,用expression改变颜色,再刷新一下界面,就能看到效果。
    // 改变颜色
    (lldb) expression -- self.view.backgroundColor = [UIColor redColor]
    // 刷新界面
    (lldb) expression -- (void)[CATransaction flush]
  • 将返回值输出。
    也就是说我们可以通过expression来打印东西。
    假如我们想打印self.view:
    (lldb) expression -- self.view
    (UIView *) $1 = 0x00007fe322c18a10
p&print&call
  1. print: 打印某个东西,可以是变量和表达式。
  2. p: 可以看做是print的简写。
  3. call: 调用某个方法。
    表面上看起来他们可能有不一样的地方,实际都是执行某个表达式(变量也当做表达式),将执行的结果输出到控制台上。所以你可以用p调用某个方法,也可以用call打印东西
    e.g: 下面代码效果相同:
    (lldb) expression -- self.view
    (UIView *) $5 = 0x00007fb2a40344a0
    (lldb) p self.view
    (UIView *) $6 = 0x00007fb2a40344a0
    (lldb) print self.view
    (UIView *) $7 = 0x00007fb2a40344a0
    (lldb) call self.view
    (UIView *) $8 = 0x00007fb2a40344a0
    (lldb) e self.view
    (UIView *) $9 = 0x00007fb2a40344a0
po

我们知道,OC里所有的对象都是用指针表示的,所以一般打印的时候,打印出来的是对象的指针,而不是对象本身。如果我们想打印对象。我们需要使用命令选项:-O。为了更方便的使用,LLDB为expression -O --定义了一个别名:po。
(lldb) expression -- self.view
(UIView *) $13 = 0x00007fb2a40344a0
(lldb) expression -O -- self.view
<UIView: 0x7fb2a40344a0; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x7fb2a4018c80>>
(lldb) po self.view
<UIView: 0x7fb2a40344a0; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x7fb2a4018c80>>

thread

有时候我们想要了解线程堆栈信息,可以使用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查看堆栈调用
(lldb) thread backtrace
* thread #1: tid = 0xdd42, 0x000000010afb380b libobjc.A.dylibobjc_msgSend + 11, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=EXC_I386_GPFLT) frame #0: 0x000000010afb380b libobjc.A.dylibobjc_msgSend + 11
* frame #1: 0x000000010aa9f75e TLLDB-[ViewController viewDidLoad](self=0x00007fa270e1f440, _cmd="viewDidLoad") + 174 at ViewController.m:23 frame #2: 0x000000010ba67f98 UIKit-[UIViewController loadViewIfRequired] + 1198
frame #3: 0x000000010ba682e7 UIKit-[UIViewController view] + 27 frame #4: 0x000000010b93eab0 UIKit-[UIWindow addRootViewControllerViewIfPossible] + 61
frame #5: 0x000000010b93f199 UIKit-[UIWindow _setHidden:forced:] + 282 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返回表达式的值。

例如我们有个方法:
- (NSInteger)testThreadReturn{
NSLog(@"thread return");

    return 10;
}

我们如果想让结果返回20, 首先在方法体的第一行打一个断点,当程序运行到方法时会暂停,这时候我们在LLDB窗口里输入:
(lldb) thread return 20

c & n & s & finish

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



这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

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


frame variable

通过frame variable命令,可以打印出当前frame的所有变量。
(lldb) frame variable
(ViewController *) self = 0x00007fd2f511cf60
(SEL) _cmd = "viewDidLoad"
(NSInteger) res = 0
可以看到,他将self,_cmd,res等本地变量都打印了出来。
如果我们要需要打印指定变量,也可以给frame variable传入参数:
(lldb) frame variable self->_count
(NSInteger) self->_count = 0
不过frame variable只接受变量作为参数,不接受表达式,也就是说我们无法使用frame variable self.string,因为self.string是调用string的getter方法。所以一般打印指定变量,我更喜欢用p或者po。

其他不常用命令

一般frame variable打印所有变量用得比较多,frame还有2个不怎么常用的命令:
frame info: 查看当前frame的信息。
(lldb) frame info
frame #0: 0x0000000101bf87d5 TLLDB-[ViewController text:](self=0x00007fa158526e60, _cmd="text:", ret=YES) + 37 at ViewController.m:38 frame select: 选择某个frame。 (lldb) frame select 1 frame #1: 0x0000000101bf872e TLLDB-[ViewController viewDidLoad](self=0x00007fa158526e60, _cmd="viewDidLoad") + 78 at ViewController.m:23
20
21 - (void)viewDidLoad {
22 [super viewDidLoad];
-> 23 [self text:YES];
24 NSLog(@"1");
25 NSLog(@"2");
26 NSLog(@"3");

breakpoint

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

breakpoint set

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

  • 使用-n根据方法名设置断点:
    e.g: 我们想给所有类中的viewWillAppear:设置一个断点:
    (lldb) breakpoint set -n viewWillAppear:
    Breakpoint 13: 33 locations.
  • 使用-f指定文件:
    e.g: 我们只需要给ViewController.m文件中的viewDidLoad设置断点:
    (lldb) breakpoint set -f ViewController.m -n viewDidLoad
    Breakpoint 22: where = TLLDB`-[ViewController viewDidLoad] + 20 at ViewController.m:22, address = 0x000000010272a6f4
    这里需要注意,如果方法未写在文件中(比如写在category文件中,或者父类文件中),指定文件之后,将无法给这个方法设置断点。
  • 使用-l指定文件某一行设置断点:
    e.g: 我们想给ViewController.m第38行设置断点
    (lldb) breakpoint set -f ViewController.m -l 38
    Breakpoint 23: where = TLLDB`-[ViewController text:] + 37 at ViewController.m:38, address = 0x000000010272a7d5
  • 使用-c设置条件断点:
    e.g: text:方法接受一个ret的参数,我们想让ret == YES的时候程序中断:
    (lldb) breakpoint set -n text: -c ret == YES
    Breakpoint 7: where = TLLDB`-[ViewController text:] + 30 at ViewController.m:37, address = 0x0000000105ef37ce
  • 使用-o设置单次断点:
    e.g: 如果刚刚那个断点我们只想让他中断一次:
    (lldb) breakpoint set -n text: -o
    '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: 假设我们需要在ViewController的viewDidLoad中查看self.view的值。
我们首先给-[ViewController viewDidLoad]添加一个断点。
(lldb) breakpoint set -n "-[ViewController viewDidLoad]"
'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为3的breakpoint增加命令。
添加完命令之后,每次程序执行到这个断点就可以自动打印出self.view的值了。
如果我们一下子想增加多条命令,比如我想在viewDidLoad中打印当前frame的所有变量,但是我们不想让他中断,也就是在打印完成之后,需要继续执行。我们可以这样写:
(lldb) breakpoint command add 3
Enter your debugger command(s). Type 'DONE' to end.
> frame variable
> continue
> DONE
多次对同一个断点添加命令,后面命令会将前面命令覆盖。

breakpoint command list

如果想查看某个断点已有的命令,可以使用breakpoint command list。
e.g: 我们查看一下刚刚的断点3已有的命令
(lldb) breakpoint command list 3
'breakpoint 3':
Breakpoint commands:
frame variable
continue
可以看到一共有2条命令,分别为frame variable和continue。

breakpoint command delete

有增加就有删除,breakpoint command delete可以让我们删除某个断点的命令。
e.g: 我们将断点3中的命令删除:
(lldb) breakpoint command delete 3
(lldb) breakpoint command list 3
Breakpoint 3 does not have an associated command.

breakpoint list

如果我们想查看已经设置了哪些断点,可以使用breakpoint list。
(lldb) breakpoint list
Current breakpoints:
4: name = '-[ViewController viewDidLoad]', locations = 1, resolved = 1, hit count = 0
4.1: where = TLLDB`-[ViewController viewDidLoad] + 20 at ViewController.m:23, address = 0x00000001055e6004, resolved, hit count = 0

breakpoint disable/enable

有的时候我们可能暂时不想要某个断点,可以使用breakpoint disable让某个断点暂时失效。
(lldb) breakpoint disable 4
1 breakpoints disabled.
当我们又需要这个断点的时候,可以使用breakpoint enable再次让他生效
(lldb) breakpoint enable 4
1 breakpoints enabled.

breakpoint delete

如果我们觉得这个断点以后再也用不上了,可以用breakpoint delete直接删除断点.
e.g: 删除断点4
(lldb) breakpoint delete 4
1 breakpoints deleted; 0 breakpoint locations disabled.
如果我们想删除所有断点,只需要不指定breakpoint delete参数即可
(lldb) breakpoint delete
About to delete all breakpoints, do you want to do that?: [Y/n] y
All breakpoints removed. (1 breakpoint)
未完待续。。。

推荐阅读更多精彩内容

  • LLDB的Xcode默认的调试器,它与LLVM编译器一起,带给我们更丰富的流程控制和数据检测的调试功能。平时用Xc...
    CoderSC阅读 340评论 0 0
  • LLDB的Xcode默认的调试器,它与LLVM编译器一起,带给我们更丰富的流程控制和数据检测的调试功能。平时用Xc...
    小笨狼阅读 15,750评论 31 182
  • 转载 与调试器共舞 - LLDB 的华尔兹: https://objccn.io/issue-19-2/ 推荐:i...
    F麦子阅读 2,228评论 0 9
  • [转]浅谈LLDB调试器文章来源于:http://www.cocoachina.com/ios/20150126/...
    loveobjc阅读 1,725评论 2 6
  • 炖了锅红豆汤。放了把薏米。添了几颗花生。还有燕麦。当然少不了一整块冰糖。炖啊炖。从昨夜炖到今晚。 趁着甜心睡着。我...
    童话镇的花姑娘阅读 81评论 0 0