[译]iOS编译器

编译器作些什么?

本文主要探讨一下编译器主要作些什么,以及如何有效的利用编译器。css

简单的说,编译器有两个职责:把 Objective-C 代码转化成低级代码,以及对代码作分析,确保代码中没有任何明显的错误。html

如今,Xcode 的默认编译器是 clang。本文中咱们提到的编译器都表示 clang。clang 的功能是首先对 Objective-C 代码作分析检查,而后将其转换为低级的类汇编代码:LLVM Intermediate Representation(LLVM 中间表达码)。接着 LLVM 会执行相关指令将 LLVM IR 编译成目标平台上的本地字节码,这个过程的完成方式能够是即时编译 (Just-in-time),或在编译的时候完成。git

LLVM 指令的一个好处就是能够在支持 LLVM 的任意平台上生成和运行 LLVM 指令。例如,你写的一个 iOS app, 它能够自动的运行在两个彻底不一样的架构(Inter 和 ARM)上,LLVM 会根据不一样的平台将 IR 码转换为对应的本地字节码。github

LLVM 的优势主要得益于它的三层式架构 -- 第一层支持多种语言做为输入(例如 C, ObjectiveC, C++ 和 Haskell),第二层是一个共享式的优化器(对 LLVM IR 作优化处理),第三层是许多不一样的目标平台(例如 Intel, ARM 和 PowerPC)。在这三层式的架构中,若是你想要添加一门语言到 LLVM 中,那么能够把重要精力集中到第一层上,若是想要增长另一个目标平台,那么你不必过多的考虑输入语言。在书 The Architecture of Open Source Applications 中 LLVM 的建立者 (Chris Lattner) 写了一章很棒的内容:关于 LLVM 架构objective-c

在编译一个源文件时,编译器的处理过程分为几个阶段。要想查看编译 hello.m 源文件须要几个不一样的阶段,咱们可让经过 clang 命令观察:macos

% clang -ccc-print-phases hello.m

0: input, "hello.m", objective-c
1: preprocessor, {0}, objective-c-cpp-output
2: compiler, {1}, assembler
3: assembler, {2}, object
4: linker, {3}, image
5: bind-arch, "x86_64", {4}, image
复制代码

本文咱们将重点关注第一阶段和第二阶段。在文章 Mach-O Executables 中,Daniel 会对第三阶段和第四阶段进行阐述。xcode

预处理

每当编源译文件的时候,编译器首先作的是一些预处理工做。好比预处理器会处理源文件中的宏定义,将代码中的宏用其对应定义的具体内容进行替换。架构

例如,若是在源文件中出现下述代码:app

#import <Foundation/Foundation.h>
复制代码

预处理器对这行代码的处理是用 Foundation.h 文件中的内容去替换这行代码,若是 Foundation.h 中也使用了相似的宏引入,则会按照一样的处理方式用各个宏对应的真正代码进行逐级替代。less

这也就是为何人们主张头文件最好尽可能少的去引入其余的类或库,由于引入的东西越多,编译器须要作的处理就越多。例如,在头文件中用:

@class MyClass;
复制代码

代替:

#import "MyClass.h"
复制代码

这么写是告诉编译器 MyClass 是一个类,而且在 .m 实现文件中能够经过 import MyClass.h 的方式来使用它。

假设咱们写了一个简单的 C 程序 hello.c:

#include <stdio.h>

int main() {
  printf("hello world\n");
  return 0;
}
复制代码

而后给上面的代码执行如下预处理命令,看看是什么效果:

clang -E hello.c | less
复制代码

接下来看看处理后的代码,一共是 401 行。若是将以下一行代码添加到上面代码的顶部::

#import <Foundation/Foundation.h>
复制代码

再执行一下上面的预处理命令,处理后的文件代码行数暴增至 89,839 行。这个数字比某些操做系统的总代码行数还要多。

幸亏,目前的状况已经改善许多了:引入了模块 - modules功能,这使预处理变得更加的高级。

自定义宏

咱们来看看另一种情形定义或者使用自定义宏,好比定义了以下宏:

