前言
编译的主要任务是将源代码文件作为输入,最终输出目标文件,这期间发生了什么?便是我们本篇文章要介绍的。在开始之前我们先了解一下编译器。
编译器
编译器(
compiler
)是一种计算机程序,它会将某种编程语言写成的源代码(原始语言)转换成另一种编程语言(目标语言)。引自维基百科
传统编译器的架构,一般分三部分:
- 前端(
Frontend
):解析源代码,检查源代码是否有错误,并构建特定语言的抽象语法树(Abstract Syntax Tree
缩写:AST
)来表示输入的代码。也负责选择性的地将AST
转换为新的表示形式以进行优化。 - 优化器(
Optimizer
):负责进行各种转换,以尝试改善代码的运行时间,例如消除冗余计算,并且通常或多或少地独立于编程语言和目标代码。 - 后端(
Backend
):也称代码生成器,将代码映射到目标架构的指令集上;其常见部分有:指令选择,寄存器分配,指定调度。
这种架构的优势在于解耦合,实现一种编程语言,只需要实现它的前端,对于优化器与后端部分是可以复用的;支持新的目标架构,只需要实现它的后端即可;如果编译器不是这种架构,三部分未分开,那么实现N
个编程语言,去支持M
个目标架构,就需要实现N*M
个编译器。
这种传统编译器的架构有三个成功的案例:
-
Java
和.Net
虚拟机;它们都提供了对JIT
编译器和运行时的支持,并且还定义了字节码的格式(bytecode
),这意味着任何可以编译为字节码的语言,都可以复用优化器和JIT
(动态编译)和运行时能力。 - 将输入源转换为
C
代码(或其他某种语言)并通过现有的C
编译器编译 - 这种模式的最终成功实施是
GCC
,GCC
支持许多前端和后端,并拥有活跃而广泛的贡献者社区。
GCC
GCC
的概述
Xcode5
之前的版本中使用的是GCC
编译器,由于GCC
,历史悠久,体系结构相对复杂,功能模块化复用难度大且不受苹果公司的约束,很难满足苹果系统的发展需求。因此在Xcode5
中抛弃了GCC
,采用Clang/LLVM
进行编译。
GCC
:是GNU Compiler Collection
的缩写,指GNU
编译器套装。Linux
系统的核心组成部分就有GNU
工具链,GCC
也是GNU
工具链的重要组成部分,因此GCC
也是作为Linux
系统的标准编译器。GCC
可处理的语言有C
、C++
、Objective-C
、Java
、Go
等。
使用GCC
命令gcc -ccc-print-phases main.m
查看编译OC
的步骤:
*deMacBook-Pro:Mach-O *$ gcc -ccc-print-phases main.m
+- 0: input, "main.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
GCC
的架构
前端读取源文件将其转化为AST
,由于每种语言生成的AST
是有差异的,所以需要需要转换为通用的与语言无关的统一形式GENERIC
。
中端将GENERIC
,利用gimplifier
技术,简化GENERIC
的复杂结构,将其转换为一种中间表示形式称为:GIMPLE
,再转换为另一种SSA
(static single assignment
)的表示形式也是用于优化的,GCC
对SSA
树执行20
多种不同的优化。经过SSA
优化后,该树将转换回GIMPLE
形式,用来生成一个RTL
树,RTL
寄存器转换语言,全称(register-transfer language
);RTL
是基于硬件的表示形式,与抽象的目标架构相对应,处理寄存器分配、指令调度等。RTL
优化过程以RTL
形式对树进行优化。
后端使用RTL
表示形式生成目标架构的汇编代码。如:x86
后端。
LLVM
LLVM
的概述
LLVM
项目是模块化和可重用的编译器及工具链技术的集合。名称LLVM
是Low Level Virtual Machine
的缩写,尽管名称如此,但是LLVM
与传统虚拟机关系不大,它是LLVM
项目的全名。
The LLVM Project is a collection of modular and reusable compiler and toolchain technologies. Despite its name, LLVM has little to do with traditional virtual machines. The name "LLVM" itself is not an acronym; it is the full name of the project. 引自LLVM官网
LLVM
有许多的子项目,比如Clang
,LLDB
,MLIR
等。
LLVM
的历史
-
LLVM
起源于2000
年Vikram Adve
与Chris Lattner
的研究,目的:为所有的静态语言(C
/C++
)和动态语言(运行时改变其结构的语言,如:OC
/JavaScript
)创造出动态编译技术。 - 苹果公司
2005
雇佣Chris Lattner
与他的团队为苹果电脑开发应用程序系统,LLVM
为现今macOS
与iOS
开发工具的一部分。 - 因
LLVM
对产业的贡献,2012
年获得了ACM软件系统奖。获得该奖项的有Unix
、Java
、TCP/IP
、DNS
、Mach
-
2019
年10
月开始,LLVM
项目的代码托管正式迁移到了GitHub
。
LLVM
的架构
LLVM
最重要的设计是中间表示LLVM Intermediate Representation
(IR
),它是在编译器中表示代码的一种形式。优化器使用LLVM IR
作中间的转换与分析处理。LLVM IR
本身就是具有良好语义定义的一流语言。
在基于LLVM
的编译器中,Frontend
负责对输入的代码进行解析,校验和分析错误,然后将解析后的代码转换为LLVM IR
(通常情况,是将构建的抽象语法树AST
转换为LLVM IR
,但不总是这样的)。可以选择通过一系列分析和优化过程来传递LLVM IR
,以改进代码,然后将其发送到代码生成器(Backend
)中,生成原始的机器码。
LLVM IR
不仅是完整的代码表示,而且也是优化器optimizer
的唯一接口。这意味着写一个LLVM
的前端只需要知道LLVM IR
即可,这是LLVM
的一个新颖的特性,也是LLVM
成功地被广泛应用的一个主要原因。反观GCC
编译器,写一个前端需要知道生成的GCC
树的数据结构以及使用GIMPLE
去写GCC
的前端,GCC
后端需要知道RTL
是如何工作的。
LLVM IR
是前端输出,后端的输入:
LLVM
广义是指LLVM
整个架构,狭义指Clang
编译器的后端。
Clang
Clang
是LLVM
的子项目,是C
、C++
、Objective C
语言的编译器的前端。Clang
编译Objective-C
代码时速度为GCC
的3
倍。详见维基百科。
Clang
编译过程
下面是一个基于简单的OC
工程,不依赖Xcode
,而是使用终端编译的例子。
编译前工程源代码主要分为main.m
和Person.m
类,代码如下:
///main.m
#import
#import "Person.h"
#define SomeDefine @"你好,世界"
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 注释
NSLog(@"Hello, World!");
#pragma mark 我是注释
NSLog(@"%@",SomeDefine);
/// MARK: 我也是注释
Person *instance = [[Person alloc]init];
[instance share];
}
return 0;
}
///Person.m
#import "Person.h"
@implementation Person
- (void)share {
NSLog(@"持之以恒");
}
@end
首先我们运行clang -ccc-print-phases main.m
查看整体的编译过程:
*deMacBook-Pro: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
+- 3: backend, {2}, assembler
+- 4: assembler, {3}, object
+- 5: linker, {4}, image
6: bind-arch, "x86_64", {5}, image
接下来,基于这个例子,我们使用终端逐步编译,生成我们的可执行文件,并最终控制台打印我们的信息。
预处理
基于输入,通过预处理器执行一系列的文本转换与文本处理。预处理器是在真正的编译开始之前由编译器调用的独立程序。
终端命令:
# 编译阶段选择参数: -E 运行预处理这一步
clang -E main.m
# 预处理结果输出到main.mi文件中
clang -E main.m -o main.mi
输出结果:
# 193 "/Library/Developer/CommandLineTools/SDKs/MacOSX10.15.sdk/System/Library/Frameworks/Foundation.framework/Headers/Foundation.h" 2 3
# 9 "main.m" 2
# 1 "./Person.h" 1
# 10 "./Person.h"
#pragma clang assume_nonnull begin
@interface Person : NSObject
- (void)share;
@end
#pragma clang assume_nonnull end
# 10 "main.m" 2
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"Hello, World!");
NSLog(@"%@",@"你好,世界");
Person *instance = [[Person alloc]init];
[instance share];
}
return 0;
}
最终C
输出.i
文件,C++
输出.ii
文件,Objective-C
输出.mi
文件,Objective-C ++
输出.mii
文件。
预处理的任务:
将输入文件读到内存,并断行;
替换注释为单个空格;
Tokenization
将输入转换为一系列预处理Tokens
;处理
#import
、#include
将所引的库,以递归的方式,插入到#import
或#include
所在的位置;替换宏定义;
条件编译,根据条件包括或排除程序代码的某些部分;
插入行标记;
在预处理的输出中,源文件名和行号信息会以# linenum filename flags
形式传递,这被称为行标记,代表着接下来的内容开始于源文件filename
的第linenum
行,而flags
则会有0
或者多个,有1
、2
、3
、4
;如果有多个flags
时,彼此使用分号隔开。详见此处。
每个标识的表示内容如下:
-
1
表示一个新文件的开始 -
2
表示返回文件(包含另一个文件后) -
3
表示以下文本来自系统头文件,因此应禁止某些警告 -
4
表示应将以下文本视为包装在隐式extern "C"
块中。
比如# 10 "main.m" 2
,表示导入Person.h
文件后回到main.m
文件的第10行。
词法分析
词法分析属于预处理部分,词法分析的整个过程,主要是按照:标识符、 数字、字符串文字、 标点符号,将我们的代码分割成许多字符串序列,其中每个元素我们称之为Token
,整个过程称为Tokenization
。
终端输入:
# -fmodules: Enable the 'modules' language feature
# -fsyntax-only, Run the preprocessor, parser and type checking stages
#-Xclang : Pass to the clang compiler
# -dump-tokens: Run preprocessor, dump internal rep of tokens
clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
-fmodules
:启用“模块”语言功能。关于Modules
特性,详见此处,大意为使用import
代替include
,编译速度快。
-fsyntax-only
:运行预处理器,解析器和类型检查阶段。
-Xclang
:传递参数到clang
的编译器。
dump-tokens
:运行预处理器,转储Token
的内部表示。
更多关于Clang
参数的描述,请前往此处。
输出结果:
....
int 'int' [StartOfLine] Loc=
identifier 'main' [LeadingSpace] Loc=
l_paren '(' Loc=
int 'int' Loc=
identifier 'argc' [LeadingSpace] Loc=
comma ',' Loc=
const 'const' [LeadingSpace] Loc=
char 'char' [LeadingSpace] Loc=
star '*' [LeadingSpace] Loc=
identifier 'argv' [LeadingSpace] Loc=
l_square '[' Loc=
r_square ']' Loc=
r_paren ')' Loc=
l_brace '{' [LeadingSpace] Loc=
at '@' [StartOfLine] [LeadingSpace] Loc=
identifier 'autoreleasepool' Loc=
l_brace '{' [LeadingSpace] Loc=
identifier 'NSLog' [StartOfLine] [LeadingSpace] Loc=
l_paren '(' Loc=
at '@' Loc=
string_literal '"Hello, World!"' Loc=
r_paren ')' Loc=
semi ';' Loc=
identifier 'NSLog' [StartOfLine] [LeadingSpace] Loc=
l_paren '(' Loc=
at '@' Loc=
string_literal '"%@"' Loc=
comma ',' Loc=
at '@' Loc=>
string_literal '"你好,世界"' Loc=>
r_paren ')' Loc=
semi ';' Loc=
identifier 'Person' [StartOfLine] [LeadingSpace] Loc=
star '*' [LeadingSpace] Loc=
identifier 'instance' Loc=
equal '=' [LeadingSpace] Loc=
l_square '[' [LeadingSpace] Loc=
l_square '[' Loc=
identifier 'Person' Loc=
identifier 'alloc' [LeadingSpace] Loc=
r_square ']' Loc=
identifier 'init' Loc=
r_square ']' Loc=
semi ';' Loc=
l_square '[' [StartOfLine] [LeadingSpace] Loc=
identifier 'instance' Loc=
identifier 'share' [LeadingSpace] Loc=
r_square ']' Loc=
semi ';' Loc=
r_brace '}' [StartOfLine] [LeadingSpace] Loc=
return 'return' [StartOfLine] [LeadingSpace] Loc=
numeric_constant '0' [LeadingSpace] Loc=
semi ';' Loc=
r_brace '}' [StartOfLine] Loc=
eof '' Loc=
词法分析中Token
包含信息(详请见此处):
Sourece Location
:表示Token
开始的位置,比如:Loc=
;Token Kind
:表示Token
的类型,比如:identifier
、numeric_constant
、string_literal
;Flags
:词法分析器和处理器跟踪每个Token
的基础,目前有四个Flag
分别是:
StartOfLine
:表示这是每行开始的第一个Token
;LeadingSpace
:当通过宏扩展Token
时,在Token
之前有一个空格字符。该标志的定义是依据预处理器的字符串化要求而进行的非常严格地定义。DisableExpand
:该标志在预处理器内部使用,用来表示identifier
令牌禁用宏扩展。NeedsCleaning
:如果令牌的原始拼写包含三字符组或转义的换行符,则设置此标志。
语法分析(Parsing
)与语义分析
此阶段对输入文件进行语法分析,将预处理器生成的Tokens
转换为语法分析树;一旦生成语法分析树后,将会进行语义分析,执行类型检查和代码格式检查。这个阶段负责生成大多数编译器警告以及语法分析过程的错误。最终输出AST
(抽象语法树)。
Parser
的意义与作用
所谓 parser,一般是指把某种格式的文本(字符串)转换成某种数据结构的过程。最常见的 parser,是把程序文本转换成编译器内部的一种叫做“抽象语法树”(AST)的数据结构。摘自对 Parser 的误解-王垠
AST
的示意图(来源):
终端输入:
# -fmodules: Enable the 'modules' language feature
# -fsyntax-only, Run the preprocessor, parser and type checking stages
#-Xclang : Pass to the clang compiler
# -ast-dump: Build ASTs and then debug dump them
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
输出结果:
TranslationUnitDecl 0x7f80ea01c408 <>
|-TypedefDecl 0x7f80ea01cca0 <> implicit __int128_t '__int128'
| `-BuiltinType 0x7f80ea01c9a0 '__int128'
#...
# cutting out internal declarations of clang
#...
|-ImportDecl 0x7f80ea27d9d8 col:1 implicit Foundation
|-ImportDecl 0x7f80ea27da18 <./Person.h:8:1> col:1 implicit Foundation
|-ObjCInterfaceDecl 0x7f80ea294ff8 line:12:12 Person
| |-super ObjCInterface 0x7f80ea27db18 'NSObject'
| `-ObjCMethodDecl 0x7f80ea2951f0 col:1 - share 'void'
`-FunctionDecl 0x7f80ea295620 line:11:5 main 'int (int, const char **)'
|-ParmVarDecl 0x7f80ea2953b0 col:14 argc 'int'
|-ParmVarDecl 0x7f80ea2954d0 col:33 argv 'const char **':'const char **'
`-CompoundStmt 0x7f80ea29e5b8
|-ObjCAutoreleasePoolStmt 0x7f80ea29e570
| `-CompoundStmt 0x7f80ea29e540
| |-CallExpr 0x7f80ea2a26f0 'void'
| | |-ImplicitCastExpr 0x7f80ea2a26d8 'void (*)(id, ...)'
| | | `-DeclRefExpr 0x7f80ea2a25e0 'void (id, ...)' Function 0x7f80ea295760 'NSLog' 'void (id, ...)'
| | `-ImplicitCastExpr 0x7f80ea2a2718 'id':'id'
| | `-ObjCStringLiteral 0x7f80ea2a2660 'NSString *'
| | `-StringLiteral 0x7f80ea2a2638 'char [14]' lvalue "Hello, World!"
| |-CallExpr 0x7f80ea298298 'void'
| | |-ImplicitCastExpr 0x7f80ea298280 'void (*)(id, ...)'
| | | `-DeclRefExpr 0x7f80ea2a2730 'void (id, ...)' Function 0x7f80ea295760 'NSLog' 'void (id, ...)'
| | |-ImplicitCastExpr 0x7f80ea2982c8 'id':'id'
| | | `-ObjCStringLiteral 0x7f80ea2a27a8 'NSString *'
| | | `-StringLiteral 0x7f80ea2a2788 'char [3]' lvalue "%@"
| | `-ObjCStringLiteral 0x7f80ea298260 'NSString *'
| | `-StringLiteral 0x7f80ea298238 'char [16]' lvalue "\344\275\240\345\245\275\357\274\214\344\270\226\347\225\214"
| |-DeclStmt 0x7f80ea29e4a8
| | `-VarDecl 0x7f80ea298320 col:17 used instance 'Person *' cinit
| | |-ObjCMessageExpr 0x7f80ea2988d0 'Person *' selector=init
| | | `-ObjCMessageExpr 0x7f80ea298658 'Person *' selector=alloc class='Person'
| | `-FullComment 0x7f80ea2a3900
| | `-ParagraphComment 0x7f80ea2a38d0
| | `-TextComment 0x7f80ea2a38a0 Text=" MARK: 我也是注释"
| `-ObjCMessageExpr 0x7f80ea29e510 'void' selector=share
| `-ImplicitCastExpr 0x7f80ea29e4f8 'Person *'
| `-DeclRefExpr 0x7f80ea29e4c0 'Person *' lvalue Var 0x7f80ea298320 'instance' 'Person *'
`-ReturnStmt 0x7f80ea29e5a8
`-IntegerLiteral 0x7f80ea29e588 'int' 0
Clang
的AST
是从TranslationUnitDecl节点开始进行递归遍历的;AST
中许多重要的Node
,继承自Type
、Decl
、DeclContext
、Stmt
。
- Type :表示类型,比如
BuiltinType
- Decl :表示一个声明
declaration
或者一个定义definition
,比如:变量,函数,结构体,typedef
; - DeclContext :用来声明表示上下文的特定
decl
类型的基类; - Stmt :表示一条陈述
statement
; - Expr:在
Clang
的语法树中也表示一条陈述statements
;
代码优化和生成
这个阶段主要任务是将AST
转换为底层中间的代码LLVM IR
,并且最终生成机器码;期间负责生成目标架构的代码以及优化生成的代码。最终输出.s
文件(汇编文件)。
LLVM IR
有三种格式:
- 文本格式:
.ll
文件 - 内存中用以优化自身时,执行检查和修改的数据结构(编译过程中载入内存的形式)
- 磁盘二进制(
BitCode
)格式:.bc
文件
LLVM
提供了.ll
与.bc
相互转换的工具:
-
llvm-as
:可将.ll
转为.bc
-
llvm-dis
:可将.bc
转为.ll
终端输入:
# -S : Run LLVM generation and optimization stages and target-specific code generation,producing an assembly file
# -fobjc-arc : Synthesize retain and release calls for Objective-C pointers
# -emit-llvm : Use the LLVM representation for assembler and object files
# -o : Write output to
# 汇编表示成.ll文件 -fobjc-arc 可忽略,不作代码优化
clang -S -fobjc-arc -emit-llvm main.m -o main.ll
# 目标文件表示成 .bc 文件
# -c : Only run preprocess, compile, and assemble steps
clang -emit-llvm -c main.m -o main.bc
#.ll与.bc的相互转换
llvm-as main.ll -o main.bc
llvm-dis main.bc -o main.ll
此处使用了参数-emit-llvm
,来查看LLVM IR
。
输出结果:
# 此处只贴main函数部分
define i32 @main(i32 %0, i8** %1) #1 {
%3 = alloca i32, align 4
%4 = alloca i32, align 4
%5 = alloca i8**, align 8
%6 = alloca %0*, align 8
store i32 0, i32* %3, align 4
store i32 %0, i32* %4, align 4
store i8** %1, i8*** %5, align 8
%7 = call i8* @llvm.objc.autoreleasePoolPush() #2
notail call void (i8*, ...) @NSLog(i8* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_ to i8*))
notail call void (i8*, ...) @NSLog(i8* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_.2 to i8*), %1* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_.4 to %1*))
%8 = load %struct._class_t*, %struct._class_t** @"OBJC_CLASSLIST_REFERENCES_$_", align 8
%9 = bitcast %struct._class_t* %8 to i8*
%10 = call i8* @objc_alloc_init(i8* %9)
%11 = bitcast i8* %10 to %0*
store %0* %11, %0** %6, align 8
%12 = load %0*, %0** %6, align 8
%13 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_, align 8, !invariant.load !9
%14 = bitcast %0* %12 to i8*
call void bitcast (i8* (i8*, i8*, ...)* @objc_msgSend to void (i8*, i8*)*)(i8* %14, i8* %13)
%15 = bitcast %0** %6 to i8**
call void @llvm.objc.storeStrong(i8** %15, i8* null) #2
call void @llvm.objc.autoreleasePoolPop(i8* %7)
ret i32 0
}
代码优化
Clang
代码优化参数有-O0
、 -O1
、 -O2
、 -O3
、 -Ofast
、-Os
、 -Oz
、-Og
、 -O
、-O4
-
-O0
:表示没有优化;编译速度最快并生成最可调试的代码 -
-O1
:优化程度介于-O0
~-O2
之间。 -
-O2
:适度的优化水平,可实现最优化 -
-O3
:与-O2
相似,不同之处在于它优化的时间比较长,可能会生成更大的代码 -
-O4
:当前等效于-O3
-
-Ofast
:启用-O3
中的所有优化并且可能启用一些激进优化 -
-Os
:与-O2
一样,具有额外的优化功能以减少代码大小 -
-Oz
:类似于-Os
,进一步减小了代码大小 -
-Og
:类似-O1
-
-O
:相当于-O2
终端输入:
clang -S -O2 -fobjc-arc -emit-llvm main.m -o main.ll
输出结果:
#LLVM IR文件头信息
; ModuleID = 'main.m'
source_filename = "main.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-macosx10.15.0"
#结构体的定义
%0 = type opaque
%struct.__NSConstantString_tag = type { i32*, i32, i8*, i64 }
%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* }
# 全局变量、私有/外部/内部常量的定义或声明
@__CFConstantStringClassReference = external global [0 x i32]
@.str = private unnamed_addr constant [14 x i8] c"Hello, World!\00", section "__TEXT,__cstring,cstring_literals", align 1
# --全局结构体定义与初始化
@_unnamed_cfstring_ = private global %struct.__NSConstantString_tag { i32* getelementptr inbounds ([0 x i32], [0 x i32]* @__CFConstantStringClassReference, i32 0, i32 0), i32 1992, i8* getelementptr inbounds ([14 x i8], [14 x i8]* @.str, i32 0, i32 0), i64 13 }, section "__DATA,__cfstring", align 8 #0
@.str.1 = private unnamed_addr constant [3 x i8] c"%@\00", section "__TEXT,__cstring,cstring_literals", align 1
@_unnamed_cfstring_.2 = private global %struct.__NSConstantString_tag { i32* getelementptr inbounds ([0 x i32], [0 x i32]* @__CFConstantStringClassReference, i32 0, i32 0), i32 1992, i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str.1, i32 0, i32 0), i64 2 }, section "__DATA,__cfstring", align 8 #0
@.str.3 = private unnamed_addr constant [6 x i16] [i16 20320, i16 22909, i16 -244, i16 19990, i16 30028, i16 0], section "__TEXT,__ustring", align 2
@_unnamed_cfstring_.4 = private global %struct.__NSConstantString_tag { i32* getelementptr inbounds ([0 x i32], [0 x i32]* @__CFConstantStringClassReference, i32 0, i32 0), i32 2000, i8* bitcast ([6 x i16]* @.str.3 to i8*), i64 5 }, section "__DATA,__cfstring", align 8 #0
@"OBJC_CLASS_$_Person" = external global %struct._class_t
@"OBJC_CLASSLIST_REFERENCES_$_" = internal global %struct._class_t* @"OBJC_CLASS_$_Person", section "__DATA,__objc_classrefs,regular,no_dead_strip", align 8
@OBJC_METH_VAR_NAME_ = private unnamed_addr constant [6 x i8] c"share\00", section "__TEXT,__objc_methname,cstring_literals", align 1
@OBJC_SELECTOR_REFERENCES_ = internal externally_initialized global i8* getelementptr inbounds ([6 x i8], [6 x i8]* @OBJC_METH_VAR_NAME_, i64 0, i64 0), section "__DATA,__objc_selrefs,literal_pointers,no_dead_strip", align 8
@llvm.compiler.used = appending global [3 x i8*] [i8* bitcast (%struct._class_t** @"OBJC_CLASSLIST_REFERENCES_$_" to i8*), i8* getelementptr inbounds ([6 x i8], [6 x i8]* @OBJC_METH_VAR_NAME_, i32 0, i32 0), i8* bitcast (i8** @OBJC_SELECTOR_REFERENCES_ to i8*)], section "llvm.metadata"
# main函数的入口:`dso_local`:main函数解析为统一链接单元的符号,而非外部替换的符号
; Function Attrs: ssp uwtable
define dso_local i32 @main(i32 %0, i8** nocapture readnone %1) local_unnamed_addr #1 {
%3 = tail call i8* @llvm.objc.autoreleasePoolPush() #2
notail call void (i8*, ...) @NSLog(i8* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_ to i8*)), !clang.arc.no_objc_arc_exceptions !8
notail call void (i8*, ...) @NSLog(i8* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_.2 to i8*), %0* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_.4 to %0*)), !clang.arc.no_objc_arc_exceptions !8
%4 = load i8*, i8** bitcast (%struct._class_t** @"OBJC_CLASSLIST_REFERENCES_$_" to i8**), align 8
%5 = tail call i8* @objc_alloc_init(i8* %4), !clang.arc.no_objc_arc_exceptions !8
%6 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_, align 8, !invariant.load !8
tail call void bitcast (i8* (i8*, i8*, ...)* @objc_msgSend to void (i8*, i8*)*)(i8* %5, i8* %6), !clang.arc.no_objc_arc_exceptions !8
tail call void @llvm.objc.release(i8* %5) #2, !clang.imprecise_release !8
tail call void @llvm.objc.autoreleasePoolPop(i8* %3) #2
ret i32 0
}
#函数声明
; Function Attrs: nounwind
declare i8* @llvm.objc.autoreleasePoolPush() #2
declare void @NSLog(i8*, ...) local_unnamed_addr #3
declare i8* @objc_alloc_init(i8*) local_unnamed_addr
; Function Attrs: nonlazybind
declare i8* @objc_msgSend(i8*, i8*, ...) local_unnamed_addr #4
; Function Attrs: nounwind
declare void @llvm.objc.release(i8*) #2
; Function Attrs: nounwind
declare void @llvm.objc.autoreleasePoolPop(i8*) #2
#属性组
attributes #0 = { "objc_arc_inert" }
attributes #1 = { 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 = { nounwind }
attributes #3 = { "disable-tail-calls"="false" "frame-pointer"="all" "less-precise-fpmad"="false" "no-infs-fp-math"="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 #4 = { nonlazybind }
#该`module`的元数据
##命名元数据
!llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6}
!llvm.ident = !{!7}
##未命名的元数据
!0 = !{i32 1, !"Objective-C Version", i32 2}
!1 = !{i32 1, !"Objective-C Image Info Version", i32 0}
!2 = !{i32 1, !"Objective-C Image Info Section", !"__DATA,__objc_imageinfo,regular,no_dead_strip"}
!3 = !{i32 1, !"Objective-C Garbage Collection", i8 0}
!4 = !{i32 1, !"Objective-C Class Properties", i32 64}
!5 = !{i32 1, !"wchar_size", i32 4}
!6 = !{i32 7, !"PIC Level", i32 2}
!7 = !{!"clang version 12.0.0"}
!8 = !{}
浅析 LLVM IR
Module:
LLVM
程序是由Module
组成的,每个Module
是输入程序的翻译单元。每个Module
都是由functions
、global variables
和symbol table entries
组成。Module
会通过LLVM
链接器组合到一起,链接器会合并函数以及全局变量的定义,解决前置声明以及合并符号表。Target Datalayout:
Module
需要以字符串的形式指定特定于目标架构的数据布局方式,该字符串指定如何在内存中布局数据。如:target datalayout = "e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
。-
元数据:
LLVM IR
允许元数据被附加到能够传递代码额外信息给优化器和代码生成器的程序指令上。所有元数据在语法上均由!
标识。元数据的两个原语:元数据字符串和元数据节点。- 元数据字符串:用
""
引起来的字符串,以!
作为前缀。如:!"clang version 12.0.0"
- 元数据节点:用
{}
括起来,使用,
隔开多个元素,以!
作为前缀。如:!{i32 7, !"PIC Level", i32 2}
- 元数据字符串:用
-
命名元数据:是元数据节点的集合
; Some unnamed metadata nodes, which are referenced by the named metadata. !0 = !{!"zero"} !1 = !{!"one"} !2 = !{!"two"} ; A named metadata. !name = !{!0, !1, !2}
-
Linkage Types:所有全局变量和函数都具有链接类型,如上述
IR
中的:-
external
:module
外部可用 -
private
:module
内部可用 -
appending
:仅应用于数组类型的全局变量的指针。当两个使用了appending
的全局变量链接到一起的时候,这两个全局的数组会被拼接到一起。 -
internal
:与private
相似,但该值在目标文件中显示为本地符号。与C
语言中static
关键字的概念相对应。
-
属性组:属性组是
IR
中的对象(函数、全局变量)引用的属性组合。它们对于保持.ll文件的可读性很重要,因为许多函数将使用相同的属性集。如上述IR
中的#0~#4
。-
函数属性:被用来传递一个函数附加信息。函数属性被认为是函数的一部分,而不是函数类型,所以不同的函数属性可以有相同的函数类型。上述
IR
中用到最多的函数属性:-
nounwind
:表示函数不会抛出异常 -
nonlazybind
:阻止函数中某些符号的延迟绑定。
-
-
参数属性:函数的返回类型以及每个参数都有与之关联的参数属性集合。被用来传递一个函数的返回值与参数的附加信息。参数属性是函数的一部分,而不是函数类型,所以有不同参数属性的函数可以有相同的参数类型。上述
IR
中用到最多的参数属性:-
nocapture
:表示函数调用不会捕获参数的指针,这个属性对于返回值是无效的,仅适用于参数。 -
readnone
:应用于参数表示函数不会取消对此参数指针的引用。
-
-
标识符:
-
@
为全局标识符。以其开头标识函数,全局变量; -
%
为本地标识符。以其开头标识寄存器名称,类型; - 标识符的不同的格式:
- 命名值,表示为以上述标识符为前缀的字符串,如:
%struct._ivar_t
、@.str
- 未命名值,表示为以上述标识为前缀的无符号的数值,如:
%0
、%1
- 命名值,表示为以上述标识符为前缀的字符串,如:
-
-
结构体的定义:
#语法 %T1 = type {
} ; Identified normal struct type %T2 = type <{ }> ; Identified packed struct type #表示结构体的对齐方式为1字节 #示例 {i32, i32} %mytype = type { %mytype*, i32 } -
数组的定义:
#语法 [<# elements> x
] #语义 `elements`是个`integer`的值;`elementtype`是任意有大小的类型 #示例 [40 x i32] Array of 40 32-bit integer values -
全局变量的定义:
#语法 @
= [Linkage] [PreemptionSpecifier] [Visibility] [DLLStorageClass] [ThreadLocal] [(unnamed_addr|local_unnamed_addr)] [AddrSpace] [ExternallyInitialized] [ ] [, section "name"] [, comdat [($name)]] [, align ] (, !name !N)* #示例 @G = external global i32 #just declare @G = external global i32 8 #InitializerConstant -
global constant
:表示该变量的内容将永远不会被修改。 -
unnamed_addr
:表示该变量的地址并不重要,仅指示内容。 -
local_unnamed_addr
:表示变量的地址在module
内并不重要。
-
-
Runtime Preemption Specifiers:运行时抢占说明符。全局变量,函数和别名可以具有一个可选的运行时抢占说明符。如果未明确指定抢占说明符,则假定该符号为
dso_preemptable
。-
dso_preemptable
:表示函数或者变量在运行时会被外部的链接单元替换 -
dso_local
:表示函数或变量将解析为同一链接单元中的符号。即使定义不在此编译单元内,也将生成直接访问
-
-
call:代表一个简单的函数调用;
#语法
= [tail | musttail | notail ] call [fast-math flags] [cconv] [ret attrs] [addrspace( )] | ( ) [fn attrs] [ operand bundles ] 可选的
tail
和musttail
标记优化器应执行尾部调用优化notail
标记用于防止执行尾部调用优化
-
ret:该指令表示函数返回
#语法 ret
; Return a value from a non-void function ret void ; Return from void function #示例 ret i32 5 ; Return an integer value of 5 ret void ; Return from a void function ret { i32, i8 } { i32 4, i8 2 } ; Return a struct of values 4 and 2 -
bitcast...to:
bitcast
将value
的类型转换为类型ty2
而不改变它的任何位bits
#语法
= bitcast to ; yields ty2 其他:
i32
:代表32-bit
的整数,i8
:代表8-bit
的整数;
代码生成
生成目标架构的汇编代码。
终端输入:
#生成目标架构的汇编代码
clang -S -fobjc-arc main.m -o main.s
输出结果:
.section __TEXT,__text,regular,pure_instructions
.build_version macos, 10, 15 sdk_version 10, 15, 6
.globl _main ## -- Begin function main
.p2align 4, 0x90
_main: ## @main
.cfi_startproc
## %bb.0:
pushq %rbp #将%rbp的内容压栈,保存栈帧到%rsp中
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp # 将栈指针传送至%rbp中,设置当前栈帧
.cfi_def_cfa_register %rbp
subq $32, %rsp # 栈指针 - 32 (申请32个字节的空间)
movl $0, -4(%rbp)# 将 0 传送至存储器中,存储器位置为: M[-4 + %rbp]
movl %edi, -8(%rbp) # 将%edi的内容 传送至存储器中,存储器位置为: M[-8 + %rbp]
movq %rsi, -16(%rbp)# 将%rsi的内容 传送至存储器中,存储器位置为: M[-16 + %rbp]
callq _objc_autoreleasePoolPush #调用_objc_autoreleasePoolPush
leaq L__unnamed_cfstring_(%rip), %rcx #将`L__unnamed_cfstring_(%rip)`的有效地址写入`%rcx`中
movq %rcx, %rdi # 将%rcx的内容 传送至寄存器%rdi
movq %rax, -32(%rbp) ## 8-byte Spill # 将%rax的内容 传送至存储器中,存储器位置为: M[-32 + %rbp]
movb $0, %al # 将立即数0 传送至寄存器的低八位的单字节寄存器`%al`中
callq _NSLog #调用 _NSLog
leaq L__unnamed_cfstring_.2(%rip), %rcx
leaq L__unnamed_cfstring_.4(%rip), %rdx
movq %rcx, %rdi # 将%rcx的内容 传送至寄存器%rdi
movq %rdx, %rsi #将%rdx的内容 传送至寄存器%rsi
movb $0, %al # 将立即数0 传送至寄存器的低八位的单字节寄存器`%al`中
callq _NSLog #调用 _NSLog
movq _OBJC_CLASSLIST_REFERENCES_$_(%rip), %rcx
movq %rcx, %rdi
callq _objc_alloc_init
movq %rax, -24(%rbp)
movq -24(%rbp), %rax
movq _OBJC_SELECTOR_REFERENCES_(%rip), %rsi
movq %rax, %rdi
callq *_objc_msgSend@GOTPCREL(%rip)
xorl %r8d, %r8d # 使用异或对寄存器`%r8d`清0
movl %r8d, %esi
leaq -24(%rbp), %rax
movq %rax, %rdi
callq _objc_storeStrong
movq -32(%rbp), %rdi ## 8-byte Reload
callq _objc_autoreleasePoolPop
xorl %eax, %eax # 使用异或对寄存器`%eax`清0
addq $32, %rsp
popq %rbp #将%rbp的内容弹出栈
retq
.cfi_endproc
## -- End function
.section __TEXT,__cstring,cstring_literals
L_.str: ## @.str
.asciz "Hello, World!"
.section __DATA,__cfstring
.p2align 3 ## @_unnamed_cfstring_
L__unnamed_cfstring_:
.quad ___CFConstantStringClassReference
.long 1992 ## 0x7c8
.space 4
.quad L_.str
.quad 13 ## 0xd
.section __TEXT,__cstring,cstring_literals
L_.str.1: ## @.str.1
.asciz "%@"
.section __DATA,__cfstring
.p2align 3 ## @_unnamed_cfstring_.2
L__unnamed_cfstring_.2:
.quad ___CFConstantStringClassReference
.long 1992 ## 0x7c8
.space 4
.quad L_.str.1
.quad 2 ## 0x2
.section __TEXT,__ustring
.p2align 1 ## @.str.3
l_.str.3:
.short 20320 ## 0x4f60
.short 22909 ## 0x597d
.short 65292 ## 0xff0c
.short 19990 ## 0x4e16
.short 30028 ## 0x754c
.short 0 ## 0x0
.section __DATA,__cfstring
.p2align 3 ## @_unnamed_cfstring_.4
L__unnamed_cfstring_.4:
.quad ___CFConstantStringClassReference
.long 2000 ## 0x7d0
.space 4
.quad l_.str.3
.quad 5 ## 0x5
.section __DATA,__objc_classrefs,regular,no_dead_strip
.p2align 3 ## @"OBJC_CLASSLIST_REFERENCES_$_"
_OBJC_CLASSLIST_REFERENCES_$_:
.quad _OBJC_CLASS_$_Person
.section __TEXT,__objc_methname,cstring_literals
L_OBJC_METH_VAR_NAME_: ## @OBJC_METH_VAR_NAME_
.asciz "share"
.section __DATA,__objc_selrefs,literal_pointers,no_dead_strip
.p2align 3 ## @OBJC_SELECTOR_REFERENCES_
_OBJC_SELECTOR_REFERENCES_:
.quad L_OBJC_METH_VAR_NAME_
.section __DATA,__objc_imageinfo,regular,no_dead_strip
L_OBJC_IMAGE_INFO:
.long 0
.long 64
.subsections_via_symbols
汇编指令
所有以.
开头的行,都是指导编译器与链接器的命令。
-
.section
指定汇编器将生成的汇编代码,写入对应的区section
。语法:
.section segname , sectname [[[ , type ] , attribute ] , sizeof_stub ]
示例:
#`regular`类型 表示该区存放程序指令或初始化数据 #`pure_instructions`属性 表示此区仅包含机器指令 .section __TEXT,__text,regular,pure_instructions #`cstring_literals`类型 表示该区存放以null结尾的c字符串 .section __TEXT,__cstring,cstring_literals
.global symbol_name
标记符号为外部符号;-
.align
对齐指令,指定汇编代码的对齐方式语法:
.align align_expression [ , 1byte_fill_expression [,max_bytes_to_fill]] .p2align align_expression [ , 1byte_fill_expression [,max_bytes_to_fill]] .p2alignw align_expression [ , 2byte_fill_expression [,max_bytes_to_fill]] .p2alignl align_expression [ , 4byte_fill_expression [,max_bytes_to_fill]] .align32 align_expression [ , 4byte_fill_expression [,max_bytes_to_fill]]
示例:
# 以16(2^4)字节的方式对齐,不足的使用0x90补齐 .p2align 4, 0x90
-
CFA
在栈上分配的内存区域,称为“调用帧”。调用帧由栈上的地址标识。我们将此地址称为CFA(
Canonical Frame Address
)。通常,将CFA
定义为前一帧调用者上的栈指针的值(可能与当前帧的值不同)。An area of memory that is allocated on a stack called a “call frame.” The call frame is identified by an address on the stack. We refer to this address as the Canonical Frame Address or CFA. Typically, the CFA is defined to be the value of the stack pointer at the call site in the previous frame (which may be different from its value on entry to the current frame).引自DWARF规范-6.4
.cfi_def_cfa_offset OFFSET
:cfi_def_cfa_offset
指令用来修改计算CFA
的规则。注意:OFFSET
是绝对偏移量,它会被加到帧指针寄存%ebp
或者%rbp
上,重新计算CFA
的地址。.cfi_def_cfa REGISTER, OFFSET
:cfi_def_cfa
这个指令从寄存器中获取地址并且加上这个OFFSET
。-
.cfi_def_cfa_register REGISTER
:cfi_def_cfa_register
这个指令让%ebp
或%rbp
被设置为新值且偏移量保持不变。上述设置只是为了用来辅助调试的,比如打断点,获取调用堆栈信息。
-
CFI
调用帧信息,英文全称:
Call Frame Information
。-
cfi_startproc
,表示函数或过程开始。 -
.cfi_endproc
,表示函数或过程结束。
-
更多细节请查看苹果官网
汇编器
这个阶段主要任务是运行目标架构的汇编程序(汇编器),将编译器的输出转换为目标架构的目标(object
)文件,即:.o
文件。
终端输入:
# -c : Run all of the above, plus the assembler, generating a target ".o" object file.
# -o : write to file
clang -c main.m -o main.o
clang -c Person.m -o person.o
输出结果:
#使用命令查看生成文件
#file main.o person.o
#输出
main.o: Mach-O 64-bit object x86_64
person.o: Mach-O 64-bit object x86_64
通过汇编器将可读的汇编代码,转换为目标架构的目标文件,最终输出.o
文件,也称机器码。
链接器
这个阶段会运行目标架构的链接器,将多个object
文件合并成一个可执行文件或动态库。最终的输出a.out
、.dylib
或.so
。
在上述OC
代码示例中,Main
函数中引用了Person
类,因此若要生成可执行的文件,需要将main.o
与person.o
进行链接
终端输入:
# no stage selection option
# If no stage selection option is specified, all stages above are run, and the
# linker is run to combine the results into an executable or shared library.
clang main.o person.o -o main
输出结果:
"_NSLog", referenced from:
_main in main.o
-[Person share] in person.o
"_OBJC_CLASS_$_NSObject", referenced from:
_OBJC_CLASS_$_Person in person.o
"_OBJC_METACLASS_$_NSObject", referenced from:
_OBJC_METACLASS_$_Person in person.o
"___CFConstantStringClassReference", referenced from:
CFString in main.o
CFString in main.o
CFString in main.o
CFString in person.o
"__objc_empty_cache", referenced from:
_OBJC_METACLASS_$_Person in person.o
_OBJC_CLASS_$_Person in person.o
"_objc_alloc_init", referenced from:
_main in main.o
"_objc_autoreleasePoolPop", referenced from:
_main in main.o
"_objc_autoreleasePoolPush", referenced from:
_main in main.o
"_objc_msgSend", referenced from:
_main in main.o
ld: symbol(s) not found for architecture x86_64
clang-12: error: linker command failed with exit code 1 (
链接器未找到上述的符号,原因是我们代码引入了Foundation
库,在生成可执行文件时,未进行链接。
在解决这个问题之前先介绍一下工具xcrun
,使用xcrun
可以从命令行定位和调用开发者工具
#--show-sdk-path : show selected SDK install path
xcrun --show-sdk-path
# 输出`MacOSX.sdk`的路径
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk
基于此路径链接我们的Foundation
库:
# -Wl, Pass the comma separated arguments in to the linker #传参给链接器
# `xcrun --show-sdk-path` 等同 $(xcrun --show-sdk-path) 视为命令替换
clang main.o person.o -Wl,`xcrun --show-sdk-path`/System/Library/Frameworks/Foundation.framework/Foundation -o main
最终输出如下图:
执行这个可执行文件:
#执行
./main
#输出
2021-05-08 17:40:45.134 main[30561:1257231] Hello, World!
2021-05-08 17:40:45.135 main[30561:1257231] 你好,世界
2021-05-08 17:40:45.135 main[30561:1257231] 持之以恒
main
文件查看:
file main
#输出
main: Mach-O 64-bit executable x86_64
符号对比
符号表查看工具nm
,允许我们查看Object
文件的符号表内容。
-
使用
nm
终端工具,先观察一下mian.o
和person.o
#输入 nm -nm main.o person.o #输出 (undefined) external _NSLog (undefined) external _OBJC_CLASS_$_Person (undefined) external ___CFConstantStringClassReference (undefined) external _objc_alloc_init (undefined) external _objc_autoreleasePoolPop (undefined) external _objc_autoreleasePoolPush (undefined) external _objc_msgSend 0000000000000000 (__TEXT,__text) external _main 00000000000000e8 (__TEXT,__ustring) non-external l_.str.3 00000000000000f8 (__DATA,__objc_classrefs) non-external _OBJC_CLASSLIST_REFERENCES_$_ 0000000000000108 (__DATA,__objc_selrefs) non-external _OBJC_SELECTOR_REFERENCES_ (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 -[Person share] 0000000000000024 (__TEXT,__ustring) non-external l_.str 0000000000000058 (__DATA,__objc_const) non-external __OBJC_METACLASS_RO_$_Person 00000000000000a0 (__DATA,__objc_const) non-external __OBJC_$_INSTANCE_METHODS_Person 00000000000000c0 (__DATA,__objc_const) non-external __OBJC_CLASS_RO_$_Person 0000000000000108 (__DATA,__objc_data) external _OBJC_METACLASS_$_Person 0000000000000130 (__DATA,__objc_data) external _OBJC_CLASS_$_Person
external
表示该符号针对当前目标文件不是私有的,与non-external
相反。undefined
表示该符号未找到。 -
使用
nm
观察一下可执行文件main
的符号表#输入 nm -nm main #输出 (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 0000000100003e80 (__TEXT,__text) external _main #私有符号 0000000100003f00 (__TEXT,__text) non-external -[Person share] 0000000100008020 (__DATA,__objc_const) non-external __OBJC_METACLASS_RO_$_Person 0000000100008068 (__DATA,__objc_const) non-external __OBJC_$_INSTANCE_METHODS_Person 0000000100008088 (__DATA,__objc_const) non-external __OBJC_CLASS_RO_$_Person #非私有 00000001000080e0 (__DATA,__objc_data) external _OBJC_METACLASS_$_Person 0000000100008108 (__DATA,__objc_data) external _OBJC_CLASS_$_Person #私有符号 0000000100008130 (__DATA,__data) non-external __dyld_private
可以发现在经过链接器处理后,为每个符号增加了来源。当我们运行可执行文件时,会由动态链接器dyld
通过这些来源对处于undifined
的符号进行解析,比如_NSLog
,来自Foundation
,在运行时会在Foundation
中找到指向它的函数地址,并最终调用执行。
系统符号
目标文件的显示工具otool
,可以查看Mach-O
文件特定Section
和Segment
的内容。
-
可执行文件是知道它需要链接那些库的
# -L :display the names and version numbers of the shared libraries that the object file uses otool -L main # 输出 main: /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)
上述输出我们发现在链接器生成可执行文件时,我们通过
-Wl
传递给链接器的Foundation
的路径与可执行文件最终链接的Foundation
路径不一致。参数路径下的文件内容:
-
.tbd
文件the .tbd files are new "text-based stub libraries", that provide a much more compact version of the stub libraries for use in the SDK, and help to significantly reduce its download size. 引自stackoverflow
.tbd
是个文本文件,提供的是SDK
的更简洁版本,明显的降低Xcode
的下载大小,具体内容:.tbd
文件包含了与文件本身相关的元数据,与架构相关的信息,还有Foundation
库针对特定架构的symbols
,以及该库所依赖的库。并指定了Foundation
库的最终安装路径。 -
查看系统符号
#输入 nm -nm /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation | grep '_NSLog' #输出NSlog的调用地址 000000000004ce6e (__TEXT,__text) external _NSLog
总结
OC
代码编译时,首先会经过预处理,接着进行词法分析将文本字符串Token
化, 再通过语法与语义分析检查代码的类型与格式,最终生成AST
,并在代码优化与生成阶段,将AST
转换为底层的中间代码LLVM IR
,并最终生成目标架构的汇编代码,交给汇编器进行处理后,将可读的汇编代码转换为目标架构的机器码,即:.O
文件,通过链接器,解决.O
文件与库的链接问题,最终根据特定的机器架构生成可执行文件。
参考资料
http://www.aosabook.org/en/llvm.html
https://en.m.wikibooks.org/wiki/GNU_C_Compiler_Internals/GNU_C_Compiler_Architecture
http://www.yinwang.org/blog-cn/2015/09/19/parser
https://objccn.io/issue-6-3/
https://llvm.org/docs/LangRef.html