小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
符号是什么?,在链接过程中,我们将函数和变量统称为符号(Symbol),变量名或函数名就是符号名(Symbol Name)。
在链接器的上下文中,有三种不同的符号:
- 由模块m定义并能被其他模块引用的
全局符号。全局连接器符号对应于非静态C函数和全局变量。 - 由其他模块定义并被模块m引用的
全局符号,这些符号称为外部符号,它们对应于在其它模块中定于的非静态C函数和全局变量。 - 只被模块m定义和引用的局部符号,它们对应于带
static属性的C函数和全局变量。这些符号在模块m中任何位置都可见,但是不能被其他模块引用。
另外为了调试,还有一种调试符号。
全局符号和本地符号
查看符号表
在 main.m中,我们定义全局变量和局部变量,使用static关键字修饰的变量为局部变量,只限定义的文件可见。
int global_int_value = 10;
static int static_int_value = 20;
int main(int argc, const char * argv[]) {
global_int_value = 30;
static_int_value = 40;
return 0;
}
使用Xcode编译后,得到Mach O文件,并查看其符号表
objdump --macho --syms SymbolTask
结果为
SymbolTask:
SYMBOL TABLE:
0000000100008004 l O __DATA,__data _static_int_value
0000000000000000 d *UND* main.m
0000000100003f80 d *UND*
0000000100003f80 d *UND* _main
000000000000002a d *UND*
000000000000002a d *UND*
0000000000000000 d *UND* _global_int_value
0000000100008004 d *UND* _static_int_value
0000000000000000 d *UND*
0000000100000000 g F __TEXT,__text __mh_execute_header
0000000100008000 g O __DATA,__data _global_int_value
0000000100003f80 g F __TEXT,__text _main
0000000000000000 *UND* dyld_stub_binder
l: 代表本地符号。g: 代表全局符号。
可以看出,局部变量会变为本地符号(local),全局变量会变成全局符号(global)。
全局符号在编译的时候,会被导出,我们使用 --exports-trie选项查看其导出符号表。
MacBook-Pro Debug % objdump --macho -exports-trie SymbolTask
SymbolTask:
Exports trie:
0x100000000 __mh_execute_header
0x100003F40 _main
0x100008010 _global_int_value
我们可以看到,3个全局符号都被导出,
visibility
全局符号和 本地符号的本质上就是符号的可见性。全局符号对整个项目都可见,本地符号只是当前文件可见。
我们可以使用 visibility("hidden")和 visibility("default")来控制的符号的可见性。
代码如下:
int global_int_value = 10;
static int static_int_value = 20;
int hidden_var __attribute__((visibility("hidden"))) = 99;
double default_var __attribute__((visibility("default"))) = 100;
int main(int argc, const char * argv[]) {
global_int_value = 30;
static_int_value = 40;
return 0;
}
查看符号表
bel@beldeMacBook-Pro Debug % objdump --macho --syms SymbolTask
SymbolTask:
SYMBOL TABLE:
0000000100008004 l O __DATA,__data _hidden_var
0000000100008010 l O __DATA,__data _static_int_value
0000000000000000 d *UND* main.m
0000000100003f80 d *UND*
0000000100003f80 d *UND* _main
000000000000002a d *UND*
000000000000002a d *UND*
0000000000000000 d *UND* _global_int_value
0000000000000000 d *UND* _hidden_var
0000000000000000 d *UND* _default_var
0000000100008010 d *UND* _static_int_value
0000000000000000 d *UND*
0000000100000000 g F __TEXT,__text __mh_execute_header
0000000100008008 g O __DATA,__data _default_var
0000000100008000 g O __DATA,__data _global_int_value
0000000100003f80 g F __TEXT,__text _main
0000000000000000 *UND* dyld_stub_binder
可以看出,使用 __attribute__((visibility("hidden"))) 可以将全局符号变为本地符号。
外部符号
假设,在Foundation框架中,定义的全局符号NSLog,如果我们在自己的代码里引用了NSLog,对于我们的代码来言,_NSLog就是一个外部符号。
外部符号存放在了Mach O文件的间接符号表中,查看其间接符号表
bel@beldeMacBook-Pro Debug % objdump --macho --indirect-symbols SymbolTask
SymbolTask:
Indirect symbols for (__TEXT,__stubs) 1 entries
address index name
0x0000000100003f8e 15 _NSLog
Indirect symbols for (__DATA_CONST,__got) 1 entries
address index name
0x0000000100004000 17 dyld_stub_binder
Indirect symbols for (__DATA,__la_symbol_ptr) 1 entries
address index name
0x0000000100008000 15 _NSLog
我们看到,_NSLog是我们引用的外部符号
OC
OC类在编译的时候,默认都是全局符号,并且会被导出,即使方法没有在.h文件里面声明。
// LYObject.m
#import "LYObject.h"
@interface LYObject : NSObject
@end
@implementation LYObject
-(void)run{
NSLog(@">>>>>>>>> run");
}
@end
查看其导出符号表
bel@beldeMacBook-Pro Debug % objdump --macho -exports-trie SymbolTask
SymbolTask:
Exports trie:
0x100000000 __mh_execute_header
0x100003F20 _main
0x1000080B8 _OBJC_METACLASS_$_LYObject
0x1000080E0 _OBJC_CLASS_$_LYObject
在Build Setting中 设置 -Xlinker -unexported_symbol -Xlinker _OBJC_CLASS_$_LYObject 将_OBJC_CLASS_$_LYObject变成本地符号。
SymbolTask:
Exports trie:
0x100000000 __mh_execute_header
0x100003F20 _main
0x1000080B8 _OBJC_METACLASS_$_LYObject
也可以使用文件的形式,批量的将导出符号变为本地符号。
Swift符号
在Swift中,
使用 Private关键字修饰的类和方法都为本地符号。
使用 Public关键字修饰的类和方法都为导出符号。
弱符号
弱定义符号
可以使用 __attribute__((weak))来定义弱定义符号。
代码如下:
// WeakSymbol.h
void weak_function(void) __attribute__((weak));
// WeakSymbol.m
#import "WeakSymbol.h"
void weak_function(void) {
NSLog(@"weak_function");
}
// man,m
int main(int argc, const char * argv[]) {
weak_function();
return 0;
}
符号表内容,该处省略了调试符号的内容。
bel@beldeMacBook-Pro Debug % objdump --macho --syms SymbolTask
......
0000000100008010 l O __DATA,__data __dyld_private
......
0000000100000000 g F __TEXT,__text __mh_execute_header
0000000100003f40 g F __TEXT,__text _main
0000000100003f70 w F __TEXT,__text _weak_function // 全局符号
......
此时该弱定义符号为全局符号。
我们可以使用 __attribute__((weak, visibility("hidden"))) 将弱定义符号变为本地符号。
SYMBOL TABLE:
0000000100003f60 w F __TEXT,__text _weak_function //本地符号
0000000100008008 l O __DATA,__data __dyld_private
....
0000000100000000 g F __TEXT,__text __mh_execute_header
0000000100003f30 g F __TEXT,__text _main
...
对于链接器而言,如果有一个强符号和多个弱符号同名,会选择强符号,不会报编译错误。
我们在main.m中,新增一个同名函数
int main(int argc, const char * argv[]) {
weak_function();
return 0;
}
void weak_function(){
NSLog(@">>>>>> new Function");
}
// 运行结果
SymbolTask[9762:510689] >>>>>> new Function
弱引用符号
使用 __attribute__((weak_import))可以定义为弱引用符号。
// WeakImportSymbol.h
void weak_import_function(void) __attribute__((weak_import));
// WeakImportSymbol.m
#import "WeakImportSymbol.h"
#import <Foundation/Foundation.h>
void weak_import_function(void) {
NSLog(@"weak_import_function");
}
// main.m
#import <Foundation/Foundation.h>
#import "WeakImportSymbol.h"
int main(int argc, const char * argv[]) {
if (weak_import_function) {
weak_import_function();
}
return 0;
}
查看其符号表:
bel@beldeMacBook-Pro Debug % objdump --macho --syms SymbolTask
......
0000000100008008 l O __DATA,__data __dyld_private
......
0000000100000000 g F __TEXT,__text __mh_execute_header
0000000100003f30 g F __TEXT,__text _main
0000000100003f60 g F __TEXT,__text _weak_import_function
......
弱引用符号也是全局符号,弱引用符号如果没有方法实现, 会报undefined symbol错误。
我们可以在 Build Settings中的Other Linker Flags选项中设置 -Xlinker -U -Xlinker _weak_import_function,告诉链接器在运行时动态查找_weak_import_function 函数。
因为是弱引用函数,
如果有实现,就可以正常调用,如果没有实现,则会将这个函数置为NULL。
调试符号
DWARF
在第一个例子中
0000000000000000 d *UND* main.m
0000000100003f80 d *UND*
0000000100003f80 d *UND* _main
000000000000002a d *UND*
000000000000002a d *UND*
0000000000000000 d *UND* _global_int_value 0000000100008004 d *UND* _static_int_value 0000000000000000 d *UND*
d:表示是调试信息符号。
在iOS中,调试符号的格式为DWARF(Debug With Arbitrary Record Format),它记录了函数名、文件名、行数。在Xcode中的Release模式下会自动生成dSYM文件,
dSYM文件是DWARF格式数据的合集。
在 Debug模式下,我们将Debug Information Format设置为DWARF with dSYM File。
将
Deployment Postprocessing设置为Yes:
这样在Debug模式下就可以生成dSYM文件。
关于如何利用dSYM文件中的DWARF调试信息格式和如何恢复函数调用栈,可以查看我的这篇文章iOS的调试文件dSYM与DWARF
总结
在我们脱符号时,哪些符号可以脱去呢?调试符号和本地符号是可以脱去的,我们自己的App的Mach O文件中的间接符号表里面的符号是不能脱出的,里面存放的是系统动态库的导出符号,是需要在动态库中动态链接的。
我们可以通过链接器参数将全局符号变为本地符号,对于我们自己的动态库,如果想要减小动态库的体积,尽量把不必要的的全局符号变为本地符号。
弱符号可以用来做版本适配相关的工作,假设在iOS中有一个API,iOS 10 中不可用,iOS 11中可用,就可以使用弱符号。
使用调试符号我们可以恢复函数调用栈,在发布时,将调试信息剔除,也可以减少包的体积。
如果觉得有收获请按如下方式给个
爱心三连:👍:点个赞鼓励一下。🌟:收藏文章,方便回看哦!。💬:评论交流,互相进步!。