DTrace(第二十五章:Hello, Dtrace)

什么?你从来没有听说DTrace?!这真是太可怕了!DTrace是一个可以让你用动态或静态的方式查看代码的工具.
http://dtrace.org/guide/preface.html
你可以创建一个DTrace probes编译到你的代码中(静态的方式), 或者你可以检查已经编译并运行起来的代码(动态的方式).DTrace是一个多用途的工具, 它有多种功能:它可以成为剖析器, 一个分析器, 一个调试器或者你想要的任何东西. 当我没有从哪里开始的头绪的时候, 我经常在我想要浏览的代码中用DTrace指定一个宽泛的搜索网.

天呐!到了学习Dtrace的时间了! Dtrace(可能)是你从来没有听说过的最酷的工具.用DTrace, 你可以使用一个叫做probe的东西拦截一个函数或者一组函数. 从这里开始, 你可以执行自定义的操作来查询指定进程的信息, 或者你电脑上系统级别的信息(并检测多个用户)!
如果你用过Instruments应用程序, 它那些让你感到惊讶的强大的底层功能就是通过强大的DTrace实现的.
在本章中, 你会看到非常小的一个篇幅介绍DTrace在已经编译过的应用程序中追踪Objective-C代码是如何强大.使用DTrace来观察iOS框架(比如UIKit)可以让你难以置信的洞察到在框架内部作者是如何设计他们的代码的.

坏消息

让我们先说一下坏消息, 因为从现在开始后面都是让人兴奋而又炫酷的功能. 关于DTrace这里有几件需要注意的事情:
• 为了正常使用DTrace你需要禁用Rootless模式. 你还记得很久以前在第一章中我提醒你为了实现某些功能需要禁用Rootless? 为了让LLDB附加到你macOS电脑的任何进程上, 如果启用了System Integrity Protection DTrace将不能正常使用. 如果你跳过了第一章, 那么现在回去禁用Rootless.
• DTrace 不是为iOS设备而做的. 尽管Instruments应用程序底层用DTrace实现了很多功能. 但是它不可以在你的iOS设备商运行自定义的脚本. 这就意味着你只能在你的iOS设备商运行预设的有限的功能. 然儿, 你依然可以在iOS模拟器(或者你macOS电脑的其他应用程序)上运行你想运行的脚本无论你是否是拥有这些程序的代码.
• DTrace 有着陡峭的学习曲线. DTrace需要你知道你再做什么和你在找什么. 它的文档会假设你已经知道了DTrace组件最基本的术语. 在本章中你会学到最基本的概念, 如果要介绍DTrace的方方面面那么已经可以单独的写一本书了, 这超出了我要教你的范围.
事实上, 这些需要你提前知道, 如果你对DTrace感兴趣, 那么可以看看这本书http://www.brendangregg.com/dtracebook/index.html. 它关注一个宽泛的话题但有可能不适合apple 调试/逆向工程策略, 但是它会教你如何使用DTrace.
现在我已经将坏消息告诉你了, 是时候来聊一些开心的话题了.

正确的跳转

我不会用讨厌的专业术语将你赶跑. 没有人会将时间花费在那上面. 取而代之的是, 你首先会把你的手弄脏, 然后弄清楚稍后你会做什么.
启动iOS 7 Plus 模拟器. 在模拟器启动之后, 创建一个新的终端窗口. 在终端中输入下面的内容:

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

不, 这不会秘密的毁坏你的电脑, 在这里你需要使用sudo 因为DTrace极其强大并且可以查询你电脑里其他用户的信息. 这就意味着你需要root权限去使用它.
这条DTrace命令有两个选项, name 选项 (-n) 和 PID选项 (-p), 这两个选项我们稍后讨论. 确保你用单引号报过了查询语句够则是不起作用的. 注意pgrep SpringBoard前后用的是抑音符而不是单引号.
如果你的输入是正确的, 在终端中你会看到类似下面的输出:

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

在模拟器中浏览同时注意观察终端窗口.
这会输出出每一个以"ViewController"做结尾的Objective-C类名. 因为我们将函数选项留空 (不要担心 - 我们会在下一部分讨论术语), 它会匹配每一个以ViewController结尾的Objective-C类中单独的Objective-C方法.
如果你厌烦了查看突然出现的内容,按下Ctrl + C 键杀掉正在运行DTrace脚本的终端窗口.
回到你的终端中, 输入下面的内容:
这一次这里有一些细微的改变:
• 查询的*ViewController 已经被改成了UIViewController.
• 查询语句中的-viewWillAppear呢? 已经被添加到了函数的位置. 再说一次, 我们会在后面讨论专业术语. 但是现在, 你所需要知道就的是 替换了匹配包含"ViewController"字符串的任何类的每一个方法, 新的DTrace脚本只会匹配-[UIViewController viewWillAppear:]. ?在 DTrace标识通配符, 用来拆分viewWillAppear:中的:.
• 最后, 你为一个叫做ustack()的函数加上了一堆大括号. -[UIViewController viewWillAppear:]每次触发的都会调用这个逻辑. ustack() 是DTrace内部的函数在一个函数触发的时候可以提取出用户的堆栈记录 (在这个例子中就是SpringBoard).
• 注意观察从entry末尾移动到移动到大括号末尾的那个单引号
如果你输入的都是正确的, 你将会得到下面的输出:

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

浏览SpringBoard.向上滑, 向下滑, 滚动到最左边点击edit按钮, 因为你需要触发UIViewControllerviewWillAppear:.
当UIViewController的viewWillAppear:触发的时候, 堆栈记录将会在终端中打印出来.
注意那些没有实际函数名而只有模块和地址的的堆栈记录.

DTrace(第二十五章:Hello, Dtrace)_第1张图片
图片.png

这是在告诉我们没有调试信息或者 没有直接引用这些函数名字的符号表.
如果你厌烦了浏览SpringBoard 进程中所有的viewWillAppear:的堆栈记录, 再次杀掉DTrace脚本.
现在... 你还记得objc_msgSend使用寄存器的完整理论以及 第一个参数将会是Objective-C某个类的一个实例(或者一个类)吗?
例如, 当objc_msgSend 执行的时候, 函数的声明看起来将会是这个样子:

 objc_msgSend(self_or_class, SEL, ...);

你可以在DTrace中用抓取arg0参数第一个参数(也就是UIViewController的一个实例). 不幸的是, 你只能得到指针的引用 - 你不能运行任何Objective-C的代码, 比如[arg0 title].
将下面这行代码添加到你的DTrace脚本中ustack()函数的前面:

printf("\nUIViewcontroller is: 0x%p\n", arg0); 

你 DTrace 单行指令看起来将会是下面这个样子:

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

在正确的打印出堆栈记录之前, 你已经打印除了调用viewWillAppear:的UIViewController的引用.
如果你复制了DTrace输出的指针的地址 并且将LLDB附加到了SpringBoard上, 你将会发现它志向了一个有效的UIViewController (假如它还没有被销毁的话).

注意: 从arg0处获取指针是很简单的,但是获取其他信息 (比如类名) 是一个狡猾的过程. 你不能再属于用户进程(比如SpringBoard)的DTrace脚本中执行任何Objective-C/ Swift代码. 你所能做的只有用你已有的引用在内存中Z型前进. 在最后的章节中, 你将能够从精简过的二进制文件中实际获取Objective-C的调用的arg0参数中获取类名, 全无调试信息!

让我们再做一个DTrace的例子.
杀掉所有的DTrace 脚本然后创建一个汇聚所有独特类的可以在你浏览SpringBoard时执行的脚本:

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

再次浏览SpringBoard. 你还得不到任何输出, 但是当你用ctrl+c终端脚本之后, 你会得到一个在这个时间内执行了一个方法的类的列表. 这叫做Aggregations是你稍后要学习的内容.

DTrace(第二十五章:Hello, Dtrace)_第2张图片
图片.png

正如你再我的输出中看到的, 在我执行DTrace单行命令期间SpringBoard 调用了 187075个NSObject实现的方法.
区分它们非常像NSObject的子类的某个实例调用了NSObject实现的方法这个事实是很重要的 (比如这个NSObject的子类没有重写这些方法中的任何一个).例如, 调用-[UIViewController class]将会作为NSObject通过向前调用记入被执行方法的总数中 因为 UIViewController 不会重写Objective-C 方法, 类, 或者 UIViewController的父类, UIResponder.

DTrace 专业术语

