第1章:开始

在这一章中,你将熟悉LLDB并研究内省和调试程序的过程。你将从内省一个你甚至没有写的程序开始 - Xcode!

您将对使用LLDB的调试会话进行一次旋风式的参观,并发现您可以对一个完全没有源代码的程序做出惊人的改变。这一章更倾向于做而不是学,所以很多概念和对某些LLDB功能的深入研究将留到后面的章节中。

让我们开始吧。

了解Rootless的情况

在你开始使用LLDB之前,你需要了解苹果公司为挫败恶意软件而引入的一项功能。不幸的是,这个功能也会挫败你使用LLDB和其他工具(如DTrace)进行反省和调试的尝试。不过不要担心,因为苹果公司包括一个关闭该功能的方法--对于那些知道自己在做什么的人来说。而你也将成为这些知道自己在做什么的人中的一员!

阻止你自省和调试尝试的功能是系统完整性保护,也被称为Rootless。这个系统限制了程序可以做的事情,即使它们有root权限--以阻止恶意软件在你的系统深处种植自己。

尽管Rootless系统是安全方面的一个重大飞跃,但它也带来了一些麻烦,因为它使程序更难调试。特别是,它可以防止其他进程将调试器附加到苹果签署的程序上。

由于本书不仅涉及到调试你自己的应用程序,而且涉及到你所好奇的任何应用程序,因此,在你学习调试的时候,你必须移除这个功能,这样你就可以检查你所选择的任何应用程序。

如果你目前启用了Rootless,你将无法附加到苹果的大部分程序。

例如,尝试将LLDB附加到Finder应用程序。

打开一个终端窗口,寻找Finder进程,像这样。

lldb -n Finder

你会注意到以下错误。

error: attach failed: cannot attach to process due to System Integrity Protection

注意:有很多方法可以附加到一个进程,以及LLDB成功附加时的具体配置。要了解更多关于附加到进程的信息,请查看第3章,"用LLDB附加"。

禁用Rootless系统

注意:遵循本书的一个更安全的方法是使用VMWare或VirtualBox创建一个专用的虚拟机,并按照以下详细步骤在该虚拟机上禁用Rootless。下载和设置一个macOS虚拟机可能需要大约一个小时,这取决于你的计算机的硬件(和网速!)。从谷歌获取最新的虚拟机安装说明,因为macOS版本和虚拟机软件会有不同的安装步骤。如果你选择在没有虚拟机的电脑上禁用Rootless,最理想的做法是在完成该特定章节后重新启用Rootless。幸运的是,本书中只有少数几个章节需要禁用Rootless!

要禁用Rootless,请执行以下步骤。

  1. 重新启动你的MacOS机器。

  2. 当屏幕变成空白时,按住Command + R,直到出现苹果的启动标志。这将使你的电脑进入恢复模式。

  3. 现在,从顶部找到 "实用工具 "菜单,然后选择 "终端"。

  4. 在终端窗口打开的情况下,输入。

    csrutil disable && reboot
    
  5. 如果csrutil disable命令成功的话,你的电脑将在禁用Rootless后重新启动。

你可以在电脑启动后在终端查询Rootless的状态,以验证你是否已经成功禁用Rootless。

csrutil status

你应该看到以下内容。

System Integrity Protection status: disabled.

现在,SIP已经被禁用,执行你之前尝试的 "附加到Finder "的LLDB命令。

lldb -n Finder

LLDB现在应该把自己附加到当前的Finder进程中。成功附加的输出应该是这样的。

在确认附加成功后,通过关闭终端窗口,或者在LLDB控制台输入退出并确认,来分离LLDB。

将LLDB附加到Xcode

现在你已经禁用了Rootless,你可以将LLDB附加到你的macOS机器上的任何进程(可能会有一些障碍,比如trace系统调用,但我们稍后会讨论这个问题)。你首先要研究的是你在日常开发中经常使用的一个应用程序。Xcode! 在继续之前,请确保你的电脑上安装了最新版本的Xcode 10。

