原文地址:caikelun.io/post/2019-0…html
這是 Android APP native 崩潰分析系列文章的第一篇。最近分析了一例線上的 Android linker SIGBUS 崩潰,在這裏記錄一下。java
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
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
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 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 調用 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
的值是 0
,R3
的值是 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 源碼:
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 的代碼就會走到這裏,就一定會崩潰。
從線上的數據統計來看,這個崩潰 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 進程的就是全部資源的回收,一切歸零。