EXC_BAD_ACCESS和僵尸对象原理

在bugly上经常会抓到EXC_BAD_ACCESS崩溃, 当遇到了EXC_BAD_ACCESS异常,意味着访问了一个已经被释放的内存区域。

僵尸对象:内存已经被回收的对象, 但是还是有指针指向该区域。

野指针:指向僵尸对象的指针,向野指针发送消息会导致崩溃。野指针错误形式在Xcode中通常表现为:EXC_BAD_ACCESS,因为你访问了一块已经不属于你的内存。

指针层面理解:从C、OC语言来解释,我们平时所操作的对象其实是一个指针,指针是指向另一块存储区域的变量。当向一个对象发送消息,指向这个对象的指针需要被使用,也就是你获得了指针指向的内存地址并且可以访问该内存块的值。当系统不再为你映射该内存块时,换句话说,该内存块已经不能够被你所使用,则不可以再次访问该内存块。 如果再次访问这块内存,发生这种情况时,内核会发送一个异常(EXC),表明您的应用程序无法访问该内存块(BAD ACCESS)。

总之,当遇到了EXC_BAD_ACCESS异常,意味你尝试向一个块已经不能执行这个消息的内存块发送消息。在某些情况下,访问野指针也会导致EXC_BAD_ACCESS。当应用程序尝试去使用一个野指针的时候,EXC_BAD_ACCESS就会被内核抛出。

调试EXC_BAD_ACCESS

因为当内存块不能被应用程序所使用的时候,并不会立即出现crash,所以导致调试EXC_BAD_ACCESS很麻烦。

对于野指针也是同样的,应用程序不会立即崩溃如果存在野指针,只有当应用程序试图去使用野指针的时候才会崩溃。

僵尸对象

僵尸对象听起来有点奇怪,但确实是可以用来帮助调试EXC_BAD_ACCESS问题。非常有效!

Xcode中,可以开启僵尸对象模式。

之所以调试EXC_BAD_ACCESS非常麻烦是因为不知道应用程序要访问哪个对象。僵尸对象可以解决这个问题。通过让已经被释放的对象存活,Xcode可以告诉我们正在访问哪个被释放的对象,进而找到是什么原因引起的。

Zombie Object 有什么用

  • 僵尸对象一种用来检测内存错误(EXC_BAD_ACCESS)的对象,它可以捕获任何对尝试访问坏内存的调用。
  • 如果给僵尸对象发送消息时,那么将在运行期间崩溃和输出错误日志。通过日志可以定位到野指针对象调用的方法和类名。

僵尸对象的原理

开启Zombie Object检测后,对象调用dealloc方法会发生变化

1.新建一个终端工程(Command Line Tool),在MRC环境下, 开启Zombie Objects:

void printClassInfo(id obj)
{
    Class cls = object_getClass(obj);
    Class superCls = class_getSuperclass(cls);
    NSLog(@"self:%s - superClass:%s", class_getName(cls), class_getName(superCls));
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        People *aPeople = [People new];
        
        NSLog(@"before release!");
        printClassInfo(aPeople);
      
        [aPeople release];
        
        NSLog(@"after release!");
        printClassInfo(aPeople);
    }
    return 0;
}

2.运行程序,查看打印信息。从打印信息可以看到开启僵尸对象检测后,People释放后所属类变成了_NSZombie_People。如此可得对象释放后会变成僵尸对象,保存当前释放对象的内存地址,防止被系统回收。

ZombieObjectDemo[1357:84410] before release!
ZombieObjectDemo[1357:84410] self:People - superClass:NSObject
ZombieObjectDemo[1357:84410] after release!
ZombieObjectDemo[1357:84410] self:_NSZombie_People - superClass:nil

3、结下来打开instruments ->Zombies ,查看dealloc 究竟做了什么。点击运行,查看Call trees。结果如下图,从dealloc的调用可以知道:Zombie Objects hook 住了对象的dealloc方法,通过调用自己的__dealloc_zombie方法来把对象进行僵尸化。在Runtime源码NSObject.mm文件中dealloc方法注释中也有说明这一点。如下:

