(WWDC) Xcode 构建过程的幕后 —— Linker

 

接下来,我们将揭晓以下问题的答案:

  • 链接器(linker)实际上做了什么?
  • 什么是符号(symbols)?
  • 什么是目标文件(object files)?
  • 什么是库(libraries)?

 
 

链接器 (linker)

  • 执行构建可执行 Mach-O 文件的最后任务;

  • 把所有编译器调用的输出整合到单个文件中:

    • 移动和修补编译器生成的代码
  • 输入两种类型的文件:

    • 目标文件 (.o)
    • 库文件 (.dylib, .tbd, .a)

 
 

符号 (symbols)

  • 一个符号是一段代码或者数据的名字;

  • 代码段可能会引用其他的符号;

  • 符号可以具有改变链接器行为的属性:

    • 比如:弱符号(weak symbol)
  • 编程语言通常会编码一些数据到符号,使其成为有"乱码(mangling)"的符号;

 
 

目标文件 (object files)

  • 单个编译器调用的产出物;

  • 一个包含代码和数据片段的不可执行的 Mach-O 文件:

    • 每个代码片段被一个符号代表
    • 代码片段可以引用未定义(undefined)的符号

 
 

库(libraries)

库定义了你的 target 中不包含的符号:

  • Dylibs(Dynamic libraries): 动态库(暴露了可执行程序可以使用的代码和数据的 Mach-O 文件)

  • TBDs(Text Based Dylib Stubs): 基于文本的 Dylib 桩(只包含符号)

  • 静态归档文件(Static archives)

    • 用 "ar" 工具构建的由多个 .o 文件归档而成的文件
    • 只有包含了你已引用的符号的 .o 文件会被包含到你的应用中

 
 

下面,让我们通过一个简单的例子来理解这些内容。

 
 

首先,你已经将 playSound 函数定义在了项目中的某一处,所以这里的定义是没有错误的。

让我们来看一下对应的汇编代码:

 

由于静态常量 purrFile 对于外部不可见,所以忽略这个常量。
最后,purr.aac 这个字符串直接被拷贝到 Cat.o 中:

使用 -[Cat purr] 作为符号表示 Cat 类中的实例方法 purr

有两条指令引用了这个文件名字符串。
arm64 架构下,我们知道完成这个操作只需要两条指令。
这里并没有这个字符串的实际内存地址,因为我们不知道这个字符串最终在可执行程序内存中的位置:

使用 @PAGE@PAGEOFF 来占位,让链接器来更新这个内存地址。

然后,调用 playSound 函数。不过,Cat.o中的汇编代码做了一些编码处理,编码部分包含了函数的参数信息,所以这个函数在汇编代码中不再命名为 playSound

至此,我们已经生成了一个 Cat.o 文件。
事实上,在构建项目时,会有很多 .o 文件生成。

 

那我们如何处理这些 .o 文件呢?
构建系统会把所有的 .o 文件作为链接器的输入,然后链接器会创建一个文件来整合这些输入。

 

现在,让我们来构建 PetWall 应用中的内嵌代码库 PetKit

首先创建一个用于容纳所有应用代码的文本段(text segment)。

然后,把 Cat.o 中的内容复制进去。不过,我们需要把内容分为两部分。
一部分是常量字符串,另一部分是可执行代码。

到这里j,我们已经知道这些数据的内存地址,所以链接器可以通过重写 Cat.o 来从特定的内存地址偏移位置读取内容。
你会发现汇编指令变了。第二条指令变为了 nop,意思是空操作。
因为移除或者创建指令会导致重新计算大小,所以这里使用了空操作来进行替换。

接下来,我们的处理会遇到一个未定义的符号。
而到目前为止,所有的 .o 文件已经被包含进来,所以我们可以去查找这个未定义的符号。
我们首先查找静态归档库(static archives),在 PetSupport.a 中的 PetSounds.o 文件中找到了 playSound 函数。

然后我们把 PetSounds.o 中的内容复制进来。
不过,我们不需要 PetCare.o 中的内容,因为其中不存在项目需要引用的符号。
注意 open$stub 是未定义的。所以在后续的步骤中需要对它进行替换。

继续查找,我们在 libSystem.tbd 系统库中找到了 open 函数的一个副本。

然后,我们需要做一些处理才能使调用成功。
所以,我们添加了一个假的方法,里面包含了模板代码。
我们可以用系统库中的 open 函数替换这个模板方法。

这个方法可以加载一个指针(像C语言里面的指针一样),然后跳到这个指针。
所以,这里我们只需要一个 open 函数的指针即可。

接下来,我们需要在数据段(data segment)中创建这个函数指针,数据段是存储全局变量的地方。
不过,这里被设置为了0。如果我们直接对这个地址取值并跳到这里,程序就会崩溃。

所以,我们需要添加链接编辑段(link edit segment)。
链接编辑是链接工具用来告知操作系统和动态链接器在运行时修复问题的元数据。

想了解动态链接的相关内容,请看这里《Optimizing App Startup Time》

 
 

总结

  • 构建系统:分析依赖,然后利用多核硬件完成构建过程
  • Clang:查找头文件、通过模块来优化编译过程
  • Swift 编译器:查找声明和生成接口
  • Linker:整合所有编译器的输出到可执行程序文件中,在这个过程中 Xcode 会对代码签名打包

 

开源资源:

 
 



参考内容:
Behind the Scenes of the Xcode Build Process

 
 
转载请注明出处,谢谢~

推荐阅读更多精彩内容