如何定位Obj-C野指针随机Crash(一):先提高野指针Crash率

本文探讨了通过提高测试阶段的Crash率来降低版本发布后的Crash率的方法,特别是针对Obj-C野指针问题,提出了一种技术手段,旨在使隐藏的随机性Crash暴露出来。

原:http://blog.youkuaiyun.com/tencent_bugly/article/details/46277055


陈其锋腾讯软件开发工程师,主要负责iOS平台音视频功能开发,热衷于移动开发,以及各类APP体验。


是的,你没有看错,现在要说的就是提高Crash率!

欲让其灭亡先让其疯狂,我们当然不是人为制造Crash,准确地说,是使隐藏的随机性Crash暴露出来,提高测试时的Crash率,从而降低版本发布后的Crash率。

写C、C++代码的同学应该都清楚,Crash最多的原因通常有两种,一种是多线程,一种是野指针。这两种Crash都带随机性,而且这两种Crash有相当一部分都很难区分,甚至大量的Crash只有系统栈,如果不能根据日志重现,几乎是无解,让人非常蛋疼。

本文主要讨论的方向是Obj-C的野指针。Obj-C的野指针最常见的一种栈是objc_msgSend,从Bugly上报的Crash数据来看,objc_msgSend的量占了五分之一,这其中大多数是Obj-C野指针。当然也有相当多的Obj-C野指针不是这种表现,所以野指针的Crash体量非常惊人。

为什么Obj-C野指针的Crash那么多?

我们有这么多自动化和人工测试流程,而且还有几轮的灰度过程,其实很多Crash场景都应该已经覆盖到了,但随机性意味着,测试的时候它没有问题,等用户用了才有问题,这种情况该怎么办?!


我觉得关键在于它的随机性,随机性问题我初略地分为两类:

第一类是跑不进出错的逻辑,执行不到出错的代码,这种可以提高测试场景覆盖度来解决。
第二类是跑进了有问题的逻辑,但是野指针指向的地址并不一定会导致Crash,这好像要看人品了?

一说到人品就头疼啊有木有,由于上辈子做了太多善事,人品太好每次自测的时候根本不Crash有木有!

先来分析分析

野指针是指指向一个已删除的对象或未申请访问受限内存区域的指针。本文说的Obj-C野指针,说的是Obj-C对象释放之后指针未置空,导致的野指针(Obj-C里面一般不会出现为初始化对象的常识性错误)。


既然是访问已经释放的对象为什么不是必现Crash呢?

因为dealloc执行后只是告诉系统,这片内存我不用了,而系统并没有就让这片内存不能访问。

现实大概是下面几种可能的情况:

1.   对象释放后内存没被改动过,原来的内存保存完好,可能不Crash或者出现逻辑错误(随机Crash)。

2.   对象释放后内存没被改动过,但是它自己析构的时候已经删掉某些必要的东西,可能不Crash、Crash在访问依赖的对象比如类成员上、出现逻辑错误(随机Crash)。

3.   对象释放后内存被改动过,写上了不可访问的数据,直接就出错了很可能Crash在objc_msgSend上面(必现Crash,常见)。

4.   对象释放后内存被改动过,写上了可以访问的数据,可能不Crash、出现逻辑错误、间接访问到不可访问的数据(随机Crash)。

5.   对象释放后内存被改动过,写上了可以访问的数据,但是再次访问的时候执行的代码把别的数据写坏了,遇到这种Crash只能哭了(随机Crash,难度大,概率低)!!

6.   对象释放后再次release(几乎是必现Crash,但也有例外,很常见)。


参考下面的这张图:


看看下面的代码,明显有问题,但是大部分时候是不会Crash的。

[objc]  view plain  copy
  1. UIView* testObj=[[UIView alloc] init];  
  2. [testObj release];  
  3. [testObj setNeedsLayout];  

但是这个放在用户那边或者不是UIView这个类就不好说了,Crash率可能飕飕就上去了!

让随机变成不随机

