iOS编译过程
Xcode编译器发展史
Xcode3 以前: GCC;
Xcode3: 增加LLVM,GCC(前端) + LLVM(后端);
Xcode4.2: 出现Clang - LLVM 3.0成为默认编译器;
Xcode4.6: LLVM 升级到4.2版本;
Xcode5: GCC被废弃,新的编译器是LLVM 5.0,从GCC过渡到Clang-LLVM的时代正式完成
1.1.为什么苹果的Xcode会使用Clang+LLVM取代GCC?
GCC最初是作为CNU(GNU是“GNU is Not Unix”)操作系统的编译器编写的,是一套由 GNU 开发的编程语言编译器,不属于苹果维护也不能完全控制开发进程,Apple为Objective-C增加许多新特性,但是GCC开发者对这些支持却不友好;Apple需要做模块化,GCC开发者却拖着迟迟不实现。
相比于 Xcode 5 版本前使用的 GCC,编译速度提高了 3 倍
1.2.GCC被取代的历史必然
随着Apple对其IDE(也就是Xcode)性能的要求越来越高,苹果公司需要找到一个可以控制的编译器。而在在科技的历史长河中,LLVM项目于2000年在伊利诺伊大学厄巴纳 - 香槟分校开始,由Vikram Adve和Chris Lattner领导。 LLVM最初是作为研究基础设施开发的,用于研究静态和动态编程语言的动态编译技术。 LLVM是根据o大学/ NCSA开源许可证发布的,是一个许可的免费软件许可证。巴巴要
1.3.GCC被取代
2005年,Apple Inc.聘请了Lattner并组建了一个团队,致力于LLVM系统,以便在Apple的开发系统中实现各种用途。 LLVM是Apple最新的macOS和iOS开发工具中不可或缺的一部分。
LLVM编译器的组成
在基于LLVM的编译器中,前端负责解析,验证和诊断输入代码中的错误,然后将解析的代码转换为LLVM IR(通常情况。但是也有例外,通过构建AST然后将AST转换为LLVM IR)。该IR可选地通过一系列改进代码的分析和优化过程提供,然后被发送到代码生成器以生成本机机器代码,如图下图所示。
LLVM采用的是三相设计
它将整个编译过程分类了三个模块:前端、公用优化器、后端。
前端:对目标语言代码进行语法分析,语义分析,生成中间代码。在这个过程中,会进行类型检查,如果发现错误或者警告会标注出来在哪一行。我们在开发的过程中,其实 Xcode 也会使用前端工具对你的代码进行分析,并实时的检查出来某些错误。前端是针对特定语言的,如果需要一个新的语言被编译,只需要再写一个针对新语言的前端模块即可。
公用优化器:将生成的中间文件进行优化,去除冗余代码,进行结构优化。
后端:后段将优化后的中间代码再次转换,变成汇编语言,并再次进行优化,最后将各个文件代码转换为机器代码并链接。链接是指将不同代码文件编译后的不同机器代码文件合并成一个可执行文件。
为什么要使用三相设计? 优势在哪?
首先解决了一个很大的问题:假如有N种语言(C、OC、C++、Swift...)的前端,同时也有M个架构(模拟器、arm64、x86...)的Target,是否就需要 N × M 个编译器?
三相架构的价值就体现出来了,通过共享优化器的中转,很好的解决了这个问题。
假如你需要增加一种语言,只需要增加一种前端;假如你需要增加一种处理器架构,也只需要增加一种后端,而其他的地方都不需要改动。这复用思想很牛逼吧。
前后端依赖统一格式的中间代码(IR),使得前后端可以独立的变化。新增一门语言只需要修改前端,而新增一个CPU架构只需要修改后端即可。
Objective C/C/C++使用的编译器前端是clang,swift是swift,后端都是LLVM。
LLVM项目是模块化和可重用的编译器和工具链技术的集合。LLVM主要的子项目有一下几个:
1.LLVM核心库:
LLVM提供一个独立的链接代码优化器为许多流行CPU(以及一些不太常见的CPU)的代码生成支持。这些库是围绕一个指定良好的代码表示构建的,称为LLVM中间表示(“LLVM IR”)。LLVM还可以充当JIT编译器 - 它支持x86 / x86_64和PPC / PPC64程序集生成,并具有针对编译速度的快速代码优化。
2.LLVM IR 生成器Clang:
Clang是一个“LLVM原生”C / C ++ / Objective-C编译器,旨在提供惊人的快速编译(例如,在调试配置中编译Objective-C代码时比GCC快3倍),非常有用的错误和警告消息以及提供构建优秀源代码工具的平台。
3.LLDB项目:
LLDB项目以LLVM和Clang提供的库为基础,提供了一个出色的本机调试器。它使用Clang AST和表达式解析器,LLVM JIT,LLVM反汇编程序等,以便提供“正常工作”的体验。在加载符号时,它也比GDB快速且内存效率更高。
4.libc ++和libc++:
libc ++和libc++ ABI项目提供了C ++标准库的标准符合性和高性能实现,包括对C ++ 11的完全支持。
5.lld项目:
lld项目旨在成为clang / llvm的内置链接器。目前,clang必须调用系统链接器来生成可执行文件。
*其他的就不再详细介绍了,详情可以参考( *LLVM 和 Clang )
总之,LLVM是Apple主导的开源框架,并提供一套使用于Apple平台的LLVM编译器,同时提供优秀的性能,所以Apple采用LLVM的方式进行编译
LVVM 的作者写了一篇关于什么是 LLVM 的文章,详细的描述了 LLVM 的使用的技术点:LLVM。
编译过程
这里,这里先简单总结一下编译的几个主要过程:
首先,你写好代码后,LLVM 会预处理你的代码,比如把宏嵌入到对应的位置。
预处理完后,LLVM 会对代码进行词法分析和语法分析,生成 AST 。AST 是抽象语法树,结构上比代码更精简,遍历起来更快,所以使用 AST 能够更快速地进行静态检查,同时还能更快地生成 IR(中间表示)。
最后 AST 会生成 IR,IR 是一种更接近机器码的语言,区别在于和平台无关,通过 IR 可以生成多份适合不同平台的机器码。对于 iOS 系统,IR 生成的可执行文件就是 Mach-O。
查看编译步骤命令
clang -ccc-print-phases main.m
编译步骤
+- 0: input, "main.m", objective-c //源码
+- 1: preprocessor, {0}, objective-c-cpp-output //预处理 编译器前端
+- 2: compiler, {1}, ir //ir 编译生成中间码IR
+- 3: backend, {2}, assembler //assembler LLVM后端生成汇编
+- 4: assembler, {3}, object //object 生成机器码
+- 5: linker, {4}, image //image 链接器
6: bind-arch, "x86_64", {5}, image //image 生成image,最后生成可执行二进制文件(image这里是镜像文件)
预处理(preprocessor)
预处理会替进行头文件引入,宏替换,注释处理,条件编译(#ifdef)等操作。
#import "Test2.h"就是告诉预处理器将这一行替换成头文件Test2.h中的内容,这个过程是递归的:因为Test2.h也有可能包含其头文件。
源代码:
#import "Test3.h"
#import "Test2.h"
#include "Test4.h"
#include "Test4.h"
#import "Test5.h"
#include "Test5.h"
#define KKYes @"12345"
@implementation Test3
- (void)test1{
NSString *str = KKYes;
NSDictionary *dict = @{};
int a = 2;
bool b = YES;
}
@end
用clang查看预处理的结果:
# 编译阶段选择参数: -E 运行预处理这一步
xcrun clang -E main.m
# 引入UIKit等框架报错
xcrun -sdk iphonesimulator clang -E main.m
# 预处理结果输出到main.mi文件中
clang -E main.m -o main.mi
最终C输出.i文件,C++输出.ii文件,Objective-C输出.mi文件,Objective-C ++输出.mii文件。
预处理后代码:
# 1 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/Foundation.framework/Headers/FoundationLegacySwiftCompatibility.h" 1 3
# 187 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/Foundation.framework/Headers/Foundation.h" 2 3
# 9 "./Test3.h" 2
#pragma clang assume_nonnull begin
@interface Test3 : NSObject
@end
#pragma clang assume_nonnull end
# 9 "test3.m" 2
# 1 "./Test2.h" 1
# 10 "./Test2.h"
# 1 "./Test1.h" 1
# 10 "./Test1.h"
#pragma clang assume_nonnull begin
@interface Test1 : NSObject
@property(nonatomic,copy)NSString *names;
- (void)ttest1;
@end
#pragma clang assume_nonnull end
# 11 "./Test2.h" 2
@interface Test2 : NSObject
@property(copy)NSString *test1;
@end
# 10 "test3.m" 2
# 1 "./Test4.h" 1
# 10 "./Test4.h"
#pragma clang assume_nonnull begin
@interface Test4 : NSObject
@property(nonatomic,strong)NSString *test1;
@end
#pragma clang assume_nonnull end
# 11 "test3.m" 2
# 1 "./Test4.h" 1
# 10 "./Test4.h"
#pragma clang assume_nonnull begin
@interface Test4 : NSObject
@property(nonatomic,strong)NSString *test1;
@end
#pragma clang assume_nonnull end
# 12 "test3.m" 2
# 1 "./Test5.h" 1
# 10 "./Test5.h"
#pragma clang assume_nonnull begin
@interface Test5 : NSObject
@end
#pragma clang assume_nonnull end
# 13 "test3.m" 2
@implementation Test3
- (void)test1{
NSString *str = [NSString stringWithFormat:@"%d",123];;
NSDictionary *dict = @{};
int a= 2 ;
if (0) {
NSLog(@"456");
}
a = 3;
int b = 3;
int c = a + b;
[self test2];
}
- (void)test2{
}
@end
预处理的任务:
- 将输入文件读到内存,并断行;
- 替换注释为单个空格;
Tokenization将输入转换为一系列预处理Tokens;- 处理
#import、#include将所引的库,以递归的方式,插入到#import或#include所在的位置; - 替换宏定义;
- 条件编译,根据条件包括或排除程序代码的某些部分;
- 插入行标记;
在预处理的输出中,源文件名和行号信息会以# linenum filename flags形式传递,这被称为行标记,代表着接下来的内容开始于源文件filename的第linenum行,而flags则会有0或者多个,有1、2、3、4;如果有多个flags时,彼此使用分号隔开。详见此处。
每个标识的表示内容如下:
1表示一个新文件的开始2表示返回文件(包含另一个文件后)3表示以下文本来自系统头文件,因此应禁止某些警告4表示应将以下文本视为包装在隐式extern "C"块中。
比如# 9 "test3.m" 2,表示导入Test3.h文件后回到Test3.m文件的第9行。
比如# 1 "./Test2.h" 1,表示进入新文件Test2.h文件后的第1行。
#import 对比 #include 可以避免重复导入,#include "Test4.h"两次,头文件内容重复导入两次
编译阶段
词法分析(lexical anaysis)
词法分析的整个过程,主要是按照:标识符、 数字、字符串文字、 标点符号,将我们的代码分割成许多字符串序列,其中每个元素我们称之为Token,整个过程称为Tokenization。
clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
-fmodules:启用“模块”语言功能。关于Modules特性,详见此处,大意为使用import代替include,编译速度快。
-fsyntax-only:运行预处理器,解析器和类型检查阶段。
-Xclang <arg>:传递参数到clang的编译器。
dump-tokens:运行预处理器,转储Token的内部表示。
更多关于Clang参数的描述,请前往此处。
词法分析后代码:
at '@' [StartOfLine] Loc=<./Test5.h:12:1>
identifier 'interface' Loc=<./Test5.h:12:2>
identifier 'Test5' [LeadingSpace] Loc=<./Test5.h:12:12>
colon ':' [LeadingSpace] Loc=<./Test5.h:12:18>
identifier 'NSObject' [LeadingSpace] Loc=<./Test5.h:12:20>
at '@' [StartOfLine] Loc=<./Test5.h:14:1>
identifier 'end' Loc=<./Test5.h:14:2>
at '@' [StartOfLine] Loc=<test3.m:15:1>
identifier 'implementation' Loc=<test3.m:15:2>
identifier 'Test3' [LeadingSpace] Loc=<test3.m:15:17>
minus '-' [StartOfLine] Loc=<test3.m:16:1>
l_paren '(' [LeadingSpace] Loc=<test3.m:16:3>
void 'void' Loc=<test3.m:16:4>
r_paren ')' Loc=<test3.m:16:8>
identifier 'test1' Loc=<test3.m:16:9>
l_brace '{' Loc=<test3.m:16:14>
identifier 'NSString' [StartOfLine] [LeadingSpace] Loc=<test3.m:17:5>
star '*' [LeadingSpace] Loc=<test3.m:17:14>
identifier 'str' Loc=<test3.m:17:15>
equal '=' [LeadingSpace] Loc=<test3.m:17:19>
l_square '[' [LeadingSpace] Loc=<test3.m:17:21 <Spelling=test3.m:14:18>>
identifier 'NSString' Loc=<test3.m:17:21 <Spelling=test3.m:14:19>>
identifier 'stringWithFormat' [LeadingSpace] Loc=<test3.m:17:21 <Spelling=test3.m:14:28>>
colon ':' Loc=<test3.m:17:21 <Spelling=test3.m:14:44>>
at '@' Loc=<test3.m:17:21 <Spelling=test3.m:14:45>>
string_literal '"%d"' Loc=<test3.m:17:21 <Spelling=test3.m:14:46>>
comma ',' Loc=<test3.m:17:21 <Spelling=test3.m:14:50>>
numeric_constant '123' Loc=<test3.m:17:21 <Spelling=test3.m:17:27>>
r_square ']' Loc=<test3.m:17:21 <Spelling=test3.m:14:52>>
semi ';' Loc=<test3.m:17:21 <Spelling=test3.m:14:53>>
semi ';' Loc=<test3.m:17:31>
identifier 'NSDictionary' [StartOfLine] [LeadingSpace] Loc=<test3.m:18:5>
star '*' [LeadingSpace] Loc=<test3.m:18:18>
identifier 'dict' Loc=<test3.m:18:19>
equal '=' [LeadingSpace] Loc=<test3.m:18:24>
at '@' [LeadingSpace] Loc=<test3.m:18:26>
l_brace '{' Loc=<test3.m:18:27>
r_brace '}' Loc=<test3.m:18:28>
semi ';' Loc=<test3.m:18:29>
int 'int' [StartOfLine] [LeadingSpace] Loc=<test3.m:19:5>
identifier 'a' [LeadingSpace] Loc=<test3.m:19:9>
equal '=' Loc=<test3.m:19:10>
numeric_constant '2' [LeadingSpace] Loc=<test3.m:19:17>
semi ';' [LeadingSpace] Loc=<test3.m:19:25>
if 'if' [StartOfLine] [LeadingSpace] Loc=<test3.m:20:5>
l_paren '(' [LeadingSpace] Loc=<test3.m:20:8>
numeric_constant '0' Loc=<test3.m:20:9>
r_paren ')' Loc=<test3.m:20:10>
l_brace '{' [LeadingSpace] Loc=<test3.m:20:12>
identifier 'NSLog' [StartOfLine] [LeadingSpace] Loc=<test3.m:21:9>
l_paren '(' Loc=<test3.m:21:14>
at '@' Loc=<test3.m:21:15>
string_literal '"456"' Loc=<test3.m:21:16>
r_paren ')' Loc=<test3.m:21:21>
semi ';' Loc=<test3.m:21:22>
r_brace '}' [StartOfLine] [LeadingSpace] Loc=<test3.m:22:5>
identifier 'a' [StartOfLine] [LeadingSpace] Loc=<test3.m:26:5>
equal '=' [LeadingSpace] Loc=<test3.m:26:7>
numeric_constant '3' [LeadingSpace] Loc=<test3.m:26:9>
semi ';' Loc=<test3.m:26:10>
int 'int' [StartOfLine] [LeadingSpace] Loc=<test3.m:28:5>
identifier 'b' [LeadingSpace] Loc=<test3.m:28:9>
equal '=' [LeadingSpace] Loc=<test3.m:28:11>
numeric_constant '3' [LeadingSpace] Loc=<test3.m:28:13>
semi ';' Loc=<test3.m:28:14>
int 'int' [StartOfLine] [LeadingSpace] Loc=<test3.m:29:5>
identifier 'c' [LeadingSpace] Loc=<test3.m:29:9>
equal '=' [LeadingSpace] Loc=<test3.m:29:11>
identifier 'a' [LeadingSpace] Loc=<test3.m:29:13>
plus '+' [LeadingSpace] Loc=<test3.m:29:15>
identifier 'b' [LeadingSpace] Loc=<test3.m:29:17>
semi ';' Loc=<test3.m:29:18>
l_square '[' [StartOfLine] [LeadingSpace] Loc=<test3.m:30:5>
identifier 'self' Loc=<test3.m:30:6>
identifier 'test2' [LeadingSpace] Loc=<test3.m:30:11>
r_square ']' Loc=<test3.m:30:16>
semi ';' Loc=<test3.m:30:17>
r_brace '}' [StartOfLine] Loc=<test3.m:31:1>
minus '-' [StartOfLine] Loc=<test3.m:32:1>
l_paren '(' [LeadingSpace] Loc=<test3.m:32:3>
void 'void' Loc=<test3.m:32:4>
r_paren ')' Loc=<test3.m:32:8>
identifier 'test2' Loc=<test3.m:32:9>
l_brace '{' Loc=<test3.m:32:14>
r_brace '}' [StartOfLine] Loc=<test3.m:34:1>
at '@' [StartOfLine] Loc=<test3.m:35:1>
identifier 'end' Loc=<test3.m:35:2>
eof '' Loc=<test3.m:35:5>
词法分析中Token包含信息(详请见此处):
Sourece Location:表示Token开始的位置,比如:Loc= <./Test3.h:12:1>;Token Kind:表示Token的类型,比如:identifier、string_literal;Flags:词法分析器和处理器跟踪每个Token的基础,目前有四个Flag分别是:
StartOfLine:表示这是每行开始的第一个Token;LeadingSpace:标记空格。DisableExpand:该标志在预处理器内部使用,用来表示identifier令牌禁用宏扩展。NeedsCleaning:如果令牌的原始拼写包含三字符组或转义的换行符,则设置此标志。
这个命令的作用是,显示每个Token的类型、值,以及位置。参考该链接,可以看到Clang定义的所有Token类型。 可以分为下面这4类:
- 关键字:语法中的关键字,比如 if、else、while、for 等;
- 标识符:变量名;
- 字面量:值、数字、字符串;
- 特殊符号:加减乘除等符号。
语法分析,输出抽象树(AST)
此阶段对输入文件进行语法分析,将预处理器生成的Tokens转换为语法分析树;一旦生成语法分析树后,将会进行语义分析,执行类型检查和代码格式检查。这个阶段负责生成大多数编译器警告以及语法分析过程的错误。最终输出AST(抽象语法树)。
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
TranslationUnitDecl是根节点,表示一个编译单元;Decl表示一个声明;Expr表示的是表达式;Literal表示字面量,是一个特殊的Expr;Stmt表示陈述。
除此之外,Clang还有众多种类的节点类型。Clang里,节点主要分成Type类型、Decl声明、Stmt陈述这三种,其他的都是这三种的派生。通过扩展这三类节点,就能够将无限的代码形态用有限的形式来表现出来。
语义分析后代码:
In file included from test3.m:11:
./Test4.h:12:1: error: duplicate interface definition for class 'Test4'
@interface Test4 : NSObject
^
./Test4.h:12:12: note: previous definition is here
@interface Test4 : NSObject
^
In file included from test3.m:11:
./Test4.h:13:38: error: property has a previous declaration
@property(nonatomic,strong)NSString *test1;
^
./Test4.h:13:38: note: property declared here
@property(nonatomic,strong)NSString *test1;
^
TranslationUnitDecl 0x7f7f1e03d008 <<invalid sloc>> <invalid sloc> <undeserialized declarations>
|-TypedefDecl 0x7f7f1e03d8c0 <<invalid sloc>> <invalid sloc> implicit __int128_t '__int128'
| `-BuiltinType 0x7f7f1e03d5a0 '__int128'
|-TypedefDecl 0x7f7f1e03d930 <<invalid sloc>> <invalid sloc> implicit __uint128_t 'unsigned __int128'
| `-BuiltinType 0x7f7f1e03d5c0 'unsigned __int128'
|-TypedefDecl 0x7f7f1e03d9d8 <<invalid sloc>> <invalid sloc> implicit SEL 'SEL *'
| `-PointerType 0x7f7f1e03d990 'SEL *' imported
| `-BuiltinType 0x7f7f1e03d800 'SEL'
|-TypedefDecl 0x7f7f1e03dab8 <<invalid sloc>> <invalid sloc> implicit id 'id'
| `-ObjCObjectPointerType 0x7f7f1e03da60 'id' imported
| `-ObjCObjectType 0x7f7f1e03da30 'id' imported
|-TypedefDecl 0x7f7f1e03db98 <<invalid sloc>> <invalid sloc> implicit Class 'Class'
| `-ObjCObjectPointerType 0x7f7f1e03db40 'Class' imported
| `-ObjCObjectType 0x7f7f1e03db10 'Class' imported
|-ObjCInterfaceDecl 0x7f7f1e03dbf0 <<invalid sloc>> <invalid sloc> implicit Protocol
|-TypedefDecl 0x7f7f1e03df90 <<invalid sloc>> <invalid sloc> implicit __NSConstantString 'struct __NSConstantString_tag'
| `-RecordType 0x7f7f1e03dd60 'struct __NSConstantString_tag'
| `-Record 0x7f7f1e03dcc0 '__NSConstantString_tag'
|-TypedefDecl 0x7f7f1e811c48 <<invalid sloc>> <invalid sloc> implicit __builtin_ms_va_list 'char *'
| `-PointerType 0x7f7f1e811c00 'char *'
| `-BuiltinType 0x7f7f1e03d0a0 'char'
|-TypedefDecl 0x7f7f1e811f58 <<invalid sloc>> <invalid sloc> implicit __builtin_va_list 'struct __va_list_tag [1]'
| `-ConstantArrayType 0x7f7f1e811f00 'struct __va_list_tag [1]' 1
| `-RecordType 0x7f7f1e811d40 'struct __va_list_tag'
| `-Record 0x7f7f1e811ca0 '__va_list_tag'
|-ImportDecl 0x7f7f2017ed98 <./Test3.h:8:1> col:1 implicit Foundation
|-ObjCInterfaceDecl 0x7f7f1f066bf8 <line:12:1, line:14:2> line:12:12 Test3
| |-super ObjCInterface 0x7f7f2017ee98 'NSObject'
| `-ObjCImplementation 0x7f7f1f0fc708 'Test3'
|-ImportDecl 0x7f7f1f066d28 <./Test2.h:8:1> col:1 implicit Foundation
|-ImportDecl 0x7f7f1f066d68 <./Test1.h:8:1> col:1 implicit Foundation
|-ObjCInterfaceDecl 0x7f7f1f066da8 <line:12:1, line:15:2> line:12:12 Test1
| |-super ObjCInterface 0x7f7f2017ee98 'NSObject'
| |-ObjCPropertyDecl 0x7f7f1e95d4e0 <line:13:1, col:36> col:36 names 'NSString * _Nonnull':'NSString *' readwrite copy nonatomic
| |-ObjCMethodDecl 0x7f7f1e95d620 <line:14:1, col:15> col:1 - ttest1 'void'
| |-ObjCMethodDecl 0x7f7f1e95d7c8 <line:13:36> col:36 implicit - names 'NSString * _Nonnull':'NSString *'
| `-ObjCMethodDecl 0x7f7f1e95d928 <col:36> col:36 implicit - setNames: 'void'
| `-ParmVarDecl 0x7f7f1e95d9b8 <col:36> col:36 names 'NSString * _Nonnull':'NSString *'
|-ObjCInterfaceDecl 0x7f7f1e09af10 <./Test2.h:12:1, line:18:2> line:12:12 Test2
| |-super ObjCInterface 0x7f7f2017ee98 'NSObject'
| |-ObjCPropertyDecl 0x7f7f1e09b060 <line:13:1, col:26> col:26 test1 'NSString *' readwrite copy atomic
| |-ObjCMethodDecl 0x7f7f1e09b0e0 <col:26> col:26 implicit - test1 'NSString *'
| `-ObjCMethodDecl 0x7f7f1e09b268 <col:26> col:26 implicit - setTest1: 'void'
| `-ParmVarDecl 0x7f7f1e09b2f8 <col:26> col:26 test1 'NSString *'
|-ImportDecl 0x7f7f1e09b438 <./Test4.h:8:1> col:1 implicit Foundation
|-ObjCInterfaceDecl 0x7f7f1e09b478 <line:12:1, line:14:2> line:12:12 Test4
| |-super ObjCInterface 0x7f7f2017ee98 'NSObject'
| |-ObjCPropertyDecl 0x7f7f1e09b5f0 <line:13:1, col:38> col:38 test1 'NSString * _Nonnull':'NSString *' readwrite nonatomic strong
| |-ObjCMethodDecl 0x7f7f1e09b670 <col:38> col:38 implicit - test1 'NSString * _Nonnull':'NSString *'
| `-ObjCMethodDecl 0x7f7f1e09b7f8 <col:38> col:38 implicit - setTest1: 'void'
| `-ParmVarDecl 0x7f7f1e09b888 <col:38> col:38 test1 'NSString * _Nonnull':'NSString *'
|-ImportDecl 0x7f7f1f0fc400 <line:8:1> col:1 implicit Foundation
|-ObjCInterfaceDecl 0x7f7f1f0fc440 prev 0x7f7f1e09b478 <line:12:1, col:12> col:12 invalid Test4
| `-super ObjCInterface 0x7f7f2017ee98 'NSObject'
|-ImportDecl 0x7f7f1f0fc598 <./Test5.h:8:1> col:1 implicit Foundation
|-ObjCInterfaceDecl 0x7f7f1f0fc5d8 <line:12:1, line:14:2> line:12:12 Test5
| `-super ObjCInterface 0x7f7f2017ee98 'NSObject'
`-ObjCImplementationDecl 0x7f7f1f0fc708 <test3.m:15:1, line:35:1> line:15:17 Test3
|-ObjCInterface 0x7f7f1f066bf8 'Test3'
|-ObjCMethodDecl 0x7f7f1f0fc7a0 <line:16:1, line:31:1> line:16:1 - test1 'void'
| |-ImplicitParamDecl 0x7f7f1f103588 <<invalid sloc>> <invalid sloc> implicit used self 'Test3 *'
| |-ImplicitParamDecl 0x7f7f1f104400 <<invalid sloc>> <invalid sloc> implicit _cmd 'SEL':'SEL *'
| |-VarDecl 0x7f7f1f104480 <line:17:5, line:14:52> line:17:15 str 'NSString *' cinit
| | `-ObjCMessageExpr 0x7f7f1f104918 <line:14:18, col:52> 'NSString * _Nonnull':'NSString *' selector=stringWithFormat: class='NSString'
| | |-ObjCStringLiteral 0x7f7f1f104558 <col:45, col:46> 'NSString *'
| | | `-StringLiteral 0x7f7f1f104538 <col:46> 'char [3]' lvalue "%d"
| | `-IntegerLiteral 0x7f7f1f104578 <line:17:27> 'int' 123
| |-VarDecl 0x7f7f1f1049c0 <line:18:5, col:28> col:19 dict 'NSDictionary *' cinit
| | `-ObjCDictionaryLiteral 0x7f7f1f105328 <col:26, col:28> 'NSDictionary *'
| |-VarDecl 0x7f7f1f105380 <line:19:5, col:17> col:9 used a 'int' cinit
| | `-IntegerLiteral 0x7f7f1e96a000 <col:17> 'int' 2
| |-VarDecl 0x7f7f1e96a280 <line:28:5, col:13> col:9 used b 'int' cinit
| | `-IntegerLiteral 0x7f7f1e96a2e8 <col:13> 'int' 3
| |-VarDecl 0x7f7f1e96a338 <line:29:5, col:17> col:9 c 'int' cinit
| | `-BinaryOperator 0x7f7f1e96a428 <col:13, col:17> 'int' '+'
| | |-ImplicitCastExpr 0x7f7f1e96a3f8 <col:13> 'int' <LValueToRValue>
| | | `-DeclRefExpr 0x7f7f1e96a3a0 <col:13> 'int' lvalue Var 0x7f7f1f105380 'a' 'int'
| | `-ImplicitCastExpr 0x7f7f1e96a410 <col:17> 'int' <LValueToRValue>
| | `-DeclRefExpr 0x7f7f1e96a3c0 <col:17> 'int' lvalue Var 0x7f7f1e96a280 'b' 'int'
| `-CompoundStmt 0x7f7f1e9723b8 <line:16:14, line:31:1>
| |-DeclStmt 0x7f7f1f104958 <line:17:5, line:14:53>
| | `-VarDecl 0x7f7f1f104480 <line:17:5, line:14:52> line:17:15 str 'NSString *' cinit
| | `-ObjCMessageExpr 0x7f7f1f104918 <line:14:18, col:52> 'NSString * _Nonnull':'NSString *' selector=stringWithFormat: class='NSString'
| | |-ObjCStringLiteral 0x7f7f1f104558 <col:45, col:46> 'NSString *'
| | | `-StringLiteral 0x7f7f1f104538 <col:46> 'char [3]' lvalue "%d"
| | `-IntegerLiteral 0x7f7f1f104578 <line:17:27> 'int' 123
| |-NullStmt 0x7f7f1f104970 <col:31>
| |-DeclStmt 0x7f7f1f105350 <line:18:5, col:29>
| | `-VarDecl 0x7f7f1f1049c0 <col:5, col:28> col:19 dict 'NSDictionary *' cinit
| | `-ObjCDictionaryLiteral 0x7f7f1f105328 <col:26, col:28> 'NSDictionary *'
| |-DeclStmt 0x7f7f1e96a020 <line:19:5, col:25>
| | `-VarDecl 0x7f7f1f105380 <col:5, col:17> col:9 used a 'int' cinit
| | `-IntegerLiteral 0x7f7f1e96a000 <col:17> 'int' 2
| |-IfStmt 0x7f7f1e96a1d0 <line:20:5, line:22:5>
| | |-IntegerLiteral 0x7f7f1e96a038 <line:20:9> 'int' 0
| | `-CompoundStmt 0x7f7f1e96a1b8 <col:12, line:22:5>
| | `-CallExpr 0x7f7f1e96a178 <line:21:9, col:21> 'void'
| | |-ImplicitCastExpr 0x7f7f1e96a160 <col:9> 'void (*)(id, ...)' <FunctionToPointerDecay>
| | | `-DeclRefExpr 0x7f7f1e96a058 <col:9> 'void (id, ...)' Function 0x7f7f1f102ee8 'NSLog' 'void (id, ...)'
| | `-ImplicitCastExpr 0x7f7f1e96a1a0 <col:15, col:16> 'id':'id' <BitCast>
| | `-ObjCStringLiteral 0x7f7f1e96a0d8 <col:15, col:16> 'NSString *'
| | `-StringLiteral 0x7f7f1e96a0b8 <col:16> 'char [4]' lvalue "456"
| |-BinaryOperator 0x7f7f1e96a248 <line:26:5, col:9> 'int' '='
| | |-DeclRefExpr 0x7f7f1e96a1f0 <col:5> 'int' lvalue Var 0x7f7f1f105380 'a' 'int'
| | `-IntegerLiteral 0x7f7f1e96a228 <col:9> 'int' 3
| |-DeclStmt 0x7f7f1e96a308 <line:28:5, col:14>
| | `-VarDecl 0x7f7f1e96a280 <col:5, col:13> col:9 used b 'int' cinit
| | `-IntegerLiteral 0x7f7f1e96a2e8 <col:13> 'int' 3
| |-DeclStmt 0x7f7f1e96a448 <line:29:5, col:18>
| | `-VarDecl 0x7f7f1e96a338 <col:5, col:17> col:9 c 'int' cinit
| | `-BinaryOperator 0x7f7f1e96a428 <col:13, col:17> 'int' '+'
| | |-ImplicitCastExpr 0x7f7f1e96a3f8 <col:13> 'int' <LValueToRValue>
| | | `-DeclRefExpr 0x7f7f1e96a3a0 <col:13> 'int' lvalue Var 0x7f7f1f105380 'a' 'int'
| | `-ImplicitCastExpr 0x7f7f1e96a410 <col:17> 'int' <LValueToRValue>
| | `-DeclRefExpr 0x7f7f1e96a3c0 <col:17> 'int' lvalue Var 0x7f7f1e96a280 'b' 'int'
| `-ObjCMessageExpr 0x7f7f1e96a4b0 <line:30:5, col:16> 'void' selector=test2
| `-ImplicitCastExpr 0x7f7f1e96a498 <col:6> 'Test3 *' <LValueToRValue>
| `-DeclRefExpr 0x7f7f1e96a460 <col:6> 'Test3 *' lvalue ImplicitParam 0x7f7f1f103588 'self' 'Test3 *'
`-ObjCMethodDecl 0x7f7f1f103428 <line:32:1, line:34:1> line:32:1 - test2 'void'
|-ImplicitParamDecl 0x7f7f1e972470 <<invalid sloc>> <invalid sloc> implicit self 'Test3 *'
|-ImplicitParamDecl 0x7f7f1e9724d8 <<invalid sloc>> <invalid sloc> implicit _cmd 'SEL':'SEL *'
`-CompoundStmt 0x7f7f1e972540 <col:14, line:34:1>
2 errors generated.
后面跟了一个内存地址,其实就是栈空间内的函数的地址变量地址(函数也是变量)
CodeGen生成(Intermediate Representation,简称IR)中间代码
上述三个步骤之后就开始告别前端
CodeGen 会负责将语法树自顶向下遍历逐步翻译成 LLVM IR,IR 是编译过程的前端的输出后端的输入。
至此前端工作完成,把IR中间码输入到优化器
at&t
clang -S -fobjc-arc -emit-llvm main.m -o main.ll
生成的IR代码:
暂时无法在文档外展示此内容
; ModuleID = 'test3.m'
source_filename = "test3.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-macosx11.0.0"
define internal void @"\01-[Test3 test1]"(%0* %0, i8* %1) #1 {
%3 = alloca %0*, align 8 //分配一个内存到局部变量%3,8字节对齐
%4 = alloca i8*, align 8 // 分配一个char *的内存(声明一个char *指针变量)到局部变量%4,8字节对齐
%5 = alloca %1*, align 8
%6 = alloca %2*, align 8
%7 = alloca i32, align 4
%8 = alloca i32, align 4
%9 = alloca i32, align 4
store %0* %0, %0** %3, align 8 //将%0*类型的变量%0存到内存%3中, 8字节对齐
store i8* %1, i8** %4, align 8
// 声明%10临时变量为struct._class_t*类型,内容为@"OBJC_CLASSLIST_REFERENCES_$_"
%10 = load %struct._class_t*, %struct._class_t** @"OBJC_CLASSLIST_REFERENCES_$_", align 8
%11 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_, align 8, !invariant.load !9
%12 = bitcast %struct._class_t* %10 to i8* // 将%10的变量强转为char *类型
%13 = call i8* (i8*, i8*, %1*, ...) bitcast (i8* (i8*, i8*, ...)* @objc_msgSend to i8* (i8*, i8*, %1*, ...)*)(i8* %12, i8* %11, %1* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_ to %1*), i32 123)
%14 = notail call i8* @llvm.objc.retainAutoreleasedReturnValue(i8* %13) #3
%15 = bitcast i8* %14 to %1*
store %1* %15, %1** %5, align 8
%16 = call i8* @llvm.objc.retain(i8* bitcast (%struct._class_t** @__NSDictionary0__struct to i8*)) #3
%17 = bitcast i8* %16 to %2*
store %2* %17, %2** %6, align 8
store i32 2, i32* %7, align 4
store i32 3, i32* %7, align 4
store i32 3, i32* %8, align 4
%18 = load i32, i32* %7, align 4
%19 = load i32, i32* %8, align 4
%20 = add nsw i32 %18, %19
store i32 %20, i32* %9, align 4
%21 = load %0*, %0** %3, align 8
%22 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_.2, align 8, !invariant.load !9
%23 = bitcast %0* %21 to i8*
call void bitcast (i8* (i8*, i8*, ...)* @objc_msgSend to void (i8*, i8*)*)(i8* %23, i8* %22)
%24 = bitcast %2** %6 to i8**
call void @llvm.objc.storeStrong(i8** %24, i8* null) #3
%25 = bitcast %1** %5 to i8**
call void @llvm.objc.storeStrong(i8** %25, i8* null) #3
ret void
}
attributes #0 = { "objc_arc_inert" }
attributes #1 = { noinline optnone ssp uwtable "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"="true" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+cx8,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "tune-cpu"="generic" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #2 = { nonlazybind }
attributes #3 = { nounwind }
!llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7}
!llvm.ident = !{!8}
!0 = !{i32 2, !"SDK Version", [2 x i32] [i32 12, i32 1]}
!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 1, !"Objective-C Garbage Collection", i8 0}
!5 = !{i32 1, !"Objective-C Class Properties", i32 64}
!6 = !{i32 1, !"wchar_size", i32 4}
!7 = !{i32 7, !"PIC Level", i32 2}
!8 = !{!"Apple clang version 13.0.0 (clang-1300.0.29.30)"}
!9 = !{}
- 注释以分号 ; 开头
- 全局标识符以@开头,局部标示符以%开头
- alloca,在当前函数栈帧中分配内存
- i32 ,32bit,4 个字节
- align,内存对齐
- store,写入数据
- load,读取数据
- bitcast为类型转换
第2-4行是Target Information
第7-43行是test1函数
第45-48行是函数属性
第50-62行是模块元信息
Target Information每一行分别是:
ModuleID:编译器用于区分不同模块的IDsource_filename:源文件名target datalayout:目标机器架构数据布局target triple:用于描述目标机器信息的一个元组,一般形式是<architecture>-<vendor>-<system>[-extra-info]
需要关注的是target datalayout,它由-分隔的一列规格组成
; ModuleID = 'test3.m'
source_filename = "test3.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-macosx11.0.0"
e:内存存储模式为小端模式m:o:目标文件的格式是Mach格式i64:64:64位整数的对齐方式是64位,即8字节对齐f80:128:80位扩展精度浮点数的对齐方式是128位,即16字节对齐n8:16:32:64:整型数据有8位的、16位的、32位的和64位的S128:128位栈自然对齐
LLVM IR结构
四个具有依次包含关系的基本概念:
- Module(模块)是一份LLVM IR的顶层容器,对应于编译前端的每个翻译单元(TranslationUnit)。每个模块由目标机器信息、全局符号(全局变量和函数)及元信息组成。
- Function(函数)就是编程语言中的函数,包括函数签名和若干个基本块,函数内的第一个基本块叫做入口基本块。
- BasicBlock(基本块)是一组顺序执行的指令集合,只有一个入口和一个出口,非头尾指令执行时不会违背顺序跳转到其他指令上去。每个基本块最后一条指令一般是跳转指令(跳转到其它基本块上去),函数内最后一个基本块的最后条指令是函数返回指令。
- Instruction(指令)是LLVM IR中的最小可执行单位,每一条指令都单占一行
zhuanlan.zhihu.com/p/102716482
Optimize - 优化IR
这里 LLVM 会去做些优化工作,在 Xcode 的编译设置里也可以设置优化级别-01,-03,-0s,还可以写些自己的 Pass。Pass 是 LLVM 优化工作的一个节点,一个节点做些事,一起加起来就构成了 LLVM 完整的优化和转化。
clang -emit-llvm -c main.m -o main.bc
IR提供了多种优化选项,-01 -02 -03 -0s.... 对应着不同的入参,有比如类似死代码清理,内联化,表达式重组,循环变量移动这样的 Pass。
LVM 优化器(和其他编译器一样)以流水线的方式为输入做不同的优化。常见的例子是内联(替换调用位置的函数实体)、重新组合表达式、移动循环不变代码等等。根据优化级别,运行不同程度的优化:例如 -O0 是不做优化,-O3 将运行 67 道优化程序(LLVM 2.8 版本)
zhuanlan.zhihu.com/p/100241322
zhuanlan.zhihu.com/p/395552440
LLVM的优化级别
| 级别 | 释义 |
|---|---|
| O0 | None,不进行IR优化 |
| O1 | Fast |
| O2 | Faster |
| O3 | Fastest |
| Os | Fastest , Smallest |
| Ofast | 比Os还要更近一步的优化 |
| Oz | 让IR代码体积最小的优化 |
Bitcode - 生成字节码
如果开启了 bitcode 苹果会做进一步的优化,有新的后端架构还是可以用这份优化过的 bitcode 去生成。
clang -emit-llvm -c man.m -o main.bc
汇编阶段
生成汇编
生成Target相关Object(Mach-O)
clang -S -fobjc-arc main.m -o main.s
汇编后代码:
.section __TEXT,__text,regular,pure_instructions
.build_version macos, 11, 0 sdk_version 12, 1
.p2align 4, 0x90 ## -- Begin function -[Test3 test1]
"-[Test3 test1]": ## @"\01-[Test3 test1]"
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
subq $48, %rsp
leaq L__unnamed_cfstring_(%rip), %rdx
movq %rdi, -8(%rbp)
movq %rsi, -16(%rbp)
movq _OBJC_CLASSLIST_REFERENCES_$_(%rip), %rdi
movq _OBJC_SELECTOR_REFERENCES_(%rip), %rsi
movl $123, %ecx
movb $0, %al
callq *_objc_msgSend@GOTPCREL(%rip)
movq %rax, %rdi
callq _objc_retainAutoreleasedReturnValue
movq ___NSDictionary0__struct@GOTPCREL(%rip), %rdi
movq %rax, -24(%rbp)
callq *_objc_retain@GOTPCREL(%rip)
movq %rax, -32(%rbp)
movl $2, -36(%rbp)
movl $3, -36(%rbp)
movl $3, -40(%rbp)
movl -36(%rbp), %eax
addl -40(%rbp), %eax
movl %eax, -44(%rbp)
movq -8(%rbp), %rdi
movq _OBJC_SELECTOR_REFERENCES_.2(%rip), %rsi
callq *_objc_msgSend@GOTPCREL(%rip)
xorl %eax, %eax
movl %eax, %esi
leaq -32(%rbp), %rdi
callq _objc_storeStrong
xorl %eax, %eax
movl %eax, %esi
leaq -24(%rbp), %rdi
callq _objc_storeStrong
addq $48, %rsp
popq %rbp
retq
.cfi_endproc
## -- End function
.p2align 4, 0x90 ## -- Begin function -[Test3 test2]
"-[Test3 test2]": ## @"\01-[Test3 test2]"
| 寄存器 | 16位 | 32位 | 64位 |
|---|---|---|---|
| 累加寄存器 | AX | EAX | RAX |
| 基址寄存器 | BX | EBX | RBX |
| 计数寄存器 | CX | ECX | RCX |
| 数据寄存器 | DX | EDX | RDX |
| 堆栈基指针 | BP | EBP | RBP |
| 变址寄存器 | SI | ESI | RSI |
| 堆栈顶指针 | SP | ESP | RSP |
| 指令寄存器 | IP | EIP | RIP |
mov命令:movb(8位)、movw(16位)、movl(32位)、movq(64位)
生成目标文件
clang -fmodules -c main.m -o main.o
链接阶段
这个阶段会运行目标架构的链接器,将多个object文件合并成一个可执行文件或动态库。最终的输出a.out、.dylib或.so。
在上述OC代码示例中,Main函数中引用了Person类,因此若要生成可执行的文件,需要将main.o与person.o进行链接
clang main.o Test3.o -o main
mach-o相关查看命令
//查看文件格式
file filename
//查看二进制文件
objdump <filename>
objdump --macho --exports-trie <filename>
// 查看二进制信息
otool -f <filename>
//内存页大小
pagesize
打开mach-o文件
open -a MachOView XX
Macho文件浏览器---MachOView介绍及使用:www.jianshu.com/p/175925ab3…
Mach-O可执行文件
结合可知 Mach-O 文件包含了三部分内容:
- Header(头部),指明了 cpu 架构、大小端序、文件类型、Load Commands 个数等一些基本信息
- Load Commands(加载命令),正如官方的图所示,描述了怎样加载每个 Segment 的信息。在 Mach-O 文件中可以有多个 Segment,每个 Segment 可能包含一个或多个 Section。
- Data(数据区),Segment 的具体数据,包含了代码和数据等。
Header部分:
magic 标志符 0xfeedface 是 32 位, 0xfeedfacf 是 64 位。
cputype 和 cpusubtype 确定 cpu 类型、平台
filetype 文件类型,可执行文件、符号文件(DSYM)、内核扩展等
ncmds 加载 Load Commands 的数量
flags dyld 加载的标志
MH_NOUNDEFS目标文件没有未定义的符号,MH_DYLDLINK目标文件是动态链接输入文件,不能被再次静态链接,MH_SPLIT_SEGS只读 segments 和 可读写 segments 分离,MH_NO_HEAP_EXECUTION堆内存不可执行…
简单总结一下就是 Headers 能帮助校验 Mach-O 合法性和定位文件的运行环境。
Load Commands
Load Commands 的定义比较简单:
-
cmd 字段,如上图它指出了 command 类型
LC_SEGMENT、LC_SEGMENT_64将 segment 映射到进程的内存空间,LC_UUID二进制文件 id,与符号表 uuid 对应,可用作符号表匹配,LC_LOAD_DYLINKER启动动态加载器,LC_SYMTAB描述在__LINKEDIT段的哪找字符串表、符号表,LC_CODE_SIGNATURE代码签名等
-
cmdsize 字段,主要用以计算出到下一个 command 的偏移量。
Segment
这里先来看看 segment 的定义:
-
cmd 就是上面分析的 command 类型
-
segname 在源码中定义的宏
#define SEG_PAGEZERO "__PAGEZERO" // 可执行文件捕获空指针的段#define SEG_TEXT "__TEXT" // 代码段,只读数据段#define SEG_DATA "__DATA" // 数据段#define SEG_LINKEDIT "__LINKEDIT" // 包含动态链接器所需的符号、字符串表等数据
-
vmaddr 段的虚存地址(未偏移),由于 ALSR,程序会在进程加上一段偏移量(slide),真实的地址 = vm address + slide
-
vmsize 段的虚存大小
-
fileoff 段在文件的偏移
-
filesize 段在文件的大小
-
nsects 段中有多少个 section
Section
segname 就是所在段的名称
sectname section名称,部分列举:
Text.__text主程序代码Text.__cstringc 字符串Text.__stubs桩代码 具体@李雷Text.__stub_helperData.__data初始化可变的数据Data.__objc_imageinfo镜像信息 ,在运行时初始化时objc_init,调用load_images加载新的镜像到 infolist 中Data.__la_symbol_ptr@李雷Data.__nl_symbol_ptr@李雷Data.__objc_classlist类列表Data.__objc_classrefs引用的类
无法复制加载中的内容
zhuanlan.zhihu.com/p/349725265
stub 是桩位代码占位,本质上是一小段会直接跳入 lazybinding 的表对应指针指向的地址的代码,而 stubs_helper 是辅助函数,lazybinding 的表中对应的指针在没有找到真正的符号地址的时候,都指向这里。
反汇编后可以看到调用 NSLog 时是用了 call imp__stubs__NSLog 指令
反汇编imp__stubs__NSLog 指令
MachOView 中显示的 NSlog 对应的 stub信息
然后 Hopper 继续往下走进到 _NSLog_ptr,显示是个外部引用,地址为 0X100008000,
再去 MachOView 中查看对应位置
然后找到 0X100003F56 对应的地址
可以看到落入了 __stub_helper 中,用 Hopper 查看对应地址可以看到 __stub_helper 中的指令都指向了 stub_helper 的头部,进而走向 dyld_stub_binder,
进而看到 dyld_stub_binder 位于非 lazy 绑定符号指针表中
那么经过前面这么一长串的跟进,可以将 stub、stub_helper、la_symbol_ptr 串起来,
首先二进制调用 NSLog、objc_release 等外部符号时,指令并没有与 NSLog 的实现地址进行绑定,而是利用 stub 放了桩指令,指令指向 la_symbol_ptr 表中,而 la_symbol_ptr 表中的指针在未绑定符号地址时,都指向 stub_helper,而 __stub_helper 作为辅助函数都指向 __DATA,__got/nl_symbol_ptr 中的 dyld_stub_binder,
实际执行时在第一次调用时走到 dyld_stub_binder,然后进行一个真正的符号 binding,并将 la_symbol_ptr 表中对应的符号从指向 stub_helper 修改为绑定真实调用地址,从而就实现了 lazy binding,后续调用时就会经由 stub、la_symbol_ptr 并拿到地址,不需要再走到 dyld_stub_binder 了。
fishhook 也正是利用了这个原理,调整 la_symbol_ptr 表中的指针走向,从而实现 C 函数的 hook。
项目的编译过程完整步骤
1.写入辅助文件:将项目的文件结构对应表、将要执行的脚本、项目依赖库的文件结构对应表写成文件,方便后面使用;并且创建一个 .app 包,后面编译后的文件都会被放入包中;
2.运行预设脚本:Cocoapods 会预设一些脚本,当然你也可以自己预设一些脚本来运行。这些脚本都在 Build Phases 中可以看到;
3.编译文件:针对每一个文件进行编译,生成可执行文件 Mach-O,这过程 LLVM 的完整流程,前端、优化器、后端;
4.链接文件:将项目中的多个可执行文件合并成一个文件;
5.拷贝资源文件:将项目中的资源文件拷贝到目标包;
6.编译 storyboard 文件:storyboard 文件也是会被编译的;
7.链接 storyboard 文件:将编译后的 storyboard 文件链接成一个文件;
8.编译 Asset 文件:我们的图片如果使用 Assets.xcassets 来管理图片,那么这些图片将会被编译成机器码,除了 icon 和 launchImage;
9.运行 Cocoapods 脚本:将在编译项目之前已经编译好的依赖库和相关资源拷贝到包中。
10.生成 .app 包
11.将 Swift 标准库拷贝到包中
12.对包进行签名
13.完成打包
参考文档: