(十七) 中级Dtrace

1. 中级Dtrace

打开Finding Ray应用程序。 在模拟器上生成并运行该项目。该项目的大部分内容都是用Swift编写的,但许多Swift子类是从NSObject继承的。

Swift代码从任何类继承,DTrace都可以处理。 只要使用objc$target provider,我们仍然可以分析从NSObject继承的Swift对象的Objective-C代码。 这种方法的缺点是,如果Swift类实现了任何新方法或实现了任何重写的方法,我们将不会在任何Objective-C探测中看到它们。

1.1 DTrace与Swift理论

我们来谈谈如何使用DTrace来分析Swift代码。有一些优点,也有一些缺点需要考虑。

首先,好消息是:Swift与DTrace模块配合得很好!这意味着很容易根据它实现的特定模块筛选出Swift代码。模块(也称为probemod)可能是Xcode中包含Swift代码的目标名称(除非在Xcode的构建设置中更改了目标名称)。

这意味着我们可以像这样过滤SomeTarget模块中实现的以下Swift代码:

pid$target:SomeTarget::entry

这将在SomeTarget模块中实现的每个函数的开始处设置一个探测。因为没有限定ObjuleC代码,所以这个探针也会选择C/C++代码。

坏消息是,由于模块的信息被占用,Swift类名和函数名都进入了Swift方法的DTrace函数部分(probefunc)。这意味着我们需要在DTrace查询方面更具创造性。
在之前的Swift(Swift 3)迭代中,DTrace返回的probefuncSwift名称是mangledSwift名称,但这在Swift 4中不再适用!DTrace现在在输出中使用未混合的Swift名称!因此,不用再费心了。让我们看一个快速的DTrace探测示例。假设我们有一个名为ViewControllerUIViewController子类重写viewDidLoad。就像这样:

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad() 
    }
}

如果要在此函数上创建断点,则此断点的全名如下:

SomeTarget.ViewController.viewDidLoad() -> ()

如果我们想在SomeTarget目标中搜索Swift实现的每个viewDidLoad,可以创建如下所示的DTrace探测描述:

pid$target:SomeTarget:*viewDidLoad*:entry

下面我们在Finding Ray项目里面试试。运行项目,并在终端中输入:

sudo dtrace -n 'pid$target:Finding?Ray::entry' -p `pgrep "Finding Ray"`
Password:
dtrace: description 'pid$target:Finding?Ray::entry' matched 233 probes

我选择了一个Xcode项目名,它故意有一个空格。注意在使用DTrace脚本时,需要执行哪些操作来解析Xcode目标中的空格。probemod部分使用?作为空间的占位符通配符。此外,在为进程名进行pgrep时,需要将查询包围起来,否则它将不起作用。

输入完密码后,在Finding Ray模块中的所有非Objective-C函数中,我们将获得233次探针输入命中。

单击Ray并在模拟器中拖动他,同时监视终端中被击中的所有方法。输出还是有点太多了了。我们只希望打印Swift函数
。不需要查看探测ID和CPU列。终止DTrace脚本并将其替换为以下内容:

sudo dtrace -qn 'pid$target:Finding?Ray::entry { printf("%s\n", probefunc); } ' -p `pgrep "Finding Ray"`

我们添加了-q(或-quiet)选项。这将告诉DTrace不要显示找到的探测数量,也不要在探测被击中时显示其默认输出。幸运的是,我们还添加了printf语句来手动地打印probefunc。等待DTrace启动,然后再次拖动。

好一些了,但是我们仍然得到一些Swift编译器生成的方法,而这些方法不是我们编写的。我们不想看到Swift编译器创建的任何代码,只想看到我们在Swift类中编写的代码。终止以前的DTrace脚本,并将此探测描述扩展为仅包含已实现的代码,而不包含Swift编译器的代码:

sudo dtrace -qn 'pid$target:Finding?Ray::entry { printf("%s\n", probefunc); } ' -p `pgrep "Finding Ray"` | grep -E "^[^@].*\."

grep使用正则表达式查询来表示返回不包含@且在输出中包含.的任何内容,就是说不返回任何@objc桥接方法。

扩展脚本以删除grep过滤,而是跟踪Finding Ray模块中的所有Swift函数条目和出口,并使用DTraceflowindent选项。flowindent选项将正确缩进函数项并返回。

sudo dtrace -qFn 'pid$target:Finding?Ray::*r* { printf("%s\n", probefunc); } ' -p `pgrep "Finding Ray"`

我们已经添加了-F选项开启了flowindent。查看探针描述中的name部分,*r*。这是干什么的?从DTrace的角度来看,进程中的大多数函数对每个汇编指令都有入口、返回和函数偏移量。其中偏移量以十六进制表示。这个参数的意思就是“给我任何包含字母‘r’的名字。”这将包含探测描述名称中的entryreturn,但忽略任何函数偏移。

启用每个Swift函数的enterreturn探测后,我们可以清楚地看到正在执行哪些函数以及从何处执行它们。等待DTrace开始,然后拖动图片。你会得到如下的输出:

测试

1.2 DTrace变量与控制流

DTrace有几种方法可以在脚本中创建和引用变量。在DTrace的使用速度和便利性之间的较量中,它们都有各自的优缺点。

标量变量

创建变量的第一种方法是使用标量变量。这些是简单的变量,只能接受固定大小的项。不需要在DTrace脚本中声明标量变量的类型或任何与此相关的变量。我倾向于在DTrace脚本中使用标量变量来表示布尔值。这是由于DTrace的条件逻辑有限。只有谓词和三元运算符才能真正构成分支逻辑。

例如,下面是使用标量变量的实际情况:

#!/usr/sbin/dtrace -s #pragma D option quiet
dtrace:::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]);
}

这个脚本声明两个标量变量:isSet标量变量将检查object标量变量是否已设置。否则,脚本将把下一个对象设置为对象变量。这个脚本将跟踪在对象变量上使用的所有Objective-C方法调用。

子句局部变量

this->这个词表示。在变量名之前使用,可以接受任何类型的值,包括char*。子句局部变量可以在同一个探测中生存。如果你试图在另一个探测器上引用它们,那就行不通了。例如,请考虑以下内容:

pid$target::objc_msgSend:entry
{
        this->object = arg0;
}
pid$target::objc_msgSend:entry / this->object != 0 / {
        /* Do some logic here */
}
objc$target:::entry { 
    this-f = this->object; /* Won’t work since different probe */ 
}

我倾向于尽可能多地使用子句局部变量,因为它们非常快,而且我不必像处理下一类变量那样手动释放它们。

线程局部变量

线程局部变量以速度为代价提供了最大的灵活性。但是,必须手动释放它们,否则会泄漏内存。线程局部变量可以在变量名前面加上self->。线程局部变量的好处是它们可以在不同的探测中使用,例如:

objc$target:NSObject:init:entry {
        self->a = arg0;
}
objc$target::-dealloc:entry / arg0 == self->a / {
        self->a = 0;
}

这将把self->a分配给正在初始化的任何对象。释放此对象时,还需要通过将a设置为0手动释放它。

下面针对DTrace中使用变量,让我们讨论一下如何使用变量执行条件逻辑。

DTrace条件

DTrace内置了极其有限的条件逻辑。DTrace中没有if/else语句!这是一个有意识的决定,因为DTrace脚本被设计为执行非常快。但是,当我们希望基于特定探测或该探测中包含的信息有条件地执行逻辑时,它确实会给我们带来问题。为了克服这个限制,有两个值得注意的方法可以用来执行条件逻辑。

  • 使用三元运算符

考虑下面这个OC逻辑:

int b = 10;
int a = 0;
if (b == 10) {
  a = 5;
} else {
  a = 6;
}

DTrace中它可以用三元运算符转换为:

int b = 10;
int a = 0;
a = b == 10 ? 5 : 6

下面是另一个没有其他语句的条件逻辑示例:

int b = 10;
int a = 0;
if (b == 10) {
        a++;
}

DTrace中:

nt b = 10;
int a = 0;
a = b == 10 ? a + 1 : a
  • 使用多个DTrace子句和一个谓词。

第一个DTrace子句将设置第二个子句所需的信息,以查看它是否应该在谓词中执行操作。

例如,假设我们希望跟踪函数开始和停止之间的每个调用。通常,我建议只设置一个DTrace脚本来捕获所有内容,然后使用LLDB来执行命令。但是如果我们只想在DTrace中做这个呢?对于此特定示例,我们希望使用以下DTrace脚本跟踪由-[UIViewController initWithNibName:bundle:]执行的所有Objective-C方法调用:

#!/usr/sbin/dtrace -s
#pragma D option quiet
dtrace:::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:返回。

1.3 检查进程内存

这可能会让人吃惊,但我们编写的DTrace脚本实际上是在内核本身中执行的。这就是它们速度如此之快的原因,也是不需要更改已编译程序中的任何代码来执行动态跟踪的原因。因为内核可以直接访问!

DTrace在计算机上到处都有探测器。内核中有探测,用户区中有探测,甚至有探测使用fbt provider来描述内核和用户区之间的交叉部分。

下面是一个可视化显示,在的计算机上DTrace探测的百分比非常小。

Dtrace

通过探索open系统调用和open_nocancel系统调用,将我们的关注范围缩小到数千个探测中的两个。这两个函数都是在内核中实现的,负责任何类型的文件打开,以便进行读、写或两者兼有。系统open具有以下功能签名:

int open(const char *path, int oflag, ...);

在内部,open有时会调用open_nocancel,它具有以下函数签名:

int open_nocancel(const char *path, int flags, mode_t mode);

这两个函数都包含一个char*作为第一个参数。在使用arg0arg1进行DTrace探测之前,我们就已经从函数中获取了参数。

我们还没有做的是取消这些指针的引用以查看它们的数据。我们可以使用DTrace在内存中进行探索,甚至可以在open系统调用中获得第一个参数的字符串表示。

不过,有一个问题。在内核中执行DTrace脚本。argX参数已提供给我们,但它们是指向程序地址空间中的值的指针。由于DTrace在内核中运行,因此我们需要手动将正在读取的任何数据复制到内核的内存空间中。

这是通过copyincopyinstr函数完成的。copyin将获取一个包含要读取的字节数的地址,而copyinstr希望复制char*表示形式。

对于open系统调用,我们可以通过以下Dtrace读取第一个参数:

sudo dtrace -n 'syscall::open:entry { printf("%s", copyinstr(arg0)); }'

例如,如果PID12345的进程试图打开/Applications/SomeApp.app/,则DTrace可以使用copyinstr(arg0)读取第一个参数。

copyinstr

对于这个例子,DTrace将读取arg0arg0等于0x7fff58034300。使用copyinstr函数,将取消对0x7fff58034300内存地址的引用,以char*的表示形式获取路径名/Applications/SomeApp.app/

1.4 探测open系统调用

用检查进程内存的知识, 创建一个检测系统调用open函数的DTrace脚本。在终端中输入下面内容:

sudo dtrace -qn 'syscall::open*:entry { printf("%s opened %s\n",
execname, copyinstr(arg0)); ustack(); }'

这将打印openopen_nocancel的内容,以及调用open*系统调用的用户区堆栈跟踪。

下面将系统调用的open*函数集中在Finding Ray进程上。

sudo dtrace -qn 'syscall::open*:entry / execname == "Finding Ray" / { printf("%s opened %s\n", execname, copyinstr(arg0)); ustack(); }'

重新构建并运行程序。堆栈记录现在将会只显示在Finding Ray应用中的系统调用的任何open*函数。

通过paths过滤open系统调用

Finding Ray项目中,我们对Ray.png图片做了一些处理,但是不记得是在哪个位置了。好消息是我们可以用DTracegrep找到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行代码。

有一种更优雅的方法可以做到这一点。可以使用DTrace子句的谓词部分搜索用户区char*输入中的Ray.png字符串。我们使用strstr函数执行这个检查。这个函数接受两个字符串,并返回指向第一个字符串中第二个字符串的第一个匹配项的指针。如果找不到匹配项,则返回空值。这意味着我们可以检查此函数在谓词中是否等于空,以搜索包含Ray.png的路径!

sudo dtrace -qn 'syscall::open*:entry / execname == "Finding Ray" && strstr(copyinstr(arg0), "Ray.png") != NULL / { printf("%s opened %s\n", execname, copyinstr(arg0)); ustack(); }' 2>/dev/null

构建并重新运行这个程序。去掉了grep并且用一个条件句来判断Finding Ray进程中代表的文件路径中是否包含Ray.png。 此外,我们可以轻松精确地找出负责打开Ray.png图片的堆栈记录。

1.5 DTrace和破坏性的操作

注意:下面将要展示的操作是非常危险的。我再重复一遍: 接下来的操作是非常危险的。如果搞砸了,可能失去一些最喜欢的图片。

安全起见, 请关闭所有正在使用图像的应用程序(比如, Photos、PhotoShop等等)!!!

我们将使用DTrace执行破坏性操作。也就是说,通常DTrace只会监视我们的电脑,但现在我们要改变程序的逻辑。

将监视Finding Ray应用程序执行的open*系统调用。如果open*系统调用,在其第一个参数中包含短语.png(也称为char*类型的参数,指向它打开的路径),我们将用另一个png图像替换该参数。

这都可以通过copyoutcopyoutstr命令来完成。在本例中,将显式地使用copyoutstr。我们会注意到这些名字与copyincopyinstr相似。这个上下文中的inout指的是将数据复制到DTrace可以读取数据的位置,或复制到进程可以读取数据的位置。

在工程目录中,有一个名为troll.png的独立图像。在Finder中用⌘ + N创建一个新窗口,然后按⌘ + Shift + H导航到主目录。将troll.png放到这个目录中。

下面我们将要在现有程序中写入内存。程序内存中只有有限的空间分配给这个字符串。这可能是一个长字符串,因为我们在模拟器中,并且进程一般只读取在它自己的沙盒中找到的图像。

还记得Ray.png的地址吗?这是我电脑上的完整路径。你的显然会不同。

/Users/xxx/Library/Developer/CoreSimulator/Devices/85225EEE-8D5B-4091-A742-5BEBAE1C4906/data/Containers/Bundle/Application/E2E33947-E19D-4873-B493-42479ED45EF4/Finding Ray.app/Ray.png

我们的计划是用一个更短的路径来使用DTrace读取一个图片,在程序的内存中看起来应该是这个样子:

/Users/xxx/troll.png\0eveloper/CoreSimulator/Devices/85225EEE-8D5B-4091-A742-5BEBAE1C4906/data/Containers/Bundle/Application/E2E33947-E19D-4873-B493-42479ED45EF4/Finding Ray.app/Ray.png

看到这里的\0了吗?这是char*的终结符。因此本质上这个字符串仅仅只是:

/Users/xxx/troll.png

因为那就是一个字符串是如何用NULL结尾的。

获取你的路径长度

在写数据时,需要知道完整路径中有多少字符指向troll.png

echo ~/troll.png | wc -m
      21

open*中的arg0指向内存中的某个内容。如果要在这个位置写入比这个字符串长的内容,那么这可能会损坏内存并杀死程序。显然,我们不需要这样做,所以我们要做的是将troll.png保存在字符数较短的目录中。还将通过DTrace谓词执行检查,以确保有足够的空间。

sudo dtrace -wn 'syscall::open*:entry / execname == "Finding Ray" && arg0 > 0xfffffffe && strstr(copyinstr(arg0), ".png") != NULL && strlen(copyinstr(arg0)) >= 21 / { this->a = "/Users/xxx/troll.png"; copyoutstr(this->a, arg0, 21); }'

在新的DTrace脚本激活之后重新构建并运行Finding Ray应用程序。假如执行是顺利的,那么当Finding Ray进程每次尝试打开一个包含.png字符的文件的时候,都会返回一个troll.png来替代。

其他破坏性的操作

除了copyoutstrcopyout之外,DTrace还有其他一些破坏性的操作需要注意:

  • stop(void): 将会冻结当前正在运行的用户进程(通过内部的pid参数给定的进程)。如果我们想停止执行一个用户进程,并将LLDB附加到进程上并进一步浏览的时候,这是一个完美的方式。
  • raise(int signal): 负责为探针增加一个信号到进程上。
  • system(string program, ...) :可以让我们像在终端中一样执行一个命令。它有一个额外的好处,可以我们访问所有DTrace内部的变量,例如execnameprobemod,用于printf格式化。

要小心的使用这些系统函数。如果使用错误,真的很容易就造成大量的破坏。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,588评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,456评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,146评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,387评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,481评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,510评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,522评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,296评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,745评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,039评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,202评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,901评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,538评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,165评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,415评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,081评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,085评论 2 352

推荐阅读更多精彩内容