线上 Zombie 计划 – CF 目标监控
zombie 触发的溃散在缺少数据竞争的仓库时,定位起来相对比较棘手,尤其是 autorelease pool 触发的问题,或许是 pb 这种通用的 model 触发的问题,看不到任何定位问题的特征信息。
autorelease pool 的溃散仓库如下图所示:
0 libobjc.A.dylib _objc_release()
1 libobjc.A.dylib AutoreleasePoolPage::releaseUntil(objc_object**)()
2 libobjc.A.dylib _objc_autoreleasePoolPop()
3 libdispatch.dylib __dispatch_last_resort_autorelease_pool_pop()
4 libdispatch.dylib __dispatch_lane_invoke()
5 libdispatch.dylib __dispatch_root_queue_drain_deferred_wlh()
6 libdispatch.dylib __dispatch_workloop_worker_thread()
7 libsystem_pthread.dylib __pthread_wqthread()
zombie 问题本质上是 double-free 或许 use-after-free 的内存问题,因而处理这个问题的关键是取到第一次 free 的仓库,再结合溃散的仓库,确保这两个仓库对目标的拜访线程安全。对于 NSObject 目标,能够直接 hook dealloc 来记载 free 的仓库,CF 目标监控 free 比较 NSObject 会有一些难度,但是也必须包含在监控范围内,从处理线上 zombie 问题的经历来看,大多数难定位的 zombie 问题都是 __NSCFString 形成的,获取 CF 目标的 free 仓库是本文讨论的要点。需求留意的是,本文中说到的计划没有在线上进行验证,仅供讨论可行性,各位前辈假如能点拨一二,将不胜感激。
后续一些探究的点会包含:
- zombie 检测的计划选型: 修正 isa vs 保存 ptr 和 bt 映射联系。
- 仓库信息怎么存储查询。
- 目标怎么加权: autorelease 目标要点监控。
- 过滤: 线上计划最难的一个点,在线上做全量目标的监控存在很大的功能开销,过滤那些不可能产生 zombie 问题的目标,尽可能的确保在触发 zombie 溃散时,问题目标的 free 仓库已经成功记载。
CFRelease
CF 目标的开释经过 CFRelease 办法,CFRelease 最终会调用 CFAllocatorDeallocate 开释 CF 目标,CFRelease 这个办法不能被直接 hook 掉,而 deallocate 这个办法里边存在许多能够替换的钩子办法,比较容易找到记载 free 仓库的计划。
CFAllocatorDeallocate 删去部分 debug 代码后的详细实现:
void CFAllocatorDeallocate(CFAllocatorRef allocator, void *ptr) {
CFAllocatorDeallocateCallBack deallocateFunc;
// allocator 假如为空,则赋值 __CFGetDefaultAllocator
if (NULL == allocator) {
allocator = __CFGetDefaultAllocator();
}
__CFGenericValidateType(allocator, __kCFAllocatorTypeID);
// 校验 _cfisa,假如 allocator 不是经过 CFAllocatorCreate 创立的,而是一个自定义的
// malloc_zone_t 则直接调用 zone 的 free 办法。
if (allocator->_base._cfisa != __CFISAForTypeID(__kCFAllocatorTypeID)) {
return malloc_zone_free((malloc_zone_t *)allocator, ptr);
}
deallocateFunc = __CFAlloc // 假如是 CFAllocator 则调用 context 的 deallocate 办法
atorGetDeallocateFunction(&allocator->_context);
if (NULL != ptr && NULL != deallocateFunc) {
INVOKE_CALLBACK2(deallocateFunc, ptr, allocator->_context.info);
}
}
依据这个函数的语义,假如 allocator 的类型是 malloc_zone_t,则直接调用 zone 的 free 办法,假如 allocator 的类型是 CFAllocatorRef 则调用 _context 的 deallocate 办法。依据这个语义,我现在能想到的 hook 计划有如下 3 种。
计划 1: set malloc_zone_t
体系供给了 api 设置 default CFAllocator
void CFAllocatorSetDefault(CFAllocatorRef allocator);
但是在这个 api 里边把 malloc_zone_t 这个类型给禁用掉了,因而不能直接调用。
if (allocator && allocator->_base._cfisa != __CFISAForTypeID(__kCFAllocatorTypeID)) { // malloc_zone_t *
return; // require allocator to this function to be an allocator
}
CFAllocatorSetDefault 这个办法中心是将 allocator 放到 __CFTSDTable 容器里边,然后将 Table 存储到线程的局部变量。
CFRetain(allocator);
_CFSetTSD(__CFTSDKeyAllocator, (void *)allocator, NULL);
}
存储的 key 值经过 pthread_key_init_np 初始化,相对于动态创立 key 值的 pthread_key_create 办法,init_np 能够指定 key 值,那 Table 在 TSD 里边的 key 是个固定的数值,咱们能够绕开 CFAllocatorSetDefault 直接将 malloc_zone_t 存储到 TSD 里边。考虑到时间本钱,咱们先 export _CFSetTSD 办法验证可行性,实现如下所示:
void (*(origin_cf_zone_free))(struct _malloc_zone_t *zone, void *ptr);
void (*(origin_cf_zone_free_definite_size))(struct _malloc_zone_t *zone, void *ptr, size_t size);
void (*(origin_cf_zone_try_free_default))(struct _malloc_zone_t *zone, void *ptr);
void new_cf_zone_free(struct _malloc_zone_t *zone, void *ptr) {
origin_cf_zone_free(zone, ptr); // <----- 在这个办法内记载对战
}
void new_cf_zone_free_definite_size(struct _malloc_zone_t *zone, void *ptr, size_t size) {
origin_cf_zone_free_definite_size(zone, ptr, size);
}
void new_cf_zone_try_free_default(struct _malloc_zone_t *zone, void *ptr) {
origin_cf_zone_try_free_default(zone, ptr);
}
void swizzle_cf_deallocate() {
malloc_zone_t *cf_zone = malloc_create_zone(0, 0);
origin_cf_zone_free = cf_zone->free;
origin_cf_zone_free_definite_size = cf_zone->free_definite_size;
origin_cf_zone_try_free_default = cf_zone->try_free_default;
mprotect(cf_zone, **sizeof**(malloc_zone_t), PROT_READ | PROT_WRITE);
cf_zone->free = new_cf_zone_free;
cf_zone->free_definite_size = new_cf_zone_free_definite_size;
cf_zone->try_free_default = new_cf_zone_try_free_default;
_CFSetTSD(1, cf_zone, nil);
}
断点 3 个 free 办法,CFRelease 在替换之后会履行到 origin_cf_zone_free 办法里边,能够在这个办法里边记载 CF 目标开释的仓库。
这儿存在一个问题,对于 default allocator 的替换是从 app 运转过程中进行的,在替换之前 allocate 的 CF 目标是否需求特别处理?也就是在 zone 的 free 办法调用 CFAllocator 的 deallocate 办法。从 debug 的现象来看是不需求的,替换之前的 allocate 在开释时会继续履行 CFAllocator 的 deallocate 办法。因为懒,这儿不做过多的源码剖析。
// 替换之前创立 CF 目标
CFStringRef cf_str_1 = CFStringCreateWithCString(kCFAllocatorDefault, "CFString_kCFAllocatorDefault", kCFStringEncodingUTF8);
// CFAllocator 替换为 malloc_zone_t
_CFSetTSD(1, cf_zone, nil);
// 替换之后创立 CF 目标
CFStringRef cf_str_2 = CFStringCreateWithCString(kCFAllocatorDefault,
"CFString_kCFAllocatorDefault", kCFStringEncodingUTF8);
// 开释替换之前创立的 CF 目标,履行 CFAllocator deallocate
CFRelease(cf_str_1);
// 开释替换之前创立的 CF 目标,履行 malloc_zone_t free
CFRelease(cf_str_2);
计划2: 自定义 CFAllocator
和计划一比较,这儿自定义的分配器是 CFAllocator,对应的 deallocate 办法在 CFAllocator 持有的 _context 结构体里边,_context 能够经过体系 api 获取。这种计划不改动分配器的类型,理论上对于 CF 目标的内存管理影响更小一些。
void (*cf_origin_deallocate)(void *ptr, void *info);
void cf_new_deallocate(void *ptr, void *info) {
cf_origin_deallocate(ptr, info);
}
CFAllocatorContext context = { 0 };
// 获取默认的 CFAllocator 的 context
CFAllocatorGetContext(CFAllocatorGetDefault(), &context);
// 记载 context 原始的 deallocate 办法
cf_origin_deallocate = context.deallocate;
// 将 deallocate 办法替换为自定义办法
context.deallocate = cf_new_deallocate;
// 使用上述 conteext 新创立一个 allocator,并设置为 default CFAllocator
CFAllocatorSetDefault(CFAllocatorCreate(kCFAllocatorDefault, &context));
存在的问题:
在履行 UIGraphicsEndImageContext 时触发了一个溃散,至今原因不明。
Example(5274,0x1e49d8800) malloc: Non-aligned pointer 0x281231c90 being freed (2)
计划3: 修正 default CFAllocator deallocate 办法
涉及到两个私有的结构体 __CFAllocator 和 CFRuntimeBase。
struct __CFAllocator {
CFRuntimeBase _base;
// CFAllocator structure must match struct _malloc_zone_t!
// The first two reserved fields in struct _malloc_zone_t are for us with CFRuntimeBase
size_t (*size)(struct _malloc_zone_t *zone, const void *ptr); /* returns the size of a block or 0 if not in this zone; must be fast, especially for negative answers */
void *(*malloc)(struct _malloc_zone_t *zone, size_t size);
void *(*calloc)(struct _malloc_zone_t *zone, size_t num_items, size_t size); /* same as malloc, but block returned is set to zero */
void *(*valloc)(struct _malloc_zone_t *zone, size_t size); /* same as malloc, but block returned is set to zero and is guaranteed to be page aligned */
void (*free)(struct _malloc_zone_t *zone, void *ptr);
void *(*realloc)(struct _malloc_zone_t *zone, void *ptr, size_t size);
void (*destroy)(struct _malloc_zone_t *zone); /* zone is destroyed and all memory reclaimed */
const char *zone_name;
/* Optional batch callbacks; these may be NULL */
unsigned (*batch_malloc)(struct _malloc_zone_t *zone, size_t size, void **results, unsigned num_requested); /* given a size, returns pointers capable of holding that size; returns the number of pointers allocated (maybe 0 or less than num_requested) */
void (*batch_free)(struct _malloc_zone_t *zone, void **to_be_freed, unsigned num_to_be_freed); /* frees all the pointers in to_be_freed; note that to_be_freed may be overwritten during the process */
struct malloc_introspection_t *introspect;
unsigned version;
/* aligned memory allocation. The callback may be NULL. */
void *(*memalign)(struct _malloc_zone_t *zone, size_t alignment, size_t size);
/* free a pointer known to be in zone and known to have the given size. The callback may be NULL. */
void (*free_definite_size)(struct _malloc_zone_t *zone, void *ptr, size_t size);
CFAllocatorRef _allocator;
CFAllocatorContext _context;
};
typedef struct __CFRuntimeBase {
uintptr_t _cfisa;
uint8_t _cfinfo[4];
#if __LP64__
uint32_t _rc;
#endif
} CFRuntimeBase;
映射 __CFAllocator 替换 _context 的 deallocate 办法
void (*cf_origin_deallocate)(void *ptr, void *info);
void cf_new_deallocate(void *ptr, void *info) {
CFTypeID ID = __XXXCFGenericTypeID_inline(ptr);
if (ID == CFStringGetTypeID()) {
// 这儿依据依据类型筛选记载仓库
}
cf_origin_deallocate(ptr, info);
}
void swizzle_cf_deallocate() {
struct __XXXCFAllocator *cf_zone = (struct __XXXCFAllocator *)CFAllocatorGetDefault();
// 可能得需求提前调用 mprotect 办法确保指针可被修正
cf_origin_deallocate = cf_zone->_context.deallocate;
cf_zone->_context.deallocate = cf_new_deallocate;
}
映射体系的私有结构体通常是是一个危险的操作,假如体系更新了该结构体仍是依照之前的结构映射修正,可能会把结构体的内存写坏。但是这儿修正的 deallocate 办法自身是能够获取的,因而咱们能够加一层校验来确保这儿的映射是安全的。
CFAllocatorContext context = { 0 };
CFAllocatorGetContext(CFAllocatorGetDefault(), &context);
if (cf_zone->_context.deallocate != context.deallocate) {
return;
}
结论
计划 1 将 CF 的分配器替换为 malloc_zone_t,计划 2 将 CF 的分配器替换为自定义的 CFAllocator,而计划 3 只修正了一个函数指针 deallocate,相对于前者影响范围更小,在均能实现功能的基础之上,现在本人更倾向于计划 3。本文只是计划的剖析探究,没有在线上验证作用,假如各位大佬有更好的计划,请不吝赐教,非常欢迎您的宝贵意见和建议。