Android APP native 崩潰分析之 linker SIGBUS 崩潰

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

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

現象

現象 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 能獲取到的信息偏少。linux

現象 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 中具體的崩潰位置。android

現象 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 加載另外一個動態庫時發生了崩潰。git

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

分析

是 APP 本身的 bug ?仍是特定 OS 版本或機型的兼容性問題?爲何 Android 6.x 及以上版本的系統中幾乎沒有這個崩潰? 有不少疑問。bash

通過分析,咱們發現這三個現象的本質是同樣的,這裏僅針對現象 3 進行分析。服務器

ELF 信息和 maps 分析

經過機型匹配和 ELF build-id 匹配,拿到了和崩潰設備相同的 linker 二進制文件,另外兩個動態庫咱們本身的服務器中有。全部 ELF 都是 armeabi 架構的。架構

linker:app

$ 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 的值是 0R3 的值是 94ca06e8,這裏試圖將 0 寫入虛擬內存地址爲 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 指向的內存中寫入數據 0

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 填 0,雖然末尾的這個區域可能沒有物理文件與之對應,可是在這個區域執行寫入並不會觸發 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) 內存空間的寫入操做沒有產生任何反作用,沒有引發崩潰:

caikelun@debian:~/test$ gcc ./test.c
caikelun@debian:~/test$ ./a.out
memset OK!
everything OK!
caikelun@debian:~/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:

caikelun@debian:~/devel/test$ gcc ./test.c
caikelun@debian:~/devel/test$ ./a.out
Bus error
caikelun@debian:~/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 進程的就是全部資源的回收,一切歸零。

相關文章
相關標籤/搜索