第6章:线、框架和Step操作

你已经学会了如何创建断点,如何打印和修改数值,以及如何在调试器中暂停时执行代码。但到目前为止,您还没有掌握如何在调试器中移动并检查眼前的数据。现在是时候解决这个问题了!

在这一章中,你将学习如何在LLDB暂停的情况下将调试器移入和移出函数。

这是一项重要的技能,因为在进入或退出代码片断时,你经常想检查随时间变化的数值。

堆栈101

当一个计算机程序执行时,它在堆栈和堆中存储数值。两者都有其优点。作为一个高级调试员,你需要对这些东西的工作原理有一个很好的理解。现在,让我们简单地看一下堆栈。

你可能已经知道了关于堆栈在计算机科学术语中是什么的全部内容。在任何情况下,都值得对一个进程在执行时如何跟踪代码和变量有一个基本的了解(或复习)。当你使用LLDB来浏览代码时,这些知识就会派上用场。

堆栈是一个LIFO(Last-In-First-Out)队列,用于存储对当前执行的代码的引用。这种后进先出的排序意味着最近添加的东西会被首先删除。想想看,一摞盘子。把一个盘子加到最上面,它就会被你首先拿下来。

堆栈指针指向堆栈的当前顶部。在盘子的比喻中,堆栈指针指向那个最上面的盘子,告诉你从哪里拿下一个盘子,或者把下一个盘子放在哪里。

在这个图中,高地址显示在顶部(0xFFFFFFFF),低地址显示在底部(0x00000000),展示了堆栈将向下增长。

有些插图喜欢把高地址放在底部,以配合盘子的比喻,因为堆栈将被显示为向上生长。然而,我相信任何展示堆栈的图示都应该从高地址开始向下生长,因为这在以后讨论堆栈指针的偏移量时将引起更多的麻烦。

你将在第13章 "汇编和堆栈 "中深入研究堆栈指针和其他寄存器,但在本章中,你将探索各种方法来浏览堆栈中的代码。

检查堆栈的框架

在本章中,你将继续使用Signals项目。

在这一章中你会瞥见一些汇编。不要害怕! 它没有那么糟糕。然而,请确保在本章中使用iPhone X模拟器,因为如果你在实际的iOS设备上生成代码,那么汇编会有所不同。

这是因为设备使用的是ARM架构,而模拟器使用的是你的Mac的本地指令集,x86_64(或者i386,如果你在比iPhone 5s模拟器低的东西上编译)。

在Xcode中打开Signals项目。接下来,用下面的函数名称添加一个符号断点。请确保尊重函数签名中的空格,否则断点将无法被识别。

Signals.MasterViewController.viewWillAppear(Swift.Bool) -> ()

这将在MasterViewController的viewWillAppear(_:)方法上建立一个符号断点。

建立并运行该程序。正如所料,调试器将在MasterViewController的viewWillAppear(_:)方法上暂停程序。接下来,看一下Xcode左边面板的堆栈跟踪。如果你还没有看到,点击左边面板上的Debug Navigator(如果你有默认的Xcode键盘,可以按Command + 7)。

确保右下角的三个按钮都被禁用。这些按钮可以帮助筛选堆栈函数,只保留有源代码的函数。因为你正在学习公共和私人代码,你应该总是禁用这些按钮,这样你就可以看到完整的堆栈跟踪。

在Debug Navigator面板中,将出现堆栈跟踪,显示堆栈框架的列表,第一个是viewWillAppear(_:)。接下来是Swift/Objective-C的衔接方法,@objc MasterViewController.viewWillAppear(Bool) ->

():. 这个方法是自动生成的,所以Objective-C可以伸入Swift代码中。

在这之后,有几个堆栈帧的Objective-C代码来自UIKit。再深入一点,你会看到一些属于CoreAnimation的C++代码。再深入一点,你会看到一些方法,这些方法的名称都是CFRunLoop,属于CoreFoundation。最后是主函数(是的,Swift程序仍然有一个主函数,只是被你隐藏起来了)。

