Android APP native 崩潰分析之使人困惑的 backtrace

原文地址:caikelun.io/post/2019-0…java

天衣無縫的代碼邏輯,必定能產生天衣無縫的程序嗎?答案是否認的。從軟件的層面來看,也許只有二進制才永遠不會欺騙你。linux

現象

近期,業務方反饋了一個奇怪的崩潰問題,認爲信息不足,沒法解決。android

Signal: 11 (SIGSEGV), Code: 1 (SEGV_MAPERR), fault addr 0x1

r0  993ff520  r1  dc3170c4  r2  00000000  r3  dabe3e08
r4  993ff520  r5  00000005  r6  00000290  r7  000007ac
r8  e83253a0  r9  00006aba  r10 bf921e39  r11 e83253a0
ip  bfa3a9e0  sp  993ff494  lr  bf88a71d  pc  bf96c31c

#00 pc 001a731c /data/data/com.package.name/files/download/libmcto_media_player.so
#01 pc 0020b7e5 /data/data/com.package.name/files/download/libmcto_media_player.so

#00 993ff494 0000022c
     993ff498  adcfd000  [anon:libc_malloc]
     993ff49c  bf88a71d  /data/data/com.package.name/files/download/libmcto_media_player.so
     993ff4a0  ffffffff
     993ff4a4  ffffffff
     993ff4a8  bf9d07e7  /data/data/com.package.name/files/download/libmcto_media_player.so
#01 993ff4ac 00000000
     993ff4b0  00000000
     993ff4b4  00000000
     993ff4b8  00000000
     993ff4bc  00000000
     993ff4c0  adcfd234  [anon:libc_malloc]
     993ff4c4  00000000
     993ff4c8  0000006e
     993ff4cc  00000000
     993ff4d0  adcfdf6c  [anon:libc_malloc]
     993ff4d4  00000000
     993ff4d8  00000000
     993ff4dc  00000000
     993ff4e0  00000000
     993ff4e4  00000000
     993ff4e8  00000000
複製代碼

第一感受這確定是動態庫的業務邏輯有 bug,致使了段錯誤。backtrace 確實是不完整的,但一下也看不出爲何不完整,確實須要協助分析一下。c++

分析

backtrace 不完整的常見緣由

backtrace 不完整的狀況時有發生,常見的緣由有:git

  • 崩潰時 stack 內存被大量誤寫。若是崩潰點附近的邏輯正在處理的外部輸入隨機性很大,狀況就更加糟糕,每每會看到大量離散的不完整 backtrace。舉例:
#00 pc 00000ffb <anonymous:c34fe000>
#01 pc 0009a885 /data/app/com.package.name-1/lib/arm/libjsc.so
#02 pc 0003ff93 /data/app/com.package.name-1/lib/arm/libjsc.so
#03 pc 0011f60f /data/app/com.package.name-1/lib/arm/libjsc.so
#04 pc fffffffb <unknown>
複製代碼
#00 pc 000092fe <anonymous:c15d0000>
#01 pc 00099ec3 /data/app/com.package.name-1/lib/arm/libjsc.so
#02 pc 00003ffe <anonymous:bf140000>
複製代碼
#00 pc 00000ffb <anonymous:ef304000>
複製代碼
  • 調用路徑上的某些 ELF 文件的 unwind table 不完整。好比某些系統的 odex/oat,還有系統的 WebView Chromium,都屬於此類。舉例:
#00 pc 00d12bcc /system/lib/libwebviewchromium.so
複製代碼
#00 pc 01a0cf72 /system/app/WebViewGoogle/WebViewGoogle.apk!libwebviewchromium.so (offset 0x46da000)
複製代碼
#00 pc 00006fde /data/app/com.package.name-1/lib/arm/libcros.so
#01 pc 00007007 /data/app/com.package.name-1/lib/arm/libcros.so
#02 pc 00007023 /data/app/com.package.name-1/lib/arm/libcros.so
#03 pc 00007037 /data/app/com.package.name-1/lib/arm/libcros.so
#04 pc 000070d1 /data/app/com.package.name-1/lib/arm/libcros.so
#05 pc 000049bf /data/app/com.package.name-1/lib/arm/libcros.so
#06 pc 000092e3 /data/app/com.package.name-1/oat/arm/base.odex
複製代碼
#00 pc 00013792 /system/lib/libc.so (__futex_wait_ex+49)
#01 pc 00013b21 /system/lib/libc.so (pthread_mutex_lock+310)
#02 pc 00028351 /system/lib/libc.so (dlfree+48)
#03 pc 0000ef33 /system/lib/libc.so (free+10)
#04 pc 0000a367 /system/lib/libjavacrypto.so
#05 pc 0000bc4d /system/lib/libjavacrypto.so
#06 pc 022fd081 /system/framework/arm/boot.oat
複製代碼
  • 調用路徑上的某些 ELF 文件自己損壞了,或被移除了。另外,若是崩潰點自己位於損壞的 ELF 中,有時收到的信號會是 SIGBUS。舉例:
#00 pc 5d9840f2 <unknown>
#01 pc 4008ab6c <unknown>
複製代碼
#00 pc 00392fd0 /system/lib/egl/libGLES_mali.so
#01 pc 0002ab7b /system/lib/libgui.so (_ZN7android10GLConsumer22bindTextureImageLockedEv+182)
#02 pc 0002b3a9 /system/lib/libgui.so (_ZN7android10GLConsumer14updateTexImageEv+208)
#03 pc b3317c6c <unknown>
複製代碼
  • 執行的指令位於 SharedMemory 中,此時讀取到的 ELF 內容多是不可靠的,爲了不誤導,通常都會選擇主動終止 unwind。舉例:
#00 pc 0007a010 /dev/ashmem/dalvik-jit-code-cache (deleted)
複製代碼
#00 pc 00019e64 /system/lib/libssl.so (SSL_clear+19)
#01 pc 000103b5 /system/lib/libjavacrypto.so (_ZL25NativeCrypto_SSL_shutdownP7_JNIEnvP7_jclassxP8_jobjectS4_+156)
#02 pc 00027a7d /system/framework/arm/boot-conscrypt.oat (com.android.org.conscrypt.NativeCrypto.SSL_shutdown+156)
#03 pc 00032a03 /system/framework/arm/boot-conscrypt.oat (com.android.org.conscrypt.OpenSSLSocketImpl.shutdownAndFreeSslNative+138)
#04 pc 0003330b /system/framework/arm/boot-conscrypt.oat (com.android.org.conscrypt.OpenSSLSocketImpl.close+434)
#05 pc 003e0931 /system/lib/libart.so (art_quick_invoke_stub_internal+64)
#06 pc 003e4ea3 /system/lib/libart.so (art_quick_invoke_stub+226)
#07 pc 000ac2d9 /system/lib/libart.so (_ZN3art9ArtMethod6InvokeEPNS_6ThreadEPjjPNS_6JValueEPKc+140)
#08 pc 001f27fb /system/lib/libart.so (_ZN3art11interpreter34ArtInterpreterToCompiledCodeBridgeEPNS_6ThreadEPNS_9ArtMethodEPKNS_7DexFile8CodeItemEPNS_11ShadowFrameEPNS_6JValueE+238)
#09 pc 001edd71 /system/lib/libart.so (_ZN3art11interpreter6DoCallILb0ELb0EEEbPNS_9ArtMethodEPNS_6ThreadERNS_11ShadowFrameEPKNS_11InstructionEtPNS_6JValueE+576)
#10 pc 003cce3d /system/lib/libart.so (MterpInvokeVirtualQuick+504)
#11 pc 003d6994 /system/lib/libart.so (ExecuteMterpImpl+29972)
#12 pc 001d5351 /system/lib/libart.so (_ZN3art11interpreterL7ExecuteEPNS_6ThreadEPKNS_7DexFile8CodeItemERNS_11ShadowFrameENS_6JValueEb+340)
#13 pc 001da6a3 /system/lib/libart.so (_ZN3art11interpreter33ArtInterpreterToInterpreterBridgeEPNS_6ThreadEPKNS_7DexFile8CodeItemEPNS_11ShadowFrameEPNS_6JValueE+142)
#14 pc 001edd5b /system/lib/libart.so (_ZN3art11interpreter6DoCallILb0ELb0EEEbPNS_9ArtMethodEPNS_6ThreadERNS_11ShadowFrameEPKNS_11InstructionEtPNS_6JValueE+554)
#15 pc 003cb927 /system/lib/libart.so (MterpInvokeStatic+322)
#16 pc 003d2d94 /system/lib/libart.so (ExecuteMterpImpl+14612)
#17 pc 001d5351 /system/lib/libart.so (_ZN3art11interpreterL7ExecuteEPNS_6ThreadEPKNS_7DexFile8CodeItemERNS_11ShadowFrameENS_6JValueEb+340)
#18 pc 001da6a3 /system/lib/libart.so (_ZN3art11interpreter33ArtInterpreterToInterpreterBridgeEPNS_6ThreadEPKNS_7DexFile8CodeItemEPNS_11ShadowFrameEPNS_6JValueE+142)
#19 pc 001ee931 /system/lib/libart.so (_ZN3art11interpreter6DoCallILb1ELb0EEEbPNS_9ArtMethodEPNS_6ThreadERNS_11ShadowFrameEPKNS_11InstructionEtPNS_6JValueE+420)
#20 pc 003cc9eb /system/lib/libart.so (MterpInvokeDirectRange+294)
#21 pc 003d3014 /system/lib/libart.so (ExecuteMterpImpl+15252)
#22 pc 001d5351 /system/lib/libart.so (_ZN3art11interpreterL7ExecuteEPNS_6ThreadEPKNS_7DexFile8CodeItemERNS_11ShadowFrameENS_6JValueEb+340)
#23 pc 001da6a3 /system/lib/libart.so (_ZN3art11interpreter33ArtInterpreterToInterpreterBridgeEPNS_6ThreadEPKNS_7DexFile8CodeItemEPNS_11ShadowFrameEPNS_6JValueE+142)
#24 pc 001ee931 /system/lib/libart.so (_ZN3art11interpreter6DoCallILb1ELb0EEEbPNS_9ArtMethodEPNS_6ThreadERNS_11ShadowFrameEPKNS_11InstructionEtPNS_6JValueE+420)
#25 pc 003cc9eb /system/lib/libart.so (MterpInvokeDirectRange+294)
#26 pc 003d3014 /system/lib/libart.so (ExecuteMterpImpl+15252)
#27 pc 001d5351 /system/lib/libart.so (_ZN3art11interpreterL7ExecuteEPNS_6ThreadEPKNS_7DexFile8CodeItemERNS_11ShadowFrameENS_6JValueEb+340)
#28 pc 001da6a3 /system/lib/libart.so (_ZN3art11interpreter33ArtInterpreterToInterpreterBridgeEPNS_6ThreadEPKNS_7DexFile8CodeItemEPNS_11ShadowFrameEPNS_6JValueE+142)
#29 pc 001edd5b /system/lib/libart.so (_ZN3art11interpreter6DoCallILb0ELb0EEEbPNS_9ArtMethodEPNS_6ThreadERNS_11ShadowFrameEPKNS_11InstructionEtPNS_6JValueE+554)
#30 pc 003cce3d /system/lib/libart.so (MterpInvokeVirtualQuick+504)
#31 pc 003d6994 /system/lib/libart.so (ExecuteMterpImpl+29972)
#32 pc 001d5351 /system/lib/libart.so (_ZN3art11interpreterL7ExecuteEPNS_6ThreadEPKNS_7DexFile8CodeItemERNS_11ShadowFrameENS_6JValueEb+340)
#33 pc 001da5f1 /system/lib/libart.so (_ZN3art11interpreter30EnterInterpreterFromEntryPointEPNS_6ThreadEPKNS_7DexFile8CodeItemEPNS_11ShadowFrameE+92)
#34 pc 003c0fbd /system/lib/libart.so (artQuickToInterpreterBridge+944)
#35 pc 003e46f1 /system/lib/libart.so (art_quick_to_interpreter_bridge+32)
#36 pc 000a5511 /dev/ashmem/dalvik-jit-code-cache (deleted)
複製代碼

崩潰位置的初步分析

回到問題自己。先看一下崩潰位置 001a731cgithub

