使用LLDB调试Swift代码

作为工程师,我们将近70%的时间用于调试。 其余的20%会继续考虑架构方法+与队友进行交流,而实际上只有10%会继续编写代码。

调试就像是犯罪电影中的侦探一样,而您也是凶手。

—通过Twitter的Filipe Fortes

因此,使我们这70%的时间尽可能愉快是非常重要的。 LLDB进行了救援。 花式Xcode调试器用户界面显示所有可用信息,而无需键入单个LLDB命令。 但是,控制台仍然是我们工作流程的重要组成部分。 让我们分解一些最有用的LLDB技巧。 我个人每天使用它们进行调试。

LLDB是一个强大的工具,它内部包含许多有用的命令。 我不会全部描述。 我想向您介绍最有用的命令。 所以这是我们的计划:

  1. 探索变量值: expressioneprintpop
  2. 获取整体应用程序的状态+语言特定的命令: bugreportframelanguage
  3. 控制应用程序的执行流程: processbreakpointthreadwatchpoint
  4. 荣誉奖: commandplatformgui

我还准备了有用的LLDB命令的图以及说明和示例。 如果需要,可以将其挂在Mac上方以记住这些命令🙂

命令: expressioneprintpop

调试器的基本功能是探索和修改变量的值。 这就是expressione的用途(实际上更多)。 您基本上可以在运行时中评估任何表达式或命令。

假设您正在调试一些函数valueOfLifeWithoutSumOf() ,该函数将两个数字相加并从42中提取结果。

我们还假设您一直得到错误的答案,而您不知道为什么。 因此,要查找问题,您可以执行以下操作:

或者…最好使用LLDB表达式代替在运行时更改值。 并找出问题发生的地方。 首先,在您感兴趣的地方设置一个断点。然后运行您的应用程序。

要以LLDB格式打印特定变量的值,应调用:

  (lldb)e  

同样的命令用于计算某些表达式:

  (lldb)e  
  (lldb)e和 
 (Int)$ R0 = 6 //您将来也可以使用$ R0来引用此变量(在当前调试会话期间)(lldb)e sum = 4 //更改sum变量(lldb)e sum的值 
 (Int)$ R2 = 4 //直到调试会话结束,sum变量将为“ 4” 

expression命令也有一些标志。 为了区分标志和实际表达式,LLDB使用双破折号--expression命令之后像这样:

  (lldb)表达式- 

expression具有将近30个不同的标志。 我鼓励您探索所有这些。 在终端中编写以下命令以获取完整的文档:

  > lldb
 >(lldb)help#探索所有可用命令
 >(lldb)help expression#探索与表达式相关的所有子命令 

我想停止以下expression的标志:

  • -D (– --depth )—设置转储聚合类型时的最大递归深度(默认为无穷大)。
  • -O (– --object-description )-如有可能,使用特定于语言的描述API进行显示。
  • -T (–show --show-types )-转--show-types时显示变量类型。
  • -f (–format --format )-指定用于显示的格式。
  • -i (–ignore --ignore-breakpoints )—在运行表达式时忽略断点命中

假设我们有一个称为logger对象。 该对象包含一些字符串和结构作为属性。 例如,您只想浏览一级属性。 只需使用具有适当深度级别的-D标志即可:

  (lldb)e -D 1-logger(LLDB_Debugger_Exploration.Logger)$ R5 = 0x0000608000087e90 {
   currentClassName =“ ViewController”
   debuggerStruct = {...}
 } 

默认情况下,LLDB将无限地查看对象,并向您显示每个嵌套对象的完整描述:

  (lldb)e-logger(LLDB_Debugger_Exploration.Logger)$ R6 = 0x0000608000087e90 {
   currentClassName =“ ViewController”
   debuggerStruct =(methodName =“名称”,lineNumber = 2,commandCounter = 23)
 } 

您也可以使用e -O --探索对象描述e -O --或仅使用别名po ,如以下示例所示:

  (lldb)po记录器 

不是那么描述性,不是吗? 要获得易于理解的描述,您必须将自定义类应用于CustomStringConvertible协议并实现var description: String { return ...}属性。 只有这样po才能返回您可读的描述。

在本节的开头,我还提到了print命令。
基本上print expression -- 。 除了print命令不带任何标志或其他参数。

bugreportframelanguage

您多久复制一次并粘贴崩溃日志并将其粘贴到任务管理器中,以便以后进行问题探讨? LLDB有一个很棒的小命令,称为bugreport ,它将生成当前应用程序状态的完整报告。 如果您遇到一些问题,但想稍后再解决,可能会很有帮助。 为了恢复您对应用程序状态的了解,您可以使用bugreport生成的报告。

  (lldb)bugreport展开--outfile  

最终报告将类似于以下屏幕截图中的示例:

假设您想快速了解当前线程中的当前堆栈框架。 frame命令可以帮助您:

使用下面的代码片段可以快速了解您目前所在的位置以及周围的状况:

  (lldb)帧信息帧#0: 0x000000010bbe4b4d LLDB-Debugger-Exploration`ViewController。  valueOfLifeWithoutSumOfa = 2b = 2self = 0x00007fa0c1406900 )->在ViewController为Int.swift :96 

此信息将在本文后面的断点管理中有用。

LLDB有一些用于特定语言的命令。 有用于C ++,Objective-C,Swift和RenderScript的命令。 在这种情况下,我们对Swift感兴趣。 所以这是这两个命令: demanglerefcount

以其名称编写的demangle只是去除了Swift类型名称(Swift在编译过程中生成以避免名称空间问题的名称)的变形。 如果您想了解更多信息,我建议您观看本WWDC14会议-“ LLDB中的高级Swift调试”。

refcount也是一个非常简单的命令。 它显示了特定对象的引用计数。 我们来看一下上一节中使用的对象logger的输出示例:

  (lldb)语言swift refcount loggerrefcount数据:(强= 4,弱= 0) 

当然,如果要调试一些内存泄漏,这可能非常有用。

processbreakpointthread

这部分是我的最爱。 因为使用LLDB中的这些命令(尤其是breakpoint命令),您可以在调试过程中自动执行许多例程。 最终可以大大加快调试过程。

使用process您基本上可以控制调试过程并附加到特定目标或从中分离调试器。 但是由于Xcode会自动为我们完成流程附加(每次运行目标时Xcode都会附加LLDB),因此我不会停止这样做。 您可以在本Apple指南“使用LLDB作为独立调试器”中阅读如何使用终端连接到目标。

使用process status您可以探索调试器正在等待您的当前位置:

  (lldb)进程状态进程27408已停止
 *线程#1,队列='com.apple.main-thread',停止原因=单步执行
框架#0:0x000000010bbe4889 LLDB-Debugger-Exploration`ViewController.viewDidLoad(self = 0x00007fa0c1406900)->()在ViewController.swift:69
 66
 67让a = 2,b = 2
 68让结果= valueOfLifeWithoutSumOf(a,and:b)
 -> 69打印(结果)
 70
 71
 72 

为了继续执行目标,直到出现下一个断点,请运行以下命令:

  (lldb)进程continue(lldb)c //或只需键入“ c”,它与前面的命令相同 

等效于Xcode调试器工具栏中的“继续”按钮:

breakpoint命令允许您以任何可能的方式操作断点。 让我们跳过一些最明显的命令,例如: breakpoint enablebreakpoint disablebreakpoint delete

首先,要探索所有断点,请使用下面的示例中的list子命令:

  (lldb)断点列表当前断点:
 1:文件='/Users/Ahmed/Desktop/Recent/LLDB-Debugger-Exploration/LLDB-Debugger-Exploration/ViewController.swift',第= 95行,exact_match = 0,位置= 1,已解决= 1,点击计数= 11.1:其中= LLDB-Debugger-Exploration`LLDB_Debugger_Exploration.ViewController.valueOfLifeWithoutSumOf(Swift.Int和:Swift.Int)-> Swift.Int + 27在ViewController.swift:95,地址= 0x0000000107f3eb3b,已解决,命中计数= 12 :file ='/Users/Ahmed/Desktop/Recent/LLDB-Debugger-Exploration/LLDB-Debugger-Exploration/ViewController.swift',第= 60行,exact_match = 0,位置= 1,已解决= 1,匹配计数= 12.1 :其中= LLDB-Debugger-Exploration`LLDB_Debugger_Exploration.ViewController.viewDidLoad()->()+ 521在ViewController.swift:60,地址= 0x0000000107f3e609,已解决,命中计数= 1 

列表中的第一个数字是一个断点ID,您可以使用它来引用任何特定的断点。 让我们直接从控制台设置一些新的断点:

  (lldb)断点设置-f ViewController.swift -l 96断点3:其中= LLDB-Debugger-Exploration`LLDB_Debugger_Exploration.ViewController.valueOfLifeWithoutSumOf(Swift.Int和:Swift.Int)-> Swift.Int + 45在ViewController.swift :96,地址= 0x0000000107f3eb4d 

在此示例中, -f是您要在其中放置断点的文件的名称。 -l是新断点的行号。 有一种使用b快捷方式设置完全相同的断点的较短方法:

  (lldb)b ViewController.swift:96 

您还可以使用以下命令使用特定的正则表达式(例如函数名称)设置断点:

  (lldb)断点集--func-regex valueOfLifeWithoutSumOf(lldb)b -r valueOfLifeWithoutSumOf //上面命令的简短版本 

有时只为一个命中设置一个断点很有用。 然后指示断点立即删除自身。 当然,有一个标志:

  (lldb)断点设置-一击-f ViewController.swift -l 90(lldb)br s -o -f ViewController.swift -l 91 //上面命令的较短版本 

现在让我们解决最有趣的部分-断点自动化。 您是否知道可以设置一个特定的操作,该操作将在发生断点时立即执行? 是的你可以! 您是否在代码中使用print()来探索您感兴趣的调试值? 请不要那样做,有更好的方法。 🙂

使用breakpoint command ,您可以设置将在遇到断点时立即执行的命令。 您甚至可以设置“不可见”的断点,而不会中断执行。 好吧,从技术上讲,这些“不可见”的断点将中断执行,但是如果您在命令链的末尾添加continue命令,您将不会注意到它。

  (lldb)b ViewController.swift:96 //让我们先添加一个断点断点2:其中= LLDB-Debugger-Exploration`LLDB_Debugger_Exploration.ViewController.valueOfLifeWithoutSumOf(Swift.Int和:Swift.Int)-> Swift.Int + 45 at ViewController.swift:96,地址= 0x000000010c555b4d(lldb)断点命令添加2 //设置一些命令输入调试器命令。 输入“ DONE”结束。
 > p sum //打印“ sum”变量的值
 > pa + b //评估a + b
 >完成 

为了确保您添加了正确的命令,请使用breakpoint command list 子命令:

  (lldb)断点命令列表2断点2:
断点命令:
和
 pa + b 

下次遇到此断点时,我们将在控制台中获得以下输出:

 过程36612恢复
和
 (整数)$ R0 = 6p a + b
 (整数)$ R1 = 4 

大! 正是我们想要的。 通过在命令链的末尾添加continue命令,可以使其更加平滑。 因此,您甚至都不会在此断点处停止。

  (lldb)breakpoint命令add 2 //设置一些命令输入调试器命令。 输入“ DONE”结束。
 > p sum //打印“ sum”变量的值
 > pa + b //评估a + b
 >继续//首次点击后恢复
 >完成 

因此结果将是:

 和
 (整数)$ R0 = 6p a + b
 (整数)$ R1 = 4继续
过程36863恢复
 #3命令“继续”继续执行目标。 

使用thread命令及其子命令,您可以完全控制执行流程: step-overstep-instep-outcontinue 。 这些直接等效于Xcode调试器工具栏上的流控制按钮。

这些特定命令还有一个预定义的LLDB快捷方式:

  (lldb)线程移步
 (lldb)next //与“线程跨步”命令相同
 (lldb)n //与“ next”命令(lldb)线程步入相同
 (lldb)步骤//与“线程插入”相同
 (lldb)s //与“ step”相同 

为了获得有关当前线程的更多信息,只需调用info子命令:

  (lldb)线程信息线程#1:tid = 0x17de17,0x0000000109429a90 LLDB-Debugger-Exploration`ViewController.sumOf(a = 2,b = 2,self = 0x00007fe775507390)->在ViewController.swift:90处为int,queue ='com .apple.main-thread',停止原因=介入 

要查看所有当前活动线程的列表,请使用list子命令:

  (lldb)线程列表进程50693已停止*线程#1:tid = 0x17de17,0x0000000109429a90 LLDB-Debugger-Exploration`ViewController.sumOf(a = 2,b = 2,self = 0x00007fe775507390)-> ViewController.swift:90处的Int,队列='com.apple.main-thread',停止原因=进入线程#2:tid = 0x17df4a,0x000000010daa4dc6 libsystem_kernel.dylib`kevent_qos + 10,队列='com.apple.libdispatch-manager'线程#3:tid = 0x17df4b,0x000000010daa444e libsystem_kernel.dylib`__workq_kernreturn + 10线程#5:tid = 0x17df4e,0x000000010da9c34a libsystem_kernel.dylib`mach_msg_trap + 10,name ='com.apple.uikit.eventfetch-thread' 

commandplatformgui platform

在LLDB中,您可以找到用于管理其他命令的命令。 听起来很奇怪,但实际上,它是非常有用的小工具。 首先,它允许您直接从文件中执行一些LLDB命令。 因此,您可以使用一些有用的命令创建一个文件,并立即执行它们,就像将其作为单个LLDB命令一样。 这是文件的一个简单示例:

 线程信息//显示当前线程信息
 br list //显示所有断点 

这是实际命令的样子:

  (lldb)命令源/ Users / Ahmed / Desktop / lldb-test-script'/ Users / Ahmed / Desktop / lldb-test-script'中的执行命令。线程信息