打开一个新的终端窗口。接下来,通过按⌘ + Shift + I来编辑终端标签的标题,一个新的弹出窗口将出现。编辑标签的标题为LLDB。

接下来,确保Xcode没有运行,否则你会出现多个Xcode的运行实例,这可能会造成混乱。

在终端,键入以下内容。

lldb

这就启动了LLDB。

按⌘ + T创建一个新的Terminal标签,用⌘ + Shift + I再次编辑该标签的标题,并将该标签命名为Xcode stderr。当你从调试器中打印内容时,这个终端标签将包含所有输出。

确保你在Xcode stderr终端标签上,并输入以下内容。

tty

你应该看到与下面类似的东西。

/dev/ttys027

如果你的电脑不一样,也不用担心;如果不一样,我会很惊讶。把它看作是你的终端会话的地址。

为了说明你将对Xcode stderr标签做什么,创建另一个标签并在其中输入以下内容。

echo "hello debugger" 1>/dev/ttys027

请确保用从tty命令中获得的唯一路径替换你的终端路径。

现在切换到Xcode stderr标签。hello debugger的字样应该已经跳出来了。你将使用同样的技巧将Xcode的stderr输出管道到这个标签。

最后,关闭第三个未命名的标签,并导航回LLDB标签。

总结一下。你现在应该有两个终端标签:一个名为 "LLDB "的标签,它包含一个正在运行的LLDB实例,另一个名为 "Xcode stderr "的标签,它包含你之前执行的tty命令。

从那里,在LLDB终端标签中输入以下内容。

(ldb) file /Applications/Xcode.app/Contents/MacOS/Xcode

这将设置可执行目标为Xcode。

注意:如果你使用的是Xcode的预发布版本,那么Xcode的名称和路径可能是不同的。

你可以通过启动Xcode并在终端输入以下内容来检查你当前运行的Xcode的路径。

ps -ef `pgrep -x Xcode`。

一旦你有了Xcode的路径,就用这个新的路径代替。

现在从LLDB启动Xcode进程,用你的Xcode stderr标签的tty地址再次替换/dev/ttys027。

(lldb) process launch -e /dev/ttys027 --

启动参数e指定了stderr的位置。常见的日志功能,如Objective-C的NSlog或Swift的print函数,都是输出到stderr--是的,不是stdout!你会在后面的文章中把自己的日志打印到stderr。稍后你会把你自己的日志打印到stderr。

Xcode将在片刻后启动。切换到Xcode并点击File ▸ New ▸ Project.... 接下来,选择iOS ▸ Application ▸ Single View Application并点击Next。将产品命名为Hello Debugger。确保选择Swift作为编程语言,并取消选择单元或UI测试的任何选项。点击 "下一步 "并在你希望的地方保存该项目。

你现在有了一个新的Xcode项目。安排好窗口,以便您可以看到终端和Xcode。

导航到Xcode并打开ViewController.swift。

注意:你可能会注意到Xcode stderr终端窗口的一些输出;这是由于Xcode的作者通过NSLog或其他stderr控制台打印功能记录的内容。

一个 "迅速 "变化的环境

苹果在自己的软件中采用Swift时一直很谨慎--这也是可以理解的。无论人们对Swift的信仰是什么(似乎是宗教信仰),不管是好是坏,它仍然是一种不成熟的语言,以令人难以置信的速度发展,充满了突破性的变化。

然而,苹果公司的情况正在发生变化。苹果现在正在更积极地在他们自己的应用程序中采用Swift,比如iOS模拟器......甚至是Xcode!

根据最后的统计,Xcode 10包含了近200个包含Swift的框架。

