咱們知道,編程語言分爲編譯語言和解釋語言。二者的執行過程不一樣。前端
編譯語言是經過編譯器將代碼直接編寫成機器碼,而後直接在CPU上運行機器碼的,這樣能使得咱們的app和手機都能效率更高,運行更快。C,C++,OC等語言,都是使用的編譯器,生成相關的可執行文件。ios
解釋語言使用的是解釋器。解釋器會在運行時解釋執行代碼,獲取一段代碼後就會將其翻譯成目標代碼(就是字節碼(Bytecode)),而後一句一句地執行目標代碼。也就是說是在運行時纔去解析代碼,比直接運行編譯好的可執行文件天然效率就低,可是跑起來以後能夠不用重啓啓動編譯,直接修改代碼便可看到效果,相似熱更新,能夠幫咱們縮短整個程序的開發週期和功能更新週期。git
把一種編程語言(原始語言)轉換爲另外一種編程語言(目標語言)的程序叫作編譯器github
編譯器的組成:前端和後端macos
先後端依賴統一格式的中間代碼(IR),使得先後端能夠獨立的變化。新增一門語言只須要修改前端,而新增一個CPU架構只須要修改後端便可。編程
Objective C/C/C++使用的編譯器前端是clang,後端都是LLVM後端
我先寫端代碼bash
#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;
}
複製代碼
使用命令:架構
xcrun clang -E main.mapp
生成代碼:
int main(int argc, const char * argv[]) {
@autoreleasepool {
printf("hello debug\n");
NSLog(@"Hello, World!");
}
return 0;
}
複製代碼
能夠看到,在預處理的時候,註釋被刪除,條件編譯被處理。
詞法分析器讀入源文件的字符流,將他們組織稱有意義的詞素(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來記錄位置。
詞法分析的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 靜態分析
使用命令:
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 文件是沒法正常運行起來的。由於,若是運行時碰到調用在其餘文件中實現的函數的狀況時,就會找不到這個調用函數的地址,從而沒法繼續執行。
連接器在連接多個目標文件的過程當中,會建立一個符號表,用於記錄全部已定義的和全部未定義的符號。
連接器在整理函數的調用關係時,會以 main 函數爲源頭,跟隨每一個引用,並將其標記爲 live。跟隨完成後,那些未被標記 live 的函數,就是無用函數。而後,連接器能夠經過打開 Dead code stripping 開關,來開啓自動去除無用代碼的功能。而且,這個開關是默認開啓的。
ios編譯過程就是生成mach—o文件的過程,在這個過程當中,進行了一系列的語法檢查,代碼優化,符號綁定等工做,那mach—o文件是怎麼存儲這些信息呢? 下篇文章講。