本文是Advanced Apple Debugging的学习笔记.
首先将Xcode升级到8.3版本.可以通过下载地址下载.
我们主要是通过LLDB,Python和DTrace来查看apple code.
在这一章中,你将会熟悉如何使用LLDB查看内部进程,并且调试一个程序.
你将在使用LLDB 的调试的过程中会发现你可以对一个你没有源代码的程序做出令人惊喜的改变.第一章会作为学习的一个过度章节, 因此许多重要的概念和深入到LLDB函数的功能将会在后面的章节介绍.
通过Rootless开始
在你开始使用LLDB之前, 你需要学习一些关于apple挫败恶意软件的特点介绍.让人不爽的是,这些特点会打击你使用LLDB和其他类似的工具比如DTrace调试内部代码的企图.但是不要怂,因为apple为那些知道自己在做什么的人提供了一个方法来关闭这个功能.并且你就将成为那些知道自己在做什么的人中的一员.
阻止你企图进入内部调试的功能是System Integrity Protection
, 也号称Rootless
.
这个系统限制了哪些程序可以(即便是他们本身有root权限)阻止恶意程序安装在你系统的中.
尽管rootless 在安全方面是实质性的进步, 但同时他也带来了一些让程序变得难以调试的麻烦.说白了就是它阻止了其他进程调试apple签名了的进程.
由于本书要调试的不仅仅是你自己的程序, 还有你非常感兴趣的程序, 因此在你学习的时候移除这些功能就变得非常重要,以便你可以查看你选择的程序.
如果你现在已经启用了roorless, 你将不能够调试apple的主要程序.但是也有例外, 比如iOS模拟器里的自带的几个程序可以用.
例如, 尝试将LLDB附加到Finder应用程序.
打开Terminal, 通过下面的命令查看finder的进程.
lldb -n Finder
你将会看到下面的错误:
error: attach failed: cannot attach to process due to System Integrity
注意: 有许多方法可以附加一个进程, 此外当LLDB附加成功的时候可以指定配置. 想要学习更多关于附加进程的知识可以查看第三章的内容,"用LLDB附加"
禁用Rootless
要禁用Rootless, 请执行下面的步骤:
- 重启你的macOS 设备
- 当屏幕变成空白的时候, 按住Command+R直到apple的启动logo出现,这将会使你的电脑进入恢复模式.
3.现在, 在顶部找到Utilities菜单然后选择Terminal,.
4.当Terminal窗口打开的时候, 输入:
csrutil disable; reboot
5.你的电脑将会用禁用Rootles的模式重新启动.
注意:练习本书中所有操作的一个安全的方法是在VMWare or VirtualBox创建一个虚拟机来操作, 并仅仅在虚拟机中禁用Rootless.
你可以通过在终端中输入同样的命令来验证一下你是否成功的禁用了Rootless.
lldb -n Finder
LLDB应该会讲自己附加到当前Finder的进程.附加成功后的输出因该是下面这个样子.
在确认成功附加以后.通过关闭Terminal窗口或者输入
quit
退出LLDB并且在LLDB的控制台中确认是否已经退出.
附加LLDB到Xcode
现在你已经禁用了Rootless, 并且你可以将LLDB附加到进程上, 是时候开始你调试的旋风之旅了.你查看的第一个应用程序将是你在日复一日的开发中经常使用的Xcode!
打开一个新的Terminal窗口. 接下来, 通过按下⌘ + Shift + I来编辑, 窗口的标题. 一个新的弹窗将会出现.在Tab Title 一栏输入LLDB.
然后确保Xcode没有运行, 否则你最终会因为运行了多个Xcode的实例而造成混乱.
在Terminal中输入下面的命令:
lldb
这将会启动LLDB.
现在通过按住⌘ + T
来创建一个新的Terminal窗口.再次通过按住⌘ + Shift + I
编辑这个窗口的标题, 并将这个窗口命名为Xcode stderr.这个窗口将会输出你在调试器中打印的所有的内容.
确保你再Xcode stderr 这个终端的窗口中, 并且输入下面的命令:
tty
你将会看到一些类似于下面的输出:
/dev/ttys027
如果你输出的内容跟上面的不一样也不要担心.如果输出的结果与我的不同的话我并不会很惊讶.因为这是您的终端会话的地址.
举例说明一下你将会用Xcode stderr这个窗口来做哪些事情, 创建另外一个窗口并键入一下内容:
echo "hello debugger" 1>/dev/ttys027
确保你将上面的/dev/ttys027
替换成了你通过tty
命令得到的终端的路径.
现在切换到Xcode stderr窗口中, hello debugger
这些单词应该已经出现了.你将使用同样的方法将Xcode’s stderr这个窗口作为输出的管道.
最后, 关闭第三个没有命名的终端的窗口, 并返回到LLDB选项卡中.
在LLDB选项卡中, 将以下命令键入到LLDB中:
(lldb) file /Applications/Xcode.app/Contents/MacOS/Xcode
这条命令会将Xcode设置为可执行的目标文件.
注意:如果你是用的是之前发布的Xcode的版本, Xcode的名字和路径可能会有所不同.
你可以通过在Terminal键入以下命令来查看你当前运行的Xcode的路径.
$ ps -ef `pgrep -x Xcode`
如果你的到了Xcode的路径, 用新的路径来替换
现在从LLDB中启动Xcode进程, 在Xcode stderr 选项卡中键入以下命令, 并将/dev/ttys027
用tty
命令得到的路径替换掉.
(lldb) process launch -e /dev/ttys027 --
启动参数e
指定了stderr的位置.常见的日志功能, 比如 Objective-C的NSLog或者Swift的print函数, 输出到stderr-是的没错, 不是stdout! 稍后你将会打印自己的日志到stderr.
过一会儿xcode就会启动. 切换到Xcode并且选择File\New\Project....然后选择, iOS\Application\Single View Application并且点击Next.将工程命名为Hello Debugger.确保选择了Swift作为程序语言,并且没有选择Unit 和 UI tests中的任意一项.点击Next并且将工程保存到你希望的位置.
现在你有了一个新的Xcode的工程. 整理一下程序的窗口以便你可以同时看到Xcode和Terminal.
在Xcode中打开
ViewController.swift
.
注意: 你可能会留意到Xcode stderr终端窗口中有一些输出.这是由于Xcode的作者通过NSLog或者其他stderr 控制台打印函数输出的日志.
点击一下找到一个类
现在Xcode已经设置好了,而且终端调试窗口也已经正确的创建并摆放在正确的位置上, 是时候开始使用调试的help来浏览Xcode了.
在调试的过程中, Cocoa SDK的知识会有极大的帮助.例如, [NSView hitTest:]
是一个在run loop中返回响应当前点击或者手势操作的类的非常有用的方法.这个方法首先得到触发时包含的NSView 并且最大程度的递归可以处理此次触摸事件的的子视图.你可以使用这些Cocoa SDK来帮助找出你点击的视图的类.
在LLDB选项卡中, 键入Ctrl + C来暂定调试器, 键入:
(lldb) breakpoint set -n "-[NSView hitTest:]"
Breakpoint 1: where = AppKit`-[NSView hitTest:], address =
0x000000010338277b
这是即将学习的许多断点中的第一个.你将在第四章'Stopping in Code'中学习到如何创建,修改和删除断点, 但是现在只需要简单的知道你在-[NSView hitTest:]
中创建了一个断点.
Xcode现在因为调试器而暂停了. 键入以下命令继续运行程序:
(lldb) continue
在Xcode窗口中点击任意位置Xcode将会立即暂停这表明LLDB触发了一个断点.
那个
hitTest:
断点被触发了.你可以通过检查CPU注册表的RDI
来检查哪一个视图被点击了.将他打印在LLDB中:
(lldb) po $rdi
这个命令要求LLDB
打印出汇编寄存器中的内存地址引用的对象的内容.
好奇为什么这个命令是po
?po
的含义是print object
.这里也可以使用p
简单的打印出RDI的内容. po
通常情况下更有用, 它给出的是NSObject的description或者debugDescription(如果可用的话).
如果你想将你的调试能力提升一个等级的话,汇编是一个你要学的非常重要的技能. 它将让你洞察apple的代码, 即便是你从来没有阅读过源码.它将让你非常了解Swift编译团队是如何在Objective-C中用Swift跳来跳去.它将会让你非常了解你的Apple设备是如何做每一件事的.你将会在第十章“Assembly Register Calling Convention”中学到更多关于寄存器和汇编的知识.
但是现在, 简单的知道$rdi
寄存器包含着上面调用hitTest:
方法的NSView或者NSView子类的一个实例.
注意输出可能产生不同的结构这取决于你使用的Xcode的版本和你点击的位置.他可能会给出一个xcode特有的私有类, 也有可能给出一个Cocoa中的公有类.
在LLDB选项卡中键入一下命令继续运行程序:
(lldb) continue
替代继续运行的是, Xcode将会触发hitTest:
的另外一个断点并且暂停执行.这是由于hitTest:
这个方法会被所有包含在被点击视图的父视图里的所有子视图递归调用的事实决定的. 你可以检查这个断点的内容但是这会很乏味因为Xcode里包含很多的视图.
为重要的内容过滤断点
鉴于Xcode创建了很多的NSView实例, 你需要过滤掉那些没用的NSView, 只关注于与你寻找的那些目标相关的NSView上.在你需找一个唯一的对象的时候这是一个在调试过程中经常调用的方法, 有助于你找到你想要的对象.
在Xcode8中, 你用来写编辑代码的地方是一个私有类NSTextView的一个子类.它就类似于UIKit中的UITextView.这个类作为可视化的界面来处理你所有的代码, 并帮助其他私有类来编译和创建你的应用程序.
如果说你只想在点击NSTextView实例的时候触发断点.你可以通过修改已有断点的breakpoint conditions
来设置只有在NSTextView被点击的时候才停止.
假如你前面设置的-[NSView hitTest:]
断点还在, 并且它是LLDB会话中唯一的断点, 你可以用下面的LLDB命令修改这个断点:
(lldb) breakpoint modify 1 -c "(BOOL)[$rdi isKindOfClass:[NSTextView
class]]"
这条命令修改了断点1并且设置了一个触发条件,只有条件语句返回的Boolean值为true的时候才触发断点.截至目前你只有一个断点,这就是为什么断点号为1.
这条Boolean表达式通过isKindOfClass:
来检查当前的类是否是NSTextView的子类.
你的断点经过上面的修改以后, 在Xcode的区域里点击. LLDB应该会停在hitTest:
.通过下面的命令打印出当前实例所属的类:
(lldb) po $rdi
输出的内容应该像下面的样子:
<NSTextViewSubclass: 0x14b7a65c0>
Frame = {{0.00, 0.00}, {1089.00, 1729.00}}, Bounds = {{0.00, 0.00},
{1089.00, 1729.00}}
Horizontally resizable: NO, Vertically resizable: YES
MinSize = {1089.00, 259.00}, MaxSize = {10000000.00, 10000000.00}
上面的NSTextViewSubclass
是一个私有类的名字.在本章节余下的内容里面你都会用到它.在输出里里面你会得到一个唯一的指针.在这个例子中, 指针的内存地址是0x14b7a65c0
, 但你得到的内存地址可能是不同的.
鉴于它没有立即显示出它是一个NSTextView
的子类, 但是你可以通过重复的手动输入下面的命令来查看它的父类来查看当前实例是否是NSTextViewsubclass
的子类.
(lldb) po [$rdi superclass]
... 一直重复到你找到为止.
(lldb) po [[$rdi superclass] superclass]
等一下-那是Objective-C. 但你要想到Swift的情况. 在swift中要那样做, 首先键入一下命令:
(lldb) ex -l swift -- import Foundation
(lldb) ex -l swift -- import AppKit
ex
命令是expression
的缩写, 可以让你执行代码.-l swift
告诉LLDB这是swift的代码.这些命令告诉LLDB需要知道Foundation 和 AppKit的相关情况.
你还将用到下面的两条命令.
输入下面的命令, 并将0x14bdd9b50
替换成你在前面获取到的NSTextView
子类的内存地址.
(lldb) ex -l swift -o -- unsafeBitCast(0x14bdd9b50, to: NSObject.self)
(lldb) ex -l swift -o -- unsafeBitCast(0x14bdd9b50, to: NSObject.self) is
NSTextView
这些命令打印出了, NSTextView的子类, 并且检查它是否是 NSTextView
的子类-但这一次使用的是swift!你将会看到类似下面的这些输出:
(lldb) ex -l swift -o -- unsafeBitCast(0x14bdd9b50, NSObject.self)
<NSTextViewSubclass: 0x14b7a65c0>
Frame = {{0.00, 0.00}, {1089.00, 1729.00}}, Bounds = {{0.00, 0.00},
{1089.00, 1729.00}}
Horizontally resizable: NO, Vertically resizable: YES
MinSize = {1089.00, 259.00}, MaxSize = {10000000.00, 10000000.00}
(lldb) ex -l swift -o -- unsafeBitCast(0x14bdd9b50, NSObject.self) is
NSTextView
true
使用swift需要输入更多的东西,.此外, 当调试器在在蓝色的地方停下来的时候, 或者在Objective-C 的代码中, LLDB将默认使用Objective-C.这些是可以改变的, 但是这本书更推荐使用 Objective-C,鉴于Swift REPL可能在调试器中无缘无故的检查出错误.
从现在开始, 你将会使用 Objective-C 的调试环境帮助控制NSTextView
.
鉴于这是一个NSTextView
的子类, 所以可以调用NSTextView
的所有方法.
输入下面的命令:
(lldb) po [$rdi string]
这将会打印出你再Xcode中打开的文件的内容.
甚至还可以设置text view的内容:
(lldb) po [$rdi setString:@"// Yay! Debugging!"]
(lldb) po [CATransaction flush]
可以看到Xcode窗口中的内容已经改变了.
获取modules中的私有类和私有方法
现在回到NSTextView
的子类, 选择从NSTextView
子类开始是因为这里可能有一些额外的或者重写了父类的方法.但是你如何去找到他们呢?Xcode中的私有类Apple是不会公开在文档中的.
输入下面的命令, 将NSTextViewSubclass
替换成你找到的text view的类:
(lldb) image lookup -rn 'NSTextViewSubclass\ '
这条命令可以让你查看正在运行的二进制文件和已经加载的所有动态库的内部.r
选项的含义是用正则表达式搜索. n
选项的含义是通过名字搜索函数或者符号表.
你将会看到NSTextView
子类实现了的方法列表.很酷是不是?如果你想要学习使用image lookup
命令查找代码, 你可以参考第七章"Image".
用代码块注入切换方法
Objective-C的运行时在逆向工程中真的很有用很有帮助.你现在即将感受到Objective-C运行时的强大, 并且了解你可以用它在LLDB中做什么.
首先, 用头文件导入的方法导入Foundation库中Objective-C的运行时信息:
(lldb) po @import Foundation
尽管代码已经编译过了,在Xcode内部知道包含哪些方法, 然而LLDB进程却不知道.通过导入Foundation可以让你通过LLDB访问Objective-C 运行时的所有内容.
现在在LLDB控制台中输入po
指令, 不必输入其他内容, 就像这样:
(lldb) po
LLDB将会进入多行模式. 你会看到下面这些输出:
(lldb) po
Enter expressions, then terminate with an empty line to evaluate:
1:
在这里, 你可以一次输入多行表达式来执行. 添加下面的代码:
@import Cocoa;
id $class = [NSObject class];
SEL $sel = @selector(init);
void *$method = (void *)class_getInstanceMethod($class, $sel);
IMP $oldImp = (IMP)method_getImplementation($method);
再次按下Return
键来输入一个空行.LLDB将会按照顺序执行上面的表达式.
注意:在键入这些代码的时候必须非常小心,只有在你按下`Return`以后你才会知道. 如果你出现了一个错误,所有这些代码你都必须重新输入, 尽管你可以通过朝上的箭头按钮使用历史输入, 确认一下你没有忘记句子末尾的分号.
你已经通过LLDB在内存中创建了几个变量: $class
, $sel
, 和 $oldImp
.LLDB中的变量需要$
作为前缀.其他部分就跟你在Xcode编辑代码的时候一样.
试着打印出一些变量来确保你正确的创建了它们:
(lldb) po $class
NSObject
(lldb) po $oldImp
(libobjc.A.dylib`-[NSObject init])
现在通过键入po
指令来回到LLDB的多行模式.
用imp_implementationWithBlock
函数来创建一个新的IMP:
id (^$block)(id) = ^id(id object) {
if ((BOOL)[object isKindOfClass:[NSView class]]) {
fprintf(stderr, "%s\n", (char *)[[[object class] description]
UTF8String]);
}
return object;
};
IMP $newImp = (IMP)imp_implementationWithBlock($block);
method_setImplementation($method, $newImp);
再次在结尾的地方输入一个空行来告诉LLDB来处理这些表达式.
这些代码的目标是替换你刚刚发现的-[NSObject init]
方法.在默认情况下, -[NSObject init]
除了返回对象本身一外不会做任何事情.这个代码块检查这个对象本事是不是NSView的一个实例.如果是, 这个对象的类就会被打印出来.
它的工作原理是这样的:
1.你创建了一个持有对象指针的代码块.
2.这个代码快检查传入的是不是NSView类型的对象.
3.如果是, 代码块会把view的description打印到stderr, 也就会出现在Xcode stderr
选项卡中.
4.代码快返回的是执行了被替换过的-[NSObject init]
方法的对象.在理想状况下,会彻底的替换这些方法的实现, 并且你可以用正确的参数简单的执行$oldImp
.然而, LLDB在这里却有一个bug, 当你用IMPs来代替block执行的时候LLDB会闪退.
5.最后, 一个新的IMP在代码块里被创建, 并且放的实现也被设置到了新的IMP里.这样你就用新的实现替换了-[NSObject init]
方法.
接下来, 通过键入continue
来继续调试.
观察Xcode stderr
控制台中的输出.检查你通过点击Xcode中不同的项目时创建的所有的类.
你也可以用这种方法将LLDB附加到任何程序上.不管是apple的程序还是你感兴趣的第三方应用程序.你可以用同样的技巧去浏览他们的类名.唯一不同的是你只要改变可执行文件的路径即可.
我们学这些干嘛?
这是我们第一次在你没有任何源代码的情况下使用LLDB并且附加到其他程序上.本章节忽略了很多细节, 但我们的目标是让你正确的调试进程.接下来还有许多章节让你深入的了解细节.