#define MY_CONSTANT 4
复制代码

那么,凡是在此行宏定义做用域内,输入了 MY_CONSTANT,在预处理过程当中 MY_CONSTANT 都会被替换成 4。咱们定义的宏也是能够携带参数的, 好比:

#define MY_MACRO(x) x
复制代码

鉴于本文的内容所限,就不对强大的预处理作更多、更全面的展开讨论了。可是仍是要强调一点,建议你们不要在须要预处理的代码中加入内联代码逻辑。

例如,下面这段代码,这样用没什么问题:

#define MAX(a,b) a > b ? a : b

int main() {
  printf("largest: %d\n", MAX(10,100));
  return 0;
}
复制代码

可是若是换成这么写:

#define MAX(a,b) a > b ? a : b

int main() {
  int i = 200;
  printf("largest: %d\n", MAX(i++,100));
  printf("i: %d\n", i);
  return 0;
}
复制代码

clang max.c 编译一下,结果是:

largest: 201
i: 202
复制代码

clang -E max.c 进行宏展开的预处理结果是以下所示:

int main() {
  int i = 200;
  printf("largest: %d\n", i++ > 100 ? i++ : 100);
  printf("i: %d\n", i);
  return 0;
}
复制代码

本例是典型的宏使用不当,并且一般这类问题很是隐蔽且难以 debug 。针对本例这类状况,最好使用 static inline:

#include <stdio.h>
static const int MyConstant = 200;

static inline int max(int l, int r) {
   return l > r ? l : r;
}

int main() {
  int i = MyConstant;
  printf("largest: %d\n", max(i++,100));
  printf("i: %d\n", i);
  return 0;
}
复制代码

这样改过以后,就能够输出正常的结果 (i:201)。由于这里定义的代码是内联的 (inlined),因此它的效率和宏变量差很少,可是可靠性比宏定义要好许多。再者,还能够设置断点、类型检查以及避免异常行为。

基本上,宏的最佳使用场景是日志输出,可使用 __FILE____LINE__ 和 assert 宏。

词法解析标记

预处理完成之后,每个 .m 源文件里都有一堆的声明和定义。这些代码文本都会从 string 转化成特殊的标记流。

例如,下面是一段简单的 Objective-C hello word 程序:

int main() {
  NSLog(@"hello, %@", @"world");
  return 0;
}
复制代码

利用 clang 命令 clang -Xclang -dump-tokens hello.m 来将上面代码的标记流导出:

int 'int'        [StartOfLine]  Loc=<hello.m:4:1>
identifier 'main'        [LeadingSpace] Loc=<hello.m:4:5>
l_paren '('             Loc=<hello.m:4:9>
r_paren ')'             Loc=<hello.m:4:10>
l_brace '{'      [LeadingSpace] Loc=<hello.m:4:12>
identifier 'NSLog'       [StartOfLine] [LeadingSpace]   Loc=<hello.m:5:3>
l_paren '('             Loc=<hello.m:5:8>
at '@'          Loc=<hello.m:5:9>
string_literal '"hello, %@"'            Loc=<hello.m:5:10>
comma ','               Loc=<hello.m:5:21>
at '@'   [LeadingSpace] Loc=<hello.m:5:23>
string_literal '"world"'                Loc=<hello.m:5:24>
r_paren ')'             Loc=<hello.m:5:31>
semi ';'                Loc=<hello.m:5:32>
return 'return'  [StartOfLine] [LeadingSpace]   Loc=<hello.m:6:3>
numeric_constant '0'     [LeadingSpace] Loc=<hello.m:6:10>
semi ';'                Loc=<hello.m:6:11>
r_brace '}'      [StartOfLine]  Loc=<hello.m:7:1>
eof ''          Loc=<hello.m:7:2>
复制代码

仔细观察能够发现,每个标记都包含了对应的源码内容和其在源码中的位置。注意这里的位置是宏展开以前的位置,这样一来,若是编译过程当中遇到什么问题,clang 可以在源码中指出出错的具体位置。

