前言
文中分享都基于自己的demo,完全模拟事发场景。
起因
前段时间,频繁收到不少QA童鞋报的crash。操作步骤各不相同,崩溃栈也没一个一样的,大致如下:
酱紫的崩溃,反正我看着是挺绝望的。。
完全是毫无头绪啊~~~
问题进展
挂了四五天的bug,期间经过不懈的努力,最后找到大致的复现规律了。在组内大腿的建议下,第一次使用起Address Sanitizer。
打开Address Sanitizer。run起来后,按照之前的复现步骤,居然一下就断住了。
显然,这是访问一个已经释放了的指针,的确是有问题。
但是。 这会跟之前的崩溃是一个原因么?
【PS:alloc和dealloc的地方,是继承的引擎库,没有代码权限,无法直接修改验证】
反正其他方式也没有进展,就当这个就是问题原因吧,那么继续分析下这样的操作之后,会发生什么事。
原因定位
搞个新项目,在vc的touchBegin中,加入如下代码
unsigned int size = 8; // size = 8,比较容易命中。
buffer = malloc(size);
snprintf(buffer, size, "Hello!");
NSLog(@"%p, %s", buffer, buffer);
free(buffer);
// memory history <#expression#>
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// 在未来的某个时机,改变该指针的值
snprintf(buffer, size, "Hello!");
});
这个与刚刚捕获到的异常,同样的是use-after-free。
run起来,碰了下viewController,咦,没崩?
再点了两下看看。
Boom~
特喵,再打开,再点! Boom~~
我不信,我点、我点、我点点点! Boom~~
导出crashLog,符号化之~
果然,几次崩溃完全不一样,全都在系统库中。
问题剖析
到这里,基本上定位到问题原因了。但是,为什么会乱蹦呢?
不弄清楚,不能忍!
打开刚刚新建的项目,搞俩断点
run ---> touch
走到断点A。同时控制台输出Log
2017-06-22 16:02:41.993918+0800 WWDC_Demo[9323:2875577] 0x148e512c0, Hello!
copy 上面的内存地址0x148e512c0。 cmd+shift+M,进入到View Memory界面,输入刚刚copy的内存地址。
这时候能看到,右侧Hello! 这是刚代码中给buffer赋的值。
代码继续往下走,走过free(buffer).
再次cmd+shift+M,进入到View Memory界面,输入刚刚copy的内存地址。
友情提醒
注意,这时候不能直接点击右侧的树状结构中,之前生成的内存。那个不会实时更新,显示的是当时的内存内容。
回到原来的话题,这时候发现,内存块的值,已经不再是Hello!
纳尼?值变了?
咱们刚的代码中,明明只是free,释放了这块内存地址呀,并没有改变他的值。
难道说,刚刚free之后,马上就有其他地方申请到了这块内存地址,并修改了?
再次验证,修改代码中,malloc的size为1028。重新来一次之前的步骤。发现free后值依然是Hello! 。 也就是那块内存地址还没被申请,同样的,之后的操作也不会崩溃。
再次提醒
修改size为1028,并不一定内存地址free后就不会再被重新申请了,只是概率较低。所以即使再被重新申请了,别慌,再来一次。~
另外,一旦被重新申请,那么程序继续跑下去,必然会导致崩溃在莫名其妙的地方。
反之,在断点B之前,还没有被重新申请,则不会崩溃。
结论
通过C语言,释放内存地址后,再操作内存区域。【oc是c的进一步封装,alloc和dealloc应该都有保护代码,操作基本都会马上崩溃】
- 如果此时这块内存区域尚未被申请,还不会有问题。
- 假如此时这块内存区域已经被申请,修改后,并不一定会马上出问题,待申请方重新使用的时候,就有可能崩溃。
当然,不管以上哪一种情况,Address Sanitizer都能捕获到。
Address Sanitizer原理简述
- 在申请的内存地址两侧插入对应的redzone ==> 检测Overflow
- 延迟已被free的堆空间的重用 ==> 检测Use-after-free
- 访问某内存时,会检查其对应的shadow memory的state。
- 管理shadow memory,同时保证shadow空间不被使用
PS:shadow memory --- 影子内存,每8个字节的内存会映射8位(1字节)的影子内存,用于表示对应内存的状态。
当检测到某块内存对应的shadow memory为0时,说明这块内存是OK,可使用的,反之,则捕获异常。