Apple高级调试与逆向工程

(十六) 你好,DTrace

2020-03-11  本文已影响0人  收纳箱

1. 你好,DTrace

DTrace可以使用prob钩住一个函数或一组函数。可以执行自定义操作来查询特定进程中的信息。如果曾经使用过Instruments应用程序,那么它下面的许多功能都是由DTrace提供的。

1.2 初识DTrace

打开模拟器和终端窗口:

sudo dtrace -n 'objc$target:*ViewController::entry' -p `pgrep SpringBoard`

加上sudo是因为DTrace是很强大,甚至可以查询电脑上其他用户的信息。这个DTrace命令有两个选项,name选项(-n)和PID选项(-p)。

如果输入的所有内容都正确,将在终端窗口中得到类似的输出
以下内容:

dtrace: description 'objc$target:*ViewController::entry' matched 42264 probes

每次探测命中将打印包含以“ViewController”结尾的Objective-C类。由于将function字段留空,只要类名以ViewController结尾,它就会输出每个匹配的Objective-C方法。

sudo dtrace -n 'objc$target:UIViewController:-viewWillAppear?:entry { ustack(); }' -p `pgrep SpringBoard`

输入正确的话会得到以下输出:

dtrace: description 'objc$target:UIViewController:-viewWillAppear?:entry ' matched 1 probe

-[UIViewController viewWillAppear:]命中时,堆栈跟踪将在终端中打印出来。

viewWillAppear

objc_msgSend执行时,函数签名将如下所示:

objc_msgSend(self_or_class, SEL, ...);

可以使用arg0参数在DTrace中获取第一个参数,也就是UIViewController的实例。不幸的是,我们只能获得指针的引用,不能运行任何Objective-C代码,如[arg0 title]

DTrace命令的ustack()函数之前添加printf("\nUIViewcontroller is: 0x%p\n", arg0);

sudo dtrace -n 'objc$target:UIViewController:-viewWillAppear?:entry
{ printf("\nUIViewcontroller is: 0x%p\n", arg0); ustack(); }' -p `pgrep SpringBoard`

dtrace: description 'objc$target:UIViewController:-viewWillAppear?:entry ' matched 1 probe
CPU     ID                    FUNCTION:NAME
  0 142401           -viewWillAppear::entry
UIViewcontroller is: 0x7ff224830000

              UIKitCore`-[UIViewController viewWillAppear:]
              SpringBoard`-[SBIconController viewWillAppear:]+0x2a
              UIKitCore`-[UIViewController _setViewAppearState:isAnimating:]+0x297
              UIKitCore`-[UIViewController __viewWillAppear:]+0x73
              BaseBoardUI`-[UIViewController(BaseBoardUI) bs_beginAppearanceTransition:animated:]+0x64
              BaseBoardUI`-[UIViewController(BaseBoardUI) bs_beginAppearanceTransitionForChildViewController:toVisible:animated:]+0xb6
              SpringBoard`-[SBHomeScreenViewController setIconControllerHidden:]+0x106
              SpringBoard`-[SBUIController restoreContentWithOptions:]+0x54e
              SpringBoard`-[SBUIController beginRequiringContentForReason:options:]+0x125
              SpringBoard`-[SBToAppsWorkspaceTransaction transaction:performTransitionWithCompletion:]+0x10d
              SpringBoard`-[SBSceneLayoutWorkspaceTransaction _beginLayoutTransition]+0x93
              SpringBoard`__55-[SBSceneLayoutWorkspaceTransaction _performTransition]_block_invoke_2+0x3c
              BaseBoard`-[BSBlockTransaction _begin]+0x85
              BaseBoard`__22-[BSTransaction begin]_block_invoke+0xa5
              BaseBoard`-[BSTransaction _preventTransactionCompletionForReason:ignoringAuditHistory:andExecuteBlock:]+0x55
              BaseBoard`-[BSTransaction begin]+0x3b3
              BaseBoard`-[BSTransaction _noteFinishedWork]+0x1fd
              BaseBoard`-[BSTransaction _checkAndReportIfCompleted]+0xc2
              BaseBoard`-[BSTransaction _removeMilestones:ignoringAuditHistory:]+0x498
              BaseBoard`__49-[BSTransaction evaluateMilestone:withEvaluator:]_block_invoke+0x9c

现在在打印出堆栈跟踪之前,打印对调用viewWillAppearUIViewController的引用。如果复制DTrace的这个指针的地址并将LLDB附着到SpringBoard,我们会发现它指向一个有效的UIViewController(如果还没有被释放的话)。

sudo dtrace -n 'objc$target:::entry { @[probemod] = count() }' -p `pgrep SpringBoard`

暂时还不会得到任何输出,但是一旦使用Ctrl + C终止这个脚本。我们将得到一个聚合列表,列出了执行特定类的方法的所有次数。从我的输出中可以看到,SpringBoard12878个由NSObject实现的方法调用。

NSObject调用命中

区分父类子类的调用很重要。例如,调用-[UIViewController class]将被视为对NSObject执行的方法总数的命中。因为UIViewController没有重写Objective-C方法classUIViewController的父类UIResponder也没有重写。

1.2 DTrace专业用语

我们可以将探测视为查询。这些探测是DTrace可以在特定进程中监视的事件,也可以跨计算机全局监视。

dtrace -n 'objc$target:NSObject:-description:entry / arg0 != 0 / { @[probemod] = count(): }' -p `pgrep SpringBoard`

这个例子将监视NSObject在名为SpringBoard的进程中对description方法的实现。此外,一旦description方法开始,就执行逻辑来聚合调用该方法的次数。

拆分

注意:$target关键字是一个特殊关键字,它将匹配我们给DTrace提供的任何PID。某些provider(比如objc)希望提供这个关键字。

$target看作实际PID的占位符,它监视特定进程中的Objective-C。如果确实引用了$target占位符,则必须在DTrace命令中通过-p-c选项标志指定目标PID。

通常,如果我们知道确切的PID,这可以通过-pPID完成,或者可能通过-p "pgrep NameOFProcess"完成。pgrep命令将查找进程名为NameOFProcess的PID,然后返回该PID,然后将其应用于$target变量。

简单来说,它的结构是这样:

provider:module:function:name / predicate / { action }

DTrace可以包含多个子句。这些子句可以使用探测描述监视不同的项,检查谓词中的不同条件,并使用不同的操作执行不同的逻辑。

dtrace -n 'objc$target:NSView:-init*:entry' -p `pgrep -x Xcode`

有一个objc$target:NSView:-init*:entry的探测描述,其中包括NSView作为模块,-init*作为函数,entry作为名称,没有谓词和操作。DTrace生成一个用于跟踪的默认输出(可以使用-q选项使其保持沉默)。这个默认输出仅显示函数和名称。例如,如果在跟踪-[NSObject init]时没有使默认DTrace操作静音,则DTrace输出将如下所示:

dtrace: description ’objc$target:NSObject:-init:entry’ matched 1 probe
  CPU          ID          FUNCTION:NAME
  2           512130       -init:entry
  2           512130       -init:entry
  2           512130       -init:entry
  2           512130       -init:entry

从输出来看,跟踪进程时-[NSObject init]被命中4次。我们可以告诉DTrace使用不同格式的输出,方法是将-q选项与一个打印函数组合起来,以显示输出的其他格式。
-n参数指定可以采用provider:module:function:namemodule:function:namefunction:name格式的DTrace名称。此外,name选项可以接受可选的探测子句。这就是为什么要将所有一行脚本内容用单引号括起来传递给-n参数的原因。

1.3 列出探测器

-l将列出在探测描述中匹配的所有探测。当我们使用-l选项时,DTrace将只列出探测,而不执行任何操作。这使得-l选项成为一个很好的工具,可以用来学习哪些要工作,哪些不工作。

在构建DTrace脚本时,我们将再次查看探测描述并系统地限制其范围。请考虑以下情况,但不要执行此操作:

sudo dtrace -ln ’objc$target:::’ -p `pgrep -x Finder`

这将在Finder应用程序中的每个Objective-C类、方法和汇编指令上创建探测描述。对于DTrace脚本来说,这是一个非常糟糕的主意。最好不要运行,因为将获得大量的命中。

注意:我们向pgrep提供了-x选项。因为我们可能获得多个pid,这将破坏占位符$target-x选项表示返回与Finder名称完全匹配的PID。如果一个进程有多个实例,可以使用-o-n选项在pgrep中获得最老的或最新的实例。

sudo dtrace -ln 'objc$target:NSView::' -p `pgrep -x Finder`

