前言
上篇文章多环境配置、Mach-O与链接器,但是Symbol还没又说道,这篇文章我们继续上篇文章内容讲下去
.xconnfig补充
上面文章在介绍多环境配置的时候讲到了.xconnfig,说到了.xconnfig可以统一管理环境配置,这里可以根据不同的条件配置不同的设置,我们那Other Linker Flags来说明
上图配置意思就是在
Debug环境下,设备为模拟器,切架构为x86时添加framework "Man"。
我们看到此时在
arm64下编译时成功的,因为Other Linker Flags并没有导入Man
这次我们看到在
x86_64环境下,编译时发现报错,告诉我们找不到Man,这是因为这种环境下我们的Man被放入了项目环境中,所以才会提示找不到
作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS开发公众号:编程大鑫,不管你是小白还是大牛都欢迎入驻 ,让我们一起进步,共同发展!
Mach-O再讲
上篇文章只是粗略的讲了Mach-O,这里再补充一下
Mach-O结构
Mach-O的结构图如下:解释如下:
- 1.
Mach Header告诉执行者自己包含哪些信息(也就是这个Mach-O的身份信息) - 2.
Load Command就是配置文件,最后三个才是我们的代码编译后的文件位置 - 3.
配置文件都记录一些必要的文件信息,以Load Command _TEXT为例,它里面记录下面内容:text代码段的大小text代码段的起始位置
- 记录其它
必要信息,比如:UUID标识符,Version版本,Dylinker连接器位置,Linkdit动态库信息,Dylib引入哪些库,指定入口为Main函数(你可以不制定Mian函数作为入口)
【注意】:每次读都能保证读完一个Load Command,这是因为这些信息的排列是按照结构体对齐的方式进行存储排列的,所以按着约定好的字节数,就能正好读完
Mach Header
Mach Header主要结构如下:解释几个主要的:
cputype:架构filetype:是可执行文件还是目标文件sizeofcmds:大小
__TEXT
我们通过命令来看下main.m在x86下编译成text情况-
最左边的是地址,我们看到main的起始地址为100003f20,结束地址为100003f5e。 -
中间的是机器码,给机器读的 -
右边是汇编,给开发者读的
这个有点像查字典,提前约定好汇编和机器码的对应关系,当读机器码55时,就对应着汇编:pushq %rbp,以此类推
Mach-O特性
上篇文章讲了Mach-O是可读,可写的。可读我们已经说了,可写是什么意思?Mach-O之所以能被执行是因为有签名,当我们修改了Mach-O文件,需要重新签名才能被苹果系统所接受。这也是为什么破解软解都需要重新签名的原因。
链接器
生成目标文件过程
- 1.
链接器(llvm-ld)并没有被执行 - 2.
目标文件不会包含Unix程序在被装载和执行时所必须的包含信息
上面不是很好理解,我们直接通过代码来解释
代码讲解
我们在main.m文件中写如下代码:我们看到.m中有定义的属性了,我们再看看此时编译为__TEXT是什么样
我们和上面的相比较发现多了很多东西例如:NSLog此时变成了一个指令callq地址0x100003f60
也就是说在编译的时候:
- 1.把
能变成汇编的先变成汇编,机器码。 - 2.把
属性转成符号进行归类->放入重定位符号表(重定位符号表就是放.m/.o用到的API) - 3.
.o->链接器->一张表->可执行文件exec
之所以要放入重定位符号表,是因为已经放入符号表中,在生成.o文件时,其地址还未虚拟化,链接器在进行连接的时候,会对重定位符号表进行合并。
通过上面我们可知链接就是处理目标文件符号的过程
指令查看重定位符号表
命令:objdump --macho --reloc +.o文件,运行后,重定位符号表打印如下:
未用到的将不会放到.o文件,通过这个特性,我们可以通过查看.o文件来查看文件对某种API的使用情况
符号(Symbol)
我们通过指令查看下main.m的符号表l:local布局的意思g:global全局的意思d:Debug的意思o:Data的意思F:Function的意思
我们发现上面符号表有很多Debug模式下的输出,下面我们用命令将这部分去掉。我们可以通过strip命令也可以通过链接器参数-S
就是
链接的时候不把调试符号放到最终生成的可执行文件中。
-
调试符号:当我们的文件通过汇编器会生成一个DWARF格式的调试文件,它会被放在Mach-O的__DWARF段中,在连接的时候会把__DWARF段干掉,同时把__DWARF段变成符号,放到符号表中。
通过上面两个图可以知道
全局变量都是g(全局符号),而本地变量都是l(局部符号),而将全局符号变为本地符号:1.加static2.使用__attribute__关键字(第16行)
导入导出符号
我们知道
NSLog是Foundation框架下的,写在19行,相当于是导入了NSLog符号,又因为Foundation又导出了NSLog符号,让其它地方使用(导出符号又是全局符号)
下面我们看下main.m中有哪些导出符号,通过在.xcconfig中写入命令,编译
下面我们创建一个OC类,再查看符号我们看到有
4个导出符号,它正好对应上面打印符号表中的4个全局符号,这也就意味着当我们声明全局符号时,会默认为导出符号,其它地方也可以使用
OC类都会默认为导出符号
间接符号表
我们知道动态库是在运行的过程中加载,也就意味着它在编译链接阶段只需要提供符号就可以了,上篇文章我们在说符号表时提到:间接符号表保存这项目使用的其它动态库的符号,下面我们通过在.xcconfig中写入命令,编译来查看间接符号表
这里面我们就只认识最后的
NSLog,这个是Foundation给我提供的导出符号
总结
- 1.
全局符号可以变成导出符号给外界使用 - 2.
间接符号表不能删除,意味着动态库中的全局符号不能删除,也就说明在strip动态库时,不能strip全局符号 - 3.
OC类在编译时都会默认为导出符号,那么我们在用OC写动态库时,如果想尽可能让动态库包小些,我们可以在.xcconfig定义参数不导出符号-
进行编译
-
和上面的相比发现少了一个
_OBJC_CLASS_$_LjOneObject,相同的方法可以让_OBJC_METACLASS_$_LjOneObject也消失
-
补充
上面总结说了可以通过不导出符号来使动态库体积减小,但是如果我们要写的类太多了怎么办,其实给了有方法:
- 1.
可以执行文件
- 2.1中的文件的获得可以
通过查看当前文件使用类库的信息
红框内是告诉开发者,生成了
几个目标文件,项目使用了哪些库文件。通过map可以导出符号信息,链接信息
Weak Symbol
Weak Symbol具体分一下两种
1.
Weak Reference Symbol:表示此未定义符号是弱引用。如果动态链接器找不到该符号的定义,则将其设置为0。链接器会将此符号设置弱链接标志。2.
Weak defintion Symbol:表示此符号为弱定义符号。如果静态链接器或动态链接器为此符号找到另一个(非弱)定义,则弱定义将被忽略。只能将合并部分中的符号标记为弱定义。
Weak Reference Symbol
Weak Reference Symbol(弱引用)写法:我们通过代码来说明一些问题,在main函数写如下代码:上面的解释:也就是如果若引入符号未被定义(不想要实现),系统不会报错
但是这么写会报错,原因:在说编译链接原理的时候说过,符号怎么来查找的呢?我们在WeakImportSymbol.h写了声明,在main函数中使用,就是用的API,但是在连接的时候,我们需要知道符号具体的地址在什么地方,否则提示找不到
我们可以告诉编译器,我这个符号时动态链接的,不要管它的具***置即使它是弱引用的,到时候dyld运行起来,自己会查找的
-U参数就是告诉编译器这个没有定义,需要动态查找
再次运行就会成功了,那么这个有什么用处呢?比如:我们可以判断其它库里,有没有这个符号,有这个符号我就调用,没有这个符号我就不调用。还有个用途就是在动态库上,我们可以将整个动态库文件声明成一个弱引用,这个有什么好处呢?也就意味着如果你这个库没有导入的话,也不会报动态库找不到的错误。
Weak defintion Symbol
Weak defintion Symbol(弱定义)写法:上面讲到弱定义符号:如果静态链接器或动态链接器为此符号找到另一个(非弱)定义,则弱定义将被忽略,怎么理解这句话呢?我们通过代码来理解
- 1.在.h中我们
弱定义了weak_function方法 - 在.m中我们实现这个
弱定义方法
方法实现和声明都是全局的,上面讲了应该转为导出符号,下面我们变一下,看下打印
当我们在.m声明相同的方法当声明为
弱定义方法,并不影响作为导出符号导出
下面我们在main函数中调用这个方法如果
正常情况下,由于方法名相同,运行应该会报错,但是由于这个方法被弱定义,此时编译是不会报错的。
如果我们把弱定义的符号声明成一个隐藏符号,此时它应该是一个弱定义的本地符号我们看到执行了
main函数的weak_function方法,并没有执行WeakSymbol的weak_function,这也就是上面说的:如果静态链接器或动态链接器为此符号找到另一个(非弱)定义,则弱定义将被忽略
重新导出符号
当我们NSLog在main函数中使用,当我想让其它项目使用这个main.m时,也能够使用NSLog,这就需要我们对NSLog进行重新导出(举的事NSLog,其实在Foundation中已经对NSLog做了重新导出,否则外界是无法使用的)
当我们重新导出NSLog,需要对间接符号的符号起别名
它会自动的将这个
NSLog变成导出符号Lj_NSLog,编译
我们发现存在了
Lj_NSLog,但是这种形式不够友好,所以我们需要换种打印方式,写入命令
我们看到这个
Lj_NSLog变成了NSLog的别名,我们再看下导出符号表符号
可以看到
Lj_NSLog是被导出了,而且是重新导出的一个符号
作用:在我们的动态库中链接另一个动态库的时候,其中一个动态库对你链接的程序是不可见的,我们就可以用这种重新导出方式让这个动态库可见,可以让一个符号可见,也可以让一个动态库可见
总结
通过上面的符号可以知道一下几点:
- 1.间接符号表中的符号不能删除,意味着
动态库中的全局符号不能删除,也就说明在strip动态库时,不能strip全局符号 - 2.
静态库是.o文件合计以及重定位符号表,由于重定位符号不能删除,所以只能strip.0文件中的调试符号
【问题】App加入动态库体积和加入静态库体积谁的更大(只考虑符号)
答案:动态库的体积更大
原因:
- 【静态库】
App在链接静态库时,会将.o文件以及重定位符号表放到App的符号表中,也就意味着它变成了可能是本地、全局、导出符号,根据我们上面说的脱离符号表规则,除了间接符号表中的符号,其它都可以脱 - 【动态库】
App在链接动态库时,符号都放到间接符号表中,导致在脱离符号表时无法脱离间接符号表
拓展Strip Style(符号脱离)
- 1.
Debugging Symbols(.o 静态库 / 可执行文件 动态库) - 2.
All Symbols - 3.
Non-Global Symbols
Strip Style过程
静态库
动态库
All Symbols
Non-Global Symbols
写到最后
文章写的有些东西没有细说,后面会介绍的!希望大家能够多多交流,共同进步,最后贴出来上面说的指令:

京公网安备 11010502036488号