原文地址: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 不完整的狀況時有發生,常見的緣由有:git
#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> 複製代碼
#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 複製代碼
#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> 複製代碼
#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) 複製代碼
回到問題自己。先看一下崩潰位置 001a731c
:github
.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 複製代碼
這是一個相對比較短的完整的調用過程。先壓棧了 R4
、R5
、LR
,而後開始執行。執行到 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:
$ 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 中的數據:
#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 等信息,已經不足以幫助業務方定位問題。
這種 「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。
開發和維護一套跨平臺的交叉編譯工具絕非易事。相對於 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 捕獲的。