前言
我們知道,程式語言分為編譯語言和解釋語言。兩者的執行過程不同。
編譯語言是通過編譯器將程式碼直接編寫成機器碼,然後直接在CPU上執行機器碼的,這樣能使得我們的app和手機都能效率更高,執行更快。C,C++,OC等語言,都是使用的編譯器,生成相關的可執行檔案。
解釋語言使用的是直譯器。直譯器會在執行時解釋執行程式碼,獲取一段程式碼後就會將其翻譯成目的碼(就是位元組碼(Bytecode)),然後一句一句地執行目的碼。也就是說是在執行時才去解析程式碼,比直接執行編譯好的可執行檔案自然效率就低,但是跑起來之後可以不用重啟啟動編譯,直接修改程式碼即可看到效果,類似熱更新,可以幫我們縮短整個程式的開發週期和功能更新週期。
ios 編譯器
把一種程式語言(原始語言)轉換為另一種程式語言(目標語言)的程式叫做編譯器
編譯器的組成:前端和後端
- 前端負責詞法分析,語法分析,生成中間程式碼;
- 後端以中間程式碼作為輸入,進行行架構無關的程式碼優化,接著針對不同架構生成不同的機器碼;
前後端依賴統一格式的中間程式碼(IR),使得前後端可以獨立的變化。新增一門語言只需要修改前端,而新增一個CPU架構只需要修改後端即可。
Objective C/C/C++使用的編譯器前端是clang,後端都是LLVM
編譯過程
先看下流程
我先寫端程式碼
#import <Foundation/Foundation.h>
#define DEBUG 1
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
#ifdef DEBUG
printf("hello debug\n");
#else
printf("hello world\n");
#endif
NSLog(@"Hello, World!");
}
return 0;
}
複製程式碼
一、預處理(preprocessor)
使用命令:
xcrun clang -E main.m
生成程式碼:
int main(int argc, const char * argv[]) {
@autoreleasepool {
printf("hello debug\n");
NSLog(@"Hello, World!");
}
return 0;
}
複製程式碼
可以看到,在預處理的時候,註釋被刪除,條件編譯被處理。
二、詞法分析(lexical anaysis)
詞法分析器讀入原始檔的字元流,將他們組織稱有意義的詞素(lexeme)序列,對於每個詞素,此法分析器產生詞法單元(token)作為輸出。
$ xcrun clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m 生成程式碼
int main(int argc, const char * argv[]) {
@autoreleasepool {
// ins' Loc=<main.m:9:1>
int 'int' [StartOfLine] Loc=<main.m:11:1>
identifier 'main' [LeadingSpace] Loc=<main.m:11:5>
l_paren '(' Loc=<main.m:11:9>
int 'int' Loc=<main.m:11:10>
identifier 'argc' [LeadingSpace] Loc=<main.m:11:14>
comma ',' Loc=<main.m:11:18>
const 'const' [LeadingSpace] Loc=<main.m:11:20>
char 'char' [LeadingSpace] Loc=<main.m:11:26>
star '*' [LeadingSpace] Loc=<main.m:11:31>
identifier 'argv' [LeadingSpace] Loc=<main.m:11:33>
l_square '[' Loc=<main.m:11:37>
r_square ']' Loc=<main.m:11:38>
r_paren ')' Loc=<main.m:11:39>
...
複製程式碼
看出詞法分析多了Loc來記錄位置。
三、語法分析(semantic analysis)
詞法分析的Token流會被解析成一顆抽象語法樹(abstract syntax tree - AST)。
clang -Xclang -ast-dump -fsyntax-only main.m 輸出如下:
`-FunctionDecl 0x106c203f0 <main.m:11:1, line:22:1> line:11:5 main 'int (int, const char **)'
|-ParmVarDecl 0x106c20220 <col:10, col:14> col:14 argc 'int'
|-ParmVarDecl 0x106c202e0 <col:20, col:38> col:33 argv 'const char **':'const char **'
`-CompoundStmt 0x106c206f8 <col:41, line:22:1>
|-ObjCAutoreleasePoolStmt 0x106c206b0 <line:12:5, line:20:5>
| `-CompoundStmt 0x106c20690 <line:12:22, line:20:5>
| |-CallExpr 0x106c20520 <line:15:11, col:33> 'int'
| | |-ImplicitCastExpr 0x106c20508 <col:11> 'int (*)(const char *, ...)' <FunctionToPointerDecay>
| | | `-DeclRefExpr 0x106c20498 <col:11> 'int (const char *, ...)' Function 0x7fd6618d23b0 'printf' 'int (const char *, ...)'
| | `-ImplicitCastExpr 0x106c20560 <col:18> 'const char *' <NoOp>
| | `-ImplicitCastExpr 0x106c20548 <col:18> 'char *' <ArrayToPointerDecay>
| | `-StringLiteral 0x106c204b8 <col:18> 'char [13]' lvalue "hello debug\n"
| `-CallExpr 0x106c20650 <line:19:9, col:31> 'void'
| |-ImplicitCastExpr 0x106c20638 <col:9> 'void (*)(id, ...)' <FunctionToPointerDecay>
| | `-DeclRefExpr 0x106c20578 <col:9> 'void (id, ...)' Function 0x7fd661b80ff0 'NSLog' 'void (id, ...)'
| `-ImplicitCastExpr 0x106c20678 <col:15, col:16> 'id':'id' <BitCast>
| `-ObjCStringLiteral 0x106c205c0 <col:15, col:16> 'NSString *'
| `-StringLiteral 0x106c20598 <col:16> 'char [14]' lvalue "Hello, World!"
`-ReturnStmt 0x106c206e8 <line:21:5, col:12>
`-IntegerLiteral 0x106c206c8 <col:12> 'int' 0
複製程式碼
這一步是把詞法分析生成的標記流,解析成一個抽象語法樹(abstract syntax tree -- AST),同樣地,在這裡面每一節點也都標記了其在原始碼中的位置。
四、靜態分析
把原始碼轉化為抽象語法樹之後,編譯器就可以對這個樹進行分析處理。靜態分析會對程式碼進行錯誤檢查,如出現方法被呼叫但是未定義、定義但是未使用的變數等,以此提高程式碼質量。當然,還可以通過使用 Xcode 自帶的靜態分析工具(Product -> Analyze)
- 型別檢查 在此階段clang會做檢查,最常見的是檢查程式是否傳送正確的訊息給正確的物件,是否在正確的值上呼叫了正常函式。如果你給一個單純的 NSObject* 物件傳送了一個 hello 訊息,那麼 clang 就會報錯,同樣,給屬性設定一個與其自身型別不相符的物件,編譯器會給出一個可能使用不正確的警告。
- 其他分析 ObjCUnusedIVarsChecker.cpp是用來檢查是否有定義了,但是從未使用過的變數。 ObjCSelfInitChecker.cpp是檢查在 你的初始化方法中中呼叫 self 之前,是否已經呼叫 [self initWith...] 或 [super init] 了。
更多請參考:clang 靜態分析
五、中間程式碼生成和優化
使用命令:
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.14.0"
%struct.__NSConstantString_tag = type { i32*, i32, i8*, i64 }
@__CFConstantStringClassReference = external global [0 x i32]
@.str.1 = private unnamed_addr constant [14 x i8] c"Hello, World!\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 ([14 x i8], [14 x i8]* @.str.1, i32 0, i32 0), i64 13 }, section "__DATA,__cfstring", align 8
@str = private unnamed_addr constant [12 x i8] c"hello debug\00", align 1
; Function Attrs: ssp uwtable
define i32 @main(i32, i8** nocapture readnone) local_unnamed_addr #0 {
%3 = tail call i8* @llvm.objc.autoreleasePoolPush() #1
%4 = tail call i32 @puts(i8* getelementptr inbounds ([12 x i8], [12 x i8]* @str, i64 0, i64 0))
notail call void (i8*, ...) @NSLog(i8* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_ to i8*))
tail call void @llvm.objc.autoreleasePoolPop(i8* %3)
ret i32 0
}
; Function Attrs: nounwind
declare i8* @llvm.objc.autoreleasePoolPush() #1
declare void @NSLog(i8*, ...) local_unnamed_addr #2
; Function Attrs: nounwind
declare void @llvm.objc.autoreleasePoolPop(i8*) #1
; Function Attrs: nounwind
declare i32 @puts(i8* nocapture readonly) local_unnamed_addr #1
attributes #0 = { ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "min-legal-vector-width"="0" "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,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #1 = { nounwind }
attributes #2 = { "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,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
!llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7}
!llvm.ident = !{!8}
!0 = !{i32 2, !"SDK Version", [2 x i32] [i32 10, i32 15]}
!1 = !{i32 1, !"Objective-C Version", i32 2}
!2 = !{i32 1, !"Objective-C Image Info Version", i32 0}
!3 = !{i32 1, !"Objective-C Image Info Section", !"__DATA,__objc_imageinfo,regular,no_dead_strip"}
!4 = !{i32 4, !"Objective-C Garbage Collection", i32 0}
!5 = !{i32 1, !"Objective-C Class Properties", i32 64}
!6 = !{i32 1, !"wchar_size", i32 4}
!7 = !{i32 7, !"PIC Level", i32 2}
!8 = !{!"Apple clang version 11.0.0 (clang-1100.0.33.12)"}
複製程式碼
接下來 LLVM 會對程式碼進行編譯優化,例如針對全域性變數優化、迴圈優化、尾遞迴優化等,最後輸出彙編程式碼。
六、生成彙編
使用命令
xcrun clang -S -o - main.m | open -f 生成程式碼如下:
.section __TEXT,__text,regular,pure_instructions
.build_version macos, 10, 14 sdk_version 10, 15
.globl _main ## -- Begin function main
.p2align 4, 0x90
_main: ## @main
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.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_.str(%rip), %rdi
movq %rax, -24(%rbp) ## 8-byte Spill
movb $0, %al
callq _printf
leaq L__unnamed_cfstring_(%rip), %rsi
movq %rsi, %rdi
movl %eax, -28(%rbp) ## 4-byte Spill
movb $0, %al
callq _NSLog
movq -24(%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 "hello debug\n"
L_.str.1: ## @.str.1
.asciz "Hello, World!"
.section __DATA,__cfstring
.p2align 3 ## @_unnamed_cfstring_
L__unnamed_cfstring_:
.quad ___CFConstantStringClassReference
.long 1992 ## 0x7c8
.space 4
.quad L_.str.1
.quad 13 ## 0xd
.section __DATA,__objc_imageinfo,regular,no_dead_strip
L_OBJC_IMAGE_INFO:
.long 0
.long 64
.subsections_via_symbols
複製程式碼
彙編器以彙編程式碼作為輸入,將彙編程式碼轉換為機器程式碼,最後輸出目標檔案(object file)。
xcrun clang -fmodules -c main.m -o main.o
裡面都是二進位制檔案
七、連結
聯結器把編譯產生的.o檔案和(dylib,a,tbd)檔案,生成一個mach-o檔案。
$ xcrun clang main.o -o main
就生成一個mach o格式的可執行檔案 我們執行下:
Mac-mini-2:測試mac jxq$ file main
main: Mach-O 64-bit executable x86_64
Mac-mini-2:測試mac jxq$ ./main
hello debug
2020-01-15 15:10:32.430 main[4269:156652] Hello, World!
Mac-mini-2:測試mac jxq$
複製程式碼
在用nm命令,檢視可執行檔案的符號表:
Mac-mini-2:測試mac jxq$ nm -nm main
(undefined) external _NSLog (from Foundation)
(undefined) external ___CFConstantStringClassReference (from CoreFoundation)
(undefined) external _objc_autoreleasePoolPop (from libobjc)
(undefined) external _objc_autoreleasePoolPush (from libobjc)
(undefined) external _printf (from libSystem)
(undefined) external dyld_stub_binder (from libSystem)
0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header
0000000100000ef0 (__TEXT,__text) external _main
複製程式碼
至此,編譯過程全部結束,生成了可執行檔案Mach-O
那麼
編譯時連結器做了什麼
Mach-O 檔案裡面的內容,主要就是程式碼和資料:程式碼是函式的定義;資料是全域性變數的定義,包括全域性變數的初始值。不管是程式碼還是資料,它們的例項都需要由符號將其關聯起來。 為什麼呢?因為 Mach-O 檔案裡的那些程式碼,比如 if、for、while 生成的機器指令序列,要操作的資料會儲存在某個地方,變數符號就需要繫結到資料的儲存地址。你寫的程式碼還會引用其他的程式碼,引用的函式符號也需要繫結到該函式的地址上。 連結器的作用,就是完成變數、函式符號和其地址繫結這樣的任務。而這裡我們所說的符號,就可以理解為變數名和函式名。
為什麼要進行符號繫結
- 如果地址和符號不做繫結的話,要讓機器知道你在操作什麼記憶體地址,你就需要在寫程式碼時給每個指令設好記憶體地址。
- 可讀性和可維護性都會很差,修改程式碼後對需要對地址的進行維護
- 需要針對不同的平臺寫多份程式碼,本可以通過高階語言一次編譯成多份
- 相當於直接寫彙編
為什麼還要把專案中的多個 Mach-O 檔案合併成一個
專案中檔案之間的變數和介面函式都是相互依賴的,所以這時我們就需要通過連結器將專案中生成的多個 Mach-O 檔案的符號和地址繫結起來。
沒有這個繫結過程的話,單個檔案生成的 Mach-O 檔案是無法正常執行起來的。因為,如果執行時碰到呼叫在其他檔案中實現的函式的情況時,就會找不到這個呼叫函式的地址,從而無法繼續執行。
連結器在連結多個目標檔案的過程中,會建立一個符號表,用於記錄所有已定義的和所有未定義的符號。
- 連結時如果出現相同符號的情況,就會出現“ld: dumplicate symbols”的錯誤資訊;
- 如果在其他目標檔案裡沒有找到符號,就會提示“Undefined symbols”的錯誤資訊。
連結器對程式碼主要做了哪幾件事兒
- 去專案檔案裡查詢目的碼檔案裡沒有定義的變數。
- 掃描專案中的不同檔案,將所有符號定義和引用地址收集起來,並放到全域性符號表中。
- 計算合併後長度及位置,生成同型別的段進行合併,建立繫結。
- 對專案中不同檔案裡的變數進行地址重定位。
連結器如何去除無用函式,保證Mach-O大小
連結器在整理函式的呼叫關係時,會以 main 函式為源頭,跟隨每個引用,並將其標記為 live。跟隨完成後,那些未被標記 live 的函式,就是無用函式。然後,連結器可以通過開啟 Dead code stripping 開關,來開啟自動去除無用程式碼的功能。並且,這個開關是預設開啟的。
總結
ios編譯過程就是生成mach—o檔案的過程,在這個過程中,進行了一系列的語法檢查,程式碼優化,符號繫結等工作,那mach—o檔案是怎麼儲存這些資訊呢? 下篇文章講。