再探iOS中的野指针问题

2,128 阅读5分钟

再探iOS中的野指针问题

野指针

野指针本质: 一个指向已经删除的对象或者受限制内存区域的指针!!!

OC野指针产生的原因: OC对象的dealloc(release)执行后系统并不是立马释放内存, 而是标记内存为等待释放.

和野指针关联最多的两个异常或者信号是:

  1. EXC_BAD_ACCESS - Mach异常 - 不能访问的内存
  2. SIGSEGV - 信号 - 段错误, 访问未分配内存, 写入没有写权限的内存等

iOS开发如果OC对象的内存管理理解不透彻很容易出现野指针. 而以下情况需要格外注意, 常见的导致野指针的场景如下:

  1. OC对象的@property的内存管理修饰符的选择
    • 例如@property (nonatomic, unsafe_unretained) id obj; OC对象尽量使用copy/strong/weak进行内存管理的修饰
  2. 关联对象中的内存管理属性误用. 例如objc_setAssociatedObject方法中该用OBJC_ASSOCIATION_RETAIN_NONATOMIC修饰的对象误用成OBJC_ASSOCIATION_ASSIGN.
    • 内存管理问题如1
  3. CoreFoundation中对象的内存管理问题
  4. KVO的addObserver与removeObserver不匹配
  5. 多线程场景, 由于ARC中对OC对象的处理在底层会进行retainrelease, 因此一定需要注意内存访问临界区
    1. 多线程导致的重复release问题
  6. MRC

野指针探测实战

最常见的方式就是:

  1. Zombie Object
  2. Malloc Scribble

Zombie Object 实现

对于Zombie Object实现可以参考https://github.com/sindrilin/LXDZombieSniffer中的实现.

核心代码如下:

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        swizzledDeallocBlock = [^void(id obj) {
            Class currentClass = [obj class];
            NSString *clsName = NSStringFromClass(currentClass);
            if ([__lxd_sniff_white_list() containsObject: clsName]) {
                __lxd_dealloc(obj);
            } else {
                NSValue *objVal = [NSValue valueWithBytes: &obj objCType: @encode(typeof(obj))];
                object_setClass(obj, [LXDZombieProxy class]);
                ((LXDZombieProxy *)obj).originClass = currentClass;
                
                dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(30 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                    __unsafe_unretained id deallocObj = nil;
                    [objVal getValue: &deallocObj];
                    object_setClass(deallocObj, currentClass);
                    __lxd_dealloc(deallocObj);
                });
            }
        } copy];
    });

核心逻辑如下:

  1. 使用method swizzling hook NSObject/NSProxy 两个根类的dealloc方法.
  2. 在对象释放时, 调用 hook_dealloc方法时, 不是真释放, 而是将obj对象的isa指向LXDZombieProxy类型----object_setClass(obj, [LXDZombieProxy class]);
  3. 后续有消息发送给obj时, LXDZombieProxy内部会响应, 这个类是一个NSProxy的子类, 在响应时候会记录上下文信息.

但是这个实现与Xcode中的Zombie Object的实现有一些出入, 主要原因是:

  1. 系统的API调用dealloc时, 会

Malloc Scribble结合Zombie Objects

这个实现逻辑可以参考github.com/fangjinfeng…

由于对象释放从层次来说, 可能有如下几个API:

  1. NSObject的dealloc
  2. runtime的object_dispose()
  3. C的free

核心的逻辑如下(有部分逻辑在xcode13上崩溃, 我这里简单修改了下):

// 类似 Aspects 中, 需要访问对象的 isa指针
typedef struct {
    Class isa;
} malloc_maybe_object_t;

