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

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

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

原: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>我们正在处理个关于AndroidAudioTrack导致SIGSEGV崩溃的问题。SIGSEGV是段错误(SegmentationViolation),通常是由于非法内存访问引起的。在Android开发中,AudioTrack类用于播放音频数据,如果使用不当,可能会导致此类崩溃。根据提供的引用,我们可以分析可能的原因和解决方案:引用[1]提到AudioTrack占用过多资源可能导致应用卡顿或崩溃,但未直接提及SIGSEGV。然而,资源占用过多可能间接导致内存问题,从而引发段错误。引用[2]和[3]提供了AudioTrack的API和资源释放的相关信息。特别是引用[3]指出,在AudioTrack的finalize方法中会自动调用release方法来释放资源。但是,如果我们没有正确释放AudioTrack资源,可能会导致内存泄漏或访问已释放内存,从而引发SIGSEGV。可能的原因:1.内存访问越界:在操作AudioTrack时,可能访问了不属于应用程序的内存区域。例如,传递了无效的指针或缓冲区。2.未正确初始化:AudioTrack对象未正确初始化,导致后续操作非法内存。3.多线程问题:在多线程环境下,AudioTrack对象被多个线程同时访问,而没有适当的同步。4.资源未释放:AudioTrack对象在使用完毕后没有调用release()方法释放资源,导致内存泄漏,最终可能引发崩溃。5.使用已释放的对象:AudioTrack对象被释放后,仍然尝试使用它。解决方案:1.确保正确初始化AudioTrack对象。检查初始化参数,如流类型、采样、声道配置、音频格式和缓冲区大小。2.在使用AudioTrack写入音频数据时,确保缓冲区大小正确,并且不会越界。3.在多线程环境中,使用同步机制(如synchronized)保护对AudioTrack对象的访问。4.在使用完AudioTrack后,务必调用release()方法释放资源。最好在finally块中调用以确保释放。5.避免在AudioTrack对象被释放后继续使用它。例如,在release()之后不要再调用write()或play()等方法。6.检查JNI代码(如果有):如果通过JNI使用AudioTrack,确保没有在本地代码中访问无效的内存地址。示例代码片段,展示如何安全使用AudioTrack:```javaAudioTrackaudioTrack=null;try{//初始化AudioTrackintminBufferSize=AudioTrack.getMinBufferSize(SAMPLE_RATE,CHANNEL_CONFIG,AUDIO_FORMAT);audioTrack=newAudioTrack(AudioManager.STREAM_MUSIC,SAMPLE_RATE,CHANNEL_CONFIG,AUDIO_FORMAT,minBufferSize,AudioTrack.MODE_STREAM);audioTrack.play();//写入音频数据...byte[]audioData=...;audioTrack.write(audioData,0,audioData.length);}catch(Exceptione){e.printStackTrace();}finally{if(audioTrack!=null){try{audioTrack.stop();//停止}catch(Exceptione){//忽略异常}audioTrack.release();//释放资源}}```此外,还可以使用AndroidStudio的内存分析工具(如MemoryProfiler)来检测内存泄漏和非法内存访问。如果问题仍然存在,检查崩溃日志以确定崩溃发生的具体位置。如果是native层的崩溃,需要检查native代码(如JNI)是否有内存问题。最后,确保使用的AudioTrackAPI是线程安全的,或者在使用时进行了适当的同步。相关问题:1.如何诊断Android应用中的SIGSEGV错误?2.如何避免Android中的内存泄漏?3.AudioTrack在多线程环境下如何安全使用?4.如何正确释放AudioTrack资源?5.使用AudioTrack时常见的错误有哪些?</think>### Android AudioTrack SIGSEGV 崩溃问题分析与解决方案 SIGSEGV (Segmentation Violation) 是 Android 开发中常见的致命错误,通常由非法内存访问引发。当 AudioTrack 发生 SIGSEGV 时,主要涉及以下核心原因和解决方案: #### 、根本原因分析 1. **内存非法访问** - 访问已释放的 AudioTrack 对象(指针- 音频缓冲区越界访问(如 `write()` 操作超出分配的内存范围) - 多线程竞争导致的内存状态不致 2. **资源生命周期问题** - 未及时调用 `release()` 导致资源泄漏(引用[3]) - 对象被垃圾回收后仍被使用(`finalize` 自动释放后继续操作) 3. **JNI 层问题** - Native 代码与 Java 对象指针不同步 - JNI 环境未正确附加到线程(`AttachCurrentThread` 缺失) 4. **硬件兼容性问题** - 特定设备驱动对 AudioTrack 的实现差异 - 采样/缓冲区大小超出设备支持范围 #### 二、关键解决方案 1. **严格资源管理** ```java AudioTrack track = null; try { track = new AudioTrack(...); track.play(); // 业务操作 } finally { if (track != null) { track.stop(); // 停止播放 track.release(); // 必须显式释放(引用[3]) } } ``` 2. **内存访问防护** - 缓冲区写入前检查长度: ```java int written = track.write(audioData, 0, Math.min(audioData.length, bufferSize)); if (written < 0) Log.e("AUDIO", "Write error: " + written); ``` - 使用 `AudioTrack.Builder` 替代直接构造(API 23+) 3. **多线程同步** - 对 AudioTrack 操作加锁: ```java private final Object audioLock = new Object(); public void safePlay() { synchronized(audioLock) { if(track != null) track.play(); } } ``` 4. **Native 层防护(JNI)** ```cpp JNIEXPORT void JNICALL Java_com_example_play(JNIEnv* env, jobject obj) { if(!env || !obj) return; // 空指针检查 AttachCurrentThreadIfNeeded(env); // 确保线程附加 // 业务代码 } ``` 5. **崩溃诊断增强** - 启用 `android:debuggable="true"` 捕获 native 崩溃栈 - 使用 `adb logcat -b crash` 分析 SIGSEGV 具体地址 #### 三、最佳实践建议 1. **资源释放原则** - 在 `Activity.onDestroy()`/`Service.onDestroy()` 中强制释放 - 避免依赖 `finalize()`(引用[3]),因其调用时机不可控 2. **参数验证** 初始化时检查参数合法性: ```java int bufferSize = AudioTrack.getMinBufferSize(44100, AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT); if(bufferSize <= 0) throw new IllegalArgumentException("Invalid params"); ``` 3. **兼容性处理** - 使用 `AudioManager.getProperty()` 查询设备能力 - 为老旧设备提供降级方案(如降低采样) > **重要提示**:崩溃多发生在 AudioTrack 状态转换时(如 `stop()` 后调用 `write()`)。推荐使用状态机管理: > ```java > private enum TrackState { UNINIT, PLAYING, STOPPED, RELEASED } > ``` ### 相关问题 1. 如何检测 AudioTrack 的内存泄漏? 2. AudioTrack 在后台服务中的最佳实践是什么? 3. 如何跨 Android 版本兼容不同 AudioTrack API? 4. SIGSEGV 崩溃日志的分析方法有哪些? 5. AudioTrack 与 OpenSL ES 的性能对比如何?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值