你怎么能自己验证这些信息呢?这些信息是通过我的帮助LLDB脚本的组合获得的,这些脚本可以在这里找到:https://github.com/DerekSelander/LLDB 。 它们对所有人都是免费的。我强烈建议你克隆并安装这个 repo,因为我偶尔会推送新的 LLDB 命令,让你的调试工作更加愉快。安装说明在这个 repo 的 README 文件中。在本书中,当有一种情况通过这些LLDB脚本变得更加容易时,我将参考这个 repo。

获得这些信息的可怕命令是下面这个。如果你想执行这个命令,你需要安装上面注释中提到的 repo。

(ldb) sys echo "$(dclass -t swift)" | grep -v _ | grep "\." | cut -d. f1 | uniq | wc -l

把这个命令分解开来,dclass -t swift 命令是一个自定义的 LLDB 命令,它将转储所有进程已知的、属于 Swift 的类。sys 命令将允许你像在终端中一样执行命令,但$()中的任何内容都将通过 LLDB 先被评估。从这里开始,就是操纵dclass命令给出的所有Swift类的输出。

Swift类的命名通常采用ModuleName.ClassName的形式,其中模块是该类实现的框架。该命令的其余部分做了以下工作。

这些自定义的LLDB命令(dclass,sys)是用Python和LLDB的Python模块(令人困惑的是也叫ldb)一起建立的。在本书第四节中,当你学习建立自定义的高级LLDB脚本时,你将非常习惯于使用这个Python模块。

通过点击找到一个类

现在Xcode已经设置好了,你的终端调试窗口也正确地创建和定位了,现在是时候开始利用调试器的帮助探索Xcode了。

在调试的时候,Cocoa SDK的知识会有很大的帮助。例如,[NSView hitTest:]是一个有用的Objective-C方法,它返回负责处理运行循环中的点击或手势事件的类。这个方法将首先在包含的NSView上被触发,并递归地钻入处理这个触摸的最远的子视图。你可以利用Cocoa SDK的这些知识来帮助确定你所点击的视图的类别。

在你的LLDB标签中,输入Ctrl + C来暂停调试器。从那里,键入。