#pragma mark -------------------------- Private  Methods
// 真实的free方法!
// https://juejin.cn/post/6895583288451465230 中有判断一个地址是否是一个OC对象的服务
void safe_free(void* p){
    // _unfreeQueue 是一个自定义的queue
    // unfreeCount 是当前queue中的缓存的OC对象的个数
    int unFreeCount = ds_queue_length(_unfreeQueue);
    // 两个条件阈值, 超过就释放部分持有的内存
    // 条件1: 持有的未释放的OC对象的个数
    // 条件2: 持有的未释放的所有内存的大小
    if (unFreeCount > MAX_STEAL_MEM_NUM*0.9 || unfreeSize>MAX_STEAL_MEM_SIZE) {
        free_some_mem(BATCH_FREE_NUM);
    }
    
    // 正是开始处理内存区域的数据
    // sizeof对象是将对象存储在内存中所需的空间. malloc_size是为其实际分配了多少空间(例如,在具有固定大小的池的内存分配系统中,根据使用的其他内存量,可能会为您分配不同的空间).
    /*
     当调用malloc(size)时,实际分配的内存大小大于size字节,这是因为在分配的内存区域头部有类似于struct control_block {    unsigned size;    int used;};这样的一个结构,如果malloc函数内部得到的内存区域的首地址为void *p,那么它返回给你的就是p + sizeof(control_block),而调用free(p)的时候,该函数把p减去sizeof(control_block),然后就可以根据((control_blcok*)p)->size得到要释放的内存区域的大小。这也就是为什么free只能用来释放malloc分配的内存,如果用于释放其他的内存,会发生未知的错误。

     作者:冯子畅
     链接:https://www.zhihu.com/question/20362709/answer/14897756
     来源:知乎
     著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
     */
    size_t memSiziee = malloc_size(p);
    if (memSiziee > sYHCatchSize) { // 内存区域足够大, 可以容纳一个 MOACatcher 对象
        malloc_maybe_object_t *obj = (malloc_maybe_object_t *)p;
        
        // objc_debug_isa_class_mask 在 arm64 中是 0x0000000ffffffff8ULL
        extern uint64_t objc_debug_isa_class_mask WEAK_IMPORT_ATTRIBUTE;
        Class origClass = (__bridge Class)((void *)((uint64_t)obj->isa & objc_debug_isa_class_mask));
        
        // 原来的逻辑, 直接强制转化成 objc_object* 指针, 然后 获取 isa!!
        // 但是现在不行了!!!
//            id obj= (id)p;
//            Class origClass= object_getClass(obj);
        // 判断是不是objc对象 ->
        char *type = @encode(typeof((id)obj)); // typeof是一个运算符
//        if (strcmp("@", type) == 0 && CFSetContainsValue(registeredClasses, origClass)) {
        if (strcmp("@", type) == 0  && origClass && CFSetContainsValue(registeredClasses, origClass)) {
            //1. 先全系用 0x55 填充
            memset(obj, 0x55, memSiziee);
            //2. 将前8个字节(isa 指针的size)强行填充成 sYHCatchIsa 的内容!!!
            memcpy(obj, &sYHCatchIsa, sizeof(void*));//把我们自己的类的isa复制过去
            object_setClass((id)obj, [MOACatcher class]);
            // 添加成一个size
            ((MOACatcher *)obj).originClass = origClass;
            __sync_fetch_and_add(&unfreeSize,(int)memSiziee);//多线程下int的原子加操作,多线程对全局变量进行自加,不用理线程锁了 -> 总共没有释放的内容
            ds_queue_put(_unfreeQueue, p); // 对象添加进入
        }else{
           orig_free(p);
        }
    }else{
       orig_free(p);
    }
}

大神选择了hook c层的free()核心思想如下:

  1. 使用fishhook hook系统的 free方法, 并持有原来的orig_free()
  2. 判断需要free的void *p的大小是否超过预定义的sYHCatchSize
  3. 如果超过, 可以将p指针指向的内存区域填充成0x55 -- 类似 Malloc Scribble
  4. 然后, 将对象的前8个字节(可以存储isa指针的大小)强制设置成MOACatcher: NSProxy的isa指针!!!

ps: 但是实践起来好像效果并不明显, 有大神清楚原因吗!!!

参考

  1. cloud.tencent.com/developer/a…
  2. cloud.tencent.com/developer/a…
  3. cloud.tencent.com/developer/a…
  4. www.jianshu.com/p/4c8a68bd0…