解析

接下来要说的东西比较有意思:以前生成的标记流将会被解析成一棵抽象语法树 (abstract syntax tree -- AST)。因为 Objective-C 是一门复杂的语言,所以解析的过程不简单。解析事后,源程序变成了一棵抽象语法树:一棵表明源程序的树。假设咱们有一个程序 hello.m

#import <Foundation/Foundation.h>

@interface World
- (void)hello;
@end

@implementation World
- (void)hello {
  NSLog(@"hello, world");
}
@end

int main() {
   World* world = [World new];
   [world hello];
}
复制代码

当咱们执行 clang 命令 clang -Xclang -ast-dump -fsyntax-only hello.m 以后,命令行中输出的结果以下所示::

@interface World- (void) hello;
@end
@implementation World
- (void) hello (CompoundStmt 0x10372ded0 <hello.m:8:15, line:10:1>
  (CallExpr 0x10372dea0 <line:9:3, col:24> 'void'
    (ImplicitCastExpr 0x10372de88 <col:3> 'void (*)(NSString *, ...)' <FunctionToPointerDecay>
      (DeclRefExpr 0x10372ddd8 <col:3> 'void (NSString *, ...)' Function 0x1023510d0 'NSLog' 'void (NSString *, ...)'))
    (ObjCStringLiteral 0x10372de38 <col:9, col:10> 'NSString *'
      (StringLiteral 0x10372de00 <col:10> 'char [13]' lvalue "hello, world"))))


@end
int main() (CompoundStmt 0x10372e118 <hello.m:13:12, line:16:1>
  (DeclStmt 0x10372e090 <line:14:4, col:30>
    0x10372dfe0 "World *world =
      (ImplicitCastExpr 0x10372e078 <col:19, col:29> 'World *' <BitCast>
        (ObjCMessageExpr 0x10372e048 <col:19, col:29> 'id':'id' selector=new class='World'))")
  (ObjCMessageExpr 0x10372e0e8 <line:15:4, col:16> 'void' selector=hello
    (ImplicitCastExpr 0x10372e0d0 <col:5> 'World *' <LValueToRValue>
      (DeclRefExpr 0x10372e0a8 <col:5> 'World *' lvalue Var 0x10372dfe0 'world' 'World *'))))
复制代码

在抽象语法树中的每一个节点都标注了其对应源码中的位置,一样的,若是产生了什么问题,clang 能够定位到问题所在处的源码位置。

延伸阅读

静态分析

一旦编译器把源码生成了抽象语法树,编译器能够对这棵树作分析处理,以找出代码中的错误,好比类型检查:即检查程序中是否有类型错误。例如:若是代码中给某个对象发送了一个消息,编译器会检查这个对象是否实现了这个消息(函数、方法)。此外,clang 对整个程序还作了其它更高级的一些分析,以确保程序没有错误。

类型检查

每当开发人员编写代码的时候,clang 都会帮忙检查错误。其中最多见的就是检查程序是否发送正确的消息给正确的对象,是否在正确的值上调用了正确的函数。若是你给一个单纯的 NSObject* 对象发送了一个 hello 消息,那么 clang 就会报错。一样,若是你建立了 NSObject 的一个子类 Test, 以下所示:

@interface Test : NSObject
@end
复制代码

而后试图给这个子类中某个属性设置一个与其自身类型不相符的对象,编译器会给出一个可能使用不正确的警告。

通常会把类型分为两类:动态的和静态的。动态的在运行时作检查,静态的在编译时作检查。以往,编写代码时能够向任意对象发送任何消息,在运行时,才会检查对象是否可以响应这些消息。因为只是在运行时作此类检查,因此叫作动态类型。

至于静态类型,是在编译时作检查。当在代码中使用 ARC 时,编译器在编译期间,会作许多的类型检查:由于编译器须要知道哪一个对象该如何使用。例如,若是 myObject 没有 hello 方法,那么就不能写以下这行代码了:

[myObject hello]
复制代码

其余分析

clang 在静态分析阶段,除了类型检查外,还会作许多其它一些分析。若是你把 clang 的代码仓库 clone 到本地,而后进入目录 lib/StaticAnalyzer/Checkers,你会看到全部静态检查内容。好比 ObjCUnusedIVarsChecker.cpp 是用来检查是否有定义了,可是从未使用过的变量。而 ObjCSelfInitChecker.cpp 则是检查在 你的初始化方法中中调用 self 以前,是否已经调用 [self initWith...][super init] 了。编译器还进行了一些其它的检查,例如在 lib/Sema/SemaExprObjC.cpp 的 2,534 行,有这样一句:

Diag(SelLoc, diag::warn_arc_perform_selector_leaks);
复制代码

这个会生成严重错误的警告 “performSelector may cause a leak because its selector is unknown” 。

代码生成

clang 完成代码的标记,解析和分析后,接着就会生成 LLVM 代码。下面继续看看hello.c

#include <stdio.h>

int main() {
  printf("hello world\n");
  return 0;
}
复制代码

要把这段代码编译成 LLVM 字节码(绝大多数状况下是二进制码格式),咱们能够执行下面的命令:

clang -O3 -emit-LLVM hello.c -c -o hello.bc
复制代码

接着用另外一个命令来查看刚刚生成的二进制文件:

llvm-dis < hello.bc | less
复制代码

输出以下:

; ModuleID = '<stdin>'
target datalayout = "e-p:64:64:64-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:64:64-f32:32:32-f64:64:64-v64:64:64-v128:128:128-a0:0:64-s0:64:64-f80:128:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.8.0"

@str = private unnamed_addr constant [12 x i8] c"hello world\00"

; Function Attrs: nounwind ssp uwtable
define i32 @main() #0 {
  %puts = tail call i32 @puts(i8* getelementptr inbounds ([12 x i8]* @str, i64 0, i64 0))
  ret i32 0
}

