iOS底層學習 - 從編譯到啓動的奇幻旅程(一)

瞭解了對象,類,方法等底層實現以後,咱們來看一下咱們開發的App,在代碼完成後到啓動的時候,經歷了哪幾個步驟前端

整體來講,一個APP從編寫完代碼到運行,就經歷了兩大步驟,即編譯運行,這一章節,主要來看一下APP的進行編譯的。linux

編譯的大致步驟以下:git

  • 預處理
  • 編譯
  • 彙編
  • 連接

iOS編譯器

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

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

總結來講:windows

  • 採用編譯器生成機器碼執行的好處是效率高,缺點是調試周期長
  • 解釋器執行的好處是編寫調試方便,缺點是執行效率低

目前Xcode使用的編譯器爲LLVM(官方連接)。LLVM 是編譯器工具鏈技術的一個集合。而其中的 lld 項目,就是內置連接器。編譯器會對每一個文件進行編譯,生成 Mach-O(可執行文件);連接器會將項目中的多個Mach-O 文件合併成一個。後端

LLVM會執行上述的整個編譯流程,大致流程以下:bash

  • 你寫好代碼後,LLVM會預處理你的代碼,好比把宏嵌入到對應的位置。
  • 預處理完後,LLVM 會對代碼進行詞法分析和語法分析,生成 AST 。AST 是抽象語法樹,結構上比代碼更精簡,遍歷起來更快,因此使用 AST 可以更快速地進行靜態檢查,同時還能更快地生成 IR(中間表示)
  • 最後 AST 會生成 IR,IR 是一種更接近機器碼的語言,區別在於和平臺無關,經過 IR 能夠生成多份適合不一樣平臺的機器碼。對於 iOS 系統,IR 生成的可執行文件就是 Mach-O。

預處理

建立一個工程,使用clang -E main.m能夠查看預處理階段的所作的工做架構

#import <Foundation/Foundation.h>
#define DEFINEEight 8

int main(){
    @autoreleasepool {
        int eight = DEFINEEight;
        int six = 6;
        NSString* site = [[NSString alloc] initWithUTF8String:"starming"];
        int rank = eight + six;
        NSLog(@"%@ rank %d", site, rank);
    }
    return 0;
}
複製代碼
# 10 "main.m"
# 1 "./AppDelegate.h" 1
# 11 "./AppDelegate.h"
@interface AppDelegate : UIResponder <UIApplicationDelegate>
@end
# 11 "main.m" 2
int main(int argc, char * argv[]) {
    @autoreleasepool {
        int eight = 8;
        int six = 6;
        NSString* site = [[NSString alloc] initWithUTF8String:"starming"];
        int rank = eight + six;
        NSLog(@"%@ rank %d", site, rank);
    }
    return 0;
}
複製代碼

