iOS编译过程

编译器

把一种编程语言(原始语言)转换为另一种编程语言(目标语言)的程序叫做编译器

大多数编译器由两部分组成:前端和后端。

  • 前端负责词法分析,语法分析,生成中间代码;
  • 后端以中间代码作为输入,进行行架构无关的代码优化,接着针对不同架构生成不同的机器码。

Objective C/C/C++使用的编译器前端是clang,swift是swift,后端都是LLVM。

llvm介绍

LLVM是什么,是low level virtual machine的简称,其实是一个编译器框架。LLVM 的优点主要得益于它的三层式架构 – 第一层支持多种语言作为输入(例如 C, ObjectiveC, C++ 和 Haskell),第二层是一个共享式的优化器(对 LLVM IR 做优化处理),第三层是许多不同的目标平台(例如 Intel, ARM 和 PowerPC)。编译器前端主要进行语法分析,语义分析,生成中间代码。编译器后端会进行机器无关的代码优化,生成机器语言,并且进行机器相关的代码优化,根据不同的系统架构生成不同的机器码。

现在,Xcode 的默认编译器是 clang。本文中我们提到的编译器都表示 clang。clang 的功能是首先对 Objective-C 代码做分析检查,然后将其转换为低级的类汇编代码:LLVM Intermediate Representation(LLVM 中间表达码)。接着 LLVM 会执行相关指令将 LLVM IR 编译成目标平台上的本地字节码,这个过程的完成方式可以是即时编译 (Just-in-time),或在编译的时候完成。而Clang 是一个C、C++、Objective-C和Objective-C++编程语言的编译器前端。它采用了底层虚拟机(LLVM)作为其后端。它的目标是提供一个GNU编译器套装(GCC)的替代品。Clang项目包括Clang前端和Clang静态分析器等。

下图是iOS编译过程:

使用llvm命令

一、使用xcode自带

使用xcrun来调用:

1
2
3
4
5
xcrun clang -v
#Apple LLVM version 10.0.0 (clang-1000.11.45.5)
#Target: x86_64-apple-darwin17.7.0
#Thread model: posix
#InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin

二、使用HomeBrew安装

1
2
3
4
5
# 1.使用以下命令安装llvm
brew install --with-toolchain llvm
# 若安装遇到问题 可以先升级brew试试
# brew update 这会更新 Homebrew 自己
# brew upgrade 升级所有可以升级的软件们

安装后可以使用 brew info llvm 检查信息。brew安装llvm后,不会直接添加命令的快捷引用,需要自己手动来完成。

1
2
3
4
5
# 查看llvm的安装路径
brew --prefix llvm
# 添加引用示例:
echo 'export PATH="/usr/local/opt/llvm/bin:$PATH"' >> ~/.bash_profile
source ~/.bash_profile

这样之后就可以直接调用自己安装的llvm了:

1
2
3
4
5
clang -v
#clang version 8.0.0 (tags/RELEASE_800/final)
#Target: x86_64-apple-darwin17.7.0
#Thread model: posix
#InstalledDir: /usr/local/opt/llvm/bin

编译器处理过程

在编译一个源文件时,编译器的处理过程分为几个阶段。要想查看编译 hello.m 源文件需要几个不同的阶段,上文中安装llvm后,我们可以让通过 clang 命令观察:

1
2
3
4
5
6
7
8
sunTongShengdeMacBook-Pro:asdasd suntongsheng$ clang -ccc-print-phases ViewController.m
0: input, "ViewController.m", objective-c
1: preprocessor, {0}, objective-c-cpp-output
2: compiler, {1}, ir
3: backend, {2}, assembler
4: assembler, {3}, object
5: linker, {4}, image
6: bind-arch, "x86_64", {5}, image

可以看到clang将其分为 input、preprocessor 、compiler、backend、assembler、linker、bind-arch几个阶段。

  • 预处理阶段:符号化、宏定义展开头文件展开
  • 语法和语义分析阶段:将符号化后的内容转化为一棵解析树、解析树做语义分析 输出一棵抽象语法树
  • 生成代码和优化阶段:将 AST 转换为更低级的中间码 (LLVM IR)、对生成的中间码做优化、生成特定目标代码、输出汇编代码
  • 汇编器阶段:将汇编代码转换为目标对象文件。
  • 链接器:将多个目标对象文件合并为一个可执行文件 (或者一个动态库)

preprocessor 预处理

每当编源译文件的时候,编译器首先做的是一些预处理工作。具体表现为import头文件替换、macro宏展开、其他预编译指令,`#这个符号是编译器预处理的标志。

一. 对头文件的处理:

例如,如果在源文件中出现下述代码:

1
#import <Foundation/Foundation.h>

预处理器对这行代码的处理是用 Foundation.h 文件中的内容去替换这行代码,如果 Foundation.h 中也使用了类似的宏引入,则会按照同样的处理方式用各个宏对应的真正代码进行逐级替代。这也就是为什么人们主张头文件最好尽量少的去引入其他的类或库,因为引入的东西越多,编译器需要做的处理就越多

示例:假设我们写了一个简单的 C 程序 hello.c:

1
2
3
4
5
6
#include <stdio.h>

int main() {
printf("hello llvm\n");
return 0;
}

然后给上面的代码执行以下预处理命令

1
2
#  -E  Only run the preprocessor
clang -E hello.c >> hello.o

打开 hello.o ,发现有542行。但是如果在上述代码上加上 #import <Foundation/Foundation.h> ,hello.o 的行数暴增到9万多行。(当然对于这种情况引入了模块 - modules功能)

打开模块功能再试试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#-fmodules 允许modules的语言特性
clang -fmodules -E hello.c >> hello.1o

#hello.1o的内容:

# 1 "hello.c"
# 1 "<built-in>" 1
# 1 "<built-in>" 3
# 361 "<built-in>" 3
# 1 "<command line>" 1
# 1 "<built-in>" 2
# 1 "hello.c" 2
#pragma clang module import Darwin.C.stdio /* clang -E: implicit import for #include <stdio.h> */

int main() {
printf("hello llvm\n");
return 0;
}

二、对于宏的处理

假设一段这样的代码:

1
2
3
4
5
6
7
8
9
// hello.c
#define MAX(a,b) a > b ? a : b

int main() {
int i = 200;
printf("largest: %d\n", MAX(i++,100));
printf("i: %d\n", i);
return 0;
}

clang -E hello.c 进行宏展开的预处理结果是如下所示:

1
2
3
4
5
6
int main() {
int i = 200;
printf("largest: %d\n", i++ > 100 ? i++ : 100);
printf("i: %d\n", i);
return 0;
}

i++被替换到了a中,而不是预想的 i++的值。

compiler 词法分析解析标记

一、词法分析-Lexical Analysis

在compiler阶段,首先代码文本都会从 string 转化成特殊的标记流。将代码切成一个个 token,比如大小括号,等于号还有字符串等。是计算机科学中将字符序列转换为标记序列的过程。 例如,下面这段程序:

1
2
3
4
5
// hello.c
int main() {
NSLog(@"hello, %@", @"world");
return 0;
}

利用 clang 命令 clang -Xclang -dump-tokens hello.m 来将上面代码的标记流导出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#clang -Xclang -dump-tokens hello.c

#控制台输出:
int 'int' [StartOfLine] Loc=<hello.c:1:1>
identifier 'main' [LeadingSpace] Loc=<hello.c:1:5>
l_paren '(' Loc=<hello.c:1:9>
r_paren ')' Loc=<hello.c:1:10>
l_brace '{' [LeadingSpace] Loc=<hello.c:1:12>
identifier 'NSLog' [StartOfLine] [LeadingSpace] Loc=<hello.c:2:3>
l_paren '(' Loc=<hello.c:2:8>
unknown '@' Loc=<hello.c:2:9>
string_literal '"hello, %@"' Loc=<hello.c:2:10>
comma ',' Loc=<hello.c:2:21>
unknown '@' [LeadingSpace] Loc=<hello.c:2:23>
string_literal '"world"' Loc=<hello.c:2:24>
r_paren ')' Loc=<hello.c:2:31>
semi ';' Loc=<hello.c:2:32>
return 'return' [StartOfLine] [LeadingSpace] Loc=<hello.c:3:3>
numeric_constant '0' [LeadingSpace] Loc=<hello.c:3:10>
semi ';' Loc=<hello.c:3:11>
r_brace '}' [StartOfLine] Loc=<hello.c:4:1>
eof '' Loc=<hello.c:4:2>

每一个标记都包含了对应的源码内容和其在源码中的位置,如果编译过程中遇到什么问题,clang 能够在源码中指出出错的具体位置。

二、语法分析 - Semantic Analysis

之后上面的标记流会解析生成抽象语法树,我们可以使用 clang -Xclang -ast-dump -fsyntax-only hello.c 来展现解析这个过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# clang -Xclang -ast-dump -fsyntax-only hello.c 

#控制台输出:
hello.c:2:3: warning: implicit declaration of function 'NSLog' is invalid in C99 [-Wimplicit-function-declaration]
NSLog(@"hello, %@", @"world");
^
hello.c:2:9: error: expected expression
NSLog(@"hello, %@", @"world");
^
hello.c:2:23: error: expected expression
NSLog(@"hello, %@", @"world");
^
TranslationUnitDecl 0x7fe1d2816c08 <<invalid sloc>> <invalid sloc>
|-TypedefDecl 0x7fe1d28174a0 <<invalid sloc>> <invalid sloc> implicit __int128_t '__int128'
| `-BuiltinType 0x7fe1d28171a0 '__int128'
|-TypedefDecl 0x7fe1d2817508 <<invalid sloc>> <invalid sloc> implicit __uint128_t 'unsigned __int128'
| `-BuiltinType 0x7fe1d28171c0 'unsigned __int128'
|-TypedefDecl 0x7fe1d28177b8 <<invalid sloc>> <invalid sloc> implicit __NSConstantString 'struct __NSConstantString_tag'
| `-RecordType 0x7fe1d28175d0 'struct __NSConstantString_tag'
| `-Record 0x7fe1d2817558 '__NSConstantString_tag'
|-TypedefDecl 0x7fe1d2817850 <<invalid sloc>> <invalid sloc> implicit __builtin_ms_va_list 'char *'
| `-PointerType 0x7fe1d2817810 'char *'
| `-BuiltinType 0x7fe1d2816ca0 'char'
|-TypedefDecl 0x7fe1d2817af8 <<invalid sloc>> <invalid sloc> implicit __builtin_va_list 'struct __va_list_tag [1]'
| `-ConstantArrayType 0x7fe1d2817aa0 'struct __va_list_tag [1]' 1
| `-RecordType 0x7fe1d2817920 'struct __va_list_tag'
| `-Record 0x7fe1d28178a0 '__va_list_tag'
`-FunctionDecl 0x7fe1d285d000 <hello.c:1:1, line:4:1> line:1:5 main 'int ()'
`-CompoundStmt 0x7fe1d285d1f8 <col:12, line:4:1>
`-ReturnStmt 0x7fe1d285d1e8 <line:3:3, col:10>
`-IntegerLiteral 0x7fe1d285d1c8 <col:10> 'int' 0

在抽象语法树中的每个节点都标注了其对应源码中的位置,同样的,如果产生了什么问题,clang 可以定位到问题所在处的源码位置。

三、静态分析 - Static Analyzer

一旦编译器把源码生成了抽象语法树,编译器可以对这棵树做分析处理,以找出代码中的错误,比如类型检查:即检查程序中是否有类型错误。例如:如果代码中给某个对象发送了一个消息,编译器会检查这个对象是否实现了这个消息(函数、方法)。此外,clang 对整个程序还做了其它更高级的一些分析,以确保程序没有错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
#命令行执行 通过clang -cc1 -analyzer-checker-help可以列出能调用的 checker,但这些checker并不是所有都是默认开启的
clang -cc1 -analyzer-checker-help
OVERVIEW: Clang Static Analyzer Checkers List

USAGE: -analyzer-checker <CHECKER or PACKAGE,...>

CHECKERS:
alpha.clone.CloneChecker Reports similar pieces of code.
alpha.core.BoolAssignment Warn about assigning non-{0,1} values to Boolean variables
alpha.core.CallAndMessageUnInitRefArg
Check for logical errors for function calls and Objective-C message expressions (e.g., uninitialized arguments, null function pointers, and pointer to undefined variables)
alpha.core.CastSize Check when castin
...

