天天看點

Android APP native 崩潰分析之 linker SIGBUS 崩潰現象分析結論關于崩潰捕獲工具

原文位址:https://caikelun.io/post/2019-05-31-android-app-native-crash-linker-sigbus/

這是 Android APP native 崩潰分析系列文章的第一篇。最近分析了一例線上的 Android linker SIGBUS 崩潰,在這裡記錄一下。

現象

現象 1

Signal: 7 (SIGBUS), Code: 2 (BUS_ADRERR)

r0  799963d8  r1  00000000  r2  00000be8  r3  3d800000
r4  6e1d5094  r5  00000003  r6  bebe53a4  r7  79998000
r8  ffffffff  r9  00000000  r10 799963d8  r11 00000000
ip  2670c8d5  sp  bebe5364  lr  26707915  pc  26708d54

#00 pc 00004d54  /system/bin/linker
#01 pc 00003911  /system/bin/linker
#02 pc 00003be5  /system/bin/linker
#03 pc 000023c1  /system/bin/linker
#04 pc 000029eb  /system/bin/linker
#05 pc 00000f43  /system/bin/linker
#06 pc 00052d97  /system/lib/libdvm.so (_Z17dvmLoadNativeCodePKcP6ObjectPPc+182)
#07 pc 0006a625  /system/lib/libdvm.so
#08 pc 000297e0  /system/lib/libdvm.so
#09 pc 00030c6c  /system/lib/libdvm.so (_Z11dvmMterpStdP6Thread+76)
#10 pc 0002e304  /system/lib/libdvm.so (_Z12dvmInterpretP6ThreadPK6MethodP6JValue+184)
#11 pc 00063719  /system/lib/libdvm.so (_Z15dvmInvokeMethodP6ObjectPK6MethodP11ArrayObjectS5_P11ClassObjectb+392)
#12 pc 0006b61f  /system/lib/libdvm.so
#13 pc 000297e0  /system/lib/libdvm.so
#14 pc 00030c6c  /system/lib/libdvm.so (_Z11dvmMterpStdP6Thread+76)
#15 pc 0002e304  /system/lib/libdvm.so (_Z12dvmInterpretP6ThreadPK6MethodP6JValue+184)
#16 pc 00063435  /system/lib/libdvm.so (_Z14dvmCallMethodVP6ThreadPK6MethodP6ObjectbP6JValueSt9__va_list+336)
#17 pc 0004cbb7  /system/lib/libdvm.so
#18 pc 0004dc37  /system/lib/libandroid_runtime.so
#19 pc 0004e95b  /system/lib/libandroid_runtime.so (_ZN7android14AndroidRuntime5startEPKcS2_+354)
#20 pc 0000105b  /system/bin/app_process
#21 pc 0000e49b  /system/lib/libc.so (__libc_init+50)
#22 pc 00000d7c  /system/bin/app_process
at java.lang.Runtime.nativeLoad(Native Method)
at java.lang.Runtime.doLoad(Runtime.java:435)
at java.lang.Runtime.load(Runtime.java:336)
at java.lang.System.load(System.java:533)
............

#00  bebe5364  799963d8  /data/data/com.package.name/files/download/libmctocurl.so
#01  bebe5368  0000004f
     bebe536c  00145000
     bebe5370  bebe53a4
     bebe5374  70ccfa00
     bebe5378  29648240  /dev/ashmem/dalvik-heap (deleted)
     bebe537c  27b9c8f0
     bebe5380  00000000
     bebe5384  00000001
     bebe5388  27c5cc38  /system/lib/libdvm.so (dvmCompilerTemplateStart+380)
     bebe538c  26707be9  /system/bin/linker
#02  bebe5390  00000000
     bebe5394  267063c5  /system/bin/linker
#03  bebe5398  27c62e18  /system/lib/libdvm.so
     bebe539c  69a75b1c
     bebe53a0  0000000c
     bebe53a4  70ccfa00
............
           

基本都出現在 Android 4.x 以及更低版本的裝置上,dvm 調用 linker 加載動态庫時發生了崩潰。由于 Android 早期的系統庫為了壓縮體積 strip 掉了大量的符号資訊,是以僅從 backtrace 能擷取到的資訊偏少。

