Mach-O的符号与链接

Mach-O符号表

点击了解更多关于Mach-O。

Symbol Table 符号表,符号名称和地址
String Table 符号名称
Indirect Symbol Table 间接符号表。保存使用的外部符号,也就是使用的外部动态库的符号。Symbol Table的子集。

常用命令

查看符号:objdump --macho -t (mach-o文件);
nm -m (mach-o文件);objdump --macho --syms (mach-o文件)。
恢复符号号:./restore-symbol /Users/lenny/Desktop/CompileTest -o symbolDemo.symbol。
查看文件: file。
查看mach-header: objdump --macho -private-header (mach-o文件)。
查看 __TEXT: objdump --macho -d (mach-o文件)。
查看导出符号:objdump --macho --exports-trie (mach-o文件)。
查看间接符号表:objdump --macho --indirect-symbols (mach-o文件)。
``

符号分类

Mach-O文件中的符号 = 全局(给别人用) + 本地(自用)+ 间接符号(别人的动态库)。将全局符号导出给其他文件访问的符号叫导出符号,使用其他Mach-O文件导出符号叫导入符号,比如间接符号。此外还可以分为弱符号和非弱符号。以下将一一讲解。

全局符号(Global Symbol)

允许全局访问的符号,默认情况下其他文件是可以访问的。通过objdump --macho --syms 命令可以查看全局符号,比如("g"标记的表示全局(global)符号):


全局符号.jpg
本地符号(Local Symbol)

只允许内部文件访问的符号,比如下面的demo,用static 声明的静态变量,通过objdump --macho --syms 命令查看编译后的Mach-O文件(本地符号的标记是"l"):


本地符号.png
间接符号(Indirect Symbol)

本文件使用的外部符号,也就是使用的外部动态库的符号。Mach-O文件的间接符号可以通过objdump --macho --indirect-symbols命令进行查看,示例如下:


间接符号.jpg
导出符号

导出符号是Mach-O文件提供外部文件访问的符号。默认情况下全局符号一般都是导出符号,但是有时候可以通过链接器来控制导出符号。通过命令objdump --macho --exports-trie可以查看导出符号,比如:


隐藏全局符号.jpg
  • 过滤掉不需要的导出符号
    有一些符号我们不希望导出提供外部访问,比如下面我们自定义的一个OC类:
#import "MyObject.h"

@interface MyObject : NSObject

- (void)doSomething;
@end

@implementation MyObject

- (void)doSomething {
    NSLog(@"%s",__func__);
}
@end

查看导出符号:


OC导出符号.jpg

假设我们不希望MyObject被导出,可以通过Other linker flags配置如下过滤MyObject的符号:

-Xlinker -unexported_symbol -Xlinker _OBJC_CLASS_$_MyObject 
-Xlinker -unexported_symbol -Xlinker _OBJC_METACLASS_$_MyObject

通过命令objdump --macho --exports-trie查看导出符号:


过滤导出符号.jpg

已经没有MyObject的符号。
作用:优化包体积大小。

导入符号

导入的外部符号,也就是导入外部动态的符号。就比如间接符号表的符号,实际上就是当前Mach-O文件导入其他文件的符号。

弱符号(Weak Symbol)

弱符号(Weak Symbol)又分为弱引用符号(Weak Reference Symbol )和弱定义符号(Weak Defined Symbol)。

  • 弱定义符号
    表示此符号为弱定义符号。如果静态链接器或动态链接器为此符号找到另一个(非弱)定义,则弱定义将被忽略。只能将合并部分中的符号标记为弱定义。
    弱定义符号.jpg

    如果静态链接器或动态链接器为此符号找到另一个(非弱)定义,则弱定义将被忽略,比如下面demo中,在main.m中定义一个弱定义变量weak_def_variable,在MyObject.h中定义一个相同名称非弱定义变量,在mian函数中打印这个变量:
// 非弱定义符号
int weak_def_variable = 20;

@interface MyObject : NSObject

- (void)doSomething;
@end
NS_ASSUM
// 弱定义符号
int weak_def_variable __attribute__((weak)) = 10;
int main(int argc, char *argv[]) {
    NSLog(@"%d", weak_def_variable);
  return 0;
}

打印结果是20。这里表示weak_def_variable使用的是MyObject.h里的定义。

弱定义符号也是可以隐藏的,比如:

// 隐藏弱定义符号
void weak_hidden_function(void) __attribute__((weak, visibility("hidden")));

