前言
上篇文章多环境配置
、Mach-O与链接器
,但是Symbol
还没又说道,这篇文章我们继续上篇文章内容讲下去
.xconnfig补充
上面文章在介绍多环境配置的时候讲到了.xconnfig
,说到了.xconnfig
可以统一管理环境配置
,这里可以根据不同的条件
配置不同的设置
,我们那Other Linker Flags
来说明 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vprpgo6G-1618818463303)(https://upload-images.jianshu.io/upload_images/25331915-de52ada8f6d6154d.image?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
上图配置意思就是在
Debug环境下,设备为模拟器,切架构为x86时
添加framework "Man"
。
我们看到此时在
arm64
下编译时成功
的,因为Other Linker Flags
并没有导入Man
这次我们看到在
x86_64环境
下,编译时发现报错,告诉我们找不到Man,这是因为这种环境
下我们的Man被放入了项目环境
中,所以才会提示找不到
作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS开发交流群:130 595 548,不管你是小白还是大牛都欢迎入驻 ,让我们一起进步,共同发展!(群内会免费提供一些群主收藏的免费学习书籍资料以及整理好的几百道面试题和答案文档!)
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:大小
方便理解,我们来打印下mach header
__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
是什么样 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uMYdF7ed-1618818463311)(https://upload-images.jianshu.io/upload_images/25331915-57af0d1adbe53049.image?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
我们和上面的相比较发现多了很多东西例如: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.加static
2.使用__attribute__
关键字(第16行)
导入导出符号
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CsEPGiz7-1618818463315)(https://upload-images.jianshu.io/upload_images/25331915-a1a2c5167453f3d2.image?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
我们知道
NSLog是Foundation框架
下的,写在19行,相当于是导入
了NSLog符号
,又因为Foundation
又导出了NSLog符号
,让其它地方使用(导出符号又是全局符号
)
下面我们看下main.m中有哪些导出符号,通过在.xcconfig中写入命令
,编译
我们看到有
4个导出符号
,它正好对应上面打印符号表
中的4个全局符号
,这也就意味着当我们声明全局符号
时,会默认为导出符号
,其它地方也可以使用
下面我们创建一个OC类,再查看符号
OC类都会默认为导出符号
间接符号表
我们知道动态库
是在运行
的过程中加载
,也就意味着它在编译链接阶段
只需要提供符号
就可以了,上篇文章我们在说符号表
时提到:间接符号表
保存这项目使用的其它动态库
的符号
,下面我们通过在.xcconfig
中写入命令,编译来查看间接符号表
这里面我们就只认识最后的
NSLog
,这个是Foundation给我提供的导出符号
总结
- 1.
全局符号可以变成导出符号给外界使用
- 2.
间接符号表不能删除
,意味着动态库
中的全局符号不能删除
,也就说明在strip动态库
时,不能strip全局符号
- 3.
OC类
在编译时
都会默认
为导出符号
,那么我们在用OC写动态库
时,如果想尽可能让动态库包小些
,我们可以在.xcconfig定义参数不导出符号
-
进行编译
-
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7htYNgFY-1618818463318)(https://upload-images.jianshu.io/upload_images/25331915-6cb3f617296ca39b.image?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
和上面的相比发现少了一个
_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(弱定义)写法: [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gUA25rta-1618818463321)(https://upload-images.jianshu.io/upload_images/25331915-f141ac8b1d8b2ef4.image?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
上面讲到弱定义符号:如果静态链接器或动态链接器为此符号找到另一个(非弱)定义,则弱定义将被忽略
,怎么理解这句话呢?我们通过代码来理解
- 1.在.h中我们
弱定义了weak_function方法
- 在.m中我们实现这个
弱定义方法
方法实现
和声明都是全局
的,上面讲了应该转为导出符号
,下面我们变一下,看下打印
当声明为
弱定义方法
,并不影响
作为导出符号导出
当我们在.m声明相同的方法
如果
正常情况
下,由于方法名相同
,运行应该会报错
,但是由于这个方法被弱定义
,此时编译是不会报错
的。
下面我们在main函数中调用这个方法
运行打印结果 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mDGMEASx-1618818463323)(https://upload-images.jianshu.io/upload_images/25331915-952cae5b4f52ce4e.image?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
我们看到执行了
main函数的weak_function方法
,并没有执行WeakSymbol的weak_function
,这也就是上面说的:如果静态链接器或动态链接器为此符号找到另一个(非弱)定义,则弱定义将被忽略
如果我们把弱定义的符号声明成一个隐藏符号,此时它应该是一个弱定义的本地符号
重新导出符号
当我们NSLog
在main函数
中使用
,当我想让其它项目使用
这个main.m
时,也能够使用NSLog
,这就需要我们对NSLog进行重新导出
(举的事NSLog,其实在Foundation
中已经对NSLog
做了重新导出
,否则外界是无法使用的
)
当我们重新导出NSLog
,需要对间接符号的符号起别名
它会自动的将这个
NSLog
变成导出符号Lj_NSLog
,编译
我们发现存在了
Lj_NSLog
,但是这种形式不够友好,所以我们需要换种打印方式,写入命令
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7R5U5BCt-1618818463326)(https://upload-images.jianshu.io/upload_images/25331915-9063b96618c990f5.image?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
我们看到这个
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
写到最后
文章写的有些东西没有细说,后面会介绍的!希望大家能够多多交流,共同进步,最后贴出来上面说的指令: