可怕的汇编, 第二部分

是时候重温一下objc_class::demangledName(bool)c++函数中有趣的第二部分了.这一次汇编代码将会聚焦于如果char不在char的初始位置里--也就是说, 如果这个类还没有被加载的时候这些逻辑做了哪些事情. 你需要在紧跟在偏移55的后面的偏移61的汇编指令处位置创建一个断点. 你可以随便调用一个类来看看哪些类没有被加载都运行时里, 我不知道你的进程里的东西而你也不知道我的进程里的东西! 取而代之的是, 在停在objc_class::demangledName(bool)处偏移61的位置出创建一个symbolic断点. 在Xcode中使用下面的步骤创建一个symbolic断点: • 为这个symbolic使用dlopen • 第一步: 用br dis 1移除这个断点 • 第二步: 用下面的命令在objc_class::demangledName(bool)偏移61的地方设置一个断点

br set -M objc_class::demangledName(bool) -R 61 • 选择 "Automatically continue after evaluating actions".

图片.png

重新构建并运行VCTransitions应用程序. 在这个断点被处罚之前你不会非常深入到你的程序里;你会看到dyld仍然忙于设置. 第二轮;我们从这里出发: 图片.png

• Offset 61:提供了在内存中的初始化位置是nil, 继续运行到61, rax + 0x8解引用的地方然后再次存储到rax里. • Offset 65: 值0x18被添加到rax里然后存回rax. rax可能是一个持有一个可以解释这个地址偏移的数据的结构体. • Offset 69: rax的值被解引用然后存储到rbx中, 稍后将会传给rdi 2指令.在那之后, 这个函数调用了copySwiftV1DemangledName函数并且设置了将这个类添加到运行时里的逻辑. 但是对你来说, 直到你需要浏览这个函数为止. 随时确认rdi将会在偏移77处产生一个有效的char*, 但是再说一次, 那将是在你自己的额时间里做的事情. 你仍然需要写一个DTrace脚本. 重新转换到代码里搜索

你已经做了必要的重新搜索来弄清楚如何在内存里蛇形获取代表一个类的字符数组.是时候实现这件事了. 在Starter文件夹中有一个叫msgsendsnoop.d的DTrace脚本. 你将用这个DTrace脚本开始然后构建出相应的代码. 如果经过测试是可行的, 你就需要将那些代码转换成能够让你动态获取你想要的代码的LLDB的python脚本. 在终端中cd到starter文件夹中.将文件夹拖拽到终端中会自动生成路径. cat这个脚本的内容:

cat ./msgsendsnoop.d 下面是输出的内容:

#!/usr/sbin/dtrace -s #pragma D option quiet dtrace:::BEGIN { printf("Starting... Hit Ctrl-C to end.\n"); } pid$target::objc_msgSend:entry { this->selector = copyinstr(arg1); printf("0x%016p, +|-[%s %s]\n", arg0, "TODO", } 让我们来拆解一下. 这个脚本将会停在传入的相应PID的objc_msgSend的入口探针处(这是pid$target的作用).一旦触发了之后, selector的char*会被拷贝到内核中然后打印出来. 正如例子中将会发生的一样, 也就是说-[UIView initWithFrame:] 将会被调用. 会打印出下面的内容:

0x00000000deadbeef, +|-[TODO initWithFrame:] 可以通过追踪VCTransitions程序中所有调用的objc_msgSend来验证一下这个是真的.

sudo ./msgsendsnoop.d -p pgrep VCTransitions 在一些类上点击.这回让你看到这些方法调用的频率.

图片.png 到了修复烦人的__TODO__时间了并且用类的实际名字替换他. 打开msgsendsnoop.d文件然后用下面的代码替换pid$target::objc_msgSend:entry:

注意:我会推荐你输入每一行代码然后确保它是可以运行的, 而不是一次输入所有代码. 一些DTrace的错误会被捕获到. pid$target::objc_msgSend:entry { /* 1 / this->selector = copyinstr(arg1); / 2 / size = sizeof(uintptr_t); / 3 */ this->isa = *((uintptr_t )copyin(arg0, size)); / 4 */ this->rax = *((uintptr_t )copyin((this->isa + 0x20), size)); this->rax = (this->rax & 0x7ffffffffff8); / 5 */ this->rbx = *((uintptr_t *)copyin((this->rax + 0x38), size)); this->rax = *((uintptr_t )copyin((this->rax + 0x8), size)); / 6 */ this->rax = *((uintptr_t )copyin((this->rax + 0x18), size)); / 7 */ this->classname = copyinstr(this->rbx != 0 ? this->rbx : this->rax); printf("0x%016p +|-[%s %s]\n", arg0, this->classname, this->selector); } 深呼吸一下. 下面是每一行代码的意思:

this->selector 做了一个copyinstr, 因为你知道第二个参数(arg1)是一个Objective-C selector(它是一个c字符串).因为C char*是以一个null字符做结尾的, DTrace可以自动检测到读多少数据. 转眼间, 你就要copyin一些数据了. 然而, copyin需要一个size, 因为不像string, DTrace不知道数据在那里结尾.你声明了一个叫size的变量, 它等于一个指针的长度. 在x64中, 就是8 bytes. 这就是获取一个类实例的引用的方法.记住, 解引用Objective-C 或者 Swift类实例的起始位置的指针会指向这个类. 现在是你在objc_class::demangledName(bool)汇编中学到的有趣的部分. 你将会复制在寄存器中找到的逻辑, 甚至会使用同样的寄存器的名字!你正在使用rax来模拟这个函数执行的逻辑. 这就是(rax + 0x38)设置this->rbx的代码, 就像在真是的汇编中一样. 如果this->rbx是0, 这就是最后一行代码(这个类还没有被加载). 你正在使用一个三元操作符来弄清楚哪一个局部变量被使用了.如果this->rbx是non-null, 就用this->rbx. 否则, 使用this->rax. 保存一下你刚才做的工作.回到终端中 从心启动这个DTrace脚本: sudo ./msgsendsnoop.d -p pgrep VCTransitions 喔喔喔喔喔喔喔喔喔喔喔喔喔!那个疯狂的脚本真的有用! 扫描一下你脚本的内容, 看起来这个脚本在objc_msgSend调用一个nil对象(也就是说 RDI也就是arg0 是0x0)的时候会抛出一些错误. 你可以用下面这个命令来查看这个错误:

sudo ./msgsendsnoop.d -p pgrep VCTransitions | grep invalid 让我们用一个简单的判断句来修复一下那个bug. 在pid$target::objc_msgSend:entry后面添加一个判断句:

pid$target::objc_msgSend:entry / arg0 > 0x100000000 / 这个判断句的意思是说"如果第一个参数是nil或者这一段内存没有被利用就不要运行这个DTrace动作". 通常情况下, 在macOS的用户进程中, 这一段内存是不允许读写和执行的.如果那里的数字小于0x100000000, DTrace就不会使用那段内存里的数据.因此, 如果它小于那个数, Dtrace就会跳过它. 当然你也可以在LLDB中用下面这行代码确认一下:

(lldb) image dump sections VCTransitions 在你空闲的时候你可以确认一下.你仍然需要结束这个脚本.

移除干扰

老实说, 我不关心编译器生成的内存管理的代码. 也就是说我们要把retain或者release相关的代码排除掉. 在你当前的探针上用新的从句创建一个新的DTrace探针:

pid$target::objc_msgSend:entry { this->selector = copyinstr(arg1); } /* old code below */ pid$target::objc_msgSend:entry / arg0 > 0x100000000 / 现在你在主从句跳过所有的内存逻辑之前在新的从句里声明了一个selector. 这会让你在主从句的判断句部分过滤Objective-C方法. 说到这儿, 现在主从句中判断句的参数是:

pid$target::objc_msgSend:entry / arg0 > 0x100000000 / && this->selector != "retain" && this->selector != "release" / 现在会忽略所有与retain或release相等的代码. 现在不需要在主从句中重新指定this->selector, 你再另外一个从句了已经做了. 尽管他不会造成坏的影响, 但它仍然是多余的逻辑. 移除它, 或者如果你开心的话也可以不移除它. 你的两个从句现在看起来应该是下面这个样子:

pid$target::objc_msgSend:entry { this->selector = copyinstr(arg1); } pid$target::objc_msgSend:entry / arg0 > 0x100000000 / && this->selector != "retain" && this->selector != "release" / size = sizeof(uintptr_t); { this->isa = *((uintptr_t *)copyin(arg0, size)); } this->rax = *((uintptr_t *)copyin((this->isa + 0x20), size)); this->rax = (this->rax & 0x7ffffffffff8); this->rbx = *((uintptr_t *)copyin((this->rax + 0x38), size)); this->rax = *((uintptr_t *)copyin((this->rax + 0x8), size)); this->rax = *((uintptr_t *)copyin((this->rax + 0x18), size)); this->classname = copyinstr(this->rbx != 0 ? this->rbx : this->rax); printf("0x%016p +|-[%s %s]\n", arg0, this->classname, this->selector); 重新启动这个脚本:

sudo ./msgsendsnoop.d -p pgrep VCTransitions 图片.png

哦 太棒了, 这次好多了! 但是这里仍然有很多干扰. 是时候将这个脚本与LLDB联合起来得到一些主执行文件的相应输出了.

用LLDB限定范围

在starter文件夹中有一个LLDB的python脚本, 这个脚本创建了一个DTrace脚本然后用你刚才实现的逻辑运行这个脚本. 你只可以在第一个地方使用这个脚本.但是这个不太让人开心. 这个文件的名字叫做snoopie.py. 将这个文件拷贝到你的~/lldb目录下.如果你看过第二十二章“SB Examples, Improved Lookup”, 那么在~/lldb目录下应该有一个lldbinit.py文件., 它会为你自动加载这个目录下所有的脚本. 如果你跳过了那一章, 那么你则需要在你的~/.lldbinit文件中假如下面这行代码:

command script import ~/lldb/snoopie.py 你将会使用这个DTrace脚本中仅仅值追只追踪属于VCTransitions可执行文件中的Objective-C/dynamic Swift 代码的创造性解决方案过滤出相应代码.通常情况下, 当窥探的代码在framework中, 我经常会抓取加载到内存里的__TEXT 部分的模块然后对比__TEXT前后的指令指针边界(这一部分的内存负责执行代码).如果这个指令指针在上下边界之间然后你就可以假定你想要用Dtrace追踪的代码就是它. 不幸的是, 在objc_msgSend之后, 这个阻塞点会被应用到所有模块中的Objective-C代码.这就意味着你可以依靠指令指针来告诉自己你在哪个模块中. 不然, 你就需要通过仅仅包含在主执行文件中__DATA部分的一个类的独立地址来得到自己当前在哪个模块中. 回到你之前的VCTransitions Xcodex项目里. 构建并运行, 停止执行然后进到LLDB中. 然后输入下面的内容:

(lldb) p/x (void *)NSClassFromString(@"ObjCViewController") 你将会得到ObjCViewController类的地址:

(void *) $0 = 0x000000010db34080 用这个地址检查一下这些事在内存中的位置:

(lldb) image lookup -a 0x000000010db34080 你会得到一些类似下面的输出:

Address: VCTransitions[0x0000000100012080] (VCTransitions.__DATA.__objc_data + 40) Summary: (void *)0x000000010db34058 因此, 你可以推断出这个类在VCTransitions __DATA部分里的__objc_data 子部分里. 你将会使用LLDB Python模块来找出这个__DATA 分段的上下边界. 现在你会用好用而古老的script命令来找出你怎样通过LLDB创建这些代码. 回到LLDB中, 输入下面的内容:

(lldb) script path = lldb.target.executable.fullpath 这回给你一个代表可执行文件VCTransitions的SBFileSpec, 而且会将SBFileSpec复制给变量path. 打印出这个path确认它是有效的:

(lldb) script path 你将会得到可执行文件的完整路径.你可以用这个路径从SBTarget中获取到正确的SBModule.在LLDB中输入下面的内容:

(lldb) script print lldb.target.module[path] 你将会获得代表主可执行文件的SBModule. 在SBModule中有一个SBSections.你可以用sections属性获取到SBModule中所有的sections, 或者你可以用section[index]获取摸个指定的section. 是的, 那个属性遵循Python的__getitem__格式. 在LLDB中输入下面的内容:

(lldb) script print lldb.target.module[path].section[0] 你将会得到一些类似下面的内容:

[0x0000000000000000-0x0000000100000000) VCTransitions.__PAGEZERO 这种__getitem__的实现也可以将SBSection作为一个目录.因此你也可以访问像下面这样访问__PAGEZEROsection:

(lldb) script print lldb.target.module[path].section['__PAGEZERO'] 这就意味着你可以轻松的访问__DATA SBSection像下面这样:

(lldb) script print lldb.target.module[path].section['__DATA'] 酷, 这是可行的. 将这个SBSection复制给一个叫section的变量, 像这样:

(lldb) script section = lldb.target.module[path].section['__DATA'] 现在你拥有了一个正确的section的引用. 这里有一些你可以细分的subsections, 但是你可能想抓取完整的section, 因为他们在内存中是一个连续的区域. 从section中获取load address, 想下面这样:

(lldb) script section.GetLoadAddress(lldb.target) 这将会打印出起始位置.同时抓取你所在位置的size:

(lldb) script section.size 图片.png

那么这些内容给了你哪些信息呢?你可以创建一个DTrace判断句检查一下这个类是否在内存中这些值的中间. 如果在, 执行这个Dtrace 动作.如果他们不在, 忽略. 让我们动手把它实现出来!

修复snoopie脚本

正如它表明的, 这个snoopie脚本是可以运行的, 因此你只需要添加一些逻辑判断并过滤出实例. 打开~/lldb/snoopie.py文件, 然后找到generateDTraceScript函数. 移除dataSectionFilter = ...这一行, 然后添加下面的代码:

target = debugger.GetSelectedTarget() path = target.executable.fullpath section = target.module[path].section['__DATA'] start_address = section.GetLoadAddress(target) end_address = start_address + section.size dataSectionFilter = '''{} <= *((uintptr_t *)copyin(arg0, sizeof(uintptr_t))) && *((uintptr_t *)copyin(arg0, sizeof(uintptr_t))) <= {} '''.format(start_address, end_address) 有趣的一点是, 你带的arg0参数和如果(并且只在如果)arg0比0x100000000大的情况下解引用这个参数, 这表明内存中有一个有效的实例.就是这样!不需要更多的代码!你已经做完了! 保存一下你做的内容, 跳转到LLDB控制台中, 在LLDB中要么使用你自定义的reload_script命令要么手动的输入script import ~/.lldbinit重新加载这个内容. 加载成功之后, 在LLDB中, 试着运行一下:

(lldb) snoopie 将这个内容粘贴到终端窗口中并运行. 现在Dtrace只会解析你(精简过的)主可执行文件中的代码.

图片.png

尽情的享受这个脚本在你电脑上其他APP上的表现吧!

我们为什么要学这些?

在最后会给你留一些作业.这个脚本不能够很好的适配Objective-C的分类. 例如, 这里可能有一个类是在主文件中用Objective-C的分类在不同的模块中实现的,. 你需要用一些创造性的方法检查一下Objective-C selector 的 objc_msgSend是否在主可执行文件中实现了. 此外, 你当前代码中的printf无法指明arg0是一个类方法或者不是一个类方法. 你需要进到内存中去弄清楚如何检查arg0参数是一个类或者仅仅是一个实例. 你怎样才能实现上面的内容呢? • 如果arg0是一个类的实例, 那么isa指针就会指向non-meta类. • 如果arg0是一个类, 那么isa指针就会指向meta类. • 查看class_isMetaClass的汇编来弄明白一个类中的哪一个值表明了它是一个meta类或者不是一个meta类. 一旦你跳到内存中找到了决定一个类是不是meta类的方法, 复制你Dtrace脚本中class_isMetaClass的逻辑.因为这可能是类的实例或者是类对象本身, 你可以在Dtrace脚本中使用类似下面的三元运算符:

this->isMeta = ... // logic here this->isMetaChar = this->isMeta ? '+' : '-' printf("0x%016p %c[%s %s]\n", arg0, this->isMetaChar, this->classname, this->selector); 额...isMetaChar. 在将来的某一天它会成为一个口袋妖怪(Pokémon)的名字. 祝你好运!