; Function Attrs: nounwind
declare i32 @puts(i8* nocapture) #1

attributes #0 = { nounwind ssp uwtable }
attributes #1 = { nounwind }
复制代码

在上面的代码中,能够看到 main 函数只有两行代码:一行输出string,另外一行返回 0

再换一个程序,拿 five.m 为例,对其作相同的编译,而后执行 LLVM-dis < five.bc | less:

#include <stdio.h>
#import <Foundation/Foundation.h>

int main() {
  NSLog(@"%@", [@5 description]);
  return 0;
}
复制代码

抛开其余的不说,单看 main 函数:

define i32 @main() #0 {
  %1 = load %struct._class_t** @"\01L_OBJC_CLASSLIST_REFERENCES_$_", align 8
  %2 = load i8** @"\01L_OBJC_SELECTOR_REFERENCES_", align 8, !invariant.load !4
  %3 = bitcast %struct._class_t* %1 to i8*
  %4 = tail call %0* bitcast (i8* (i8*, i8*, ...)* @objc_msgSend to %0* (i8*, i8*, i32)*)(i8* %3, i8* %2, i32 5)
  %5 = load i8** @"\01L_OBJC_SELECTOR_REFERENCES_2", align 8, !invariant.load !4
  %6 = bitcast %0* %4 to i8*
  %7 = tail call %1* bitcast (i8* (i8*, i8*, ...)* @objc_msgSend to %1* (i8*, i8*)*)(i8* %6, i8* %5)
  tail call void (%1*, ...)* @NSLog(%1* bitcast (%struct.NSConstantString* @_unnamed_cfstring_ to %1*), %1* %7)
  ret i32 0
}
复制代码

上面代码中最重要的是第 4 行,它建立了一个 NSNumber 对象。第 7 行,给这个 number 对象发送了一个 description 消息。第 8 行,将 description 消息返回的内容打印出来。

优化

要想了解 LLVM 的优化内容,以及 clang 能作哪些优化,咱们先看一个略微复杂的 C 程序:这个函数主要是递归计算 阶乘

#include <stdio.h>

int factorial(int x) {
   if (x > 1) return x * factorial(x-1);
   else return 1;
}

