(十六) 你好,DTrace

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?
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容