符号(Symbol):简单来讲,类、函数和变量的统称;类名、函数名或变量名称为符号名(Symbol Name);git
按类型分,符号能够分三类:github
符号表(Symbol Table):符号表是内存地址与函数名、文件名、行号的映射表;每一个定义的符号有一个对应的值,叫作符号值(Symbol Value),对于变量和函数来讲,符号值就是他们的地址;符号表元素以下所示:objective-c
<起始地址> <结束地址> <函数> [<文件名:行号>]
复制代码
dSYM(debug symbols):是iOS的符号表文件,存储16进制地址信息和符号的映射文件;文件名一般为:xxx.app.dSYM,相似Android构建release产生的mapping文件;利用dSYM文件文件,能够将堆栈信息中地址信息还原成对应的符号,帮助问题排查;shell
Symbol Table
存储着全局符号和局部符号; DWARF
存储着符号的行号信息。Xcode 编译时有几个选项是和符号是相关的。数组
Debug Information Format:DWARF
OR DWARF with dSYM File
。 这配置对于静态库会无影响;对动态库有影响:设置为 DWARF with dSYM File
,生成动态库时会生成相应的 dSYM 文件;若是设置为 DWARF,则 dwarf 段即调试信息没有地方存放将丢失。bash
Generate Debug Symbols:设置为YES,编译生成目标文件时会生成调试信息;设置为 NO,那么 dwarf 段不会生成,也不会有 dSYM 文件生成,而且调试过程使用的断点也不会生效,由于地址已经没法和对应代码行关联起来了。app
Deployment:函数
Deployment Postprocessing:若是为 YES
,在编译生成目标文件以后要进行后续处理;若是为 NO
,则不会有后续处理;使用 Xcode Archive
进行编译,Deloyment Postprocessing
的值恒为YES
;post
Strip Linked Product:若是为 YES
,则进行裁剪;若是为 NO,则不进行裁剪;至于裁剪什么级别的符号由 Strip Style 配置决定;若是Deployment Postprocessing为NO,Strip Linked Product设置无效;测试
Strip Style(Deployment Postprocessing和Strip Linked Product都为YES,才生效;去除的是二进制中的符号):
DWARF
中的调试信息以及Symbol Table
中目标模块定义的全局、局部符号信息。补充1: 动态库和静态库不能去除所有符号(Strip All Symbols),要保留全局符号(选择Non-Global Symbols),他们是库和其余库连接时沟通的桥梁;失去了全局符号,动态库和静态库就成为了黑盒。
补充2: 去除符号的操做对于 dSYM 文件中的符号信息没有影响;对于动态库和可执行二进制文件,能够将符号尽量去除掉减小二进制体积的大小。须要符号进行符号化崩溃日志时,再从 dSYM 文件中找对应符号。
Symbols Hidden by Default :这是全局的开关,用来设置符号的默承认见性,设置为YES,会把全部符号都定义成”private extern”;
也能够可使用编译器属性__attribute__((visibility("default")))
和__attribute__((visibility("hidden")))
来控制符号的可见性;
__attribute__((visibility("default"))) void MyFunction1() {} //可见
__attribute__((visibility("hidden"))) void MyFunction2() {} //不可见
复制代码
Debug Information Format 设置为 DWARF
;由于生成 dSYM 文件是一个比较耗时的过程,选择DWARF
能节省调试时间;
Generate Debug Symbols:设置为 YES;这样才能支持断点调试;注意Debug模式下,Deployment Postprocessing 必定要NO,不然Generate Debug Symbols的设置了YES,也不支持断点调试;
Deployment配置:
如此配置后,App二进制中带有全局符号和局部符号信息,二进制自己能够支持不使用 dSYM 文件自解析出符号(自解析出的符号不包含行号)。
Symbols Hidden by Default 设置为YES;
Debug Information Format 设置为 DWARF with dSYM File
;这样生成ipa的同时,会一并生成 dSYM文件。
Generate Debug Symbols:设置为 NO;这样才能支持断点调试;注意Debug模式下,Deployment Postprocessing 必定要NO,不然Generate Debug Symbols的设置了YES,也不支持断点调试;
Deployment配置:
如此配置,App中不带任何符号,能够减小安装包大小,还能避免符号泄漏。定位问题时,能够经过 dSYM 文件去获取符号。
Symbols Hidden by Default 设置为YES;
静态库配置:Debug Information Format
默认就好,Generate Debug Symbols
设置YES,Deployment Postprocessing
设置为 NO,Strip Linked Product
设置为 NO,Strip Style
默认就行(Strip Linked Product设置为 NO,Strip Style配置无所谓;Symbols Hidden by Default
设置为NO;
静态库如此配置,实际上是没有裁剪二进制的符号的;所以,静态库的二进制的大小将会大大增长,可是静态库的大小并不影响最终安装包二进制的大小,同时调试符号能支持安装包或者连接的动态库生成相应的 dSYM 文件,方便定位静态库中的问题。
动态库配置:Debug Information Format
分Debug和Release,配置同App;Generate Debug Symbols
设置YES,Symbols Hidden by Default
设置为NO;
Deployment Postprocessing
设置为 YES;Strip Linked Product
设置为 YES; Strip Style
设置为 Non-Global Symbols
Deployment Postprocessing
设置为 NO;Strip Linked Product
设置为 NO;Strip Style
设置啥均可以(由于Strip Linked Product 是NO,Strip Style随便配置什么都无影响)若是动态库
Symbols Hidden by Default
设置为 YES,动态库仍然能编译经过,可是App会报一堆连接错误,由于符号变成了hidden。
symbol默认是strong的,可是能够增长 __attribute__ ((weak))
属性将其变成weak symbol;weak symbol在连接时候比较特殊:
应用场景:用weak symbol提供默认实现,外部能够提供strong symbol把实现注入进来,以此来实现依赖注入。
.a
中的某个.o
符号被引用的时候,这个.o
才会被ld写到最后的二进制文件中,不然会被丢掉,other linker flags
提供三个选项来解决保留代码的问题。
-ObjC
保留全部Objective C的代码;-force_load
保留某一个静态库的所有代码;-all_load
保留参与连接的所有的静态库代码;other linker flags
里添加*-ObjC
*。当连接的时候类找不到了,会报错符号_OBJC_CLASS_$_CLASSNAME
找不到;
以前在接入 AlipaySDK遇到过(缘由是: AlipaySDK和阿里百川SDK冲突致使,须要接入UTDID framework)
Undefined symbols for architecture x86_64:
"_OBJC_CLASS_$_UTDevice", referenced from:
objc-class-ref in AlipaySDK
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
复制代码
补充1:若是类的符号没有被裁减掉,运行时就用
_OBJC_CLASS_$_CLASSNAME
做为参数,经过dlsym来获取类指针。补充2:nm app_name.app/app_name 执行返回中,小写字母对应着本地符号,大写字母表示全局符号;U表示undefined,即未定义的外部符号;
运行时,使用还能够用lldb去查询符号相关的信息;
查看符号的定义
image lookup -t symbol_name
复制代码
查看符号的位置
image lookup -s symbol_name
复制代码
设置符号断点
breakpoint set -F "symbol" #也可经过Xcode的GUI能设置
复制代码
开发阶段,不会裁剪符号,因此一切都比较美好;对一个地址进行符号化比较直接:找到地址所属的内存镜像,而后定位该镜像中的符号表,最后从符号表中匹配目标地址的符号。
可是裁剪符号的包,如企业内测包AppStore选择了裁剪符号的方式,甚至是裁剪所有符号(Strip Style 设置为 All Symbols );常规符号化不能解决问题;
符号裁剪的好处:减小了安装包大小,还避免符号泄漏;
企业内测包不一样于AppStore包,主要用于内部测试和灰度,有时候须要收集问题的上下文信息,这些信息中包括发生问题的代码行数、代码文件和函数名,甚至包括堆栈符号;
裁剪符号后,[NSThread callStackSymbols]
获取的不少堆栈地址须要符号恢复;而此时的dladdr
也不能根据地址获取符号信息;
不论符号有没有被裁剪,均可以经过如下C语言中的预约义符获取,具体以下:
__FILE__ //File path
__LINE__ //Code Line
__FUNCTION__ //Funcation Name
//demo
printf("File = %s\nLine = %d\nFunc=%s\n", __FILE__, __LINE__, __FUNCTION__);
复制代码
不论符号有没有被裁剪,也能够经过Objective-C的_cmd
方法获取当前方法名,eg以下
printf("call %s", [NSStringFromSelector(_cmd) UTF8String]);
复制代码
经过预约义符和_cmd
方法获取当前调试符号信息,在系统API中也常有使用,如NSAssert宏的使用,源码以下:
#define NSAssert(condition, desc, ...) \
do { \
__PRAGMA_PUSH_NO_EXTRA_ARG_WARNINGS \
if (__builtin_expect(!(condition), 0)) { \
NSString *__assert_file__ = [NSString stringWithUTF8String:__FILE__]; \
__assert_file__ = __assert_file__ ? __assert_file__ : @"<Unknown File>"; \
[[NSAssertionHandler currentHandler] handleFailureInMethod:_cmd \
object:self file:__assert_file__ \
lineNumber:__LINE__ description:(desc), ##__VA_ARGS__]; \
} \
__PRAGMA_POP_NO_EXTRA_ARG_WARNINGS \
} while(0)
#endif
复制代码
NSAssert适合于Objective-C方法,利用
__FILE__
、__LINE__
、_cmd
来获取发生问题时候的代码文件路径、代码行数、方法名,而后将这些交给[[NSAssertionHandler currentHandler] handleFailureInMethod:object:file:lineNumber:description:]
处理;
咱们习惯性利用[NSThread callStackSymbols]
获取当前线程调用堆栈符号信息,可是这只在Debug模式下比较理想;在符号被裁剪的状况下,获取的地址须要作符号恢复;
若是利用dSYM文件来符号化是能够的;能够参考个另类方案:根据全部的类方法、方法名、方法实现地址,将调用栈的内存地址符号化;相似Frida调用栈符号恢复,方案具体描述:
[NSThread callStackReturnAddresses]
获取调用栈的内存地址;须要说明的是,这里的符号指的是:Objective-C的函数符号
,由于若是C函数符号被strip后,是没有办法恢复其符号的;
在符号裁剪状况下,dladdr
通常不能经过地址获取到符号;能够用以下代码测试
NSArray<NSNumber *> *addresses = [NSThread callStackReturnAddresses];
NSNumber *firstAddress = [addresses objectAtIndex:0];
Dl_info info;
int result = dladdr((const void *)[firstAddress integerValue], &info);
if (result != 0 && info.dli_sname) {
//Debug模式配置
printf("经过dladdr函数获取symbol_name = %s", [[NSString stringWithUTF8String:info.dli_sname] UTF8String]);
} else {
//Release模式配置
printf("符号裁剪后,不能经过dladdr函数获取符号,须要[新符号恢复方案]");
}
复制代码
若是
dladdr
能经过地址拿到符号信息,就说明符号没有裁剪,能够直接用[NSThread callStackSymbols]
Crash主要有两类:Mach 异常和Objective-C 异常(NSException)引发的;
Mach异常是最底层的内核级异常,如EXC_BAD_ACCESS(内存访问异常);而Objective-C 层不能获取Mach异常,可是Mach 异常到了 BSD 层会转换为对应的 Signal 信号,咱们能够注册SIGABRT, SIGBUS, SIGSEGV等信号发生时的处理函数。
//注册处理SIGSEGV信号
signal(SIGSEGV,handleSignal);
// 注册处理其余信号 ....
//信号处理函数
static void handleSignal( int sig ) {
}
复制代码
NSException异常是iOS库或者各类第三方库或Runtime验证出错误而抛出的异常。如NSRangeException
(数组越界异常),它们能够被try catch捕获(苹果不建议用),若是未被捕获或被@throw抛出,能够经过注册NSSetUncaughtExceptionHandler
函数来捕获处理。
//注册异常处理函数
NSSetUncaughtExceptionHandler(&uncaught_exception_handler);
//异常处理函数
static void uncaught_exception_handler (NSException *exception) {
//能够取到 NSException 信息
//...
abort();
}
复制代码
目前常见的符号号手段
第一种作法通常是研发本身用;第二种适用于作成标准方案,批量帮助将线上的Crash 堆栈符号还原;