int main() {
  printf("factorial 10: %d\n", factorial(10));
}
复制代码

先看看不作优化的编译状况,执行下面命令:

clang -O0 -emit-llvm factorial.c  -c -o factorial.bc && llvm-dis < factorial.bc
复制代码

重点看一下针对 阶乘 部分生成的代码:

define i32 @factorial(i32 %x) #0 {
  %1 = alloca i32, align 4
  %2 = alloca i32, align 4
  store i32 %x, i32* %2, align 4
  %3 = load i32* %2, align 4
  %4 = icmp sgt i32 %3, 1
  br i1 %4, label %5, label %11

; <label>:5                                       ; preds = %0
  %6 = load i32* %2, align 4
  %7 = load i32* %2, align 4
  %8 = sub nsw i32 %7, 1
  %9 = call i32 @factorial(i32 %8)
  %10 = mul nsw i32 %6, %9
  store i32 %10, i32* %1
  br label %12

; <label>:11                                      ; preds = %0
  store i32 1, i32* %1
  br label %12

; <label>:12                                      ; preds = %11, %5
  %13 = load i32* %1
  ret i32 %13
}
复制代码

看一下 %9 标注的那一行,这行代码正是递归调用阶乘函数自己,实际上这样调用是很是低效的,由于每次递归调用都要从新压栈。接下来能够看一下优化后的效果,能够经过这样的方式开启优化 -- 将 -03 标志传给 clang:

clang -O3 -emit-llvm factorial.c  -c -o factorial.bc && llvm-dis < factorial.bc
复制代码

如今 阶乘 计算相关代码编译后生成的代码以下:

define i32 @factorial(i32 %x) #0 {
  %1 = icmp sgt i32 %x, 1
  br i1 %1, label %tailrecurse, label %tailrecurse._crit_edge

tailrecurse:                                      ; preds = %tailrecurse, %0
  %x.tr2 = phi i32 [ %2, %tailrecurse ], [ %x, %0 ]
  %accumulator.tr1 = phi i32 [ %3, %tailrecurse ], [ 1, %0 ]
  %2 = add nsw i32 %x.tr2, -1
  %3 = mul nsw i32 %x.tr2, %accumulator.tr1
  %4 = icmp sgt i32 %2, 1
  br i1 %4, label %tailrecurse, label %tailrecurse._crit_edge

tailrecurse._crit_edge:                           ; preds = %tailrecurse, %0
  %accumulator.tr.lcssa = phi i32 [ 1, %0 ], [ %3, %tailrecurse ]
  ret i32 %accumulator.tr.lcssa
}
复制代码

即使咱们的函数并无按照尾递归的方式编写,clang 仍然能对其作优化处理,让该函数编译的结果中只包含一个循环。固然 clang 能对代码进行的优化还有不少方面。能够看如下这个比较不错的 gcc 的优化例子ridiculousfish.com

延伸阅读

如何在实际中应用这些特性

刚刚咱们探讨了编译的全过程,从标记到解析,从抽象语法树到分析检查,再到汇编。读者不由要问,为何要关注这些?

使用 libclan g或 clang 插件

之因此 clang 很酷:是由于它是一个开源的项目、而且它是一个很是好的工程:几乎能够说全身是宝。使用者能够建立本身的 clang 版本,针对本身的需求对其进行改造。好比说,能够改变 clang 生成代码的方式,增长更强的类型检查,或者按照本身的定义进行代码的检查分析等等。要想达成以上的目标,有不少种方法,其中最简单的就是使用一个名为 libclang 的C类库。libclang 提供的 API 很是简单,能够对 C 和 clang 作桥接,并能够用它对全部的源码作分析处理。不过,根据个人经验,若是使用者的需求更高,那么 libclang 就不怎么行了。针对这种状况,推荐使用 Clangkit,它是基于 clang 提供的功能,用 Objective-C 进行封装的一个库。