使用scan-build可以从命令行运行分析器,类似如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
scan-build xcodebuild -project asdasd.xcodeproj
scan-build: Using '/usr/local/Cellar/llvm/8.0.0/bin/clang-8' for static analysis
Build settings from command line:
CLANG_ANALYZER_EXEC = /usr/local/Cellar/llvm/8.0.0/bin/clang-8
CLANG_ANALYZER_OTHER_FLAGS =
CLANG_ANALYZER_OUTPUT = plist-html
CLANG_ANALYZER_OUTPUT_DIR = /var/folders/s6/5jq8fd4x12v9blzt68qbv18m0000gn/T/scan-build-2019-04-18-141047-45509-1
RUN_CLANG_STATIC_ANALYZER = YES

note: Using new build system
note: Planning build
note: Constructing build description
Build system information
error: Signing for "asdasd" requires a development team. Select a development team in the project editor. (in target 'asdasd')

** BUILD FAILED **

scan-build: Removing directory '/var/folders/s6/5jq8fd4x12v9blzt68qbv18m0000gn/T/scan-build-2019-04-18-141047-45509-1' because it contains no reports.
scan-build: No bugs found.

关于静态分析更多可以查看 :Clang 静态分析器

生成代码和优化阶段

一、生成 LLVM 代码

clang 完成代码的标记,解析和分析后,接着就会生成 LLVM 代码。下面继续看看hello.c:

1
2
3
4
5
6
#include <stdio.h>

int main() {
printf("hello world\n");
return 0;
}

要把这段代码编译成 LLVM 字节码(绝大多数情况下是二进制码格式),我们可以执行下面的命令:

clang -emit-llvm hello.c -c -o hello.bc

接着用另一个命令来查看刚刚生成的二进制文件:

llvm-dis < hello.bc | less

部分输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
; ModuleID = '<stdin>'
source_filename = "hello.c"
target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.13.0"

@.str = private unnamed_addr constant [13 x i8] c"hello world\0A\00", align 1

; Function Attrs: noinline nounwind optnone ssp uwtable
define i32 @main() #0 {
%1 = alloca i32, align 4
store i32 0, i32* %1, align 4
%2 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([13 x i8], [13 x i8]* @.str, i32 0, i32 0))
ret i32 0
}

declare i32 @printf(i8*, ...) #1

...

二、优化 LLVM 代码

LLVM 会去做些优化工作,在 Xcode 的编译设置里也可以设置优化级别-01,-03,-0s,还可以写些自己的 Pass,Pass就是LLVM系统转化和优化的工作的一个节点,每个节点做一些工作,这些工作加起来就构成了LLVM整个系统的优化和转化。官方有比较完整的 Pass 教程: Writing an LLVM Pass — LLVM 5 documentation 。如果开启了 bitcode 苹果会做进一步的优化

三、输出汇编代码

我们可以使用下面的命令让clang输出汇编代码:

1
2
#输入
clang -S -o - hello.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#输出
.section __TEXT,__text,regular,pure_instructions # .section 指令指定接下来会执行哪一个段
.macosx_version_min 10, 13
.globl _main # .globl 指令说明 _main 是一个外部符号
.p2align 4, 0x90 # .align 指令指出了后面代码的对齐方式 如果需要的话,用 0x90 补齐
_main: ## 接下来是 main 函数的头部:
.cfi_startproc # .cfi_startproc 指令通常用于函数的开始处
## %bb.0:
pushq %rbp # rbp 寄存器 (基础指针寄存器 base pointer register)
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
subq $16, %rsp
movl $0, -4(%rbp)
leaq L_.str(%rip), %rdi
movb $0, %al
callq _printf #调用了 printf
xorl %ecx, %ecx
movl %eax, -8(%rbp) ## 4-byte Spill
movl %ecx, %eax
addq $16, %rsp
popq %rbp # 与pushq对应
retq
.cfi_endproc # 与 .cfi_startproc 相匹配,以此标记出 main() 函数结束
## -- End function
.section __TEXT,__cstring,cstring_literals # __TEXT __cstring 开启了一个新的段。
L_.str: #L_.str 标记运行在实际的代码中获取到字符串的一个指针 ## @.str
.asciz "hello world\n" #.asciz 指令告诉编译器输出一个以 ‘\0’ (null) 结尾的字符串。


.subsections_via_symbolsv # .subsections_via_symbols 指令是静态链接编辑器使用的。

具体的代码解读,有兴趣的同学可以到苹果的 OS X Assembler Reference 了解详细。

汇编器

汇编器将可读的汇编代码转换为机器代码。它会创建一个目标对象文件,一般简称为 对象文件。这些文件以 .o 结尾。如果用 Xcode 构建应用程序,可以在工程的 derived data 目录中,Objects-normal 文件夹下找到这些文件。

1
2
#例如 生成 main.o文件
clang -fmodules -c main.c -o main.o

链接器

链接器解决了目标文件和库之间的链接。例如上面汇编代码中 callq _printfprintf() 是 libc 库中的一个函数。无论怎样,最后的可执行文件需要能需要知道 printf() 在内存中的具体位置:例如,_printf 的地址符号是什么。链接器会读取所有的目标文件 (此处只有一个) 和库 (此处是 libc),并解决所有未知符号 (此处是 _printf) 的问题。然后将它们编码进最后的可执行文件中 (可以在 libc 中找到符号 _printf),接着链接器会输出可以运行的执行文件。

1
2
#生成 hello.out 的 Mach-O 二进制文件
clang hello.c -o hello.out

关于Mach-O文件格式有兴趣的同学可以通过apple文档来了解。

这里说 hello.out 是可执行文件,可以通过file来判断:

1
2
3
4
5
6
7
8
file hello.out
#输出
hello.out: Mach-O 64-bit executable x86_64

#执行hello.out
./hello.out
#输出
hello world

编译多个文件

之前上面我们的实验都是使用单个hello.c文件。现在我们通过多个文件情况 了解下汇编器和链接器 ,如下三个文件:

1
2
3
4
5
6
7
8
9
//File1.h:

#import <Foundation/Foundation.h>

@interface Foo : NSObject

- (void)run;

@end
1
2
3
4
5
6
7
8
9
10
11
12
//File1.m:

#import "File1.h"

@implementation Foo

- (void)run
{
NSLog(@"%@", NSFullUserName());
}

@end
1
2
3
4
5
6
7
8
9
10
11
12
//File2.m:

#import "File1.h"

int main(int argc, char *argv[])
{
@autoreleasepool {
Foo *foo = [[Foo alloc] init];
[foo run];
return 0;
}
}

上面有三个文件,需要我们编译多个文件。我们需要让clang对输入每个文件生成对应的目标文件:

1
2
3
4
# clang  -c Only run preprocess, compile, and assemble steps

clang -c File1.m #生成 File1.o
clang -c File2.m #生成 File2.o

这里我们加了-c 并没有编译头文件,现在我们来完成链接器这一步,将两个 .o 文件与Foundation库链接起来:

1
2
3
#  -Wl,<arg>               Pass the comma separated arguments in <arg> to the linker
clang File1.o File2.o -Wl,`xcrun --show-sdk-path`/System/Library/Frameworks/Foundation.framework/Foundation
# 输出 a.out

现在我们可以运行 a.out 了。

1
2
./a.out 
2019-04-16 20:49:35.070 a.out[71207:10299638] sunTongSheng

符号表和链接

File1 和 File2 都使用了 Foundation framework。 File2.o 目标文件使用了它的 autorelease pool,并间接的使用了 libobjc.dylib 中的 Objective-C 运行时。它需要运行时函数来进行消息的调用。所有的这些关联的东西都被形象的称之为符号。

每个函数、全局变量和类等都是通过符号的形式来定义和使用的。当我们将目标文件链接为一个可执行文件时,链接器在目标文件和动态库之间对符号做了解析处理。

可执行文件和目标文件有一个符号表,这个符号表规定了它们的符号。如果我们用 nm 工具观察一下 File2.o 目标文件,可以看到如下内容:

1
2
3
4
5
6
7
8
#nm : llvm symbol table dumper
nm -nm File2.o
(undefined) external _OBJC_CLASS_$_Foo
(undefined) external _objc_alloc
(undefined) external _objc_autoreleasePoolPop
(undefined) external _objc_autoreleasePoolPush
(undefined) external _objc_msgSend
0000000000000000 (__TEXT,__text) external _main

上面就是那个目标文件的所有符号。_OBJC_CLASS_$_Foo 是 Foo Objective-C 类的符号。该符号是 undefined, external 。External 的意思是指对于这个目标文件该类并不是私有的,相反,non-external 的符号则表示对于目标文件是私有的。我们的 File2.o 目标文件引用了类 Foo,不过这并没有实现它。因此符号表中将其标示为 undefined。接着是 4 个 Objective-C 运行时函数。它们同样是 undefined的,需要链接器进行符号解析。

接下来是 _main 符号,它是表示 main() 函数,同样为 external,这是因为该函数需要被调用,所以应该为可见的。由于在 helloworld.o 文件中实现了 这个 main 函数。这个函数地址位于 0处,并且需要转入到 TEXT,text section。

接下来看下 File1.o,看看有什么输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
nm -nm File1.o
(undefined) external _NSFullUserName
(undefined) external _NSLog
(undefined) external _OBJC_CLASS_$_NSObject
(undefined) external _OBJC_METACLASS_$_NSObject
(undefined) external ___CFConstantStringClassReference
(undefined) external __objc_empty_cache
0000000000000000 (__TEXT,__text) non-external -[Foo run]
0000000000000060 (__DATA,__objc_const) non-external l_OBJC_METACLASS_RO_$_Foo
00000000000000a8 (__DATA,__objc_const) non-external l_OBJC_$_INSTANCE_METHODS_Foo
00000000000000c8 (__DATA,__objc_const) non-external l_OBJC_CLASS_RO_$_Foo
0000000000000110 (__DATA,__objc_data) external _OBJC_METACLASS_$_Foo
0000000000000138 (__DATA,__objc_data) external _OBJC_CLASS_$_Foo

File1.o 同样有 undefined 的符号。首先是使用了符号 NSFullUserName()NSLog()NSObject。接着显示了 _OBJC_CLASS_$_Foo 已经定义了,并且对于 File1.o 是一个外部符号 , File1.o 包含了这个类的实现。

最后先来同样看下 a.out 的输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
nm -nm a.out 
(undefined) external _NSFullUserName (from Foundation)
(undefined) external _NSLog (from Foundation)
(undefined) external _OBJC_CLASS_$_NSObject (from CoreFoundation)
(undefined) external _OBJC_METACLASS_$_NSObject (from CoreFoundation)
(undefined) external ___CFConstantStringClassReference (from CoreFoundation)
(undefined) external __objc_empty_cache (from libobjc)
(undefined) external _objc_alloc (from libobjc)
(undefined) external _objc_autoreleasePoolPop (from libobjc)
(undefined) external _objc_autoreleasePoolPush (from libobjc)
(undefined) external _objc_msgSend (from libobjc)
(undefined) external dyld_stub_binder (from libSystem)
0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header
0000000100000e90 (__TEXT,__text) non-external -[Foo run]
0000000100000ec0 (__TEXT,__text) external _main
0000000100001138 (__DATA,__objc_data) external _OBJC_METACLASS_$_Foo
0000000100001160 (__DATA,__objc_data) external _OBJC_CLASS_$_Foo

