iOS編譯過程

前言

iOS 開發中使用的是編譯語言,所謂編譯語言是在執行的時候,必須先經過編譯器生成機器碼,機器碼能夠直接在CPU上執行,因此執行效率較高。他是使用 Clang / LLVM 來編譯的。LLVM是一個模塊化和可重用的編譯器和工具鏈技術的集合,Clang 是 LLVM 的子項目,是 C,C++ 和 Objective-C 編譯器,目的是提供驚人的快速編譯。下面咱們來看看編譯過程,總的來講編譯過程分爲幾個階段:
預處理 -> 詞法分析 -> 語法分析 -> 靜態分析 -> 生成中間代碼和優化 -> 彙編 -> 連接html

具體過程

1、預處理

咱們以一個實際例子來看看,預處理的過程,源碼:git

#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,結果以下:github

@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獲得相同的結果
這一步編譯器所作的處理是:macos

  • 宏替換
    在源碼中使用的宏定義會被替換爲對應#define的內容)bash

    建議你們不要在須要預處理的代碼中加入內聯代碼邏輯。app

  • 頭文件引入(#include,#import)
    使用對應文件.h的內容替換這一行的內容,因此儘可能減小頭文件中的#import,使用@class替代,把#import放到.m文件中。less

  • 處理條件編譯指令 (#if,#else,#endif)ide

2、詞法解析

使用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中都標記出了源碼所在的對應源文件和具體行數,方便在報錯時定位問題。函數

3、語法分析

執行 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),一樣地,在這裏面每一節點也都標記了其在源碼中的位置。

4、靜態分析

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

  • 類型檢查
    在此階段clang會作檢查,最多見的是檢查程序是否發送正確的消息給正確的對象,是否在正確的值上調用了正常函數。若是你給一個單純的 NSObject* 對象發送了一個 hello 消息,那麼 clang 就會報錯,一樣,給屬性設置一個與其自身類型不相符的對象,編譯器會給出一個可能使用不正確的警告。

    通常會把類型分爲兩類:動態的和靜態的。動態的在運行時作檢查,靜態的在編譯時作檢查。以往,編寫代碼時能夠向任意對象發送任何消息,在運行時,纔會檢查對象是否可以響應這些消息。因爲只是在運行時作此類檢查,因此叫作動態類型。

    至於靜態類型,是在編譯時作檢查。當在代碼中使用 ARC 時,編譯器在編譯期間,會作許多的類型檢查:由於編譯器須要知道哪一個對象該如何使用。

  • 其餘分析
    ObjCUnusedIVarsChecker.cpp是用來檢查是否有定義了,可是從未使用過的變量。
    ObjCSelfInitChecker.cpp是檢查在 你的初始化方法中中調用 self 以前,是否已經調用 [self initWith...] 或 [super init] 了。

    checkers
    更多請看: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.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 全時優化》

6、彙編

在這一階段,彙編器將上一步生成的可讀的彙編代碼轉化爲機器代碼。最終產物就是 以 .o 結尾的目標文件。使用Xcode構建的程序會在DerivedData目錄中找到這個文件。如圖:

.o

7、連接

這一階段是將上個階段生成的目標文件和引用的靜態庫連接起來,最終生成可執行文件,連接器解決了目標文件和庫之間的連接。

使用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文件的結構以下:

Mach-O

  1. Header
    保存了Mach-O的一些基本信息,包括了平臺、文件類型、LoadCommands的個數等等。 使用otool -v -h a.out查看其內容:

    Mach-o Header

  2. Load commands
    這一段緊跟Header,加載Mach-O文件時會使用這裏的數據來肯定內存的分佈

  3. 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。
      包含了方法和變量的元數據,代碼簽名等信息。

資料:
Mach-O Executable Format
編譯器
Mach-O 可執行文件

相關文章
相關標籤/搜索