[譯]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期 編譯器

相關文章
相關標籤/搜索