原文位址: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