前言
一般可以将编程语言分为两种,编译语言和直译式语言。
像C++,Objective C都是编译语言。编译语言在执行的时候,必须先通过编译器生成机器码,机器码可以直接在CPU上执行,所以执行效率较高。
像JavaScript,Python都是直译式语言。直译式语言不需要经过编译的过程,而是在执行的时候通过一个中间的解释器将代码解释为CPU可以执行的代码。所以,较编译语言来说,直译式语言效率低一些,但是编写的更灵活,也就是为啥JS大法好。
iOS开发目前的常用语言是:Objective和Swift。二者都是编译语言,换句话说都是需要编译才能执行的。二者的编译都是依赖于Clang + LLVM. 篇幅限制,本文只关注Objective C,因为原理上大同小异。
Clang和LLVM
不管是OC还是Swift,都是采用Clang作为编译器前端,LLVM(Low level vritual machine)作为编译器后端。所以简单的编译过程如下:
Clang编译过程
预处理: 预处理器会处理源文件中的宏定义,将代码中的宏用其对应定义的具体内容进行替换,删除注释,展开头文件,产生 .i 文件。
词法分析:预处理完成了以后,开始词法分析,这里会把代码切成一个个 Token,比如大小括号,等于号还有字符串等。
语法分析: 语法分析,在 Clang 中由 Parser 和 Sema 两个模块配合完成,验证语法是否正确,根据当前语言的语法,生成语意节点,并将所有节点组合成抽象语法树 AST。
静态分析: 一旦编译器把源码生成了抽象语法树,编译器可以对这棵树做分析处理,以找出代码中的错误,比如类型检查:即检查程序中是否有类型错误。例如:如果代码中给某个对象发送了一个消息,编译器会检查这个对象是否实现了这个消息(函数、方法)。此外,clang 对整个程序还做了其它更高级的一些分析,以确保程序没有错误。
类型检查:一般会把类型检查分为两类:动态的和静态的。动态的在运行时做检查,静态的在编译时做检查。以往,编写代码时可以向任意对象发送任何消息,在运行时,才会检查对象是否能够响应这些消息。由于只是在运行时做此类检查,所以叫做动态类型。至于静态类型,是在编译时做检查。当在代码中使用 ARC 时,编译器在编译期间,会做许多的类型检查:因为编译器需要知道哪个对象该如何使用。
目标代码的生成与优化: CodeGen 负责将语法树 AST 丛顶至下遍历,翻译成 LLVM IR 中间码,LLVM IR 中间码编译过程的前端的输出后端的输入。编译器后端主要包括代码生成器、代码优化器。代码生成器将中间代码转换为目标代码,代码优化器主要是进行一些优化,比如删除多余指令,选择合适寻址方式等,如果开启了 bitcode 苹果会做进一步的优化,有新的后端架构还是可以用这份优化过的 bitcode 去生成。优化中间代码生成输出汇编代码,把之前的 .i 文件转换为汇编语言,产生 .s 文件.
LLVM编译过程
汇编: 目标代码需要经过汇编器处理,把汇编语言文件转换为机器码文件,产生 .o 文件。
链接: 对 .o 文件中的对于其他的库的引用的地方进行引用,生成最后的可执行文件(同时也包括多个 .o 文件进行 link)。链接又分为静态链接和动态链接。
- 静态链接:在编译链接期间发挥作用,把目标文件和静态库一起链接形成可执行文件.
- 动态链接:链接过程推迟到运行时再进行.
如果多个程序都用到了一个库,那么每个程序都要将其链接到可执行文件中,非常冗余,动态链接的话,多个程序可以共享同一段代码,不需要在磁盘上存多份拷贝,但是动态链接发生在启动或运行时,增加了启动时间,造成一些性能的影响。
静态库不方便升级,必须重新编译,动态库的升级更加方便。
代码案列
上面总结了编译的流程,接下来我们用实际的代码来看看具体的转化流程.首先创建一个main.m文件
#import <Foundation/Foundation.h>
//来个注释
#define DEBUG 1
int main(){
#ifdef DEBUG
NSLog(@"DEBUG模式");
#else
NSLog(@"RELEASE模式");
#endif
return 0;
}
预处理
预处理器会处理源文件中的宏定义,将代码中的宏用其对应定义的具体内容进行替换,删除注释,展开头文件,产生 .i 文件。
'#import <Foundation/Foundation.h>'这一行是告诉预处理器将这行用Foundation.h中的内容替换.这个过程是递归的,因为Foundation.h中也import了其他文件.使用clang查看预处理结果
xcrun clang -E main.m
与处理后的文件会有很多代码.其中基本上都是引用的其他文件然后被递归替换的内容.划到最底部可以看到main函数.
int main(){
NSLog(@"DEBUG模式");
return 0;
}
同时,我们也可以发现,在这个阶段,我们所写的注释被删除,条件编译也被处理了.
词法分析
词法分析器读入源文件的字符流,将他们组织称有意义的词素(lexeme)序列,对于每个词素,此法分析器产生词法单元(token)作为输出。
$ xcrun clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
输出
annot_module_include '#import <Foundation/Foundation.h>
//' Loc=<main.m:2:1>
int 'int' [StartOfLine] Loc=<main.m:5:1>
identifier 'main' [LeadingSpace] Loc=<main.m:5:5>
l_paren '(' Loc=<main.m:5:9>
r_paren ')' Loc=<main.m:5:10>
l_brace '{' Loc=<main.m:5:11>
identifier 'NSLog' [StartOfLine] [LeadingSpace] Loc=<main.m:7:5>
l_paren '(' Loc=<main.m:7:10>
at '@' Loc=<main.m:7:11>
string_literal '"DEBUG模式"' Loc=<main.m:7:12>
r_paren ')' Loc=<main.m:7:25>
semi ';' Loc=<main.m:7:26>
return 'return' [StartOfLine] [LeadingSpace] Loc=<main.m:11:5>
numeric_constant '0' [LeadingSpace] Loc=<main.m:11:12>
semi ';' Loc=<main.m:11:13>
r_brace '}' [StartOfLine] Loc=<main.m:12:1>
eof '' Loc=<main.m:12:2>
Loc=<main.m:2:1>标示这个token位于源文件main.m的第2行,从第1个字符开始。保存token在源文件中的位置是方便后续clang分析的时候能够找到出错的原始位置。
语法分析
语法分析,在 Clang 中由 Parser 和 Sema 两个模块配合完成,验证语法是否正确,根据当前语言的语法,生成语意节点,并将所有节点组合成抽象语法树 AST.简单点来说,就是将词法分析的Token流会被解析成一颗抽象语法树.
$ xcrun clang -fsyntax-only -Xclang -ast-dump main.m | open -f
得到的AST结构,部分如下
�[0;34m| |-�[0m�[0;32mBuiltinType�[0m�[0;33m 0x7fa22903ae60�[0m �[0;32m'void'�[0m
�[0;34m| |-�[0m�[0;32mAttributedType�[0m�[0;33m 0x7fa22a204fc0�[0m �[0;32m'id _Nullable'�[0m sugar
�[0;34m| | |-�[0m�[0;32mTypedefType�[0m�[0;33m 0x7fa22a204310�[0m �[0;32m'id'�[0m sugar
�[0;34m| | | |-�[0m�[0;1;32mTypedef�[0m�[0;33m 0x7fa22903b898�[0m�[0;1;36m 'id'�[0m
�[0;34m| | | `-�[0m�[0;32mObjCObjectPointerType�[0m�[0;33m 0x7fa22903b840�[0m �[0;32m'id'�[0m
�[0;34m| | | `-�[0m�[0;32mObjCObjectType�[0m�[0;33m 0x7fa22903b810�[0m �[0;32m'id'�[0m
�[0;34m| | `-�[0m�[0;32mTypedefType�[0m�[0;33m 0x7fa22a204310�[0m �[0;32m'id'�[0m sugar
�[0;34m| | |-�[0m�[0;1;32mTypedef�[0m�[0;33m 0x7fa22903b898�[0m�[0;1;36m 'id'�[0m
�[0;34m| | `-�[0m�[0;32mObjCObjectPointerType�[0m�[0;33m 0x7fa22903b840�[0m �[0;32m'id'�[0m
�[0;34m| | `-�[0m�[0;32mObjCObjectType�[0m�[0;33m 0x7fa22903b810�[0m �[0;32m'id'�[0m
�[0;34m| `-�[0m�[0;32mAttributedType�[0m�[0;33m 0x7fa22a3925f0�[0m �[0;32m'NSError * _Nullable'�[0m sugar
�[0;34m| |-�[0m�[0;32mObjCObjectPointerType�[0m�[0;33m 0x7fa22a3925b0�[0m �[0;32m'NSError *'�[0m
�[0;34m| | `-�[0m�[0;32mObjCInterfaceType�[0m�[0;33m 0x7fa22a103b30�[0m �[0;32m'NSError'�[0m
�[0;34m| | `-�[0m�[0;1;32mObjCInterface�[0m�[0;33m 0x7fa22a527de0�[0m�[0;1;36m 'NSError'�[0m
�[0;34m| `-�[0m�[0;32mObjCObjectPointerType�[0m�[0;33m 0x7fa22a3925b0�[0m �[0;32m'NSError *'�[0m
�[0;34m| `-�[0m�[0;32mObjCInterfaceType�[0m�[0;33m 0x7fa22a103b30�[0m �[0;32m'NSError'�[0m
�[0;34m| `-�[0m�[0;1;32mObjCInterface�[0m�[0;33m 0x7fa22a527de0�[0m�[0;1;36m 'NSError'�[0m
有了抽象语法树,Clang就可以对这个树进行分析,找出代码中的错误。Clang Static Analyzer是开源编译器前端clang中内置的针对C,C++和Objective-C源代码的静态分析工具,能提供普通warning之外的检查,涵盖内存操作,安全等方面。这部分功能可通过clang --analyze命令或者库文件等方式调用.由于需要实现checker.这一步我们先过掉.有兴趣的话可以在做研究.
目标代码的生成与优化
CodeGen 会负责将语法树自顶向下遍历逐步翻译成 LLVM IR,IR 是编译过程的前端的输出,也是后端的输入。 Objective C代码也在这一步会进行runtime的桥接:property合成,ARC处理等。
clang -S -fobjc-arc -emit-llvm main.m -o main.ll
得到的的main.ll内容而下
; ModuleID = 'main.m'
source_filename = "main.m"
target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.15.0"
%struct.__NSConstantString_tag = type { i32*, i32, i8*, i64 }
@__CFConstantStringClassReference = external global [0 x i32]
@.str = private unnamed_addr constant [8 x i16] [i16 68, i16 69, i16 66, i16 85, i16 71, i16 27169, i16 24335, i16 0], section "__TEXT,__ustring", align 2
@_unnamed_cfstring_ = private global %struct.__NSConstantString_tag { i32* getelementptr inbounds ([0 x i32], [0 x i32]* @__CFConstantStringClassReference, i32 0, i32 0), i32 2000, i8* bitcast ([8 x i16]* @.str to i8*), i64 7 }, section "__DATA,__cfstring", align 8 #0
; Function Attrs: noinline optnone ssp uwtable
define i32 @main() #1 {
%1 = alloca i32, align 4
store i32 0, i32* %1, align 4
notail call void (i8*, ...) @NSLog(i8* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_ to i8*))
ret i32 0
}
declare void @NSLog(i8*, ...) #2
attributes #0 = { "objc_arc_inert" }
attributes #1 = { noinline optnone ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "darwin-stkchk-strong-link" "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" "probe-stack"="___chkstk_darwin" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+cx8,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #2 = { "correctly-rounded-divide-sqrt-fp-math"="false" "darwin-stkchk-strong-link" "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"="false" "probe-stack"="___chkstk_darwin" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+cx8,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
!llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7}
!llvm.ident = !{!8}
!0 = !{i32 2, !"SDK Version", [3 x i32] [i32 10, i32 15, i32 4]}
!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 Class Properties", i32 64}
!6 = !{i32 1, !"wchar_size", i32 4}
!7 = !{i32 7, !"PIC Level", i32 2}
!8 = !{!"Apple clang version 11.0.3 (clang-1103.0.32.62)"}
中间代码生成后,需要将LLVM代码转化为汇编语言,生成.s文件交给后面的汇编器处理.
clang -S -fobjc-arc main.m -o main.s
使用上面命令行的到汇编文件,部分内容如下
.section __TEXT,__text,regular,pure_instructions
.build_version macos, 10, 15 sdk_version 10, 15, 4
.globl _main ## -- Begin function main
.p2align 4, 0x90
_main: ## @main
.cfi_startproc
## %bb.0:
汇编
目标代码需要经过汇编器处理,把汇编语言文件转换为机器码文件,产生 .o 文件(object file)。
clang -fmodules -c main.m -o main.o
使用命令行查看main.o文件
nm -nm main.o
输出
(undefined) external _NSLog
(undefined) external ___CFConstantStringClassReference
0000000000000000 (__TEXT,__text) external _main
0000000000000028 (__TEXT,__ustring) non-external l_.str
这里可以看到_NSLog是一个是undefined external的。undefined表示在当前文件暂时找不到符号_NSLog,而external表示这个符号是外部可以访问的,对应表示文件私有的符号是non-external。
链接生成可执行文件
拿到.o机器码文件后,需要对 .o 文件中的对于其他的库的引用的地方进行引用,生成最后的match-o可执行文件.
clang main.o -o main
当然,这个命令行是封装完成的.内部是使用
cc main.o -framework Foundation
来链接其他库的.
最终可以拿到我们的执行文件.运行 ./
得到输出结果 "DEBUG模式".
我们查看可执行文件的符号表
U _NSLog
U ___CFConstantStringClassReference
0000000100002008 d __dyld_private
0000000100000000 T __mh_execute_header
0000000100000f50 T _main
U dyld_stub_binder
关于match-o文件里的符号表的解释,会专门在出一篇文章来做解释.
从上我们可以大致了解了,iOS代码带match-o可执行文件的整个过程.
了解这些知识后,在深入研究可以解决很多问题,譬如:
- 自动化打包;
- 在拿到AST后对代码规范进行review;
- 提高项目编译速度
...