首先我们来认识一下什么是链接:
- 链接的本质就是把一个或多个目标文件和需要的库(静态库/动态库,如果需要的话)组合成一个文件(
Mach-O
可执行文件)
通常.o
文件被我们称之为目标文件。
下面我们来看一下目标文件的生成过程:
- 这里大家要注意一下:
在生成目标文件的过程中
1、链接器 (llvm-ld
) 并没有被执行。
2、目标文件不会包含Unix
程序在被装载和执行时所必须的包含信息。
那么上面这句话究竟是什么意思呢?
接下来我们来探索一下:
首先我们在Mach-O 文件这篇文章里面已经简单了解了Mach-o
文件的格式,这里我们再来加深一下印象,这里我们将通过终端打印一下Mach-o
的一些相关信息。
1、首相我们建立一个命令行工程test
,在工程中引入一个脚本脚本地址,配置如下:
2、创建我们自己的xcconfig
文件(这一步有不明白的同学可以阅读Xcode 多环境的配置这一批文章)
3、在xcconfig
文件中输入脚本要用的一些参数(注意:1、这里不是shell
指令,是Key-Value的形式,2、此时我们还没有写任何代码)
脚本中需要输入两个变量:
CMD
: 指令
和
TTY
:指定的终端(可以终端输入tty
,会打印当前终端的信息)
同时我们还需要知道当前Mach-O
的地址:
MACH_PATH=${BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/${PRODUCT_NAME}
MACH_PATH
:我们自己定义的变量,用来存储Mach-O
的地址${BUILD_DIR}
:当前编译的路径$(CONFIGURATION)
:构建产品的目录$(EFFECTIVE_PLATFORM_NAME)
:Mach-O
所在的目录${PRODUCT_NAME}
:项目名称,也就是Mach-O
的名称
① 首先我们打印一下Mach header
// 查看mach-header
CMD = objdump --macho -private-header ${MACH_PATH}
这里大家可以看到
Mach header
的一些原始的信息。
② 接下来我们查看一下
Mach-o
里面的__TEXT
段(因为:main
函数被编译之后会放到__TEXT
段的)
可以看到显示的
__TEXT
段、__text section
的机器指令(如:55
),后面跟着的汇编代码是给开发者看的。
在底层会有一个类似与字典的东西,会将汇编指令和机器码一一对应起来
下面我们在
main.m
文件中添加一些代码,在来看一下__TEXT
段(代码段):
/// 全局变量
int global_uninit_value;
int global_init_value = 10;
//__attribute__关键字主要是用来在函数或数据声明中设置其属性。给函数赋予属性的主要目的在于让编译器进行优化。
double defaule_x __attribute__((visibility("hidden")));
/// 静态变量 -》本地符号
static int static_uninit_value;
int main(int argc, const char * argv[]) {
static_uninit_value = 10;
NSLog(@"%d", static_uninit_value);
return 0;
}
我们会发现
__TEXT
段多了很多东西。
可以看到
NSLog
直接就变成了callq 0x100003f90
指令,一个有明确地址的指令。
其实在编译生成
.o
文件的过程中是做了这样一件事情:
1、能变成汇编的,尽量变成机器码
2、符号归类(比如:数据放到数据段,等等);再比如NSLog
,在生成目标文件的时候,我们并不知道它的地址,这个时候就要将它临时发到一个地方。将符号归类之后,放到重定位符号表里面。为什么要放到重定位符号表里面呢?
1、因为在生成.o
文件的时候,整个的地址并没有虚拟化(虚拟内存的地址)
2、在生成.o
的过程中,我们链接的符号,一些我们知道它的位置(如:global_init_value
,因为在同一个Mach-o
中,我们可以通过偏移地址直接取到符号);但是有一些导入符号我们是不知道的(如:NSLog
)
也就是说:重定位符号表里面放的就是.o
文件里面用到的API,没有用到的就不在这里面。
既然重定位符号表是这样的,那么我们也就可以通过检查.o
文件里面的重定位符号表来查看文件里面对某种API的使用情况。接下来
.o
会进入链接过程,处理我们编译的情况;会把生成的多个.o
文件合并成一个,这也就意味着,大家的符号表(包括重定位符号表)都会被合并到一张表中。最后生成可执行文件(exec)
因此我们说的链接,就是在处理
.o
文件中符号的过程
全局符号与本地符号
结合上面的代码:
- 全局符号:
int global_uninit_value;
全局符号对整个项目可见,对使用它的项目也是可见的 - 本地符号:
static int static_uninit_value;
本地符号只对定义它的文件可见
接下来我们来查看一下符号表:
// 查看符号表
CMD = objdump --syms ${MACH_PATH}
上图中有一点不同,不知道大家注意到没有:
defaule_x
这个符号我们定义的是一个全局符号,但是最终它是一个本地符号,这是为什么呢?
因为我们是这样写的:
double defaule_x __attribute__((visibility("hidden")));
这里我们就要引入visibility
属性:
// visibility属性,控制文件导出符号,限制符号可见性
/**
-fvisibility:clang参数
default:用它定义的符号将被导出。
hidden:用它定义的符号将不被导出。
*/
// 隐藏 -> 本地
int hidden_y __attribute__((visibility("hidden"))) = 99;
// 符号
double default_y __attribute__((visibility("default"))) = 100;
这里说明一下,我们在开发中经常会遇到被Apple遗弃的方法,会有一条黄线的警告,其实也是用了这个属性。
导入符号 & 导出符号
还是上面的代码,我们用到了NSLog
(它存在于Foundation
动态库中);那么对于我们自己的可执行文件NSLog
就是导入符号;对于Foundation
动态库NSLog
就是导出符号。
这也就意味着导出符号是全局符号。
下面我们打印一下导出符号:
// 查看导出符号
CMD = objdump --macho --exports-trie ${MACH_PATH}
可以看到正好对应的是我们设置的全局变量。
在日常开发中我们要注意:当我们把变量设置成全局变量的时候,也就意味着会被默认设置成导出符号。
- 这里我们补充一下,间接符号表里面保存着,我们当前可执行文件使用的其他的动态库里面的导出符号
下面我们来打印一下间接符号表:
// 查看间接符号表
CMD = objdump --macho --indirect-symbols ${MACH_PATH}
- 这里就有一个问题了,在我们在
strip
符号剥离的时候,间接符号表里面的面的符号是我们不能剥离的,那我们反过来向上推,也就意味着我们在写自己的OC的动态库的时候没办法剥离我们没有暴露的符号。这里跟大家讲一下,OC默认的符号都是全局符号,而全局符号又是导出符号,我们来验证一下:
接着我们来打印一下导出符号
// 查看导出符号
CMD = objdump --macho --exports-trie ${MACH_PATH}
- 不导出符号
OTHER_LDFLAGS=$(inherited) -Xlinker -unexported_symbol -Xlinker _OBJC_METACLASS_$_YSOneObject
我们成功的将符号_OBJC_METACLASS_$_YSOneObject
设置成非导出符号
这样我们就可以对齐进行符号剥离,进一步减少动态库的体积;同时外界也就看不到我们的符号了。
⚠️ 开发中我们也不必一个一个的去将符号设置成不导出,我们可以指定一个文件来设置符号的导出属性,使用-unexported_symbol_list
这个指令
- 我们还可以查看我们使用的所有的符号,并写入到相应的文件中:
OTHER_LDFLAGS=$(inherited) -Xlinker -S -Xlinker -map -Xlinker /Users/aaron/Desktop/test-02/Source.text
弱符号(Weak Symbol)
弱符号分为:
- Weak Reference Symbol(弱引用符号):表示此未定义符号是弱引用。如果动态连接器找不到该符号的定义,则将其设置为0。链接器会将此符号设置弱链接标志。
- Weak defintion Symbol(弱定义符号):表示此符号为弱定义符号。如果静态链接器或动态链接器为此符号找到另一个(非弱)定义,则弱定义将被忽略。只能将合并部分中的符号标记为弱定义。
那么怎么理解上面的两句话呢?
首先我们来看在代码中怎么写:
// 弱引用
void weak_import_function(void) __attribute__((weak_import));
// 弱定义
// weak def
void weak_function(void) __attribute__((weak));
// weak 本地符号
void weak_hidden_function(void) __attribute__((weak, visibility("hidden")));
首先我们来看一下:
- Weak defintion Symbol(弱定义符号)
其中weak_function
为全局弱定义符号;weak_hidden_function
为本地弱定义符号。
接着我们查看一下到处符号表:
CMD = objdump --macho --exports-trie ${MACH_PATH}
可以看到弱定义并不影响其作为导出符号。
接着我们在其他地方在定义一个名字相同的全军符号,如下:
我们会发现,工程并不会报错。运行起来也没有问题,这正式我们上面说的:如果静态链接器或动态链接器为此符号找到另一个(非弱)定义,则弱定义将被忽略。
接下来我们再看一下:
- Weak Reference Symbol(弱引用符号)
首先我们只声明,不实现:
void weak_import_function(void) __attribute__((weak_import));
同样的查看导出符号表:
我们会发现导出符号表里面并没有该符号。
接下来我们实现以下该函数:
void weak_import_function(void) {
NSLog(@"weak_import_function");
}
然后再查看一下导出符号表:
这也就是当动态链接器找不到它的定义,则将其定义为0,也就不会出现在导出符号表里面了。
- 既然是这样,那我们只声明,不定义该符号。通过判断的符号是否为0,去做一些事情呢?
if (weak_import_function) {
weak_import_function();
/**
一些其他的业务
*/
}
我们cmd + B
会发现,工程报错:
也就是说说在链接的过程中,链接器找不到这个符号(不知道符号具体在哪个地方)。
这个时候我们可以告诉链接器,不要管这个符号,即使是未定义的也不要管,我这个符号是动态链接的,到时候会自己找到这个符号。那么我们可以通过下面的指令来达到这个目的:
OTHER_LDFLAGS=$(inherited) -Xlinker -U -Xlinker _weak_import_function
重新导出符号
举一个例子:
我们上面的代码在mian.m
文件中用到的NSLog
这个函数。然而NSLog
对于当前工程来说是一个未定义符号
那么此时,我们如果想要使用我们这个库的工程,也能够使用
NSLog
,此时我们就需要将NSLog
以别名的形式从新导出。
⚠️ 这里要注意:只能给间接符号表中的符号起别名。
OTHER_LDFLAGS=$(inherited) -Xlinker -alias -Xlinker _NSLog -Xlinker YS_NSLog
我们再来查看一下导出符号表:
可以看得到
YS_NSLog
作为导出符号,并且被标记为re-export
Swift 符号
首先我们在swift
文件中定义一些结构体、方法。
struct YSSwiftStructSymbol {
func testSwiftStructSymbol(o: Int) {}
}
private protocol YSSwiftProtocolSymbol: class {
func testSwiftProtocolSymbol()
}
private class YSSwiftClassSymbol {
func testSwiftSymbol() {}
}
接着我们查看一下现在的符号表:
// 查看符号表
CMD = objdump --syms ${MACH_PATH}
会发现这里面多了很多的符号。
那么我们来定向查找
swift
产生的符号:
CMD = objdump --syms ${MACH_PATH} | grep "SwiftClass"
这样就少了很多,并且全部都是本地符号。下面我们修改一下
swift
中方法的权限:
public class YSSwiftClassSymbol {
func testSwiftSymbol() {}
}
再来查看一下swift
符号:
可以看到有一些符号已经变成了全局符号
总结:Swift是一门静态语言,跟OC不一样。Swift在编译的时候就能确定符号的类别。