iOS/OSX 调试:跳舞吧!与LLDB共舞华尔兹

原文连接:http://www.objc.io/issue-19/lldb-debugging.htmlhtml

// 速翻,无校对版

前言

你是否呕心沥血的尝试去理解代码和打印出来的变量内容?git

NSLog(@"%@", whatIsInsideThisThing);

或是漏过函数调用来就简化工程行为?github

NSNumber *n = @7; // theFunctionThatShouldReallyBeCalled();

或者短路的检查逻辑?shell

if (1 || theBooleanAtStake) { ... }

亦或者是函数的伪实现?express

int calculateTheTrickyValue {
  return 9;

  /*
   Figure this out later.
   ...
}

那是否是要不断的重编译,而后又开始新的轮回?数组

构建软件是复杂的并且BUG无处不藏。一个正常的修正过程是修改代码,编译,再次运行,而后祈祷上帝。数据结构

彷佛也不用墨守成规。你能够用调试器啊!假设你已经知道怎么检视变量值,这里有更多你须要掌握的东西。架构

这篇文章的目的是挑战你的调试知识,把你可能知道得基础知识点解析的更透彻,而后向你展现了一系列有趣的栗子。开始吧!ide

LLDB

LLDB是个开源调试器,REPL特性,自带C++以及Python插件。它与Xcode绑定而且驻在控制台界面化于窗口的下端。函数

调试器容许你在一个特定执行时刻暂停程序,检视变量值,执行自定义命令,以及按你认为合适得步骤进行程序步骤操控。(调试器主要功能戳这里

你之前使用调试器的部分极可能仅限于Xcode的UI上打个断点。可是这有些技巧,你能够作一些更酷比的事情。经过GDB与LLDB之间对比是针对全部支持的命令行的一个很好鸟瞰式的学习法,你还可能想要去安装Chisel,一套开源的LLDB插件让你的调试更加有趣。

与此同时,让咱们开始如何使用调试器打印变量值的旅程吧。

基础

这里有一个简单短小的程序来打印字符串。注意到断点被添加到了第八行:
图片描述
程序到此会停下来而后打开控制台,让咱们能与调试器进行交互。此时咱们应该输入什么呢?

帮助

最简单得命令是键入help,你能够获取一个命令行列表。若是你忘记一个命令或者想知道该命令更细致的使用方法,那么你能够经过调用help <command>,好比help printhelp thread。若是你甚至忘记了命令自己,你能够尝试使用help help,可是若是你懂得足够多,你可能已经完全不要这个命令了。

打印

打印值很容易,只要试着键入print命令:
图片描述
LLDB实际上支持前缀命令判断,因此你一样可使用prin, pri或者p。可是你不能使用pr,由于LLDB不能分辨出你是不是想执行process命令。(吐槽幸亏p没有歧义,暴露属性)

你同时也注意到告终果带一个$0。实际上你能够用这个来引用变量!试着键入$0 + 7而后你会看到106。任何带美圆符号是LLDB的命名空间,其存在是为了为你提供帮助。

表达式

若是你想修改一个值?修改,你说的算?好吧,修改!下面来一个简单得表达式命令行:
图片描述
这并不修改调试器中的值。实际上修改的是程序中的值!若是你继续程序,它很神奇地会打印出42红气球(上下文)。

从如今开始注意一点,咱们为了方便用pe代替printexpression

什么是打印命令?

这里有一个有意思的表达式来考虑下:p count = 18。若是咱们执行命令而后打印count的内容,咱们会看到它确实至关于执行了表达式count = 18

这二者的区别是print命令不带参数,这点与expression不一样。考虑e -h +17。在选择是否要进行输入源为+17,带-h标志的操做,仍是选择是否要进行计算区分17h操做,在这两个选择上面是不明确的。调试器认为连字符致使了混淆,你可能得不到想要的结果。

幸运的是,这个解决方法十分简单。使用--来表示表示符号的结束以及输入源的开始。此时若是你想要用-h标志,你可使用e -h -- +17,若是你想要进行区分,则你能够执行e -- -h +17。不带标志则是十分普通,它(e --)有一个别名print

若是你键入help print而且往下拖拽,你会看到:

'print' is an abbreviation for 'expression --'.

打印对象

若是咱们尝试键入

p objects

那输出会有点冗繁:

(NSString *) $7 = 0x0000000104da4040 @"red balloons"

当尝试打印一个更加复杂的数据结构时候会状况会更糟:

(lldb) p @[ @"foo", @"bar" ]

(NSArray *) $8 = 0x00007fdb9b71b3e0 @"2 objects"

好吧,咱们想看下对象的description方法。咱们须要告诉expression命令做为对象来打印这个结果,使用-O标志(这不是0):

(lldb) e -O -- $8
<__NSArrayI 0x7fdb9b71b3e0>(
foo,
bar
)

很走运,e -O --也有别名,其别名为po,咱们能够只要这样使用:

(lldb) po $8
<__NSArrayI 0x7fdb9b71b3e0>(
foo,
bar
)
(lldb) po @"lunar"
lunar
(lldb) p @"lunar"
(NSString *) $13 = 0x00007fdb9d0003b0 @"lunar"

打印变量

print命令有许多种不一样的格式能够由你来指定。它们以命令格式为print/<fmt>或者更简单p/<fmt>。接下来举个栗子。

默认的格式:

(lldb) p 16
16

16进制格式:

(lldb) p/x 16
0x10

二进制格式(t表明tow):

(lldb) p/t 16
0b00000000000000000000000000010000
(lldb) p/t (char)16
0b00010000

你还可使用p/c打印字符,或者是p/s打印一个非终止类型的字符串char *。完整列表戳这里

变量

至此你能够打印对象跟简单得类型,并能够在调试器中使用expression命令更改它们的值,让咱们使用一些变量来减小咱们输入工做。你能够声明一个变量C来表示int a = 0,一样你能够在LLDB中作一样的事情。而后,变量必须以美圆符号做为开头:

(lldb) e int $a = 2
(lldb) p $a * 19
38
(lldb) e NSArray *$array = @[ @"Saturday", @"Sunday", @"Monday" ]
(lldb) p [$array count]
2
(lldb) po [[$array objectAtIndex:0] uppercaseString]
SATURDAY
(lldb) p [[$array objectAtIndex:$a] characterAtIndex:0]
error: no known method '-characterAtIndex:'; cast the message send to the method's return type
error: 1 errors parsing expression

噢。LLDB不能识别出所牵扯的变量类型。不时会遇到,咱们能够给一点提示:

(lldb) p (char)[[$array objectAtIndex:$a] characterAtIndex:0]
'M'
(lldb) p/d (char)[[$array objectAtIndex:$a] characterAtIndex:0]
77

变量特性让调试器更容易被使用,你这么认为吗?

流程控制

你的程序会在你打上断点的位置停下来。

此时你看到在调试工具栏有四个按钮,经过使用它们你能够控制程序的执行流程:
图片描述

这四个按钮从左到右依次为:继续,单步,跳入,跳出。

首先,继续按钮将会让你得程序继续正常执行(可能一直运行或者遇到下一个断点)。在LLDB中,你可使用process continue来继续执行,别名为c

其次,单步执行将会将单行代码当作黑盒同样执行。若是那行你调用了函数,那将不会进入这个函数,而是直接执行这个函数后继续运行。LLDB中相对应的命令是thread step-overnext,或者 n

若是你想进入一个函数调用来检查调试该函数的执行,你可使用第三个按钮,跳入,LLDB一样提供了thread step-instep, 和s。注意到nextstep在当前行代码不涉及函数调用的时候效果是同样的。

大部分知道c,n,s。可是还有第四个按钮,跳出。若是你不当心跳入了一个函数而你本意是想跳过它,通常反应是不断的按n知道函数返回。跳出帮你节省时间。它会执行到return语句(知道执行了出栈操做),而后会停下来。

举个栗子

来看下以下的代码片断:
图片描述

代码停在断点,而后咱们执行以下的命令行:

p i
n
s
p i
finish
p i
frame info

这里,frame info将会告诉你当前行以及源文件是啥,能够经过键入help framehelp thread,以及help process获取更多信息。那么输出什么呢?先思考以前的描述想下答案!

(lldb) p i
(int) $0 = 99
(lldb) n
2014-11-22 10:49:26.445 DebuggerDance[60182:4832768] 101 is odd!
(lldb) s
(lldb) p i
(int) $2 = 110
(lldb) finish
2014-11-22 10:49:35.978 DebuggerDance[60182:4832768] 110 is even!
(lldb) p i
(int) $4 = 99
(lldb) frame info
frame #0: 0x000000010a53bcd4 DebuggerDance`main + 68 at main.m:17

仍在17行的缘由是finish命令会让程序运行直到isEven()函数返回,而后立刻中止。可是请注意,17行已经执行完了。

线程返回

还有一个特别帮的功能是你在调试的时候能够用thread return来控制程序流程。它使用可选参数,将这个参数载入寄存器,单后立刻执行返回命令,而后函数出栈。这意味着剩下函数没有被执行。这样由于ARC的引用计数/记录出现问题,或者遗漏一些清除操做。但在一个函数的开头执行这个命令是一个很是棒得函数打桩而且反悔了一个伪结果。

让咱们来对上述相同的代码段跑以下的指令:

p i
s
thread return NO
n
p even0
frame info

在看答案以前乡下结果,答案以下:

(lldb) p i
(int) $0 = 99
(lldb) s
(lldb) thread return NO
(lldb) n
(lldb) p even0
(BOOL) $2 = NO
(lldb) frame info
frame #0: 0x00000001009a5cc4 DebuggerDance`main + 52 at main.m:17

断点

咱们一直都使用断点来让程序中止,检视当前状态从而捕获BUG。可是若是咱们转变对断点的理解,咱们能够得到更多可能。

A breakpoint allows you to instruct a program when to stop, and then allows the running of commands.

考虑在函数刚开始处打一个断点,使用thread return来重写函数行为,而后继续。如今想象下自动实现这种处理。是否是听起来很牛X,不是么?

断点管理

Xcode提供了一套工具来建立和操做断点。咱们将会逐一过一遍而且进行描述与之对应的LLDB命令行。

在Xcode的左面板上,有一堆按钮集合。有一个长得很像断点。点击打开断点导航栏,进去以后你一眼看到你所操做的全部断点:
图片描述

这里你能够看到全部的断点 - 对应LLDB中的breakpoint list或者是br li。你能够点击单个断点进行打开或者关闭 - 对应LLDB中的breakpoint enable <breakpointID>breakpoint disable <breakpointID>

(lldb) br li
Current breakpoints:
1: file = '/Users/arig/Desktop/DebuggerDance/DebuggerDance/main.m', line = 16, locations = 1, resolved = 1, hit count = 1

  1.1: where = DebuggerDance`main + 27 at main.m:16, address = 0x000000010a3f6cab, resolved, hit count = 1

(lldb) br dis 1
1 breakpoints disabled.
(lldb) br li
Current breakpoints:
1: file = '/Users/arig/Desktop/DebuggerDance/DebuggerDance/main.m', line = 16, locations = 1 Options: disabled

  1.1: where = DebuggerDance`main + 27 at main.m:16, address = 0x000000010a3f6cab, unresolved, hit count = 1

(lldb) br del 1
1 breakpoints deleted; 0 breakpoint locations disabled.
(lldb) br li
No breakpoints currently set.

建立断点

(UI建立略了。。。是人都会吧。。)

在调试器中打断点,使用breakpoint set命令:

(lldb) breakpoint set -f main.m -l 16
Breakpoint 1: where = DebuggerDance`main + 27 at main.m:16, address = 0x

缩写能够用brb是另一个彻底不一样的命令,是_regexp-break的别名,可是它足够健壮来进行建立上述命令同样效果的断点:

(lldb) b main.m:17
Breakpoint 2: where = DebuggerDance`main + 52 at main.m:17, address = 0x

你也能够防止一个断点在一个符号(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

如今这些断点会中止正在将要执行的函数,一样适用与OC方法:

(lldb) breakpoint set -F "-[NSArray objectAtIndex:]"
Breakpoint 5: where = CoreFoundation`-[NSArray objectAtIndex:], address = 0x000000010ac7a950
(lldb) b -[NSArray objectAtIndex:]
Breakpoint 6: where = CoreFoundation`-[NSArray objectAtIndex:], address = 0x000000010ac7a950
(lldb) breakpoint set -F "+[NSSet setWithObject:]"
Breakpoint 7: where = CoreFoundation`+[NSSet setWithObject:], address = 0x000000010abd3820
(lldb) b +[NSSet setWithObject:]
Breakpoint 8: where = CoreFoundation`+[NSSet setWithObject:], address = 0x000000010abd3820

若是你想经过UI来建立象征性断点,你能够点击左下端断点导航栏的+号:
图片描述

而后选择第三个选项:
图片描述
此时出现弹出框让你输入好比-[NSArray objectAtIndex:]的符号,而后程序在这个函数调用的时候即可以中止下来,不论是你的代码或者仍是大苹果的代码!

若是咱们看下其余选项,咱们能够发现一些有意思的选项,一样提供了各类条件触发的锻炼只要你点击了Xcode的UI而且选择了“Edit Breakpoint”选项:
图片描述

如上图,断点只有在i为99的时候才会中止程序。你能够一样设置“ ignore”选项来告诉断点在前n次调用的时候不用中止程序(条件为真)。

这里还有一个“Add Action”按钮。。。

断点动做

可能上面断点的栗子中,你想知道每次断点时候i值是多少。咱们可使用动做p i,而后当断点触发的时候咱们进入调试器,它会预先执行这个命令在将控制流程交给你以前:
图片描述
你也能够加多重动做,能够是调试器指令,shell指令或者更健壮的打印信息:
图片描述

如上你能够看到打印出i值,还有强调语句,打印出自定义的表达式。

下面是上述功能用纯LLDB命令代替Xcode的UI:

(lldb) breakpoint set -F isEven
Breakpoint 1: where = DebuggerDance`isEven + 16 at main.m:4, address = 0x00000001083b5d00
(lldb) breakpoint modify -c 'i == 99' 1
(lldb) breakpoint command add 1
Enter your debugger command(s).  Type 'DONE' to end.
> p i
> DONE
(lldb) br li 1
1: name = 'isEven', locations = 1, resolved = 1, hit count = 0
    Breakpoint commands:
      p i

Condition: i == 99

  1.1: where = DebuggerDance`isEven + 16 at main.m:4, address = 0x00000001083b5d00, resolved, hit count = 0

自动化,咱们来了!

计算值以后继续

若是视线停留在断点弹出框的底端,你会额外看到一个选项:“Automatically continue after evaluation actions(计算动做后自动执行)。”它只是一个勾选框,可是它却有强大的能力。若是你勾选上了,调试器将会苹果你全部的命令而后继续执行程序。表面上看上跟断点没有打住同样(除非你断点太多了,拖慢了程序进度)。

这个勾选框功能与最后一个动做断点继续执行效果同样,可是有勾选框更加容易点。对应调试器的指令以下:

(lldb) breakpoint set -F isEven
Breakpoint 1: where = DebuggerDance`isEven + 16 at main.m:4, address = 0x00000001083b5d00
(lldb) breakpoint command add 1
Enter your debugger command(s).  Type 'DONE' to end.
> continue
> DONE
(lldb) br li 1
1: name = 'isEven', locations = 1, resolved = 1, hit count = 0
    Breakpoint commands:
      continue

  1.1: where = DebuggerDance`isEven + 16 at main.m:4, address = 0x00000001083b5d00, resolved, hit count = 0

计算后自动继续运行让你能够单独经过使用断点来修改你的程序!你能够中止在单行,运行一个expression命令来改变变量,而后继续。

举个栗子

考虑下简陋残酷的“打印式调试”技术。不是用:

NSLog(@"%@", whatIsInsideThisThing);

而是用断点处设置打印变量值替代吊打印日志打印语句而后继续。

不是用:

int calculateTheTrickyValue {
  return 9;

  /*
   Figure this out later.
   ...
  */
}

而是用断点处调用thread return 9而后继续执行。

带动做的象征断点确实真的很强大。你也能够添加这些断点到你朋友的Xcode工程而且让动做将全部信息细致展现出来。接下来看看要耗时多久来进行计算以及会发生什么吧。

调试器完整操做

在起舞以前还有一点须要咱们注意。你真的能够在调试器中执行任何的C/OC/C++/Swift命令。比较弱的是咱们不能建立一个新的函数。。。这意味着没有新的类,块,函数,带虚方法的C++类等等。除了这个,调试器什么都能知足!

咱们能够分配一些字节:

(lldb) e char *$str = (char *)malloc(8)
(lldb) e (void)strcpy($str, "munkeys")
(lldb) e $str[1] = 'o'
(char) $0 = 'o'
(lldb) p $str
(char *) $str = 0x00007fd04a900040 "monkeys"

或者咱们能够检查一些内存(使用x命令)来看咱们新数组的4个字节:

(lldb) x/4c $str
0x7fd04a900040: monk

咱们还能够后三个字节:

(lldb) x/1w `$str + 3`
0x7fd04a900043: keys

当你所要的活结束的时候别忘记了释放内存避免形成内存泄露:

(lldb) e (void)free($str)

跳舞吧,骚年!

如今咱们已经清楚基础步骤,是时候来整一些比较疯狂的东西了。我过去曾写过一篇博客(你们本身收藏。。。)发表在looking at the internals of NSArray。当时用了大量的NSLog语句,后来全用调试器搞定了。它是一个很好的调试器使用练习。

