Xcode构建过程的后台工作(一)构建过程
Xcode构建过程的后台工作(二)clang构建
Xcode构建过程的后台工作(三)swift构建
构建过程:链接
这是 Xcode构建过程的最后一步。
首先浏览一下我们要讨论的内容。我们将讨论链接器是 什么,它所采用的输入,即dylib和目标文件及其定义。还会讲到符号及其内容。最后会举例总结,因为内容比较难懂。
链接器是什么
链接器是构建中的最后一个过程。我们所做的是将两个编译器构建的所有.o文件 组合成一个可执行文件。它所做的就是移动和修补代码。 它无法创建代码,这 很重要,我将在示例中显示。我们有两种输入文件。第一个是目标文件(.o),在构建过程中产生。第二个是库,包括dylib,tbd和.a文件或静态文档。
符号(symbols)
符号是一个代表代码或数据片段的名称。当一个函数调用另一个函数,这些片段可能会指向其他符号。符号可以有很多影响链接器行为方式的属性。我只举一个 弱符号的例子。弱符号的注释表示当我们在系统上运行或者执行文件时它可能不存在。还有可用性标记,标记这个API用于iOS12,那个API用于iOS11.这就引出了现在的主题链接器。链接器可以确定哪些符号肯定会出现和哪些符号可以在运行时处理。如前所述,语言可以通过命名修饰将数据编码为符号。在C++和Swift中都能看到。所以符号就是指代码和数据的名称。
目标文件
目标文件就是代码和数据的集合。它们不可执行。因为是编译代码,所以还没有完成。还有缺失就需要链接器整合和修复。每个文件的片段都用符号表示。例如对于printf函数,就以符号代替代码。对于PetKit的代码后文会展示。
片段可能引用未定义的符号。 因此,如果您的.o文件引用另一个.o文件中的函数,那么.o文件是未定义的。链接器将找到那些未定义的符号并进行匹配。所以目标文件是编译器操作的输出。那么什么是库?库是定义符号的文件,但不属于构建的目标。我们有动态库,那些Mach-O文件,显示了可执行文件的代码和数据片段。这些是系统的一部分。这就是我们的框架。你也可能会用自己的框架。
还有TBD文件,基于文本的dylib文件。在为iOS和macOS制作SDK时,会有所有这些dylibs和以及您可能想要使用的功能,如MapKit和WebKit。但是我们不想把所有这些跟SDK一起加载,因为它会很大而且编译器和链接器不需要它,它只在运行程序时有用。因此,我们创建了stub dylib,删除所有符号的主体,只留下名称。完成之后,转用文本表示,这对我们来说更容易使用。目前,它们仅用于分发SDK以减小大小。所以你在项目中看到它们时不必担心,它们只是符号。
最后是静态库(static archives)。静态库是使用AR工具构建的.o文件的集合,也可能是lib,它是lib工具的包装器。根据AR操作文档,AR创建并维护的文件组,将它们合并为一个库。听起来很像TAR文件或ZIP文件,这正是它的本质。实际上,.a格式是UNIX在使用更强大的工具之前使用的原始库格式。但是现在的编译器和连接器可以完全理解它们,所以继续使用它们。它就只是个档案 文件。值得注意的是,它们孕育了动态链接,在过去所有代码都会被存档。因此, 不能使用的是一个函数涵盖所有C库 。所以行为是,如果.o文件中有符号,我们会将整个.o文件从库中拉出来。但是不会引入其他.o文件。如果你在它们之间引用符号,只要带入即可。如果你是非符号行为,比如静态初始化程序,或者将它们重新导出为您自己的dylib的一部分,您要明确地用到强制加载,或定制加载让链接器提取所有或者这些文件,即便之间没有关联。我们通过一个例子串联起这些内容。
上面是playSound函数的例子,只看宠物不听声音有什么乐趣?cat上有一个调用playSound的函数。上图右边是生成的程序集。输出文件是cat.o。字符串purr.aac,是AAC声音文件。这会被复制到cat.o. 您会注意到名称purr文件不见了。因为它是静态的。如果你熟悉C语言,这是非导出命名。没有其他人可以引用它。既然如此,我们不需要它,排除掉。
然后我们看到Cat purr变成了符号:-[Cat purr]。跟预想的差不多。
然后我们要把这个变量传递给playSound。这里出现了两个指令,这是因为我们不知道这个字符串最后在可执行文件中的位置,我们没有具体的地址 。 但是我们知道RM64就是这个程序集,它最多可能需要两条指令。所以编译器给我们留下了两条指令。它留下符号偏移量,值为PAGE和PAGEOFF,链接器之后回来修复。 最后,既然已经将字符串加载到x0中,我们可以调用playSound,我们写入__z9playSoundPKc。这是一个变形的符号,如果仔细看会看到cat.mm,这是Objective-C++。playSound实际上是一个C++函数。所以如果你不熟悉,你可以在终端输入命令。
如果运行Swift-demangle并传入符号,然后反修饰。没有用,它不是swift的符号。但是C++ 反修饰器C++ filts告诉我们这实际上是playSound的符号。 除了playSound,它还有一个实参。这个参数是一个const char* 因为C++会将更多信息编入修饰符号中。现在有了.o文件,实际构建中会有更多。那我们该怎么做呢?
首先,构建系统将把所有.o作为输入传递给链接器。链接器会创建一个文件来放入它们 。这里构建的PetKit,是PetWall的内嵌框架。 因此,我们只要复制,创建一个文本片段用来保存app的所有代码的。然后复制cat.o到这里。但是要分成两部分,一个用于字符串,一个用于可执行代码。现在已知这些东西的绝对地址,因此 链接器可以重写cat.o,以从特定偏移量加载。 你会注意到第二条指令就消失了。它被一个null指令代替,没有任何行动。但我们无法删除 指令,因为我们无法创建或删除代码,这会打乱所有已完成的工作。所以与其删除,不如替换为无行动。
最后是分支。我们有一个未定义的符号,我们将继续浏览所有已经导入的.o文件。
所以我们将开始查看静态库,上图是PetSupport.a。在PetSupport.a中有几个文件,包括PetSounds.o。大家能看到匹配playSound的符号。 所以我们把它拉进来。PetCare.o不能被引入,因为.o文件没有任何符号能被app的其他部分引用。我们把它拉进来,但现在需要_open,但是我们没有定义。拉入的对话已经变成_open $stub。为什么呢?因为我们发现open的副本在lib系统的TBD文件中。
我们知道这不是系统库的一部分,我不会其复制到我的app中。但是我需要在app中加入足够的信息 以便调用它。
因此,我们创建了一个假函数 ,它只是一个模板,用来代替从lib系统拿走的函数,这里就是open。观察该函数,它实际上是来自指针open$pointer,然后跳到它。这需要一个函数指针,就像任何普通的C函数指针一样。然后在数据段中 创建它,如果有全局变量,那么就会出现在这里。但它只是设置为零,如果如果空引用会导致崩溃。然后我们添加一个LINKEDIT部分。
LINKEDIT是链接器工具用于为操作系统保留信息的元数据,这就是在运行时解决问题的动态链接器。有关这方面的更多信息,请查看2016年的 Optimizing App Startup Time 演讲。