// Replaced by NSZombies
- (void)dealloc {
    _objc_rootDealloc(self);
}

由对象dealloc方法调用栈( Call Tree )很好的验证了步骤3的打印信息。那么这个过程又是怎么样的?继续探索。

Zombie Object的生成过程是怎么样的

创建__dealloc_zombie符号断点来看一探究竟

CoreFoundation`-[NSObject(NSObject) __dealloc_zombie]:
    0x7fff3fa2dee7 <+23>:  leaq   0x5a59c4a2(%rip), %rax    ; __CFZombieEnabled
    0x7fff3fa2defa <+42>:  callq  0x7fff3fa7d930            ; symbol stub for: object_getClass
    0x7fff3fa2df0a <+58>:  callq  0x7fff3fa7d486            ; symbol stub for: class_getName
    0x7fff3fa2df12 <+66>:  leaq   0x237d1b(%rip), %rsi      ; "_NSZombie_%s"
    0x7fff3fa2df2b <+91>:  callq  0x7fff3fa7d8b8            ; symbol stub for: objc_lookUpClass
    0x7fff3fa2df38 <+104>: leaq   0x2376a9(%rip), %rdi      ; "_NSZombie_"
    0x7fff3fa2df3f <+111>: callq  0x7fff3fa7d8b8            ; symbol stub for: objc_lookUpClass
    0x7fff3fa2df4d <+125>: callq  0x7fff3fa7d870            ; symbol stub for: objc_duplicateClass
    0x7fff3fa2df61 <+145>: callq  0x7fff3fa7d86a            ; symbol stub for: objc_destructInstance
    0x7fff3fa2df6c <+156>: callq  0x7fff3fa7d948            ; symbol stub for: object_setClass 

从此处断点可以大概看出Zombie Object 的生成过程。_NSZombie_%s验证了开启僵尸对象检测后的对象所指向的类。从这个调用栈也可以说明系统开启僵尸对象检测后不会释放该对象所占用的内存,只是释放了与该对象所有的相关引用。让runtime源码告诉你:

/***********************************************************************
* object_dispose
* fixme
* Locking: none
**********************************************************************/
id object_dispose(id obj)
{
    if (!obj) return nil;

    objc_destructInstance(obj);    
    free(obj);

    return nil;
}

上面是为开启僵尸对象检测对象释放的调用过程,开启僵尸对象检测后将没有 free(obj) 这一步的调用,而是执行objc_destructInstance(obj)方法后就直接return了。我们也可以看看objc_destructInstance到底都干了些什么。从其注释可以知道该方法做了下面几件事:【C++ destructors【ARC ivar cleanup】 【Removes associative references】并没有释放其内存。

//***********************************************************************
* objc_destructInstance
* Destroys an instance without freeing memory. 
* Calls C++ destructors.
* Calls ARC ivar cleanup.
* Removes associative references.
* Returns `obj`. Does nothing if `obj` is nil.
**********************************************************************/
void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj);
        obj->clearDeallocating();
    }

    return obj;
}

从汇编的调用顺序可以大概总结出僵尸对象的生成过程,如下:

//1、获取到即将deallocted对象所属类(Class)
Class cls = object_getClass(self);

//2、获取类名
const char *clsName = class_getName(cls)

//3、生成僵尸对象类名
const char *zombieClsName = "_NSZombie_" + clsName;

//4、查看是否存在相同的僵尸对象类名,不存在则创建
Class zombieCls = objc_lookUpClass(zombieClsName);
if (!zombieCls) {
//5、获取僵尸对象类 _NSZombie_
Class baseZombieCls = objc_lookUpClass(“_NSZombie_");

//6、创建 zombieClsName 类
zombieCls = objc_duplicateClass(baseZombieCls, zombieClsName, 0);
}
//7、在对象内存未被释放的情况下销毁对象的成员变量及关联引用。
objc_destructInstance(self);

//8、修改对象的 isa 指针,令其指向特殊的僵尸类
objc_setClass(self, zombieCls);

Zombie Object是如何被触发的

1、再次调用 [aPeople release] 可以看到程序断在___forwarding___,从此处的汇编代码中可以看到关键字_NSZombie_,在调用abort( )函数退出进程时会有对应的信息输出@"*** -[%s %s]: message sent to deallocated instance %p"。所以可以大概猜出系统是在消息转发过程中做了手脚。

CoreFoundation`___forwarding___:
    0x7fff3f90b1cd <+269>:  leaq   0x35a414(%rip), %rsi      ; "_NSZombie_"

