iOS编译过程

iOS 开发中 Objective-C 是 Clang / LLVM 来编译的。swift 是 Swift / LLVM

LLVM是一个模块化和可重用的编译器和工具链技术的集合,Clang 是 LLVM 的子项目,是 C,C++ 和 Objective-C 编译器,其中的 clang static analyzer 主要是进行语法分析,语义分析和生成中间代码

编译流程

  1. 编译信息写入辅助文件,创建文件架构 .app 文件
  2. 处理文件打包信息
  3. 执行 CocoaPod 编译前脚本,checkPods Manifest.lock
  4. 编译.m文件,使用 CompileC 和 clang 命令
  5. 链接需要的 Framework
  6. 编译 xib
  7. 拷贝 xib ,资源文件
  8. 编译 ImageAssets
  9. 处理 info.plist
  10. 执行 CocoaPod 脚本
  11. 拷贝标准库
  12. 创建 .app 文件和签名

其中 3-10 步骤的数量和顺序并不固定,可以在 Build Phases 中可以修改。

本文以下面代码进行演示,文件名是ViewController.m

#import "ViewController.h"
@interface ViewController ()
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    UIView *view = [[UIView alloc] init];
    [self.view addSubview:view];
}
@end

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

target 文件结构是

image.png

command+B,编译
在 Xcode 编译过后,可以通过Show the report navigator里对应 target 的 build 中查看编译状态
image.png

上面这个图片也描述了编译的过程

Create product structure
Process product packaging
Run custom shell script ‘Check Pods Manifest.lock’
Compile … 各个项目中的.m文件
Link /Users/… 路径
Copy … 静态文件
Compile asset catalogs
Compile Storyboard file …
Process info.plist
Link Storyboards
Run custom shell script ‘Embed Pods Frameworks’
Run custom shell script ‘Copy Pods Resources’
…
Touch Target.app
Sign Target.app

控制Target 的 Build 过程

在 Xcode 的 Project editor 中的 Build Setting,Build Phases 和 Build Rules 能够控制编译的过程。

Build Phases 构建可执行文件的规则

指定 target 的依赖项目,在 target build 之前需要先 build 的依赖。实际上这并不属于真正的 build phase,在这里,Xcode 只不过将其与 build phase 显示到一块罢了。

Compile Source 中指定所有必须编译的文件,这些文件会根据 Build Setting 和 Build Rules 里的设置来处理。

Link Binary With Libraries 里会列出所有的静态库和动态库,会和编译生成的目标文件进行链接。

build phase 还会把静态资源(例如图片和字体)拷贝到 app bundle 中。需要注意的是,如果图片资源是PNG格式,那么不仅仅对其进行拷贝,还会做一些优化(如果 build settings 中的 PNG 优化是打开的话)。

可以通过在 build phases 里添加自定义脚本来做些事情,比如像 CocoaPods 所做的那样。
build 过程中,要想观察所有已存在的环境变量,你可以在 build phase 中添加一个 "Run Script",并勾选上 "Show environment variables in build log"。

Build Rules

指定不同文件类型如何编译。每条 build rule 指定了该类型如何处理以及输出在哪。如果你需要对特定类型的文件添加处理方法,那么可以在此处添加一条新的规则。

一条 build rule 指定了其应用于哪种类型文件,该类型文件是如何被处理的,以及输出的内容该如何处置。比方说,我们创建了一条预处理规则,该规则将 Objective-C 的实现文件当做输入,解析文件中的注释内容,最后再输出一个 .m 文件,文件中包含了生成的代码。由于我们不能将 .m 文件既当做输入又当做输出,所以我使用了 .mal 后缀,定制的 build rule 如下所示:

Build Settings

在 build settings 中,我们可以配置每个任务(之前在 build log 输出中看到的任务)的详细内容。

你会发现 build 过程的每一个阶段,都有许多选项:从编译、链接一直到 code signing 和 packaging。注意,settings 是如何被分割为不同的部分 -- 其实这大部分会与 build phases 有关联,有时候也会指定编译的文件类型。

pbxproj工程文件

build 过程控制的设置都会被保存在工程文件 .pbxproj 里。在这个文件中可以找 rootObject 的 ID 值

rootObject = 02E680D92560F4BE009EEC2E /* Project object */;

然后根据这个 ID 找到 工程的定义。

/* Begin PBXProject section */
        02E680D92560F4BE009EEC2E /* Project object */ = {
            isa = PBXProject;
            attributes = {
                LastUpgradeCheck = 1220;
                TargetAttributes = {
                    02E680E02560F4BE009EEC2E = {
                        CreatedOnToolsVersion = 12.2;
                    };
                };
            };
            buildConfigurationList = 02E680DC2560F4BE009EEC2E /* Build configuration list for PBXProject "VScrollContentView" */;
            compatibilityVersion = "Xcode 9.3";
            developmentRegion = en;
            hasScannedForEncodings = 0;
            knownRegions = (
                en,
                Base,
            );
            mainGroup = 02E680D82560F4BE009EEC2E;
            productRefGroup = 02E680E22560F4BE009EEC2E /* Products */;
            projectDirPath = "";
            projectRoot = "";
            targets = (
                02E680E02560F4BE009EEC2E /* VScrollContentView */,
            );
        };
/* End PBXProject section */

在 targets 里会指向各个 taget 的定义

/* Begin PBXNativeTarget section */
        02E680E02560F4BE009EEC2E /* VScrollContentView */ = {
            isa = PBXNativeTarget;
            buildConfigurationList = 02E680FA2560F4C3009EEC2E /* Build configuration list for PBXNativeTarget "VScrollContentView" */;
            buildPhases = (
                02E680DD2560F4BE009EEC2E /* Sources */,
                02E680DF2560F4BE009EEC2E /* Resources */,
                02E680DE2560F4BE009EEC2E /* Frameworks */,
            );
            buildRules = (
            );
            dependencies = (
            );
            name = VScrollContentView;
            productName = VScrollContentView;
            productReference = 02E680E12560F4BE009EEC2E /* VScrollContentView.app */;
            productType = "com.apple.product-type.application";
        };
/* End PBXNativeTarget section */

这个里面又有更多的 ID 可以得到更多的定义,其中 buildConfigurationList 指向了可用的配置项,包含 Debug 和 Release。可以看到还有 buildPhases,buildRules 和 dependencies 都能够通过这里索引找到更详细的定义。

Clang 编译流程

以ViewController.m为例,首先对文件进行描述

CompileC  DerivedData path/ViewController.o Project path/ViewController.m normal x86_64 objective-c com.apple.compilers.llvm.clang.1_0.compiler (in target 'Target Name' from project 'Project Name')