預處理主要處理規則以下:app

  • 刪除全部#define,並將全部宏定義展開,在源碼中使用的宏定義會被替換爲對應代碼
  • 將被包含的文件插入到預編譯指令(#include)所在位置(這個過程是遞歸的)
  • 刪除全部註釋:// 、/* */等
  • 添加行號和文件名標識,以便於編譯時編譯器產生調試用的行號信息及編譯時可以顯示警告和錯誤的所在行號
  • 保留全部的#pragma編譯器指令,由於編譯器需要使用它們

當咱們沒法判斷宏定義是否正確或者頭文件是否包含時能夠查看預編譯後的文件來肯定問題

編譯

編譯的過程就是把預處理完的文件進行一些列詞法分析、語法分析、語義分析及優化後生產相應的彙編代碼文件,這個過程每每是咱們整個程序構建的核心部分.

詞法分析

使用clang -Xclang -dump-tokens main.m來進行詞法分析,獲得以下結果

at '@'	 [StartOfLine]	Loc=<./AppDelegate.h:11:1>
identifier 'interface'		Loc=<./AppDelegate.h:11:2>
identifier 'AppDelegate'	 [LeadingSpace]	Loc=<./AppDelegate.h:11:12>
colon ':'	 [LeadingSpace]	Loc=<./AppDelegate.h:11:24>
identifier 'UIResponder'	 [LeadingSpace]	Loc=<./AppDelegate.h:11:26>
less '<'	 [LeadingSpace]	Loc=<./AppDelegate.h:11:38>
identifier 'UIApplicationDelegate'		Loc=<./AppDelegate.h:11:39>
greater '>'		Loc=<./AppDelegate.h:11:60>
at '@'	 [StartOfLine]	Loc=<./AppDelegate.h:14:1>
identifier 'end'		Loc=<./AppDelegate.h:14:2>
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>

...
複製代碼

這一步把源文件中的代碼轉化爲特殊的標記流,源碼被分割成一個一個的字符和單詞,在行尾Loc中都標記出了源碼所在的對應源文件和具體行數,方便在報錯時定位問題

語法分析

使用clang -Xclang -ast-dump -fsyntax-only main.m命令來進行語法分析,結果以下

...

| `-PointerType 0x7f9824831b10 'char *'
|   `-BuiltinType 0x7f9824830ca0 'char'
|-TypedefDecl 0x7f9825006458 <<invalid sloc>> <invalid sloc> implicit __builtin_va_list 'struct __va_list_tag [1]'
| `-ConstantArrayType 0x7f9825006400 'struct __va_list_tag [1]' 1
|   `-RecordType 0x7f9825006280 'struct __va_list_tag'
|     `-Record 0x7f9825006200 '__va_list_tag'
|-ObjCInterfaceDecl 0x7f98250064a8 <./AppDelegate.h:11:1, line:14:2> line:11:12 AppDelegate
|-FunctionDecl 0x7f98250067e0 <main.m:14:1, line:23:1> line:14:5 main 'int (int, char **)'
| |-ParmVarDecl 0x7f98250065b8 <col:10, col:14> col:14 argc 'int'
| |-ParmVarDecl 0x7f98250066d0 <col:20, col:32> col:27 argv 'char **':'char **'
| `-CompoundStmt 0x7f9825006f28 <col:35, line:23:1>
|   |-ObjCAutoreleasePoolStmt 0x7f9825006ee0 <line:15:5, line:21:5>
|   | `-CompoundStmt 0x7f9825006eb8 <line:15:22, line:21:5>
|   |   |-DeclStmt 0x7f9825006960 <line:16:9, col:32>
|   |   | `-VarDecl 0x7f98250068e0 <col:9, line:12:21> line:16:13 used eight 'int' cinit
|   |   |   `-IntegerLiteral 0x7f9825006940 <line:12:21> 'int' 8
|   |   |-DeclStmt 0x7f9825006a10 <line:17:9, col:20>
|   |   | `-VarDecl 0x7f9825006990 <col:9, col:19> col:13 used six 'int' cinit
|   |   |   `-IntegerLiteral 0x7f98250069f0 <col:19> 'int' 6
|   |   `-DeclStmt 0x7f9825006b30 <line:19:9, col:31>
|   |     `-VarDecl 0x7f9825006a40 <col:9, col:28> col:13 used rank 'int' cinit
|   |       `-BinaryOperator 0x7f9825006b10 <col:20, col:28> 'int' '+'
|   |         |-ImplicitCastExpr 0x7f9825006ae0 <col:20> 'int' <LValueToRValue>
|   |         | `-DeclRefExpr 0x7f9825006aa0 <col:20> 'int' lvalue Var 0x7f98250068e0 'eight' 'int'
|   |         `-ImplicitCastExpr 0x7f9825006af8 <col:28> 'int' <LValueToRValue>
|   |           `-DeclRefExpr 0x7f9825006ac0 <col:28> 'int' lvalue Var 0x7f9825006990 'six' 'int'
|   `-ReturnStmt 0x7f9825006f18 <line:22:5, col:12>
|     `-IntegerLiteral 0x7f9825006ef8 <col:12> 'int' 0
`-FunctionDecl 0x7f9825006bd0 <line:20:9> col:9 implicit used NSLog 'void (id, ...)' extern
  |-ParmVarDecl 0x7f9825006c68 <<invalid sloc>> <invalid sloc> 'id':'id'
  `-FormatAttr 0x7f9825006cd0 <col:9> Implicit NSString 1
  
...
複製代碼

這一步是把詞法分析生成的標記流,解析成一個抽象語法樹(abstract syntax tree -- AST),一樣地,在這裏面每一節點也都標記了其在源碼中的位置。

靜態分析

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

類型檢查

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

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

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

其餘分析

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

參考資料

clang靜態分析

LLVM IR 中間產物

使用clang -O3 -S -emit-llvm main.m -o main.ll命令,生成LLVM中間產物IR(生成main.ll文件),IR是編譯過程的前端的輸出後端的輸入。

; 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優化

使用clang -emit-llvm -c main.m -o main.bc命令,會使用LLVM對代碼進行優化。

  • 針對全局變量優化、循環優化、尾遞歸優化等。
  • 在 Xcode 的編譯設置裏也能夠設置優化級別-01,-03,-0s,還能夠寫些本身的 Pass。
  • Pass 是 LLVM 優化工做的一個節點,一個節點作些事,一塊兒加起來就構成了 LLVM 完整的優化和轉化。
  • 若是開啓了 bitcode蘋果會作進一步的優化,有新的後端架構仍是能夠用這份優化過的 bitcode 去生成。

生成彙編代碼

使用clang -S -fobjc-arc main.m -o main.s會生成相對應的彙編代碼

至此,編譯階段完成,將書寫代碼轉換成了機器能夠識別的彙編代碼

彙編

彙編器是將彙編代碼轉變成機器能夠執行的指令,每個彙編語句幾乎都對應一條機器指令。因此彙編器的彙編過程相對於編譯器來說比較簡單,它沒有複雜的語法,也沒有語義,也不須要作指令優化,只是根據彙編指令和機器指令的對照表一一翻譯就能夠了。

使用clang -fmodules -c main.m -o main.o生成對應的目標文件

使用Xcode構建的程序會在DerivedData目錄中找到這個文件,以下圖

連接

連接主要分爲靜態連接動態連接,編譯器階段的連接爲靜態連接,相關動態連接的部分,會在下一章App啓動中講解

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

使用clang main.m生成可執行文件,能夠看出可執行文件類型爲Mach-O類型,在 MAC OS 和 iOS 平臺的可執行文件都是這種類型。

至此,編譯過程所有結束,生成了可執行文件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 開關,來開啓自動去除無用代碼的功能。而且,這個開關是默認開啓的。

Mach-O分析

Mach-O實際上是Mach Object文件格式的縮寫,是mac以及iOS上可執行文件的格式, 相似於windows上的PE格式 (Portable Executable ), linux上的elf格式 (Executable and Linking Format)

Mach-O是OS X中二進制文件的原生可執行格式,是傳送代碼的首選格式。可執行格式決定了二進制文件中的代碼和數據讀入內存的順序。代碼和數據的順序會影響內存使用和分頁活動,從而直接影響程序的性能。

Mach-O二進制文件被組織成段。每一個部分包含一個或多個部分。段的大小由它所包含的全部部分的字節數來度量,並四捨五入到下一個虛擬內存頁邊界。所以,一個段老是4096字節或4千字節的倍數,其中4096字節是最小大小。

常見的Mach-O文件

一、目標文件:.o

二、庫文件:.a .dylib Framework

三、可執行文件:dyld .dsym

Mach-O文件格式

MachO能夠是多架構的二進制文件,稱之爲「通用二進制文件」

主要架構有armv7,armv7s,arm64,i386,x86_64,其中iPhone中多數使用arm64

通用二進制文件是蘋果公司提出的一種程序代碼。能同時適用多種架構的二進制文件

  • 同一個程序包中同時爲多種架構提供最理想的性能。
  • 由於須要儲存多種代碼,通用二進制應用程序一般比單一平臺二進制的程序要大。
  • 可是因爲兩種架構有共通的非執行資源,因此並不會達到單一版本的兩倍之多。
  • 並且因爲執行中只調用一部分代碼,運行起來也不須要額外的內存。

Mach-O的文件結構

Header

Header 包含該二進制文件的通常信息 字節順序、架構類型、加載指令的數量等。 使得能夠快速確認一些信息,好比當前文件用於32位仍是64位,對應的處理器是什麼、文件類型是什麼

可以使用otool -v -h a.out查看其結構,或者使用MachOView來直接查看

Load Commons

Load commands是一張包含不少內容的表。內容包括區域的位置、符號表、動態符號表等。這一段緊跟Header,加載Mach-O文件時會使用這裏的數據來肯定內存的分佈

LC_LOAD_DYLINKER

LC_LOAD_DYLINKER 該字段標明咱們的MachO是被誰加載進去的。通常狀況下都是dyld,下一個章節咱們會講dyld是如何對Mach-o進行加載的

LC_LOAD_DYLIB

LC_LOAD_DYLIB 該字段標記了全部動態庫的地址,只有在LC_LOAD_DYLIB中有標記,咱們MachO外部的動態庫(如:Framework)才能被dyld正確的引用,不然dyld不會主動加載

Data

Data 一般是對象文件中最大的部分,包含Segement的具體數據,如靜態C字符串,帶參數/不帶參數的OC方法,帶參數/不帶參數的C函數。

包含 Load commands 中須要的各個 segment,每一個 segment 中又包含多個 section。當運行一個可執行文件時,虛擬內存 (virtual memory) 系統將 segment 映射到進程的地址空間上。

使用xcrun size -x -l -m a.out查看segment內容,或者MachOView

名稱 含義
Segment __PAGEZERO 大小爲 4GB,規定進程地址空間的前 4GB 被映射爲不可讀不可寫不可執行
Segment __TEXT 包含可執行的代碼,以只讀和可執行方式映射。
Segment __DATA 包含了將會被更改的數據,以可讀寫和不可執行方式映射。
Segment __LINKEDIT 包含了方法和變量的元數據,代碼簽名等信息。

參考

相關文章
相關標籤/搜索