iOS編譯過程
前言
iOS 開發中使用的是編譯語言,所謂編譯語言是在執行的時候,必須先通過編譯器生成機器碼,機器碼可以直接在CPU上執行,所以執行效率較高。他是使用 Clang / LLVM 來編譯的。LLVM是一個模組化和可重用的編譯器和工具鏈技術的集合,Clang 是 LLVM 的子專案,是 C,C++ 和 Objective-C 編譯器,目的是提供驚人的快速編譯。下面我們來看看編譯過程,總的來說編譯過程分為幾個階段:
預處理 -> 詞法分析 -> 語法分析 -> 靜態分析 -> 生成中間程式碼和優化 -> 彙編 -> 連結
具體過程
一、預處理
我們以一個實際例子來看看,預處理的過程,原始碼:
#import "AppDelegate.h"
#define NUMBER 1
int main(int argc, char * argv[]) {
@autoreleasepool {
NSLog(@"%d",NUMBER);
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
複製程式碼
使用終端到main.m所在資料夾,使用命令:clang -E main.m
,結果如下:
@interface AppDelegate : UIResponder <UIApplicationDelegate>
@property (strong, nonatomic) UIWindow *window;
@end
# 11 "main.m" 2
int main(int argc, char * argv[]) {
@autoreleasepool {
NSLog(@"%d",1);
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
複製程式碼
也可以使用Xcode的Product->Perform Action -> Preprocess
得到相同的結果
這一步編譯器所做的處理是:
-
巨集替換
在原始碼中使用的巨集定義會被替換為對應#define的內容)建議大家不要在需要預處理的程式碼中加入內聯程式碼邏輯。
-
標頭檔案引入(#include,#import)
使用對應檔案.h的內容替換這一行的內容,所以儘量減少標頭檔案中的#import,使用@class替代,把#import放到.m檔案中。 -
處理條件編譯指令 (#if,#else,#endif)
二、詞法解析
使用clang -Xclang -dump-tokens main.m
詞法分析結果如下:
int 'int' [StartOfLine] Loc=<main.m:14:1>
identifier 'main' [LeadingSpace] Loc=<main.m:14:5>
l_paren '(' Loc=<main.m:14:9>
int 'int' Loc=<main.m:14:10>
identifier 'argc' [LeadingSpace] Loc=<main.m:14:14>
comma ',' Loc=<main.m:14:18>
char 'char' [LeadingSpace] Loc=<main.m:14:20>
star '*' [LeadingSpace] Loc=<main.m:14:25>
identifier 'argv' [LeadingSpace] Loc=<main.m:14:27>
l_square '[' Loc=<main.m:14:31>
r_square ']' Loc=<main.m:14:32>
r_paren ')' Loc=<main.m:14:33>
l_brace '{' [LeadingSpace] Loc=<main.m:14:35>
at '@' [StartOfLine] [LeadingSpace] Loc=<main.m:15:5>
identifier 'autoreleasepool' Loc=<main.m:15:6>
l_brace '{' [LeadingSpace] Loc=<main.m:15:22>
identifier 'NSLog' [StartOfLine] [LeadingSpace] Loc=<main.m:17:9>
l_paren '(' Loc=<main.m:17:14>
at '@' Loc=<main.m:17:15>
string_literal '"%d"' Loc=<main.m:17:16>
comma ',' Loc=<main.m:17:20>
numeric_constant '1' Loc=<main.m:17:21 <Spelling=main.m:12:16>>
r_paren ')' Loc=<main.m:17:27>
semi ';' Loc=<main.m:17:28>
return 'return' [StartOfLine] [LeadingSpace] Loc=<main.m:19:9>
identifier 'UIApplicationMain' [LeadingSpace] Loc=<main.m:19:16>
l_paren '(' Loc=<main.m:19:33>
identifier 'argc' Loc=<main.m:19:34>
comma ',' Loc=<main.m:19:38>
identifier 'argv' [LeadingSpace] Loc=<main.m:19:40>
comma ',' Loc=<main.m:19:44>
identifier 'nil' [LeadingSpace] Loc=<main.m:19:46>
comma ',' Loc=<main.m:19:49>
identifier 'NSStringFromClass' [LeadingSpace] Loc=<main.m:19:51>
l_paren '(' Loc=<main.m:19:68>
l_square '[' Loc=<main.m:19:69>
identifier 'AppDelegate' Loc=<main.m:19:70>
identifier 'class' [LeadingSpace] Loc=<main.m:19:82>
r_square ']' Loc=<main.m:19:87>
r_paren ')' Loc=<main.m:19:88>
r_paren ')' Loc=<main.m:19:89>
semi ';' Loc=<main.m:19:90>
r_brace '}' [StartOfLine] [LeadingSpace] Loc=<main.m:20:5>
r_brace '}' [StartOfLine] Loc=<main.m:21:1>
eof '' Loc=<main.m:21:2>
複製程式碼
這一步把原始檔中的程式碼轉化為特殊的標記流,原始碼被分割成一個一個的字元和單詞,在行尾Loc中都標記出了原始碼所在的對應原始檔和具體行數,方便在報錯時定位問題。
三、語法分析
執行 clang 命令 clang -Xclang -ast-dump -fsyntax-only maim.m
得到如下結果:
|-FunctionDecl 0x7f9fa085a9b8 <main.m:14:1, line:21:1> line:14:5 main 'int (int, char **)'
| |-ParmVarDecl 0x7f9fa085a788 <col:10, col:14> col:14 used argc 'int'
| |-ParmVarDecl 0x7f9fa085a8a0 <col:20, col:32> col:27 used argv 'char **':'char **'
| `-CompoundStmt 0x7f9fa1002240 <col:35, line:21:1>
| `-ObjCAutoreleasePoolStmt 0x7f9fa1002230 <line:15:5, line:20:5>
| `-CompoundStmt 0x7f9fa1002210 <line:15:22, line:20:5>
| `-CallExpr 0x7f9fa085aec0 <line:17:9, col:27> 'void'
| |-ImplicitCastExpr 0x7f9fa085aea8 <col:9> 'void (*)(id, ...)' <FunctionToPointerDecay>
| | `-DeclRefExpr 0x7f9fa085ac90 <col:9> 'void (id, ...)' Function 0x7f9fa085ab38 'NSLog' 'void (id, ...)'
| |-ImplicitCastExpr 0x7f9fa085aef8 <col:15, col:16> 'id':'id' <BitCast>
| | `-ObjCStringLiteral 0x7f9fa085ae08 <col:15, col:16> 'NSString *'
| | `-StringLiteral 0x7f9fa085acf8 <col:16> 'char [3]' lvalue "%d"
| `-IntegerLiteral 0x7f9fa085ae28 <line:12:16> 'int' 1
|-FunctionDecl 0x7f9fa085ab38 <line:17:9> col:9 implicit used NSLog 'void (id, ...)' extern
| |-ParmVarDecl 0x7f9fa085abd0 <<invalid sloc>> <invalid sloc> 'id':'id'
| `-FormatAttr 0x7f9fa085ac38 <col:9> Implicit NSString 1 2
|-FunctionDecl 0x7f9fa085af60 <<invalid sloc>> line:19:16 implicit used UIApplicationMain 'int ()'
`-FunctionDecl 0x7f9fa085b098 <<invalid sloc>> col:51 implicit used NSStringFromClass 'int ()'
複製程式碼
這一步是把詞法分析生成的標記流,解析成一個抽象語法樹(abstract syntax tree -- AST),同樣地,在這裡面每一節點也都標記了其在原始碼中的位置。
四、靜態分析
把原始碼轉化為抽象語法樹之後,編譯器就可以對這個樹進行分析處理。靜態分析會對程式碼進行錯誤檢查,如出現方法被呼叫但是未定義、定義但是未使用的變數等,以此提高程式碼質量。當然,還可以通過使用 Xcode 自帶的靜態分析工具(Product -> Analyze)
-
型別檢查
在此階段clang會做檢查,最常見的是檢查程式是否傳送正確的訊息給正確的物件,是否在正確的值上呼叫了正常函式。如果你給一個單純的 NSObject* 物件傳送了一個 hello 訊息,那麼 clang 就會報錯,同樣,給屬性設定一個與其自身型別不相符的物件,編譯器會給出一個可能使用不正確的警告。一般會把型別分為兩類:動態的和靜態的。動態的在執行時做檢查,靜態的在編譯時做檢查。以往,編寫程式碼時可以向任意物件傳送任何訊息,在執行時,才會檢查物件是否能夠響應這些訊息。由於只是在執行時做此類檢查,所以叫做動態型別。
至於靜態型別,是在編譯時做檢查。當在程式碼中使用 ARC 時,編譯器在編譯期間,會做許多的型別檢查:因為編譯器需要知道哪個物件該如何使用。
-
其他分析
更多請看:clang靜態分析
ObjCUnusedIVarsChecker.cpp
是用來檢查是否有定義了,但是從未使用過的變數。
ObjCSelfInitChecker.cpp
是檢查在 你的初始化方法中中呼叫 self 之前,是否已經呼叫 [self initWith...] 或 [super init] 了。
五、中間程式碼生成和優化
使用clang -O3 -S -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.13.0"
%struct.__NSConstantString_tag = type { i32*, i32, i8*, i64 }
@__CFConstantStringClassReference = external global [0 x i32]
@.str = private unnamed_addr constant [3 x i8] c"%d\00", section "__TEXT,__cstring,cstring_literals", align 1
@_unnamed_cfstring_ = private global %struct.__NSConstantString_tag { i32* getelementptr inbounds ([0 x i32], [0 x i32]* @__CFConstantStringClassReference, i32 0, i32 0), i32 1992, i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str, i32 0, i32 0), i64 2 }, section "__DATA,__cfstring", align 8
; Function Attrs: ssp uwtable
define i32 @main(i32, i8** nocapture readnone) local_unnamed_addr #0 {
%3 = tail call i8* @objc_autoreleasePoolPush() #2
notail call void (i8*, ...) @NSLog(i8* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_ to i8*), i32 1)
tail call void @objc_autoreleasePoolPop(i8* %3)
ret i32 0
}
declare i8* @objc_autoreleasePoolPush() local_unnamed_addr
declare void @NSLog(i8*, ...) local_unnamed_addr #1
declare void @objc_autoreleasePoolPop(i8*) local_unnamed_addr
attributes #0 = { ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #1 = { "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #2 = { nounwind }
!llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6}
!llvm.ident = !{!7}
!0 = !{i32 1, !"Objective-C Version", i32 2}
!1 = !{i32 1, !"Objective-C Image Info Version", i32 0}
!2 = !{i32 1, !"Objective-C Image Info Section", !"__DATA,__objc_imageinfo,regular,no_dead_strip"}
!3 = !{i32 4, !"Objective-C Garbage Collection", i32 0}
!4 = !{i32 1, !"Objective-C Class Properties", i32 64}
!5 = !{i32 1, !"wchar_size", i32 4}
!6 = !{i32 7, !"PIC Level", i32 2}
!7 = !{!"Apple LLVM version 9.1.0 (clang-902.0.39.2)"}
複製程式碼
接下來 LLVM 會對程式碼進行編譯優化,例如針對全域性變數優化、迴圈優化、尾遞迴優化等,最後輸出彙編程式碼。
使用xcrun clang -S -o - main.m | open -f
生成彙編程式碼:
.section __TEXT,__text,regular,pure_instructions
.macosx_version_min 10, 13
.globl _main ## -- Begin function main
.p2align 4, 0x90
_main: ## @main
.cfi_startproc
## BB#0:
pushq %rbp
Lcfi0:
.cfi_def_cfa_offset 16
Lcfi1:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Lcfi2:
.cfi_def_cfa_register %rbp
subq $32, %rsp
movl $0, -4(%rbp)
movl %edi, -8(%rbp)
movq %rsi, -16(%rbp)
callq _objc_autoreleasePoolPush
leaq L__unnamed_cfstring_(%rip), %rsi
movl $1, %edi
movl %edi, -20(%rbp) ## 4-byte Spill
movq %rsi, %rdi
movl -20(%rbp), %esi ## 4-byte Reload
movq %rax, -32(%rbp) ## 8-byte Spill
movb $0, %al
callq _NSLog
movq -32(%rbp), %rdi ## 8-byte Reload
callq _objc_autoreleasePoolPop
xorl %eax, %eax
addq $32, %rsp
popq %rbp
retq
.cfi_endproc
## -- End function
.section __TEXT,__cstring,cstring_literals
L_.str: ## @.str
.asciz "%d"
.section __DATA,__cfstring
.p2align 3 ## @_unnamed_cfstring_
L__unnamed_cfstring_:
.quad ___CFConstantStringClassReference
.long 1992 ## 0x7c8
.space 4
.quad L_.str
.quad 2 ## 0x2
.section __DATA,__objc_imageinfo,regular,no_dead_strip
L_OBJC_IMAGE_INFO:
.long 0
.long 64
.subsections_via_symbols
複製程式碼
前面的三行:
.section __TEXT,__text,regular,pure_instructions
.macosx_version_min 10, 13
.globl _main ## -- Begin function main
.p2align 4, 0x90
複製程式碼
他們是彙編指令而不是彙編程式碼。
.section
指令指定了接下來會執行哪一個段.globl
指令說明_main
是一個外部符號。這就是我們的main()函式。這個函式對外部是可見的,因為系統要呼叫它來執行可執行檔案。.p2align
指令指出了後面程式碼的對齊方式。在我們的程式碼中,後面的程式碼會按照 16(2^4) 位元組對齊,如果需要的話,用 0x90 補齊。
想要了解更多可以看一下這篇文章:《LLVM 全時優化》。
六、彙編
在這一階段,彙編器將上一步生成的可讀的彙編程式碼轉化為機器程式碼。最終產物就是 以 .o 結尾的目標檔案。使用Xcode構建的程式會在DerivedData目錄中找到這個檔案。如圖:
七、連結
這一階段是將上個階段生成的目標檔案和引用的靜態庫連結起來,最終生成可執行檔案,連結器解決了目標檔案和庫之間的連結。
使用clang main.m
生成可執行檔案a.out(不指定名字預設為a.out),使用file a.out
可以看到其型別資訊:
a.out: Mach-O 64-bit executable x86_64
複製程式碼
可以看出可執行檔案型別為 Mach-O 型別,在 MAC OS 和 iOS 平臺的可執行檔案都是這種型別。因為我使用的是模擬器,所以處理器指令集為 x86_64。
至此,編譯過程結束。
Mach-O檔案
Mach-O簡介
根據官方文件的描述:
Mach-O是OS X中二進位制檔案的原生可執行格式,是傳送程式碼的首選格式。可執行格式決定了二進位制檔案中的程式碼和資料讀入記憶體的順序。程式碼和資料的順序會影響記憶體使用和分頁活動,從而直接影響程式的效能。
Mach-O二進位制檔案被組織成段。每個部分包含一個或多個部分。段的大小由它所包含的所有部分的位元組數來度量,並四捨五入到下一個虛擬記憶體頁邊界。因此,一個段總是4096位元組或4千位元組的倍數,其中4096位元組是最小大小。
Mach-O結構
Mach-O檔案的結構如下:
-
Header
儲存了Mach-O的一些基本資訊,包括了平臺、檔案型別、LoadCommands的個數等等。 使用otool -v -h a.out
檢視其內容: -
Load commands
這一段緊跟Header,載入Mach-O檔案時會使用這裡的資料來確定記憶體的分佈 -
Data
包含 Load commands 中需要的各個 segment,每個 segment 中又包含多個 section。當執行一個可執行檔案時,虛擬記憶體 (virtual memory) 系統將 segment 對映到程序的地址空間上。使用
xcrun size -x -l -m a.out
檢視segment中的內容:- Segment __PAGEZERO。
大小為 4GB,規定程序地址空間的前 4GB 被對映為不可讀不可寫不可執行。 - Segment __TEXT。
包含可執行的程式碼,以只讀和可執行方式對映。 - Segment __DATA。
包含了將會被更改的資料,以可讀寫和不可執行方式對映。 - Segment __LINKEDIT。
包含了方法和變數的元資料,程式碼簽名等資訊。
- Segment __PAGEZERO。