接下来对会更新工作路径

    cd Project Path
    export LANG\=en_US.US-ASCII

接下来就是实际的编译命令

clang Path/clang -x objective-c -target x86_64....../x86_64/ViewController.o

Clang Static Analyzer静态代码分析

静态分析前会对源代码分词成 Token,这个过程称为词法分析(Lexical Analysis)。Token 可以分为以下几类:

  • 关键字:语法中的关键字,if else while for 等。
  • 标识符:变量名
  • 字面量:值,数字,字符串
  • 特殊符号:加减乘除等符号
    ViewController.m 文件词法分析
In file included from ViewController.m:8:
./ViewController.h:8:9: fatal error: 'UIKit/UIKit.h' file not found
#import <UIKit/UIKit.h>
        ^~~~~~~~~~~~~~~
at '@'   [StartOfLine]  Loc=<./ViewController.h:10:1>
identifier 'interface'      Loc=<./ViewController.h:10:2>
identifier 'ViewController'  [LeadingSpace] Loc=<./ViewController.h:10:12>
colon ':'    [LeadingSpace] Loc=<./ViewController.h:10:27>
identifier 'UIViewController'    [LeadingSpace] Loc=<./ViewController.h:10:29>
at '@'   [StartOfLine]  Loc=<./ViewController.h:13:1>
identifier 'end'        Loc=<./ViewController.h:13:2>
at '@'   [StartOfLine]  Loc=<ViewController.m:9:1>
identifier 'interface'      Loc=<ViewController.m:9:2>
identifier 'ViewController'  [LeadingSpace] Loc=<ViewController.m:9:12>
l_paren '('  [LeadingSpace] Loc=<ViewController.m:9:27>
r_paren ')'     Loc=<ViewController.m:9:28>
at '@'   [StartOfLine]  Loc=<ViewController.m:10:1>
identifier 'end'        Loc=<ViewController.m:10:2>
at '@'   [StartOfLine]  Loc=<ViewController.m:12:1>
identifier 'implementation'     Loc=<ViewController.m:12:2>
identifier 'ViewController'  [LeadingSpace] Loc=<ViewController.m:12:17>
minus '-'    [StartOfLine]  Loc=<ViewController.m:14:1>
l_paren '('  [LeadingSpace] Loc=<ViewController.m:14:3>
void 'void'     Loc=<ViewController.m:14:4>
r_paren ')'     Loc=<ViewController.m:14:8>
identifier 'viewDidLoad'        Loc=<ViewController.m:14:9>
l_brace '{'  [LeadingSpace] Loc=<ViewController.m:14:21>
l_square '['     [StartOfLine] [LeadingSpace]   Loc=<ViewController.m:15:5>
identifier 'super'      Loc=<ViewController.m:15:6>
identifier 'viewDidLoad'     [LeadingSpace] Loc=<ViewController.m:15:12>
r_square ']'        Loc=<ViewController.m:15:23>
semi ';'        Loc=<ViewController.m:15:24>
identifier 'UIView'  [StartOfLine] [LeadingSpace]   Loc=<ViewController.m:17:5>
star '*'     [LeadingSpace] Loc=<ViewController.m:17:12>
identifier 'view'       Loc=<ViewController.m:17:13>
equal '='    [LeadingSpace] Loc=<ViewController.m:17:18>
l_square '['     [LeadingSpace] Loc=<ViewController.m:17:20>
l_square '['        Loc=<ViewController.m:17:21>
identifier 'UIView'     Loc=<ViewController.m:17:22>
identifier 'alloc'   [LeadingSpace] Loc=<ViewController.m:17:29>
r_square ']'        Loc=<ViewController.m:17:34>
identifier 'init'    [LeadingSpace] Loc=<ViewController.m:17:36>
r_square ']'        Loc=<ViewController.m:17:40>
semi ';'        Loc=<ViewController.m:17:41>
l_square '['     [StartOfLine] [LeadingSpace]   Loc=<ViewController.m:18:5>
identifier 'self'       Loc=<ViewController.m:18:6>
period '.'      Loc=<ViewController.m:18:10>
identifier 'view'       Loc=<ViewController.m:18:11>
identifier 'addSubview'  [LeadingSpace] Loc=<ViewController.m:18:16>
colon ':'       Loc=<ViewController.m:18:26>
identifier 'view'       Loc=<ViewController.m:18:27>
r_square ']'        Loc=<ViewController.m:18:31>
semi ';'        Loc=<ViewController.m:18:32>
r_brace '}'  [StartOfLine]  Loc=<ViewController.m:19:1>
at '@'   [StartOfLine]  Loc=<ViewController.m:20:1>
identifier 'end'        Loc=<ViewController.m:20:2>
eof ''      Loc=<ViewController.m:20:5>
1 error generated.

可以获得每个 token 的类型,值还有类似 StartOfLine的位置类型和 Loc=<./ViewController.h:10:1>这个样的具体位置。

接着进行语法分析(Semantic Analysis)将 token 先按照语法组合成语义生成 VarDecl 节点,然后将这些节点按照层级关系构成抽象语法树 Abstract Syntax Tree (AST)。
打个比方,如果遇到 token 是 = 符号进行赋值的处理,遇到加减乘除就先处理乘除,然后处理加减,这些组合经过嵌套后会生成一个语法数的结构。这个过程完成后会进行赋值操作时类型是不是匹配的处理。

clang -fmodules -fsyntax-only -Xclang -ast-dump ViewController.m

ViewController.m 文件语法分析

In file included from ViewController.m:8:
./ViewController.h:8:9: fatal error: 'UIKit/UIKit.h' file not found
#import <UIKit/UIKit.h>
        ^~~~~~~~~~~~~~~
