1. 程式人生 > >Virsualizer模塊野指針問題分析報告

Virsualizer模塊野指針問題分析報告

androi ibm kill obj mut 出錯 mea 打印 eth

【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模塊野指針問題分析報告