iOS底層編譯過程

前言

我們知道,編程語言分為編譯語言和解釋語言。兩者的執(zhí)行過程不同。

編譯語言是通過編譯器將代碼直接編寫成機器碼,然后直接在CPU上運行機器碼的,這樣能使得我們的app和手機都能效率更高,運行更快。C,C++,OC等語言,都是使用的編譯器,生成相關的可執(zhí)行文件。

解釋語言使用的是解釋器。解釋器會在運行時解釋執(zhí)行代碼,獲取一段代碼后就會將其翻譯成目標代碼(就是字節(jié)碼(Bytecode)),然后一句一句地執(zhí)行目標代碼。也就是說是在運行時才去解析代碼,比直接運行編譯好的可執(zhí)行文件自然效率就低,但是跑起來之后可以不用重啟啟動編譯,直接修改代碼即可看到效果,類似熱更新,可以幫我們縮短整個程序的開發(fā)周期和功能更新周期。

iOS編譯器

把一種編程語言(原始語言)轉換為另一種編程語言(目標語言)的程序叫做編譯器

編譯器的組成:前端和后端

  • 前端負責詞法分析,語法分析,生成中間代碼;
  • 后端以中間代碼作為輸入,進行行架構無關的代碼優(yōu)化,接著針對不同架構生成不同的機器碼;

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)序列,對于每個詞素,此法分析器產(chǎn)生詞法單元(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來記錄位置

`-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),同樣地,在這里面每一節(jié)點也都標記了其在源碼中的位置。

四、靜態(tài)分析

把源碼轉化為抽象語法樹之后,編譯器就可以對這個樹進行分析處理。靜態(tài)分析會對代碼進行錯誤檢查,如出現(xiàn)方法被調(diào)用但是未定義、定義但是未使用的變量等,以此提高代碼質(zhì)量。當然,還可以通過使用 Xcode 自帶的靜態(tài)分析工具(Product -> Analyze)

  • 類型檢查在此階段clang會做檢查,最常見的是檢查程序是否發(fā)送正確的消息給正確的對象,是否在正確的值上調(diào)用了正常函數(shù)。如果你給一個單純的 NSObject* 對象發(fā)送了一個 hello 消息,那么 clang 就會報錯,同樣,給屬性設置一個與其自身類型不相符的對象,編譯器會給出一個可能使用不正確的警告。
  • 其他分析ObjCUnusedIVarsChecker.cpp是用來檢查是否有定義了,但是從未使用過的變量。ObjCSelfInitChecker.cpp是檢查在 你的初始化方法中中調(diào)用 self 之前,是否已經(jīng)調(diào)用 [self initWith...] 或 [super init] 了。

更多請參考:clang 靜態(tài)分析

五、中間代碼生成和優(yōu)化

使用命令:

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 會對代碼進行編譯優(yōu)化,例如針對全局變量優(yōu)化、循環(huán)優(yōu)化、尾遞歸優(yōu)化等,最后輸出匯編代碼。

六、生成匯編

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

里面都是二進制文件

七、鏈接

連接器把編譯產(chǎn)生的.o文件和(dylib,a,tbd)文件,生成一個mach-o文件。

$ xcrun clang main.o -o main

就生成一個mach o格式的可執(zhí)行文件 我們執(zhí)行下:

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命令,查看可執(zhí)行文件的符號表:

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

至此,編譯過程全部結束,生成了可執(zhí)行文件Mach-O

那么

編譯時鏈接器做了什么

Mach-O 文件里面的內(nèi)容,主要就是代碼和數(shù)據(jù):代碼是函數(shù)的定義;數(shù)據(jù)是全局變量的定義,包括全局變量的初始值。不管是代碼還是數(shù)據(jù),它們的實例都需要由符號將其關聯(lián)起來。
為什么呢?因為 Mach-O 文件里的那些代碼,比如 if、for、while 生成的機器指令序列,要操作的數(shù)據(jù)會存儲在某個地方,變量符號就需要綁定到數(shù)據(jù)的存儲地址。你寫的代碼還會引用其他的代碼,引用的函數(shù)符號也需要綁定到該函數(shù)的地址上。
鏈接器的作用,就是完成變量、函數(shù)符號和其地址綁定這樣的任務。而這里我們所說的符號,就可以理解為變量名和函數(shù)名。

為什么要進行符號綁定

  • 如果地址和符號不做綁定的話,要讓機器知道你在操作什么內(nèi)存地址,你就需要在寫代碼時給每個指令設好內(nèi)存地址。
  • 可讀性和可維護性都會很差,修改代碼后對需要對地址的進行維護
  • 需要針對不同的平臺寫多份代碼,本可以通過高級語言一次編譯成多份
  • 相當于直接寫匯編

為什么還要把項目中的多個 Mach-O 文件合并成一個

項目中文件之間的變量和接口函數(shù)都是相互依賴的,所以這時我們就需要通過鏈接器將項目中生成的多個 Mach-O 文件的符號和地址綁定起來。
沒有這個綁定過程的話,單個文件生成的 Mach-O
文件是無法正常運行起來的。因為,如果運行時碰到調(diào)用在其他文件中實現(xiàn)的函數(shù)的情況時,就會找不到這個調(diào)用函數(shù)的地址,從而無法繼續(xù)執(zhí)行。
鏈接器在鏈接多個目標文件的過程中,會創(chuàng)建一個符號表,用于記錄所有已定義的和所有未定義的符號。

  • 鏈接時如果出現(xiàn)相同符號的情況,就會出現(xiàn)“l(fā)d: dumplicate symbols”的錯誤信息;
  • 如果在其他目標文件里沒有找到符號,就會提示“Undefined symbols”的錯誤信息。

鏈接器如何去除無用函數(shù),保證Mach-O大小

鏈接器在整理函數(shù)的調(diào)用關系時,會以 main 函數(shù)為源頭,跟隨每個引用,并將其標記為 live。跟隨完成后,那些未被標記 live 的函數(shù),就是無用函數(shù)。然后,鏈接器可以通過打開 Dead code stripping 開關,來開啟自動去除無用代碼的功能。并且,這個開關是默認開啟的。

總結

ios編譯過程就是生成mach—o文件的過程,在這個過程中,進行了一系列的語法檢查,代碼優(yōu)化,符號綁定等工作

文章原作者 點擊這里

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容