Android NDK Crash定位分析之寄存器

最近協助分析了一個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的官方文檔可以看到各個寄存器的作用:

1.png

X0-X7是參數和結果寄存器,函數的參數和返回值由它們去傳遞,正常情況下可以在這里看到參數的值;

2.jpg

但是函數中間的臨時變量也可以用它們去保存,如果甚至在參數使用完成之后可以修改掉保存參數的寄存器的值,而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:

3.png

不得不感慨一句,c++真的太難了...

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,488評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,034評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,327評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,554評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,337評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,883評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,975評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,114評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,625評論 1 332
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,555評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,737評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,244評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,973評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,615評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,343評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,699評論 2 370

推薦閱讀更多精彩內容