这样它就不会出现在导出符号表中。

  • 弱引用符号
    表示此未定义符号是弱引用。如果动态链接器找不到该符号的定义,则将其置为0,链接器会将此符号设置为弱连接标志。
    比如下面的弱引用方法weak_reference_function,只有声明,没有定义:
// 弱引用
void weak_reference_function(void) __attribute__((weak_import));
int main(int argc, char *argv[]) {
    
//    NSLog(@"%d", weak_def_variable);
    if(weak_reference_function){
        weak_reference_function();
    }
  return 0;
}

在Other Linker Flags 加入:

-Xlinker -U -Xlinker _weak_reference_function

这个告诉链接器weak_reference_function为未定义符号,只在运行时使用。这时候的weak_reference_function被连接器置为0,所以访问时没有报错。
弱引用读好的可以单个指定,也可以将整个动态库的导出符号全部制定为弱引用,这样即使存在未定义的符号也不会报错,有运行时处理(慎用,有风险)。

未定义符号(undefined)

未定义符号一般是在编译时无法确定它的定义符号。当dyld加载动态库时会根据undefined的符号加载对应的动态库。加载后,将未定义的符号绑定到动态库里对应的地址上。以下"UND"标记的是未定义符号:

未定义符号.jpeg

Swift符号

Swift是静态语言,其符号跟OC不一样,当然它也有全局和本地符号之分,比如下面的demo,定义了一个public类和private类:

public class SwiftPublicClass{

    func testFunc() { 
    }
}

private class SwiftPrivateClass {
   
    func testFunc() {
    }
}

通过objdump --macho --syms命令查看Swift符号:


swift符号.jpeg

可以看出Swift代码的符号比OC多得多。

符号剥除(Strip)

动态库:对于动态库来说,全局符号不能剥除掉,因为它是给外部文件访问的;
静态库对于静态库,它是.o文件和重定位符号表的合集。重定位符号符号不能剥除;
app:对于app来说除了间接符号表(访问其他动态库的符号表)不能剥除,其他如全局符号和本地符号都可以剥除以优化app的体积。
dead_code_stripping:连接器去除无用代码。剥除本地符号以及未使用的符号;
strip_style:

Debugging Symbols 剥除调试符号
Non-Global Symbols 保留全局符号(动态库)
All Symbols 剥除所有符号(除了间接符号,app)

问题:就符号来说,使用静态库体积和动态体积谁会比较小?
答:静态库。因为静态库直接编译连接到app中,除了间接符号都可剥除,而动态库不行。

符号冲突

动态库:动态库使用的是二维命名空间,符号调用先找库再找符号,所以没有符号冲突。
静态库:

1、没有调用的符号,静态库不会连接。
2、链接器在找到符号之后就不会再链接相同的符号了。
3、对OC来说,连接的最小单位是类。
4、ObjC告诉编译器添加(链接)所有的分类,解决冲突使用(-force_load 库路径),force_load表示在库重复时优先链接指定路径的库。

链接

Mach-O文件在编译时会生成相应的符号,链接是处理目标文件(.o文件)的过程。多个目标文件合并到到一起,多张表合并到一张表中。链接是根据符号表链接;LLVM是编译器工具链的一个集合:先对每个文件进行编译生成Mach-O(可执行文件);连接器会将多个Mach-O文件合并成一个。

连接器做了什么?

(1)去项目文件里查找目标代码文件里没有定义的变量;
(2)扫描项目中的不同文件,将所有符号定义和引用地址收集起来;
(3)计算合并后长度和位置,生成同类型的段并进行合并,建立绑定;
(4)对项目中不同文件里的变量进行重定位。

动态链接库和静态链接库

静态链接库是编译时链接的库,需要链接进你的Mach-O文件里,如果需要更新就要重新编译一次,无法动态加载和更新。
动态链接库是运行时进行链接的库,使用dyld实现动态加载。使用dyld加载动态库的两种方式:程序加载时绑定和符号第一次被用到时绑定。dyld做了什么事:

(1)先执行Mach-O文件,根据Mach-O文件里的undefined的符号加载对应的动态库,系统会设置一个共享缓存来解决加载的递归依赖问题。
(2)加载后,将undefined的符号绑定到动态库里对应的地址上;
(3)最后再处理+load方法,main函数返回后运行static terminator。

注:运行时通过dlopen和dlsym导入动态库时,先根据记录的路径找到对应的库,再通过符号名称在符号表找到绑定的地址。dlopen打开动态库后返回的是引用的指针,dlsym的作用就是通过dlopen返回的动态库指针和函数符号,得到函数的地址然后使用。

你可能感兴趣的:(Mach-O的符号与链接)