这几天 iOS 的朋友圈被戴老师的新书《跟戴铭学iOS编程》刷屏了。这几天读完了这本书的第二章编译器,结合《编译原理》和《程序员的自我修养》—— 链接、装载与库 这两本书,决定写此文普及一下程序员从写代码,到呈现到手机上一个个活灵活现的 App ,都经历了哪些过程。

在现实生活中有这么一类人,他们通常很忙,每天都在电脑面前写着称之为代码的东西。下面是小明写的一段 Objective-C 代码:


小明所写的代码使用的 Objective-C 语言,这是苹果公司用来开发苹果软件指定的语言,它建立在 C 语言基础之上,苹果通过 runtime 的机制让它成为了一门面向对象的语言,实现了比如类、继承、多态等多种特性。
小明写完代码后,通过点击执行按钮后,就可以喝着咖啡,等待 Xcode 来做剩余的事情了,当点击执行按钮到 App 展示,Xcode 都做了哪些事情呢?
一、预编译(Preprocess)
预编译体现在一个「预」字,编译之前要干点啥。这件事情是编译器 clang 干的事情,以前总是傻傻的分不清啥是 LLVM,啥是 clang。你可以把 LLVM 看做是一个“傻大个”,能力大,啥都干,而 clang 就是他的一个小弟,负责编译 C、C++ 和 Objective-C 代码的。
预处理主要干小明写下的 “#” 开头的代码,比如下面这些:

clang -E -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk ViewController.m > 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 8store 表示写入store %0* %0, %0** %5, align 8store i8* %1, i8** %6, align 8store %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) #2store %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 0store 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 1store i8* %20, i8** %21, align 8%22 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_.28, align 8, !invariant.load !10call 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) #2ret void}
在生成中间代码的过程中,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, -16movq %rsp, %rbp.cfi_def_cfa_register %rbpsubq $64, %rspmovq %rdi, -8(%rbp)movq %rsi, -16(%rbp)movq $0, -24(%rbp)leaq -24(%rbp), %rsimovq %rsi, %rdimovq %rdx, %rsimovq %rcx, -56(%rbp) ## 8-byte Spillcallq _objc_storeStrongmovq $0, -32(%rbp)leaq -32(%rbp), %rcxmovq -56(%rbp), %rdx ## 8-byte Reloadmovq %rcx, %rdimovq %rdx, %rsicallq _objc_storeStrongmovq -8(%rbp), %rcxmovq -24(%rbp), %rdxmovq -32(%rbp), %rsimovq %rcx, -48(%rbp)movq L_OBJC_CLASSLIST_SUP_REFS_$_(%rip), %rcxmovq %rcx, -40(%rbp)movq L_OBJC_SELECTOR_REFERENCES_.28(%rip), %rcxleaq -48(%rbp), %rdimovq %rsi, -64(%rbp) ## 8-byte Spillmovq %rcx, %rsimovq -64(%rbp), %rcx ## 8-byte Reloadcallq _objc_msgSendSuper2leaq L__unnamed_cfstring_.30(%rip), %rcxmovq -8(%rbp), %rdxmovq _OBJC_IVAR_$_ViewController._label(%rip), %rsimovq (%rdx,%rsi), %rdxmovq L_OBJC_SELECTOR_REFERENCES_.8(%rip), %rsimovq %rdx, %rdimovq %rcx, %rdxcallq *_objc_msgSend@GOTPCREL(%rip)xorl %eax, %eaxmovl %eax, %esileaq -32(%rbp), %rcxmovq %rcx, %rdicallq _objc_storeStrongxorl %eax, %eaxmovl %eax, %esileaq -24(%rbp), %rcxmovq %rcx, %rdicallq _objc_storeStrongaddq $64, %rsppopq %rbpretq.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 mainld: warning: building for macOS, but linking in object file (ViewController.o) built for iOS SimulatorUndefined 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