这将列出NSView实现的每个方法以及每个方法中的每个汇编指令的探测。仍然是一个可怕的想法,但至少这个会在一秒钟后打印出来。这有多少个探测器?我们可以通过将输出发送到wc命令来获得答案:

 ~> sudo dtrace -ln 'objc$target:NSView::' -p `pgrep -x Finder` | wc -l
   41307

我们再过滤一下:

sudo dtrace -ln 'objc$target:NSView:-initWithFrame?:' -p `pgrep -x Finder`

这将把探测描述筛选到-[NSView initWithFrame:]中执行的每个汇编指令。注意到?的用法了吗?而不是冒号来指定Objective-C选择器。如果使用冒号,则DTrace将错误地分析输入,认为函数部分已完成,并已转到DTrace探测中指定名称。函数描述的开头还有-表示这是一个实例Objective-C方法。

仍然输出太多,我们只想监视-[NSView initWithFrame:]方法的开头。

 ~>  sudo dtrace -ln 'objc$target:NSView:-initWithFrame?:entry' -p `pgrep -x Finder`
Password:
   ID   PROVIDER            MODULE                          FUNCTION NAME
1156826    objc358            NSView                   -initWithFrame: entry

1.4 一个创建DTrace脚本的脚本

在使用DTrace时,不仅要处理异常陡峭的学习曲线,如果遇到构建时或运行时DTrace错误,还要处理一些神秘错误。

为了帮助您在学习DTrace时减轻这些构建问题,这里创建了一个的小脚本tobjectivec.py(trace Objective-C)。这是一个LLDB Python脚本,会为我们生成一个自定义DTrace脚本。

通过tobjectivec.py探索DTrace

运行Allocator项目,然后在调试器中暂停。

(lldb) tobjectivec -g

通常,tobjectivec脚本将在计算机的/tmp/目录中生成一个脚本。但是,这个-g选项表示我们正在调试脚本并将输出显示到LLDB,而不是在/tmp/中创建文件。使用-g(--debug)选项时,当前脚本将显示在控制台上。这个没有额外参数的tobjectivec.py运行将产生以下输出:

#!/usr/sbin/dtrace -s /* 1 */
#pragma D option quiet /* 2 */
dtrace:::BEGIN { printf("Starting... use Ctrl + c to stop\n"); } /* 3 */
dtrace:::END { printf("Ending...\n" ); }
/* Script content below */
objc$target:::entry /* 5 */
{
    printf("0x%016p %c[%s %s]\n", arg0, probefunc[0], probemod, (string)&probefunc[1]); /* 6 */
}
  1. 执行DTrace脚本时,第一行必须是#!/usr/sbin/dtrace-s,否则脚本可能无法正常运行。
  2. 表示在探测触发时不列出探测计数,也不执行默认的DTrace操作。相反,我们将给DTrace设置自定义操作。
  3. 这是此脚本中DTrace子句的三分之一。让DTrace用于监视某些DTrace事件。比如当DTrace脚本即将启动时。一旦DTrace开始,就打印出“Starting... use Ctrl + c to stop“字符串。
  4. DTrace脚本完成时,打印“Ending...”。
  5. 我们感兴趣的DTrace探测描述。意思是在提供给脚本的进程ID中跟踪找到的所有Objective-C代码。
  6. 这个子句的action部分,输出触发的Objective-C探测的实例,然后输出Objective-C样式的输出。在这里,可以看到使用probefuncprobemod,这将是函数和模块的char*表示。DTrace有几个可以使用的内置变量,probefuncprobemodprobeprovprobename。记住,模块将表示类名,而函数将表示Objective-C方法。这里用到了probemodprobefunc,并以我们习惯的C语法显示它。

重新执行:

(lldb) tobjectivec
Copied script to clipboard... paste in Terminal

在终端窗口粘贴执行:

 ~> sudo /tmp/lldb_dtrace_profile_objc.d -p 73610
Password:
Starting...use Ctrl + c to stop

在Xcode的LLDB中输入po [NSObject class]

测试

如果我们随便玩一玩这个app会发现有大量的输出。东西太多了,下面我们通过向模块说明符添加内容来过滤一些噪声。在LLDB中键入以下内容:

tobjectivec -m *StatusBar* -g

我们看一下这次的探测描述:

objc$target:*StatusBar*::entry 
{
    printf("0x%016p %c[%s %s]\n", arg0, probefunc[0], probemod, (string)&probefunc[1]);
}