最后,clang 还提供了一个直接使用 LibTooling 的 C++ 类库。这里要作的事儿比较多,并且涉及到 C++,可是它可以发挥 clang 的强大功能。用它你能够对源码作任意类型的分析,甚至重写程序。若是你想要给 clang 添加一些自定义的分析、建立本身的重构器 (refactorer)、或者须要基于现有代码作出大量修改,甚至想要基于工程生成相关图形或者文档,那么 LibTooling 是很好的选择。

自定义分析器

开发者能够按照 Tutorial for building tools using LibTooling 中的说明去构造 LLVM ,clang 以及 clan g的附加工具。须要注意的是,编译代码是须要花费一些时间的,即时机器已经很快了,可是在编译期间,我仍是能够吃顿饭的。

接下来,进入到 LLVM 目录,而后执行命令cd ~/llvm/tools/clang/tools/。在这个目录中,能够建立本身独立的 clang 工具。例如,咱们建立一个小工具,用来检查某个库是否正确使用。首先将 样例工程 克隆到本地,而后输入 make。这样就会生成一个名为 example 的二进制文件。

咱们的使用场景是:假若有一个 Observer 类, 代码以下所示:

@interface Observer
+ (instancetype)observerWithTarget:(id)target action:(SEL)selector;
@end
复制代码

接下来,咱们想要检查一下每当这个类被调用的时候,在 target 对象中是否都有对应的 action 方法存在。能够写个 C++ 函数来作这件事(注意,这是我第一次写 C++ 程序,可能不那么严谨):

virtual bool VisitObjCMessageExpr(ObjCMessageExpr *E) {
  if (E->getReceiverKind() == ObjCMessageExpr::Class) {
    QualType ReceiverType = E->getClassReceiver();
    Selector Sel = E->getSelector();
    string TypeName = ReceiverType.getAsString();
    string SelName = Sel.getAsString();
    if (TypeName == "Observer" && SelName == "observerWithTarget:action:") {
      Expr *Receiver = E->getArg(0)->IgnoreParenCasts();
      ObjCSelectorExpr* SelExpr = cast<ObjCSelectorExpr>(E->getArg(1)->IgnoreParenCasts());
      Selector Sel = SelExpr->getSelector();
      if (const ObjCObjectPointerType *OT = Receiver->getType()->getAs<ObjCObjectPointerType>()) {
        ObjCInterfaceDecl *decl = OT->getInterfaceDecl();
        if (! decl->lookupInstanceMethod(Sel)) {
          errs() << "Warning: class " << TypeName << " does not implement selector " << Sel.getAsString() << "\n";
          SourceLocation Loc = E->getExprLoc();
          PresumedLoc PLoc = astContext->getSourceManager().getPresumedLoc(Loc);
          errs() << "in " << PLoc.getFilename() << " <" << PLoc.getLine() << ":" << PLoc.getColumn() << ">\n";
        }
      }
    }
  }
  return true;
}
复制代码

上面的这个方法首先查找消息表达式, 以 Observer 做为接收者, observerWithTarget:action: 做为 selector,而后检查 target 中是否存在相应的方法。虽然这个例子有点儿刻意,但若是你想要利用 AST 对本身的代码库作某些检查,按照上面的例子来就能够了。

clang的其余特性

clang还有许多其余的用途。好比,能够写编译器插件(例如,相似上面的检查器例子)而且动态的加载到编译器中。虽然我没有亲自实验过,可是我以为在 Xcode 中应该是可行的。再好比,也能够经过编写 clang 插件来自定义代码样式(具体能够参见 编译过程)。

另外,若是想对现有的代码作大规模的重构, 而 Xcode 或 AppCode 自己集成的重构工具没法达你的要求,你彻底能够用 clang 本身写个重构工具。听起来有点儿可怕,读读下面的文档和教程,你会发现其实没那么难。

最后,若是是真的有这种需求,你彻底能够引导 Xcdoe 使用你本身编译的 clang 。再一次,若是你去尝试,其实这些事儿真的没想象中那么复杂,反而会发现许多个中乐趣。

延伸阅读

原文: The Compiler

译文 objc.io 第6期 编译器