通过a.out的符号表,我们可以观察链接器是如何解析所有符号表的。当我们将这两个目标文件和 Foundation framework (是一个动态库) 进行链接处理时,链接器会尝试解析所有的 undefined 符号。它可以解析 _OBJC_CLASS_$_Foo。另外,它将使用 Foundation framework。当链接器通过动态库 (此处是 Foundation framework) 解析成功一个符号时,它会在最终的链接图中记录这个符号是通过动态库进行解析的。链接器会记录输出文件是依赖于哪个动态链接库,并连同其路径一起进行记录。在我们的例子中,_NSFullUserName,_NSLog,_OBJC_CLASS_$_NSObject,_objc_autoreleasePoolPop 等符号都是遵循这个过程。

虽然所有的 Foundation 和 Objective-C 运行时符号依旧是 undefined,不过现在的符号表中已经多了如何解析它们的信息,例如在哪个动态库中可以找到对应的符号。

可执行文件同样知道去哪里找到所需库:

1
2
3
4
5
6
otool -L a.out 
a.out:
/System/Library/Frameworks/Foundation.framework/Versions/C/Foundation (compatibility version 300.0.0, current version 1560.12.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1252.50.4)
/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 150.0.0, current version 1454.90.0)
/usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)

动态链接器

在运行时,动态链接器dyld可以解析这些 undefined 符号,dyld将会确定好 _NSFullUserName 等符号,并指向它们在 Foundation 中的实现等。
上面提到文件符号表指向了需要的库,添加DYLD_PRINT_LIBRARIES环境变量可以打印出什么库被加载了:

1
2
3
4
5
6
(export DYLD_PRINT_LIBRARIES=; ./a.out )
dyld: loaded: /Users/suntongsheng/Desktop/tmp/asdasd/Test/./a.out
dyld: loaded: /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation
dyld: loaded: /usr/lib/libSystem.B.dylib
dyld: loaded: /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation
...

比如可以针对 Foundation 运行 nm,并检查这些符号的定义情况:

1
2
nm -nm /System/Library/Frameworks/Foundation.framework/Foundation | grep NSFullUserName
00000000000bb1be (__TEXT,__text) external _NSFullUserName

上面将会显示出在加载 Foundation 时,同时会加载的 70 个动态库。这是由于 Foundation 依赖于另外一些动态库。运行下面的命令:

1
xcrun otool -L /System/Library/Frameworks/Foundation.framework/Foundation

可以看到 Foundation 关联的库。

动态链接器dyld 的共享缓存

当你构建一个真正的程序时,将会链接各种各样的库。它们又会依赖其他一些 framework 和 动态库。需要加载的动态库会非常多。而对于相互依赖的符号就更多了。可能将会有上千个符号需要解析处理,这将花费很长的时间:一般是好几秒钟。

为了缩短这个处理过程所花费时间,在 OS X 和 iOS 上的动态链接器使用了共享缓存,共享缓存存于 /var/db/dyld/。对于每一种架构,操作系统都有一个单独的文件,文件中包含了绝大多数的动态库,这些库都已经链接为一个文件,并且已经处理好了它们之间的符号关系。当加载一个 Mach-O 文件 (一个可执行文件或者一个库) 时,动态链接器首先会检查 共享缓存 看看是否存在其中,如果存在,那么就直接从共享缓存中拿出来使用。每一个进程都把这个共享缓存映射到了自己的地址空间中。这个方法大大优化了 OS X 和 iOS 上程序的启动时间。

最后总结一下就是以下编译过程:

Xcode 编译

以上说的是Clang如何编译C语言文件的过程,那么在Xcode里会经过哪些过程呢?

我们可以简单新建一个单页面工程,Build后在Report Navigation视图中查看详细日志:

详细的步骤如下:

  1. 创建文件夹
  2. 把Entitlements.plist写入到DerivedData里,处理打包的时候需要的信息(比如application-identifier)。
  3. 创建一些辅助文件,比如各种.hmap (headermap是帮助编译器找到头文件的辅助文件:存储这头文件到其物理路径的映射关系)
  4. 编译.m文件,生成.o文件。
  5. 链接动态库,o文件,生成一个mach o格式的可执行文件。
  6. 编译assets,编译storyboard,链接storyboard
  7. 对App签名
  8. 生成 .app

在 build 处理过程中,每个任务都会出现类似上面的这些 log 信息,我们就通过上面的 log 信息进一步了解详情。