线程#1:tid = 0x17de17,0x0000000109429a90 LLDB-Debugger-Exploration`ViewController.sumOf(a = 2,b = 2,self = 0x00007fe775507390)->在ViewController.swift:90处为Int,queue ='com.apple.main-线程”,停止原因=步骤inbr列表
当前断点:
 1:文件='/Users/Ahmed/Desktop/Recent/LLDB-Debugger-Exploration/LLDB-Debugger-Exploration/ViewController.swift',第= 60行,exact_match = 0,位置= 1,已解决= 1,点击计数= 0
 1.1:其中= LLDB-Debugger-Exploration`LLDB_Debugger_Exploration.ViewController.viewDidLoad()->()+ 521 at ViewController.swift:60,地址= 0x0000000109429609,已解决,命中计数= 0 

不幸的是,还有一个缺点,就是您不能将任何参数传递给源文件(除非您要在脚本文件本身中创建一个有效变量)。

如果您需要更高级的功能,则可以始终使用script子命令。 这将允许您管理( adddeleteimportlist )自定义Python脚本。 使用script实现真正的自动化。 请查看有关LLDB的Python脚本的这份不错的指南。 仅出于演示目的,让我们创建一个脚本文件script.py并编写一个简单的命令print_hello() ,该命令仅在控制台中打印“ Hello Debugger!”:

您可以使用status子命令快速检查当前平台信息。 status会告诉您:SDK路径,处理器体系结构,操作系统版本,甚至此SDK的可用设备列表。

  (lldb)平台状态平台:ios-simulator
三重:x86_64-apple-macosx
操作系统版本:10.12.5(16F73)
内核:达尔文内核版本16.6.0:2017年4月14日星期五太平洋夏令时间16:21:16; 根目录:xnu-3789.60.24〜6 / RELEASE_X86_64
主机名:127.0.0.1
 WorkingDir:/
 SDK路径:“ / Applications / Xcode.app / Contents / Developer / Platforms / iPhoneSimulator.platform / Developer / SDKs / iPhoneSimulator.sdk”可用的设备:
 614F8701-3D93-4B43-AE86-46A42FEB905A:iPhone 4s
 CD516CF7-2AE7-4127-92DF-F536FE56BA22:iPhone 5
 0D76F30F-2332-4E0C-9F00-B86F009D59A3:iPhone 5s
 3084003F-7626-462A-825B-193E6E5B9AA7:iPhone 6
 ... 

好吧,您不能在Xcode中使用LLDB GUI模式,但是您始终可以从终端上进行操作。

  (lldb)gui //如果您尝试在Xcode中执行gui命令,则会看到此错误
错误:gui命令需要交互式终端。 

在本文中,我只是简单介绍了LLDB的真正功能。 尽管LLDB在我们这里已经存在了很长时间,但仍有许多人没有充分利用它的潜力。 我快速概述了基本功能以及LLDB如何使调试过程自动化。 我希望它是有用的。

留下了太多的LLDB功能。 还有一些我什至没有提到的视图调试技术。 如果您对此主题感兴趣,请在下面发表评论。 我很乐意写这本书。

我强烈建议您打开终端,启用LLDB并输入help 。 这将向您显示完整的文档。 您可以花数小时阅读它。 但我保证这将是合理的时间投入。 因为了解您的工具是工程师真正提高生产力的唯一途径。


关于LLDB的参考和有用的文章

  • LLDB官方网站-您将在此处找到与LLDB相关的所有可能材料。 文档,指南,教程,资源等等。
  • Apple的LLDB快速入门指南-与往常一样,Apple拥有出色的文档。 本指南将帮助您真正快速地开始使用LLDB。 此外,他们还介绍了如何在不使用Xcode的情况下使用LLDB进行调试。
  • 调试器的工作方式:第1部分-基础-我非常喜欢本系列文章。 这只是一个很棒的概述,调试器是如何工作的。 本文使用C编写的手工调试器代码描述了所有基本原理。我强烈建议您阅读这些精彩系列的所有部分(第2部分,第3部分)。
  • WWDC14 LLDB中的高级Swift调试-概述了有关Swift调试的LLDB新增功能。 LLDB如何使用内置函数和功能帮助您在整个调试过程中提高工作效率。
  • LLDB Python脚本编制简介— LLDB Python脚本编制指南,可让您真正快速启动。
  • 在调试器中跳舞。 带有LLDB的Waltz —对LLDB基本知识的巧妙介绍。 有些信息有些过时了(例如(lldb) thread return命令。不幸的是,它不能与Swift一起正常使用,因为它可能给引用计数带来一些损害)。 尽管如此,它还是开始您的LLDB旅程的很棒的文章。