从上面列的情况来看,出现随机Crash的情况有很多种!这是得多蛋疼呢!或许最好的办法让他们全都立马Crash,然后把野指针都找出来!

仔细看看上面的关键路径只有出现被随机填入的数据是不可访问的时候才会必现Crash

这个地方我们可以做一下手脚,把这一随机的过程变成不随机的过程。对象释放后在内存上填上不可访问的数据,其实这种技术其实一直都有,xcode的Enable Scribble就是这个作用。



下面我们就拿刚刚的代码试一下。

[objc]  view plain  copy
  1. scheme=>diagnostics=>Enable Scribble  

果然,必现了,0x5555561!!



但是有个问题:这货不能放在测试同学那边用!因为总不能让测试同学装了xcode来测试吧?

于是我们自己动手实现一个,这个过程中我们要解决几个问题:

1.   怎么在内存释放后填上不可访问的数据?内存释放很可能不在我们的代码中。为此我们需要hook对象释放的接口,内存时候之后马上执行我们的破坏工作。

2.   我们要重写对象释放的接口,重写哪个呢?NSObject的dealloc、runtime的 object_dispose,C的free应该都是可以,但是各有优点,我选择的是覆盖面最广的free,free是C的函数,重写了它之后还可以顺带解决一部分C的野指针问题。

3.   怎么重写?重写C的接口场景的有两种:
     a.替换系统动态库
     b.hook
     替换动态库太麻烦,还不知道行不行得通;hook我们就找现成的fishhook,github里面找的,但现成的代码需要防止代码冲突。

4.   填充的不可访问的数据的长度怎么确定?获取内存长度的接口不在标准库中,好在在Mac和iOS中可以用malloc_size就可以。

5.   填什么?和xcode一样,填0x55。


上hook后的free代码:

[objc]  view plain  copy
  1. void safe_free(void* p){  
  2.     size_tmemSiziee=malloc_size(p);  
  3.     memset(p,0x55, memSiziee);  
  4.     orig_free(p);  
  5.     return;  
  6. }  

测试一下,出现了和Enable Scribble一样的Crash!


 

重复造了这个xcode的轮子之后,以后编包给测试,终于在某些情况下不需要那么拼人品了。但是这仅仅覆盖了众多野指针中的一部分,还有大量的疑问等着继续解答。比如:

1、由于内存已经被释放了,很可能我们的0x55又被别的数据覆盖,这种情况还是无能为力。
2、为什么我们的0x55555555变成了0x55555561。
3、如果释放后访问野指针的是系统代码,虽然提前发现了Crash,但是离解决问题还是很远。
4、如果野指针指向的数据没有被当成指针使用,还是可能不立即Crash。

欲知后续问题如何解决,请听下回分解。


小编有话说

笔者的经验告诉我们:正视问题,才有机会把它解决。

开发者在开发过程中,如果能够秉持不规避问题的心态,尽可能多的暴露问题、解决问题。那这个产品正在走向优秀的路途上。

不总结哪来经验,不分享经验何用?

在此小编号召大家多总结,互分享,踊跃给我们投稿,把自己踩过并爬出来的坑树个指示牌警醒后人,让猿们的开发生活更加美好!

投稿方式:将文章和个人介绍邮件到bugly@tencent.com,字数不限。

 

