前言

上篇文章多环境配置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:大小
方便理解,我们来打印下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是什么样

我们和上面的相比较发现多了很多东西例如: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行)

导入导出符号

我们知道NSLog是Foundation框架下的,写在19行,相当于是导入NSLog符号,又因为Foundation导出了NSLog符号,让其它地方使用(导出符号又是全局符号

下面我们看下main.m中有哪些导出符号,通过在.xcconfig中写入命令,编译

我们看到有4个导出符号,它正好对应上面打印符号表中的4个全局符号,这也就意味着当我们声明全局符号时,会默认为导出符号,其它地方也可以使用

下面我们创建一个OC类,再查看符号

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,这也就是上面说的:如果静态链接器或动态链接器为此符号找到另一个(非弱)定义,则弱定义将被忽略

如果我们把弱定义的符号声明成一个隐藏符号,此时它应该是一个弱定义的本地符号

重新导出符号

当我们NSLogmain函数使用,当我想让其它项目使用这个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

写到最后

文章写的有些东西没有细说,后面会介绍的!希望大家能够多多交流,共同进步,最后贴出来上面说的指令: