不管你使用的是Swift, Objective-C, C++, C,或者其他的编程语言, 你都需要学习如何创建一个断点.在Xcode这样的GUI程序中, 在编辑界面的左边点一下创建一个断点是非常简单的, 但是在LLDB控制台中可以让你更灵活的控制断点.
在本章中, 你将会学到LLDB中所有关于断点的知识.
Signals
在本章中, 你将会使用我提供的一个工程; 这个工程的名字叫做Signals, 你可以在资源文件夹里面找到它.
用Xcode打开Signals项目.Signals是一个以美式橄榄球项目为主题的APP, 显示一些叫做nerdily的攻击型打法.
在程序内部, 这个项目会监测几个Unix signals并在Signals程序收到这些信号的时候显示这些信号.
Unix signals
是进程之间通信的基本形式. 例如, signals中的SIGSTOP
可以保存一个进程的状态并且暂停进程的执行.与之对应的是SIGCONT
, 他告诉程序继续执行.这两个signals都可以被调试器用来暂停或继续程序的执行.
在前面这些章节中这算是一个有趣的程序, 因为它不仅用来浏览Unix 处理signal, 而且会高亮显示(LLDB)控制的线程在处理经过的Unix signals时发生了什么.默认情况下, LLDB在处理不同的signals时有自定义的动作.当附加了LLDB以后有些signals是不会经过被控制的线程的.
为了显示signal
, 你既可以在应用程序中增加一个Signal, 也可以在外部的应用程序中发送一个signal
, 比如在Terminal中.
此外, 这里有一个UISwitch
可以切换处理signal的代码块, 代码块的名字叫做sigprocmask
来启用或者禁用signal
的处理.
最后, Signal应用程序还有在应用程序里增加SIGSTOP
的Timeout按钮, 从根本上冻结应用程序.然而, 如果LLDB附加到了Signals应用程序上 (并且在默认情况下就是这样做的, 当你通过Xcode来构建和运行Signals的时候), 调用SIGSTOP
可以让你在Xcode里用LLDB检查线程的至此NG状态.
确保选中的是iPhone 7 Simulator. 构建并运行这个APP.一旦项目运行起来了以后, 在Xcode的控制台里找到并暂停调试器.
继续运行Xcode并留意观察模拟器.当调试器暂停然后继续执行的时候一行新数据会被添加到UITableView上. 这是Signals监测器监测到Unix signal的SIGSTOP事件时添加的一个数据.当线程被停止的时候, 任何新的
signals
都不会被立即处理, 因为可以认为程序 已经停止了.
Xcode断点
在你学完之前, LLDB控制台的断点会是一个亮点, 非常值得用来替代你再Xode中学到的断点;
Symbolic breakpoints 是一个Xcode中一个非常好的调试功能.它允许你在应用程序中的某些symbol处设置断点.一个非常简单的例子就是-[NSObject init]
, 用的是NSObject实例的 init
方法.
在Xcode中可以灵活的使用symbolic
断点, 一旦你创建了一个断点, 下次你启动应用程序的时候可以不用重新输入.
现在你将会用symbolic断点来显示出所有被创建的NSObject的实例.
如果APP正在运行那么就杀掉APP的进程.然后切换到Breakpoint导航栏.在左下角点击+按钮, 并选择 Symbolic Breakpoint选项.
会出现一个弹窗.在弹窗的Symbol部分输入:
-[NSObject init]
.在Action下面, 选择Add Action
然后在下拉选项中选择Debugger Command
, 接下来在输入框中输入po [$arg1 class]
.最后,选择
Automatically continue after evaluating actions
. 你的弹窗看起来应该是下面的样子:
构建并运行APP.当Signals程序运行的时候Xcode将会在控制台输出初始化的所有的类名, 你会看到非常的多.
在这里你设置了一个
-[NSObject init]
每调用一次都会触发的断点.当断点触发的时候LLDB会运行一条指令, 并自动让应用程序继续执行.
注意: 在第十章“Assembly, Registers and Calling Convention”中你将会学到如果恰当的使用和操纵寄存器, 但是现在只需要简单的知道`$arg1`与寄存器中的`$rdi`的同义词, 可以简单的看作是调用`init`方法的类的一个实例.
如果你完成了查看所有类名的提取操作, 然后用在breakpoint导航栏中点击右键选择删除断点的方法删除断点.
除了symbolic断点, Xcode还支持几种其他错误类型的断点.其中有一个叫Exception Breakpoint
.有时候你的程序会抛出一些错误,这些错误会导致崩溃.你的第一反应应该就是启用exception breakpoint
. Xcode将会自动定位到出问题的那行代码, 这对于捕获崩溃有很大的帮助.
最后, 还有一个断点类型是Swift Error Breakpoint
.这种类型的断点本质上是在在swift_willThrow
方法中创建一个断点并在swift抛出异常的时候触发.在我们使用那些容易出错的APIs的时候这是我们可以用的一个非常好的选项.它可以让你在没有对代码的对错做出判断的时候快速的诊断出现的问题.
LLDB断点语法
现在你已经掌握了Xcode中调试崩溃功能的课程, 是时候学习一下如何通过LLDB控制台创建断点了. 为了创建有用的断点, 你需要学习一下如何查询你要找的东西.
image
命令是一个查看内部详情的极其有用的工具, 对于设置断点来说是至关重要的.
本书中在搜索代码的时候有两个至关重要的配置.第一个就是:
(lldb) image lookup -n "-[UIViewController viewDidLoad]"
这个命令可以提取出-[UIViewController viewDidLoad]
函数的加载地址.-n
参数告诉LLDB可以查找symbol或者函数名.输出的可能是下面这个样子:
1 match found in /Applications/Xcode.app/Contents/Developer/Platforms/
iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk//System/
Library/Frameworks/UIKit.framework/UIKit:
Address: UIKit[0x00000000001c67c8] (UIKit.__TEXT.__text +
1854120)
Summary: UIKit`-[UIViewController viewDidLoad]
另外一个与之相似并非常有用的命令是:
(lldb) image lookup -rn test
这条命用来查找test
这个单词的而且是区分大小写的.如果小写的单词test
在当前的可执行文件中加载的任何模块中的任何函数里(例如:UIKit, Foundation, Core Data, 等等) 找到了, 这条命令都会输出出来.
注意:当你想要提取匹配到的内容的参数时候使用`-n`选项(如果你查询的内容中包含空格则需要用引号引起来).`-n`只能帮助你提取出匹配到的断点的准确的参数, 尤其是处理swift的时候, 同时`-rn`选项在本书中将会经常用到, 因为你很快就会发现一个漂亮的正则表达式可以减少许多输入.
Objective-C的属性
学习如何在代码中查询加载的代码是学习如何创建断点的最终目标.
无论是Objective-C 还是Swift代码在被编译器创建的时候都有指定的属性签名, 我们需要使用不同的断点策略.
例如在Signals项目中下面的Objective-C类声明了一个属性:
@interface TestClass : NSObject
@property (nonatomic, strong) NSString *name;
@end
编译器将会为这个属性创建getter
方法和setter
方法.
getter看起来是这个样子:
-[TestClass name]
setter
方法看起来是这个样子:
-[TestClass setName:]
构建并运行APP, 然后暂停调试器. 接下来在LLDB中键入以下命令验证一下这些方法是存在的:
(lldb) image lookup -n "-[TestClass name]"
在控制台中你将会看到下面这些输出:
1 match found in /Users/derekselander/Library/Developer/Xcode/
DerivedData/Signals-bqrjxlceauwfuihjesxmgfodimef/Build/Products/Debug-
iphonesimulator/Signals.app/Signals:
Address: Signals[0x0000000100001470] (Signals.__TEXT.__text + 0)
Summary: Signals`-[TestClass name] at TestClass.h:28
LLDB将会提取出可执行文件中包含的函数的信息.这些代码看起来有点吓人, 但是却有一些有用的信息在里面.
这些输出告诉你LLDB在Signals可执行文件中能够找到这些函数的实现.并且在__TEXT
分段中准确的偏移量是0x0000000100001470
.LLDB还告诉我们这个方法生命在TestClass.h
文件中的第28行.
你同样可以用下面的方法查看setter
方法:
(lldb) image lookup -n "-[TestClass setName:]"
你应该会得到和之前类似的输出, 这一次显示的是name
这个属性setter
方法的实现和实现的地址.
Swift 属性
在Swift中声明属性的语法有些不同. 看一下SwiftTestClass.swift
文件你会发现下面的代码:
class SwiftTestClass: NSObject {
var name: String!
}
确保Signals项目正在运行并且暂停在LLDB中. 你可以按下Command + K
快速的清空LLDB的控制台.
在LLDB控制台中输入以下命令:
(lldb) image lookup -rn Signals.SwiftTestClass.name.setter
你将会得到一些类似下面的输出:
2 matches found in /Users/derekselander/Library/Developer/Xcode/
DerivedData/Signals-bqrjxlceauwfuihjesxmgfodimef/Build/Products/Debug-
iphonesimulator/Signals.app/Signals:
Address: Signals[0x000000010000aba0] (Signals.__TEXT.__text +
38704)
Summary: Signals`@objc Signals.SwiftTestClass.name.setter :
Swift.ImplicitlyUnwrappedOptional<Swift.String> at SwiftTestClass.swift
Address: Signals[0x000000010000ac60] (Signals.__TEXT.__text + 38896)
Summary: Signals`Signals.SwiftTestClass.name.setter :
Swift.ImplicitlyUnwrappedOptional<Swift.String> at SwiftTestClass.swift
在输出中找到Summary这个单词.这里有两个有趣的事情需要注意.
首先, 有两个symbols被发现了.第一个和第二个有着同样的名字;然而, 第一个有@objc
的前缀. 这是编译器加上去的特定的函数是用来作为一个桥接函数的. 这有助于swift和Objective-C的混编.
第二, 你看到了函数名字是多么的长吗?要创建一个有效的Swift断点就需要这么一个完整的名字.
如果你想在这个setter
方法中设置一个断点, 你需要按照下面的方法做:
(lldb) b Signals.SwiftTestClass.name.setter :
Swift.ImplicitlyUnwrappedOptional<Swift.String>
使用正则表达式可以减少大量的输入.
撇开你产生的swift函数名的长度不说, 注意一下Swift的属性的形式.在属性name
函数的签名中单词setter
紧跟在属性的后面. 也许getter
方法也有着同样的格式?
尝试捕获一下SwiftTestClass
类的name
属性的getter
和setter
方法, 与此同时, 使用下面的正则表达式来查询:
(lldb) image lookup -rn Signals.SwiftTestClass.name
这条命令使用正则表达式查询并提取Signals.SwiftTestClass.name
中的一切.
由于这是一个正则表达式, 所以.
是一个通配符, 同时在函数签名单重又用来匹配点语法.
你会得到一些合理的输出, 但是每一次在控制台的输出中你都会看到Summary
.你会发现输出匹配了getter
, (Signals.SwiftTestClass.name.getter) , setter
,(Signals.SwiftTestClass.name.setter), @objc对应的桥接符, 以及一个包含materializeForSet
的方法, 这个方法你再后面会学到.
下面是Swift属性函数名的样本:
ModuleName.Classname.PropertyName.(getter|setter)
提取方法,找到样本和缩小你搜索范围的能力, 是在你代码中创建智能断点解开Swift/Objective-C语言奥秘的非常伟大的途径.
最后...创建断点
现在你已经知道了如何在你的代码中查询已经存在的函数和方法, 是时候在这些方法上创建断点了.
如果你的Signals项目正在运行, 停止并重启这个程序, 然后点击暂停按钮停止应用程序并进入LLDB控制台.
有几种不同的方法创建断点.最简单的方法是输入小写字母b
紧跟着后面是函数名.这在Objective-C 和 C当中非常的简单, 因为它的名字很短也很容易输入(例如, -[NSObject init]). 而在C++和swift中却十分复杂, 因为编译器会为函数返回给你一个相当长的名字.
鉴于UIKit主要是用Objective-C
(至少在写这本书的时候是这样实现的), 尝试用 b
参数创建一个断点, 像下面这样:
(lldb) b -[UIViewController viewDidLoad]
你会看到下面这些输出:
Breakpoint 1: where = UIKit`-[UIViewController viewDidLoad], address =
0x0000000102bbd788
每当你创建了一个有效的断点, 控制台都会输出一些关于这个断点的信息.
在这次特定的情况下, 这个断点作为Breakpoint 1
被创建, 因为在这个特定的调试会话中这是创建的第一个断点.当你创建更多断点的时候, 断点ID会不断的增长.
继续运行调试器, 一旦你继续执行一个新的SIGSTOP
会被显示出来.在单元格上点击进入详情的UIViewController.在详情视图控制器调用viewDidLoad
的时候程序应该会暂停.
注意:像许多快捷命令一样, `b`是LLDB命令中一个长单词的缩写. 你可以自己运行help b 命令查看它的帮助, 并学习它很酷的技巧.
除了b
命令以外,还有一个长的breakpoint set
命令, 这个命令有很多可用的选项. 你在后面的几个章节中会用到它. 许多命令都是breakpoint set
命令的分支.
正则断点和范围
另一个极其有用的命令是正则表达式断点rbreak
是breakpoint set -r %1
.你可以用智能的正则表达式断点快速的在你想要停下来的许多地方创建断点.
让我们回头看看之前的很长的那个swift属性name
函数的例子, 与下面的输入不同的是:
(lldb) b Breakpoints.SwiftTestClass.name.setter :
Swift.ImplicitlyUnwrappedOptional<Swift.String>
你可以输入:
(lldb) rb SwiftTestClass.name.setter
尽管上面的命令更短一点, 但是也有一个烦人的地方.这个断点也会捕捉到Objective-C桥接的name
属性的setter
方法, 当这个方法被调用的时候, 会强制停止两次.
你可以给这个断点添加一个参数^(@).*
, 这个参数的本质是说"过滤掉以@xxx开始的函数".在未来的几个章节中, 你将构建一个执行正则搜索的命令自动过滤掉这些桥接函数.
从现在开始你只需要处理两个断点. 另一种更加简洁的写法是, 下面这种形式:
(lldb) rb name\.setter
这会在任何包含name.setter
的地方创建一个断点.这在你知道项目里没有其他地方swift属性的明知叫做name
的时候是行的通的.否则的话你就会在在每一处都创建一个断点.
现在尝试在UIViewController
的每一个Objective-C 的实例方法里创建一个断点. 我们可以在LLDB中输入下面的指令:
(lldb) rb '\-\[UIViewController\ '
反斜杠是转义字符用来指明你再正则表达式中想要搜索的原意字符.那么结果就是, 这条指令会在每一个字符串里包含-[UIViewController
后面跟着一个空格的地方停下来.
等一下...在Objective-C的categories
里是怎样的呢?他们提供了一种(-|+)[ClassName(categoryName) method]
的形式.你可以用包含categories的形式重写正则表达式.
在LLDB会话中输入以下命令, 并在提示用输入y
进行确认:
(lldb) breakpoint delete
这条命令会删除你设置的所有的断点.
然后输入下面的内容:
(lldb) rb '\-\[UIViewController(\(\w+\))?\ '
这条命令在断点的UIViewController
后面在空格前面提供了一个包含一个或多个字符的括号选项.
正则表达式断点让你用一个表达式捕获一个宽泛的断点.
你可以用-f
选项来将断点指定在特定的文件中.
(lldb) rb . -f DetailViewController.swift
这在你调试DetailViewController.swift
的时候很有用.它会在这个文件的所有属性的getters/setters
, blocks/closures
, extensions/ categories
, 和 functions/methods
出打下断点.-f
选项是用来限定范围的.
如果你彻底疯了或者想要折磨一下自己, 你可以想下面这样简单的指定一下范围:
(lldb) rb .
这会在所有的地方创建一个断点...是的, 是所有地方!这会在Signals
项目的所有代码里创建断点, 所有UIKit
的代码和所有Foundation
的代码, 所有运行循环的代码.结果是, 你需要在调试器中不停的输入continue
才能继续执行.
还有另外一种方式来指定搜索的范围.你可以用-s
选项将范围限制在单一的库中:
(lldb) rb . -s Commons
这会在Commons
库中的每一个地方设置一个断点, Commons
是Signals
项目中包含的一个动态库.
你也可以用同样的方法在UIKit
的每一个函数里创建断点:
(lldb) rb . -s UIKit
这显然太疯狂了.在iOS10.0系统里UIKit
里大概有66,189个方法.如何在UIKit
里只在你第一次触发这个断点的时候停下来呢?-o
选项提供了一个解决方案.它创建了one-shot
型的断点. 这种断点在触发一次后就会自动删除. 因此只会触发一次.
要看这种方法的效果, 可以在LLDB会话中输入下面的命令:
(lldb) breakpoint delete
(lldb) rb . -s UIKit -o
注意:当LLDB执行这些命令的时候请耐心等待, 因为LLDB创建了大量的断点.并且要确保你使用的是模拟器, 否则的话你将会等待非常长的时间.
接下来, 让调试器继续执行, 并在tableView中的一个cell上点击一下.调试器就会在UIKit
的方法第一次调用的时候停下来.最后, 继续运行调试器, 并且这个断点将不会再次触发.
修改和删除断点
现在你已经对如何创建断点有了基本的理解, 你可能还会想知道你如何改变它们. 当你想要修改, 删除或者暂时停用一个断点的时候该怎么做?当你想让断点下次触发的时候执行一个特定的命令你又该怎么做呢?
首先, 你需要知道怎样唯一的标记一个或者一组断点. 在你创建断点的时候你也可以用-N
选项命名一个断点...用数字做标记可能真的不适合你.
构建并运行APP来清空LLDB会话.接下来, 暂停调试器并在LLDB会话中输入一下命令:
(lldb) b main
你可能会看到下面这些输出:
Breakpoint 1: 20 locations.
这句话的是意思是说你再不同模块中的与main
匹配的20个地方创建了断点.
在这种情况下, 这个断点的的ID是1, 因为它是你创建的第一个断点. 要查看这个断点的详细信息, 你可以用breakpoint list
命令.输入下面的内容:
(lldb) breakpoint list 1
应该会输出下面类似的内容:
1: name = 'main', locations = 20, resolved = 20, hit count = 0
1.1: where = Breakpoints`main + 22 at AppDelegate.swift:12, address =
0x00000001057676e6, resolved, hit count = 0
1.2: where = Foundation`-[NSThread main], address = 0x000000010584d182,
resolved, hit count = 0
1.3: where = Foundation`-[NSBlockOperation main], address =
0x000000010585df4a, resolved, hit count = 0
1.4: where = Foundation`-[NSFilesystemItemRemoveOperation main],
address = 0x00000001058990ff, resolved, hit count = 0
1.5: where = Foundation`-[NSFilesystemItemMoveOperation main], address
= 0x0000000105899c23, resolved, hit count = 0
1.6: where = Foundation`-[NSInvocationOperation main], address =
0x00000001058c4fb9, resolved, hit count = 0
1.7: where = Foundation`-[NSDirectoryTraversalOperation main], address
= 0x000000010590a87f, resolved, hit count = 0
1.8: where = Foundation`-[NSOperation main], address =
0x000000010595209c, resolved, hit count = 0
1.9: where = UIKit`-[UIStatusBarServerThread main], address =
0x00000001068b84f0, resolved, hit count = 0
1.10: where = UIKit`-[_UIDocumentActivityItemProvider main], address =
0x000000010691898c, resolved, hit count = 0
1.11: where = UIKit`-[_UIDocumentActivityDownloadOperation main],
address = 0x0000000106975d51, resolved, hit count = 0
1.12: where = UIKit`-[_UIGetAssetThread main], address =
0x000000010698ef4d, resolved, hit count = 0
1.13: where = UIKit`-[UIWebPDFSearchOperation main], address =
0x0000000106ae7c99, resolved, hit count = 0
1.14: where = UIKit`-[UIActivityItemProvider main], address =
0x0000000106c4e525, resolved, hit count = 0
1.15: where = MobileCoreServices`-[LSOpenOperation main], address =
0x000000010879703c, resolved, hit count = 0
1.16: where = ImageIO`main, address = 0x000000010c87535d, resolved, hit
count = 0
1.17: where = AppSupport`-[_CPPowerAssertionThread main], address =
0x000000010ed95f03, resolved, hit count = 0
1.18: where = AppSupport`-[CPDistributedMessagingAsyncOperation main],
address = 0x000000010ed9ba53, resolved, hit count = 0
1.19: where = JavaScriptCore`WTF::RunLoop::main(), address =
0x0000000111c68af0, resolved, hit count = 0
1.20: where = ConstantClasses`main, address = 0x0000000114329cd2,
resolved, hit count = 0
这里显示了这个断点的详细内容, 包含所有包含main
这个单词的位置.
显示这些信息的简介内容的方式是输入下面的内容:
(lldb) breakpoint list 1 -b
这会输出一些简洁的更可视化的信息.如果你有一个断点的ID, 并且这个ID里包含着多个断点, 这个简明的标志是一个好的解决方案.
如果你想要查看LLDB中所有的断点, 只需要简单的输入下面的内容:
(lldb) breakpoint list
你也可以指名多个断点的ID和范围:
(lldb) breakpoint list 1 3
(lldb) breakpoint list 1-3
用breakpoint delete
命令删除所有的断点是一个重量级的操作.你可以简单的使用断点的ID来删除一个断点或者断点的集合:
你可以用指定断点ID的方式删除一个断点:
(lldb) breakpoint delete 1
然而, "main"断点里面包含了20个断点.你也可以用下面这总方式删除一个断点:
(lldb) breakpoint delete 1.1
这会删除1
断点的第一个自断点.
我们为什么要学这些?
在本章节中你学到了大量的内容. 断点是一个很大的话题并且对调试专家来说是一门主要的艺术用来快速的发现并找到实物本质.你也已经了解了如何用正则表达式来搜索代码.现在是时候梳理一下正则表达式的语法了, 在本书的后面你将会用到大量的正则表达式.
浏览 https://docs.python.org/2/library/re.html 来学习正则表达式.并尝试找出如何创建不区分大小写的正则表达式.
现在你只不过是出不得了解了一下编译器是如何生成Objective-C 和 Swift的函数的.尝试找出如何在Objective-C的blocks或者Swift的closures里创建断点的代码.如果你做到了, 尝试设计出一个断点只在Signals
项目里的Commons
framework的Objective-C blocks里停止的断点.这些都是你再未来创建更复杂的断点所需要的技能.