(十六) 你好,DTrace

作者: 收纳箱 | 来源:发表于2020-03-11 22:56 被阅读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`
    
    • *ViewController查询更改为UIViewController
    • -viewWillAppear?添加到了function位置。这个新的DTrace脚本将只匹配-[UIViewController viewWillAppear:],而不是匹配任何包含字符串“ViewController”的类的每个函数。?表示DTrace中的通配符,它将解析为viewWillAppear:方法中的“:”。
    • 在一个大括号中使用一个名为ustack()的函数。每次命中-[UIViewController viewWillAppear:]时都会调用此逻辑。ustack()DTrace的内置函数之一。当这个方法被命中时,它会打印调用栈。

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

    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方法开始,就执行逻辑来聚合调用该方法的次数。

    拆分
    • Probe Description:封装一组指定0个或多个探测的项。它由providermodulefunctionname组成,每个都用冒号分隔。在冒号之间省略这些项将导致探测描述包含所有匹配项。你可以用*?用于模式匹配的运算符。?运算符将充当单个字符的通配符,而*将匹配任何字符。
    • Provider:provider视为一组代码或公共功能。我们将主要使用objcprovider程序来跟踪Objective-C方法调用。objcprovider程序将聚合所有Objective-C代码。

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

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

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

    • Module:在objc提供程序中,Module部分是指定要观察的类名的位置。在这个意义上,使用objc提供程序有点独特。因为通常模块用于引用代码来自的库。事实上,在一些提供商中,根本没有模块!然而,objc provider的作者选择使用模块来引用Objective-C类名。对于这个例子来说,模块是NSObject

    • Function:探测描述中可以指定要观察的函数名。对于这个例子来说,函数是-descriptionobjc provider的作者使用+-来确定Objective-C函数是类还是实例方法。如果将函数更改为+description,它将查询任何带有+[NSObject description]的探测。

    • Name:这通常指定函数中探测的位置。entry表示进入函数,return表示离开函数。此外,在objc provider中,还可以指定要在其上创建探测的任何汇编指令偏移量!对于这个例子来说,名称是entry,或者函数的开始。

    • Predicate:一个可选表达式,用于评估操作是否是action的候选者。把谓词看作if语句中的条件。只有谓词的计算结果为true时,才会执行action部分。如果省略谓词部分,则每次对给定探测执行操作块。对于这个特定的例子,谓词是/ arg0 = 0 /,这意味着只有当arg0不是nil时,才会计算谓词后面的内容。

    • Action:如果探测与探测描述匹配,并且谓词的计算结果为true,则要执行的操作。可以执行将某些内容打印到控制台,或者更高级的功能。对于本例,操作是@[probemod] = count();代码。

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

    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?
    

    相关文章

      网友评论

        本文标题:(十六) 你好,DTrace

        本文链接:https://www.haomeiwen.com/subject/nvvhjhtx.html