这几天 iOS 的朋友圈被戴老师的新书《跟戴铭学iOS编程》刷屏了。这几天读完了这本书的第二章编译器,结合《编译原理》和《程序员的自我修养》—— 链接、装载与库 这两本书,决定写此文普及一下程序员从写代码,到呈现到手机上一个个活灵活现的 App ,都经历了哪些过程。
在现实生活中有这么一类人,他们通常很忙,每天都在电脑面前写着称之为代码的东西。下面是小明写的一段 Objective-C 代码:
当小明点击一个称之为运行的按钮时,神奇的事情发生了,只见他手机上出现了下面的页面。标题是 520,页面中间还显示着一行文字“我是小明”,当小明触摸手机的时候文字变为了“别摸我”。
从代码到最终展现到小明面前的视觉效果,究竟做了哪些事情呢?
在小明写代码的时候,有一个称之为 Xcode 的工具,我们把它看做是一个生产车间,负责各种任务的处理。小明所写的代码仅仅是这个车间流水线的一小部分。
小明所写的代码使用的 Objective-C 语言,这是苹果公司用来开发苹果软件指定的语言,它建立在 C 语言基础之上,苹果通过 runtime 的机制让它成为了一门面向对象的语言,实现了比如类、继承、多态等多种特性。
小明写完代码后,通过点击执行按钮后,就可以喝着咖啡,等待 Xcode 来做剩余的事情了,当点击执行按钮到 App 展示,Xcode 都做了哪些事情呢?
一、预编译(Preprocess)
预编译体现在一个「预」字,编译之前要干点啥。这件事情是编译器 clang 干的事情,以前总是傻傻的分不清啥是 LLVM,啥是 clang。你可以把 LLVM 看做是一个“傻大个”,能力大,啥都干,而 clang 就是他的一个小弟,负责编译 C、C++ 和 Objective-C 代码的。
预处理主要干小明写下的 “#” 开头的代码,比如下面这些:
我总结了下预处理主要干下面这些事情(主要来源 《程序员的自我修养》—— 链接、装载与库 这本书):
1、将所有的 #define 删除,并展开所有的宏定义;
2、处理有条件编译的指令,比如 #if、#ifdef、#elif;
3、处理 #import 、#include,会将包含的文件插入到该 #import 的位置,这个过程是递归进行的,因为被引用的文件可能会引用其它文件;
4、删除注释(曾经以为代码中写了 JSPatch 这个注释,会被苹果发现,可笑);
5、添加行号和文件名的标识,方便调试获取代码的行号,保留源代码的原始信息;
6、保留 #pragma 编译指令,因为编译器会使用他们;
clang 是一个编译器,它提供和很多命令来让代码变个戏法。比如下面使用 clang 命令来做一下预编译:
clang -E -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk ViewController.m > ViewController.i
执行完这条命令后,预处理结果保存到了 ViewController.i 这个文件中。
这个过程也可以通过 Xcode 直接操作来查看效果:
二、编译
编译这个过程比较复杂,主要由词法分析、语法分析和语义分析。
1、词法分析(lexical analysis)
小明写的代码其实是一些「有规则」的字符串,那么如何才能把这些有规则的字符进行解析呢?比如有的人是这样写代码的:
不管程序员如何写代码,只要符合规则就行。clang 可以把这些“字符串”解析成一个一个 token,没啥高深的,你完全可以理解为类似于分词的功能。执行命令:
clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk -E -Xclang -dump-tokens ViewController.m
看下图,通过 Loc 记录 token 所在文件的位置 :
这些 token 的定义可以在 clang 源码中的 TokenKinds.def 文件中找到。
到这里你应该知道为啥 Xcode 能够对关键字高亮显示了吧,因为每一个关键字都可以被标记成一个独立的个体。
2、语法分析(syntax analysis)
有了这些 token 以后,需要进行语法分析,生成抽象语法树,简单来说每个树的节点就是一个表达式。而语言的本身就是由一个一个表达式组合而成。
执行命令:
clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk -Xclang -ast-dump -fsyntax-only ViewController.m
函数对应下面的语法树:
3、语义分析(semantic analyzer)
使用语法树和符号表中的信息来检查源程序是否和语言定义的语义一致。它同时也收集类型信息,并把这些信息存放到语法树或符合表中,以便在随后的中间代码生成过程中使用。
《编译原理》
在语义分析阶段比较实用的例子是类型检查。
三、中间代码
1、生成中间代码
中间代码是编程语言的另一种表现方式,编译器能够编译的语言最终都会转换成同一种中间代码,不然 clang 为什么能够同时支持编译 C、C++、Objective-C 语言呢。抽象语法树已经是一种语言的另一种表现方式了,而中间代码是在抽象语法树的基础上生成的另一种中间表示形式,下面这段代码的中间代码是什么呢?
执行xia'm命令:
clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk -S -fobjc-arc -emit-llvm ViewController.m -o ViewController.ll
生成的中间代码,这是一种类似于机器语言的表示方式,能够轻松地被翻译成机器语言:
define internal void @"\01
-[ViewController touchesBegan:withEvent:]"(%0*, i8*, %6*, %7*) #0 {
%开头的代表局部变量
alloca 当前函数执行分配内存
align 表示占几位
%5 = alloca %0*, align 8
%6 = alloca i8*, align 8
%7 = alloca %6*, align 8
%8 = alloca %7*, align 8
%9 = alloca %struct._objc_super, align 8
store 表示写入
store %0* %0, %0** %5, align 8
store i8* %1, i8** %6, align 8
store %6* null, %6** %7, align 8
%10 = bitcast %6** %7 to i8**
%11 = bitcast %6* %2 to i8*
call void @llvm.objc.storeStrong(i8** %10, i8* %11) #2
store %7* null, %7** %8, align 8
%12 = bitcast %7** %8 to i8**
%13 = bitcast %7* %3 to i8*
call void @llvm.objc.storeStrong(i8** %12, i8* %13) #2
%14 = load %0*, %0** %5, align 8
%15 = load %6*, %6** %7, align 8
%16 = load %7*, %7** %8, align 8
%17 = bitcast %0* %14 to i8*
%18 = getelementptr inbounds %struct._objc_super, %struct._objc_super* %9, i32 0, i32 0
store i8* %17, i8** %18, align 8
%19 = load %struct._class_t*, %struct._class_t** @"OBJC_CLASSLIST_SUP_REFS_$_", align 8
%20 = bitcast %struct._class_t* %19 to i8*
%21 = getelementptr inbounds %struct._objc_super, %struct._objc_super* %9, i32 0, i32 1
store i8* %20, i8** %21, align 8
%22 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_.28, align 8, !invariant.load !10
call void bitcast (i8* (%struct._objc_super*, i8*, ...)* @objc_msgSendSuper2 to void (%struct._objc_super*, i8*, %6*, %7*)*)(%struct._objc_super* %9, i8* %22, %6* %15, %7* %16)
%23 = load %0*, %0** %5, align 8
%24 = load i64, i64* @"OBJC_IVAR_$_ViewController._label", align 8, !invariant.load !10
%25 = bitcast %0* %23 to i8*
%26 = getelementptr inbounds i8, i8* %25, i64 %24
%27 = bitcast i8* %26 to %1**
%28 = load %1*, %1** %27, align 8
%29 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_.8, align 8, !invariant.load !10
%30 = bitcast %1* %28 to i8*
call void bitcast (i8* (i8*, i8*, ...)* @objc_msgSend to void (i8*, i8*, %3*)*)(i8* %30, i8* %29, %3* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_.30 to %3*))
%31 = bitcast %7** %8 to i8**
call void @llvm.objc.storeStrong(i8** %31, i8* null) #2
%32 = bitcast %6** %7 to i8**
call void @llvm.objc.storeStrong(i8** %32, i8* null) #2
ret void
}
2、中间代码优化
在生成中间代码的过程中,clang 会对代码进行优化处理。
五、生成汇编代码
中间代码是一种与机器无关的表现形式。可以通过命令生成指定 CPU 架构的汇编代码:
clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk -S -fobjc-arc ViewController.m -o ViewController.s
"-[ViewController touchesBegan:withEvent:]": ## @"\01-[ViewController touchesBegan:withEvent:]"
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
subq $64, %rsp
movq %rdi, -8(%rbp)
movq %rsi, -16(%rbp)
movq $0, -24(%rbp)
leaq -24(%rbp), %rsi
movq %rsi, %rdi
movq %rdx, %rsi
movq %rcx, -56(%rbp) ## 8-byte Spill
callq _objc_storeStrong
movq $0, -32(%rbp)
leaq -32(%rbp), %rcx
movq -56(%rbp), %rdx ## 8-byte Reload
movq %rcx, %rdi
movq %rdx, %rsi
callq _objc_storeStrong
movq -8(%rbp), %rcx
movq -24(%rbp), %rdx
movq -32(%rbp), %rsi
movq %rcx, -48(%rbp)
movq L_OBJC_CLASSLIST_SUP_REFS_$_(%rip), %rcx
movq %rcx, -40(%rbp)
movq L_OBJC_SELECTOR_REFERENCES_.28(%rip), %rcx
leaq -48(%rbp), %rdi
movq %rsi, -64(%rbp) ## 8-byte Spill
movq %rcx, %rsi
movq -64(%rbp), %rcx ## 8-byte Reload
callq _objc_msgSendSuper2
leaq L__unnamed_cfstring_.30(%rip), %rcx
movq -8(%rbp), %rdx
movq _OBJC_IVAR_$_ViewController._label(%rip), %rsi
movq (%rdx,%rsi), %rdx
movq L_OBJC_SELECTOR_REFERENCES_.8(%rip), %rsi
movq %rdx, %rdi
movq %rcx, %rdx
callq *_objc_msgSend@GOTPCREL(%rip)
xorl %eax, %eax
movl %eax, %esi
leaq -32(%rbp), %rcx
movq %rcx, %rdi
callq _objc_storeStrong
xorl %eax, %eax
movl %eax, %esi
leaq -24(%rbp), %rcx
movq %rcx, %rdi
callq _objc_storeStrong
addq $64, %rsp
popq %rbp
retq
.cfi_endproc
## -- End function
.p2align 4, 0x90 ## -- Begin function -[ViewController label]
六、生成目标文件
clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk -c -fobjc-arc ViewController.m -o ViewController.o
七、生成可执行文件
可执行文件就是一个「二进制文件」,由多个目标文件最终链接生成一个可执行文件。在可执行文件中,每一段二进制代表什么含义都被提前定义好了。操作系统加载可执行文件的时候,会解析可执行文件。如果你对可执行文件可以查看 class-dump 的源码。
这时候如果你输入下面的命令发现会报错:
clang ViewController.o -o main
ld: warning: building for macOS, but linking in object file (ViewController.o) built for iOS Simulator
Undefined symbols for architecture x86_64:
"_OBJC_CLASS_$_NSNumber", referenced from:
objc-class-ref in ViewController.o
"_OBJC_CLASS_$_UIFont", referenced from:
objc-class-ref in ViewController.o
"_OBJC_CLASS_$_UILabel", referenced from:
objc-class-ref in ViewController.o
"_OBJC_CLASS_$_UIViewController", referenced from:
_OBJC_CLASS_$_ViewController in ViewController.o
由于这是个 iOS App 项目,需要链接系统相关的库,整个过程参考编译一个 App 时的链接过程:
八、签名
用来保证可执行文件、相关资源不能被修改。到此从源码到一个可执行文件的全部过程。
App 运行阶段
由于文章篇幅有限,操作系统是如何把可执行文件加载到内存中的,后续分析。
在生成可执行文件的过程中不只是本文提到的这些过程,还有其它比较细的地方需要读者慢慢挖掘,比如资源文件、静态库。
最后推荐一下关于编译原理相关的书籍:
参考:
《程序员的自我修养》—— 链接、装载与库
《编译原理》
https://llvm-tutorial-cn.readthedocs.io/en/latest/index.html
http://clang.llvm.org/docs/IntroductionToTheClangAST.html
http://clang.llvm.org/docs/index.html
推荐阅读:
普及一下“我”