(十八) Dtrace vs objc_msgSend

1. Dtrace vs objc_msgSend

我们已经看到了DTrace对Objective-C和Swift代码的强大功能,无论是我们自己的源代码,还是在类似UIKit的框架中的代码。在对已编译的源代码执行零修改的同时,使用DTrace跟踪此代码并进行有趣的调整。

不幸的是,当针对stripped的可执行文件设置DTrace时,它无法创建任何探测来动态检查这些函数。

然而,在探索苹果代码时,我们仍然有一个非常强大的盟友:objc_msgSend。下面我们将使用DTrace钩住objc_msgSendentry,并取出该类的类名和Objective-C选择器。

我们将用LLDB将生成一个DTrace脚本,用来跟踪调用objc_msgSend的主可执行文件中的代码。

1.1 建立概念

我们将使用VCTransitions应用程序。它是一个非常基本的Objective-C/Swift应用程序,显示了一个普通的UINavigationController的push过渡动画和自定义过渡动画。

打开这个项目,在模拟器上构建并运行,然后快速浏览一下。需要注意的是,这个应用程序中有两种方案:VCTransitionsStripped 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探测是否会触发coolBooleanDTraceTestcoolViewDTraceTest?要回答这个问题,首先要看看这些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类的引用重新执行该类的getName函数。

~> 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])
objc_class::demangledName()汇编
可怕的汇编,第一部分

像往常一样,这些东西一开始看起来很吓人。实际上,我们将把程序汇编函数分成块来研究。第一个块将在偏移量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的值失败了(也就是说,如果rbx0),执行将会执行下一句汇编指令<+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这个对象,它的debugDescriptiondescription方法是这个类独有(重写)的!因此,在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应用程序。

objc_class::demangledName(bool)汇编
  • 偏移量 61:如果内存中的初始位置为nil,则控制继续来到61,其中rax+0x8被解引用并再次存储到rax中。

  • 偏移量 65:0x18被添加到rax并存储回raxrax可能是一个持有感兴趣值的结构,这可以解释偏移这个地址的原因。

  • 偏移量 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);
}
  1. this->selector执行copyinstr。因为我们知道第二个参数(arg1)是Objective-C选择器(C string) 。由于C字符以空字符结尾,DTrace可以自动确定要读取的数据量。
  2. 一会儿,我们就要复制一些数据。然而,copyin需要一个大小,因为与字符串不同,DTrace不知道任意数据何时结束。声明一个名为size的变量,它等于指针的长度。在x64中,这将是8字节。
  3. 这是对实例类的引用。请记住,Objective-C或Swift实例的起始地址处的解引用指针将指向该类。
  4. 现在来看看从objc_class::demangledName(bool)中的汇编中学到的有趣的部分。我们将复制寄存器中的逻辑,甚至对寄存器使用相同的名称!用rax来模拟这个函数执行的逻辑。
  5. 这是(rax+0x38)设置为this->rbx的逻辑,就像在实际汇编中一样。
  6. 如果this->rbx的值为0(也就是说类尚未加载),这是最后一行。
  7. 使用三元运算符来确定要使用哪个子句局部变量。如果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用户区进程中,内存的这一部分禁止读取、写入和执行。如果有任何东西低于0x100000000DTrace就不会关心它,还有其他读取内存的东西。因此,如果低于这个数字,就让DTrace跳过它。当然,可以使用LLDB通过以下命令确认这一点:

(lldb) image dump sections VCTransitions
移除干扰

老实说,我非常关心跟踪编译器生成的内存管理代码。这意味着任何有retainrelease的东西都需要离开这里。
使用当前探测上方的相同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" /

这将忽略任何等于retainrelease的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只分析主(精简过的)可执行文件中的代码。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,302评论 5 470
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,232评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,337评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,977评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,920评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,194评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,638评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,319评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,455评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,379评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,426评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,106评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,696评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,786评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,996评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,467评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,043评论 2 341

推荐阅读更多精彩内容