ios底層 編譯過程

前言

咱們知道,編程語言分爲編譯語言和解釋語言。二者的執行過程不一樣。前端

編譯語言是經過編譯器將代碼直接編寫成機器碼,而後直接在CPU上運行機器碼的,這樣能使得咱們的app和手機都能效率更高,運行更快。C,C++,OC等語言,都是使用的編譯器,生成相關的可執行文件。ios

解釋語言使用的是解釋器。解釋器會在運行時解釋執行代碼,獲取一段代碼後就會將其翻譯成目標代碼(就是字節碼(Bytecode)),而後一句一句地執行目標代碼。也就是說是在運行時纔去解析代碼,比直接運行編譯好的可執行文件天然效率就低,可是跑起來以後能夠不用重啓啓動編譯,直接修改代碼便可看到效果,相似熱更新,能夠幫咱們縮短整個程序的開發週期和功能更新週期。git

ios 編譯器

把一種編程語言(原始語言)轉換爲另外一種編程語言(目標語言)的程序叫作編譯器github

編譯器的組成:前端和後端macos

  • 前端負責詞法分析,語法分析,生成中間代碼;
  • 後端以中間代碼做爲輸入,進行行架構無關的代碼優化,接着針對不一樣架構生成不一樣的機器碼;

先後端依賴統一格式的中間代碼(IR),使得先後端能夠獨立的變化。新增一門語言只須要修改前端,而新增一個CPU架構只須要修改後端便可。編程

Objective C/C/C++使用的編譯器前端是clang,後端都是LLVM後端

編譯過程

先看下流程

dsf

我先寫端代碼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;
}
複製代碼

1、預處理(preprocessor)

使用命令:架構

xcrun clang -E main.mapp

生成代碼:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        printf("hello debug\n");
        NSLog(@"Hello, World!");
    }
    return 0;
}
複製代碼

能夠看到,在預處理的時候,註釋被刪除,條件編譯被處理。

2、詞法分析(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來記錄位置。

3、語法分析(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),一樣地,在這裏面每一節點也都標記了其在源碼中的位置。

4、靜態分析

把源碼轉化爲抽象語法樹以後,編譯器就能夠對這個樹進行分析處理。靜態分析會對代碼進行錯誤檢查,如出現方法被調用可是未定義、定義可是未使用的變量等,以此提升代碼質量。固然,還能夠經過使用 Xcode 自帶的靜態分析工具(Product -> Analyze)

  • 類型檢查 在此階段clang會作檢查,最多見的是檢查程序是否發送正確的消息給正確的對象,是否在正確的值上調用了正常函數。若是你給一個單純的 NSObject* 對象發送了一個 hello 消息,那麼 clang 就會報錯,一樣,給屬性設置一個與其自身類型不相符的對象,編譯器會給出一個可能使用不正確的警告。
  • 其餘分析 ObjCUnusedIVarsChecker.cpp是用來檢查是否有定義了,可是從未使用過的變量。 ObjCSelfInitChecker.cpp是檢查在 你的初始化方法中中調用 self 以前,是否已經調用 [self initWith...] 或 [super init] 了。

更多請參考:clang 靜態分析

5、中間代碼生成和優化

使用命令:

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 會對代碼進行編譯優化,例如針對全局變量優化、循環優化、尾遞歸優化等,最後輸出彙編代碼。

6、生成彙編

使用命令

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

裏面都是二進制文件

7、連接

鏈接器把編譯產生的.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文件是怎麼存儲這些信息呢? 下篇文章講。

相關文章
相關標籤/搜索