(lldb) b -[NSView hitTest:] 
Breakpoint 1: where = AppKit`-[NSView hitTest:], address = 0x000000010338277b

这是你的第一个断点,接下来还有很多。你将在第4章 "代码中的停止 "中了解如何创建、修改和删除断点的细节,但现在只需知道你已经在-[NSView hitTest:]上创建了一个断点。

由于调试器的存在,Xcode现在已经暂停了。恢复程序。

(lldb) continue

点击Xcode窗口中的任何地方(或者在某些情况下,甚至在Xcode上移动你的光标也会这样做);Xcode将立即暂停,LLDB将显示一个断点已经被击中。

hitTest:断点已经变红。你可以通过检查RDI CPU寄存器来检查哪个视图被击中。在LLDB中把它打印出来。

(ldb) po $rdi

这个命令指示LLDB打印出对象在内存地址上的内容,这个内存地址被存储在RDI汇编寄存器中的内容所引用。

注意:想知道为什么这个命令是po吗? po代表打印对象。po通常更有用,因为它给出了NSObject(或Swift的SwiftObject)的描述或debugDescription方法,如果有的话。

如果你想让你的调试工作更上一层楼,汇编是一项重要的学习技能。它能让你深入了解苹果的代码--即使你没有任何源代码可读。它将使你更加了解Swift编译器团队是如何在Objective-C中与Swift共舞的,它将使你更加了解苹果设备上的一切运作方式。

你会在第11章 "汇编寄存器调用约定 "中了解更多关于寄存器和汇编的知识。

现在,只需知道上述LLDB命令中的$rdi寄存器包含hitTest:方法被调用的子类NSView的实例。

注意:输出将产生不同的结果,这取决于你点击的地方和你使用的Xcode版本。它可能给出一个专门针对Xcode的私有类,也可能给你一个属于Cocoa的公共类。

在LLDB中,输入以下内容来恢复程序。

(lldb) continue

Xcode没有继续,而是很可能为hitTest:找到另一个断点并暂停执行。这是由于hitTest:方法对所有包含在被点击的父视图中的子视图递归地调用这个方法。你可以检查这个断点的内容,但这很快就会变得乏味,因为有这么多的视图组成了Xcode。

自动进行hitTest。

点击一个视图,停止,po'ing RDI寄存器然后继续的过程很快就会让人疲惫。如果你创建一个断点来自动完成这一切呢?

有几种方法可以做到这一点,但也许最干净的方法是声明一个新的断点,并具有你想要的所有特征。这不是很好吗?

用以下命令删除前一个断点。

(lldb) breakpoint delete

LLDB会问你是否确定要删除所有断点,按回车键或按'Y'然后回车确认。

现在,用以下方法创建一个新的断点。

(lldb) breakpoint set -n  "-[NSView hitTest:]" -C "po $rdi" -G1

这个命令的要点是在-[NSView hitTest:]上创建一个断点,让它执行 "po $rdi "命令,然后在执行该命令后自动继续。你将在后面的章节中进一步了解这些选项。

用continue命令恢复执行。

(lldb) continue

现在,点击Xcode中的任何地方,在终端控制台中查看输出。你会看到许多NSViews被调用,看看它们是否应该接受鼠标的点击!

为重要内容过滤断点

由于有这么多的NSViews组成了Xcode,你需要一种方法来过滤掉一些噪音,只在与你寻找的内容相关的NSView上停止。这是一个调试经常被调用的方法的例子,你想找到一个独特的案例,帮助确定你真正在寻找的东西。

从Xcode 10开始,负责在Xcode IDE中直观显示代码的类是一个属于IDESourceEditor模块的私有Swift类,名为IDESourceEditorView。这个类作为视觉协调者,将您的所有代码移交给其他私有类,以帮助编译和创建您的应用程序。

假设你只想在点击IDESourceEditorView的一个实例时断点。你可以通过使用断点条件来修改现有的断点,只在IDESourceEditorView点击时停止。

只要你仍然设置了-[NSView hitTest:]断点,并且它是LLDB会话中唯一的活动断点,你就可以用下面的LLDB命令修改该断点。

(lldb) breakpoint modify -c '(BOOL)[NSStringFromClass((id)[$rdi class]) containsString:@"IDESourceEditorView"]' -G0

这个命令修改了调试会话中所有现有的断点,并创建了一个条件,每次-[NSView hitTest:] 触发时都会进行评估。如果该条件评估为真,那么调试器的执行将暂停。这个条件检查NSView的实例是否为IDESourceEditorView类型。最后的-G0说要修改断点,使其在执行完动作后不自动恢复执行。

修改完上面的断点后,在Xcode中点击代码区。LLDB应该在hitTest:时停止。打印出这个方法所调用的类的实例。

(lldb) po $rdi

你的输出应该看起来类似于下面的内容。

IDESourceEditorView: Frame: (0.0, 0.0, 1109.0, 462.0), Bounds: (0.0, 0.0, 1109.0, 462.0) contentViewOffset: 0.0

这是打印出该对象的描述。你会注意到,这里面没有指针引用,因为Swift隐藏了指针引用。如果你需要指针引用,有几种方法可以解决这个问题。最简单的是使用打印格式化。在LLDB中键入以下内容。

(ldb) p/x $rdi

你会得到与下面类似的东西。

(unsigned long) $3 = 0x0000000110a42600

由于 RDI 指向一个有效的 Objective-C NSObject 子类(用 Swift 写的),你也可以通过 po'ing 这个地址而不是寄存器来获得同样的信息。

在LDDB中键入以下内容,同时确保用你自己的地址替换。

(ldb) po 0x0000000110a42600

你会得到和前面一样的输出。

你可能会怀疑这个由RDI寄存器指向的引用是否真的指向显示你代码的NSView。你可以通过在LLDB中输入以下内容来轻松地验证这是否是真的。

(lldb) po [``rdi setHidden:!(BOOL)[``rdi isHidden]]; [CATransaction flush]

注意:打出的命令有点长,对吗?在第10章:"Regex命令 "中,你将学习如何建立方便的快捷方式,这样你就不必打出这些长的LLDB命令。如果你选择安装前面提到的LLDB repo,上面这个行动的便利命令是tv命令,或 "切换视图"

只要 RDI 指向正确的参考文献,你的代码编辑器视图就会消失!


你可以通过反复按回车键来切换这个视图的开启和关闭。LLDB会自动执行之前的命令。

由于这是NSView的一个子类,NSView的所有方法都适用。例如,字符串命令可以通过LLDB查询你的源代码的内容。输入以下内容。

(ldb) po [$rdi string] 。

这将倾倒出你的源代码编辑器的内容。很好!

永远记住,你在开发周期中的任何API都可以在LLDB中使用。如果你足够疯狂,你可以通过执行LLDB命令来创建一个完整的应用程序。

当你玩腻了这个实例上的NSView APIs时,把RDI引用的地址复制下来(复制到剪贴板上或添加到贴纸应用中)。你马上就会再次引用它。

另外,你是否注意到在p/x $rdi命令中的十六进制值之前的输出?在我的输出中,我得到了3美元,这意味着你可以用3美元作为你刚刚抓取的那个指针值的参考。当RDI寄存器指向其他东西,而你以后还想引用这个NSView时,这就非常有用。

Swift与Objective-C的调试

等等--我们在一个Swift类上使用Objective-C?你说对了! 你会发现,一个Swift类大部分都是Objective-C语言(不过Swift结构体就不一样了)。你将通过使用Swift在LLDB中修改控制台的源代码来证实这一点。

首先,在Swift的调试上下文中导入以下模块。

(lldb) ex -l swift -- import Foundation 
(lldb) ex -l swift -- import AppKit

ex命令(表达式的简称)让你评估代码,是你的p/po LLDB命令的基础。-l swift告诉LLDB将你的命令解释为Swift代码。你刚刚导入了头文件,通过Swift调用这两个模块中的适当方法。在接下来的两条命令中,你会需要这些。

输入以下内容,将0x0110a42600替换为你最近复制到剪贴板的NSView子类的内存地址。

(lldb) ex -l swift -o -- unsafeBitCast(0x0110a42600, to: NSView.self)

这个命令打印出了IDESourceEditorView实例--但这次是用Swift!

现在,通过LLDB向你的源代码添加一些文本。

(ldb) ex -l swift -o -- unsafeBitCast(0x0110a42600, to: NSView.self).insertText("Yay! Swift!" )

根据你的光标在Xcode控制台的位置,你会看到新的字符串 "Yay! Swift!"添加到你的源代码中。

当突然停止调试器,或在Objective-C代码上,LLDB将默认在调试时使用Objective-C上下文。这意味着你执行的po将期望使用Objective-C语法,除非你强迫LLDB使用不同的语言,比如你这一点是可以改变的。这是有可能改变的,但本书更倾向于使用Objective-C,因为Swift REPL在查错方面是很残酷的,执行命令的编译时间很慢,一般来说错误更多,而且会阻止你执行Swift LLDB上下文所不知道的方法。

所有这些最终都会消失,但我们必须有耐心。Swift ABI必须先稳定下来。只有这样,Swift工具才能真正变得坚如磐石。

接下来该怎么走?

这是一个广义的、旋风式的介绍,介绍了如何使用LLDB和附加到一个没有任何源代码帮助你的过程。这一章略过了很多细节,但目的是让你直接进入调试/逆向工程过程。

对某些人来说,这第一章可能有点吓人,但我们会放慢速度,从这里开始详细描述方法。剩余的很多章节可以让你深入了解细节!

继续阅读,在第1节的剩余部分中学习精华部分。调试愉快!


上一章 目录 下一章