现在你已经让一些DTrace单行命令弄脏了手, 是时候学些专业术语了以便你实实在在的弄清楚这些脚本做了哪些事情.
让我们再看一个DTrace probe. 你可以将一个probe. These 看做一个query. probe即可以是DTrace检测到的特定进程或者你电脑的全局进程的事件.
思考一下下面这行DTrace单行命令:

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

我将问这个脚本中有用的问题, 这个例子将会检测名字叫做SpringBoard的进程中NSObject实现的description方法.此外, 这就是说在description开始的时候,执行这个方法调用次数的逻辑.
这个DTrace单行命令可以进一步被拆解为下面的术语:
• Probe Description: 包裹着0或者更多探针的一组项目.这组成了每一个被冒号分开的provider, module, function, 和name. 优化任何冒号之间的项目将会导致探针描述包含所有的匹配. 你可以运算符为这些匹配使用* 或者?运算符 . ?将会作为单个字符的通配符, *将会匹配任何内容.
• Provider: 将provider想象成一组代码或者普通的功能.在本章总, 你主要会使用objc provider来追踪Objective-C方法的调用. objc provider 将所有的Objective-C代码分组. 你稍后就会浏览这些providers.

 Note: 关键词` $target ` 是一个匹配提供给DTrace的PID的特殊关键字. 某些` providers` (比如 objc) 期望你能提供这些内容. 将`$target` 想象成一个在某个具体的进程中检车Objective- C的实际PID的占位符. 如果你引用了`$target`占位符, 你必须在你的DTrace命令中通过-p 或者 -c选项指明目标进程的PID. 通常情况下这即可以在你知道的具体PID时通过`-p PID`实现, 也可以更多的使用``` -p `pgrep NameOFProcess` ```实现. 终端命令`pgrep`将会查看名字叫做`NameOFProcess`的进程的PID然后返回这个PID, 然后将它应用到`$target`变量中.

• Module: 在objc provider中, module 部分是你指明你想观察的类名的地方. 使用objc provider在这种场景下有点独一无二, 因为 通常情况下 module 通常会引用一个代码库. 事实上, 在某些providers中, 没有一点module的内容! 然而, objc provider的作者选择使用module来引用Objective- C 的类名. 在这个例子中, module就是NSObject.
• Function: 是可以指明你希望观察的函数名字的probe描述部分. 在这个例子中, 函数是- description. objc provider的作者使用 +或者- 来决定 Objective-C 函数是类方法还是实例方法 (正如你期望的那样!). 如果你啊讲函数改为+description, 它将会查询任何使用+ [NSObject description]probes替代.

• Name: 这通常指明了一个函数的探针的位置. 通常情况下, 这里有函数入口 和 出口对应的的入口名和返回点的名字. 此外, 在objc provider内部, 你也可以指定任何汇编指令偏移创建探针! 在这个例子中, 名字是函数入口, 或者是函数的起始位置.
• Predicate: 如果要执行的操作是可选的那就执行一个条件表达式. 将predicate想象成if语句中的条件. 操作只会在predicatetrue的情况下才会执行. 如果你省略了条件句, 则操作每次都会在探针上执行. 在这个例子中, 条件句就是/ arg0 != 0 /, 这就意味着条件句后面的内容只有在arg0不为 nil的情况下才会执行.
• Action: 如果探针与探针的描述相符并且条件句为真则操作就会被执行. 动作要紧可能像打印一些东西一样简单, 或者执行更高级的函数. 在这个例子中, 操作是@[probemod] = count(); 这些代码.
当所有这几部分组合起来之后, DTrace就是DTraceclause(从句)的形式.这些可选的条件句和可选的侗族组成了probe description.
简单的说, 一个DTrace从句应该像下面这个样子:

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

DTrace "one-liners" 可以由多个可以检测不同项目的探针描述的从句组成, 在条件句中判断不同的条件然后执行不同的逻辑操作.
下面是一个例子:

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

我们有一个包含NSView模块的探针描述objc$target:NSView:-init*:entry, -init*a是函数部分 而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] 调用了四次. 你可以通过使用-q 选项告诉DTrace的print函数输出不同的格式.
-n参数的含义是什么呢? -n参数指明了provider:module:function:name, module:function:name 或者 function:name形式的DTrace名字. 此外, name选项可以带一个可选的probe从句, w但是要用单引号包裹着这个单行命令脚本传递给-n参数.
明白了吗? 没有? 我们将会用一个有用的DTrace选项来巩固一下你刚才学习的的知识和术语.