畅通无阻(无断点模式)

当你的应用在跑的时候,Xcode中的调试工具栏展现一个中止按钮而非继续状态的按钮:
图片描述

选中这个按钮的时候,应用遇到断点将会中止(就像输入了process interrupt)。这时候将会让你进入调试器。

这里有一个有趣的地方。若是你运行一个iOS应用,你能够尝试这个(全局变量可提供)

(lldb) po [[[UIApplication sharedApplication] keyWindow] recursiveDescription]
<UIWindow: 0x7f82b1fa8140; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 0x7f82b1fa92d0>; layer = <UIWindowLayer: 0x7f82b1fa8400>>
   | <UIView: 0x7f82b1d01fd0; frame = (0 0; 320 568); autoresize = W+H; layer = <CALayer: 0x7f82b1e2e0a0>>

能够看到整个层级!Chisel(上文说起)用pviews来实现。

更新UI

而后,经过上述的输出,咱们能够看到隐藏的视图:

(lldb) e id $myView = (id)0x7f82b1d01fd0

而后在调试器中修改它的背景色:

(lldb) e (void)[$myView setBackgroundColor:[UIColor blueColor]]

在你下次继续运行这个程序的时候你才会看到变化。这由于这个变化须要传递给渲染服务而后视图展现才会被更新。