TranslationUnitDecl 0x7fb6e0827e08 <<invalid sloc>> <invalid sloc>
|-TypedefDecl 0x7fb6e08286a0 <<invalid sloc>> <invalid sloc> implicit __int128_t '__int128'
| `-BuiltinType 0x7fb6e08283a0 '__int128'
|-TypedefDecl 0x7fb6e0828710 <<invalid sloc>> <invalid sloc> implicit __uint128_t 'unsigned __int128'
| `-BuiltinType 0x7fb6e08283c0 'unsigned __int128'
|-TypedefDecl 0x7fb6e08287b0 <<invalid sloc>> <invalid sloc> implicit SEL 'SEL *'
| `-PointerType 0x7fb6e0828770 'SEL *'
|   `-BuiltinType 0x7fb6e0828600 'SEL'
|-TypedefDecl 0x7fb6e0828898 <<invalid sloc>> <invalid sloc> implicit id 'id'
| `-ObjCObjectPointerType 0x7fb6e0828840 'id'
|   `-ObjCObjectType 0x7fb6e0828810 'id'
|-TypedefDecl 0x7fb6e0828978 <<invalid sloc>> <invalid sloc> implicit Class 'Class'
| `-ObjCObjectPointerType 0x7fb6e0828920 'Class'
|   `-ObjCObjectType 0x7fb6e08288f0 'Class'
|-ObjCInterfaceDecl 0x7fb6e08289d0 <<invalid sloc>> <invalid sloc> implicit Protocol
|-TypedefDecl 0x7fb6e0828d68 <<invalid sloc>> <invalid sloc> implicit __NSConstantString 'struct __NSConstantString_tag'
| `-RecordType 0x7fb6e0828b40 'struct __NSConstantString_tag'
|   `-Record 0x7fb6e0828aa0 '__NSConstantString_tag'
|-TypedefDecl 0x7fb6e0834600 <<invalid sloc>> <invalid sloc> implicit __builtin_ms_va_list 'char *'
| `-PointerType 0x7fb6e0828dc0 'char *'
|   `-BuiltinType 0x7fb6e0827ea0 'char'
|-TypedefDecl 0x7fb6e0834908 <<invalid sloc>> <invalid sloc> implicit __builtin_va_list 'struct __va_list_tag [1]'
| `-ConstantArrayType 0x7fb6e08348b0 'struct __va_list_tag [1]' 1 
|   `-RecordType 0x7fb6e08346f0 'struct __va_list_tag'
|     `-Record 0x7fb6e0834658 '__va_list_tag'
|-ObjCInterfaceDecl 0x7fb6e0834960 <./ViewController.h:10:1, line:13:2> line:10:12 ViewController
| `-ObjCImplementation 0x7fb6e0834b80 'ViewController'
|-ObjCCategoryDecl 0x7fb6e0834aa0 <ViewController.m:9:1, line:10:2> line:9:12
| `-ObjCInterface 0x7fb6e0834960 'ViewController'
`-ObjCImplementationDecl 0x7fb6e0834b80 <line:12:1, line:20:1> line:12:17 ViewController
  |-ObjCInterface 0x7fb6e0834960 'ViewController'
  `-ObjCMethodDecl 0x7fb6e0834cd0 <line:14:1, line:19:1> line:14:1 - viewDidLoad 'void'
    |-ImplicitParamDecl 0x7fb6e0834e88 <<invalid sloc>> <invalid sloc> implicit used self 'ViewController *'
    |-ImplicitParamDecl 0x7fb6e0834ef0 <<invalid sloc>> <invalid sloc> implicit _cmd 'SEL':'SEL *'
    `-CompoundStmt 0x7fb6e0834fa8 <col:21, line:19:1>
1 error generated.

TranslationUnitDecl 是根节点,表示一个源文件。Decl 表示一个声明,Expr 表示表达式,Literal 表示字面量是特殊的 Expr,Stmt 表示语句。

clang 静态分析是通过建立分析引擎和 checkers 所组成的架构,所有 checker 都是基于底层分析引擎之上,通过分析引擎提供的功能能够编写新的 checker。
这种方式能够方便用户扩展对代码检查规则或者对 bug 类型进行扩展,但是这种架构也有不足,每执行完一条语句后,分析引擎会遍历所有 checker 中的回调函数,所以 checker 越多,速度越慢。

CodeGen 生成 IR 代码

将语法树翻译成 LLVM IR 中间代码,这个过程中还会跟 runtime 桥接。

各种类,方法,成员变量等的结构体的生成,并将其放到对应的Mach-O的section中。
Non-Fragile ABI 合成 OBJC_IVAR_$_ 偏移值常量。
ObjCMessageExpr 翻译成相应版本的 objc_msgSend,super 翻译成 objc_msgSendSuper。
strong,weak,copy,atomic 合成 @property 自动实现 setter 和 getter。
@synthesize 的处理。
生成 block_layout 数据结构
__block 和 __weak
_block_invoke
ARC 处理,插入 objc_storeStrong 和 objc_storeWeak 等 ARC 代码。ObjCAutoreleasePoolStmt 转 objc_autorealeasePoolPush / Pop。自动添加 [super dealloc]。给每个 ivar 的类合成 .cxx_destructor 方法自动释放类的成员变量。

IR 结构

; ModuleID = 'ViewController.m'
source_filename = "ViewController.m"
target datalayout = "e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-ios14.2.0-simulator"

%0 = type opaque
%1 = type opaque
%struct._class_t = type { %struct._class_t*, %struct._class_t*, %struct._objc_cache*, i8* (i8*, i8*)**, %struct._class_ro_t* }
%struct._objc_cache = type opaque
%struct._class_ro_t = type { i32, i32, i32, i8*, i8*, %struct.__method_list_t*, %struct._objc_protocol_list*, %struct._ivar_list_t*, i8*, %struct._prop_list_t* }
%struct.__method_list_t = type { i32, i32, [0 x %struct._objc_method] }
%struct._objc_method = type { i8*, i8*, i8* }
%struct._objc_protocol_list = type { i64, [0 x %struct._protocol_t*] }
%struct._protocol_t = type { i8*, i8*, %struct._objc_protocol_list*, %struct.__method_list_t*, %struct.__method_list_t*, %struct.__method_list_t*, %struct.__method_list_t*, %struct._prop_list_t*, i32, i32, i8**, i8*, %struct._prop_list_t* }
%struct._ivar_list_t = type { i32, i32, [0 x %struct._ivar_t] }
%struct._ivar_t = type { i64*, i8*, i8*, i32, i32 }
%struct._prop_list_t = type { i32, i32, [0 x %struct._prop_t] }
%struct._prop_t = type { i8*, i8* }
%struct._objc_super = type { i8*, i8* }

