Table of Contents
- iOS Crash 流程化4:打造自己的收集、符號化程序
- 實現(xiàn)代碼
- 發(fā)布包沒帶符號表
- Mach-O File Format
- header
- load Command
- LC_SEGMENT
- LC_SYMTAB
- 數(shù)據(jù)部分
- 小小結
- 獲取構架建瘫、鏡像加載地址
- 輸出Crash日志
- 小結
當APP發(fā)布到AppStore后崭捍,如果發(fā)生了Crash,通常情況下我們拿不到崩潰手機啰脚,也就是說拿不到Crash日志殷蛇。這是一個棘手的問題。有人說可以在開發(fā)者中心找到用戶上傳到蘋果的日志橄浓,但是粒梦,不是所有的用戶都會在程序Crash后上傳Crash日志,所以有必要打造一個屬于我們自己的異常收集系統(tǒng)荸实。
下面就講講打造的異常收集系統(tǒng)谍倦,主要思路:使用NSSetUncaughtExceptionHandler注冊異常處理函數(shù),當APP 發(fā)生Crash時泪勒,回調到異常處理函數(shù)昼蛀,在異常處理函數(shù)中收集Crash信息,然后上傳到服務器圆存;當需要分析的時候叼旋,從服務器取回Crash日志,如果沒有符號化沦辙,使用atos命令符號化夫植。由于暫時沒有服務器,就保存到了沙盒路徑的Document目錄下油讯,可以使用itunes方便的導出日志详民。這里提供了一個簡單示例代碼:UncaughtException嘀略,先從代碼入手洼哎。
實現(xiàn)代碼
這里會分別列出關鍵的代碼埋合。下面是 AppDelegate.m 中的代碼
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[LJCaughtException setDefaultHandler];
// Override point for customization after application launch.
return YES;
}
在application:didFinishLaunchingWithOptions:
中注冊異常處理函數(shù)剃诅,所有的異常注冊和異常處理函數(shù)的代碼都封裝到LJCaughtException.m中饥瓷,如下:
///先前注冊的處理句柄
NSUncaughtExceptionHandler *preHander;
/// 異常處理函數(shù)
void UncaughtExceptionHandler(NSException * exception)
{
[LJCaughtException processException:exception];
}
@implementation LJCaughtException
+ (void)setDefaultHandler
{
///首先保存先前注冊的異常處理句柄
preHander = [LJCaughtException getHandler];
///注冊異常處理句柄
NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler);
}
+ (NSUncaughtExceptionHandler *)getHandler
{
return NSGetUncaughtExceptionHandler();
}
///異常處理句柄
+ (void)processException:(NSException *)exception
{
/// 異常的堆棧信息
NSArray *aryCrashBackTrace = [exception callStackSymbols];
if (!aryCrashBackTrace)
{
return;
}
/// 出現(xiàn)異常的原因
NSString *strCrashReason = [exception reason];
/// 異常名稱
NSString *strCrashName = [exception name];
....
}
...
@end
上面代碼可以分解為三個部分理解:
- 定義異常處理函數(shù)颂碧,異常處理函數(shù)的原型為:
typedef void NSUncaughtExceptionHandler(NSException *exception);
注冊異常處理函數(shù):使用
NSSetUncaughtExceptionHandler
注冊異常處理函數(shù),注冊的代碼為:NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler)
執(zhí)行異常處理函數(shù):當異常發(fā)生時窟却,自動執(zhí)行異常處理函數(shù)剖笙。異常處理函數(shù)內部完成收集Crash信息的功能。
下面是在Debug和Release模式下涧窒,Crash時捕獲的線程回溯:
可以看出,使用系統(tǒng)的API可以完美的捕獲到崩潰日志纠吴,而且符號化了硬鞍,一行代碼 callStackSymbols
就獲取了異常線程的回溯并完成了符號化工作。其實戴已,事情沒有這么簡單固该,不妨試試發(fā)布包,是不是也能像在debug和release模式那樣恭陡,獲取到符號化的異常線程回溯蹬音?
發(fā)布包沒帶符號表
將測試程序打為發(fā)布包,查看異常線程回溯圖休玩,如下:
發(fā)布包的Crash日志
圖中紅框是異常線程的關鍵回溯,顯示的是鏡像的名字拴疤,沒有被轉化為有效的代碼符號永部。為什么?
仔細想想呐矾,前面提到符號化的前提條件苔埋,是得有符號表,那么我們推測debug和release的APP包含了符號表蜒犯,而發(fā)布包沒有包含符號表组橄,是不是?在終端中使用nm命令驗證下罚随。
確實是,發(fā)布包沒有符號表淘菩,為什么遵班?
原來,符號表是一個debug產(chǎn)物潮改,如果使用archive模式打包狭郑,那么符號表會被剪裁掉。不過你也可以在Xcode的編譯選項中配置為符號表不剪裁汇在。方法是設置Strip Style
選項為Debugging Symbols
翰萨。下圖是設置發(fā)布包帶符號表的方法:
但是這會讓最后生成的IPA變大不少(5%)。用我們項目測試趾疚,居然大了約30%缨历,可能是代碼太多的原因吧以蕴。這個對于嚴格限制APP大小的人來說糙麦,是無法接受的辛孵。
天無絕人之路,在使用archive打包時赡磅,生成了一個dSYM符號文件魄缚,這個文件不發(fā)布,在本地保存著焚廊。這個文件太有用了冶匹,也是我們符號化的唯一選擇了。
顯然咆瘟,對于發(fā)布到用戶手中的發(fā)布包嚼隘,在程序Crash后,不能在用戶設備上完成符號化工作袒餐,callStackSymbols
只能返回帶地址的日志信息飞蛹,需要我們線下符號化,還好蘋果提供了一個命令行工具—–atos灸眼,完成符號化工作卧檐。
若想通過atos工具在符號文件中查找到地址對應的符號,需要代碼構架焰宣、鏡像加載地址這兩個參數(shù)霉囚,查看發(fā)布包的Crash日志圖片,這兩個參數(shù)都沒有匕积,怎么辦盈罐?只能祭出OS X ABI Mach-O File Format Reference和KSCrash 開源框架這兩個終極神器。
OS X ABI Mach-O File Format Reference闡述了可執(zhí)行二進制程序的存儲格式闪唆,提供原理性的支撐盅粪。
KSCrash包含了獲取代碼構架和鏡像加載地址的代碼。
依據(jù)這兩個神器苞氮,我們可以順利的拿到代碼構架湾揽、鏡像加載地址。
Mach-O File Format
Mach-O 是Mach object 的意思笼吟,就是OS X系統(tǒng)中對象文件的存儲格式库物,對象文件包括:
- kernel extensions
- command-line tools
- applications
- frameworks
- libraries (shared and static)
詳細的可以參考Mach-O Programming Topics
一個Mach-O 文件包括下面三個部分
- Header: Specifies the target architecture of the file, such as PPC, PPC64, IA-32, or x86-64.
- Load commands: Specify the logical structure of the file and the layout of the file in virtual memory.
- Raw segment data: Contains raw data for the segments defined in the load commands.
下面是官網(wǎng)上的一張圖形化的Mach-O結構示意圖:
下面依次講解這三部分,他們的數(shù)據(jù)結構定義在mach-o/loader.h中贷帮。我們通過三種方式來呈現(xiàn)Mach-O文件結構:
- 代碼定義
- 通過命令行工具
otool
呈現(xiàn) - 通過
MachOView
呈現(xiàn)戚揭。
這其中otool
是系統(tǒng)自帶的對象文件查看工具。MachOView
是網(wǎng)上下載的可視化查看Mach-O結構工具撵枢。由于存在兩個代碼構架民晒,armv7s精居、ARM64,他們的定義稍微有點區(qū)別潜必,僅以ARM64構架為例靴姿。
header
header的數(shù)據(jù)結構的定義如下:
struct mach_header_64
{
uint32_t magic; ///魔數(shù),標記這個是Mach-O文件
cpu_type_t cputype; ///cup 的類型
cpu_subtype_t cpusubtype;
uint32_t filetype;
uint32_t ncmds; /// load commands 個數(shù)
uint32_t sizeofcmds;
uint32_t flags;
uint32_t reserved;
};
終端中查看header:
otool -hV ~/Desktop/收集磁滚、解析IOS崩潰日式/Exception/UncaughtException_archive
輸出如下:
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
MH_MAGIC ARM V7 0x00 EXECUTE 23 2432 NOUNDEFS DYLDLINK TWOLEVEL PIE
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
MH_MAGIC_64 ARM64 ALL 0x00 EXECUTE 23 2872 NOUNDEFS DYLDLINK TWOLEVEL PIE
MachOView顯示的結果:
-
magic
是MH_MAGIC_64
佛吓,固定值:0xfeedfacf,標記這是一個Mach-O文件
-
filetype
文件類型是EXECUTE垂攘,可執(zhí)行程序 -
ncmds
维雇,load command個數(shù)是23
load Command
load Command 種類特別多,大概有60多種晒他,每種command的數(shù)據(jù)結構是不同的吱型, 不會去一一的說明,只拿LC_SEGMENT陨仅、LC_SYMTAB 做個示例津滞。下面列表了部分load command。
#define LC_SEGMENT 0x1 /* segment of this file to be mapped */
#define LC_SYMTAB 0x2 /* link-edit stab symbol table info */
#define LC_SYMSEG 0x3 /* link-edit gdb symbol table info (obsolete) */
#define LC_THREAD 0x4 /* thread */
#define LC_UNIXTHREAD 0x5 /* unix thread (includes a stack) */
#define LC_LOADFVMLIB 0x6 /* load a specified fixed VM shared library */
.....
LC_SEGMENT
LC_SEGMENT: segment load command indicates that a part of this file is to be mapped into a 64-bit task’s address space.
說白了掂名,就是映射到內存中的所有數(shù)據(jù)据沈,自然包括代碼、數(shù)據(jù)等等饺蔑。
segment進一步可以分為
-
**PAGEZERO
: 該類型的segment是可執(zhí)行程序的第一個segment锌介,代表指針地址NULL。
-
**TEXT
: 就是可執(zhí)行代碼猾警,當然是只讀了 -
**DATA
: 可寫的數(shù)據(jù)segment孔祸,應該就是代碼中的變量區(qū)域 -
**OBJC
: Objective-C runtime support library **IMPORT
-
**LINKEDIT
: contains raw data used by the dynamic linker, such as symbol, string, and relocation table entries。
每種segment可能包含多種類型的內容发皿,例如**TEXT
代碼段崔慧,可以有代碼(**text
)、字符串(**cstring
) 穴墅、常量(**const
)惶室、符號(**symbol_stub
)、字面量(**literal4
玄货、__literal8)皇钞,所以進一步用二級目錄(section)表示。下面是segment松捉、section的數(shù)據(jù)結構:
struct segment_command_64
{
/* for 64-bit architectures */
uint32_t cmd; /* LC_SEGMENT_64 */
uint32_t cmdsize; /* includes sizeof section_64 structs */
char segname[16]; /* segment name */
uint64_t vmaddr; /* memory address of this segment */
uint64_t vmsize; /* memory size of this segment */
uint64_t fileoff; /* file offset of this segment */
uint64_t filesize; /* amount to map from the file */
vm_prot_t maxprot; /* maximum VM protection */
vm_prot_t initprot; /* initial VM protection */
uint32_t nsects; /* number of sections in segment */
uint32_t flags; /* flags */
};
struct section_64
{
/* for 64-bit architectures */
char sectname[16]; /* name of this section */
char segname[16]; /* segment this section goes in */
uint64_t addr; /* memory address of this section */
uint64_t size; /* size in bytes of this section */
uint32_t offset; /* file offset of this section */
uint32_t align; /* section alignment (power of 2) */
uint32_t reloff; /* file offset of relocation entries */
uint32_t nreloc; /* number of relocation entries */
uint32_t flags; /* flags (section type and attributes)*/
uint32_t reserved1; /* reserved (for offset or index) */
uint32_t reserved2; /* reserved (for count or sizeof) */
uint32_t reserved3; /* reserved */
};
終端輸入:
otool -lV ~/Desktop/收集夹界、解析IOS崩潰日式/Exception/UncaughtException_archive
輸出:
........
cmd LC_SEGMENT_64
cmdsize 712
segname __TEXT
vmaddr 0x0000000100000000
vmsize 0x0000000000008000
fileoff 0
filesize 32768
maxprot r-x
initprot r-x
nsects 8
flags (none)
.......
MachOView顯示的結果:
圖中直觀的顯示出了LC_SEGMENT
的數(shù)據(jù)、LC_SEGMENT
的二級目錄section的數(shù)據(jù)隘世。
LC_SYMTAB
LC_SYMTAB的數(shù)據(jù)結構如下:
struct symtab_command {
uint32_t cmd; /* LC_SYMTAB */
uint32_t cmdsize; /* sizeof(struct symtab_command) */
uint32_t symoff; /* symbol table offset */
uint32_t nsyms; /* number of symbol table entries */
uint32_t stroff; /* string table offset */
uint32_t strsize; /* string table size in bytes */
};
終端輸出的結果:
Load command 6
cmd LC_SYMTAB
cmdsize 24
symoff 132944
nsyms 48
stroff 133916
strsize 1152
MachOView看到的結果:
LC_SYMTAB 指定了符號的個數(shù)和相對Mach-O的偏移量可柿。
數(shù)據(jù)部分
緊跟著 load command 后面的是數(shù)據(jù)部分鸠踪,就是各個 load command 對應的具體數(shù)據(jù)。
小小結
Mach-O文件的格式非常像一篇文章的結構:
- Header部分是文章的摘要复斥,總體描述了非常重要部分营密。
- Load commands 相當于目錄,Mach-O文件所有內容的索引永票。
- Raw segment data 正文內容卵贱。
Mach-O 文件格式就是一個規(guī)范滥沫,各個部分都有自己的數(shù)據(jù)格式侣集,內容繁多,只能多看兰绣。
不過之前提到了一個有用的工具—otool世分,查看Mach-O對象文件的命令行工具。
獲取構架缀辩、鏡像加載地址
上面說了那么多Mach-O文件結構臭埋,主要是提供原理支撐,目的是通過對Mach-O文件結構的理解臀玄,找到獲取構架瓢阴、鏡像加載地址的方法。
構架很好獲取健无,就在Mach-O的文件頭中荣恐,獲取的關鍵代碼如下:
/*
獲取代碼的構架
*/
NSString * getCodeArch()
{
NSString *strSystemArch =nil;
///獲取應用程序的名稱
NSDictionary *dicInfo = [[NSBundle mainBundle] infoDictionary];
if (LJM_Dic_Not_Valid(dicInfo))
{
return strSystemArch;
}
NSString *strAppName = dicInfo[@"CFBundleName"];
if (!strAppName)
{
return strSystemArch;
}
///獲取 cpu 的大小版本號
uint32_t count = _dyld_image_count();
cpu_type_t cpuType = -1;
cpu_type_t cpuSubType =-1;
for(uint32_t iImg = 0; iImg < count; iImg++)
{
const char* szName = _dyld_get_image_name(iImg);
if (strstr(szName, strAppName.UTF8String) != NULL)
{
const struct mach_header* machHeader = _dyld_get_image_header(iImg);
cpuType = machHeader->cputype;
cpuSubType = machHeader->cpusubtype;
break;
}
}
if(cpuType < 0 || cpuSubType <0)
{
return strSystemArch;
}
///轉化cpu 版本為文字類型
switch(cpuType)
{
case CPU_TYPE_ARM:
{
strSystemArch = @"arm";
switch (cpuSubType)
{
case CPU_SUBTYPE_ARM_V6:
strSystemArch = @"armv6";
break;
case CPU_SUBTYPE_ARM_V7:
strSystemArch = @"armv7";
break;
case CPU_SUBTYPE_ARM_V7F:
strSystemArch = @"armv7f";
break;
case CPU_SUBTYPE_ARM_V7K:
strSystemArch = @"armv7k";
break;
#ifdef CPU_SUBTYPE_ARM_V7S
case CPU_SUBTYPE_ARM_V7S:
strSystemArch = @"armv7s";
break;
#endif
}
break;
}
#ifdef CPU_TYPE_ARM64
case CPU_TYPE_ARM64:
strSystemArch = @"arm64";
break;
#endif
case CPU_TYPE_X86:
strSystemArch = @"i386";
break;
case CPU_TYPE_X86_64:
strSystemArch = @"x86_64";
break;
}
return strSystemArch;
}
主要思路是:通過 _dyld_image_count
獲取到所有的鏡像個數(shù),然后根據(jù)鏡像索引(0…鏡像個數(shù)-1)累贤,依次枚舉出鏡像的名字叠穆,然后,鏡像名字使用_dyld_get_image_header
函數(shù)獲取到鏡像的header結構體信息臼膏,賦值到:mach_header* machHeader
中硼被。最后,通過 machHeader->cputype
( CPU的類型)和 machHeader->cpusubtype
(CPU的子類型)轉化為具體的代碼構架渗磅。
對于鏡像的加載地址嚷硫,其實就是鏡像的header結構體的首地址。詳細代碼如下:
/*
獲取應用程序的加載地址
*/
NSString * getImageLoadAddress()
{
NSString *strLoadAddress =nil;
NSString * strAppName = getAppName();
if (!strAppName)
{
return strLoadAddress;
}
///獲取應用程序的load address
uint32_t count = _dyld_image_count();
for(uint32_t iImg = 0; iImg < count; iImg++)
{
const char* szName = _dyld_get_image_name(iImg);
if (strstr(szName, strAppName.UTF8String) != NULL)
{
const struct mach_header* header = _dyld_get_image_header(iImg);
strLoadAddress = [NSString stringWithFormat:@"0x%lX",(uintptr_t)header];
break;
}
}
return strLoadAddress;
}
主要思路就是:利用_dyld_get_image_header
獲取鏡像的header結構體始鱼,header結構體是整個Mach-O的起始部分仔掸,所以,header結構體的首地址就是鏡像的加載地址风响。
好了嘉汰,到目前為止,使用atos符號化崩潰日志的三個條件(符號文件状勤、代碼構架鞋怀、鏡像加載地址)都有了双泪,那么我們就可以完成異常地址的符號化工作了。所以密似,到目前為止焙矛,我們定制的異常系統(tǒng)基本完成了,收集功能残腌、符號化動能都有了村斟。下面來看看我們的系統(tǒng)輸出的內容。
輸出Crash日志
本崩潰收集系統(tǒng)的輸出格式使用 JSON 格式抛猫,輸出的信息包括 arch蟆盹、CrashName、CrashReason闺金、CrashBackTrace逾滥、CrashSystemVersion 。有了這些信息败匹,我們完全可以符號化崩潰地址了寨昙。
{
"strCrashArch" : "arm64", ///代碼構架
"strCrashName" : "NSRangeException",
"strCrashSystemVersion" : "10.0.2",
"strCrashReason" : "*** -[__NSArrayI objectAtIndex:]: index 2 beyond bounds [0 .. 1]",
"aryCrashBackTrace" : [
{
"strStackAddress" : "0x000000018ec6c1d8",
"strImageName" : "CoreFoundation",
"strImageLoadAddress" : "<redacted>"
},
{
"strStackAddress" : "0x000000018d6a455c",
"strImageName" : "libobjc.A.dylib",
"strImageLoadAddress" : "objc_exception_throw"
},
{
"strStackAddress" : "0x000000018eb48584",
"strImageName" : "CoreFoundation",
"strImageLoadAddress" : "CFRunLoopRemoveTimer"
},
{
"strStackAddress" : "0x00000001000b48a0", ///崩潰地址
"strImageName" : "UncaughtException",
"strImageLoadAddress" : "0x1000B0000" ///鏡像加載地址
},
{
"strStackAddress" : "0x0000000194aea7b0",
"strImageName" : "UIKit",
"strImageLoadAddress" : "<redacted>"
},
........
........
{
"strStackAddress" : "0x0000000194b1b360",
"strImageName" : "UIKit",
"strImageLoadAddress" : "UIApplicationMain"
},
{
"strStackAddress" : "0x00000001000b4df0",
"strImageName" : "UncaughtException",
"strImageLoadAddress" : "0x1000B0000"
},
{
"strStackAddress" : "0x000000018db285b8",
"strImageName" : "libdyld.dylib",
"strImageLoadAddress" : "<redacted>"
}
]
}
小結
這章,我們使用蘋果的API完成了Crash日志收集系統(tǒng)掀亩,這個系統(tǒng)輸出的日志可以使用atos在線下符號化舔哪。同時介紹了Mach-O的文件結構。