iOS 理解符号多一点

3,110 阅读9分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

符号是什么?,在链接过程中,我们将函数变量统称为符号(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

也可以使用文件的形式,批量的将导出符号变为本地符号

unexport_file.png

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错误。

截屏2021-10-25 下午11.44.33.png

我们可以在 Build Settings中的Other Linker Flags选项中设置 -Xlinker -U -Xlinker _weak_import_function,告诉链接器在运行时动态查找_weak_import_function 函数。

-u xlinker.png 因为是弱引用函数,如果有实现,就可以正常调用,如果没有实现,则会将这个函数置为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截屏2021-10-26 上午12.41.05.pngDeployment Postprocessing设置为Yes:

截屏2021-10-26 上午12.41.05.png

这样在Debug模式下就可以生成dSYM文件。

关于如何利用dSYM文件中的DWARF调试信息格式和如何恢复函数调用栈,可以查看我的这篇文章iOS的调试文件dSYM与DWARF

总结

在我们脱符号时,哪些符号可以脱去呢?调试符号本地符号是可以脱去的,我们自己的App的Mach O文件中的间接符号表里面的符号是不能脱出的,里面存放的是系统动态库的导出符号,是需要在动态库中动态链接的。

我们可以通过链接器参数全局符号变为本地符号,对于我们自己的动态库,如果想要减小动态库的体积,尽量把不必要的的全局符号变为本地符号。

弱符号可以用来做版本适配相关的工作,假设在iOS中有一个API,iOS 10 中不可用,iOS 11中可用,就可以使用弱符号

使用调试符号我们可以恢复函数调用栈,在发布时,将调试信息剔除,也可以减少包的体积。

如果觉得有收获请按如下方式给个 爱心三连:👍:点个赞鼓励一下。🌟:收藏文章,方便回看哦!。💬:评论交流,互相进步!