@"OBJC_CLASS_$_ViewController" = global %struct._class_t { %struct._class_t* @"OBJC_METACLASS_$_ViewController", %struct._class_t* @"OBJC_CLASS_$_UIViewController", %struct._objc_cache* @_objc_empty_cache, i8* (i8*, i8*)** null, %struct._class_ro_t* @"_OBJC_CLASS_RO_$_ViewController" }, section "__DATA, __objc_data", align 8
@"OBJC_CLASSLIST_SUP_REFS_$_" = private global %struct._class_t* @"OBJC_CLASS_$_ViewController", section "__DATA,__objc_superrefs,regular,no_dead_strip", align 8
@OBJC_METH_VAR_NAME_ = private unnamed_addr constant [12 x i8] c"viewDidLoad\00", section "__TEXT,__objc_methname,cstring_literals", align 1
@OBJC_SELECTOR_REFERENCES_ = internal externally_initialized global i8* getelementptr inbounds ([12 x i8], [12 x i8]* @OBJC_METH_VAR_NAME_, i32 0, i32 0), section "__DATA,__objc_selrefs,literal_pointers,no_dead_strip", align 8
@"OBJC_CLASS_$_UIView" = external global %struct._class_t
@"OBJC_CLASSLIST_REFERENCES_$_" = internal global %struct._class_t* @"OBJC_CLASS_$_UIView", section "__DATA,__objc_classrefs,regular,no_dead_strip", align 8
@OBJC_METH_VAR_NAME_.1 = private unnamed_addr constant [5 x i8] c"view\00", section "__TEXT,__objc_methname,cstring_literals", align 1
@OBJC_SELECTOR_REFERENCES_.2 = internal externally_initialized global i8* getelementptr inbounds ([5 x i8], [5 x i8]* @OBJC_METH_VAR_NAME_.1, i32 0, i32 0), section "__DATA,__objc_selrefs,literal_pointers,no_dead_strip", align 8
@OBJC_METH_VAR_NAME_.3 = private unnamed_addr constant [12 x i8] c"addSubview:\00", section "__TEXT,__objc_methname,cstring_literals", align 1
@OBJC_SELECTOR_REFERENCES_.4 = internal externally_initialized global i8* getelementptr inbounds ([12 x i8], [12 x i8]* @OBJC_METH_VAR_NAME_.3, i32 0, i32 0), section "__DATA,__objc_selrefs,literal_pointers,no_dead_strip", align 8
@_objc_empty_cache = external global %struct._objc_cache
@"OBJC_METACLASS_$_NSObject" = external global %struct._class_t
@"OBJC_METACLASS_$_UIViewController" = external global %struct._class_t
@OBJC_CLASS_NAME_ = private unnamed_addr constant [15 x i8] c"ViewController\00", section "__TEXT,__objc_classname,cstring_literals", align 1
@"_OBJC_METACLASS_RO_$_ViewController" = internal global %struct._class_ro_t { i32 129, i32 40, i32 40, i8* null, i8* getelementptr inbounds ([15 x i8], [15 x i8]* @OBJC_CLASS_NAME_, i32 0, i32 0), %struct.__method_list_t* null, %struct._objc_protocol_list* null, %struct._ivar_list_t* null, i8* null, %struct._prop_list_t* null }, section "__DATA, __objc_const", align 8
@"OBJC_METACLASS_$_ViewController" = global %struct._class_t { %struct._class_t* @"OBJC_METACLASS_$_NSObject", %struct._class_t* @"OBJC_METACLASS_$_UIViewController", %struct._objc_cache* @_objc_empty_cache, i8* (i8*, i8*)** null, %struct._class_ro_t* @"_OBJC_METACLASS_RO_$_ViewController" }, section "__DATA, __objc_data", align 8
@"OBJC_CLASS_$_UIViewController" = external global %struct._class_t
@OBJC_METH_VAR_TYPE_ = private unnamed_addr constant [8 x i8] c"v16@0:8\00", section "__TEXT,__objc_methtype,cstring_literals", align 1
@"_OBJC_$_INSTANCE_METHODS_ViewController" = internal global { i32, i32, [1 x %struct._objc_method] } { i32 24, i32 1, [1 x %struct._objc_method] [%struct._objc_method { i8* getelementptr inbounds ([12 x i8], [12 x i8]* @OBJC_METH_VAR_NAME_, i32 0, i32 0), i8* getelementptr inbounds ([8 x i8], [8 x i8]* @OBJC_METH_VAR_TYPE_, i32 0, i32 0), i8* bitcast (void (%0*, i8*)* @"\01-[ViewController viewDidLoad]" to i8*) }] }, section "__DATA, __objc_const", align 8
@"_OBJC_CLASS_RO_$_ViewController" = internal global %struct._class_ro_t { i32 128, i32 8, i32 8, i8* null, i8* getelementptr inbounds ([15 x i8], [15 x i8]* @OBJC_CLASS_NAME_, i32 0, i32 0), %struct.__method_list_t* bitcast ({ i32, i32, [1 x %struct._objc_method] }* @"_OBJC_$_INSTANCE_METHODS_ViewController" to %struct.__method_list_t*), %struct._objc_protocol_list* null, %struct._ivar_list_t* null, i8* null, %struct._prop_list_t* null }, section "__DATA, __objc_const", align 8
@"OBJC_LABEL_CLASS_$" = private global [1 x i8*] [i8* bitcast (%struct._class_t* @"OBJC_CLASS_$_ViewController" to i8*)], section "__DATA,__objc_classlist,regular,no_dead_strip", align 8
@llvm.compiler.used = appending global [12 x i8*] [i8* bitcast (%struct._class_t** @"OBJC_CLASSLIST_SUP_REFS_$_" to i8*), i8* getelementptr inbounds ([12 x i8], [12 x i8]* @OBJC_METH_VAR_NAME_, i32 0, i32 0), i8* bitcast (i8** @OBJC_SELECTOR_REFERENCES_ to i8*), i8* bitcast (%struct._class_t** @"OBJC_CLASSLIST_REFERENCES_$_" to i8*), i8* getelementptr inbounds ([5 x i8], [5 x i8]* @OBJC_METH_VAR_NAME_.1, i32 0, i32 0), i8* bitcast (i8** @OBJC_SELECTOR_REFERENCES_.2 to i8*), i8* getelementptr inbounds ([12 x i8], [12 x i8]* @OBJC_METH_VAR_NAME_.3, i32 0, i32 0), i8* bitcast (i8** @OBJC_SELECTOR_REFERENCES_.4 to i8*), i8* getelementptr inbounds ([15 x i8], [15 x i8]* @OBJC_CLASS_NAME_, i32 0, i32 0), i8* getelementptr inbounds ([8 x i8], [8 x i8]* @OBJC_METH_VAR_TYPE_, i32 0, i32 0), i8* bitcast ({ i32, i32, [1 x %struct._objc_method] }* @"_OBJC_$_INSTANCE_METHODS_ViewController" to i8*), i8* bitcast ([1 x i8*]* @"OBJC_LABEL_CLASS_$" to i8*)], section "llvm.metadata"
; Function Attrs: noinline optnone ssp uwtable
define internal void @"\01-[ViewController viewDidLoad]"(%0* %0, i8* %1) #0 {
  %3 = alloca %0*, align 8
  %4 = alloca i8*, align 8
  %5 = alloca %struct._objc_super, align 8
  %6 = alloca %1*, align 8
  store %0* %0, %0** %3, align 8
  store i8* %1, i8** %4, align 8
  %7 = load %0*, %0** %3, align 8
  %8 = bitcast %0* %7 to i8*
  %9 = getelementptr inbounds %struct._objc_super, %struct._objc_super* %5, i32 0, i32 0
  store i8* %8, i8** %9, align 8
  %10 = load %struct._class_t*, %struct._class_t** @"OBJC_CLASSLIST_SUP_REFS_$_", align 8
  %11 = bitcast %struct._class_t* %10 to i8*
  %12 = getelementptr inbounds %struct._objc_super, %struct._objc_super* %5, i32 0, i32 1
  store i8* %11, i8** %12, align 8
  %13 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_, align 8, !invariant.load !10
  call void bitcast (i8* (%struct._objc_super*, i8*, ...)* @objc_msgSendSuper2 to void (%struct._objc_super*, i8*)*)(%struct._objc_super* %5, i8* %13)
  %14 = load %struct._class_t*, %struct._class_t** @"OBJC_CLASSLIST_REFERENCES_$_", align 8
  %15 = bitcast %struct._class_t* %14 to i8*
  %16 = call i8* @objc_alloc_init(i8* %15)
  %17 = bitcast i8* %16 to %1*
  store %1* %17, %1** %6, align 8
  %18 = load %0*, %0** %3, align 8
  %19 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_.2, align 8, !invariant.load !10
  %20 = bitcast %0* %18 to i8*
  %21 = call %1* bitcast (i8* (i8*, i8*, ...)* @objc_msgSend to %1* (i8*, i8*)*)(i8* %20, i8* %19)
  %22 = bitcast %1* %21 to i8*
  %23 = notail call i8* @llvm.objc.retainAutoreleasedReturnValue(i8* %22) #2
  %24 = bitcast i8* %23 to %1*
  %25 = load %1*, %1** %6, align 8
  %26 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_.4, align 8, !invariant.load !10
  %27 = bitcast %1* %24 to i8*
  call void bitcast (i8* (i8*, i8*, ...)* @objc_msgSend to void (i8*, i8*, %1*)*)(i8* %27, i8* %26, %1* %25)
  %28 = bitcast %1* %24 to i8*
  call void @llvm.objc.release(i8* %28) #2, !clang.imprecise_release !10
  %29 = bitcast %1** %6 to i8**
  call void @llvm.objc.storeStrong(i8** %29, i8* null) #2
  ret void
}
declare i8* @objc_msgSendSuper2(%struct._objc_super*, i8*, ...)
declare i8* @objc_alloc_init(i8*)
; Function Attrs: nonlazybind
declare i8* @objc_msgSend(i8*, i8*, ...) #1
; Function Attrs: nounwind
declare i8* @llvm.objc.retainAutoreleasedReturnValue(i8*) #2
; Function Attrs: nounwind
declare void @llvm.objc.release(i8*) #2
; Function Attrs: nounwind
declare void @llvm.objc.storeStrong(i8**, i8*) #2

attributes #0 = { noinline optnone ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "frame-pointer"="all" "less-precise-fpmad"="false" "min-legal-vector-width"="0" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="core2" "target-features"="+cx16,+cx8,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #1 = { nonlazybind }
attributes #2 = { nounwind }

!llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7, !8}
!llvm.ident = !{!9}

!0 = !{i32 2, !"SDK Version", [2 x i32] [i32 14, i32 2]}
!1 = !{i32 1, !"Objective-C Version", i32 2}
!2 = !{i32 1, !"Objective-C Image Info Version", i32 0}
!3 = !{i32 1, !"Objective-C Image Info Section", !"__DATA,__objc_imageinfo,regular,no_dead_strip"}
!4 = !{i32 4, !"Objective-C Garbage Collection", i32 0}
!5 = !{i32 1, !"Objective-C Is Simulated", i32 32}
!6 = !{i32 1, !"Objective-C Class Properties", i32 64}
!7 = !{i32 1, !"wchar_size", i32 4}
!8 = !{i32 7, !"PIC Level", i32 2}
!9 = !{!"Apple clang version 12.0.0 (clang-1200.0.32.27)"}
!10 = !{}

IR 语法关键字,如下:

@ - 代表全局变量
% - 代表局部变量
alloca - 指令在当前执行的函数的堆栈帧中分配内存,当该函数返回到其调用者时,将自动释放内存。
i32:- i 是几这个整数就会占几位,i32就是32位4字节
align - 对齐,比如一个 int,一个 char 和一个 int。单个 int 占4个字节,为了对齐只占一个字节的 char需要向4对齐占用4字节空间。
Load - 读出,store 写入
icmp - 两个整数值比较,返回布尔值
br - 选择分支,根据 cond 来转向 label,不根据条件跳转的话类似 goto
indirectbr - 根据条件间接跳转到一个 label,而这个 label 一般是在一个数组里,所以跳转目标是可变的,由运行时决定的
label - 代码标签

之后可以进行 lli 解释执行 LLVM IR。
llc 编译器是专门编译 LLVM IR 的编译器用来生成汇编文件。
调用系统汇编器比如 GNU 的 as 来编译生成 .o Object 文件,接下来就是用链接器链接相关库和 .o 文件一起生成可执行的 .out 或者 exe 文件了。

编译后生成的二进制内容 Link Map File

在 Build Settings 里设置 Write Link Map File 为 Yes 后每次编译都会在指定目录生成这样一个文件。文件内容包含 Object files,Sections,Symbols。

Object files
这个部分的内容都是 .m 文件编译后的 .o 和需要 link 的 .a 文件。前面是文件编号,后面是文件路径。

[  0] linker synthesized
[  1] /Users/aibo/Library/Developer/Xcode/DerivedData/VScrollContentView-gwqgglemsizgjsdwjklvhytmwogd/Build/Intermediates.noindex/VScrollContentView.build/Debug-iphonesimulator/VScrollContentView.build/VScrollContentView.app-Simulated.xcent
[  2] /Users/aibo/Library/Developer/Xcode/DerivedData/VScrollContentView-gwqgglemsizgjsdwjklvhytmwogd/Build/Intermediates.noindex/VScrollContentView.build/Debug-iphonesimulator/VScrollContentView.build/Objects-normal/x86_64/ViewController.o
[  3] /Users/aibo/Library/Developer/Xcode/DerivedData/VScrollContentView-gwqgglemsizgjsdwjklvhytmwogd/Build/Intermediates.noindex/VScrollContentView.build/Debug-iphonesimulator/VScrollContentView.build/Objects-normal/x86_64/AppDelegate.o
[  4] /Users/aibo/Library/Developer/Xcode/DerivedData/VScrollContentView-gwqgglemsizgjsdwjklvhytmwogd/Build/Intermediates.noindex/VScrollContentView.build/Debug-iphonesimulator/VScrollContentView.build/Objects-normal/x86_64/main.o
[  5] /Users/aibo/Library/Developer/Xcode/DerivedData/VScrollContentView-gwqgglemsizgjsdwjklvhytmwogd/Build/Intermediates.noindex/VScrollContentView.build/Debug-iphonesimulator/VScrollContentView.build/Objects-normal/x86_64/SceneDelegate.o
[  6] /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.2.sdk/System/Library/Frameworks//UIKit.framework/UIKit.tbd
[  7] /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.2.sdk/System/Library/Frameworks//Foundation.framework/Foundation.tbd
[  8] /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.2.sdk/usr/lib/libobjc.tbd

Sections
这里描述的是每个 Section 在可执行文件中的位置和大小。每个 Section 的 Segment 的类型分为 __TEXT 代码段和 __DATA 数据段两种。

0x100001E30 0x000005D3  __TEXT  __text
0x100002404 0x00000042  __TEXT  __stubs
0x100002448 0x0000007E  __TEXT  __stub_helper
0x1000024C6 0x00000D55  __TEXT  __objc_methname
0x10000321B 0x00000070  __TEXT  __objc_classname
0x10000328B 0x00000B0F  __TEXT  __objc_methtype
0x100003D9A 0x00000090  __TEXT  __cstring
0x100003E2A 0x00000186  __TEXT  __entitlements
0x100003FB0 0x00000048  __TEXT  __unwind_info
0x100004000 0x00000018  __DATA_CONST    __got
0x100004018 0x00000020  __DATA_CONST    __cfstring
0x100004038 0x00000018  __DATA_CONST    __objc_classlist
0x100004050 0x00000020  __DATA_CONST    __objc_protolist
0x100004070 0x00000008  __DATA_CONST    __objc_imageinfo
0x100008000 0x00000058  __DATA  __la_symbol_ptr
0x100008058 0x00001328  __DATA  __objc_const
0x100009380 0x00000028  __DATA  __objc_selrefs
0x1000093A8 0x00000018  __DATA  __objc_classrefs
0x1000093C0 0x00000008  __DATA  __objc_superrefs
0x1000093C8 0x00000008  __DATA  __objc_ivar
0x1000093D0 0x000000F0  __DATA  __objc_data
0x1000094C0 0x00000188  __DATA  __data

Symbols
Symbols 是对 Sections 进行了再划分。这里会描述所有的 methods,ivar 和字符串,及它们对应的地址,大小,文件编号信息。

dSYM 文件

在每次编译后都会生成一个 dSYM 文件,程序在执行中通过地址来调用方法函数,而 dSYM 文件里存储了函数地址映射,这样调用栈里的地址可以通过 dSYM 这个映射表能够获得具体函数的位置。一般都会用来处理 crash 时获取到的调用栈 .crash 文件将其符号化。
dSYM文件也属于Mach-O文件


image.png

Mach-O 文件

记录编译后的可执行文件,对象代码,共享库,动态加载代码和内存转储的文件格式。不同于 xml 这样的文件,它只是二进制字节流,里面有不同的包含元信息的数据块,比如字节顺序,cpu 类型,块大小等。文件内容是不可以修改的,因为在 .app 目录中有个 _CodeSignature 的目录,里面包含了程序代码的签名,这个签名的作用就是保证签名后 .app 里的文件,包括资源文件,Mach-O 文件都不能够更改。

Mach-O 文件包含三个区域

Mach-O Header:包含字节顺序,magic,cpu 类型,加载指令的数量等
Load Commands:包含很多内容的表,包括区域的位置,符号表,动态符号表等。每个加载指令包含一个元信息,比如指令类型,名称,在二进制中的位置等。
Data:最大的部分,包含了代码,数据,比如符号表,动态符号表等。
大概结构如下图所示


image.png

Mach-O Header

数据结构
struct mach_header_64 {
    uint32_t    magic;      /* mach magic number identifier */
    cpu_type_t  cputype;    /* cpu specifier */
    cpu_subtype_t   cpusubtype; /* machine specifier */
    uint32_t    filetype;   /* type of file */
    uint32_t    ncmds;      /* number of load commands */
    uint32_t    sizeofcmds; /* the size of all the load commands */
    uint32_t    flags;      /* flags */
    uint32_t    reserved;   /* reserved */
};

根据定义与注释,得到以下解释

名称 含义
magic Mach-O魔数,FAT:0xcafebabeARMv7:0xfeedface,ARM64:0xfeedfacf
cputype、cpusubtype CPU架构及子版本
filetype MH_EXECUTABLE(可执行二进制文件)、MH_OBJECT(目标文件)、MH_DYLIB(动态库),有11种宏定义类型,具体可查看源码
ncmds 加载命令的数量
sizeofcmds 所有加载命令的大小
flags dyld加载需要的一些标记,有28种宏定义,具体看源码,其中MH_PIE表示启用ASLR地址空间布局随机化
reserved 64位保留字段

Load Commands

//load_commands紧跟mach_header,
//load_commands展开后的数目与总大小已经在mach_header有记录,所有加载指令都是以cmd、cmdsize起头。
//cmd字段用该命令类型的常量表示,有专门的结构;
//cmdsize字段以字节为单位,主要记录偏移量让load command指针进入下一条加载指令,
//32位架构的cmdsize是以4字节的倍数,64位结构的cmdsize是以8字节的倍数(加载指令永远是这样对齐),不够用0填充字节。
//文件中的所有表都遵循这样的规则,这样就可以被映射到内存,否则的话指针不能很好地指向。
struct load_command {
    uint32_t cmd;       /* type of load command */
    uint32_t cmdsize;   /* total size of command in bytes */
};

Load Commands下常见的加载指令:

指令 含义
LC_SEGMENT_64 定义一段(Segment),加载后被映射到进程的内存空间中,包括里面的节(Section)
LC_DYLD_INFO_ONLY 记录有关链接的信息,包括在__LINKEDIT中动态链接的相关信息的具体偏移大小(重定位,绑定,弱绑定,懒加载绑定,导出信息等),ONLY表示该指令是程序运行所必需的。
LC_SYMTAB 定义符号表和字符串表,链接文件时被dyld使用,也用于调试器映射符号到源文件。符号表定义的本地符号仅用于调试,而已定义和未定义的external符号被链接器使用
LC_DYSYMTAB 将符号表中给出符号的额外信息提供给dyld
LC_LOAD_DYLINKER dyld的默认路径
LC_UUID Mach-O唯一ID
LC_VERSION_MIN_IPHONES 系统要求的最低版本
LC_SOURCE_VERSION 构建二进制文件的源代码版本号
LC_MAIN 应用程序入口,dyld的_main函数获取该地址,然后跳转
LC_ENCRYPTION_INFO_64 文件加密标志,加密内容偏移和大小
LC_LOAD_DYLIB 依赖的动态库,含动态库名,版本号等信息
LC_RPATH @rpath搜索路径
LC_DATA_IN_CODE 定义在代码段内的非指令的表
LC_CODE_SIGNATURE 代码签名信息

LC_SEGMENT_64段数据结构(说明附在注释部分):

/*
 * The 64-bit segment load command indicates that a part of this file is to be
 * mapped into a 64-bit task's address space.  If the 64-bit segment has
 * sections then section_64 structures directly follow the 64-bit segment
 * command and their size is reflected in cmdsize.
 */
struct segment_command_64 { /* for 64-bit architectures */
    uint32_t    cmd;        /* Load Command类型 */
    uint32_t    cmdsize;    /*包含的所有section结构体的大小 */
    char        segname[16];    /* 段名 */
    uint64_t    vmaddr;     /* 映射到虚拟地址的偏移 */
    uint64_t    vmsize;     /* 映射到虚拟地址的大小 */
    uint64_t    fileoff;    /* 相对于当前架构文件的偏移 */
    uint64_t    filesize;   /* 文件大小 */
    vm_prot_t   maxprot;    /* 段页面的最高内存保护 */
    vm_prot_t   initprot;   /* 初始内存保护 */
    uint32_t    nsects;     /* 包含的section数 */
    uint32_t    flags;      /* 段页面标志 */
};

该数据结构的段主要有以下4种:

含义
_PAGEZERO 空指针陷阱段,映射到虚拟内存空间第一页,捕捉对NULL指针的引用
_TEXT 代码段、只读数据段
_DATA 读取和写入数据段
_LINKEDIT dyld需要使用的信息,包括重定位、绑定、懒加载信息等

Data

Load Commands区域下来接着就是DATA区域,展开Load Commands下的LC_SEGMENT_64可以看到多个Section64,各个Section的具体信息可以在Load Commands紧接着的部分查看,它们是一一对应的:
section的数据结构如下:

struct section_64 { /* for 64-bit architectures */
    char        sectname[16];   /* 节名 */
    char        segname[16];    /* 所属段名 */
    uint64_t    addr;       /* 映射到虚拟地址的偏移 */
    uint64_t    size;       /* 节的大小 */
    uint32_t    offset;     /* 节在当前架构文件中的偏移 */
    uint32_t    align;      /* 节的字节对齐大小n,2^n */
    uint32_t    reloff;     /* 重定位入口的文件偏移 */
    uint32_t    nreloc;     /* 重定位入口个数 */
    uint32_t    flags;      /* 节的类型和属性*/
    uint32_t    reserved1;  /* reserved (for offset or index) */
    uint32_t    reserved2;  /* reserved (for count or sizeof) */
    uint32_t    reserved3;  /* 保留位,以上两同理 */
};

section节已经是最小的分类,大部分内容集中在__TEXT,__DATA这两段中,部分内容如下:

__TEXT节 含义
__text 程序可执行代码区域
__stubs 间接符号存根,用于跳转到懒加载指针表
__stubs_helper 懒加载符号加载辅助函数
__cstring 只读的C字符串,包含OC的部分字符串和属性名
...... ......
__DATA 含义
__nl_symbol_ptr 非懒加载指针表,dyld加载时立即绑定值
__la_symbol_ptr 懒加载指针表,第1次调用才绑定值
__got 非懒加载全局指针表
__mod_init_func constructor函数
__cfstring OC字符串

任意的片段

使用链接符号 -sectcreate 我们可以给可执行文件以 section 的方式添加任意的数据。这就是如何将一个 Info.plist 文件添加到一个独立的可执行文件中的方法。Info.plist 文件中的数据需要放入到 __TEXT segment 里面的一个 __info_plist section 中。可以将 -sectcreate segname sectname file 传递给链接器(通过将下面的内容传递给 clang):

-Wl,-sectcreate,__TEXT,__info_plist,path/to/Info.plist

同样,-sectalign 规定了对其方式。如果你添加的是一个全新的 segment,那么需要通过 -segprot 来规定 segment 的保护方式 (读/写/可执行)。这些所有内容在链接器的帮助文档中都有,例如 ld(1)。

我们可以利用定义在 /usr/include/mach-o/getsect.h 中的函数 getsectdata() 得到 section,例如 getsectdata() 可以得到指向 section 数据的一个指针,并返回相关 section 的长度。

可执行文件的链接

A.h

#import <Foundation/Foundation.h>
@interface A : NSObject
- (void)doSomething;
@end

A.m

#import "A.h"
@implementation A
- (void)doSomething{
    NSLog(@"just try");
}
@end

B.m

#import "A.h"
int main(int argc, char *argv[])
{
    @autoreleasepool {
        A *a = [[A alloc] init];
        [a doSomething];
        return 0;
    }
}

先编译多个文件

xcrun clang -c A.m
xcrun clang -c B.m

再将编译后的文件链接起来,这样就可以生成 a.out 可执行文件了。

xcrun clang A.o B.o -Wl,`xcrun —show-sdk-path`/System/Library/Frameworks/Foundation.framework/Foundation
./a.out
2020-11-24 22:26:52.729 a.out[31482:350561] just try

dyld动态链接

生成可执行文件后就是在启动时进行动态链接了,进行符号和地址的绑定。首先会加载所依赖的 dylibs,修正地址偏移,因为 iOS 会用 ASLR 来做地址偏移避免攻击,确定 Non-Lazy Pointer 地址进行符号地址绑定,加载所有类和分类,最后执行 load 方法和 clang attribute 的 constructor 修饰函数。

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

符号表会规定它们的符号,使用 nm 工具看看

xcrun nm -nm SayHi.o

                 (undefined) external _OBJC_CLASS_$_A
                 (undefined) external _objc_alloc_init
                 (undefined) external _objc_autoreleasePoolPop
                 (undefined) external _objc_autoreleasePoolPush
                 (undefined) external _objc_msgSend
0000000000000000 (__TEXT,__text) external _main
0000000000000060 (__DATA,__objc_classrefs) non-external _OBJC_CLASSLIST_REFERENCES_$_
0000000000000078 (__DATA,__objc_selrefs) non-external _OBJC_SELECTOR_REFERENCES_
  • OBJC_CLASS_$_A 表示 A 的 OC 符号。
  • (undefined) external 表示未实现非私有,如果是私有就是 non-external。
  • external _main 表示 main() 函数,处理 0 地址,将要到 __TEXT,__text section

再看看 A