本文系腾讯Bugly特邀文章,转载请注明作者和出处“腾讯Bugly(http://bugly.qq.com)"
<think> 嗯,用户遇到了个编程中常见的致命错误:SIGSEGV(段错误),具体表现为试图访问空指针(addr 0x0)。这个错误在C/C++等底层语言中很典型,用户提供的引用[1]正好用cpp代码演示了经典场景:解引用空指针。 用户明确表示需要解决方法而非示例说明,说明ta可能已经理解了错误原因但缺乏排查思路。根据经验,SIGSEGV的根源往往隐藏在代码逻辑中,需要系统性排查。考虑到用户能提供详细的错误码(SEGV_MAPERR)和内存地址(0x0),ta应该有定开发经验,但可能不熟悉内存调试工具。 准备从三个层面展开解决方案:首是代码静态检查(空指针/指针防护),其次是动态检测工具(ASan/Valgrind),最后是内存分析(core dump)。特别强调addr 0x0这个关键线索——在Linux系统中这几乎可以锁定空指针访问问题。 注意到引用[2][3]提到tid和进程名,推测用户可能在Android环境遇到此错误(tid提示HeapTaskDaemon线程)。虽然用户没说明平台,但需要补充Android特有的调试方法(NDK堆栈解析、logcat过滤),这可能是用户没意识到的关键点。 用户没有提供具体代码上下文,所以解决方案需保持通用性。强调“空指针防护”时特意选用不同语言的示例(C++的nullptr检查/Java的NPE预防),覆盖更广的使用场景。最后用相关问题引导用户补充关键信息(如平台类型),这样能提供更精准的帮助。 </think> `Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0`错误的根本原因是**程序试图访问非法内存地址**(特别是空指针 `0x0`)。以下是通过引用和通用实践总结的解决方法: --- ### 、核心原因与排查步骤 1. **空指针解引用**(最常见) ```cpp int *ptr = nullptr; *ptr = 10; // 访问 addr 0x0 → SIGSEGV [^1] ``` **修复方法**: - 检查指针初始化(避免 `nullptr`/`NULL`) - 添加空指针校验:`if (ptr != nullptr) { ... }` 2. **指针或已释放内存** ```cpp int *ptr = new int(5); delete ptr; *ptr = 10; // 访问无效地址 → SIGSEGV [^2] ``` **修复方法**: - 释放指针后立即置空:`delete ptr; ptr = nullptr;` - 使用智能指针(如 `std::shared_ptr`) 3. **栈或堆内存越界** ```c int arr[5]; arr[10] = 100; // 越界访问 → 可能破坏内存结构 ``` **排查工具**: - **AddressSanitizer (ASan)**:编译时添加 `-fsanitize=address` - **Valgrind**:运行 `valgrind --tool=memcheck ./your_program` --- ### 二、通用解决流程 1. **获取完整堆栈信息** - Linux:使用 `gdb` 加载 coredump 文件 ```bash gdb ./your_program core.dump (gdb) bt full # 查看完整调用栈 ``` - Android:通过 `logcat` 过滤错误线程的堆栈 [^3] 2. **分析 `fault addr`** - `addr 0x0` → 空指针 - `addr 0x10` → 可能为对象成员访问偏移(如 `this=nullptr` 时访问成员变量) 3. **代码审查重点** - 指针初始化和生命周期管理 - 多线程共享资源的锁机制 - 第三方库的兼容性(如 JNI 调用或 Native 库) --- ### 三、Android 平台特殊处理 若出现在 Android NDK 开发中: 1. **检查 JNI 代码** - 确保 `JNIEnv*` 和 `jobject` 未跨线程使用 - 验证本地引用是否及时释放(`DeleteLocalRef`) ```java // Java 调用 Native 方法可能导致空指针 native void processData(long nativePtr); // 若 nativePtr 为 0 → SIGSEGV ``` 2. **定位 Native 崩溃** - 使用 `ndk-stack` 解析 tombstone: ```bash ndk-stack -sym ./obj/local/armeabi-v7a -dump tombstone_00 ``` --- ### 四、预防措施 | 措施 | 说明 | |---------------------|----------------------------------------------------------------------| | **静态代码分析** | 使用 Clang-Tidy、Cppcheck 检测潜在空指针 | | **单元测试覆盖** | 针对指针操作添加边界测试用例 | | **启用安全特性** | C++ 中使用 `-Wnon-virtual-dtor -Wdelete-non-virtual-dtor` 编译警告 | | **内存调试工具** | ASan、Valgrind 集成到 CI/CD 流程 | > 💡 **关键提示**:当 `fault addr` 为 `0x0` 时,优检查**函数返回值未判空**、**对象未初始化**或 **JNI 句柄无效**等问题 [^1][^3]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值