注意探针的模块部分是如何改变的。在正则表达式中,*可以被认为是任何我们感兴趣的。当探测进入函数的开头时,查询包含任何Objective-C类的区分大小写单词StatusBar的探测。在LLDB中,删除-g选项以便将此脚本复制到剪贴板,然后重新执行该命令。

(lldb) tobjectivec -m *StatusBar*

终端中粘贴:

 ~> sudo /tmp/lldb_dtrace_profile_objc.d -p 73610
Password:
Starting...use Ctrl + c to stop

跳转到模拟器并使用⌘ + Y切换通话状态栏,或使用⌘ + ←⌘ + →旋转模拟器,同时注意DTrace终端窗口。再次得到大量的输出。我们可以使用DTrace在代码上撒下一个大网,并在需要时快速向下深挖。

跟踪调试命令

我们来观察一下要调用多少Objective-C方法才能生成一个简单的Objective-C NSString。在LLDB中,输入以下内容:

(lldb) tobjectivec

然后在LLDB中输入:

(lldb) po @"hi this is a long string to avoid tagged pointers"
测试
我们刚刚打印了一个简单的NSString,看看这需要多少Objective-C调用!

返回到LLDB并键入以下内容:

expression -l swift -O -- class b { }; let a = b()

我们使用Swift调试上下文创建一个纯Swift类,然后将其实例化。创建这个类时,请观察Objective-C方法调用。

0x0000000105f81c58 +[_TtCs12_SwiftObject class]
0x00000001088db7f8 +[_TtCs12_SwiftObject initialize]
0x00000001088db7f8 -[_TtCs12_SwiftObject self]

如果把DTrace抛出的地址复制出来,然后po一下。你会看到这个纯Swift类调用了多少Objective-C方法。一个“纯粹的”Swift并不像我们想象的那样纯粹,对吧?

跟踪一个对象

我们可以使用DTrace轻松跟踪特定引用的方法调用。暂停应用程序,使用LLDB获取对UIApplication的引用。

(lldb) po UIApp
<UIApplication: 0x7ffc65601750>

复制引用并使用它来构建一个谓词,该谓词仅在该引用为arg0时停止。

(lldb) tobjectivec -g -p 'arg0 == 0x7ffc65601750'

#!/usr/sbin/dtrace -es
#pragma D option quiet
dtrace:::BEGIN { printf("Starting...use Ctrl + c to stop\n"); }
dtrace:::END   { printf("Ending...\n"  ); }
/* Script content below */
objc$target:::entry / arg0 == 0x7ffc65601750 /
{
    printf("0x%016p %c[%s %s]\n", arg0, probefunc[0], probemod, (string)&probefunc[1]);
}

然后去掉-g选项:

(lldb) tobjectivec -p 'arg0 == 0x7ffc65601750'

触发模拟器中的home按钮(⌘+Shift + H)或状态栏(⌘ + Y)。
这将打印[UIApplication sharedApplication]实例上的每个Objective-C方法调用。

是不是输出太多内容了?我们来将内容聚合:

(lldb) tobjectivec -g -p 'arg0 == 0x7ffc65601750' -a '@[probefunc] = count()'

#!/usr/sbin/dtrace -es
#pragma D option quiet
dtrace:::BEGIN { printf("Starting...use Ctrl + c to stop\n"); }
dtrace:::END   { printf("Ending...\n"  ); }
/* Script content below */
objc$target:::entry / arg0 == 0x7ffc65601750 /
{
    @[probefunc] = count()
}

在不使用-g选项的情况下重新运行上面的tobjectivec命令,然后将剪贴板内容粘贴到终端并在LLDB中继续执行。这时终端中尚未显示任何内容。但DTrace正在悄悄地聚合发送到UIApplication实例的每个方法。

在模拟器中随意玩一玩,获取发送到UIApplication的方法的正常计数。一旦使用通常的Ctrl + C终止脚本,DTrace将打印应用于UIApplication实例的所有Objective-C方法的总数。

其他DTrace小技巧

追踪所有对象的所有初始化方法:

(lldb) tobjectivec -f ?init*

检测进程内通信相关的逻辑(比如,Webviews、keyboards等等):

(lldb) tobjectivec -m NSXPC*

打印出在iOS设备上处理开始触摸事件的UIControl的子类 :

(lldb) tobjectivec -m UIControl -f -touchesBegan?withEvent?
上一篇 下一篇

猜你喜欢

热点阅读