現象 2

Signal: 7 (SIGBUS), Code: 2 (BUS_ADRERR)

r0  00000000  r1  00000000  r2  000008d8  r3  a4ec86e8
r4  87e16094  r5  00000003  r6  a47034ec  r7  a4ed6000
r8  ffffffff  r9  00000000  r10 a4ec86e8  r11 00000000
ip  00000000  sp  a4703484  lr  400040fb  pc  40004870

#00 pc 00004870  /system/bin/linker (__dl_memset+48)
#01 pc 000040f7  /system/bin/linker (__dl__ZN9ElfReader12LoadSegmentsEv+238)
#02 pc 00004569  /system/bin/linker (__dl__ZN9ElfReader4LoadEPK17android_dlextinfo+40)
#03 pc 000023b3  /system/bin/linker (__dl__ZL12find_libraryPKciPK17android_dlextinfo+574)
#04 pc 00002543  /system/bin/linker (__dl__Z9do_dlopenPKciPK17android_dlextinfo+122)
#05 pc 00000e99  /system/bin/linker (__dl__ZL10dlopen_extPKciPK17android_dlextinfo+24)
#06 pc 001d4697  /system/lib/libart.so (_ZN3art9JavaVMExt17LoadNativeLibraryERKNSt3__112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEENS_6HandleINS_6mirror11ClassLoaderEEEPS7_+498)
#07 pc 001fa4a9  /system/lib/libart.so (_ZN3artL18Runtime_nativeLoadEP7_JNIEnvP7_jclassP8_jstringP8_jobjectS5_+480)
#08 pc 02324cc1  /system/framework/arm/boot.oat
at java.lang.Runtime.nativeLoad(Native Method)
at java.lang.Runtime.doLoad(Runtime.java:429)
at java.lang.Runtime.load(Runtime.java:330)
at java.lang.System.load(System.java:982)
............

#00  a4703484  a4ec86e8  /data/data/com.package.name/files/download/libcupid.so
#01  a4703488  000000e5
     a470348c  0079a000
     a4703490  00000000
     a4703494  a47034ec
     a4703498  00000000
     a470349c  00000000
     a47034a0  9706b430
     a47034a4  00000000
     a47034a8  a47034ec
     a47034ac  a4703548
     a47034b0  00000001
     a47034b4  4000456d  /system/bin/linker (__dl__ZN9ElfReader4LoadEPK17android_dlextinfo+44)
#02  a47034b8  00000000
     a47034bc  00000000
     a47034c0  0000b314
     a47034c4  400023b7  /system/bin/linker (__dl__ZL12find_libraryPKciPK17android_dlextinfo+578)
#03  a47034c8  427f9fec
     a47034cc  4246ca58
     a47034d0  4245c180
     a47034d4  9f45a800
............
           

基本都出現在 Android 5.x 的裝置上,art 調用 linker 加載動态庫時發生了崩潰。随着 Android 裝置硬體配置的更新,對于系統庫的符号多占用幾兆 flash 空間已經不那麼敏感,我們也能更加直覺的從 backtrace 中看到 linker 中具體的崩潰位置。

現象 3

Signal: 7 (SIGBUS), Code: 2 (BUS_ADRERR)

r0  00000000  r1  00000000  r2  000008d8  r3  94ca06e8
r4  a5c67094  r5  00000003  r6  be837874  r7  94cae000
r8  ffffffff  r9  00000000  r10 94ca06e8  r11 00000001
ip  00000000  sp  be837814  lr  b6f8a069  pc  b6f8a7e0

#00 pc 000047e0  /system/bin/linker (__dl_memset+48)
#01 pc 00004065  /system/bin/linker (__dl__ZN9ElfReader12LoadSegmentsEv+232)
#02 pc 000044d9  /system/bin/linker (__dl__ZN9ElfReader4LoadEPK17android_dlextinfo+40)
#03 pc 000022f3  /system/bin/linker (__dl__ZL12find_libraryPKciPK17android_dlextinfo+578)
#04 pc 00002489  /system/bin/linker (__dl__Z9do_dlopenPKciPK17android_dlextinfo+128)
#05 pc 00000eb5  /system/bin/linker (__dl__ZL10dlopen_extPKciPK17android_dlextinfo+24)
#06 pc 00020d5d  /data/data/com.package.name/files/download/libCube.so
#07 pc 00020599  /data/data/com.package.name/files/download/libCube.so
#08 pc 00024491  /data/data/com.package.name/files/download/libCube.so
#09 pc 000244fb  /data/data/com.package.name/files/download/libCube.so
#10 pc 0003c159  /data/data/com.package.name/files/download/libCube.so
#11 pc 0003da23  /data/data/com.package.name/files/download/libCube.so
#12 pc 00016cd1  /data/data/com.package.name/files/download/libCube.so
#13 pc 00020b81  /data/data/com.package.name/files/download/libCube.so (InitHCDNDownloaderCreator+140)
#14 pc 0004671f  /data/data/com.package.name/files/download/libCube.so (Java_com_package_name_hcdndownloader_HCDNDownloaderCreator_InitCubeCreatorNative+322)
#15 pc 051d3453  /data/data/com.package.name/tinker/patch-2f63d418/odex/tinker_classN.dex
at com.package.name.HCDNDownloaderCreator.InitCubeCreatorNative(Native Method)
at com.package.name.HCDNDownloaderCreator.InitCubeCreator(Unknown Source)
at com.package.name.download.CubeLoadManager.b(Unknown Source)
............

#00  be837814  94ca06e8  /data/data/com.package.name/files/download/libHCDNClientNet.so
#01  be837818  0000002a
     be83781c  00792000
     be837820  be837874
     be837824  00000000
     be837828  00000000
     be83782c  b818ceec  [heap]
     be837830  00000000
     be837834  be837874
     be837838  be8378d0
     be83783c  b6f8a4dd  /system/bin/linker (__dl__ZN9ElfReader4LoadEPK17android_dlextinfo+44)
#02  be837840  00000000
     be837844  00000000
     be837848  00010300
     be83784c  b6f882f7  /system/bin/linker (__dl__ZL12find_libraryPKciPK17android_dlextinfo+582)
#03  be837850  00000000
     be837854  00000000
     be837858  00000000
     be83785c  00000000
............

............
943e8000-944e4000 rw-       0   fc000 [stack:8572]
944e4000-94c77000 r-x       0  793000 /data/data/com.package.name/files/download/libHCDNClientNet.so
94c77000-94ca1000 rw-  792000   2a000 /data/data/com.package.name/files/download/libHCDNClientNet.so
94ca1000-94cae000 ---       0    d000 
............
94eab000-94fa8000 rw-       0   fd000 [stack:8568]
94fa8000-9508f000 r-x       0   e7000 /data/data/com.package.name/files/download/libCube.so
9508f000-95094000 r--   e6000    5000 /data/data/com.package.name/files/download/libCube.so
95094000-95095000 rw-   eb000    1000 /data/data/com.package.name/files/download/libCube.so
95095000-95096000 rw-       0    1000 
............
b6f85000-b6f86000 r-x       0    1000 [sigpage]
b6f86000-b6f93000 r-x       0    d000 /system/bin/linker
b6f93000-b6f94000 r--    c000    1000 /system/bin/linker
b6f94000-b6f95000 rw-    d000    1000 /system/bin/linker
b6f95000-b6f96000 rw-       0    1000 
............
           

基本都出現在 Android 5.x 以及更低版本的裝置上(這裡的例子是發生在 5.x 上),某個動态庫調用 linker 加載另一個動态庫時發生了崩潰。

stack 和 maps 都太長了,隻列了一部分。

分析

是 APP 自己的 bug ?還是特定 OS 版本或機型的相容性問題?為什麼 Android 6.x 及以上版本的系統中幾乎沒有這個崩潰? 有很多疑問。

經過分析,我們發現這三個現象的本質是一樣的,這裡僅針對現象 3 進行分析。

ELF 資訊和 maps 分析

通過機型比對和 ELF build-id 比對,拿到了和崩潰裝置相同的 linker 二進制檔案,另外兩個動态庫我們自己的伺服器中有。所有 ELF 都是 armeabi 架構的。

linker:

$ arm-linux-androideabi-readelf -l ./linker

Elf file type is DYN (Shared object file)
Entry point 0xa18
There are 9 program headers, starting at offset 52

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  PHDR           0x000034 0x00000034 0x00000034 0x00120 0x00120 R   0x4
  INTERP         0x000154 0x00000154 0x00000154 0x00013 0x00013 R   0x1
      [Requesting program interpreter: /system/bin/linker]
  LOAD           0x000000 0x00000000 0x00000000 0x0c444 0x0c444 R E 0x1000
  LOAD           0x00ca5c 0x0000da5c 0x0000da5c 0x00734 0x01c60 RW  0x1000
  DYNAMIC        0x00cef8 0x0000def8 0x0000def8 0x000c0 0x000c0 RW  0x4
  GNU_EH_FRAME   0x00c2e8 0x0000c2e8 0x0000c2e8 0x0015c 0x0015c R   0x4
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0
  EXIDX          0x009060 0x00009060 0x00009060 0x00660 0x00660 R   0x4
  GNU_RELRO      0x00ca5c 0x0000da5c 0x0000da5c 0x005a4 0x005a4 RW  0x4

 Section to Segment mapping:
  Segment Sections...
   00     
   01     .interp 
   02     .interp .dynsym .dynstr .hash .rel.dyn .rel.plt .plt .text .ARM.exidx .rodata .ARM.extab .eh_frame .eh_frame_hdr 
   03     .data.rel.ro.local .init_array .dynamic .got .data .bss 
   04     .dynamic 
   05     .eh_frame_hdr 
   06     
   07     .ARM.exidx 
   08     .data.rel.ro.local .init_array .dynamic .got
           

libCube.so:

$ arm-linux-androideabi-readelf -l ./libCube.so

Elf file type is DYN (Shared object file)
Entry point 0x0
There are 9 program headers, starting at offset 52

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  PHDR           0x000034 0x00000034 0x00000034 0x00120 0x00120 R   0x4
  INTERP         0x000154 0x00000154 0x00000154 0x00013 0x00013 R   0x1
      [Requesting program interpreter: /system/bin/linker]
  LOAD           0x000000 0x00000000 0x00000000 0xe6af4 0xe6af4 R E 0x1000
  LOAD           0x0e6de8 0x000e7de8 0x000e7de8 0x04e44 0x0599c RW  0x1000
  DYNAMIC        0x0ea940 0x000eb940 0x000eb940 0x00108 0x00108 RW  0x4
  NOTE           0x000168 0x00000168 0x00000168 0x00024 0x00024 R   0x4
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0
  EXIDX          0x0b9e34 0x000b9e34 0x000b9e34 0x071a0 0x071a0 R   0x4
  GNU_RELRO      0x0e6de8 0x000e7de8 0x000e7de8 0x04218 0x04218 RW  0x8

 Section to Segment mapping:
  Segment Sections...
   00     
   01     .interp 
   02     .interp .note.gnu.build-id .dynsym .dynstr .hash .rel.dyn .rel.plt .plt .text .ARM.exidx .ARM.extab .rodata 
   03     .data.rel.ro.local .fini_array .init_array .data.rel.ro .dynamic .got .data .bss 
   04     .dynamic 
   05     .note.gnu.build-id 
   06     
   07     .ARM.exidx 
   08     .data.rel.ro.local .fini_array .init_array .data.rel.ro .dynamic .got
           

libHCDNClientNet.so:

$ arm-linux-androideabi-readelf -l ./libHCDNClientNet.so

Elf file type is DYN (Shared object file)
Entry point 0x0
There are 9 program headers, starting at offset 52

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  PHDR           0x000034 0x00000034 0x00000034 0x00120 0x00120 R   0x4
  INTERP         0x000154 0x00000154 0x00000154 0x00013 0x00013 R   0x1
      [Requesting program interpreter: /system/bin/linker]
  LOAD           0x000000 0x00000000 0x00000000 0x792a40 0x792a40 R E 0x1000
  LOAD           0x792b10 0x00793b10 0x00793b10 0x28bd8 0x35ff0 RW  0x1000
  DYNAMIC        0x7af594 0x007b0594 0x007b0594 0x00100 0x00100 RW  0x4
  NOTE           0x000168 0x00000168 0x00000168 0x00024 0x00024 R   0x4
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0
  EXIDX          0x60fa90 0x0060fa90 0x0060fa90 0x26190 0x26190 R   0x4
  GNU_RELRO      0x792b10 0x00793b10 0x00793b10 0x1e4f0 0x1e4f0 RW  0x8

 Section to Segment mapping:
  Segment Sections...
   00     
   01     .interp 
   02     .interp .note.gnu.build-id .dynsym .dynstr .hash .rel.dyn .rel.plt .plt .text .ARM.extab .ARM.exidx .rodata 
   03     .data.rel.ro.local .fini_array .init_array .data.rel.ro .dynamic .got .data .bss 
   04     .dynamic 
   05     .note.gnu.build-id 
   06     
   07     .ARM.exidx 
   08     .data.rel.ro.local .fini_array .init_array .data.rel.ro .dynamic .got
           

對照崩潰時的 maps 資訊,發現 libHCDNClientNet.so 的動态加載過程确實沒有走完,第一個 LOAD Segment 被完全 mmap 到了記憶體,但是第二個 LOAD Segment 沒有,隻 mmap 了

2a000

長度,接着就崩潰了,從 maps 來看,并沒有走到對

.got

等 section 做隻讀保護的階段。

linker 崩潰位置和記憶體資料分析

linker 調用 memset:

............
.text:00004050   LDR             R3, [R4,#0x18]
.text:00004052   LSLS            R3, R3, #0x1E
.text:00004054   BPL             loc_4068
.text:00004056   UBFX.W          R2, R10, #0, #0xC
.text:0000405A   CBZ             R2, loc_4068
.text:0000405C   MOV             R0, R10
.text:0000405E   MOVS            R1, #0
.text:00004060   RSB.W           R2, R2, #0x1000
.text:00004064   BLX             __dl_memset
.text:00004068   ADDW            R1, R10, #0xFFF
.text:0000406C   BIC.W           R10, R1, #0xFF0
.text:00004070   BIC.W           R0, R10, #0xF
............
           

memset 中發生了 SIGBUS 崩潰:

............
.text:000047B0 __dl_memset
.text:000047B0 ; __unwind {
.text:000047B0   STMFD           SP!, {R0}
.text:000047B4   CMP             R2, #0x10
.text:000047B8   BCC             loc_4890
.text:000047BC   MOV             R3, R0
.text:000047C0   MOV             R1, R1,LSL#24
.text:000047C4   ORR             R1, R1, R1,LSR#8
.text:000047C8   ORR             R1, R1, R1,LSR#16
.text:000047CC   ANDS            R12, R3, #7
.text:000047D0   BNE             loc_4868
.text:000047D4   MOV             R0, R1
.text:000047D8   SUBS            R2, R2, #0x40
.text:000047DC   BCC             loc_480C
.text:000047E0   STRD            R0, [R3]  ;在這個位置發生了 SIGBUS
.text:000047E4   STRD            R0, [R3,#8]
.text:000047E8   STRD            R0, [R3,#0x10]
.text:000047EC   STRD            R0, [R3,#0x18]
.text:000047F0   STRD            R0, [R3,#0x20]
.text:000047F4   STRD            R0, [R3,#0x28]
.text:000047F8   STRD            R0, [R3,#0x30]
.text:000047FC   STRD            R0, [R3,#0x38]
.text:00004800   ADD             R3, R3, #0x40
.text:00004804   SUBS            R2, R2, #0x40
.text:00004808   BGE             loc_47E0
............
           

R0

的值是

R3

的值是

94ca06e8

,這裡試圖将

寫入虛拟記憶體位址為

94ca06e8

的記憶體中。

94ca06e8

位于之前提到的 libHCDNClientNet.so 中 “不完整的第二個 LOAD Segment 的 mmap 記憶體映射區域” 中:

94c77000-94ca1000 rw-  792000   2a000 /data/data/com.package.name/files/download/libHCDNClientNet.so
           

這個 Segment 的 ELF 資訊:

Type  Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
LOAD  0x792b10 0x00793b10 0x00793b10 0x28bd8 0x35ff0 RW  0x1000
           

由于需要 4K 對其,我們看到這個 Segment 是從檔案 Offset

792000

開始映射的,映射到虛拟記憶體位址

94c77000

,是以實際映射到記憶體的檔案長度是:

792b10 - 792000 + 28bd8 = 296e8

,對應的虛拟記憶體區間是:

[94c77000, 94ca06e8)

。而 memset 發生崩潰時正試圖向

94ca06e8

指向的記憶體中寫入資料

memset 之類的 libc/bionic 函數由于功能比較明确單一,一般都經過了很嚴格的測試,發生問題的機率很小。這裡也比較明顯是調用 memset 時指定的記憶體寫入位址有問題。

AOSP 5.x 源碼分析

崩潰位置的 AOSP 5.x 源碼:

bool ElfReader::LoadSegments() {
  for (size_t i = 0; i < phdr_num_; ++i) {
    const ElfW(Phdr)* phdr = &phdr_table_[i];
    
    if (phdr->p_type != PT_LOAD) {
      continue;
    }
    
    // Segment addresses in memory.
    ElfW(Addr) seg_start = phdr->p_vaddr + load_bias_;
    ElfW(Addr) seg_end   = seg_start + phdr->p_memsz;
    
    ElfW(Addr) seg_page_start = PAGE_START(seg_start);
    ElfW(Addr) seg_page_end   = PAGE_END(seg_end);
    
    ElfW(Addr) seg_file_end   = seg_start + phdr->p_filesz;
    
    // File offsets.
    ElfW(Addr) file_start = phdr->p_offset;
    ElfW(Addr) file_end   = file_start + phdr->p_filesz;
    
    ElfW(Addr) file_page_start = PAGE_START(file_start);
    ElfW(Addr) file_length = file_end - file_page_start;
    
    if (file_length != 0) {
      void* seg_addr = mmap(reinterpret_cast<void*>(seg_page_start),
                            file_length,
                            PFLAGS_TO_PROT(phdr->p_flags),
                            MAP_FIXED|MAP_PRIVATE,
                            fd_,
                            file_page_start);
      if (seg_addr == MAP_FAILED) {
        DL_ERR("couldn't map \"%s\" segment %zd: %s", name_, i, strerror(errno));
        return false;
      }
    }
    
    // if the segment is writable, and does not end on a page boundary,
    // zero-fill it until the page limit.
    if ((phdr->p_flags & PF_W) != 0 && PAGE_OFFSET(seg_file_end) > 0) {
      //這裡調用 memset,之後發生了崩潰。
      memset(reinterpret_cast<void*>(seg_file_end), 0, PAGE_SIZE - PAGE_OFFSET(seg_file_end));
    }    
............
           

Google Source 上的對應源碼在 這裡。

簡單分析後發現并沒有問題。這裡将 LOAD Segment 用 mmap 映射到記憶體後,如果發現該 Segment 對應的映射記憶體是可寫的,且結尾處不是剛好 4K 對齊的,就将最後 4K 記憶體中剩餘的未映射部分用 memset 填

,雖然末尾的這個區域可能沒有實體檔案與之對應,但是在這個區域執行寫入并不會觸發 SIGBUS,見 mmap manpage:

A file is mapped in multiples of the page size. For a file that is not a multiple of the page size, the remaining memory is zeroed when mapped, and writes to that region are not written out to the file.

反之,如果是由于這個原因導緻 SIGBUS,那就不是千分之一的崩潰機率了,隻要可寫 Segment 的結尾處不是 4K 對齊的,linker 的代碼就會走到這裡,就必定會崩潰。

AOSP 6.x 源碼分析

從線上的資料統計來看,這個崩潰 99% 以上發生在 Android 5.x 及以下系統中。那麼 Android 6.x linker 做了什麼呢?

bool ElfReader::LoadSegments() {
  for (size_t i = 0; i < phdr_num_; ++i) {
    const ElfW(Phdr)* phdr = &phdr_table_[i];
    
    if (phdr->p_type != PT_LOAD) {
      continue;
    }
    
    // Segment addresses in memory.
    ElfW(Addr) seg_start = phdr->p_vaddr + load_bias_;
    ElfW(Addr) seg_end   = seg_start + phdr->p_memsz;
    
    ElfW(Addr) seg_page_start = PAGE_START(seg_start);
    ElfW(Addr) seg_page_end   = PAGE_END(seg_end);
    
    ElfW(Addr) seg_file_end   = seg_start + phdr->p_filesz;
    
    // File offsets.
    ElfW(Addr) file_start = phdr->p_offset;
    ElfW(Addr) file_end   = file_start + phdr->p_filesz;
    
    ElfW(Addr) file_page_start = PAGE_START(file_start);
    ElfW(Addr) file_length = file_end - file_page_start;
    
    if (file_size_ <= 0) {
      DL_ERR("\"%s\" invalid file size: %" PRId64, name_, file_size_);
      return false;
    }
    
    if (file_end >= static_cast<size_t>(file_size_)) {
      DL_ERR("invalid ELF file \"%s\" load segment[%zd]:"
          " p_offset (%p) + p_filesz (%p) ( = %p) past end of file (0x%" PRIx64 ")",
          name_, i, reinterpret_cast<void*>(phdr->p_offset),
          reinterpret_cast<void*>(phdr->p_filesz),
          reinterpret_cast<void*>(file_end), file_size_);
      return false;
    }
    
    if (file_length != 0) {
      void* seg_addr = mmap64(reinterpret_cast<void*>(seg_page_start),
                            file_length,
                            PFLAGS_TO_PROT(phdr->p_flags),
                            MAP_FIXED|MAP_PRIVATE,
                            fd_,
                            file_offset_ + file_page_start);
      if (seg_addr == MAP_FAILED) {
        DL_ERR("couldn't map \"%s\" segment %zd: %s", name_, i, strerror(errno));
        return false;
      }
    }
    
    // if the segment is writable, and does not end on a page boundary,
    // zero-fill it until the page limit.
    if ((phdr->p_flags & PF_W) != 0 && PAGE_OFFSET(seg_file_end) > 0) {
      memset(reinterpret_cast<void*>(seg_file_end), 0, PAGE_SIZE - PAGE_OFFSET(seg_file_end));
    }
............
           

Google Source 上的對應源碼在 這裡。

我們确實看到了顯著的差別,從 Android 6.0 開始,執行 mmap 之前增加了額外的檢查,如果映射的檔案結尾 offset 大于等于了檔案的長度,就直接

return false

。這會在 native 層表現為

dlopen()

傳回失敗,在 java 層表現為

System.loadLibrary()

抛出異常。

檢視 Android 源碼的 git log 我們發現:2015 年 6 月 26 日,Dmitriy Ivanov 将這個問題的修複 patch merge 到了 bionic 的 master 分支。commit message 為:"Fix crash when trying to load invalid ELF file.”。至此,這個問題得到了修複。

結論

崩潰原因

Android 6.0 之前版本的 linker 完全信任了目标 ELF 中的各種基本資訊。當 ELF 檔案的頭部完整(能解析出 Program Headers 等資訊)但檔案本身有缺失時,可能會導緻某個可寫 LOAD segment 缺失了一部分或全部,隻要缺失長度超過 4K,就會導緻 mmap manpage 中提到的條件不再滿足:

A file is mapped in multiples of the page size. For a file that is not a multiple of the page size, the remaining memory is zeroed when mapped, and writes to that region are not written out to the file.

當缺失長度超過 4K,後續 memset 的寫入嘗試就會導緻 SIGBUS。如 mmap manpage 中提到的那樣:

SIGBUS: Attempted access to a portion of the buffer that does not correspond to the file (for example, beyond the end of the file, including the case where another process has truncated the file).

引起崩潰的原因已經很清楚了:linker 加載的 ELF 檔案不完整,缺失了後面的一部分。

進一步分析線上的資料,發現 linker 隻在加載動态下發的動态庫時發生這個崩潰,而加載安裝包内的動态庫時幾乎從來沒有遇到過這個問題,是以基本可以斷定是在動态庫的下載下傳/解壓/校驗的某個環節出了問題。

驗證

我們可以很友善的在 Linux 上寫一小段 C 代碼進行驗證:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <string.h>

#define MAP_SZ       8192
#define MAP_FILE_SZ  (4096 + 10)
#define REAL_FILE_SZ (4096 + 10)

int main(int argc, char *argv[])
{
    int fd = open("./data", O_RDWR | O_CREAT | O_TRUNC, 0644);
    ftruncate(fd, REAL_FILE_SZ);

    void *addr = mmap(NULL, MAP_SZ, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
    memset(addr + MAP_FILE_SZ, 0, MAP_SZ - MAP_FILE_SZ);
    printf("memset OK!\n");

    munmap(addr, MAP_SZ);
    close(fd);
    
    printf("everything OK!\n");
    return 0;
}
           

這裡建立了一個 4K + 10 位元組長度的檔案,按照 mmap 的頁對齊要求,指定配置設定了 8K 長度的記憶體空間,用 memset 向

addr[4K + 10, 8K)

記憶體空間寫入資料。最後生成了一個 4K + 10 位元組長度的檔案。向

addr[4K + 10, 8K)

記憶體空間的寫入操作沒有産生任何副作用,沒有引起崩潰:

[email protected]:~/test$ gcc ./test.c
[email protected]:~/test$ ./a.out
memset OK!
everything OK!
[email protected]:~/test$ ls -al
-rw-r--r--  1 caikelun caikelun  4106 May 31 18:06 data
           

把代碼稍作修改,将實際檔案長度從 4K + 10 位元組改為 4K - 10 位元組,其他不變:

#define REAL_FILE_SZ (4096 + 10)
           

改為:

#define REAL_FILE_SZ (4096 - 10)
           

再次編譯和執行程式。運作到 memset 時發生了 SIGBUS:

[email protected]:~/devel/test$ gcc ./test.c
[email protected]:~/devel/test$ ./a.out
Bus error
[email protected]:~/test$ ls -al
-rw-r--r--  1 caikelun caikelun  4086 May 31 18:10 data
           

關于崩潰捕獲工具

因為這是本系列的第一篇,在這裡介紹一下我們一直在使用的自己開發的 Android APP 崩潰捕獲工具: xCrash。

完整準确的 backtrace 對于定位線上崩潰問題很重要。寄存器、stack、maps、FD list、記憶體統計、各種 log、甚至 ELF build-id 資訊也同樣重要。

有些線上的系統相關 native 崩潰,我們竭盡全力的去擷取崩潰現場的各種資訊,卻依然會感到資訊不足。為了擷取更多需要的資訊,我們隻能冒着 “xCrash 本身發生崩潰” 的風險,小心翼翼的去改進。當 xCrash 被喚起運作時,經常要面對這樣的情況:棧已經溢出、堆記憶體不可用、FD完全耗盡、flash空間完全耗盡、正在崩潰中的程序随時會被系統強殺。這些極端的情況并非是我們自虐式的臆想,而是線上崩潰都遇到過的真實情況。有時,這些極端情況的出現,本身就是導緻程序崩潰的間接原因。

如果線上 xCrash 本身發生了崩潰,沒有人能告訴我們崩潰在哪裡?為什麼崩潰?如果 xCrash 無法如預期的運作拿到所有需要的資訊,也沒有人能告訴我們為什麼?因為這是最後一道防線,下一刻,等待 APP 程序的就是所有資源的回收,一切歸零。

轉載于:https://my.oschina.net/nomagic/blog/3056759