本文共 13694 字,大约阅读时间需要 45 分钟。
这一章将更多的将会作为Dtrace的抓包原理, 毁灭动作(耶!), 以及如何用Swift使用Dtrace. 在进入理论之前我首先会告诉你一兴奋的东西.我将会首先讲解如何用Swift使用Dtrace然后进到让你眼泪汪汪想要入睡的概念中. 相信我, 这会很有趣!
在这一章节, 你将会学些DTrace剖析代码的其他方式, 以及如何在不动可执行文件一根手指头的情况下增强已经存在的代码.神奇吧!开始
我们没有在Ray Wenderlich上摘抄. 本章节中包含的还有另外一个用穿插有Ray的名字源自电影主题灵感的项目.
打开starter目录下的Finding Ray程序.不需要做任何特殊的设置.只需要用iPhone 7Plus模拟器构建并运行就可以了.这个项目主要是用swift写的, 尽管swift的许多子类继承自NSObject.因为它用到了很多UIKit里的组件, 而UIKit里面仍然有很多Objective-C代码.Dtrace不知道swift代码继承自哪个类, 因为在Dtrace看来他们都是一样的. 你仍然可以通过swift代码剖析Objective-C代码的代码, 因为它们都通过objc$target继承自NSObject. 在底层看来只是swift的类有没有实现新的方法或者重写了父类的方法, 你在任何Objective-C的探针中都看不到他们.Dtrace和Swift的理论
让我们讨论一下如何使用DTrace剖析Swfit的代码. 网上有些不错的意见值得考虑一下.
首先, 好消息是: swift可以很好的兼容DTrace模块!也就是说 可以轻松的过滤出特定模块中的swift代码. 这个模块可能会成为你Xcode中包含swift代码的的target的名字(除非你再Xcode的构建设置中改变了target的名字).这就意味着你可以在SomeTarget模块中过滤出下面的swift代码的实现.pid$target:SomeTarget::entry
这会在SomeTarget模块中每一个函数实现署名开始的地方创建一个探针.因为pid$target在所有非Objective-C代码的后面, 所以这些探针也同样会捕获到C或C++的代码, 但是在第二部分你会看到我们可以用设计的很好的查询语句很轻松的过滤掉.现在来说一下坏消息. 因为关于模块的信息已经拿掉了, 所以swift方法的类名和函数名都在Dtrace section(也就是probefunc)里. 这就意味着在你需要在DTrace查询代码里更有创造性一点.此外, swift函数的名字是被打乱了的. 这就意味着你需要将Dtrace的输出转接到一个可以将被打乱的swift函数名重新整理的命令上以便更容易理解一点.幸运的是, swift中有一个非常好的终端命令叫做swift-demangle, 我们可以在这里找到它:/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift-demangle
通过终端设置下面的符号链接以便你不需要每次使用这个命令重组swift函数名的时候都将这个path输入一遍.ln -s /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift-demangle /usr/local/bin/
将路径添加到/usr/local/bin/, 你只需要输入swift-demangle就可以轻松的使用这个命令:注意: 你也可以通过xcrun swift-demangle运行这个命令, 但是在用多个swift探针剖析Dtrace代码的时候性能开销就需要特别注意下. 在通过swift使用Dtrace的时候这个符号链接就可以最大程度的解决这个问题.
我们来看一个通过swift使用Dtrace探针的例子.想象一下你有一个UIViewController的子类ViewController, 它只重写了viewDidLoad方法. 就像下面这样:class ViewController: UIViewController {
override func viewDidLoad() { super.viewDidLoad()}}如果你想在这个方法上创建一个断点, 这个断点全名应该是下面这个样子:SomeTarget.ViewController.viewDidLoad () -> ()
不要惊讶. 你已经将第一部分的概念击败了. 但是被打乱之后的函数名是什么样子的呢? 这就是高效的c函数返回的swift函数. 他看起来就像下面这个样子:_TToFC10SomeTarget14ViewController11viewDidLoadfTT
如果你不相信我, 你可以在终端中试一下下面的代码:$ echo "_TToFC10SomeTarget14ViewController11viewDidLoadfTT" | xcrunswift-demangle
你将会得到下面的输出:@objc SomeTarget.ViewController.viewDidLoad () -> ()
意外吧!再次得到了打乱之前的函数名.打乱之后的名字就是DTrace在swift探针中看到名字.如果你想搜索SomeTargettarget(易记的名字, 对吧?)中每一个用swift实现的viewDidLoad(), 你可以创建一个像下面这样的探针:pid$target:SomeTarget:SomeTargetviewDidLoad*:entry
这句话很高效, 它的意思是说"只要SomeTarget和viewDidLoad在函数区, 就给我这个探针."是时候在Finding Ray程序中将理论付诸实践了.Dtrace和Swift 练习
如果Finding Ray应用程序还没有运行起来, 那就用iPhone 7 Plus模拟器让它跑起来.
新开一个终端窗口然后输入下面命令:sudo dtrace -n 'pid$target:Finding?Ray::entry' -p pgrep "Finding Ray"
图片.png
可恶!名字没打乱了. 幸运的是你可以用前面看到的swift-demangle命令轻松的解决这个问题.
用Ctrl + C杀掉Dtrace脚本, 让后在swift-demangle后面接上一个管道, 看起来应该是下面这个样子:sudo dtrace -n 'pid$target:Finding?Ray::entry' -ppgrep "Finding Ray"
|swift-demangle -simplified确保这个新的Drace脚本是可以运行的, 然后在模拟器中拖拽Ray Wenderlich并且注意查看终端中的输出.杀掉Dtrace脚本然后用下面的命令替代它: sudo dtrace -qn 'pid$target:Finding?Ray::entry { printf("%s\n",probefunc); } ' -p pgrep "Finding Ray"
| swift-demangle -simplified
sudo dtrace -qn 'pid$target:Finding?Ray:Finding?Ray:entry
{ printf("%s\n", probefunc); } ' -ppgrep "Finding Ray"
| swift-demangle -simplified编译运行然后拖转Ray. 注意到了不同之处吗?这里你已经为函数添加了Finding?Ray.正如你在断点中知道的, 这个模块中包含所有的swift函数.这里过滤掉了任何一个编译器生成的swift方法, 过滤的非常非常干净.根据我们之前的设想, 你已经将之前设想的对象的指针输出了出来, 正如在前面的章节中tobjectivec.py所做的那样.在终端中杀掉这个脚本, 然后输入下面的内容: sudo dtrace -qn 'pid$target:Finding?Ray:Finding?Ray:entry
{ printf("0x%p %s\n", arg0, probefunc); } ' -ppgrep "Finding Ray"
|swift-demangle -simplified运行它然后拖拽Ray. 你将会得到一些类似下面的输出: 0x7f8b17d02e40 MotionView.transformAmount.getter
0x7f8b17d02e40 MotionView.motionAmount.getter0x7f8b17d02280 type metadata accessor for MotionView如果你通过LLDB暂停执行Finding Ray可执行文件并且复制, 粘贴, 然后po其中的一个地址, 你将会范县这个地址引用着一个有效的对象.最后加一点. 将脚本中打印指针的代码移除掉, 用追踪Swift函数入口和出口来替换, 然后使用DTrace的flowindent在显示出一个函数相对于其他函数执行的位置 :sudo dtrace -qFn 'pid$target:Finding?Ray:Finding?Ray:r
{ printf("%s\n", probefunc); } ' -ppgrep "Finding Ray"
| swift-demangle -simplified在这里有几点需要注意的. 你已经为flowindent添加了-F选项. 检查一下探针描述中的名字部分, r.这是干什么的呢?从DTrace的角度来看, 进程中的大部分函数都有入口, 返回点和每一条汇编指令的函数偏移. 这些偏移是以十六进制的形式给出的.这就是说"给我所有包含r字母的名字." 这回返回探针描述的名字中的入口和返回点, 但是会优化任何函数偏移因为汇编仅仅从f开始.很聪明, 对吧?有了每一个可用的swift函数的入口和返回点, 你可以清除的看到什么函数被执行了以及它们是从哪里执行的. 等待DTrace开始, 然后为着Ray Wenderlich的脸部拖拽.你将会得到想下面这样完美的输出: 图片.png
额.....你应该会踢出其中的一个!
DTrace变量和控制流
现在你会学到一些在本章余下部分需要使用的理论.
DTrace有几种方法可以在你的脚本中创建和引用变量. 在使用DTrace的时候它们在速度和便利性上各有利弊.scalar variable (标量变量)
第一种创建变量的方法是使用scalar variable. 这些简单的变量只能带一些固定大小的数据.你不需要声明scalar variables的类型, 或者你DTrace脚本中其他变量的类型.我更倾向于在DTrace脚本中将scalar variable作为一个Boolean值使用, 这是由于DTrace条件语句逻辑的限制--你只能用判断句和三元运算符来真正的将逻辑分开.
例如, 这里有一个运用scalar variable的典型例子:#!/usr/sbin/dtrace -s
#pragma D option quietdtrace:::BEGIN{ isSet = 0;object = 0; }objc$target:NSObject:-init:return / isSet == 0 /{object = arg1;isSet = 1; }objc$target:::entry / isSet && object == arg0 /{ printf("0x%p %c[%s %s]\n", arg0, probefunc[0], probemod,(string)&probefunc[1]);}这个脚本声明了两个scalar variables: isSet这个scalar variables会检查和判断object scalar variables是否被赋值. 如果没有, 这个脚本会将下一个object赋值给object变量. 这个脚本将会追踪所有使用object变量的Objective-C方法.局部从句变量(Clause-local variables)
下一步是局部从句变量. 它们用this->将变量名放在右边来表示并且可一直想任意类型, 包括char*类型. 局部从句变量在经过同样探针的时候可以幸存下来.如果你尝试通过一个不同的探针引用他们, 那是行不通的.例如, 看一下下面的代码:
pid$target::objc_msgSend:entry
{ this->object = arg0;}pid$target::objc_msgSend:entry / this->object != 0 / { / Do some logic here /}obc$target:::entry { this-f = this->object; / Won't work since different probe /}我倾向于尽我所能的粘贴从句局部变量, 因为这样速度很快而且我不需要手动释放他们, 就像我对下一种类型的变量所做的那样...线程-局部变量(Thread-local variables)
线程局部变量在运行速度上提供了很大的灵活性. 此外, 你还需要手动释放它们, 不然会产生内存泄露.线程局部变量可以通过在变量名前面加上self->来访问和使用.
线程局部变量的一个优点是他们可以被不同的探针使用, 就像下面这样:objc$target:NSObject:init:entry {
self->a = arg0;}objc$target::-dealloc:entry / arg0 == self->a / { self->a = 0; }这会将初始化的对象赋值给self->a.当这个对象被释放的时候, 你同样需要通过将a设置为0手动的释放刚才赋值给a的对象.讨论DTrace中的变量已经偏离了我们的主题, 让我们来说一说如何用变量来执行条件语句.DTrace 条件
DTrace内部的条件句极其有限. 在DTrace中没有像if-else这样的语句. 这是一个明智的选择, 因为DTrace 脚本设计的初衷就是方便快捷.
然而, 这并不代表着你想在特定的探针或者探针中包含的信息上执行条件语句的时候会遇到问题. 围绕这个问题, 这里有两个主要的方法可以让你执行条件语句.第一种方法是使用三元运算符.看一下在Objective-C中的逻辑:int b = 10;
int a = 0;if (b == 10) { a = 5;} else { a = 6; }这个逻辑在DTrace中可以被重写为:b = 10;
a = 0;a = b == 10 ? 5 : 6这里还有一个没有else语句的例子:int b = 10;
int a = 0;if (b == 10) { a++; }在DTrace中的形式, 看起来就像下面这样:b = 10;
a = 0;a = b == 10 ? a + 1 : a另外一种解决方案是用多条DTrace语句来判断一种情况.第一条语句会为第二条语句设置必要的信息, 这要看第二条语句在判断句中是否要执行一个动作.我知道你已经忘记了这些DTrace组件中所有的术语因此我们来看一个例子吧.例如, 如果你想追踪一个函数中从开始到结束的每一个调用.通常情况下, 我会推荐你设置一个DTrace脚本来捕获每一次调用然后用LLDB来执行命令. 但是哪些是你只能在DTrace中做的事情呢?在这个特定的例子中, 你想用下面的DTrace脚本来追踪所有-[UIViewController initWithNibName:bundle:]被执行时调用的方法:#!/usr/sbin/dtrace -s
#pragma D option quietdtrace:::BEGIN{ trace = 0;}objc$target:target:UIViewController:-initWithNibName?bundle?:entry { trace = 1 }objc$target:target:::entry / trace / { printf("%s\n", probefunc);}objc$target:target:UIViewController:-initWithNibName?bundle?:return { trace = 0 }initWithNibName:bundle:一旦执行完毕, 追踪变量就被设置了. 从此刻开始, 每个单一的Objective-C方法都会在initWithNibName:bundle:返回之后显示.在写DTrace脚本的时候首先不能使用烦人的循环语句和条件语句, 但是想一下你已经变成了习惯于脑筋急转弯不依赖普通语法习惯的人.到了头讨论另一个大问题的时候了:在你的DTrace脚本中检查进程的内存.检查进程内存
它也许是作为一个惊喜而来的, 但是你之前写的DTrace脚本是在它自己的内核中执行的.这就是为什么他们的速度如此之快也是为什么你在已经编译过的程序中执行动态追踪的时候不需要改变任何代码的原因. 都是直接访问内核的!
DTrace有的探针遍布你的电脑. 这其中有内核中的探针, 也有用户区的探针, 甚至还有使用fbt提供的用来描述内核和用户区衔接的探针.这里有一幅图形象的描述了你电脑中非常非常小的一部分探针.图片.png
将你的经理集中在成千上万探针中的两个一个是系统调用的open, 另一个是系统调用的open_nocancel.这两函数都是是现在内核中的而且是用来负责打开任何类型的文件的,无论它是只读的只写的还是既可以读也可以写的.
系统中open函数的声明是下面这个样子:int open(const char *path, int oflag, ...);本质上, open函数有时候会调用open_nocancel函数, open_nocancel函数的声明是这样的:int open_nocancel(const char path, int flags, mode_t mode);这两个函数的第一个参数都是char型. 前面在DTrace的探针中你已经用arg0和arg1从函数中抓取到了参数. 你还没有做的就是解引用这些指针来查看它们的数据. 就像前面的SBValue章节, 你可以用DTrace来查看内存, 甚至可以获取系统调用的open函数中第一个参数的字符串.
虽然你已经明白了. DTrace脚本是在内核中执行的.argX参数已经给到你了, 但是这些是程序内存空间中值的指针.然而, DTrace是在内核中执行的. 因此你需要手动复制你在内核的内存空间中读到的任何数据.这是通过copyin和copyinstr函数实现的. copyin 带了一个你想要读取的数据的数量的地址, 同时copyinstr会复制一个char*.就系统调用的open家族的函数来说, 你可以用下面的DTrace语句将第一个参数以string的形式读取出来:sudo dtrace -n 'syscall::open:entry { printf("%s", copyinstr(arg0)); }'
例如, 如果一个PID是12345的进程试图打开/Applications/SomeApp.app/, DTrace就可以使用copyinstr(arg0)读取第一个参数.图片.png
在这个例子中, DTrace会读到arg0中, 在这个例子中等同于0x7fff58034300. 在copyinstr函数中, 内存地址0x7fff58034300将会被解引用然后抓取到代表pathname的char*, "/Applications/SomeApp.app/".open syscalls的运用
用你检查进程内存的知识, 创建一个检测系统调用open家族函数的DTrace脚本. 在终端中输入下面内容:
sudo dtrace -qn 'syscall::open:entry { printf("%s opened %s\n",execname, copyinstr(arg0)); ustack(); }'这将会顺着程序在用户区的堆栈记录中调用的系统的open打印出open(或者open_nocancel)的内容.
DTrace很强大对吧?将系统调用的open家族的函数集中在Finding Ray进程上.sudo dtrace -qn 'syscall::open:entry / execname == "Finding Ray" /{ printf("%s opened %s\n", execname, copyinstr(arg0)); ustack(); }'注意: 你在终端中用DTrace执行的动作有时候会产生一些错误到stderr
. 根据这些错误, 你可用创建的DTrace判断句检查输入的内容是否恰当来绕过这些错误, 或者你可以用少量的查询探针来过滤你的探针描述.提示一点, 忽略所有DTrace通过2>/dev/null
添加的单命令行程序产生的错误. 这可以高效的告诉你的DTrace单命令行程序输出的任何stderr
内容都会被忽略.我经常用这种方法解决那些容易出错的探针, 但是忽略我追踪时产生的任何错误.重新构建并运行程序.堆栈记录现在将会只显示在Finding Ray应用中的系统调用的任何open函数. 在模拟器中运行这个程序然后看一下你是否能让他输出一个内容!
通过paths过滤open syscalls
在Finding Ray项目中, 我记得我用Ray.png图片做了一些事情, 但是我不记得是在那个位置了. 好消息是我用DTrace沿着grep找到了Ray.png被打开的位置.
杀掉你当前的DTrace脚本然后添加一个grep查询, 就像下面这个样子:sudo dtrace -qn 'syscall::open*:entry / execname == "Finding Ray" /
{ printf("%s opened %s\n", execname, copyinstr(arg0)); ustack(); }' |grep Ray.png -A40这将所有的输出嫁接到grep并且搜索任何Ray.png图片的引用.如果搜索到了, 则打印出接下来的40行代码.注意:在你的电脑中的/usr/bin/
下有一个相当可怕的叫做opensnoop
的DTrace脚本, 它有很多选项去检测系统调用的open
家族的函数并且与写的脚本比起来用起来更简单. 但是如果你只用现成的东西那么你就学不到任何东西. 对吧?在你有空的时候你可以看一下这个脚本. 它能做的事情不会让你失望的.
sudo dtrace -qn 'syscall::open*:entry / execname == "Finding Ray" &&
strstr(copyinstr(arg0), "Ray.png") != NULL / { printf("%s opened %s\n",execname, copyinstr(arg0)); ustack(); }'构建并重新运行这个程序.你丢掉了grep管道并且用一个用一个条件句来判断FindingRay进程中×××的文件路径中是否包含Ray.png.此外, 你可以轻松的精确找出负责打开Ray.png图片的堆栈记录.DTrace和毁灭性的操作
注意: 我将要向你展示的是非常危险的.我再重复一遍: 接下来的操作是非常危险的.
如果你搞砸了一个命令你可能失去一些你最爱的图像. 只在你自己的硬盘上!事实上, 是安全的, 请关闭所有正在使用图像的应用程序(比如, Photos, PhotoShop, 等等). 我和Razeware团队对你电脑上发生的任何事情都不负任何法律责任.我们已经警告过你了!呃...我打赌上面的法律声明肯定让你紧张了!你将会使用DTrace来执行一个破坏性的操作. 那就是, 通常DTrace只会检测你的电脑, 但是现在你将会实际修改你程序的逻辑. 你将会检测到被Finding RayAPP执行的系统调用的open家族的函数. 如果有一个系统调用的open函数第得一个参数包含单词.png(也就是它打开的char*类型的路径参数), 你将会用一个不同的PNG图片替换那个参数.这些可以通过copyout和copyoutstr 这两个DTrace命令实现. 在这个例子中你将明确使用copyoutstr. 你将注意到类似copyin和copyinstr的名字. 在这个上下文中的in和out指明了你拷贝的数据的目录, 既有内部DTrace可以读取数据的目录, 也有外部process可以读取它的目录.在这个projects目录中, 有一个叫做troll.png的图片.用 ⌘ + N创建一个新的Finder窗口, 然后按下⌘ + Shift + H进到你的home目录下. 将troll.png拖拽到这个目录下(当本章结束的时候你就可以删掉了). 这里有一个疯狂的只有我能承受的方法来做这件事! 为什么你需要做呢?你将在一个令人兴奋的程序里改写内存.在这个程序的内存里为这个字符串分配的只有有限的空间. 这有可能是一个很长的字符串因为你再iPhone模拟器中并且你的进程(极有可能)是在他自己的沙盒中读取图片.你还记得搜索Ray.png? 下面是我电脑上的完整路径. 你的路径与我的肯定不同:/Users/derekselander/Library/Developer/CoreSimulator/Devices/
97F8BE2C-4547-470C-955F-3654A8347C41/data/Containers/Bundle/Application/102BDE66-79CB-453C-BA71-4062B2BC5297/Finding Ray.app/Ray.png我们的计划是用一个更短的路径来使用DTrace读取一个图片, 在程序的内存中看起来应该是这个样子:/Users/derekselander/troll.png\0veloper/CoreSimulator/Devices/
97F8BE2C-4547-470C-955F-3654A8347C41/data/Containers/Bundle/Application/102BDE66-79CB-453C-BA71-4062B2BC5297/Finding Ray.app/Ray.png你看到这里的\0了吗?这是char*的终结符. 因此本质上这个字符串仅仅只是:/Users/derekselander/troll.png
因为那就是一个字符串是如何用NULL结尾的!获取你的路径长度
在写完数据之后, 你需要弄清楚troll.png图片的完成路径有多少个chars. 我知道我的路径的长度, 不幸的是, 我不知道你的路径的长度.
在终端中输入:echo ~/troll.png
你将会获取到troll.png图片的完整路径. 待会儿你会将这个路径粘贴到你的脚本中.还需要在终端中找出这个字符串的长度.echo ~/troll.png | wc -m
在我这里, /Users/derekselander/troll.png是31个字符. 但是很明显: 你需要将结束符null算进来.这就意味着我需要插入的新字符串的长度是32或者更多.open*函数中的arg0参数指向内存中的一些数据. 如果你将比这个字符串更长的字符串写到这个位置, 然后这回破坏内存并退出程序.很显然, 你不希望这样的事情发生, 因此你需要将troll.png复制到一个路径字符数量较短的目录中.你同样要用DTrace的判断句来检查一下, 确保你有足够的控件存储这个字符串.拜托, 你是一个严谨而勤奋的程序员, 对吧?在终端中输入下面的内容, 用你的值替换/Users/derekselander和32:sudo dtrace -wn 'syscall::open*:entry / execname == "Finding Ray" && arg0
0xfffffffe && strstr(copyinstr(arg0), ".png") != NULL &&
strlen(copyinstr(arg0)) >= 32 / { this->a = "/Users/derekselander/troll.png"; copyoutstr(this->a, arg0, 32); }'在新的DTrace脚本激活之后重新构建并运行Finding Ray应用程序.假如你的执行是顺利的, 那么当Finding Ray进程每次尝试打开一个包含.png字符的文件的时候, 你都会返回一个troll.png来替代.
图片.png
其他毁灭性的操作除了copyoutstr和copyout之外, DTrace还有其他一些破坏性的操作需要注意:
• stop(void): 这将会冻结当前正在运行的用户进程(通过内部的pid参数给定的进程). 这是一个完美的方式如果你想停止执行一个用户进程, 并将LLDB附加附加到进程上并进一步浏览的时候.• raise(int signal): 这负责为探针增加一个信号到进程上.• system(string program, ...): 这可以让你想在终端中一样执行一个命令.它有一个额外的好处可以你访问所有DTrace内部的变量, 例如execname和probemod, 用于printf风格的格式化.我鼓励你查看这个破坏性的操作(尤其是stop()操作), 在你空闲的时候. 但是要小心的使用这些系统函数. 如果使用的不正确你真的很容易就造成大梁破坏.我们为什么要学这些?
在你的macOS机器里有许多强大的DTrace脚本. 你可以通过man -k dtrace捕获它们, 然后慢慢弄明白这些脚本的作用. 此外, 你可以在它们的代码里学到许多知识. 记住, 它们是脚本, 不是编译过的可执行文件, 所以源代码是公平的游戏.
同样, 在执行破坏性操作的时候必须要小心一点. 也就是说, 你可以将ayWenderlich放到你电脑里的任何地方.图片.png
这不正式你想要的效果吗?
严肃的说, 你可以在你的电脑上做一些十分疯狂的操作并且可以用DTrace洞察许多信息.转载于:https://blog.51cto.com/haidragon/2126801