.text:001A7310  STMFD   SP!, {R4,R5,LR}
.text:001A7314  LDR     R5, [R1]
.text:001A7318  MOV     R4, R0
.text:001A731C  LDR     R3, [R5,#-4] ;崩潰發生在這裏
.text:001A7320  SUB     SP, SP, #0xC
.text:001A7324  CMP     R3, #0
.text:001A7328  SUB     R0, R5, #0xC
.text:001A732C  BLT     loc_1A7350
.text:001A7330  LDR     R3, =(dword_2759D4 - 0x1A733C)
.text:001A7334  ADD     R3, PC, R3 ; dword_2759D4
.text:001A7338  CMP     R0, R3
.text:001A733C  BNE     loc_1A7364
.text:001A7340 loc_1A7340
.text:001A7340  STR     R5, [R4]
.text:001A7344  MOV     R0, R4
.text:001A7348  ADD     SP, SP, #0xC
.text:001A734C  LDMFD   SP!, {R4,R5,PC}
.text:001A7350  ADD     R1, SP, #0x18+var_14
.text:001A7354  MOV     R2, #0
.text:001A7358  BL      sub_1A6EA8
.text:001A735C  MOV     R5, R0
.text:001A7360  B       loc_1A7340
.text:001A7364  MOV     R1, #1
.text:001A7368  ADD     R0, R0, #8
.text:001A736C  BL      sub_1C2CAC
.text:001A7370  B       loc_1A7340
複製代碼

這是一個相對比較短的完整的調用過程。先壓棧了 R4R5LR,而後開始執行。執行到 LDR R3, [R5,#-4] 時發生了段錯誤,R5 的當前值是 0x5,幾乎不用查 maps 也能知道 0x1 (0x5 - 0x4 = 0x1) 這個虛擬內存地址確定是非法的,發生段錯誤是正常的。Signal code SEGV_MAPERR 和 fault addr 0x1 也徹底符合預期。web

因爲只有兩行 backtrace,咱們繼續看下一行,在 0020b7e5:bash

............
.rodata:0020B795              DCB "try_count_=%d",0
.rodata:0020B7E3 asc_20B7E3   DCB "://",0 
.rodata:0020B7E7 aCdn         DCB "CDN",0
............
複製代碼

出乎意料的是 0020b7e5 位於 .rodata 中,但這也正好解釋了爲何 unwind 過程當中斷了(backtrace 不完整)。markdown

可疑之處

再次回看崩潰位置附近的指令,確實發現了可疑之處:app

.text:001A7310  STMFD   SP!, {R4,R5,LR}
............
.text:001A731C  LDR     R3, [R5,#-4] ;崩潰發生在這裏
.text:001A7320  SUB     SP, SP, #0xC
............
.text:001A7348  ADD     SP, SP, #0xC
.text:001A734C  LDMFD   SP!, {R4,R5,PC}
複製代碼

在這個相對較短的調用過程當中,一共才使用了 24 字節的棧內存空間,可是 SP 竟然並無一次性移動到位,這是很不尋常的。

unwind table

看一下 unwind table:

$ arm-linux-androideabi-readelf -u ./libmcto_media_player.so

............

0x1a7268: 0x80b108ab
  Compact model index: 0
  0xb1 0x08 pop {r3}
  0xab      pop {r4, r5, r6, r7, r14}
  
0x1a7310: 0x8002a9b0
  Compact model index: 0
  0x02      vsp = vsp + 12
  0xa9      pop {r4, r5, r14}
  0xb0      finish
  
0x1a7424: 0x8001a8b0
  Compact model index: 0
  0x01      vsp = vsp + 8
  0xa8      pop {r4, r14}
  0xb0      finish
............
複製代碼

崩潰位置 001a731c 匹配了起始 offset 爲 1a7310 的 unwind 信息碼 0x8002a9b0,根據這裏的信息,unwind 時 SP 的值一共須要增長 24 個字節(出棧 24 個字節的數據)。可是從前述的彙編指令能夠看出,當崩潰發生時(執行到 001a731c),SP 的值只減小了 12 個字節(STMFD SP!, {R4,R5,LR}),這就是問題所在了。

再看 stack

根據 stack 中的數據:

#00 993ff494 0000022c
     993ff498  adcfd000  [anon:libc_malloc]
     993ff49c  bf88a71d  /data/data/com.package.name/files/download/libmcto_media_player.so
     993ff4a0  ffffffff
     993ff4a4  ffffffff
     993ff4a8  bf9d07e7  /data/data/com.package.name/files/download/libmcto_media_player.so
#01 993ff4ac 00000000
     993ff4b0  00000000
     993ff4b4  00000000
............
複製代碼

咱們看到 unwind 過程實際上是嚴格按照 unwind table 中的信息進行的,因而就被誤導了,SP 比實際須要的多移動了 12 個字節,真正的 LR 保存在內存地址 993ff49c 中,它的值是 bf88a71d,根據 maps 信息,咱們計算出了這個絕對地址相對於當前 ELF 的 offset,惋惜因爲業務方動態庫的邏輯比較複雜,調用層次也比較深,而 unwind 過程過早的終止了,僅憑現有的寄存器、stack 和 memory 等信息,已經不足以幫助業務方定位問題。

001a731c 處到底是什麼?

這種 「unwind table 信息」 與 「對應的彙編指令序列」 相矛盾的狀況並不常見。001a731c 處到底是什麼函數?爲何會有這樣的指令序列 ?

從業務方那裏拿到了帶調試符號的動態庫文件:

$arm-linux-androideabi-addr2line -f -e ./libmcto_media_player.so 001a731c
_ZNSsC1ERKSs
libgcc2.c:?

$arm-linux-androideabi-c++filt -n _ZNSsC1ERKSs
std::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string(std::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)
複製代碼

原來是 std::basic_string 的構造函數。

這個問題頗有多是 NDK 的 bug 形成的。從業務方那裏瞭解到,他們用的 NDK 版本是 r9d。

瞭解你使用的 NDK

開發和維護一套跨平臺的交叉編譯工具絕非易事。相對於 C 語言,編譯器須要付出不少額外的努力,才能確保 C++ 的各類語法特性在運行時可以如預期的那樣工做,C++ 標準庫長久以來也是各類不一樣版本共存的局面。要支持 Android 新版本對底層的改動,又要維護向後的兼容性。NDK 其實並不如咱們預想的那樣穩定可靠。能夠到 NDK 的 github 官方 issues 瞭解一下現狀。

NDK 從 r11 開始纔在 Changelog 裏明確的列出了重要的 Known Issues。

咱們看到在 r11 Changelog 的 Known Issues 中赫然寫到:

Exception handling will often fail when using c++_shared on ARM32. The root cause is incompatibility between the LLVM unwinder used by libc++abi for ARM32 and libgcc. This is not a regression from r10e.

r12 Changelog 的 Known Issues 中寫到:

Exception unwinding with c++_shared still does not work for ARM on Gingerbread or Ice Cream Sandwich.

咱們知道 C++ 的異常處理機制在運行時也是依賴於 unwind 的。問題應該就出在這裏了。

結論

業務方使用較新版本的 NDK 從新編譯了動態庫,咱們檢查了 std::basic_string 對應的彙編指令,發現此次 SP 在函數開頭處一次性移動到位了。應該沒有問題了。業務方上線從新編譯的動態庫,拿到完整的 backtrace 後,就能夠定位並修復這個段錯誤問題了。

所以,此次 backtrace 不完整問題的緣由是:低版本 NDK 的 bug,致使生成的動態庫在某些狀況下沒法正確的執行 unwind

根據前述 Known Issues 中的描述,不但崩潰後的 backtrace 獲取有時會受到影響,業務邏輯自身用到 C++ 異常機制的地方,也有可能會受到影響,具體來講就是:拋出異常後,可能沒法如代碼邏輯預期的那樣去逐層的執行異常捕獲邏輯。若是真的存在這樣的隱藏問題,但願此次的 NDK 版本升級也能把它們一塊兒修復了。

關於崩潰捕獲工具

最後又到了咱們的廣告時間。

上述全部的線上崩潰信息,都是使用咱們本身開發的 Android APP 崩潰捕獲工具 xCrash 捕獲的。

相關文章
相關標籤/搜索