Android Native報錯定位
今天調試Android stagefright模塊,修改MediaCodec.cpp文件時,一不小心在代碼裏寫了個空指針進去。
於是得到了下面這個報錯日誌:
--------- beginning of crash
libc : Fatal signal 11 (SIGSEGV), code 1, fault addr 0x0 in tid 5104 (MediaCodec_loop)
DEBUG : *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
DEBUG : Build fingerprint: '*******************************************'
DEBUG : Revision: '0'
DEBUG : ABI: 'arm'
DEBUG : pid: 5050, tid: 5104, name: MediaCodec_loop >>> com.google.android.exoplayer2.demo <<<
DEBUG : signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0
DEBUG : Cause: null pointer dereference
DEBUG : r0 ******** r1 ******** r2 ******** r3 ********
DEBUG : r4 ******** r5 ******** r6 ******** r7 ********
DEBUG : r8 ******** r9 ******** sl ******** fp ********
DEBUG :
DEBUG : backtrace:
DEBUG : #00 pc 00018e90 /system/lib/libc.so (__memcpy_base+185)
DEBUG : #01 pc 000446e9 /system/lib/libc.so (__sfvwrite+310)
DEBUG : #02 pc 00044943 /system/lib/libc.so (fwrite+122)
DEBUG : #03 pc 000dc169 /system/lib/libstagefright.so (_ZN7android10MediaCodec21onReleaseOutputBufferERKNS_2spINS_8AMessageEEE+1432)
DEBUG : #04 pc 000d7a59 /system/lib/libstagefright.so (_ZN7android10MediaCodec17onMessageReceivedERKNS_2spINS_8AMessageEEE+156)
想着平時分析日誌時,經常會遇到這樣的打印,因爲看不懂,通常都是直接繞過。索性今天悠閒,清掃一下知識上的戰爭迷霧吧。
源碼部分:
status_t MediaCodec::onReleaseOutputBuffer(const sp<AMessage> &msg) {
// 這是我添加的代碼 也是報空指針的代碼,代碼在MediaCodec.cpp文件中的3077行
size_t count = fwrite(heliBuf, sizeof(heliBuf) , 1, fp);
// *****************
}
日誌分析
簡單看看logcat,能夠提取到Cause: null pointer dereference
。
我們知道,是一個空指針異常導致了crash。
那麼這個空指針是在哪一行代碼調用的呢?
可以看到backtrace:
下面有crash時的調用棧信息。格式爲:
#編號 pc 異常地址 共享庫路徑 (函數名)
例1.0:
#00 pc 00018e90 /system/lib/libc.so (__memcpy_base+185)
所以,示例1.0,可以翻譯爲:在共享庫 /system/lib/libc.so的*__memcpy_base*函數中報了一個空指針。
logcat中打印的調用順序,都是從下到上看的。例如,這裏是先調用#04這一行,再調用#03… ,最後調用#00。所以,嚴格的說,#00這一行,是本次報錯的代碼。
但通常,我們編碼都會直接或間接的使用系統現有庫,而這些庫久經沙場,基本不會有什麼問題,如果#00編號的調用指向的是一個系統庫,不要猶豫,一定是你非法調用了它。
線索一
回到backtrace,#00、#01、#02行的共享庫都是libc.so,這就是一個系統庫。在#02行的函數名中,能夠得知是調用fwrite時出現了異常。
線索二
#03 pc 000dc169 /system/lib/libstagefright.so (_ZN7android10MediaCodec21onReleaseOutputBufferERKNS_2spINS_8AMessageEEE+1432)
從這裏,終於看到了我修改的代碼所屬模塊:libstagefright.so
MediaCodec.cpp 代碼目錄就位於:/frameworks/av/media/libstagefright目錄下
這裏能給我們提供的信息有:
-
異常出現在libstagefright.so庫中。
-
異常出現在_ZN7android10MediaCodec21onReleaseOutputBufferERKNS_2spINS_8AMessageEEE函數中。
從第二點那串亂碼中,大致已經可以看到發生異常的函數名稱了。但爲什麼是亂碼!!!
google了一把,原來編譯器在編譯時,對函數名做了一定優化。
既然是編譯器的事情,那一定是有一定規則,想想這規則應該也不會簡單。有沒有簡單的方法,能把這堆亂碼還原?
當然有,國外有大兄弟做了個網站專門“翻譯”這堆亂碼:https://demangler.com/
DEMANGLE 了一下,得到了這串亂碼的實際函數名:android::MediaCodec::onReleaseOutputBuffer(android::sp<android::AMessage> const&)
)
直白的表明了,這是MediaCodec.cpp中的onReleaseOutputBuffer函數。xia! xia!
這個時候,我選擇去茶水間,衝一杯…好吧,我沒有咖啡。
讓我們來理一理:
-
異常出現在libstagefright.so庫中
-
異常是一個空指針。
-
發生在
fwrite
調用中。 -
fwrite又被onReleaseOutputBuffer調用。
離真相已經很近了。我去代碼裏看了一下onReleaseOutputBuffer函數,居然只有我插入的代碼裏調用了fwrite。
爲了讓文檔繼續下去,我們來假裝onReleaseOutputBuffer函數中,有無數的fwrite調用、或者報錯的是一個第三方庫,我們並不知道源碼。怎麼知道報錯的代碼,具體在文件的哪一行呢?
addr2line使用
從名字上就可以看出,這玩意兒是將一個地址轉換成行號的工具。
位置
如果你有在用Android Studio,那麼恭喜你,可以很方便在自帶NDK目錄下找到它。
比如我的是這樣:
\Android\Sdk\ndk-bundle\toolchains\arm-linux-androideabi-4.9\prebuilt\windows-x86_64\bin\arm-linux-androideabi-addr2line.exe
沒有裝Android Studio的同學,可以自行到官網下載NKD,解壓後就可以看到了,路徑類似。
使用
arm-linux-androideabi-addr2line [option(s)] [addr(s)]
簡單來說就是arm-linux-androideabi-addr2line + 可選項 + 異常地址
先來看看可選項都有哪些:
[option(s)] | 介紹 |
---|---|
@ | 從文件中讀取options(暫時不研究) |
-a | 在結果中顯示地址addr(ps:沒啥用,因爲這個地址本來就是我們輸入命令中的) |
-b | 設置二進制文件的格式(暫不研究) |
-e | 設置輸入文件(常用:選項後面需要跟報錯的共享庫,用於addr2line程序分析) |
-i | unwind inline function(暫不研究) |
-j | Read section-relative offsets instead of addresses(暫不研究) |
-p | 讓輸出更易讀(親測,沒啥區別,可能其它選項已經很易懂了) |
-s | 在輸出中,剝離文件夾名稱(這個還不錯,前面有一長串目錄確實噁心) |
-f | 顯示函數名稱(也比較有用,可以直觀的看出是在哪個函數中報的錯) |
-C(大寫的) | 將輸出的函數名demangle(類似於demangler.com這個網站的功能) |
-h | 輸出幫助 |
-v | 輸出版本信息 |
那麼多選項,在這裏能用上(也是常用)的,也就是 -e、 -s、-C、-f了。
實戰addr2line出現“??:?”
鋪墊了那麼多,是時候試驗一下了:
C:\SuperLi>arm-linux-androideabi-addr2line -e \system\lib\libstagefright.so -s -f -C 000dc169
_ZN7android10MediaCodec21onReleaseOutputBufferERKNS_2spINS_8AMessageEEE
??:?
eum~ 出現:_ZN7android10MediaCodec17onMessageReceivedERKNS_2spINS_8AMessageEEE,加個-C選項再試試:
C:\SuperLi>arm-linux-androideabi-addr2line -e \system\lib\libstagefright.so -s -f -C 000dc169
android::MediaCodec::onReleaseOutputBuffer(android::sp<android::AMessage> const&)
??:?
??:? 蝦米,看到這個問號,我不由想起黑人問號的表情包。
只能google了:
在使用 addr2line 過程中經常會遇到 “??:?” 或 “??:0” 這種情況,原因就是一般 C/C++ SDK 都會進行添加 map 混淆,在編譯配置選項中不生成符號表 symbolic 信息,不過 AndroidStudio 會默認爲 so 文件添加符號表。
也就是說,我使用了一個不帶符號表的庫。
google說了,如果是aosp編譯的話,在out/target/product/[productname]/symbols/system/lib/****.so下面會自動生成帶了符號表的共享庫。找了找,真的有。於是我改了一下addr2line輸入文件的位置:
C:\SuperLi>arm-linux-androideabi-addr2line -e \symbols\system\lib\libstagefright.so -s -f -C 000dc169
fwrite(void const*, unsigned int pass_object_size0, unsigned int, __sFILE*)
stdio.h:375
Bingo!!!成功的去掉了討厭的黑人問號(??:?)。但是爲什麼函數名稱不一樣了?(哪位大神知道原因,求解惑啊)索性把和libstagefright.so相關的addr都加進來看看吧:
C:\SuperLi>arm-linux-androideabi-addr2line -e \symbols\system\lib\libstagefright.so -s -f -C 000dc169
fwrite(void const*, unsigned int pass_object_size0, unsigned int, __sFILE*)
stdio.h:375
android::MediaCodec::onMessageReceived(android::sp<android::AMessage> const&)
MediaCodec.cpp:2508
結果中可以看出,報錯的地方在MediaCodec.cpp文件的2508行。
推了推鼻樑上不存在的眼鏡,嘴角扯出一抹弧度:小樣,我找到你了。
然鵝,在MediaCodec.cpp文件中ctrl + g 了一把。我擦,2508行代碼居然只是這個:
我是2508行: status_t err = onReleaseOutputBuffer(msg);
而我們真實報錯的地方是在3077行。
因爲fwrite在onReleaseOutputBuffer函數中只有一處調用,我們能通過閱讀onReleaseOutputBuffer函數的代碼,將異常鎖定在3077行,但這只是妥協的方法。如果onReleaseOutputBuffer函數中有無數的fwrite怎麼搞?暫時能想到的只有加log一步步調試了。
好吧,目前也只能做到這一步了。
小結
1、通過實驗addrline + symbolic版本的so,只能將異常定位到庫中的函數級別,而不能具體到行號
2、要精準定位,還需要結合上下文才能確定。
那麼,所有的庫都需要這麼麻煩嗎?
準備使用Android studio來測試一下。
android studio編譯的庫
打開Android studio,通過File > New > New Project > Native C++ … 創建一個Test 工程。
在自動生成的native-lib.cpp 文件的Java_com_dali_myapplication_MainActivity_stringFromJNI函數中,故意加入了一個a = b / c
的除零異常。
native-lib.cpp 代碼如下:
#include <jni.h>
#include <string>
using namespace std;
extern "C" JNIEXPORT jstring JNICALL
Java_com_dali_myapplication_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
string hello = "Hello from C++";
int b = 3, c = 0;
int a = b / c;
return env->NewStringUTF(hello.c_str());
}
Activity代碼如下:
public class MainActivity extends AppCompatActivity {
static {
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView tv = findViewById(R.id.sample_text);
tv.setText(stringFromJNI());
}
public native String stringFromJNI();
}
運行後,立馬報錯,關鍵日誌如下:
DEBUG : signal 8 (SIGFPE), code -6 (SI_TKILL), fault addr --------
DEBUG : r0 00000000 r1 000016aa r2 00000008 r3 8efbcbe4
DEBUG : r4 8efbcbe4 r5 00000001 r6 00000000 r7 0000010c
DEBUG : r8 0000004e r9 8dc10000 sl 9eadb790 fp 8dc10000
DEBUG : ip 9eadb768 sp 9eadb6c8 lr 79a1ee9b pc 8ef4c964 cpsr 00000010
DEBUG :
DEBUG : backtrace:
DEBUG : #00 pc 0004a964 /system/lib/libc.so (tgkill+12)
DEBUG : #01 pc 0001ce97 /data/app/com.dali.myapplication-H8qoqUb6cjv0hUDfGYRtRQ==/lib/arm/libnative-lib.so
DEBUG : #02 pc 000064e1 /data/app/com.dali.myapplication-H8qoqUb6cjv0hUDfGYRtRQ==/lib/arm/libnative-lib.so (Java_com_dali_myapplication_MainActivity_stringFromJNI+108)
使用addr2line運行一下:
C:\SuperLi>arm-linux-androideabi-addr2line -e libnative-lib.so 000064e1 -s
native-lib.cpp:11
可以看到,Android studio中編譯出來的so,完全沒有各種困擾,很直接的指出的異常發生的行號。
原因是,Android studio會在編譯時,自動生成附帶有符號表的so。
其它
在對應庫的編譯目錄下的Android.mk文件中,增加gcc警告和調試標誌:
LOCAL_CFLAGS := -Wl,-Map=test.map -g
可能有不一樣的效果,爲啥說不一樣的效果呢?因爲俺們沒驗證,暫時感覺沒需求。
總結
1、使用addr2line需要附帶符號表的so文件。
2、addr2line工具並不是一定能將異常位置定位到行,還需要其它調試手段輔助。
3、如果c/c++庫程序是使用Android studio開發,因爲生成的so自帶符號表,所以使用addr2line工具可以直接定位。