深刻淺出iOS編譯

前言

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

  • Clang和LLVM到底是什麼
  • 源文件到機器碼的細節
  • Linker作了哪些工做
  • 編譯順序如何肯定
  • 頭文件是什麼?XCode是如何找到頭文件的?
  • Clang Module
  • 簽名是什麼?爲何要簽名

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

編譯器

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

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

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

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

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

編譯器架構

LLVM

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

LLVM的核心庫提供了現代化的source-target-independent優化器和支持諸多流行CPU架構的代碼生成器,這些核心代碼是圍繞着LLVM IR(中間代碼)創建的。後端

基於LLVM,又衍生出了一些強大的子項目,其中iOS開發者耳熟能詳的是:ClangLLDBxcode

clang

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

clang編譯流程

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

#include <stdio.h>
// 一點註釋
#define DEBUG 1
int main() {
#ifdef DEBUG
  printf("hello debug\n");
#else
  printf("hello world\n");
#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\n");
  return 0;
}
複製代碼

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

詞法分析(lexical anaysis)

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

$ xcrun clang -fmodules -fsyntax-only -Xclang -dump-tokens main.c
複製代碼

輸出:

annot_module_include '#include <s'		Loc=<main.c:1:1>
int 'int'	 [StartOfLine]	Loc=<main.c:4:1>
identifier 'main'	 [LeadingSpace]	Loc=<main.c:4:5>
....
複製代碼

Loc=<main.c: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`-FunctionDecl 0x7fcc188dc700 <main.c:4:1, line:11:1> line:4:5 main 'int ()'
  `-CompoundStmt 0x7fcc188dc918 <col:12, line:11:1>
    |-CallExpr 0x7fcc188dc880 <line:6:3, col:25> 'int'
    | |-ImplicitCastExpr 0x7fcc188dc868 <col:3> 'int (*)(const char *, ...)' <FunctionToPointerDecay>
    | | `-DeclRefExpr 0x7fcc188dc7a0 <col:3> 'int (const char *, ...)' Function 0x7fcc188c5160 'printf' 'int (const char *, ...)'
    | `-ImplicitCastExpr 0x7fcc188dc8c8 <col:10> 'const char *' <BitCast>
    |   `-ImplicitCastExpr 0x7fcc188dc8b0 <col:10> 'char *' <ArrayToPointerDecay>
    |     `-StringLiteral 0x7fcc188dc808 <col:10> 'char [13]' lvalue "hello debug\n"
    `-ReturnStmt 0x7fcc188dc900 <line:10:3, col:10>
      `-IntegerLiteral 0x7fcc188dc8e0 <col:10> 'int' 0
複製代碼

有了抽象語法樹,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 debug\0A\00", 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_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模塊中,能夠找到編譯的詳細日誌:

XCode編譯

詳細的步驟以下:

  • 建立Product.app的文件夾
  • 把Entitlements.plist寫入到DerivedData裏,處理打包的時候須要的信息(好比application-identifier)。
  • 建立一些輔助文件,好比各類.hmap,這是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的編譯順序:

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 <Foundation/Foundation.h> //系統或者內部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 (headermap)
 /Users/.../Build/Intermediates.noindex/Demo.build/Debug-iphoneos/Demo.build/Demo-project-headers.hmap (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-headers.hmap (headermap)
 /Users/.../Build/Intermediates.noindex/Demo.build/Debug-iphoneos/Demo.build/Demo-all-non-framework-target-headers.hmap (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,headermap是幫助編譯器找到頭文件的輔助文件:存儲這頭文件到其物理路徑的映射關係。

能夠經過一個輔助的小工具hmap查看hmap中的內容:

192:Desktop Leo$ ./hmap print Demo-project-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"的時候,先在headermap(Demo-generated-files.hmap,Demo-project-headers.hmap)裏查找,若是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
複製代碼

系統的頭文件查找的時候也是優先headermap,headermap查找不到會查找own target framework,最後查找SDK目錄。

#import <Foundation/Foundation.h>爲例,在SDK目錄查找時:

首先查找framework是否存在

$SDKROOT/System/Library/Frameworks/Foundation.framework
複製代碼

若是framework存在,再在headers目錄裏查找頭文件是否存在

$SDKROOT/System/Library/Frameworks/Foundation.framework/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 <Foundation/NSString.h>的時候,編譯器會檢查NSString.h是否在一個module裏,若是是的話,這一行會被替換成@import Foundation

Module

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

framework module Foundation  [extern_c] [system] {
	umbrella header "Foundation.h" // 全部要暴露的頭文件
 	export *
	module * {
 		export *
 	}
 	explicit module NSDebug { //submodule
 		header "NSDebug.h"
 		export *
 	}
 }
複製代碼

swift是能夠直接import一個clang module的,好比你有一些C庫,須要在Swift中使用,就能夠用modulemap的方式。

Swift編譯

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

編譯器幹了這些髒活累活。編譯一個Swift頭文件,須要解析module中全部的Swift文件,找到對應的聲明

swift編譯

當開發中不免要有Objective C和Swfit相互調用的場景,兩種語言在編譯的時候查找符號的方式不一樣,如何一塊兒工做的呢?

Swift引用Objective C

Swift的編譯器內部使用了clang,因此swift能夠直接使用clang module,從而支持直接import Objective C編寫的framework。

swiftc & clang

swift編譯器會從objective c頭文件裏查找符號,頭文件的來源分爲兩大類:

  • Bridging-Header.h中暴露給swfit的頭文件
  • framework中公開的頭文件,根據編寫的語言不一樣,可能從modulemap或者umbrella header查找

XCode提供了宏定義NS_SWIFT_NAME來讓開發者定義Objective C => Swift的符號映射,能夠經過Related Items -> Generate Interface來查看轉換後的結果:

Objective C => Swift

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\n");
}
複製代碼

.o文件:

#代碼
adrp    x0, l_.str@PAGE
add     x0, x0, l_.str@PAGEOFF
bl      _printf

#字符串符號
l_.str:                                 ; @.str
        .asciz  "hello world\n"
複製代碼

在.o文件中,字符串"hello world\n"做爲一個符號(l_.str)被引用,彙編代碼讀取的時候按照l_.str所在的頁加上偏移量的方式讀取,而後調用printf符號。到這一步,CPU還不知道怎麼執行,由於還有兩個問題沒解決:

  1. l_.str在可執行文件的哪一個位置?
  2. printf函數來自哪裏?

再來看看連接以後的mach o文件:

Linker

連接器如何解決這兩個問題呢?

  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

能夠經過如下命令來查看Provisioning Profile中的內容:

security cms -D -i embedded.mobileprovision > result.plist
open result.plist
複製代碼

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

Provisioning Profile

iOS簽名

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

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

<key>Base.lproj/LaunchScreen.storyboardc/01J-lp-oVM-view-Ze5-6b-2t3.nib</key>
<data>
T2g5jlq7EVFHNzL/ip3fSoXKoOI=
</data>
<key>Info.plist</key>
<data>
5aVg/3m4y30m+GSB8LkZNNU3mug=
</data>
<key>PkgInfo</key>
<data>
n57qDP4tZfLD1rCS43W0B4LQjzE=
</data>
<key>embedded.mobileprovision</key>
<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指正。

相關文章
相關標籤/搜索