BAD_ACCESS在什么情况下出现?
- 访问了野指针,比如对一个已经释放的对象执行了release、访问已经释放对象的成员变量或者发消息。
- 死循环
EXC_BAD_ACCESS 的本质
在C和Objective-C中,你一直在处理指针。指针无非是存储另一个变量的内存地址的变量。当您向一个对象发送消息时,指向该对象的指针将会被引用。这意味着,你获取了指针所指的内存地址,并访问该存储区域的值。
当该存储器区域不再映射到您的应用时,或者换句话说,该内存区域在你认为使用的时候却没有使用,也就是说该内存区域是无法访问的。 这时内核会抛出一个异常( EXC ),表明你的应用程序不能访问该存储器区域(BAD ACCESS) 。
当你碰到EXC_BAD_ACCESS ,这意味着你试图发送消息到的内存块,但内存块无法执行该消息。但是,在某些情况下, EXC_BAD_ACCESS是由被损坏的指针引起的。每当你的应用程序尝试引用损坏的指针,一个异常就会被内核抛出。
如何调试BAD_ACCESS错误
1.重写object的respondsToSelector方法,实现出现EXEC_BAD_ACCESS前访问的最后一个object
2.僵尸调试模式:通过 Zombie
3.设置全局断点快速定位问题代码所在行
4.Xcode 7 已经集成了BAD_ACCESS捕获功能:Address Sanitizer。
下面会对着四种方法一一介绍
1.重写object的respondsToSelector方法,实现出现EXEC_BAD_ACCESS前访问的最后一个object
2.僵尸调试模式:通过 Zombie
僵尸听起来有点戏剧性,但它实际上是为帮助我们调试 EXC_BAD_ACCESS 功能而取得一个伟大的名字。让我来解释它是如何工作的。
在Xcode中,您可以启用僵尸对象,这意味着被释放的对象将会以僵尸的形式被保留。换言之,保留释放的对象就是为了调试。这里没有涉及任何魔法。如果您向僵尸对象发送消息,你的应用程序将会由于 EXC_BAD_ACCESS 而崩溃。
这样做的好处在于,让EXC_BAD_ACCESS难以调试的原因是,你不知道你的应用程序试图访问哪个对象。僵尸对象在许多情况下解决这个问题。通过保留已释放的对象,Xcode可以告诉你你试图访问哪个对象,这使的查找问题原因容易得多。
在Xcode中启用僵尸对象是很容易的。注意,这可能会因的Xcode的版本而不同的。以下方法适用于Xcode的8,单击左上角的Edit Scheme,并选中Edit Scheme。
在左侧选中Run ,在上方打开 Diagnostics选项。要启用僵尸对象,勾选 Enable Zombie Objects选框。
这时如果遇到 EXC_BAD_ACCESS ,在Xcode的控制台输出,会告诉你问题出在哪里。
2015-08-12 06:31:55.501 Debug[2371:1379247] -[ChildViewController respondsToSelector:] message sent to deallocated instance 0x17579780
在上面的例子中, 告诉我们, respondsToSelector的消息:被发送到一个僵尸对象。然而,僵尸对象不再是ChildViewController类的一个实例。以前分配给ChildViewController实例的内存区域不再映射到您的应用程序。这为你了解问题产生的根本原因提供一个不错的建议。
不幸的是,僵尸对象将无法保存您的一天每次崩溃的EXC_BAD_ACCESS的记录。既然僵尸对象没有这些方法,那么你可以采取其他的方法进行一些适当的分析。
3.设置全局断点快速定位问题代码所在行
4.Xcode 7 已经集成了BAD_ACCESS捕获功能:Address Sanitizer。 用法如下:在配置中勾选✅Enable Address Sanitizer
顺便对 AddressSanitizer 和 Zombie 做些比较
原理
-
zombie
:
zombie的原理是用生成僵尸对象来替换dealloc的实现,当对象引用计数为0的时候,将需要dealloc的对象转化为僵尸对象。如果之后再给这个僵尸对象发消息,则抛出异常,并打印出相应的信息,调试者可以很轻松的找到异常发生位置。 -
AddressSanitizer
:
AddressSanitizer的原理是当程序创建变量分配一段内存时,将此内存后面的一段内存也冻结住,标识为中毒内存。如图所示,黄色是变量所占内存,紫色是冻结的中毒内存。
当程序访问到中毒内存时(越界访问),就会抛出异常,并打印出相应log信息。调试者可以根据中断位置和的log信息,识别bug。如果变量释放了,变量所占的内存也会标识为中毒内存,这时候访问这段内存同样会抛出异常(访问已经释放的对象)。
适用性
了解原理之后我们可以大概猜到Zombie和AddressSanitizer的适用性,不过一切还得以实验结果为准:
实验后发现AddressSanitizer比Zombie拥有更强大的捕获能力,特别是在malloc对象和内存越界方面,zombie几乎无能为力。如果在debug的时候无法捕获异常,上线之后crash log中概率性的EXC_BAD_ACCESS简直是一种灾难。
缺陷
上面研究发现AddressSanitizer比zombie更有优势,那么AddressSanitizer有什么缺陷呢?
- AddressSanitizer可能会没有log,不过会在访问中毒内存的代码处断住,这倒是对debug影响不大
- 使用AddressSanitizer除了分配对象的内存之外,还需要额外的内存,这会导致App内存大量增加,用起来有可能会比较卡
虽然AddressSanitizer有一些缺陷,但是总的来说AddressSanitizer还是一个非常好用的debug工具。
AddressSanitizer使用
在了解AddressSanitizer的功能之后,我们来看看AddressSanitizer用。
AddressSanitizer的使用其实非常简单,在Xcode上方选择设备的地方,点击工程名字,选择Edit Scheme.
在Diagnostics中选中enable address sanitizer即可。
AddressSanitizer开启之后,在debug过程中,如果遇到EXC_BAD_ACCESS的问题,Xcode会自动中断,抛出异常
其他compiler flags
实际AddressSanitizer很早以前就有了,只是没在Xcode中集成而已。除了AddressSanitizer还有很多其他的compiler flags,undefined-trap就是其中的一种。undefined-trap的功能也非常强大,它可以检测出程序中的不明确行为,如数据溢出等。
下面我们以undefined-trap举例,看看怎么用其他的compiler flags:
在Build Settings中的Custom Compiler Flags下为other C Flags添加-fsanitize=undefined-trap -fsanitize-undefined-trap-on-error
完成undefined-trap的设置之后,当程序的数据发生溢出行为时,系统就会抛出异常。
End
经过ARC的洗礼之后,普通的访问释放对象产生的EXC_BAD_ACCESS已经大量减少了,现在出现的EXC_BAD_ACCESS有很大一部分来自malloc的对象或者越界访问。简单的敌人已经被干掉,剩下的都是难缠的对手了。还好Apple给我们升级了装备,以后遇到EXC_BAD_ACCESS应该不用那么心惊胆战了吧?
Reference
Xcode7中你一定要知道的炸裂调试神技
在Xcode 7上直接使用Address Sanitizer
Clang 3.8 documentation
参考 出处