歡迎光臨
每天分享高質量文章

深入淺出 iOS 編譯

作者:黃文臣

連結:https://juejin.im/post/5c22eaf1f265da611b5863b2

前言

兩年前曾經寫過一篇關於編譯的文章《iOS編譯過程的原理和應用》,這篇文章介紹了iOS編譯相關基礎知識和簡單應用,但也很有多問題都沒有解釋清楚:

  • Clang和LLVM究竟是什麼
  • 源檔案到機器碼的細節
  • Linker做了哪些工作
  • 編譯順序如何確定
  • 頭檔案是什麼?XCode是如何找到頭檔案的?
  • Clang Module
  • 簽名是什麼?為什麼要簽名

為了搞清楚這些問題,我們來挖掘下XCode編譯iOS應用的細節。

編譯器

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

大多數編譯器由兩部分組成:前端和後端。

  • 前端負責詞法分析,語法分析,生成中間程式碼;

  • 後端以中間程式碼作為輸入,進行行架構無關的程式碼最佳化,接著針對不同架構生成不同的機器碼。

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

Objective C/C/C++使用的編譯器前端是 clangswift是 swift (https://swift.org/compiler-stdlib/#compiler-architecture),後端都是 LLVM

LLVM

LLVM(Low Level Virtual Machine)是一個強大的編譯器開發工具套件,聽起來像是虛擬機器,但實際上LLVM和傳統意義的虛擬機器關係不大,只不過專案最初的名字是LLVM罷了。

LLVM的核心庫提供了現代化的 source-target-independent 最佳化器 和支援諸多流行 CPU 架構的程式碼生成器,這些核心程式碼是圍繞著LLVM IR(中間程式碼)建立的。

基於LLVM,又衍生出了一些強大的子專案,其中iOS開發者耳熟能詳的是:Clang 和 LLDB

clang

clang是C語言家族的編譯器前端,誕生之初是為了替代GCC,提供更快的編譯速度。一張圖瞭解clang編譯的大致流程:

接下來,從程式碼層面看一下具體的轉化過程,新建一個main.c:

#include 
// 一點註釋
#define DEBUG 1
int main() {
#ifdef DEBUG
  printf(“hello debug
);
#else
  printf(“hello world
);
#endif
  return 0;
}


預處理(preprocessor)

預處理會替進行頭檔案引入,宏替換,註釋處理,條件編譯(#ifdef)等操作

#include "stdio.h"就是告訴前處理器將這一行替換成頭檔案stdio.h中的內容,這個過程是遞迴的:因為stdio.h也有可能包含其頭檔案。

用clang檢視預處理的結果:

xcrun clang -E main.c


預處理後的檔案有400多行,在檔案的末尾,可以找到main函式

int main() {
  printf(“hello debug
);
  return 0;
}


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

詞法分析(lexical anaysis)

詞法分析器讀入源檔案的字元流,將他們組織稱有意義的詞素(lexeme)序列,對於每個詞素,此法分析器產生詞法單元(token)作為輸出。

$ xcrun clang -fmodules -fsyntax-only -Xclang -dump-tokens main.c


輸出:

annot_module_include ‘#include         Loc=1:1>
int ‘int’     [StartOfLine]  Loc=4:1>
identifier ‘main’     [LeadingSpace] Loc=4:5>
….


Loc=<1:1/>標示這個token位於源檔案main.c的第1行,從第1個字元開始。儲存token在源檔案中的位置是方便後續clang分析的時候能夠找到出錯的原始位置。

語法分析(semantic analysis)

詞法分析的Token流會被解析成一顆抽象語法樹(abstract syntax tree – AST)。

$ xcrun clang -fsyntax-only -Xclang -ast-dump main.c | open -f


main函式AST的結構如下:

[0;34m`-[0m[0;1;32mFunctionDecl[0m[0;33m 0x7fcc188dc700[0m  [0;33mline:4:5[0m[0;1;36m main[0m [0;32m’int ()'[0m
[0;34m  `
-[0m[0;1;35mCompoundStmt[0m[0;330x7fcc188dc918[0m 0;33mcol:12[0m, [0;33mline:11:1[0m>
[0;34m    |-[0m[0;1;35mCallExpr[0m[0;330x7fcc188dc880[0m 0;33mline:6:3[0m, [0;33mcol:25[0m> [0;32m‘int’[0m[0;36m[0m[0;36m[0m
[0;34m    | |-[0m[0;1;35mImplicitCastExpr[0m[0;330x7fcc188dc868[0m 0;33mcol:3[0m> [0;32m‘int (*)(const char *, …)’[0m[0;36m[0m[0;36m[0m 0;31mFunctionToPointerDecay[0m>
[0;34m    | | `-[0m[0;1;35mDeclRefExpr[0m[0;33m 0x7fcc188dc7a0[0m  [0;32m’int (const char *, …)'[0m[0;36m[0m[0;36m[0m [0;1;32mFunction[0m[0;33m 0x7fcc188c5160[0m[0;1;36m ‘printf'[0m [0;32m’int (const char *, …)'[0m
[0;34m    | `
-[0m[0;1;35mImplicitCastExpr[0m[0;330x7fcc188dc8c8[0m 0;33mcol:10[0m> [0;32m‘const char *’[0m[0;36m[0m[0;36m[0m 0;31mBitCast[0m>
[0;34m    |   `-[0m[0;1;35mImplicitCastExpr[0m[0;33m 0x7fcc188dc8b0[0m  [0;32m’char *'[0m[0;36m[0m[0;36m[0m 
[0;34m    |     `
-[0m[0;1;35mStringLiteral[0m[0;330x7fcc188dc808[0m 0;33mcol:10[0m> [0;32m‘char [13]’[0m[0;36m lvalue[0m[0;36m[0m[0;1;36“hello debug
[0m
[0;34m    `-[0m[0;1;35mReturnStmt[0m[0;33m 0x7fcc188dc900[0m 
[0;34m      `
-[0m[0;1;35mIntegerLiteral[0m[0;330x7fcc188dc8e0[0m 0;33mcol:10[0m> [0;32m‘int’[0m[0;36m[0m[0;36m[0m[0;1;360[0m


有了抽象語法樹,clang就可以對這個樹進行分析,找出程式碼中的錯誤。比如型別不匹配,亦或Objective C中向target發送了一個未實現的訊息。

AST是開發者編寫clang外掛主要互動的資料結構,clang也提供很多API去讀取AST。更多細節:Introduction to the Clang AST

CodeGen

CodeGen遍歷語法樹,生成LLVM IR程式碼。LLVM IR是前端的輸出,後端的輸入。

xcrun clang -S -emit-llvm main.c -o main.ll


main.ll檔案內容:


@.str = private unnamed_addr constant [13 x i8] c“hello debugA”, align 1

; Function Attrs: noinline nounwind optnone ssp uwtable
define i32 @main() #0 {
  %1 = alloca i32, align 4
  store i32 0, i32* %1, align 4
  %2 = call i32 (i8*, …) @printf(i8* getelementptr inbounds ([13 x i8], [13 x i8]* @.str, i32 0, i32 0))
  ret i32 0
}


Objective C程式碼在這一步會進行runtime的橋接:property合成,ARC處理等。

LLVM會對生成的IR進行最佳化,最佳化會呼叫相應的Pass進行處理。Pass由多個節點組成,都是 Pass 類的子類,每個節點負責做特定的最佳化,更多細節:Writing an LLVM Pass

生成彙編程式碼

LLVM對IR進行最佳化後,會針對不同架構生成不同的標的程式碼,最後以彙編程式碼的格式輸出:

生成arm 64彙編:

$ xcrun clang -S main.c -o main.s


檢視生成的main.s檔案,篇幅有限,對彙編感興趣的同學可以看看我的這篇文章:iOS彙編快速入門

_main:                                  ## @main
        .cfi_startproc
## %bb.0:
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset %rbp, –16
        movq    %rsp, %rbp


彙編器

彙編器以彙編程式碼作為輸入,將彙編程式碼轉換為機器程式碼,最後輸出標的檔案(object file)。

$ xcrun clang -fmodules -c main.c -o main.o


還記得我們程式碼中呼叫了一個函式printf麼?透過nm命令,檢視下main.o中的符號

$ xcrun nm -nm main.o
                 (undefined) external _printf
0000000000000000 (__TEXT,__text) external _main


_printf是一個是undefined external的。undefined表示在當前檔案暫時找不到符號_printf,而external表示這個符號是外部可以訪問的,對應表示檔案私有的符號是non-external

Tips:什麼是符號(Symbols)? 符號就是指向一段程式碼或者資料的名稱。還有一種叫做WeakSymols,也就是並不一定會存在的符號,需要在執行時決定。比如iOS 12特有的API,在iOS11上就沒有。

連結

聯結器把編譯產生的.o檔案和(dylib,a,tbd)檔案,生成一個mach-o檔案。

$ xcrun clang main.o -o main


我們就得到了一個mach o格式的可執行檔案

$ file main
main: Mach-O 64-bit executable x86_64
$ ./main 
hello debug


在用nm命令,檢視可執行檔案的符號表:

$ nm -nm main
                 (undefined) external _printf (from libSystem)
                 (undefined) external dyld_stub_binder (from libSystem)
0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_essay-header
0000000100000f60 (__TEXT,__text) external _main


_printf仍然是undefined,但是後面多了一些資訊:from libSystem,表示這個符號來自於libSystem,會在執行時動態系結。

XCode編譯

透過上文我們大概瞭解了Clang編譯一個C語言檔案的過程,但是XCode開發的專案不僅僅包含了程式碼檔案,還包括了圖片,plist等。XCode中編譯一次都要經過哪些過程呢?

新建一個單頁面的Demo工程:CocoaPods依賴AFNetworking和SDWebImage,同時依賴於一個內部Framework。按下Command+B,在XCode的Report Navigator模組中,可以找到編譯的詳細日誌:

詳細的步驟如下:

  • 建立Product.app的檔案夾

  • 把Entitlements.plist寫入到DerivedData裡,處理打包的時候需要的資訊(比如application-identifier)。

  • 建立一些輔助檔案,比如各種.hmap,這是essay-headermap檔案,具體作用下文會講解。

  • 執行CocoaPods的編譯前指令碼:檢查Manifest.lock檔案。

  • 編譯.m檔案,生成.o檔案。

  • 連結動態庫,o檔案,生成一個mach o格式的可執行檔案。

  • 編譯assets,編譯storyboard,連結storyboard

  • 複製動態庫Logger.framework,並且對其簽名

  • 執行CocoaPods編譯後指令碼:複製CocoaPods Target生成的Framework

  • 對Demo.App簽名,並驗證(validate)

  • 生成Product.app

Tips: Entitlements.plist儲存了App需要使用的特殊許可權,比如iCloud,遠端通知,Siri等。

編譯順序

編譯的時候有很多的Task(任務)要去執行,XCode如何決定Task的執行順序呢?

答案是:依賴關係。

還是以剛剛的Demo專案為例,整個依賴關係如下:

可以從XCode的Report Navigator看到Target的編譯順序:

XCode編譯的時候會盡可能的利用多核效能,多Target併發編譯。

那麼,XCode又從哪裡得到了這些依賴關係呢?

  • Target Dependencies – 顯式宣告的依賴關係

  • Linked Frameworks and Libraries – 隱式宣告的依賴關係

  • Build Phase – 定義了編譯一個Target的每一步

增量編譯

日常開發中,一次完整的編譯可能要幾分鐘,甚至幾十分鐘,而增量編譯只需要不到1分鐘,為什麼增量編譯會這麼快呢?

因為XCode會對每一個Task生成一個雜湊值,只有雜湊值改變的時候才會重新編譯。

比如,修改了ViewControler.m,只有圖中灰色的三個Task會重新執行(這裡不考慮build phase指令碼)。

頭檔案

C語言家族中,頭檔案(.h)檔案用來引入函式/類/宏定義等宣告,讓開發者更靈活的組織程式碼,而不必把所有的程式碼寫到一個檔案裡。

頭檔案對於編譯器來說就是一個promise。頭檔案裡的宣告,編譯會認為有對應實現,在連結的時候再解決具體實現的位置。

當只有宣告,沒有實現的時候,聯結器就會報錯。

Undefined symbols for architecture arm64:  
“_umimplementMethod”, referenced from:  
-[ClassA method] in ClassA.o  
ld: symbol(s) not found for architecture arm64  
clang: error: linker command failed with exit code 1 (use -v to see invocation)

Objective C的方法要到執行時才會報錯,因為Objective C是一門動態語言,編譯器無法確定對應的方法名(SEL)在執行時到底有沒有實現(IMP)。

日常開發中,兩種常見的頭檔案引入方式:

#include “CustomClass.h” //自定義
#include  //系統或者內部framework


引入的時候並沒有指明檔案的具體路徑,編譯器是如何找到這些頭檔案的呢?

回到XCode的Report Navigator,找到上一個編譯記錄,可以看到編譯ViewController.m的具體日誌:

把這個日誌整體複製到命令列中,最後加上-v,表示我們希望得到更多的日誌資訊,執行這段程式碼,在日誌最後可以看到clang是如何找到頭檔案的:

#include “…” search starts here:
 /Users/…/Build/Intermediates.noindex/Demo.build/Debug-iphoneos/Demo.build/Demo-generated-files.hmap (essay-headermap)
 /Users/…/Build/Intermediates.noindex/Demo.build/Debug-iphoneos/Demo.build/Demo-project-essay-headers.hmap (essay-headermap)
 /Users/…/Build/Products/Debug-iphoneos/AFNetworking/AFNetworking.framework/Headers
 /Users/…/Build/Products/Debug-iphoneos/SDWebImage/SDWebImage.framework/Headers

#include <…> search starts here:</…>
 /Users/…/Build/Intermediates.noindex/Demo.build/Debug-iphoneos/Demo.build/Demo-own-target-essay-headers.hmap (essay-headermap)
 /Users/…/Build/Intermediates.noindex/Demo.build/Debug-iphoneos/Demo.build/Demo-all-non-framework-target-essay-headers.hmap (essay-headermap)
 /Users/…/Build/Intermediates.noindex/Demo.build/Debug-iphoneos/Demo.build/DerivedSources
 /Users/…/Build/Products/Debug-iphoneos (framework directory)
 /Users/…/Build/Products/Debug-iphoneos/AFNetworking (framework directory)
 /Users/…/Build/Products/Debug-iphoneos/SDWebImage (framework directory)
 /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/10.0.0/include
 /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include
 $SDKROOT/usr/include
 $SDKROOT/System/Library/Frameworks (framework directory)

End of search list.


這裡有個檔案型別叫做heademap,essay-headermap是幫助編譯器找到頭檔案的輔助檔案:儲存這頭檔案到其物理路徑的對映關係。

可以透過一個輔助的小工具 hmap 檢視hmap中的內容:

192:Desktop Leo$ ./hmap print Demo-project-essay-headers.hmap 
AppDelegate.h -> /Users/huangwenchen/Desktop/Demo/Demo/AppDelegate.h
Demo-Bridging-Header.h -> /Users/huangwenchen/Desktop/Demo/Demo/Demo-Bridging-Header.h
Dummy.h -> /Users/huangwenchen/Desktop/Demo/Framework/Dummy.h
Framework.h -> Framework/Framework.h
TestView.h -> /Users/huangwenchen/Desktop/Demo/Demo/View/TestView.h
ViewController.h -> /Users/huangwenchen/Desktop/Demo/Demo/ViewController.h


Tips: 這就是為什麼備份/恢復Mac後,需要clean build folder,因為兩臺mac對應檔案的物理位置可能不一樣。

clang發現#import "TestView.h"的時候,先在essay-headermap(Demo-generated-files.hmap,Demo-project-essay-headers.hmap)裡查詢,如果essay-headermap檔案找不到,接著在own target的framework裡找:

/Users/…/Build/Products/Debug-iphoneos/AFNetworking/AFNetworking.framework/Headers/TestView.h
/Users/…/Build/Products/Debug-iphoneos/SDWebImage/SDWebImage.framework/Headers/TestView.h


系統的頭檔案查詢的時候也是優先essay-headermap,essay-headermap查詢不到會查詢own target framework,最後查詢SDK目錄。

#import 為例,在SDK目錄查詢時:

首先查詢framework是否存在

$SDKROOT/System/Library/Frameworks/Foundation.framework


如果framework存在,再在essay-headers目錄裡查詢頭檔案是否存在

$SDKROOT/System/Library/Frameworks/Foundation.framework/essay-headers/Foundation.h


Clang Module

傳統的#include/#import都是文字語意:前處理器在處理的時候會把這一行替換成對應頭檔案的文字,這種簡單粗暴替換是有很多問題的:

  1. 大量的預處理消耗。假如有N個頭檔案,每個頭檔案又#include了M個頭檔案,那麼整個預處理的消耗是N*M

  2. 檔案匯入後,宏定義容易出現問題。因為是文字匯入,並且按照include依次替換,當一個頭檔案定義了#define std hello_world,而第另一個頭檔案剛好又是C++標準庫,那麼include順序不同,可能會導致所有的std都會被替換。

  3. 邊界不明顯。拿到一組.a和.h檔案,很難確定.h是屬於哪個.a的,需要以什麼樣的順序匯入才能正確編譯。

clang module不再使用文字模型,而是採用更高效的語意模型。clang module提供了一種新的匯入方式:@import,module會被作為一個獨立的模組編譯,並且產生獨立的快取,從而大幅度提高預處理效率,這樣時間消耗從M*N變成了M+N

XCode建立的Target是Framework的時候,預設define module會設定為YES,從而支援module,當然像Foundation等系統的framwork同樣支援module。

#import 的時候,編譯器會檢查NSString.h是否在一個module裡,如果是的話,這一行會被替換成@import Foundation

那麼,如何定義一個module呢?答案是:modulemap檔案,這個檔案描述了一組頭檔案如何轉換為一個module,舉個例子:

framework module Foundation  [extern_c] [system] {
    umbrella essay-header “Foundation.h” // 所有要暴露的頭檔案
     export *
    module * {
         export *
     }
     explicit module NSDebug { //submodule
         essay-header “NSDebug.h”
         export *
     }
 }


swift是可以直接import一個clang module的,比如你有一些C庫,需要在Swift中使用,就可以用modulemap的方式。

Swift編譯

現代化的語言幾乎都拋棄了頭檔案,swift也不例外。問題來了,swift沒有頭檔案又是怎麼找到宣告的呢?

編譯器幹了這些臟活累活。編譯一個Swift頭檔案,需要解析module中所有的Swift檔案,找到對應的宣告

當開發中難免要有Objective C和Swfit相互呼叫的場景,兩種語言在編譯的時候查詢符號的方式不同,如何一起工作的呢?

Swift取用Objective C

Swift的編譯器內部使用了clang,所以swift可以直接使用clang module,從而支援直接import Objective C編寫的framework。

swift編譯器會從objective c頭檔案裡查詢符號,頭檔案的來源分為兩大類:

  • Bridging-Header.h中暴露給swfit的頭檔案

  • framework中公開的頭檔案,根據編寫的語言不同,可能從modulemap或者umbrella essay-header查詢

XCode提供了宏定義NS_SWIFT_NAME來讓開發者定義Objective C => Swift的符號對映,可以透過Related Items -> Generate Interface來檢視轉換後的結果:

Objective取用swift

xcode會以module為單位,為swift自動生成頭檔案,供Objective C取用,通常這個檔案命名為ProductName-Swift.h

swift提供了關鍵詞@objc來把型別暴露給Objective C和Objective C Runtime。

@objc public class MyClass


深入理解Linker

聯結器會把編譯器編譯生成的多個檔案,連結成一個可執行檔案。連結並不會產生新的程式碼,只是在現有程式碼的基礎上做移動和補丁。

 

聯結器的輸入可能是以下幾種檔案:

  • object file(.o),單個源檔案的編輯結果,包含了由符號表示的程式碼和資料。

  • 動態庫(.dylib),mach o型別的可執行檔案,連結的時候只會系結符號,動態庫會被複製到app裡,執行時載入

  • 靜態庫(.a),由ar命令打包的一組.o檔案,連結的時候會把具體的程式碼複製到最後的mach-o

  • tbd,只包含符號的庫檔案

這裡我們提到了一個概念:符號(Symbols),那麼符號是什麼呢?

符號是一段程式碼或者資料的名稱,一個符號內部也有可能取用另一個符號。

以一段程式碼為例,看看連結時究竟發生了什麼?

原始碼:

– (void)log{
    printf(“hello world
);
}


.o檔案:

#程式碼
adrp    x0l_.str@PAGE
add     x0x0l_.str@PAGEOFF
bl      _printf

#字串符號
l_.str:                                 ; @.str
        .asciz  “hello world


在.o檔案中,字串”hello world “作為一個符號(l_.str)被取用,彙編程式碼讀取的時候按照l_.str所在的頁加上偏移量的方式讀取,然後呼叫printf符號。到這一步,CPU還不知道怎麼執行,因為還有兩個問題沒解決:

  1. l_.str在可執行檔案的哪個位置?

  2. printf函式來自哪裡?

再來看看連結之後的mach o檔案:

聯結器如何解決這兩個問題呢?

  1. 連結後,不再是以頁+偏移量的方式讀取字串,而是直接讀虛擬記憶體中的地址,解決了l_.str的位置問題。

  2. 連結後,不再是呼叫符號_printf,而是在DATA段上建立了一個函式指標_printf$ptr,初始值為0x0(null),程式碼直接呼叫這個函式指標。啟動的時候,dyld會把DATA段上的指標進行動態系結,系結到具體虛擬記憶體中的_printf地址。更多細節,可以參考我之前的這篇文章:深入理解iOS App的啟動過程。

Tips: Mach-O有一個區域叫做LINKEDIT,這個區域用來儲存啟動的時dyld需要動態修複的一些資料:比如剛剛提到的printf在記憶體中的地址。

理解簽名

基礎回顧

非對稱加密。在密碼學中,非對稱加密需要兩個金鑰:公鑰和私鑰。私鑰加密的只能用公鑰解密,公鑰加密的只能用私鑰解密。

數字簽名。數字簽名錶示我對資料做了個標記,表示這是我的資料,沒有經過篡改。

資料傳送方Leo產生一對公私鑰,私鑰自己儲存,公鑰發給接收方Lina。Leo用摘要演演算法,對傳送的資料生成一段摘要,摘要演演算法保證了只要資料修改,那麼摘要一定改變。然後用私鑰對這個摘要進行加密,和資料一起傳送給Lina。

Lina收到資料後,用公鑰解密簽名,得到Leo發過來的摘要;然後自己按照同樣的摘要演演算法計算摘要,如果計算的結果和Leo的一樣,說明資料沒有被篡改過。

但是,現在還有個問題:Lina有一個公鑰,假如攻擊者把Lina的公鑰替換成自己的公鑰,那麼攻擊者就可以偽裝成Leo進行通訊,所以Lina需要確保這個公鑰來自於Leo,可以透過數字證書來解決這個問題。

數字證書由CA(Certificate Authority)頒發,以Leo的證書為例,裡麵包含了以下資料:簽發者Leo的公鑰Leo使用的Hash演演算法證書的數字簽名;到期時間等。

有了數字證書後,Leo再傳送資料的時候,把自己從CA申請的證書一起傳送給Lina。Lina收到資料後,先用CA的公鑰驗證證書的數字簽名是否正確,如果正確說明證書沒有被篡改過,然後以信任鏈的方式判斷是否信任這個證書,如果信任證書,取出證書中的資料,可以判斷出證書是屬於Leo的,最後從證書中取出公鑰來做資料簽名驗證。

iOS App簽名

為什麼要對App進行簽名呢?簽名能夠讓iOS識別出是誰簽名了App,並且簽名後App沒有被篡改過

除此之外,Apple要嚴格控制App的分發:

  1. App來自Apple信任的開發者

  2. 安裝的裝置是Apple允許的裝置 

證書

透過上文的講解,我們知道數字證書裡包含著申請證書裝置的公鑰,所以在Apple開發者後臺建立證書的時候,需要上傳CSR檔案(Certificate Signing Request),用keychain生成這個檔案的時候,就生成了一對公/私鑰:公鑰在CSR裡,私鑰在本地的Mac上。Apple本身也有一對公鑰和私鑰:私鑰儲存在Apple後臺,公鑰在每一臺iOS裝置上

Provisioning Profile

iOS App安裝到裝置的途徑(非越獄)有以下幾種:

  1. 開發包(插線,或者archive匯出develop包)

  2. Ad Hoc

  3. App Store

  4. 企業證書

開發包和Ad Hoc都會嚴格限制安裝裝置,為了把裝置uuid等資訊一起打包進App,開發者需要配置Provisioning Profile。

可以透過以下命令來檢視Provisioning Profile中的內容:

security cms -D -i embedded.mobileprovision > result.plist
open result.plist


本質上就是一個編碼過後的plist

iOS簽名

生成安裝包的最後一步,XCode會呼叫codesign對Product.app進行簽名。

建立一個額外的目錄_CodeSignature以plist的方式存放安裝包內每一個檔案簽名

<key>Base.lproj/LaunchScreen.storyboardc/01J-lp-oVM-view-Ze5-6b-2t3.nibkey>



<data>
T2g5jlq7EVFHNzL/ip3fSoXKoOI=
data>
<key>Info.plistkey>
<data>
5aVg/3m4y30m+GSB8LkZNNU3mug=
data>
<key>PkgInfokey>
<data>
n57qDP4tZfLD1rCS43W0B4LQjzE=
data>
<key>embedded.mobileprovisionkey>
<data>
tm/I1g+0u2Cx9qrPJeC0zgyuVUE=
data>

程式碼簽名會直接寫入到mach-o的可執行檔案裡,值得註意的是簽名是以頁(Page)為單位的,而不是整個檔案簽名:

驗證

在安裝App的時候,

  • 從embedded.mobileprovision取出證書,驗證證書是否來自Apple信任的開發者

  • 證書驗證透過後,從證書中取出Leo的公鑰

  • 讀取_CodeSignature中的簽名結果,用Leo的公鑰驗證每個檔案的簽名是否正確

  • 檔案embedded.mobileprovision驗證透過後,讀取裡面的裝置id串列,判斷當前裝置是否可安裝(App Store和企業證書不做這步驗證)

  • 驗證透過後,安裝App

啟動App的時候:

  • 驗證bundle id,entitlements和embedded.mobileprovision中的AppId,entitlements是否一致

  • 判斷device id包含在embedded.mobileprovision裡

    • App Store和企業證書不做驗證

  • 如果是企業證書,驗證使用者是否信任企業證書

  • App啟動後,當缺頁中斷(page fault)發生的時候,系統會把對應的mach-o頁讀入物理記憶體,然後驗證這個page的簽名是否正確。

  • 以上都驗證透過,App才能正常啟動

小結

如有內容錯誤,歡迎 issue (https://link.juejin.im/?target=https%3A%2F%2Fgithub.com%2FLeoMobileDeveloper%2FBlogs)指正。

相關連結

  • iOS編譯過程的原理和應用
    https://github.com/LeoMobileDeveloper/Blogs/blob/master/iOS/iOS%E7%BC%96%E8%AF%91%E8%BF%87%E7%A8%8B%E7%9A%84%E5%8E%9F%E7%90%86%E5%92%8C%E5%BA%94%E7%94%A8.md

  • Introduction to the Clang AST
    https://clang.llvm.org/docs/IntroductionToTheClangAST.html

  • Writing an LLVM Pass
    https://llvm.org/docs/WritingAnLLVMPass.html

  • iOS彙編快速入門
    https://github.com/LeoMobileDeveloper/Blogs/blob/master/Basic/iOS%20assembly%20toturial%20part%201.md

  • hmap
    https://github.com/milend/hmap

贊(0)

分享創造快樂