原文链接=https://mikeash.com/pyblog/friday-qa-2014-01-10-lets-break-cocoa.html
作者=Mikeash
原文日期=2014/01/10
译者:在传统的文章中,我们一直致力于如何编写高效稳定的代码,努力提高代码的鲁棒性。然而在本文中,我们将会改变一下思维方式,采用破坏的方式去挖掘 Cocoa 的一些特性,虽然文中作者表现出一种“病态”的破坏心理,但正因为有这种精神,通过文中那些黑暗代码,可以让我们更加深刻地理解 Cocoa 。
让我们编写系列文章是这个博客中我最喜欢的部分。但是,有时候搞崩程序比编写他们更有趣。现在,我将要开发一些好玩且不同寻常的方式去让 Cocoa 崩溃。
带有 NUL 的字符串
NUL(译者:应该为 ‘\0’) 字符在 ASCII 和 Unicode 中代表 0,是一个不寻常的麻烦鬼。当在 C 字符串中时,它不作为一个字符,而是一个代表字符串结束的标识符。在其他的上下文环境中,它就会跟其他字符一样了。
当你混合 C 字符串和其它上下文环境,就会产生很有趣的结果。例如:NSString 对象,使用 NUL 字符毫无问题:
|
|
如果我们认真的话,我们可以使用 lldb 打印它:
然而,展示这个字符串更为典型的方式是,字符串被当做 C 字符串在某个点结束。由于 ‘\0’ 字符意味着 C 字符串的结尾,因此字符串会在转换时缩短:
原始的字符已然包含预计的字符数量:
试图对这个字符串进行操作会让你真正感到困惑:
如果你不知道字符串的中间包含一个 NUL ,这类问题会让你感到这个世界满满的恶意。
一般来说,你不会遇到 NUL 字符,但是它很有可能通过加载外部资源的数据进来。-initWithData:encoding: 会很轻易地读入零比特并且在返回的 NSString 中产生 NUL 字符。
循环容器
这里有一个数组:
|
|
这里有一个数组包含其他的数据:
目前为止,看起来还不错。现在我们让一个数组包含自身:
|
|
猜猜会打印出什么?
|
|
以下就是调用堆栈的信息(译者:bt 命令为打印调用堆栈的信息):
这里还删除了上千个栈帧。描述方法无法处理递归容器,所以它持续尝试去追踪到“树”的结束,并最终发生异常。
我们可以用它跟自身比较对等性:
|
|
这姑且看起来是 YES。让我们创造另一个结构上相同的数组 b 然后用 a 和它比较:
|
|
很抱歉:
|
|
对等性检查同样也不知道如何处理递归容易。
循环视图
你可以用NSView实例做同样的实验:
为了让这个程序崩溃,你只需要尝试去显示视窗。你甚至不需要去打印一个描述或者做对等性比较。当试图去显示视窗时,应用就会由于尝试去追踪底部的视图结构而崩溃。
Hash Abuse
滥用 Hash
让我们创建一个实例一直等于其他类的类 AlwaysEqual,但是 hash 值并不一样:
这显然违反了 Cocoa 的要求,当两个对象被认为是相等时,他们的 hash 应该总是返回相等的值。当然,这不是非常严格的强制要求,所以上述代码依然可以编译和运行。
让我们添加一个实例到 NSMutableSet 中:
这产生了一个有趣的日志:
每次运行都不能保证一样,但是综合看起来就是这样。addObject:通常先添加一个新对象,然后在更多的对象添加进来的时候很少成功,最后顶部只有三个对象。现在这个集合包含三个看起来是独一无二的对象,而且看起来应该不会包含更多的对象了。所以,在重写 isEqual: 时总是应该重写 hash方法。
滥用 Selector
Selector 是一个特殊的数据类型,在运行期用于表示方法名。在我们习惯中,它们必须是独一无二的字符串,尽管它们并不是严格地要求是字符串。在现在的 Objective-C 运行期,它们是字符串,并且我们都知道利用 Selector 去搞崩程序是很好玩儿的事。
马上行动,下面就是一个例子:
当编译和运行之后,在运行期产生了很令人费解的错误:
|
|
通过创建奇怪的 selector,会产生真正奇怪的错误:
|
|
你甚至让错误看起来像是停止响应完整信息的 NSObject :
显然,这不是真正的 alloc selector,它是一个碰巧指向一个包含 “alloc” 字符串的伪装 selector。但是,runtime 依然把它打印为 alloc 。
伪造对象
虽然现在越来越复杂,但是 Objective-C 依然是分配给所有对象类的大内存中的一小块内存。在这样的思维下,我们就可以创造一个伪造对象:
这些伪造对象也完全能工作:
上述代码不仅可以运行并且打印日志如下:
可惜的是,看起来所有伪造对象都是以同样的地址结束的。但是还是可以继续工作的。好了,当你退出方法并且 autorelease pool 试图去清理时:
因为这些伪造对象没有合适分配内存,所以一旦autorelease pool 试图在方法返回时去操作它们,就会出现严重的错误,并且内存会被重写。
KVC
下面是一个类数组:
下面一个这些类实例的数组:
键值编码并不意味着要这样使用,但是看起来也可以正常运行。
调用者检查
编译器的 builtin __builtin_return_address 方法可以返回调用你的代码的地址:
|
|
因此,我们可以获取调用者的信息,包括它的名字:
通过这个,我们可以做一些穷凶极恶的事(译者:并不认为是穷凶极恶的事,反而可作为调用动态方法的一种可选方法,虽然并不可靠),比如说完全可以根据不同的调用者调用合适的方法:
这里是一些测试的代码:
当然,这种方式不是很可靠,因为 __CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__是 Apple 的内部符号,并且很有可能在未来修改。
Dealloc Swizzle
让我们使用 swizzle (方法调配技术)去调配-[NSObject dealloc]到一个不做任何事情的方法。在 ARC 下获得 @selector(dealloc) 有点棘手,因为我们不能直接读取它了:
|
|
现在我们坐下来欣赏这个例子所产生的混乱(简直就是代码界的黑暗料理):
调配 dealloc 方法导致这个代码完美且合理地疯狂泄露,因为对象不能在任何地方被摧毁。
总结
用全新和有趣的方法搞崩 Cocoa 能够提供无尽的娱乐性。这也在真实的代码里体现出来了。想起我第一次遇到字符串中嵌入了 NUL ,那是充满痛苦的调试经历。其他只是为了好玩和适当的教学目的。
That’s it for today! Come back next time for more fun and games.
Friday Q&A is driven by reader suggestions, as always, so if you have
something you’d like to see discussed here, send it in!