xcrun nm -nm A.o

                 (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 -[A doSomething]
0000000000000058 (__DATA,__objc_const) non-external __OBJC_METACLASS_RO_$_A
00000000000000a0 (__DATA,__objc_const) non-external __OBJC_$_INSTANCE_METHODS_A
00000000000000c0 (__DATA,__objc_const) non-external __OBJC_CLASS_RO_$_A
0000000000000108 (__DATA,__objc_data) external _OBJC_METACLASS_$_A
0000000000000130 (__DATA,__objc_data) external _OBJC_CLASS_$_A

因为 undefined 符号表示该文件类未实现的,所以在目标文件和 Fundation framework 动态库做链接处理时,链接器会尝试解析所有的 undefined 符号。

链接器通过动态库解析成符号会记录是通过哪个动态库解析的,路径也会一起记录。对比下 a.out 符号表看看是怎么解析符号的。

xcrun nm -nm a.out

                 (undefined) external _NSLog (from Foundation)
                 (undefined) external _OBJC_CLASS_$_NSObject (from libobjc)
                 (undefined) external _OBJC_METACLASS_$_NSObject (from libobjc)
                 (undefined) external ___CFConstantStringClassReference (from CoreFoundation)
                 (undefined) external __objc_empty_cache (from libobjc)
                 (undefined) external _objc_alloc_init (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
0000000100003eb0 (__TEXT,__text) non-external -[A doSomething]
0000000100003ee0 (__TEXT,__text) external _main
0000000100008020 (__DATA,__objc_const) non-external __OBJC_METACLASS_RO_$_A
0000000100008068 (__DATA,__objc_const) non-external __OBJC_$_INSTANCE_METHODS_A
0000000100008088 (__DATA,__objc_const) non-external __OBJC_CLASS_RO_$_A
00000001000080e0 (__DATA,__objc_data) external _OBJC_METACLASS_$_A
0000000100008108 (__DATA,__objc_data) external _OBJC_CLASS_$_A
0000000100008130 (__DATA,__data) non-external __dyld_private

undefined 的符号,有了更多信息,可以知道在哪个动态库能够找到。

通过 otool 可以找到所需库在哪

xcrun otool -L a.out

a.out:
    /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation (compatibility version 300.0.0, current version 1677.104.0)
    /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1281.100.1)
    /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 150.0.0, current version 1677.104.0)
    /usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)

libSystem 里有很多我们熟悉的lib

  • libdispatch:GCD
  • libsystem_c:C语言库
  • libsystem_blocks:Block
  • libcommonCrypto:加密,比如md5

动态链接编辑器

dylib 这种格式的表示是动态链接的,编译的时候不会被编译到执行文件中,在程序执行的时候才 link,这样就不用算到包的大小里,而且也能够不更新执行程序就能够更新库。

有一些环境变量对于 dyld 的输出信息非常有用。首先,如果设置了 DYLD_PRINT_LIBRARIES,那么 dyld 将会打印出什么库被加载了:

(export DYLD_PRINT_LIBRARIES=; ./a.out )

dyld: loaded: <69390B5E-90A6-326E-B0E6-C124932BC142> /Users/aibo/Desktop/OC/OC/./a.out
dyld: loaded: <7C69F845-F651-3193-8262-5938010EC67D> /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation
dyld: loaded: <0A6C8BA1-30FD-3D10-83FD-FF29E221AFFE> /usr/lib/libSystem.B.dylib
dyld: loaded: <C0D70026-EDBE-3CBD-B317-367CF4F1C92F> 
...

数数还挺多的,因为 Fundation 还会依赖一些其它的动态库,其它的库还会再依赖更多的库,这样相互依赖的符号会很多,需要处理的时间也会比较长,这里系统上的动态链接器会使用共享缓存,共享缓存在 /var/db/dyld/。当加载 Mach-O 文件时动态链接器会先检查共享内存是否有。每个进程都会在自己地址空间映射这些共享缓存,这样可以优化启动速度。

dyld 做了些什么事

  • kernel 做启动程序初始准备,开始由dyld负责。
  • 基于非常简单的原始栈为 kernel 设置进程来启动自身。
  • 使用共享缓存来处理递归依赖带来的性能问题,ImageLoader 会读取二进制文件,其中包含了我们的类,方法等各种符号。
  • 立即绑定 non-lazy 的符号并设置用于 lazy bind 的必要表,将这些库 link 到执行文件里。
  • 为可执行文件运行静态初始化。
  • 设置参数到可执行文件的 main 函数并调用它。
  • 在执行期间,通过绑定符号处理对 lazily-bound 符号存根的调用提供 runtime 动态加载服务,并为gdb和其它调试器提供钩子以获得关键信息。runtime 会调用 map_images 做解析和处理,load_images 来调用 call_load_methods 方法遍历所有加载了的 Class,按照继承层级依次调用 +load 方法。
  • 在 mian 函数返回后运行 static terminator。
  • 在某些情况下,一旦 main 函数返回,就需要调用 libSystem 的 _exit。
    在 +load 方法里断点查看走到这里调用的堆栈如下:
0  +[someclass load]
1  call_class_loads()
2  ::call_load_methods
3  ::load_images(const char *path __unused, const struct mach_header *mh)
4  dyld::notifySingle(dyld_image_states, ImageLoader const*, ImageLoader::InitializerTimingList*)
11 _dyld_start

prepare_load_methods 这个方法会获取所有类的列表然后收集其中的 +load 方法,在代码里可以发现 Class 的 +load 是先执行的,然后执行 Category 的。为什么这样做,原因可以通过 prepare_load_methods 这个方法看出,在遍历 Class 的 +load 方法时会执行 schedule_class_load 这个方法,这个方法会递归到根节点来满足 Class 收集完整关系树的需求。

最后 call_load_methods 会创建一个 autoreleasePool 使用函数指针来动态调用类和 Category 的 +load 方法。

未完待续~

这篇文章其实主要是学习笔记,后续会精简。

参考:
深入剖析-iOS-编译-Clang---LLVM
Build 过程
编译器
Mach-O 可执行文件

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容

  • 引言 维基百科:编译语言(英语:Compiled language)是一种以编译器来实现的编程语言。它不像解释型语...
    Flame_Dream阅读 8,553评论 5 52
  • 前言 语言类型 我们有很多维度可以将计算机语言进行分类,其中以编译/执行方式为维度,可以将计算机语言分为: 编译型...
    AiLearn阅读 2,393评论 1 6
  • 编译器 iOS编译和打包时,编译器直接将代码编译成机器码,然后直接在CPU上运行。而不用使用解释器运行代码。因为这...
    shawnr阅读 6,828评论 1 22
  • 前言 一般可以将编程语言分为两种,编译语言和直译式语言。 像C++,Objective C都是编译语言。编译语言在...
    一川烟草i蓑衣阅读 10,340评论 1 27
  • iOS学习之深入理解程序编译过程 https://juejin.im/post/5a352bb0f265da433...
    111浪子111阅读 1,685评论 0 2