无论你使用的是Swift、Objective-C、C++、C,还是你的技术栈中完全不同的语言,你都需要学习如何创建断点。在Xcode中点击侧板,使用GUI创建一个断点是很容易的,但是LLDB控制台可以让你对断点有更多的控制。
在这一章中,你将学习所有关于断点的知识以及如何使用LLDB创建断点。
在本章中,你将会看到我提供的一个项目;它叫做Signals,你可以在本章的资源包中找到它。
使用Xcode打开Signals项目。Signals是一个基本的主-细节项目,其主题是一个美式足球的应用程序,显示一些相当呆板的命名的进攻性比赛呼叫。
在内部,这个项目监控了几个Unix信号,并在Signals程序收到信号时显示它们。
Unix信号是进程间通信的一种基本形式。例如,其中一个信号,SIGSTOP,可以用来保存一个进程的状态并暂停执行,而它的对应信号,SIGCONT,则被发送给一个程序以恢复执行。这两个信号都可以被调试器用来暂停和继续一个程序的执行。
这是一个在几个方面都很有趣的应用,因为它不仅探索了Unix信号的处理,而且还强调了当一个控制进程(LLDB)处理Unix信号传递给被控制进程时发生的情况。默认情况下,LLDB有处理不同信号的自定义动作。当LLDB被连接时,一些信号不会被传递给受控进程。
为了显示一个信号,你可以从应用程序中提出一个信号,或者从外部的不同应用程序中发送一个信号,比如终端。
此外,有一个UISwitch可以切换信号处理。当开关被切换时,它会调用一个C函数sigprocmask来禁用或启用信号处理程序。
最后,Signal应用程序有一个Timeout bar按钮,它从应用程序中发出SIGSTOP信号,基本上是 "冻结 "程序。然而,如果LLDB连接到Signal程序(默认情况下,当你通过Xcode构建和运行时,它将是),调用SIGSTOP将允许你在Xcode中用LLDB检查执行状态。
选择你所选择的iOS模拟器,同时确保iOS模拟器至少是iOS 12.0或更高版本。构建并运行该应用程序。一旦项目运行,导航到Xcode控制台并暂停调试器。
恢复Xcode,并留意模拟器的情况。每当调试器停止然后恢复执行时,一个新的行将被添加到UITableView中。这是通过Signals监控SIGSTOP Unix信号事件并在其发生时向数据模型添加一行来实现的。当一个进程被停止时,任何新的信号将不会被立即处理,因为程序有点,嗯,停止。
在你通过LLDB控制台学习很酷很亮的断点之前,值得介绍一下你可以通过Xcode单独实现什么。
符号断点是Xcode的一个伟大的调试功能。它允许您在您的应用程序中的某个符号上设置断点。一个符号的例子是[NSObject init],它指的是NSObject实例的init方法。
Xcode中的符号断点的好处是,一旦你输入了一个符号断点,你就不必在下次程序启动时再次输入它。
你现在要尝试使用一个符号断点来显示所有正在创建的NSObject的实例。
如果应用程序目前正在运行,请关闭它。接下来,切换到断点导航器。在左下方,点击加号按钮,选择符号断点...选项。
将出现一个弹出窗口。在弹出窗口的符号部分输入。-[NSObject init]. 在Action下,选择Add Action,然后从下拉菜单中选择Debugger Command。接下来,在下面的框中输入po [$arg1 class]。
最后,选择评估行动后自动继续。你的弹出窗口应该类似于下面的样子。
构建并运行该应用程序。Xcode在运行Signals程序时,会通过控制台转储所有初始化的类的名称......经查看,这是很重要的。
你在这里所做的是设置一个断点,在每次调用-[NSObject init]时触发。当断点发生时,LLDB中会有一条命令运行,程序的执行会自动继续。
注意:你将在第11章 "汇编、寄存器和调用约定 "中学习如何正确使用和操作寄存器,但现在只需知道\(arg1是\)rdi寄存器的同义词,可以宽泛地认为是init被调用时保存类的实例。
一旦你检查完所有被倾倒出来的类名,在断点导航器中右击断点,选择删除断点,就可以删除这个符号断点。
除了符号断点之外,Xcode还支持几种类型的错误断点。其中之一就是异常断点。有时候,你的程序出了问题,它只是简单地崩溃了。当这种情况发生时,你的第一反应应该是启用一个异常断点,它将在每次抛出异常时触发。Xcode会向你显示违规的行,这对找出导致崩溃的罪魁祸首有很大帮助。
最后,还有一个Swift错误断点,它通过在swift_willThrow方法上创建一个断点来停止任何Swift抛出的错误。如果你正在使用任何容易出错的API,这是一个很好的选择,因为它可以让你快速诊断情况,而不会对你的代码的正确性做出错误的假设。
现在你已经有了一个使用Xcode的IDE调试功能的速成课程,现在是时候学习如何通过LLDB控制台创建断点了。为了创建有用的断点,您需要学习如何查询您所寻找的东西。
图像命令是一个很好的工具,可以帮助反省那些对设置断点至关重要的细节。
在本书中,你将使用两种构架进行代码搜索。第一种是下面这种。
(lldb) image lookup -n "-[UIViewController viewDidLoad]"
该命令转储了[UIViewController viewDidLoad]函数的实现地址(该方法在框架二进制中的偏移地址)。参数-n告诉LLDB去查找符号或函数名。输出结果将类似于下面。
你可以从-[UIViewController viewDidLoad]方法的位置看出这是iOS 12。在以前的iOS版本中,这个方法位于UIKit中,但现在可能由于macOS/iOS的统一过程,现在已经转移到了UIKitCore中,也就是俗称的Marzipan。
另一个有用的、类似的命令是这个。
(lldb) image lookup -rn test
这将对 "test "这个词进行区分大小写的重写查找。如果在当前可执行文件中加载的任何模块(即UIKit、Foundation、Core Data等)的任何函数中发现小写的 "test "一词(未从发布版本中剥离......后面会详细介绍),该命令将吐出结果。
注意:当你想要精确匹配时,使用-n参数(如果你的查询包含空格,则在你的查询周围加上引号),使用-rn参数来做一个反义词搜索。仅仅是-n命令有助于找出准确的参数来匹配断点,特别是在处理Swift的时候,而-rn参数选项在本书中会受到很大的青睐,因为一个聪明的词组可以消除相当多的输入--正如你很快会发现的。
学习如何查询加载的代码对于学习如何在该代码上创建断点至关重要。Objective-C和Swift在被编译器创建时都有特定的属性签名,这导致在寻找代码时有不同的查询策略。
例如,在Signals项目中声明了下面这个Objective-C类。
@interface TestClass : NSObject
@property (nonatomic, strong) NSString *name; @end
编译器将为属性名的setter和getter生成代码。获取器将看起来像下面这样。
-[TestClass name]
...而设置器将看起来像这样。
-[TestClass setName:]
建立并运行应用程序,然后暂停调试器。接下来,通过在LLDB中键入以下内容来验证这些方法确实存在。
(lldb) image lookup -n "-[TestClass name]"
在控制台输出中,你会得到类似于以下的东西。
LLDB将转储可执行文件中包含的函数的信息。输出可能看起来很吓人,但这里有一些好的花絮。
注意:图像查找命令可以产生大量的输出,当查询匹配了大量的代码时,会让人感到很难受。在第26章 "SB实例,改进的查找 "中,你将建立一个更简洁的LLDB图像查找命令的替代方案,使你的眼睛不用看太多的输出。
控制台输出告诉你,LLDB能够发现这个函数是在Signals可执行文件中实现的,准确地说,是在__text部分的__TEXT段的0x0000000100002150处(如果这没有任何意义,不要担心,你会在本书后面学到所有这些)。LLDB也能知道这个方法是在TestClass.h的第28行声明的。
你也可以检查设置器,像这样。
(lldb) image lookup -n "-[TestClass setName:]"
你会得到与前面命令类似的输出,这次显示的是实现地址和setter对name的声明。
对于入门级的Objective-C(或者只有Swift)开发者来说,经常被误导的东西是Objective-C点符号的属性语法。
Objective-C点符号是一个有点争议的编译器功能,它允许属性使用一个速记的getter或setter。
请考虑以下情况。
TestClass *a = [[TestClass alloc] init];
// Both equivalent for setters
[a setName:@"hello, world"];
a.name = @"hello, world";
// Both equivalent for getters
NSString *b;
b = [a name]; // b = @"hello, world"
b = a.name; //b = @"hello, world"
在上面的例子中,-[TestClass setName:]方法被调用了两次,即使使用点符号。对于getter, -[TestClass name]也是如此。如果你在处理Objective-C代码,并试图用点符号在属性的setters和getters上创建断点,那么知道这一点很重要。
在Swift中,属性的语法有很大不同。看看SwiftTestClass.swift中的代码,其中包含以下内容。
class SwiftTestClass: NSObject {
var name: String!
}
确保Signals项目正在运行并在LLDB中暂停。随意在调试窗口中输入Command + K来清除LLDB控制台,以便重新开始。
在LLDB控制台,键入以下内容。
(lldb) image lookup -rn Signals.SwiftTestClass.name.setter
你会得到与下面类似的输出。
获取输出中Summary一词之后的信息。这里有几件有趣的事情要注意。
你看到这个函数名有多长了吗!?为了一个有效的Swift断点,这整件事都需要打出来!?如果你想在这个setter上设置一个断点,你就得输入以下内容。
(lldb) b Signals.SwiftTestClass.name.setter : Swift.Optional<Swift.String>
使用正则表达式是一个很有吸引力的替代方法,因为它可以代替打出这种怪异的东西。
除了你产生的Swift函数名的长度之外,注意Swift属性是如何形成的。包含属性名称的函数签名在属性后面紧跟着setter这个词。也许同样的约定也适用于getter方法?
使用下面的正则表达式查询,同时搜索SwiftTestClass的setter和getter的name属性。
(lldb) image lookup -rn Signals.SwiftTestClass.name
这使用了一个正则查询来转储所有包含 Signals.SwiftTestClass.name 短语的内容。
由于这是一个正则表达式,句号(......)被评估为通配符,这反过来又与实际函数签名中的句号匹配。
你会得到相当多的输出,但每次看到控制台输出中的Summary这个词时,你都要仔细观察。你会发现输出结果与getter(Signals.SwiftTestClass.name.getter)、setter(Signals.SwiftTestClass.name.setter)以及两个含有
materializeForSet,是Swift构造函数的辅助方法。
Swift属性的函数名称有一个模式。
ModuleName.Classname.PropertyName.(getter|setter)
当你在代码中创建智能断点时,倾倒方法、找到模式并缩小搜索范围的能力是发现Swift/Objective-C语言内部的一个好方法。
现在你知道如何查询代码中是否存在函数和方法了,是时候开始为它们创建断点了。
如果你已经运行了Signals应用程序,请停止并重启该应用程序,然后按下暂停按钮,停止该应用程序并调出LLDB控制台。
有几种不同的方法来创建断点。最基本的方法是简单地输入字母b,然后再输入断点的名称。这在Objective-C和C语言中相当容易,因为名称很短,很容易输入(例如:-[NSObject init]或-[UIView setAlpha:])。在C++和Swift中,它们的键入相当麻烦,因为编译器会把你的方法变成名字相当长的符号。
由于UIKit主要是Objective-C(至少在写这篇文章的时候是这样!),使用b参数创建一个断点,像这样。
(lldb) b -[UIViewController viewDidLoad] 。
你会看到下面的输出。
breakPoint1:where = UIKitCore`-[UIViewController viewDidLoad], address = 0x0000000114a4a13c
当你创建一个有效的断点时,控制台会吐出关于该断点的一些信息。在这个特定的案例中,该断点被创建为断点1,因为这是这个特定调试会话中的第一个断点。当你创建更多的断点时,这个断点的ID会递增。
恢复调试器。一旦你恢复了执行,一个新的SIGSTOP信号将被显示。点击该单元格,调出详细的UIViewController。当详细视图控制器的viewDidLoad被调用时,程序应该暂停。
注意:像很多速记命令一样,b是另一个更长的LLDB命令的缩写。运行b命令的帮助,可以自己摸索出实际的命令,并学习b在引擎盖下能做的所有很酷的技巧。
除了b命令外,还有另一条较长的断点设置命令,它有一系列可用的选项。你将在接下来的几节中探索这些选项。许多命令都来自于断点设置命令的各种选项。
另一个非常强大的命令是正则表达式断点,rbreak,它是breakpoint set -r %1的缩写。你可以使用聪明的正则表达式快速创建许多断点,在你想要的地方停止。
如果你已经运行了Signals应用程序,请停止并重启该应用程序,然后按下暂停按钮,停止该应用程序并调出LLDB控制台。
有几种不同的方法来创建断点。最基本的方法是简单地输入字母b,然后再输入断点的名称。这在Objective-C和C语言中相当容易,因为名称很短,很容易输入(例如:-[NSObject init]或-[UIView setAlpha:])。在C++和Swift中,它们的键入相当麻烦,因为编译器会把你的方法变成名字相当长的符号。
由于UIKit主要是Objective-C(至少在写这篇文章的时候是这样!),使用b参数创建一个断点,像这样。
(lldb) b -[UIViewController viewDidLoad]
你会看到下面的输出。
Breakpoint 1: where = UIKitCore`-[UIViewController viewDidLoad], address = 0x0000000114a4a13c
当你创建一个有效的断点时,控制台会吐出关于该断点的一些信息。在这个特定的案例中,该断点被创建为断点1,因为这是这个特定调试会话中的第一个断点。当你创建更多的断点时,这个断点的ID会递增。
恢复调试器。一旦你恢复了执行,一个新的SIGSTOP信号将被显示。点击该单元格,调出详细的UIViewController。当详细视图控制器的viewDidLoad被调用时,程序应该暂停。
注意:像很多速记命令一样,b是另一个更长的LLDB命令的缩写。运行b命令的帮助,可以自己摸索出实际的命令,并学习b在引擎盖下能做的所有很酷的技巧。
除了b命令外,还有另一条较长的断点设置命令,它有一系列可用的选项。你将在接下来的几节中探索这些选项。许多命令都来自于断点设置命令的各种选项。
另一个非常强大的命令是正则表达式断点,rbreak,它是breakpoint set -r %1的缩写。你可以使用聪明的正则表达式快速创建许多断点,在你想要的地方停止。
回到前面的例子,用长得惊人的Swift属性函数名,而不是打字。
(lldb) b Signals.SwiftTestClass.name.setter : Swift.Optional<Swift.String>
你可以简单地输入
(lldb) rb SwiftTestClass.name.setter
rb命令将被扩展为rbreak(前提是你没有任何其他以 "rb "开头的LLDB命令)。这将在SwiftTestClass中name的setter属性上创建一个断点。
为了更简短,你可以简单地使用下面的方法。
(lldb) rb name\.setter
这将在任何包含name.setter短语的地方产生一个断点。如果你知道你的项目中没有其他叫name的Swift属性,这就可以了;否则你会为每个包含有setter的 "name "属性的类创建多个断点。
让我们提高这些正则表达式的复杂性。
在UIViewController的每个Objective-C实例方法上创建一个断点。在你的LLDB会话中键入以下内容。
(lldb) rb '\-\[UIViewController\ '
丑陋的反斜线是转义字符,表示你希望正则表达式搜索中的字面字符。结果是,这个查询在每一个包含字符串-[UIViewController后跟一个空格的方法上都会失效。
但是,等等......那Objective-C类别呢?它们的形式是(-|+) [ClassName(categoryName) method]。你必须重写正则表达式以包括类别。
在你的LLDB会话中键入以下内容,并在提示下键入Y来确认。
(lldb) breakpoint delete
该命令删除了你设置的所有断点。
接下来,键入以下内容。
(lldb) rb '\-\[UIViewController(\(\w+\))?\ '
这在断点中的UIViewController后面提供了一个可选的括号,括号里有一个或多个字母数字字符,后面是一个空格。
Regex断点让你用一个表达式就能捕捉到各种各样的断点。
你可以使用-f选项将断点的范围限制在某个文件中。例如,你可以输入以下内容。
(lldb) rb . -f DetailViewController.swift
如果你在调试DetailViewController.swift,这将很有用。它将在这个文件中的所有属性getters/setters、block/closures、extensions/category和函数/方法上设置一个断点。-f被称为范围限制。
如果你完全是个疯子,而且喜欢痛苦(医生说这是受虐狂?),你可以省略范围限制,直接这样做。
(lldb) rb .
这将在所有的东西上建立一个断点......是的,所有的东西! 是的,所有的东西!这将在所有东西上建立断点。这将在Signals项目中的所有代码、UIKit和Foundation中的所有代码、所有事件运行循环代码(希望是60赫兹的)上创建断点。因此,如果你执行这个命令,你会在调试器中频繁地输入继续。
还有其他方法来限制你的搜索范围。你可以使用-s选项限制到一个库。
(lldb) rb . -s Commons
这将对Commons库中的所有内容设置断点,Commons是Signals项目中的一个动态库。
这并不局限于你的代码;你可以使用同样的策略在UIKitCore中的每个函数上创建断点,像这样。
(lldb) rb . -s UIKitCore
即使这样做还是有点疯狂。有很多方法--在iOS 12.0中大约有86,760个UIKitCore方法。如果只停在UIKitCore中的第一个方法上,然后继续下去呢?-o选项为此提供了一个解决方案。它创建了一个所谓的 "一次性 "断点。当这些断点被击中时,断点被删除。因此,它只会被击中一次。
要看到这个动作,在你的LLDB会话中输入以下内容。
(lldb) breakpoint delete
(lldb) rb . -s UIKitCore -o 1
注意:当你的计算机执行这个命令时要有耐心,因为LLDB必须创建大量的断点。还要确保你使用的是模拟器,否则你将等待很长时间
接下来,继续调试器,在表视图中点击一个单元格。调试器会在这个动作调用的第一个UIKitCore方法时停止。最后,继续调试器,断点将不再触发。
-L选项可以让你根据源语言进行过滤。因此,如果你只想追踪Signals应用程序的Commons模块中的Swift代码,你可以这样做。
(lldb) breakpoint set -L swift -r . -s Commons
这将在Commons模块中的每个Swift方法上设置一个断点。
如果你想在Swift if let周围寻找一些有趣的东西,但完全忘记了它在你的应用程序中的位置,怎么办?你可以使用源编码断点来帮助确定感兴趣的位置。就像这样。
(lldb) breakpoint set -A -p "if let"
这将在每一个包含if let的源代码位置创建一个断点。当然,你还可以更花哨一些,因为-p需要一个正则表达式断点来追踪复杂的表达式。A选项表示在项目已知的所有源代码文件中进行搜索。
如果你想把上面的断点查询限定在MasterViewController.swift和DetailViewController.swift上,你可以这样做。
(lldb) breakpoint set -p "if let" -f MasterViewController.swift -f DetailViewController.swift
注意-A已经消失了,每个-f都会让你指定一个filename。我很懒,所以我通常会默认为-A来提供所有的文件,然后从那里钻进去。
最后,你也可以通过一个特定的模块进行过滤。如果你想为Signals可执行文件中的 "if let "创建一个断点(同时忽略其他框架如Commons),你可以这样做。
(lldb) breakpoint set -p "if let" -s Signals -A
这将抓取所有源文件(-A),但只筛选属于Signals可执行文件的文件(有-s Signals选项)。
还有一个很酷的断点选项例子?好吧,你说服了我。你要做一个断点,每当viewDidLoad被击中时,就打印出UIViewController,但你要通过LLDB控制台而不是符号断点窗口来做。然后,你将把这个断点导出到一个文件中,这样你就可以通过使用断点读取和断点写入命令向你的同事展示你有多酷了
首先,删除所有断点。
(lldb) breakpoint delete
现在创建以下(复杂的!)断点。
(lldb) breakpoint set -n "-[UIViewController viewDidLoad]" -C "po $arg1" -G1
请确保使用大写的-C,因为LLDB的-c执行的是不同的选项
这表示在-[UIViewController viewDidLoad]上创建一个断点,然后执行(C)指令 "po $arg1",它打印出UIViewController的实例。
从这里开始,-G1选项告诉断点在执行命令后自动继续。
通过点击含有Unix信号的UITableViewCells之一来触发viewDidLoad,验证控制台显示预期的信息。
现在,你如何把这个东西发给同事?在LLDB中,输入以下内容。
(lldb) breakpoint write -f /tmp/br.json
这将把你会话中的所有断点写到/tmp/br.json文件中。你可以通过断点ID指定单个断点或断点列表,但这需要你在自己的时间内通过帮助文档来确定。
你可以在终端或通过LLDB验证断点数据,使用平台shell命令突破到使用终端。
使用cat Terminal命令来显示断点数据。
(lldb) platform shell cat /tmp/br.json
这意味着你可以把这个文件发给你的同事,让她通过断点读取命令打开它。
为了模拟这种情况,再次删除所有断点。
(lldb) breakpoint delete
现在你将拥有一个没有断点的干净调试会话。
现在,重新导入你的自定义断点命令。
(lldb) breakpoint read -f /tmp/br.json
再次,如果你要触发UIViewController的viewDidLoad方法,由于你的自定义断点逻辑,该实例将被打印出来! 使用这些命令,你可以很容易地发送和接收LLDB断点命令,以帮助复制一个难以捕捉的bug!
现在您对如何创建这些断点有了基本了解,您可能想知道如何改变它们。如果你找到了你感兴趣的对象,想删除断点,或者暂时禁用它,怎么办?如果你需要修改断点,使其在下次触发时执行特定的动作,又该怎么办?
首先,你需要了解如何独特地识别一个断点或一组断点。
建立并运行应用程序,以获得一个干净的LLDB会话。接下来,暂停调试器,在LLDB会话中输入以下内容。
(lldb) b main
输出将类似于下面的内容。
Breakpoint 1: 70 locations.
这创建了一个有70个位置的断点,与各个模块中的函数 "main "相匹配。
在本例中,断点ID为1,因为它是您在本会话中创建的第一个断点。要查看该断点的详细信息,可以使用断点列表子命令。键入以下内容。
(lldb) breakpoint list 1
输出结果将类似于下面的截断输出。
这显示了该断点的细节,包括所有包含 "main "一词的位置。
一个更简洁的查看方法是输入以下内容。
(lldb) breakpoint list 1 -b
这将使你的输出在视觉感官上更容易一些。如果你有一个封装了很多断点的断点ID,这个简短的标签是一个很好的解决方案。
如果你想查询LLDB会话中的所有断点,只需像这样省略ID。
(lldb) breakpoint list
你也可以指定多个断点ID和范围。
(lldb) breakpoint list 1 3
(lldb) breakpoint list 1-3
使用breakpoint delete来删除所有的断点是有点重口味的。你可以简单地使用在断点列表命令中使用的相同ID模式来删除一组。
你可以像这样指定ID来删除一个单独的断点。
(ldb)breakpoint delete 1
然而,你的 "main "的断点有70个位置(可能更多或更少,取决于iOS的版本)。你也可以删除单个位置,像这样。
(lldb) breakpoint delete 1.1
这将删除断点1的第一个子断点,这样做的结果是只删除一个主函数断点,而保留其余的主断点。
在本章中你已经讲了很多。断点是一个很大的话题,掌握快速找到感兴趣的项目的艺术是成为调试专家的关键。你也已经开始探索使用正则表达式进行函数搜索。现在是学习正则表达式语法的好时机,因为在本书的其余部分,你将会使用大量的正则表达式。
查阅https://docs.python.org/2/library/re.html, 学习(或重新学习)正则表达式。试着弄清楚如何进行不区分大小写的断点查询。
你才开始发现编译器是如何在Objective-C和Swift中生成函数的。试着弄清楚在Objective-C块或Swift闭包上停止的语法。一旦你完成了这些,试着设计一个断点,只在Signals项目的Commons框架内停止在Objective-C块上。这些都是你在未来构建更复杂的断点时所需要的重码技巧。
上一章 | 目录 | 下一章 |
---|