学习监听探针

在DTrace中有一个非常好用的选项-l, -l可以列出probe description匹配到的所有探针. 当你在使用-l选项的时候, DTrace仅仅会列出探针而不会执行任何操作, 无论你是否使用了操作DTrace都不会执行.
这让-l成为了学习 哪些是行得通哪些是行不通的好工具.
让我们再来看一下在编译DTrace脚本时的probe description 并指定它的范围. 思考下面的指令, 但是不要执行:

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

这会在Finder中的每一个Objective-C类的每一个方法和每一条汇编指令处创建一个probe description. 这对于DTrace脚本是一个非常坏的注意并且可能无法在你的电脑上运行因为触发的次数实在太多.

  注意: 我已经将` -x `选项应用到`pgrep`因为我在使用`pgrep`查询的时候可能得到多个`PIDs`, 这回搞砸$target参数. `-x` 选项的意思是只给我与名字`Finder`完全匹配的. 如果有一个进程的多个实例. 你可以在pgrep中使用`-o` 或者` -n`选项获取最开始的那个进程或者最后的那个进程.如果这些听起来让你觉得很混乱, 在终端中在没有 DTrace的情况下练习使用`pgrep`命令来理解它是如何工作的.

不要执行上面的脚本因为它会花费很长时间. 然而, 执行下面脚本以便你理解发生了什么.
让我们将这个命令稍微精简一下. 在终端中, 输入下面命令:

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

按下回车键, 然后输入你的密码.
这将会列出NSView实现的所有方法中的每一个单独的方法的probe以及每一个方法的汇编指令. 仍然是一个可怕的方法, 至少这一次过几秒之后真的会打印出内容来.
有多少probes? 你可以将输出嫁接到wc命令获取答案:

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

在我的macOS 机器上是 10.12.4系统 (在我写这本书的时候), 在finder进程中我得到了46k 个与NSView相关的Objective-C DTrace probes. 哇.
进一步过滤这些probe description:

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

这会过滤出执行-[NSView initWithFrame:]方法时除了入口和返回点以外的每一个汇编指令的probe description.
注意我是如何使用?代替冒号指明Objective-C selector(带有参数)的?. 这是因为如果我使用了冒号,DTrace将会错误的认为我已经完成了函数部分并且已经开始指明DTrace probe名字了. 函数描述开头的- 指明了这是一个实例方法.
这里已然有太多的输出, 我只想设置一个能够检测到-[NSView initWithFrame:] 方法起始位置的探针而不需要其他探针.

sudo dtrace -ln 'objc$target:NSView:-initWithFrame?:entry' -p `pgrep -xFinder`

这条指令的意思是说只在-[NSView initWithFrame:]的起始位置设置一个探针 并且不在这个方法的其他位置设置探针.
使用-l 选项是学习在你的DTrace操作中指定探针范围的好方法. 我推荐你在学习DTrace的时候多使用(重用) -l 选项.

一个创建DTrace脚本的脚本

在使用DTrace的时候, 你不仅需要处理陡峭的学习曲线, 你还要处理在构建时或者运行时出现的一些隐式错误(耶, 它们的隐秘程度和那些swfit编译器的错误处在同一级别).
在学习DTrace的时候为了缓解那些构建是的问题 , 我创建了一个叫做tobjectivec.py的小脚本 (跟踪 Objective-C), 它是可以为你生成自定义 DTrace 脚本的LLDB Python 脚本.

 注意: 欧耶, 现在是提醒你创建DTrace 脚本以及 DTrace 单行命令的好时候了. 随着你DTrace脚本中逻辑复杂度的增加, 使用脚本成为了一个更好的选择. 简单的DTrace查询, 可以使用单行命令.

你可以在本章的starter目录下找到tobjectivec.py 脚本.我假设你已经看过了第22章, “SB Examples, Improved Lookup” 并且已经安装了lldbinit.py 脚本 并且已经将它放在了~/lldb 文件夹下. 假设你已经做过了, 你所需要做的就是把tobjectivec.py 脚本复制/粘贴到你的~/lldb 目录下 并且它会在LLDB下次运行的时候启动.
如果你还没有做, 返回到第22章中 并按照指令安装lldbinit.py文件. 提示一下, 如果你非常固执, 我猜想你可以通过拓展~/.lldbinit文件手动安装`tobjectivec.py.

通过tobjectivec.py浏览 DTrace

是时候开启在Objective-C代码中浏览DTrace的旋风之旅了.
在starter 文件夹中有可循环利用的Allocator项目. 打开那个项目, 构建, 运行, 然后暂停调试器.
当你把Allocator项目暂停在调试器中之后, 进入LLDB 控制台然后输入下面的内容:

  (lldb) tobjectivec -g

通常情况下, tobjectivec 脚本会在你电脑的/tmp/目录下生成一个脚本. 然而, -g 选项的意思是你正在调试你的脚本 用将输出显示到LLDB中来代替在/tmp/displays the output 目录下生成一个脚本. 使用-g (or -- 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. 这hi这个脚本中DTrace语句的三分之一.这是一个 DTrace中用于 检测某种事件的实际探针... 比如当一个DTrace脚本启动的时候. 这也就是说, DTrace 一启动, 就打印出"Starting... use Ctrl + c to stop" 字符串.
  4. 这里还有另外一个当DTrace脚本一结束就打印"Ending..."的DTrace语句.
  5. 这是一个有趣的DTrace probe description.这句话的意思是说跟踪你应用到脚本上的进程ID中的所有的Objective-C 代码 .
  6. 这个语句中打印出触发探针的Objective-C实例的操作, 后面跟着Objective-C 风格的输出. 在这里, 你可以看到这里使用了代表函数char*probefunc 和模块char*probemod. DTrace有几个你可以用的内部变量, probefuncprobemod 只是其中两个. 你还有probeprovprobename 可以使用. 记住模块将会代表类名同时函数代表Objective-C 方法. 这里使用了probemodprobefunc组合 并且 完美的显示在了你熟悉的Objective-C语法中.
    现在你已经理解了这个脚本, 移除-g 选项以便你不再使用调试选项. 在LLDB中输入:
  (lldb) tobjectivec

这一次你会得到一个不同的输出:
Copied script to clipboard... paste in Terminal
你剪切板上的内容已经被改变了. 回到你的终端中, 然后粘贴你粘贴板上的内容. 下面是我粘贴板上的内容, 但是你的可能会有点不容:

  sudo /tmp/lldb_dtrace_profile_objc.d  -p 95129  2>/dev/null

你之前看到的内容都被放到了/tmp/ lldb_dtrace_profile_objc.d文件中.如果你固执的想知道这个脚本做了什么, 我推荐你首先cat 它然后确保你知道它做了什么.
这个脚本提供了LLDB附加的进程的标识(因此你不需要输入pgrep Allocator).
如果你看到了输入密码的提示, 输入你的密码来获取root 权限:

$ sudo /tmp/lldb_dtrace_profile_objc.d  -p 95129  2>/dev/null
Password:
Starting... use Ctrl + c to stop

等待直到DTrace脚本提示你它已经开始运行了.
确保Xcode 和 Terminal 都是可见的, 在控制台中输入po [NSObject class]. 检查这个方法输出的大量的Objective-C 消息.

DTrace(第二十五章:Hello, Dtrace)_第3张图片
图片.png

这回让你为即将到来的事情做好准备. 用LLDB继续执行, 就像这样:

  (lldb) continue

在iOS模拟器中浏览Allocator app (在视图上点击, 按下⌘ + Y调出来电状态栏)同时注意观察DTrace终端窗口.
很可怕, 对吧?
这里输出了太多内容. 让我们通过在模块修饰符上添加内容来过滤掉一些干扰信息.
回到Xcode中, 暂停执行Allocator进程并进到LLDB中.
生成一个新的只关注 Objective-C 类名中包含StatusBar的脚本. 在LLDB中输入下面内容:

  (lldb) tobjectivec -m *StatusBar* -g

这会运行然后输出下面精简后的内容:

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

注意模块部分的probe是如何被改变的. *可以当做你在正则表达式中最爱用的.*. 这意味着我们在查询一个区分大小写的包含StatusBar的Objective-C类的探针.
在 LLDB中, 移除-g选项以便将脚本复制到剪切板, 然后重新运行这个命令.
跳到你的终端窗口中. 通过按下Ctrl + C杀掉之前的DTrace实例, 然后粘贴你的新脚本.

(lldb) tobjectivec -m *StatusBar*

回到Xcode中继续执行.
回到模拟器中并且按下⌘ + Y 调出来电状态栏或者使用⌘ + ← 或者 ⌘ + →旋转模拟器同时注意观察DTrace终端窗口.
你会再次得到一些输出.
在你需要的时候你可以使用DTrace在代码中用最小额性能指定一个宽网来触发并快速练习.

跟踪调试命令

在我执行简单的调试命令的时候我经常会洞察出在现象后面发生了什么 和在他们后面为我工作的代码.
观察一下调用了多少 带了一个简单的的NSString做参数的Objective-C方法.
回到LLDB中, 输入下面内容:

  (lldb) tobjectivec

将内容粘贴到终端窗口中, 但是不要继续在LLDB中执行. 取而代之的是, 只需要输入下面的内容:

(lldb) po @"hi this is a long string to avoid tagged pointers"

在你按下回车键的时候, 检查DTrace终端窗口中的内容 并且 查看一下输出了什么内容. 你将会得到一些类似下面的输出:

DTrace(第二十五章:Hello, Dtrace)_第4张图片
图片.png

我们只是输出了简单的NSString 并且查看调用了多少Objective-C方法!
这里输出的都是swift风格的代码.
用⌘ + K清空终端中的内容, 确保终端中的DTrace脚本仍然在运行. 回到LLDB中并且输入下面的内容:

  (lldb) expression -l swift -O -- class b { }; b()

你再用Swift 的调试环境创建一个纯正的swift类并初始化它. 当这个类被创建的时候观察调用的swift代码.
DTrace 将会提取出:

0x00000001087541b8 +[SwiftObject class]
0x0000000119149778 +[SwiftObject initialize]
0x0000000119149778 +[SwiftObject class]

如果你复制任何一个DTrace输出的地址然后po它, 你就会看到这个Swift 类调用的大量的Objective-C方法.
并不是像你想想的那样纯正, 对吧?

跟踪一个对象

你可以使用DTrace轻松的追踪一个特定的引用调用的方法. 按下Ctrl + C移除之前的DTrace脚本.
同时应用程序处于暂停状态, 用 LLDB获取UIApplication的应用. 确保你再Objective-C的栈帧当中.

  (lldb) po UIApp

你将会得到类似下面的输出:

  

复制这个引用然后用这个引用创建一个只在这个引用是arg0参数的时候才会停下的判断句, objc_msgSend的第一个参数是一个类的实例或者是这个类本身.

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

你的脚本运行之后在控制台中你将会得到类似下面的输出:

#!/usr/sbin/dtrace -s
#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 == 0x7fa774600f90 /
{
    printf("0x%016p %c[%s %s]\n", arg0, probefunc[0], probemod,
(string)&probefunc[1]);
}

看起来不错! 再次执行将-g 选项移除后的这个命令:

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

继续在LLDB中执行, 然后将你的脚本粘贴在终端中.
在模拟器中按下home键(⌘ + Shift + H) 或者来电状态栏 (⌘ + Y).
这会提取出[UIApplication sharedApplication]实例调用的每一个Objective-C 方法.
哦, 输出的内容看起来太多了是吧? 然后统计一下他们的数量!
回到Xcode中, 暂停执行并回到LLDB中:

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

这将会产生下面的脚本:

 #!/usr/sbin/dtrace -s
#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 == 0x7fa774600f90 /
{
    @[probefunc] = count()
}

你懂这个练习.重新运行上面的没有-g选项的tobjectivec 命令, 然后将你剪切板的内容粘贴到终端中然后在LLDB中继续执行.终端中还不会显示任何内容. 但是 DTrace 暗中统计了UIApplication实例调用的每一个方法.
在模拟器中移动 以获取UIApplication调用的大量方法 . 当你用Ctrl + c杀掉DTrace脚本的时候, 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的冰山一角. DTrace还能做许多事情.
我推荐你看一下下面的网站它们有许多学习DTrace非常好的资源.
• https://www.bignerdranch.com/blog/hooked-on-dtrace-part-1/
• https://www.objc.io/issues/19-debugging/dtrace/
在下一章中, 你会深入学习DTrace 以及浏览和剖析Swift代码.

你可能感兴趣的:(DTrace(第二十五章:Hello, Dtrace))