再探iOS中的野指针问题
野指针
野指针本质
: 一个指向已经删除的对象或者受限制内存区域的指针!!!
OC野指针产生的原因: OC对象的dealloc(release)执行后系统并不是立马释放内存, 而是标记内存为等待释放.
和野指针关联最多的两个异常或者信号是:
- EXC_BAD_ACCESS - Mach异常 - 不能访问的内存
- SIGSEGV - 信号 - 段错误, 访问未分配内存, 写入没有写权限的内存等
iOS开发如果OC对象的内存管理理解不透彻很容易出现野指针. 而以下情况需要格外注意, 常见的导致野指针的场景如下:
- OC对象的
@property
的内存管理修饰符的选择- 例如
@property (nonatomic, unsafe_unretained) id obj;
OC对象尽量使用copy/strong/weak
进行内存管理的修饰
- 例如
- 关联对象中的内存管理属性误用. 例如
objc_setAssociatedObject
方法中该用OBJC_ASSOCIATION_RETAIN_NONATOMIC
修饰的对象误用成OBJC_ASSOCIATION_ASSIGN
.- 内存管理问题如1
CoreFoundation
中对象的内存管理问题- KVO的addObserver与removeObserver不匹配
- 多线程场景, 由于ARC中对OC对象的处理在底层会进行
retain
和release
, 因此一定需要注意内存访问临界区- 多线程导致的重复release问题
- MRC
野指针探测实战
最常见的方式就是:
- Zombie Object
- 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];
});
核心逻辑如下:
- 使用method swizzling hook NSObject/NSProxy 两个根类的
dealloc
方法. - 在对象释放时, 调用
hook_dealloc
方法时, 不是真释放, 而是将obj对象的isa
指向LXDZombieProxy
类型----object_setClass(obj, [LXDZombieProxy class]);
- 后续有消息发送给
obj
时,LXDZombieProxy
内部会响应, 这个类是一个NSProxy
的子类, 在响应时候会记录上下文信息.
但是这个实现与Xcode中的Zombie Object
的实现有一些出入, 主要原因是:
- 系统的API调用dealloc时, 会
Malloc Scribble结合Zombie Objects
这个实现逻辑可以参考github.com/fangjinfeng…
由于对象释放从层次来说, 可能有如下几个API:
- NSObject的dealloc
- runtime的object_dispose()
- 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()
核心思想如下:
- 使用fishhook hook系统的 free方法, 并持有原来的
orig_free()
- 判断需要free的
void *p
的大小是否超过预定义的sYHCatchSize
- 如果超过, 可以将p指针指向的内存区域填充成
0x55
-- 类似Malloc Scribble
- 然后, 将对象的前8个字节(可以存储isa指针的大小)强制设置成
MOACatcher: NSProxy
的isa指针!!!
ps: 但是实践起来好像效果并不明显, 有大神清楚原因吗!!!