渲染服务其实是另外一个进程(称做后台),而且甚至咱们调试进程被中止了,这个后台也不会被中止!

这意味着不经过继续,你能够执行:

(lldb) e (void)[CATransaction flush]

在模拟器中或者设备中的UI会进行刷新而你还在调试器中!Chisel提供了一个别名函数叫作caflush,而且它被用来实现其它捷径像hide <view>show <view>还有其余许多许多。全部的Chisel命令都有对应的文档,因此就在安装它以后键入help来为所欲为的获取更多的信息吧。

压入视图控制器

想象一个简单的应用有一个UINavigationController做为根视图控制器。你能够在调试器中至关简易的执行以下操做:

(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]

你会看到立刻压入了一个视图控制器。

找到按钮的目标

想象下你调试器中有一个变量,$myButton,你想要去建立它,并从UI中抓取它,或者简单地只是你想在断点停下来的时候将它做为个局部变量。你可能想知道当你点击它的时候是谁接收了这个动做。这里展现达到这点有多么的简单:

(lldb) po [$myButton allTargets]
{(
    <MagicEventListener: 0x7fb58bd2e240>
)}
(lldb) po [$myButton actionsForTarget:(id)0x7fb58bd2e240 forControlEvent:0]
<__NSArrayM 0x7fb58bd2aa40>(
_handleTap:
)

如今你可能想在事件发生的时候添加一个断点。只要在LLDB或者Xcode设置象征性断点在-[MyEventListener _handleTap:]。and you are all set to go!

观察实例变量值变化

想象一个假设的场景你有一个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

对应Chisel里面的wivar $myView _layer

在非重写方法上的象征性断点

想象你想知道何时-[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找到它)。这是个经过的历程,由于你知道已经知道有四种体系架构了。吐槽略。。幸运的是,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上输入脚本,它会打开一个Python REPL。若是你在LLDB中键入script,它会打开一个Python REPL。你能够传入一行Python语句到script命令来不进入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

或者,将这行代码放置于/.lldbinit让LLDB每次运行的时候都执行一次。Chisel不过就是一堆Python脚本用来组合字符串,而后告诉LLDB来执行这些字符串。听起来很简单吧!呃?

驰骋调试器

略。。。 “乐观”调试!

相关文章
相关标签/搜索