为此也可以大概总结出它的调用过程,如下:

//1、获取对象class
Class cls = object_getClass(self);

//2、获取对象类名
const char *clsName = class_getName(cls);

//3、检测是否带有前缀_NSZombie_
if (string_has_prefix(clsName, "_NSZombie_")) {
//4、获取被野指针对象类名
  const char *originalClsName = substring_from(clsName, 10);
 
 //5、获取当前调用方法名
 const char *selectorName = sel_getName(_cmd);
  
 //6、输出日志
 Log(''*** - [%s %s]: message sent to deallocated instance %p", originalClsName, selectorName, self);

 //7、结束进程
 abort();
}

总结下僵尸对象的原理

  1. 开启僵尸对象后,系统在回收对象时,不将其真的回收,不会执行正常的dealloc,而是把它转化为僵尸对象。这种对象所在的内存无法重用,因此不会遭到重写,所以将随机变成必然。

  2. 在执行僵尸对象的dealloc时, 根据此类创建一个新类_NSZombie_Class,_NSZombie_Class是一个空类,没有任何方法,由系统模板类_NSZombie_复制一份出来,复制效率更高

  3. 系统会修改对象的 isa 指针,令其指向刚刚创建的特殊僵尸类,从而使该对象变为僵尸对象。

  4. 后续僵尸类收到消息时, 能够响应所有的选择器,响应方式为:打印一条包含消息内容及其接收者的消息,然后终止应用程序。

EXC_BAD_ACCESS是一个比较常见的但同时也非常麻烦的问题。 导致这个崩溃的地方可能会有很多,想要解决这个问题得一个一个找到在处理.

iOS Zombie Objects(僵尸对象)原理探索

iOS-底层原理:野指针探测

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
iOS中的exc_bad_access通常是由于访问了无效的内存地址而触发的错误。要解决这个问题,我们可以遵循以下几个步骤: 1. 检查Crash日志:首先,我们应该查看Crash日志以了解问题的具体原因。Crash日志将显示出错的位置以及相关的堆栈信息,这有助于我们确定问题的根源。 2. 使用断点:如果我们知道大概出错的位置,可以在代码中设置断点来逐步调试。这样,我们可以在错误出现前暂停应用程序的执行,从而更好地分析错误。 3. 检查空指针:空指针访问是常见的exc_bad_access错误。我们应该检查代码中的指针变量是否为空并确保在使用前进行了正确的初始化。 4. 检查内存释放:内存管理是另一个常见的exc_bad_access错误的原因。我们需要确保在释放内存之后不再访问已释放的内存。可以使用工具如Instruments来检测内存泄漏和野指针。 5. 使用ARC(自动引用计数):如果我们的应用程序使用了手动管理内存,那么我们应该考虑迁移到ARC来减少内存管理错误的发生。ARC会自动处理内存释放,从而降低了内存相关的问题。 6. 避免循环引用:循环引用也可能导致exc_bad_access错误。我们应该小心使用强引用和弱引用,以避免循环引用的产生。 7. 更新代码库和依赖项:如果我们使用的是第三方库或依赖项,那么我们应该确保它们是最新的版本并且与我们应用程序的其他部分兼容。有时,exc_bad_access错误可能是由于库或依赖项的错误导致的。 总之,解决exc_bad_access错误需要仔细检查代码和内存管理,并根据具体情况进行调试和修复。通过遵循上述步骤,我们可以更好地理解问题并找到适当的解决方案。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值