最近協助分析了一個audioserver crash的問題,堆棧如下:
DEBUG : Cmdline: /system/bin/audioserver
DEBUG : pid: 26089, tid: 26136, name: binder:26089_4 >>> /system/bin/audioserver <<<
DEBUG : uid: 1041
DEBUG : tagged_addr_ctrl: 0000000000000001 (PR_TAGGED_ADDR_ENABLE)
DEBUG : signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0000000000000000
DEBUG : Cause: null pointer dereference
DEBUG : x0 0000000000000000 x1 000000000000005c x2 0000000000000000 x3 0000000000000000
DEBUG : x4 0000000000000000 x5 8080808080808080 x6 fefefefefefefeff x7 7f7f7f7f7f7f7f7f
DEBUG : x8 a7a790ac07c2d30e x9 a7a790ac07c2d30e x10 0000000000000000 x11 0000000000000001
DEBUG : x12 0000006fde42d5d0 x13 0000000000000001 x14 0000000000000000 x15 000000729470a662
DEBUG : x16 00000072947a9d78 x17 00000072947301c0 x18 0000006fdd444000 x19 00000000000007cf
DEBUG : x20 0000006fde42dcd0 x21 0000000080000008 x22 0000000000000000 x23 b4000070cd42b7f0
DEBUG : x24 0000006fde42d9f0 x25 0000006fde42d980 x26 000000729a8dfee8 x27 000000729a8e1da0
DEBUG : x28 0000006fde42db10 x29 0000006fde42dc50
DEBUG : lr 000000729a8874a4 sp 0000006fde42d860 pc 000000729a887574 pst 0000000060001000
DEBUG : backtrace:
DEBUG : #00 pc 0000000000035574 /system/lib64/libaudiopolicyenginedefault.so (android::audio_policy::Engine::getDeviceForInputSource(audio_source_t) const+5404) (BuildId: dedb284e4a36dc5488c54ec52bf97f75)
DEBUG : #01 pc 000000000002b4c4 /system/lib64/libaudiopolicyenginedefault.so (android::audio_policy::Engine::getInputDeviceForAttributes(audio_attributes_t const&, unsigned int, android::sp<android::AudioPolicyMix>*) const+672) (BuildId: dedb284e4a36dc5488c54ec52bf97f75)
...
從堆棧上能看出來是在getDeviceForInputSource方法里面出現了空指針導致奔潰。如果可以找到帶符號表的so那么可以通過android-addr2line -e 帶符號表so路徑 0000000000035574
命令直接定位到是c++哪一行源碼出現的空指針。
例如這篇筆記最后的那個崩潰堆棧,用addr2line查看000000000009c758地址就能得到奔潰在native-api.cpp的57行(這部分詳見我之前的博客就不過多贅述了):
/Users/linjw/Library/Android/sdk/ndk/22.1.7171670/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-addr2line -e ./app/.cxx/cmake/debug/arm64-v8a/lib/libdemo-native-api.so 000000000009c758
/Users/linjw/workspace/Demo/app/.cxx/cmake/debug/arm64-v8a/../../../../src/main/cpp/native-api.cpp:57
但是這里是發生在系統庫里,我們這邊不會保留系統的編譯中間產物(各種帶符號表的so),而且通過file命令也可以看到系統里的libaudiopolicyenginedefault.so顯示stripped
代表是已去除符號表的:
$ file /system/lib64/libaudiopolicyenginedefault.so
/system/lib64/libaudiopolicyenginedefault.so: ELF shared object, 64-bit LSB arm64, for Android 33, BuildID=2dcd2508ac02afde1acb25f0e6601435, stripped
如果沒有去除符號表,會顯示顯示not stripped
:
file libtest.so
libtest.so: ELF shared object, 64-bit LSB arm64, for Android 21, built by NDK r22b (7171670), BuildID=e678e5b301afc7fadfa7cd6b56e48c6223ed671e, not stripped
硬是要做的話可能需要切到這個軟件的commit號本地編譯,看看是否能編出對應的帶符號so。但是這個編譯環境和構建服務器的不一樣不一定編出來的是和之前正式的軟件一模一樣的,另外編譯也十分耗時。
所以只能先從日志的上下文還有源代碼進行分析,從日志里面有看到下面的日志:
11-27 11:39:51.021 1041 26153 26195 W APM::AudioPolicyEngine: audiopolicydebug getDeviceForInputSource user selected off mic
而對應源代碼里面有這樣的代碼,由于還會判斷inputSource,只能說可能是這個原因,如果能看到inputSource的值確認會進入這個if判斷的話才能百分百確認:
sp<DeviceDescriptor> Engine::getDeviceForInputSource(audio_source_t inputSource) const
{
if (...) {
ALOGW("audiopolicydebug %s user selected off mic", __func__);
device = nullptr;
}
...
if(AUDIO_SOURCE_HOTWORD == inputSource) {
if(device->type() == AUDIO_DEVICE_IN_BUILTIN_MIC) {
...
}
}
...
}
從源碼看到AUDIO_SOURCE_HOTWORD
的值是1999:
AUDIO_SOURCE_HOTWORD = 1999;
而1999=0x7cf,從堆棧上看x19寄存器的值剛好就是00000000000007cf
,所以基本能確定就是這個原因了,就算中間其他的代碼沒有問題走到這個if里面也會奔潰。
寄存器
我原本以為函數的入參都能在奔潰堆棧的寄存器信息里面看到,但是后面自己做了下實驗發現還不一定能在寄存器里面看到參數的值。我們先從正常的情況來看如下面的代碼(debug函數里面的這么多LOGD是為了增加行數防止內聯):
void debug(int i, const char *str) {
int x = 0xaaaa;
int y = i + str[0] + x;
LOGD("y=%d", y);
LOGD("y=%d", y);
LOGD("y=%d", y);
LOGD("y=%d", y);
LOGD("y=%d", y);
LOGD("y=%d", y);
LOGD("y=%d", y);
LOGD("y=%d", y);
LOGD("y=%d", y);
LOGD("y=%d", y);
}
debug(0xabcd, nullptr);
運行之后就能看到空指針奔潰,從寄存器信息也能看到x0寄存器的值000000000000abcd就是我們的參數值:
DEBUG : tagged_addr_ctrl: 0000000000000001 (PR_TAGGED_ADDR_ENABLE)
DEBUG : signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0000000000000000
DEBUG : Cause: null pointer dereference
DEBUG : x0 000000000000abcd x1 0000000000000000 x2 0000000000000000 x3 0000000000000000
DEBUG : x4 0000007487673000 x5 0000007fc0cfa740 x6 0000007fc0cfa718 x7 0000000000000000
DEBUG : x8 984d9d4eddfe2a3b x9 984d9d4eddfe2a3b x10 0000000000000007 x11 0000007fc0cfa478
DEBUG : x12 0000007fc0cfa360 x13 0000007fc0cfa330 x14 000000000000000a x15 00000000ebad6a89
DEBUG : x16 000000714f39a460 x17 000000714f1fa2ec x18 00000074883f8000 x19 b400007291553190
DEBUG : x20 0000000000000000 x21 0000000000000000 x22 0000007484938dce x23 0000000000001071
DEBUG : x24 00000071cdc00880 x25 0000007487673000 x26 00000000705d08a0 x27 0000007fc0cfb968
DEBUG : x28 0000007fc0cfb860 x29 0000007fc0cfb790
DEBUG : lr 000000714f1fa964 sp 0000007fc0cfb790 pc 000000714f1fa2fc pst 0000000060001000
DEBUG : backtrace:
DEBUG : #00 pc 00000000001ee2fc /data/app/~~n278xi-BFwNh8wPmNeFUPA==/me.linjw.demo-bF94GJVWCMKCefwoNGhsrQ==/base.apk!libdemo-native-api.so (debug(int, char const*)+16) (BuildId: ca1c32722869b1a536437fd445250401017034de)
DEBUG : #01 pc 00000000001ee960 /data/app/~~n278xi-BFwNh8wPmNeFUPA==/me.linjw.demo-bF94GJVWCMKCefwoNGhsrQ==/base.apk!libdemo-native-api.so (BuildId: ca1c32722869b1a536437fd445250401017034de)
DEBUG : #02 pc 000000000021a354 /apex/com.android.art/lib64/libart.so (art_quick_generic_jni_trampoline+148) (BuildId: 7185f17e1e47100e6396535885066af5)
...
我們可以通過objdump對so進行反匯編,這個工具在ndk包里面,例如我的機器上可以通過下面命令將so反匯編輸出到asm.txt:
/Users/linjw/Library/Android/sdk/ndk/22.1.7171670/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-objdump -d libdemo-native-api.so > asm.txt
直接搜奔潰執行的匯編地址(1ee2fc)定位到對應代碼:
00000000001ee2ec <_Z5debugiPKc@@Base>:
1ee2ec: a9bd7bfd stp x29, x30, [sp,#-48]!
1ee2f0: f9000bf5 str x21, [sp,#16]
1ee2f4: a9024ff4 stp x20, x19, [sp,#32]
1ee2f8: 910003fd mov x29, sp
1ee2fc: 39400028 ldrb w8, [x1]
1ee300: 52955549 mov w9, #0xaaaa // #43690
1ee304: f0fffc74 adrp x20, 17d000 <_ZN3MD511HEX_NUMBERSE@@Base+0x2d30>
1ee308: d0fffc75 adrp x21, 17c000 <_ZN3MD511HEX_NUMBERSE@@Base+0x1d30>
1ee30c: 0b080008 add w8, w0, w8
1ee310: 0b090113 add w19, w8, w9
...
...
...
00000000001ee888 <_ZN7_JNIEnv16CallObjectMethodEP8_jobjectP10_jmethodIDz@@Base>:
...
1ee950: 529579a0 mov w0, #0xabcd // #43981
1ee954: aa1f03e1 mov x1, xzr
1ee958: aa0203f4 mov x20, x2
1ee95c: f81f83a8 stur x8, [x29,#-8]
1ee960: 9405a7f0 bl 358920 <_Z5debugiPKc@plt>
...
1ee2fc
的匯編代碼為ldrb w8, [x1]
將存儲器地址為x1的字節數據讀入寄存器w8,而x1的值在奔潰堆棧里面可以看到的確是x1 0000000000000000
。
返回堆棧上一級的1ee960
附近可以看到1ee950這里會將0xabcd設置到w0寄存器。
AArch64架構提供了31個通用寄存器,每個寄存器都可以用作64位X寄存器(X0~X30)或32位W寄存器(W0~W30),所以w0其實就是x0寄存器,在堆棧里面可以看到的確是x0 000000000000abcd
然后在debug函數里面可以看到后面會用w8去做加法:
1ee30c: 0b080008 add w8, w0, w8
1ee310: 0b090113 add w19, w8, w9
所以參數在前一級的函數里面設置到了寄存器里面,然后在debug函數里面就能直接使用了。
從arm的官方文檔可以看到各個寄存器的作用:
X0-X7是參數和結果寄存器,函數的參數和返回值由它們去傳遞,正常情況下可以在這里看到參數的值;
但是函數中間的臨時變量也可以用它們去保存,如果甚至在參數使用完成之后可以修改掉保存參數的寄存器的值,而X19-X28則是Callee-saved寄存器,當函數退棧的時候需要恢復回去,官方文檔里面是這么說的:
For example, the function foo() can use registers X0 to X15 without needing to preserve their values. However, if foo() wants to use X19 to X28 it must save them to stack first, and then restore from the stack before returning.
另外就是我從一些博客里面看到Callee-saved也會用于傳參(雖然在arm的官方資料里面沒有看到).類似一開始看到的audioserver奔潰寄存器信息里面inputSource的值并沒有存到X0-X15,而是被存到了X19寄存器里面(因為這種奇怪數字能剛好撞中的幾率還是蠻低的)。畢竟編譯器為了優化代碼執行效率可什么事情都做得出來,既然都把數據放到了X19寄存器了,也沒有必要再在X0-X15里面也放多一份。
還有就是如果參數比較多的時候寄存器放不下也會通過壓棧的方式去傳參,這部分參數在還沒有用到的時候也不會在寄存器信息中看到。
編譯器優化
由于編譯器會對代碼做各種優化所以有時候甚至會將函數調用給優化掉,就更難從寄存器信息里面看到參數的值了。
void debug(int i, const char *str) {
int x = 0xaaaa;
int y = i + str[0] + x;
LOGD("y=%d", y);
// LOGD("y=%d", y);
// LOGD("y=%d", y);
// LOGD("y=%d", y);
// LOGD("y=%d", y);
// LOGD("y=%d", y);
// LOGD("y=%d", y);
// LOGD("y=%d", y);
// LOGD("y=%d", y);
// LOGD("y=%d", y);
}
debug(0xabcd, nullptr);
例如我將其他的LOGD都注釋掉只剩下一個的話,奔潰堆棧就變成了下面的樣子,libdemo-native-api.so里面的函數堆棧只剩下了一層了,所以函數被內聯了,所以沒有函數調用的過程:
DEBUG : tagged_addr_ctrl: 0000000000000001 (PR_TAGGED_ADDR_ENABLE)
DEBUG : signal 5 (SIGTRAP), code 1 (TRAP_BRKPT), fault addr 0x0000007151f31758
DEBUG : x0 b400007291553190 x1 00000071be622180 x2 0000000000000000 x3 0000000000000000
DEBUG : x4 0000007487673000 x5 0000007fc0cfa740 x6 0000007fc0cfa718 x7 0000000000000000
DEBUG : x8 984d9d4eddfe2a3b x9 984d9d4eddfe2a3b x10 0000000000000007 x11 0000007fc0cfa478
DEBUG : x12 0000007fc0cfa360 x13 0000007fc0cfa330 x14 000000000000000a x15 00000000ebad6a89
DEBUG : x16 0000007151f31758 x17 0000007fc0cfb850 x18 00000074883f8000 x19 b40000734154f380
DEBUG : x20 0000000000000000 x21 0000000000000000 x22 0000007484938dce x23 0000000000001071
DEBUG : x24 00000071cdc00880 x25 0000007fc0cfb968 x26 00000000705d08a0 x27 0000007fc0cfb968
DEBUG : x28 0000007fc0cfb860 x29 0000007fc0cfb860
DEBUG : lr 00000071cdc1a358 sp 0000007fc0cfb850 pc 0000007151f31758 pst 0000000060001000
DEBUG : backtrace:
DEBUG : #00 pc 000000000009c758 /data/app/~~cluxSX8rLOQhNQR0I2DMFA==/me.linjw.demo-ym0C4_78KOXiumCBzhKjpQ==/base.apk!libdemo-native-api.so (BuildId: 16aa24ceb636e7bcae7459d6e1403c8a2fa209ca)
DEBUG : #01 pc 000000000021a354 /apex/com.android.art/lib64/libart.so (art_quick_generic_jni_trampoline+148) (BuildId: 7185f17e1e47100e6396535885066af5)
...
然后由于從編譯器看來內聯之后的代碼都沒有機會用到i這個參數,所以直接優化掉了,在反匯編的代碼里面都搜索不到0xabcd的賦值,然后生成的代碼這里直接一個BRK生成斷點異常:
000000000009c6b8 <_ZN7_JNIEnv16CallObjectMethodEP8_jobjectP10_jmethodIDz@@Base>:
...
9c758: d4200020 brk #0x1
所以我們這里看到的堆棧也不是空指針異常,而是TRAP_BRKPT
異常。
另外更加神奇的是如果我直接將所有的LOGD都注釋掉,編譯器判斷這個代碼沒有任何的作用,則會直接都優化掉,然后我們運行的時候程序就會正常運行不會崩潰...
void debug(int i, const char *str) {
int x = 0xaaaa;
int y = i + str[0] + x;
// LOGD("y=%d", y);
// LOGD("y=%d", y);
// LOGD("y=%d", y);
// LOGD("y=%d", y);
// LOGD("y=%d", y);
// LOGD("y=%d", y);
// LOGD("y=%d", y);
// LOGD("y=%d", y);
// LOGD("y=%d", y);
// LOGD("y=%d", y);
}
debug(0xabcd, nullptr);
當然上面所說的優化都屬于編譯器的行為不在c/c++的語言規范里面,不同的編譯器編譯出來的結果可能不一樣。
另外說一句題外話,這種編譯器優化有時候會引起難以理解的bug,就類似下面這個表情包的BUG:
不得不感慨一句,c++真的太難了...