首页 > 代码库 > Virsualizer模块野指针问题分析报告

Virsualizer模块野指针问题分析报告

【NE现场】

pid: 1040, tid: 19988, name: Visualizer  >>> com.android.systemui <<<
signal 11 (SIGSEGV), code 2 (SEGV_ACCERR), fault addr 546f2cf8
    r0 56106e28  r1 546f2d18  r2 0200000f  r3 02000008
    r4 56106e28  r5 546f2d18  r6 566f2d24  r7 566f2d20
    r8 566f2d18  r9 565f5000  sl be93c294  fp 400952ec
    ip 00000000  sp 546f2d10  lr 40aeb275  pc 40aeb0dc  cpsr 00020030
backtrace:
    #00  pc 000650dc  /system/lib/libmedia.so (android::Visualizer::getIntMeasurements(unsigned int, unsigned int, int*)+163)
    #01  pc 032be33f  /dev/ashmem/dalvik-heap (deleted)

栈的低地址边界上会留没有任何访问权限的一个页作(---p属性)为保护页来监测代码中的栈溢出。

栈溢出问题,在代码中查找当前调用栈对应的代码里是否有大数组、递归等有隐患的代码,就能初步定位问题。

 

【初步分析】

从getIntMeasurements+163这个信息可以定位到当前PC其实是在0X6772c上,

就是android::Visualizer::getWaveForm()的起始位置:

00067688 <_ZN7android10Visualizer18getIntMeasurementsEjjPi>:
   67688:       b5f0            push    {r4, r5, r6, r7, lr}
   6768a:       b087            sub     sp, #28
   6768c:       f8d0 60f8       ldr.w   r6, [r0, #248]  ; 0xf8
   67690:       4604            mov     r4, r0
   67692:       4617            mov     r7, r2
   ...

0006772c <_ZN7android10Visualizer11getWaveFormEPh>:
   6772c:       b51f            push    {r0, r1, r2, r3, r4, lr}             <<<<<<<<<<<<
   6772e:       b1f1            cbz     r1, 6776e <_ZN7android10Visualizer11getWaveFormEPh+0x42>
   ...

也就是android::Visualizer::getWaveForm() 函数刚进来,保存寄存器的时候挂掉了,进来之前SP寄存器就异常了。

在代码中搜索调用 getWaveForm()的 地方,发现有如下两处:

status_t Visualizer::getFft(uint8_t *fft)
{
    ...
    status_t status = NO_ERROR;
    if (mEnabled) {
        uint8_t buf[mCaptureSize];
        status = getWaveForm(buf);   <<<<<<<<<<
        if (status == NO_ERROR) {
            status = doFft(fft, buf);
        }
    } else {
        memset(fft, 0, mCaptureSize);
    }
    return status;
}

void Visualizer::periodicCapture()
{
    Mutex::Autolock _l(mCaptureLock);
    ALOGV("periodicCapture() %p mCaptureCallBack %p mCaptureFlags 0x%08x",  this, mCaptureCallBack, mCaptureFlags);
    if (mCaptureCallBack != NULL &&
        (mCaptureFlags & (CAPTURE_WAVEFORM|CAPTURE_FFT)) &&
        mCaptureSize != 0) {
        uint8_t waveform[mCaptureSize];
        status_t status = getWaveForm(waveform);  <<<<<<<<<<
        ...

其中getFft在调用 getWaveForm()之 前,r1和sp的值 是一样的:

0006782c <_ZN7android10Visualizer6getFftEPh>:
   6782c:       b5f8            push    {r3, r4, r5, r6, r7, lr}
   6782e:       4606            mov     r6, r0
   ...
   67840:       3207            adds    r2, #7
   67842:       466c            mov     r4, sp
   67844:       f022 0107       bic.w   r1, r2, #7
   67848:       ebad 0d01       sub.w   sp, sp, r1
   6784c:       4669            mov     r1, sp
   6784e:       f7ff ff6d       bl      6772c <_ZN7android10Visualizer11getWaveFormEPh>
   ...

但从出问题时的寄存器中可以 看到r1=546f2d18    sp=546f2d10,它俩是相差8个字节,所以排除 getFft。

再看看下一个periodicCapture():

0006787c <_ZN7android10Visualizer15periodicCaptureEv>:
   6787c:       e92d 43f0       stmdb   sp!, {r4, r5, r6, r7, r8, r9, lr}
   67880:       b085            sub     sp, #20
   ...
   678b6:       4620            mov     r0, r4
   678b8:       ebad 0d05       sub.w   sp, sp, r5
   678bc:       ad02            add     r5, sp, #8                 <<<<<<<<<<
   678be:       4629            mov     r1, r5                     <<<<<<<<<<
   678c0:       f7ff ff34       bl      6772c <_ZN7android10Visualizer11getWaveFormEPh>
   ...

可以看到调用 getWaveForm() 前,r1 = r5 = sp + 8,和 NE时的寄存器状态完全吻合,可以确定就是这块出问题了.

源码如下:

void Visualizer::periodicCapture()
{
    Mutex::Autolock _l(mCaptureLock);
    ALOGV("periodicCapture() %p mCaptureCallBack %p mCaptureFlags 0x%08x",
            this, mCaptureCallBack, mCaptureFlags);
    if (mCaptureCallBack != NULL &&
        (mCaptureFlags & (CAPTURE_WAVEFORM|CAPTURE_FFT)) &&
        mCaptureSize != 0) {
        uint8_t waveform[mCaptureSize];
        status_t status = getWaveForm(waveform);
        ...

这里uint8_t waveform[mCaptureSize];这句话相当于在线程栈里面开辟mCaptureSize大小的栈。

而栈的大小最多是1M-12K = 1012K的大小,如果mCaptureSize大到一定程度,就有可能冲爆栈。

mCaptureSize值异常,有两种可能:

1、在堆空间中Visualizer对象被别的模块给修改掉,一般是其他模块的野指针。

2、模块本身逻辑问题。

 

如果是堆问题,它一般是随机性的,但这个栈溢出问题,已经在多个版本中出现过,所以当时就排除掉了第一个可能性,于是就去查找模块逻辑的漏洞。

其实在这里进入了一个小误区,耗了不少无用功。后来发现这个Visualizer线程还出现过其他类型的NativeCrash,而且数量也不少,如:

pid: 1275, tid: 13552, name: Visualizer >>> com.android.systemui <<<
signal 6 (SIGABRT), code -6 (SI_TKILL), fault addr --------
r0 00000000 r1 000034f0 r2 00000006 r3 00000000
r4 00000006 r5 00000000 r6 000034f0 r7 0000010c
r8 4cd23640 r9 4e369a7a sl 00000001 fp 575598d8
ip 54e79fb0 sp 575595b0 lr 401131e5 pc 4012218c cpsr 000f0010
backtrace:
#00 pc 0002218c /system/lib/libc.so (tgkill+12)
#01 pc 000131e1 /system/lib/libc.so (pthread_kill+48)
#02 pc 000133f5 /system/lib/libc.so (raise+10)
#03 pc 0001212b /system/lib/libc.so
#04 pc 00021a40 /system/lib/libc.so (abort+4)
#05 pc 00048c9f /system/lib/libdvm.so (dvmAbort+78)
#06 pc 0002a7c8 /system/lib/libdvm.so (IndirectRefTable::get(void*) const+116)
#07 pc 0004d5b3 /system/lib/libdvm.so (dvmDecodeIndirectRef(Thread*, _jobject*)+62)
#08 pc 00063505 /system/lib/libdvm.so (dvmCallMethodV(Thread*, Method const*, Object*, bool, JValue*, std::__va_list)+156)
#09 pc 0004cd37 /system/lib/libdvm.so
#10 pc 000013cf /system/lib/libaudioeffect_jni.so
#11 pc 000025fd /system/lib/libaudioeffect_jni.so
#12 pc 000652d3 /system/lib/libmedia.so (android::Visualizer::periodicCapture()+166)
#13 pc 000652f5 /system/lib/libmedia.so (android::Visualizer::CaptureThread::threadLoop()+14)
#14 pc 0000ea5d /system/lib/libutils.so (android::Thread::_threadLoop(void*)+216)
#15 pc 0004db5d /system/lib/libandroid_runtime.so (android::AndroidRuntime::javaThreadShell(void*)+68)

看到这个突然想到,mCaptureSize值异常可能存在第三个可能性:Visualizer自己本身就是个野指针!

 

【深入分析】

因为都是Visualizer线程并且是在执行periodicCapture()的时候出问题,于是去查看相关代码:

看到这个突然想到,mCaptureSize值异常可能存在第三个可能性:Visualizer自己本身就是个野指针!

因为都是Visualizer线程并且是在执行periodicCapture()的时候出问题,于是去查看相关代码:

bool Visualizer::CaptureThread::threadLoop()
{
    ALOGV("CaptureThread %p enter", this);
    while (!exitPending())
    {
        usleep(mSleepTimeUs);
        mReceiver.periodicCapture();
    }
    ALOGV("CaptureThread %p exiting", this);
    return false;
}

这里的mReceiver就是Visualizer对象的指针,而在这个Visualizer对象的new和delete都在主线程做的。

创建Visualizer对象后通过android::Visualizer::setCaptureCallBack()函数创建CaptureThread线程的时候,将Visualizer的地址付给mReceiver了。

当Visualizer线程在使用这个Visualizer对象前有一个sleep,如果这个sleep时,主线程释放掉Visualizer,则这里mReceiver就是野指针了。

因为sleep后没有任何同步措施,所以这种可能性是完全存在的。

为了进一步确定问题,查了下Visualizer对象的的创建和释放过程:

创建过程:

Visualizer::Visualizer()
android_media_visualizer_native_setup
native_setup()
Visualizer.Visualizer()
SpectrumVisualizer.enableUpdate(ture)

释放过程:

Visualizer::~Visualizer()
android_media_visualizer_native_finalize()
android_media_visualizer_native_release()
native_release()
Visualizer.release()
SpectrumVisualizer.enableUpdate(false

也就是主线程通过enableUpdate()的函数来创建和释放这个Visualizer对象。

那Visualizer线程什么时候创建和销毁呢?

也是在SpectrumVisualizer.enableUpdate():

SpectrumVisualizer.enableUpdate(true)

    public void enableUpdate(boolean enable) {
        try {
            if (mIsEnableUpdate != enable) {
                if (enable && mVisualizer == null) {
                    if (IS_LPA_DECODE) {
                        Log.v(TAG, "lpa decode is on, can‘t enable");
                    } else {
                        mVisualizer = new Visualizer(0);
                        if (!mVisualizer.getEnabled()) {
                            mVisualizer.setCaptureSize(VISUALIZATION_SAMPLE_LENGTH * 2); mVisualizer.setDataCaptureListener(mOnDataCaptureListener, Visualizer.getMaxCaptureRate(), false, true);
                            mVisualizer.setEnabled(true);
                        }
                    }
                } else if (!enable && mVisualizer != null) {
                    ...
                }
        ...

当enable为true时,先创建java层的Visualizer对象,调用Visualizer的setDataCaptureListener()设置毁掉函数。

这个setDataCaptureListener()最终对应native的Visualizer::setCaptureCallBack()

status_t Visualizer::setCaptureCallBack(capture_cbk_t cbk, void* user, uint32_t flags, uint32_t rate) {
    ...
    if (cbk != NULL) {
        mCaptureThread = new CaptureThread(*this, rate, ((flags & CAPTURE_CALL_JAVA) != 0));
    }
    ...

当enable为true时,先创建java层的Visualizer对象,调用Visualizer的setDataCaptureListener()设置毁掉函数。

这个setDataCaptureListener()最终对应native的Visualizer::setCaptureCallBack():

status_t Visualizer::setCaptureCallBack(capture_cbk_t cbk, void* user, uint32_t flags, uint32_t rate) {
    ...
    if (cbk != NULL) {
        mCaptureThread = new CaptureThread(*this, rate, ((flags & CAPTURE_CALL_JAVA) != 0));
    }
    ...

这里只是创建线程结构体,真正让它执行起来的是在setDataCaptureListener()后的setEnabled(ture)。

这个setEnable最终对应native的status_t Visualizer::setEnabled()。

status_t Visualizer::setEnabled(bool enabled)
{
    ...
    status_t status = AudioEffect::setEnabled(enabled);

    if (status == NO_ERROR) {
        if (t != 0) {
            if (enabled) {               
                t->run("Visualizer");
            } else {
                t->requestExit();
            }
        }
    }
    ...
}

当enable为ture时让线程run起来,false的时候就让线程退出,注意这里是异步地退出线程的。

所以启动流程是:

    mVisualizer = new Visualizer(0);             // 创建Visualizer对象
    mVisualizer.setDataCaptureListener(...);    // 创建Visualizer::CaptureThread对象
    mVisualizer.setEnabled(true);                // 启动Visualizer::CaptureThread线程

那什么时候让Visualizer线程退出呢?还是上面java层的SpectrumVisualizer.enableUpdate():

    public void enableUpdate(boolean enable) {
        try {
            if (mIsEnableUpdate != enable) {
                if (enable && mVisualizer == null) {
                    if (IS_LPA_DECODE) {
                        Log.v(TAG, "lpa decode is on, can‘t enable");
                    } else {
                        mVisualizer = new Visualizer(0);
                        if (!mVisualizer.getEnabled()) {
                            mVisualizer.setCaptureSize(VISUALIZATION_SAMPLE_LENGTH * 2);
                            mVisualizer.setDataCaptureListener(mOnDataCaptureListener, Visualizer.getMaxCaptureRate(), false, true);
                            mVisualizer.setEnabled(true);
                        }
                    }
                } else if (!enable && mVisualizer != null) {
                    mVisualizer.setEnabled(false);
                    //这里需要延时的原因:
                    //1.规避移植机型的Visualizer native实现的BUG —— Visualizer线程无法结束。
                    //2.一般操作系统延时最小单位50ms,更小的值没有意义,更大的值影响使用。
                    //3.延时主要是为了JAVA虚拟机能够进行线程切换,从而让Visualizer线程能够结束。
                    Thread.sleep(50);
                    mVisualizer.release();
                    mVisualizer = null;
                }
                mIsEnableUpdate = enable;
            }
        ...

退出流程:

mVisualizer.setEnabled(false);            //退出线程
mVisualizer.release();                    //释放Visualizer

注意退出过程里的Thread.sleep(50)以及它的注释,很明显这里曾经有人发现并修复过bug。

我们从启动和退出流程中不难看出,这里确实存在问题:

退出流程中,主线程请求退出Visualizer线程是调用Thread类的requestExit()函数来异步执行的。

如果这个时候Visualizer线程在sleep或则在执行mReceiver.periodicCapture(),则该线程无法立即退出。

而如果在这个过程中主线程调用mVisualizer.release()释放Visualizer对象,则Visualizer线程里的mReceiver就是野指针了。

明显上面的Thread.sleep(50)就是规避这个问题的。不过这里sleep 50毫秒够么?保险么?

对此在Visualizer线程加了log,发现Visualizer线程每次也是sleep 50毫秒。

显然这个50毫秒是很不靠谱的,一旦系统调度有些许偏差,这里就会出问题。

典型的就是虚拟机在GC或者打印调用栈的时候会挂起所有线程,而sleep是在native层执行等待的。

也就是说在被虚拟机挂起的情况下,这个50毫秒还是会继续计数的,也就是说在这种情况下等待sleep可能起不到同步作用。

而虚拟机在重新唤醒线程时,主线程又会先被唤醒。所以只需要频繁的调用SpectrumVisualizer.enableUpdate(),

然后再给进程发singnal 3,让其打印调栈,应该很容易复现问题。

一般一段内存free完以后没被重新申请,那继续访问也不会出错,而打印调用栈的时候会用到很多堆,所以也更容易复现问题。

 

【复现问题】

在APP同学们的帮助下,找到使用SpectrumVisualizer.enableUpdate()的场景,

就是在使用"Droid LM"主题包后,进入音乐播放界面,频繁点击"播放/暂停"按钮即可。

写脚本一边不停点击"播放/暂停",一边给systemui发signal 3,一般1~2分钟内就能复现问题。

 

【解决方案】

于此同时,在aosp上找到了相关patch:

Visualizer::~Visualizer()
{
    ALOGV("Visualizer::~Visualizer()");
    if (mCaptureThread != NULL) {
        mCaptureThread->requestExitAndWait();
        mCaptureThread.clear();
    }
    mCaptureCallBack = NULL;
    mCaptureFlags = 0;
}

显然这个patch是可以解决问题的。

这里的requestExitAndWait()是同步操作,也就是在Visualizer线程退出前,主线程会一直block在Visualizer的析构函数中不会释放内存。

这种修改量比智能指针修改量少且是官方的,所以很快就决定采用这个patch。

合入版本后,再把enableUpdate()里的sleep去掉,用脚本压力测试1个多小时未复现问题,确定修改有效!

 

Virsualizer模块野指针问题分析报告