你在Xcode中看到的堆栈跟踪只是LLDB可以告诉你的一个漂亮的打印版本。现在让我们来看看。

在LLDB控制台,输入以下内容。

(lldb) thread backtrace

如果你愿意,你也可以简单地输入bt,它的作用是一样的。这实际上是一个不同的命令,如果你拿出你可信赖的朋友help,你就可以看到其中的区别。

在上面的命令之后,你会看到一个堆栈跟踪,就像你在Xcode的调试导航器中看到的那样。

在LLDB中键入以下内容。

(lldb) frame info

你会得到一点类似于下面的输出。

frame #0: 0x000000010ba1f8dc 
Signals`MasterViewController.viewWillAppear(animated=false, 
self=0x00007fd286c0af10) at MasterViewController.swift:50

正如你所看到的,这个输出与Debug Navigator中发现的内容一致。那么,如果你可以从Debug Navigator中看到所有的内容,为什么这一点还这么重要呢?好吧,使用LLDB控制台可以让你更精确地控制你想看到的信息。此外,你将制作自定义LLDB脚本,这些命令将变得非常有用。知道Xcode从哪里得到它的信息也很好,对吗?

回顾一下Debug Navigator,你会看到一些数字,从0开始,随着你往下调用堆栈而递增。这个数字可以帮助你联系到你正在看的堆栈框架。通过输入以下内容选择一个不同的堆栈。

(lldb) frame select 1

Xcode将跳转到@objc桥接方法,即位于堆栈中索引1的方法。什么是@objc桥接方法?这是一个由Swift编译器生成的方法,用于与Objective-C的动态特性进行交互。在早期版本的Swift(Swift <= 3.2)中,任何NSObject都意味着@objc桥接方法的生成。在Swift 4的默认构建设置下,即使是Objective-C NSObject也需要有@objc(或@objcMembers)属性,以便Swift编译器生成桥接方法。

只要你使用的是模拟器而不是实际的设备,你就会得到一些看起来类似于下面的汇编。

请注意汇编中的绿色线条。就在这一行之前是负责执行viewWillAppear(_:)的callq指令,你在前面设置了一个断点。

不要让程序集太模糊你的眼睛。你还没有走出汇编的阴影......

踩点

在掌握LLDB时,当程序暂停时,你可以做的三个最重要的导航动作都是围绕着步进程序进行的。通过LLDB,你可以跳过、跳入或跳出代码。

每一个动作都允许你继续执行你的程序代码,但是是小块的,以便让你检查程序的执行情况。

Stepping over

Stepping over允许您在调试器当前暂停的上下文中跨入下一条代码语句(通常是下一行)。这意味着如果当前语句正在调用另一个函数,LLDB将运行到这个函数完成并返回。

让我们看看这个动作。

在LLDB控制台输入以下内容。

(lldb) run

这将重新启动Signals程序而不需要Xcode重新编译。很好! Xcode将像以前一样停止在你的符号断点上。

接下来,输入以下内容。

(lldb) next

调试器将向前移动一行。这就是你Stepping over的方式。很简单,但是很有用!

Stepping in

Stepping in意味着如果下一个语句是一个函数调用,调试器将进入该函数的起点,然后再次暂停。

让我们看看这个动作。

从LLDB重新启动断点程序。

(lldb) run

接下来,输入以下内容。

(lldb) step

运气不好。程序应该已经介入了,因为它所在的行包含了一个函数调用(好吧,实际上它包含了几个!)。

在这种情况下,LLDB的行为更像是一个 "step over",而不是一个 "step into"。这是因为LLDB在默认情况下,如果一个函数没有调试符号,就会忽略踏入该函数的行为。在这种情况下,函数调用都是进入UIKit的,而你没有调试符号。

然而,有一个设置规定了LLDB在进入一个不存在调试符号的函数时应该如何操作。在LLDB中执行以下命令,看看这个设置在哪里。

(lldb) settings show target.process.thread.step-in-avoid-nodebug

如果为真,那么在这些情况下,步入将作为步入的行为。你可以改变这个设置(你会在将来这样做),或者告诉调试器忽略这个设置,你现在就可以这样做。

在LLDB中键入以下内容。

(lldb) step -a0

这告诉LLDB步入,不管你是否有需要的调试符号。

Stepping out

Stepping out意味着一个函数将在其持续时间内继续执行,然后在其返回时停止。从堆栈的角度来看,执行继续进行,直到堆栈框架被弹出。

再次运行Signals项目,这次当调试器暂停时,快速看一下堆栈跟踪。接下来,在LLDB中键入以下内容。

(lldb) finish

你会注意到,现在调试器在堆栈跟踪中暂停了一个函数。试着多执行几次这个命令。

记住,只要按回车键,LLDB就会执行你最后输入的命令。完成命令将指示LLDB走出当前函数。

注意左边面板中的堆栈框架,它们会一个个地消失。

Xcode GUI中的Stepp

尽管你使用控制台可以得到更细化的控制,但Xcode已经为你提供了这些选项,就在LLDB控制台上面的按钮。这些按钮在应用程序运行时出现。

它们依次显示为 step over, step in 和 step out。

最后,Step over和Step in按钮还有一个很酷的技巧。你可以手动控制不同线程的执行,方法是在点击这些按钮的同时按住Control和Shift。

这将导致在调试器暂停的线程中步进,而其余的线程保持暂停状态。如果你正在处理一些难以调试的并发代码,如网络或使用Grand Central Dispatch的东西,这是一个很好的技巧,可以放在你的工具箱后面。

当然,LLDB有一个命令行等价物,通过使用--run-mode选项,或者更简单的-m后跟适当的选项,从控制台做同样的事情。

检查堆栈中的数据

frame命令的一个非常有趣的选项是frame变量子命令。这个命令将采用在你的可执行文件头中发现的调试符号信息(如果你的应用程序被剥离了,则采用dYSM......后面会详细介绍),并为该特定的堆栈帧转储信息。由于有了这些调试信息,框架变量命令可以很容易地告诉你函数中所有变量的范围,以及使用适当选项的程序中的任何全局变量。

再次运行Signals项目,并确保你击中viewWillAppear(_:)断点。接下来,通过点击Xcode的Debug Navigator中的栈顶框架或在控制台中输入框架选择0,或使用LLDB的速记命令f 0来导航到栈顶。

接下来,输入以下内容。

(lldb) frame variable

你会得到与下面类似的输出。

这将转储当前堆栈帧和代码行的可用变量。如果可能的话,它还会从当前可用的变量中转储所有的实例变量,包括公共和私有的。

你,作为一个善于观察的读者,可能会注意到框架变量的输出也与变量视图(Variables View)中的内容相吻合,该面板位于控制台窗口的左侧。

如果还没有,请点击Xcode右下角的左边图标来展开变量视图。你可以比较框架变量和变量视图的输出。你可能会注意到框架变量实际上会比变量视图给你提供更多关于苹果私有API的信息。

接下来,键入以下内容。

(lldb) frame variable -F self

这是一个更简单的方法来查看MasterViewController可用的所有私有变量。它使用了-F选项,代表了 "FLAT"。

这将保持缩进为0,并且只打印出MasterViewController.swift中关于self的信息。

你会得到类似于下面截断的输出。

self = 0x00007fff5540eb40 
self = 
self = 
self = 
self = {} 
self.detailViewController = 0x00007fc728816e00 
self.detailViewController.some = 
self.detailViewController.some = 
self.detailViewController.some = {} 
self.detailViewController.some.signal = 0x00007fc728509de0

正如你所看到的,在与苹果公司的框架合作时,这是一种探索公共变量的有吸引力的方式。

接下来该怎么做?

在本章中,你已经探索了堆栈框架和其中的内容。你还学会了如何通过踏入、踏出和踏过代码来浏览堆栈。

在你没有涉及的线程命令中,有很多选项。试着用help thread命令来探索其中的一些,看看你是否能学到一些很酷的选项。

看看thread until、thread jump和thread return等子命令。你以后会用到它们,但它们是很有趣的命令,所以现在就试一下,看看它们有什么作用


上一章 目录 下一章