让我们来搞崩 Cocoa 吧(黑暗代码)

Let’s Build系列文章是这个博客中我最喜欢的部分。但是,有时候搞崩程序比编写它们更有趣。现在,我将要开发一些好玩且不同寻常的方式去让 Cocoa 崩溃。

带有 NUL 的字符串

NUL(译者:应该为 ”) 字符在 ASCII 和 Unicode 中代表 0,是一个不寻常的麻烦鬼。当在 C 字符串中时,它不作为一个字符,而是一个代表字符串结束的标识符。在其他的上下文环境中,它就会跟其他字符一样了。

当你混合 C 字符串和其它上下文环境,就会产生很有趣的结果。例如:NSString 对象,使用 NUL 字符毫无问题:

如果我们仔细的话,我们可以使用 lldb 打印它:

然而,展示这个字符串更为典型的方式是,字符串被当做 C 字符串在某个点结束。由于 ” 字符意味着 C 字符串的结尾,因此字符串会在转换时缩短:

原始的字符依然包含预计的字符数量:

对这个字符串进行操作会让你真正感到困惑:

如果你不知道字符串的中间包含一个 NUL ,这类问题会让你感到这个世界满满的恶意。

一般来说,你不会遇到 NUL 字符,但是它很有可能通过加载外部资源的数据进来。-initWithData:encoding: 会很轻易地读入零比特并且在返回的 NSString 中产生 NUL 字符。

循环容器

这里有一个数组:

这里有一个包含其他数组的数组

目前为止,看起来还不错。现在我们让一个数组包含自身:

猜猜会打印出什么?

以下就是调用堆栈的信息(译者:bt 命令为打印调用堆栈的信息):

 

这里还删除了上千个栈帧。描述方法无法处理递归容器,所以它持续尝试去追踪到“树”的结束,并最终发生异常。

我们可以用它跟自身比较对等性:

这姑且看起来是 YES。让我们创造另一个结构上相同的数组 b 然后用 a 和它比较:

哎呦:

对等性检查同样也不知道如何处理递归容器。

循环视图

你可以用NSView实例做同样的实验:

为了让这个程序崩溃,你只需要尝试去显示视窗。你甚至不需要打印一个描述或者做对等性比较。当试图去显示视窗时,应用就会因尝试追踪底部的视图结构而崩溃。

滥用 Hash

让我们创建一个实例一直等于其他类的类 AlwaysEqual,但是 hash 值并不一样:

这显然违反了 Cocoa 的要求,当两个对象被认为是相等时,他们的 hash 应该总是返回相等的值。当然,这不是非常严格的强制要求,所以上述代码依然可以编译和运行。

让我们添加一个实例到 NSMutableSet 中:

这产生了一个有趣的日志:

QQ截图20151113150646.png

每次运行都不能保证一样,但是综合看起来就是这样。addObject:通常先添加一个新对象,然后在更多的对象添加进来的时候很少成功,最后顶部只有三个对象。现在这个集合包含三个看起来是独一无二的对象,而且看起来应该不会包含更多的对象了。所以,在重写 isEqual: 时总是应该重写 hash方法。

滥用 Selector

Selector 是一个特殊的数据类型,在运行期用于表示方法名。在我们习惯中,它们必须是独一无二的字符串,尽管它们并不是严格地要求是字符串。在现在的 Objective-C 运期间,它们是字符串,并且我们都知道利用 Selector 去搞崩程序是很好玩儿的事。

马上行动,下面就是一个例子:

编译和运行后,在运行期产生了很令人费解的错误:

通过创建奇怪的 selector,会产生真正奇怪的错误:

你甚至让错误看起来像是停止响应完整信息的 NSObject :

显然,这不是真正的 alloc selector,它是一个碰巧指向一个包含 “alloc” 字符串的伪装 selector。但是,runtime 依然把它打印为 alloc 。

伪造对象

虽然现在越来越复杂,但是 Objective-C 对象依然是分配给所有对象类的大内存中的一小块内存。在这样的思维下,我们就可以创造一个伪造对象:

这些伪造对象也完全能工作:

上述代码不仅可以运行,并且打印日志如下:

QQ截图20151113150601.png

可惜的是,看起来所有伪造对象都是以同样的地址结束的。但是还是可以继续工作。好了,当你退出方法并且 autorelease pool 试图去清理时:

因为这些伪造对象没有合适分配内存,所以一旦autorelease pool 试图在方法返回时去操作它们,就会出现严重的错误,并且内存会被重写。

KVC

下面是一个类数组:

下面一个这些类实例的数组:

16.png

键值编码并不意味着要这样使用,但是看起来也可以正常运行。

调用者检查

编译器的 builtin __builtin_return_address 方法可以返回调用你的代码的地址:

因此,我们可以获取调用者的信息,包括它的名字:

通过这个,我们可以做一些穷凶极恶的事(译者:并不认为是穷凶极恶的事,反而可作为调用动态方法的一种可选方法,虽然并不可靠),比如说完全可以根据不同的调用者调用合适的方法:

这里是一些测试的代码:

当然,这种方式不是很可靠,因为 __CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__是 Apple 的内部符号,并且很有可能在未来修改。

Dealloc Swizzle

让我们使用 swizzle (方法调配技术)去调配-[NSObject dealloc]到一个不做任何事情的方法。在 ARC 下获得 @selector(dealloc) 有点棘手,因为我们不能直接读取它:

现在我们来欣赏这个例子所产生的混乱(简直就是代码界的黑暗料理):

调配 dealloc 方法导致这个代码完美且合理地疯狂泄露,因为对象不能被摧毁。

总结

用全新和有趣的方法搞崩 Cocoa 能够提供无尽的娱乐性。这也在真实的代码里体现出来了。想起我第一次遇到字符串中嵌入了 NUL ,那是充满痛苦的调试经历。其他只是为了好玩和适当的教学目的。

就是这些了!如果你有任何想要讨论的问题,可以给我发送邮件(mike@mikeash.com)。

1 1 收藏 评论

相关文章

可能感兴趣的话题



直接登录
跳到底部
返回顶部