概述:
為什么 crash log 內 Exception Backtrace 部分的地址(stack address)不能從 dsym 文件中查出對應的代碼?
因為 ASLR(Address space layout randomization),因為 ASLR 引入了一個 slide (偏移) 。
symbol address = stack address - slide;
slide 可以在運行時 由 API 獲取到
- dyld_get_image_vmaddr_slide()
也可以根據運行時的 binary image 和 ELF 文件的 load command 計算的到。
slide = (運行時)load address - (鏈接時)load address;
注意,如果你沒有在運行時用 api 獲取slide,那么 binary image 就必須要收集,否則你無法從dsym 文件中解析出符號。
這是一個 iOS crash log 文件,為了簡潔刪除了部分不需要的內容
- Incident Identifier: 975CF16A-5259-4DD1-BFDA-D1B155EF5BF0
- CrashReporter Key: 562a7cefe034ac086cae453c61278cdd9a4b3288
- Hardware Model: iPad4,1
- Process: MedicalRecordsFolder [382]
- Path: /var/mobile/Applications/05C398CE-21E9-43C2-967F-26DD0A327932/MedicalRecordsFolder.app/MedicalRecordsFolder
- Identifier: com.xingshulin.MedicalRecordIOS
- Version: 1 (4.14.0)
- Code Type: ARM-64 (Native)
- Parent Process: launchd [1]
- Date/Time: 2015-12-03 19:14:59.921 +0800
- OS Version: iOS 7.1.2 (11D257)
- Report Version: 104
- Exception Type: EXC_CRASH (SIGABRT)
- Exception Codes: 0x0000000000000000, 0x0000000000000000
- Triggered by Thread: 0
- Last Exception Backtrace:
- 0 CoreFoundation 0x189127100 __exceptionPreprocess + 132
- 1 libobjc.A.dylib 0x1959e01fc objc_exception_throw + 60
- 2 CoreFoundation 0x189127040 +[NSException raise:format:] + 128
- 3 MedicalRecordsFolder 0x100a8666c 0x10003c000 + 10790508
- 4 libsystem_platform.dylib 0x19614bb0c _sigtramp + 56
- 5 MedicalRecordsFolder 0x1006ef164 0x10003c000 + 7024996
- 6 MedicalRecordsFolder 0x1006e8580 0x10003c000 + 6997376
- 7 MedicalRecordsFolder 0x1006e8014 0x10003c000 + 6995988
- 8 MedicalRecordsFolder 0x1006e7c94 0x10003c000 + 6995092
- 9 MedicalRecordsFolder 0x1006f2460 0x10003c000 + 7038048
- 10 libdispatch.dylib 0x195fb8014 _dispatch_call_block_and_release + 24
- 11 libdispatch.dylib 0x195fb7fd4 _dispatch_client_callout + 16
- 12 libdispatch.dylib 0x195fbe4a8 _dispatch_queue_drain + 640
- 13 libdispatch.dylib 0x195fba4c0 _dispatch_queue_invoke + 68
- 14 libdispatch.dylib 0x195fbf0f4 _dispatch_root_queue_drain + 104
- 15 libdispatch.dylib 0x195fbf4fc _dispatch_worker_thread2 + 76
- 16 libsystem_pthread.dylib 0x19614d6bc _pthread_wqthread + 356
- 17 libsystem_pthread.dylib 0x19614d54c start_wqthread + 4
- Thread 0 Crashed:
- 0 libsystem_kernel.dylib 0x00000001960ce58c __pthread_kill + 8
- 1 libsystem_c.dylib 0x0000000196062804 abort + 108
- 2 libc++abi.dylib 0x0000000195288990 abort_message + 84
- 3 libc++abi.dylib 0x00000001952a5c28 default_terminate_handler() + 296
- 4 libobjc.A.dylib 0x00000001959e04d0 _objc_terminate() + 124
- 5 libc++abi.dylib 0x00000001952a3164 std::__terminate(void (*)()) + 12
- 6 libc++abi.dylib 0x00000001952a2d38 __cxa_rethrow + 140
- 7 libobjc.A.dylib 0x00000001959e03a4 objc_exception_rethrow + 40
- 8 CoreFoundation 0x0000000189025e48 CFRunLoopRunSpecific + 572
- 9 GraphicsServices 0x000000018ecb5c08 GSEventRunModal + 164
- 10 UIKit 0x000000018c156fc0 UIApplicationMain + 1152
- 11 MedicalRecordsFolder 0x000000010018fc70 0x10003c000 + 1391728
- 12 libdyld.dylib 0x0000000195fd3a9c start + 0
- Thread 1:
- 0 libsystem_kernel.dylib 0x00000001960b5aa8 kevent64 + 8
- 1 libdispatch.dylib 0x0000000195fb9998 _dispatch_mgr_thread + 48
- ....
- Thread 0 crashed with ARM Thread State (64-bit):
- x0: 0x0000000000000000 x1: 0x0000000000000000 x2: 0x0000000000000000 x3: 0xffffffffffffffff
- x4: 0x0000000000003060 x5: 0x000000016fdc3530 x6: 0x000000000000006e x7: 0x0000000000000640
- x8: 0x0000000008000000 x9: 0x0000000004000000 x10: 0x0000000098efe6f7 x11: 0x0000000198efde94
- x12: 0x000000000000006f x13: 0x0000000000000000 x14: 0x0000000000000000 x15: 0x000000019607bdcb
- x16: 0x0000000000000148 x17: 0x004b96d3524ed02c x18: 0x0000000000000000 x19: 0x0000000000000006
- x20: 0x0000000198f112a0 x21: 0x434c4e47432b2b00 x22: 0x434c4e47432b2b00 x23: 0x0000000000000001
- x24: 0x00000001701578c0 x25: 0x0000000000000001 x26: 0x0000000170002ea0 x27: 0x00000001963e1410
- x28: 0x0000000000000000 fp: 0x000000016fdc34b0 lr: 0x000000019615116c
- sp: 0x000000016fdc3490 pc: 0x00000001960ce58c cpsr: 0x00000000
- Binary Images:
- 0x10003c000 - 0x100f7bfff MedicalRecordsFolder arm64 <b5ae3570a013386688c7007ee2e73978> /var/mobile/Applications/05C398CE-21E9-43C2-967F-26DD0A327932/MedicalRecordsFolder.app/MedicalRecordsFolder
- 0x12007c000 - 0x1200a3fff dyld arm64 <628da833271c3f9bb8d44c34060f55e0> /usr/lib/dyld
- .......
現在來指出其中比較重要的部分
uuid信息
- Incident Identifier: 975CF16A-5259-4DD1-BFDA-D1B155EF5BF0
這行指出文件的 uuid ,根據次 uuid 可確定 dsym 文件是否匹配
確定方法如下,后面會打印出該 dsym 文件內所有 架構的 uuid ,看一下 是否包含 就知道了
- dwarfdump --uuid MedicalRecordsFolder.app.dSYM/
arch信息不解釋
- Code Type: ARM-64 (Native)
下面是 異常信息,異常線程 為 thread 0
- Exception Type: EXC_CRASH (SIGABRT)
- Exception Codes: 0x0000000000000000, 0x0000000000000000
- Triggered by Thread: 0
接下來看 拋出異常的線程的函數調用棧信息
- Last Exception Backtrace:
- 0 CoreFoundation 0x189127100 __exceptionPreprocess + 132
- 1 libobjc.A.dylib 0x1959e01fc objc_exception_throw + 60
- 2 CoreFoundation 0x189127040 +[NSException raise:format:] + 128
- 3 MedicalRecordsFolder 0x100a8666c 0x10003c000 + 10790508
- 4 libsystem_platform.dylib 0x19614bb0c _sigtramp + 56
- 5 MedicalRecordsFolder 0x1006ef164 0x10003c000 + 7024996
- 6 MedicalRecordsFolder 0x1006e8580 0x10003c000 + 6997376
- 7 MedicalRecordsFolder 0x1006e8014 0x10003c000 + 6995988
- 8 MedicalRecordsFolder 0x1006e7c94 0x10003c000 + 6995092
- 9 MedicalRecordsFolder 0x1006f2460 0x10003c000 + 7038048
- 10 libdispatch.dylib 0x195fb8014 _dispatch_call_block_and_release + 24
- 11 libdispatch.dylib 0x195fb7fd4 _dispatch_client_callout + 16
- 12 libdispatch.dylib 0x195fbe4a8 _dispatch_queue_drain + 640
- 13 libdispatch.dylib 0x195fba4c0 _dispatch_queue_invoke + 68
- 14 libdispatch.dylib 0x195fbf0f4 _dispatch_root_queue_drain + 104
- 15 libdispatch.dylib 0x195fbf4fc _dispatch_worker_thread2 + 76
- 16 libsystem_pthread.dylib 0x19614d6bc _pthread_wqthread + 356
- 17 libsystem_pthread.dylib 0x19614d54c start_wqthread + 4
我們從 binary image 這列里面可以看出 好多都是 動態庫調用,動態庫也就是說 這是 sdk 里面的東西,即使出了bug 你也修復不了,所以我們需要關心的就只有
第 3、5、6、7、8、9、這些行 ,只有這行行對應的代碼 才是你自己的 創作(我工程名就是MedicalRecordsFolder)
是 函數調用順序是 從下往上,也就是說 第 3 行對應的函數才是出問題時 正在執行的代碼片段。
所以 從 dsym 中找到 第三行對應的 符號信息 才可能定位到 問題代碼。
第三行第三列
0x100a8666c ,這是 stack address ,注意是 stack address,如果系統沒有 ASLR 的話,用這個 stack address 就能在dsym 中找到對應符號信息,但是事實 iOS是有 ASLR 的 。
ASLR 技術Address space layout randomization,ASLR通過將系統可執行程序隨機裝載到內存里,從而防止緩沖區溢出攻擊
由於 ASLR 的緣故,導致 程序crash后生成的crash log 中的 stack address 與 對應的 symbol address 不一致,有一個偏移量 slide,slide是程序裝在時隨機生成的隨機數。
引入新的概念:
stack address
: 程序運行時線程棧中 所有 函數調用的地址
symble address
: dsym文件中函數符號對應的地址,用此地址 在 dsym 文件中可以 查出對應的 符號信息。
無 ASLR 機制時 stack address 等於symble address 。
定義 slide
在ASLR機制下每次啟動APP 裝在之前 ,會在連接時指定的 進程空間上 加上一個隨意的 偏移量,這個偏移量就是 slide。
很簡單 symble address = stack address -slide;
但是這個 slide 每次 啟動 程序都不同,如何 知道 當時啟動時 slide 的值呢 ? 帶着疑問繼續吧
Load Command
一個 iOS 程序編譯鏈接完之后,生成一個 ELF 二進制文件(也就是程序運行時的映射文件),該文件的詳細格式不再贅述,這里只強調一個 segment _TEXT
下面是 使用 otool 工具查看到的 MedicalRecordsFolder(我的demo程序)的 加載命令 。
- $otool -l MedicalRecordsFolder.app/MedicalRecordsFolder
- MedicalRecordsFolder.app/MedicalRecordsFolder:
- Load command 0
- cmd LC_SEGMENT_64
- cmdsize 72
- segname __PAGEZERO
- vmaddr 0x0000000000000000
- vmsize 0x0000000100000000
- fileoff 0
- filesize 0
- maxprot 0x00000000
- initprot 0x00000000
- nsects 0
- flags 0x0
- Load command 1
- cmd LC_SEGMENT_64
- cmdsize 792
- segname __TEXT
- vmaddr 0x0000000100000000
- vmsize 0x000000000000c000
- fileoff 0
- filesize 49152
- maxprot 0x00000005
- initprot 0x00000005
- nsects 9
- flags 0x0
- ……
- Load command 2
- cmd LC_SEGMENT_64
- cmdsize 1352
- segname __DATA
- vmaddr 0x000000010000c000
- vmsize 0x0000000000004000
- fileoff 49152
- filesize 16384
- maxprot 0x00000003
- initprot 0x00000003
- nsects 16
- flags 0x0
- ……
- Load command 3
- cmd LC_SEGMENT_64
- cmdsize 72
- segname __LINKEDIT
- vmaddr 0x0000000100010000
- vmsize 0x000000000000c000
- fileoff 65536
- filesize 35056
- maxprot 0x00000001
- initprot 0x00000001
- nsects 0
- flags 0x0
_TEXT 段的加載命令如下,可知到映射文件中segment _TEXT 對應的 虛擬地址空間從 0x0000000100000000 開始 。
- segname __TEXT
- vmaddr 0x0000000100000000
- vmsize 0x000000000000c000
- fileoff 0
- filesize 49152
segname __TEXT 就是代碼段,也就是說所有的二進制指令
沒有 ASLR機制時:
加載時 裝載器會將此 ELF 文件的 前 49152 (offset 0 ,filesize 49152)個字節(因為 offset 0 ,filesize 49152)映射到 進程空間以 0x0000000100000000 開始的一塊虛擬內存空間里.
有ASLR 機制時:
加載時 裝載器會將此 ELF 文件的 前 49152 (offset 0 ,filesize 49152)個字節(因為 offset 0 ,filesize 49152)映射到 進程空間以 0x0000000100000000 (+slide)開始的一塊虛擬內存空間里.
所以 : 如果沒有 ASLR 機制,那么運行時的內存布局 就和 Load command 中指定的布局一致,也就意味着stack address和 symbol address 一致
有 ASLR 的情況也不復雜,只是 加了一個 隨意的偏移量 slide
binary image
程序運行時 的 映射 信息,
- Binary Images:
- 0x10003c000 - 0x100f7bfff MedicalRecordsFolder arm64 <b5ae3570a013386688c7007ee2e73978> /var/.../MedicalRecordsFolder
- 0x12007c000 - 0x1200a3fff dyld arm64 <628da833271c3f9bb8d44c34060f55e0> /usr/lib/dyld
左側
第一列,虛擬地址空間區塊
第二列,映射文件 名
第三列,uuid吧,還不知道,以后再補上
第四列,映射文件路徑
計算 slide 和 symbol address
第一行可以看出 進程空間的 0x10003c000 - 0x100f7bfff 這個區域 在運行時被映射為 MedicalRecordsFolder 內的內容,也就是我們的 ELF 文件。
注意這個 區域起始地址 為 0x10003c000
而我們在 Load Command 中看到的卻是 0x0000000100000000
- segname __TEXT
- vmaddr 0x0000000100000000
- vmsize 0x000000000000c000
顯而易見:
slide = 0x10003c000 - 0x100000000 = 0x3c000;
symbol address = stack address - slide;
stack address 在crash log 中已經找到了。
用的到的symbol地址去 dsym 文件中 查詢,命令如下
- $dwarfdump --arch arm64 --lookup 0x00123 MedicalRecordsFolder.app.dSYM/
就可以定位下來具體的 代碼 函數名,所處的文件,行 等信息了
讀取 slide 的 API
這個 slide 的計算還是挺 惡心的 ,要 查看 binary image 的到 load address ,還要查看 對用 ELF 中 _TEXT 的 Load Command 虛擬空間范圍.
如果 自己寫一個 模塊 來 收集 NSException 的話 ,大可不必這么繁瑣,因為 程序 運行時 有 api 是可以 直接獲取這個 binary image 對應的 slide 值的 。
如下:
- #import <mach-o/dyld.h>
- void calculate(void) {
- for (uint32_t i = 0; i < _dyld_image_count(); i++) {
- if (_dyld_get_image_header(i)->filetype == MH_EXECUTE) {
- long slide = _dyld_get_image_vmaddr_slide(i);
- break;
- }
- }
- }
這樣 就可以 將 stack address 直接 減去 slide 之后 再 上傳到自己的 服務端,豈不是 很完美。