1. Dtrace vs objc_msgSend
我们已经看到了DTrace
对Objective-C和Swift代码的强大功能,无论是我们自己的源代码,还是在类似UIKit的框架中的代码。在对已编译的源代码执行零修改的同时,使用DTrace跟踪此代码并进行有趣的调整。
不幸的是,当针对stripped
的可执行文件设置DTrace
时,它无法创建任何探测来动态检查这些函数。
然而,在探索苹果代码时,我们仍然有一个非常强大的盟友:objc_msgSend
。下面我们将使用DTrace
钩住objc_msgSend
的entry
,并取出该类的类名和Objective-C选择器。
我们将用LLDB将生成一个DTrace
脚本,用来跟踪调用objc_msgSend
的主可执行文件中的代码。
1.1 建立概念
我们将使用VCTransitions应用程序。它是一个非常基本的Objective-C/Swift应用程序,显示了一个普通的UINavigationController的push
过渡动画和自定义过渡动画。
打开这个项目,在模拟器上构建并运行,然后快速浏览一下。需要注意的是,这个应用程序中有两种方案:VCTransitions和Stripped VCTransitions。确保在运行时选择VCTransitions方案。我们稍后将详细讨论Stripped VCTransitions方案。
有两个按钮可以执行两次导航push
,还有一个名为Execute Methods
的按钮,可以循环遍历一个给定类的所有已知Objective-C方法。如果被遍历的方法不接受任何参数,则执行该方法。
例如,显示的第一个视图控制器是ObjCViewController
。如果点击Execute Methods
,它将调用anEmptyMethod
以及所有重写属性的getter
方法,因为所有这些方法都不需要参数。
跳到OjbCViewController.m
并查看这个类实现的IBAction方法。在终端中生成一行DTrace
,以确保可以看到这些方法被命中。确保模拟器处于活动状态并运行VCTransitions项目。
sudo dtrace -n 'objc$target:ObjCViewController::entry' -p `pgrep VCTransitions`
DTrace
会需要我们输入密码,然后回到模拟器中开始点击Execute Methods
按钮。我们将会看到DTrace
终端窗口充满了ObjCViewController
实现的IBAction
方法。
CPU ID FUNCTION:NAME
2 364044 -executeLotsOfMethodsButtonTapped::entry
2 364047 -anEmptyMethod:entry
2 364048 -coolViewDTraceTest:entry
2 364050 -coolBooleanDTraceTest:entry
现在,点击其中一个push
按钮,就可以来到SwiftViewController
。尽管这是UIViewController
的一个子类,但是点击IBActions
不会为objcPID探测产生任何结果。尽管SwiftViewController
实现或重写了动态方法,并通过objc_msgSend
执行,但实际的代码是Swift代码(甚至是那些@objc
桥接方法)。
如果SwiftViewController包含以下代码:
class SwiftViewController: UIViewController, UIViewControllerTransitioningDelegate {
@objc var coolViewDTraceTest: UIView? = nil
@objc var coolBooleanDTraceTest: Bool = false
// ...
Objective-CDTrace
探测是否会触发coolBooleanDTraceTest
或coolViewDTraceTest
?要回答这个问题,首先要看看这些Swift属性是否作为objective-C探针公开。他们应该是,对吧?它们具有@objc
属性。
sudo dtrace -ln 'objc$target::*cool*Test*:entry' -p `pgrep VCTransitions`
只显示Objective-CObjCViewController
的属性,而不显示SwiftViewController
!这是因为Swift提议160 https://github.com/apple/Swift-evolution/blob/master/proposals/0160-objc-inference.md,其中包括NSObject不再推断@objc
的提议。此外,Swift甚至不会为动态代码创建Objective-C符号。
这意味着我们必须使用非Objective-Cprovider
来查询SwiftDTrace
探测器。可以通过扩充DTrace
脚本来探测*cool*Test*
,如下所示:
sudo dtrace -n 'pid$target::*cool*Test*:entry' -p `pgrep VCTransitions`
这是使用objc_msgSend
而不是objc$target
探测的另一个原因。因为对objc_msgSend
的调用将捕获动态执行的Swift代码,而objc$target
将错过这些代码。
在stripped scheme中重复刚才的操作步骤
运行的目标(可执行文件)与VCTransitions应用程序完全相同,只是生成一个不包含任何调试信息的版本。选择Stripped VCTransitions,运行。运行之后,暂停应用程序并启动LLDB。
(lldb) lookup SwiftViewController
(lldb) lookup ObjCViewController
什么都没有,为什么呢?因为这个可执行文件的信息已被删除。我们不能使用通常可用的调试符号来引用内存中的地址。
然而,LLDB足够聪明,能够意识到内存中的这些位置实际上是函数。LLDB将为其没有信息的方法生成唯一的函数名。自动生成的函数名将采用以下形式:
___lldb_unnamed_symbol[FUNCTION_ID]$$[MODULE_NAME]
这意味着我们可以使用以下查找命令列出LLDB在VCTransitions可执行文件中生成的所有函数:
(lldb) lookup VCTransitions
****************************************************
255 hits in: VCTransitions
****************************************************
___lldb_unnamed_symbol1$$VCTransitions
___lldb_unnamed_symbol2$$VCTransitions
___lldb_unnamed_symbol3$$VCTransitions
...
LLDB无法获取这些函数的名称。那么DTrace
可以读取精简过的二进制文件中的内容吗?在终端中键入以下内容:
~> sudo dtrace -n 'objc$target:ObjCViewController::entry' -p `pgrep VCTransitions`
Password:
ID PROVIDER MODULE FUNCTION NAME
dtrace: failed to match objc96862:ObjCViewController::: No probe matches description
这将查询VCTransitions进程,以获取包含模块ObjCViewController
的探测计数。没有任何命中。
1.2 如何绕过精简过的没有探针的二进制文件
那么,如何构建一个DTrace
探测来绕过这个无法检查剥离的二进制文件的障碍呢?
由于我们知道Objective-C(和dynamic Swift)方法需要通过objc_msgSend
,所以可以使用objc_msgSend
知识来找出如何创建一个好的DTrace
操作,该操作将打印出类的名称和Objective-C选择器。objc_msgSend
函数签名如下:
objc_msgSend(instance_or_class, SEL, ...);
因此,objc_msgSend
将类或实例作为第一个参数,将Objective-C选择器作为第二个参数,然后是可变数量的参数。
UIViewController *vc = [UIViewController new];
[vc setTitle:@"yay, DTrace"];
编译器会将其转换为以下伪代码:
vc = objc_msgSend(UIViewControllerClassRef, "new");
objc_msgSend(vc, "setTitle:", @"yay, DTrace");
从DTrace
的角度来看,获得Objective-C选择器相当容易。只需要用到copyinstr(arg1)
。从arg1
复制Objective-C选择器指针(也称为char*
)到内核中,以便DTrace可以读取它。
对于困难的部分:我们需要将第一个参数的类名作为char*
传递给objc_msgSend
。
DTrace不允许执行任意方法,因此不能依赖Objective-C运行时,或它实现的任何方法,为我们挖掘信息。相反,我们可以在arg0
实例的内存中进行探索,并找到char*
表示类名,然后将其自动转换为DTrace脚本。
1.3 使用DTrace重新搜索方法调用
让我们看看有没有什么记录在案的方法来追查这件事。在objc/runtime.h
头文件中,有以下声明:
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class
OBJC2_UNAVAILABLE;
const char *name
OBJC2_UNAVAILABLE;
long version
OBJC2_UNAVAILABLE;
long info
OBJC2_UNAVAILABLE;
long instance_size
OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars
OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists
OBJC2_UNAVAILABLE;
struct objc_cache *cache
OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols
OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
回到在64位的机器上使用Objective-C 2.0的日子里,如果我们有一个指向一个有效类X
的指针,我们就可以获取到那个在#if !__OBJC2__
中描述的const char *name
。
po *(char *)(X + 0x10)
不幸的是,这是相当久远的事了。这个类结构可以追溯到Objective-C 2.0之前。结构和指针位置早就改变了。这意味着我们需要寻找一个接受Objective-C类(或该类的实例)并为该类返回char*
的函数,这样我们就可以知道它在做什么。
幸运的是,跳回objc/runtime.h
头文件时,还有一个名为@getName
的函数。class_getName具有以下签名:
/**
* Returns the name of a class.
*
* @param cls A class object.
*
* @return The name of the class, or the empty string if cls is Nil.
*/
OBJC_EXPORT const char *class_getName(Class cls) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
此函数接受一个类并返回一个char*
。我们将使用DTrace来跟踪此方法,并查看该类在封面下调用的方法。
获取对表示UIView
的类的引用:
(lldb) p/x [UIView class]
(lldb) p/x [UIView class]
(Class) $0 = 0x00007fff898df878 UIView
(lldb) po class_getName(0x00007fff898df878)
0x00007fff526e8954
//记得使用(char *)
(lldb) po (char *)class_getName(0x00007fff898df878)
"UIView"
现在,我们将使用DTrace
在幕后跟踪所有非Objective-C方法类的getName
调用。跳转到新的终端会话并执行以下代码:
sudo dtrace -n 'pid$target:::entry' -p `pgrep VCTransitions`
跳转回LLDB并使用对UIView
类的引用重新执行该类的getNam
e函数。
~> sudo dtrace -n 'pid$target:::entry' -p `pgrep VCTransitions`
Password:
dtrace: description 'pid$target:::entry' matched 939026 probes
CPU ID FUNCTION:NAME
0 380442 class_getName:entry
0 380435 objc_class::demangledName():entry
2 364870 _NSPrintForDebugger:entry
2 380732 objc_opt_respondsToSelector:entry
看起来,objc_class::demangledName()
函数是一个有趣的探索地方。终止DTrace脚本。我们不想让它破坏LLDB断点。因为在LLDB断点上设置DTrace
探测可能会产生意外的结果。一旦DTrace
脚本终止,就在
objc_class::demangledName()
设置断点,如下所示:
(lldb) b objc_class::demangledName(bool)
(lldb) exp -i0 -O -- class_getName([UIView class])
可怕的汇编,第一部分
像往常一样,这些东西一开始看起来很吓人。实际上,我们将把程序汇编函数分成块来研究。第一个块将在偏移量0-68之间。检查寄存器,这样就知道我们在处理什么:
(lldb) po $rdi
UIView
我们得到UIView输出,它是UIView
类的description
方法。但为什么这是第一个参数?函数签名似乎表明它应该是bool
值。因为,这是一个C++函数,C++在对象上调用函数的方式就像OC。有一个隐式的第一个参数,它是函数被调用的对象。但是在Swift中,作为第一个寄存器传入的实例并不总是这样。
转到第二个参数:
(lldb) po (bool)$rsi
true
- Offset 13: 在这一行之后,函数序言就执行完毕了。
-
Offset 17:这将
esi
赋值给r12d
。这就是传进来的Boolean值.我们之前查看的rsi
并且看到了它是0
, 因此r12d
将会同样是0。 -
Offset 20:
rdi
包含着UIView类的引用并且赋值给了r15
。 -
Offset 33:这与
r15
的偏移量是0x20
的值并且解引用,也就是说rax = (*([UIView class] + 0x20))
。 -
Offset 37:这个值存储在
rax
是用0x7ffffffffff8
相与存储到了rax
。 -
Offset 48:这个值是
rax
偏移0x38
后的值然后解引用并存储在rbx
。也就是说,rbx = *(rax + 0x38)
。 -
Offset 52-55:检查
rab
是否是0。如果它返回一个非零的数字,然后跳到<+310>
结束这个函数,在函数结束之前是正确的。
如果这个检查偏移量55的值失败了(也就是说,如果rbx
是0
),执行将会执行下一句汇编指令<+61>
。偏移量在0~55的逻辑是负责将一个Objective-C的类作为一个char*
返回给你。如果(并且仅仅只在如果)那个类已经被正确的加载之后。这通常发生在那个类里面至少有一个方法被执行了。
例如,如果一个新类被调用了,然而在进程存活期间还没有执行任何初始化,偏移量在0~55之间的逻辑将会返回nil
。稍后我们将会构建一个command regex
来确认这点。
看这些汇编,我们可以推断出下面的内容。如果有一个已经初始化的X
类的实例,将X
偏移了0x20
然后解引用它,输出的内容看起来应该是下面这个样子:
*(uint64_t *)(X + 0x20)
然后用0x7ffffffffff8
按位与
这个值:
*(uint64_t *)(X + 0x20) & 0x7ffffffffff8
接下来,使用这个值并偏移0x38
这个值,然后解引用:
*(uint64_t *)((*(uint64_t *)(X + 0x20) & 0x7ffffffffff8) + 0x38)
这是最终的地址,因此我们只需要将它输出到正确的类型里,char*
:
(char *)*(uint64_t *)((*(uint64_t *)(X + 0x20) & 0x7ffffffffff8) + 0x38)
如果有一个NSObject
的引用,那么这个对象起始位置的内存地址指向类自己(就是那个isa
指针)。将所有这些内容放在一起,将一个实例的类名作为一个char*
获取,看这个怪物:
(char *)*(uint64_t *)((*(uint64_t *)((*(uint64_t *)Instance_of_X) + 0x20) & 0x7ffffffffff8) + 0x38)
在LLDB中进行验证:
(lldb) x/gx '0x000000010c09ce60 + 0x20'
0x10c09ce80: 0x0000608000064b80
(lldb) p/x 0x7ffffffffff8 & 0x0000608000064b80
0x0000608000064b80
(lldb) x/gx '0x0000608000064b80 + 0x38'
0x608000064bb8: 0x000000010bce319f
(lldb) po (char *)0x000000010bce319f
"UIView"
注意:这不适用于还没有被初始化的Objective-C的类。为什么使用UIView的原因, 是因为如果我们可以在屏幕上看到UI, 那么UIView类已经明确的被初始化了, 或者说至少有一个UIView被初始化了。
创建一个新的regex command
来确认一下。
(lldb) command regex getcls 's/(.+)/expression -lobjc -O -- (char *)*(uint64_t*)((*(uint64_t *)((*(uint64_t *)%1) + 0x20) & 0x7ffffffffff8) + 0x38)/'
这条指令会从已经加载到进程里的任何实例上抓取char*
类名。将这条指令输入到LLDB控制台之后,用它验证一下之前UIView
:
(lldb) getcls [UIView new]
"UIView"
现在看一下还没有被初始化或者还没有执行任何方法的那些类,比如UIAlertController
:
(lldb) getcls [UIAlertController new]
nil
(lldb) po [UIAlertController class]
(lldb) getcls [UIAlertController new]
"UIAlertController"
记住如果这个类独有的方法被执行了,Objective-C运行时就会加载这个类。这个类方法(比如-[NSObject class]
)不是UIAlertController
独有的,为什么成功了呢?因为我们执行了po
这个对象,它的debugDescription
和description
方法是这个类独有(重写)的!因此,在po
一个UIAlertController
类的时候,它会被加载到运行时里。
1.4 可怕的汇编, 第二部分
下面讨论objc_class::demangledName(bool)
C++函的第二部分。如果char*
的初始位置不在感兴趣的初始位置(也就是说,如果类尚未加载),则此汇编将关注逻辑的功能。我们需要在汇编指令偏移量61上创建断点,该指令紧跟在偏移量55上的指令之后。
创建一个符号断点,该断点在objc_class::demangledName(bool)
的偏移量61处停止。
使用以下详细信息在Xcode中创建符号断点:
使用
dlopen
作为符号。在操作1中:使用
br dis 1
删除此断点。-
在操作2中:使用以下命令在
objc_class::demangledName(bool)
的偏移量61上设置断点:br set -M objc_class::demangledName(bool) -R 61
勾选Automatically continue after evaluating actions。
重新构建并运行VCTransitions应用程序。
偏移量 61:如果内存中的初始位置为nil,则控制继续来到61,其中
rax+0x8
被解引用并再次存储到rax
中。偏移量 65:值
0x18
被添加到rax
并存储回rax
。rax
可能是一个持有感兴趣值的结构,这可以解释偏移这个地址的原因。偏移量 69:
rax
处的值被解引用并存储到rbx
中,rbx
稍后(2指令之后)将被传递到rdi
。在这之后,会出现一条调用指令,根据反汇编注释,该指令期望char const*
作为第一个参数。
这对我们来说是这个函数的“有趣”部分。之后,该函数调用copySwiftV1DemangledName
函数,并设置将类加载到Objective-C运行时的逻辑。
1.5 重新转换到代码里搜索
我们已经做了必要的研究,找出如何遍历内存以获得类的字符数组表示。是时候实施这件事了。
起始脚本中包含一个名为msgsendsnoop.d的骨架DTrace脚本。我们将从这个DTrace
脚本开始并构建它的代码。完成工作和测试后,将把代码传输到LLDB Python脚本中,该脚本将动态生成所需的代码。
cat
一下脚本:
starter>cat ./msgsendsnoop.d
#!/usr/sbin/dtrace -s
#pragma D option quiet
dtrace:::BEGIN
{
printf("Starting... Hit Ctrl-C to end.\n");
}
pid$target::objc_msgSend:entry
{
this->selector = copyinstr(arg1);
printf("0x%016p, +|-[%s %s]\n", arg0, "__TODO__",
this->selector);
}
让我们把这个分解一下。传入相应的PID,此脚本将在objc_msgSend entry probe
上停止。一旦点击,选择器的char*
被复制到内核中并打印出来。例如,将要调用-[UIView initWithFrame:]
。将打印出以下内容:
0x00000000deadbeef, +|-[__TODO__ initWithFrame:]
通过跟踪VCTransitions中的所有objc_msgSend
调用来验证这一点是否正确:
sudo ./msgsendsnoop.d -p `pgrep VCTransitions`
是时候修正这个烦人的TODO
并用类的实际名称替换它了。打开msgsendsnoop.d
并替换现有的pid$target::objc_msgSend:entry
:
pid$target::objc_msgSend:entry
{
/* 1 */
this->selector = copyinstr(arg1);
/* 2 */
size = sizeof(uintptr_t);
/* 3 */
this->isa = *((uintptr_t *)copyin(arg0, size));
/* 4 */
this->rax = *((uintptr_t *)copyin((this->isa + 0x20), size));
this->rax = (this->rax & 0x7ffffffffff8);
/* 5 */
this->rbx = *((uintptr_t *)copyin((this->rax + 0x38), size));
this->rax = *((uintptr_t *)copyin((this->rax + 0x8), size));
/* 6 */
this->rax = *((uintptr_t *)copyin((this->rax + 0x18), size));
/* 7 */
this->classname = copyinstr(this->rbx != 0 ?
this->rbx : this->rax);
printf("0x%016p +|-[%s %s]\n", arg0, this->classname,
this->selector);
}
-
this->selector
执行copyinstr
。因为我们知道第二个参数(arg1
)是Objective-C选择器(C string) 。由于C字符以空字符结尾,DTrace
可以自动确定要读取的数据量。 - 一会儿,我们就要复制一些数据。然而,
copyin
需要一个大小,因为与字符串不同,DTrace
不知道任意数据何时结束。声明一个名为size
的变量,它等于指针的长度。在x64中,这将是8字节。 - 这是对实例类的引用。请记住,Objective-C或Swift实例的起始地址处的解引用指针将指向该类。
- 现在来看看从
objc_class::demangledName(bool)
中的汇编中学到的有趣的部分。我们将复制寄存器中的逻辑,甚至对寄存器使用相同的名称!用rax
来模拟这个函数执行的逻辑。 - 这是
(rax+0x38)
设置为this->rbx
的逻辑,就像在实际汇编中一样。 - 如果
this->rbx的值为0
(也就是说类尚未加载),这是最后一行。 - 使用三元运算符来确定要使用哪个子句局部变量。如果
this->rbx
是非空的,使用它;否则,使用this->rax
。
保存并跳转到终端并重新启动此DTrace
脚本:
sudo ./msgsendsnoop.d -p `pgrep VCTransitions`
扫描脚本中的内容时,偶尔当objc_msgSend调用nil对象时(即RDI,arg0是0x0),脚本似乎正在抛出错误。使用以下命令只能查看错误:
sudo ./msgsendsnoop.d -p `pgrep VCTransitions` | grep invalid
现在我们用一个简单的谓词来解决这个问题。紧跟在pid$target::objc_msgSend:entry
之后,添加以下谓词:
pid$target::objc_msgSend:entry / arg0 > 0x100000000 /
这意味着,“如果第一个参数为nil或内存部分未被利用,则不要运行此DTrace
操作。”通常,在macOS用户区进程中,内存的这一部分禁止读取、写入和执行。如果有任何东西低于0x100000000
,DTrace
就不会关心它,还有其他读取内存的东西。因此,如果低于这个数字,就让DTrace
跳过它。当然,可以使用LLDB通过以下命令确认这一点:
(lldb) image dump sections VCTransitions
移除干扰
老实说,我非常关心跟踪编译器生成的内存管理代码。这意味着任何有retain
或release
的东西都需要离开这里。
使用当前探测上方的相同DTrace探测创建新子句:
pid$target::objc_msgSend:entry
{
this->selector = copyinstr(arg1);
}
/* old code below */
pid$target::objc_msgSend:entry / arg0 > 0x100000000 /
现在,在主子句之前的一个新子句中声明选择器,其中包含所有的内存跳跃逻辑。这将允许我们在主子句的谓词部分中筛选Objective-C方法。现在把主句中的谓语扩充一下:
pid$target::objc_msgSend:entry / arg0 > 0x100000000 / &&
this->selector != "retain" &&
this->selector != "release" /
这将忽略任何等于retain
或release
的Objective-C选择器。在这里,不需要在主子句中重新分配this->selector
,现在在另一个子句中执行它。最终的代码是这样:
pid$target::objc_msgSend:entry
{
this->selector = copyinstr(arg1);
}
pid$target::objc_msgSend:entry / arg0 > 0x100000000 / &&
this->selector != "retain" &&
this->selector != "release" /
{
size = sizeof(uintptr_t);
this->isa = *((uintptr_t *)copyin(arg0, size));
this->rax = *((uintptr_t *)copyin((this->isa + 0x20), size));
this->rax = (this->rax & 0x7ffffffffff8);
this->rbx = *((uintptr_t *)copyin((this->rax + 0x38), size));
this->rax = *((uintptr_t *)copyin((this->rax + 0x8), size));
this->rax = *((uintptr_t *)copyin((this->rax + 0x18), size));
this->classname = copyinstr(this->rbx != 0 ?
this->rbx : this->rax);
printf("0x%016p +|-[%s %s]\n", arg0, this->classname,
this->selector);
}
重新启动这个脚本:
sudo ./msgsendsnoop.d -p `pgrep VCTransitions`
这次好多了,但是这里仍然有很多干扰。是时候将这个脚本与LLDB联合起来得到一些主执行文件的相应输出了。
1.6 用LLDB限定范围
starter文件夹中包含一个LLDB Python脚本,该脚本创建一个DTrace脚本,并使用刚刚实现的逻辑运行它。此文件名为snoopie.py。把这个文件复制到~/lldb
目录中。
我们将使用一个创造性的解决方案来过滤掉这个DTrace
脚本中的代码,从而只跟踪属于VCTransitions可执行文件的Objective-C/dynamic Swift代码。通常,在框架中窥探代码时,我会经常获取模块的__TEXT
段,并将指令指针与加载到内存中的__TEXT
段(内存中包含可执行代码的区域)的上下限进行比较。如果指令指针位于上下界之间,则可以假定要使用DTrace
跟踪代码。
不幸的是,我们正在寻找objc_msgSend
,它是所有模块中用于Objective-C代码的咽喉点。这意味着我们不能依赖指令指针来告诉我们所处的模块。相反,我们需要隔离一个类的地址,使其仅包含在主可执行文件的__DATA
段中。
运行、停止执行VCTransitions并启动LLDB。然后键入以下内容:
(lldb) p/x (void *)NSClassFromString(@"ObjCViewController")
(void *) $0 = 0x00000001045e1bc0
(lldb) image lookup -a 0x00000001045e1bc0
Address: VCTransitions[0x0000000100013bc0] (VCTransitions.__DATA.__objc_data + 40)
Summary: (void *)0x00000001045e1b98: ObjCViewController
因此,可以推断该类位于VCTransitions __DATA段中的__objc_data段中。我们将使用LLDB Python模块来查找这个数据段的上下限。使用script
命令来查找如何通过LLDB模块创建此代码。在LLDB中,键入以下内容:
(lldb) script path = lldb.target.executable.fullpath
(lldb) script print(lldb.target.module[path])
(x86_64) /Users/xxx/Library/Developer/Xcode/DerivedData/VCTransitions-gxzcdoxigcmztnduvannyvlhzqff/Build/Products/Debug-iphonesimulator/VCTransitions.app/VCTransitions
(lldb) script print(lldb.target.module[path].section[0])
[0x0000000000000000-0x0000000100000000) VCTransitions.__PAGEZERO
(lldb) script print(lldb.target.module[path].section['__PAGEZERO'])
[0x0000000000000000-0x0000000100000000) VCTransitions.__PAGEZERO
(lldb) script print(lldb.target.module[path].section['__DATA'])
[0x0000000100010000-0x0000000100015000) VCTransitions.__DATA
在SBModule
中,有一些SBSection
。可以使用sections
属性获取SBModule
中的所有节,也可以使用section[index]
获取特定节。
(lldb) script section = lldb.target.module[path].section['__DATA']
获取section
的加载地址和大小,如下所示:
(lldb) script section.GetLoadAddress(lldb.target)
4368228352
(lldb) script section.size
20480
现在,我们可以生成DTrace
谓词,检查类是否在内存中的这些值之间。如果是,则执行DTrace
操作。如果不是,就别理他们。让我们来实现这个!
1.7 修复snoopie脚本
这个snoopie.py脚本按之前的说的方式工作,因此我们只需向谓词添加一些小逻辑,以便仅过滤实例。打开~/lldb/snoopie.py
并找到到generateDTraceScript
函数。删除dataSectionFilter=...
行。然后添加以下代码:
target = debugger.GetSelectedTarget()
path = target.executable.fullpath
section = target.module[path].section['__DATA']
start_address = section.GetLoadAddress(target)
end_address = start_address + section.size
dataSectionFilter = '''{} <= *((uintptr_t *)copyin(arg0,
sizeof(uintptr_t))) &&
*((uintptr_t *)copyin(arg0, sizeof(uintptr_t))) <= {}
'''.format(start_address, end_address)
这里有趣的一点是,获取arg0
,当arg0大于0x100000000
时才解引用它,用来表示内存中存在有效实例。来到LLDB控制台,通过自定义的reload_script
命令或通过命令script import~/.lldbinit
手动重新加载LLDB中的内容。
(lldb) reload_script
(lldb) snoopie
Copied script to clipboard... paste in Terminal
将内容粘贴到终端窗口,现在DTrace
只分析主(精简过的)可执行文件中的代码。