深入剖析IOS Clang

前言

2000年,伊利諾伊大學厄巴納-香檳分校(University of Illinois at Urbana-Champaign 簡稱UIUC)這所享有世界聲望的一流公立研究型大學的 Chris Lattner(他的 twitter @clattner_llvm ) 開發了一個叫作 Low Level Virtual Machine 的編譯器開發工具套件,後來涉及範圍越來越大,可以用於常規編譯器,JIT編譯器,彙編器,調試器,靜態分析工具等一系列跟編程語言相關的工作,於是就把簡稱 LLVM 這個簡稱作為了正式的名字。Chris Lattner 後來又開發了 Clang,使得 LLVM 直接挑戰 GCC 的地位。2012年,LLVM 獲得美國計算機學會 ACM 的軟件系統大獎,和 UNIX,WWW,TCP/IP,Tex,JAVA 等齊名。

Chris Lattner。這哥們生於1978年。

2005年,Chris Lattner加入Apple。因為Apple對於GCC支持Objective-C不力的不滿,LLVM和Clang成為Apple替代GCC的殺手級武器。

iOS 開發中 Objective-C 是 Clang / LLVM 來編譯的。

swift 是 Swift / LLVM,其中 Swift 前端會多出 SIL optimizer,它會把 .swift 生成的中間代碼 .sil 屬於 High-Level IR, 因為 swift 在編譯時就完成了方法綁定直接通過地址調用屬於強類型語言,方法調用不再是像OC那樣的消息發送,這樣編譯就可以獲得更多的信息用在後面的後端優化上。

LLVM是一個模塊化和可重用的編譯器和工具鏈技術的集合,Clang 是 LLVM 的子項目,是 C,C++ 和 Objective-C 編譯器,目的是提供驚人的快速編譯,比 GCC 快3倍,其中的 clang static analyzer 主要是進行語法分析,語義分析和生成中間代碼,當然這個過程會對代碼進行檢查,出錯的和需要警告的會標註出來。LLVM 核心庫提供一個優化器,對流行的 CPU 做代碼生成支持。lld 是 Clang / LLVM 的內置鏈接器,clang 必須調用鏈接器來產生可執行文件。

LLVM 比較有特色的一點是它能提供一種代碼編寫良好的中間表示 IR,這意味著它可以作為多種語言的後端,這樣就能夠提供語言無關的優化同時還能夠方便的針對多種 CPU 的代碼生成。

LLVM 還用在 Gallium3D 中進行 JIT 優化,Xorg 中的 pixman 也有考慮使用 LLVM 優化執行速度, LLVM-Lua 用LLVM 來編譯 lua 代碼, gpuocelot 使用 LLVM 可以讓 CUDA 程序無需重新編譯就能夠在多種 CPU 機器上跑。

這裡是 Clang 官方詳細文檔: Welcome to Clang’s documentation! — Clang 4.0 documentation

這篇是對 LLVM 架構的一個概述: The Architecture of Open Source Applications

將編譯器之前對於編譯的前世今生也是需要了解的,比如回答下這個問題,編譯器程序是用什麼編譯的?看看 《linkers and loaders》 這本書就知道了。

編譯流程

在列出完整步驟之前可以先看個簡單例子。看看是如何完成一次編譯的。

#import <foundation>

#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;

}

在命令行輸入

clang -ccc-print-phases main.m

可以看到編譯源文件需要的幾個不同的階段

0: input, "main.m", objective-c

1: preprocessor, {0}, objective-c-cpp-output

2: compiler, {1}, ir

3: backend, {2}, assembler

4: assembler, {3}, object

5: linker, {4}, image

6: bind-arch, "x86_64", {5}, image

這樣能夠了解到過程和重要的信息。查看oc的c實現可以使用如下命令

clang -rewrite-objc main.m

查看操作內部命令,可以使用 -### 命令

clang -### main.m -o main

想看清clang的全部過程,可以先通過-E查看clang在預處理處理這步做了什麼。

clang -E main.m

執行完後可以看到文件

# 1 "/System/Library/Frameworks/Foundation.framework/Headers/FoundationLegacySwiftCompatibility.h" 1 3

# 185 "/System/Library/Frameworks/Foundation.framework/Headers/Foundation.h" 2 3

# 2 "main.m" 2

int main(){

@autoreleasepool {

int eight = 8;

int six = 6;

NSString* site = [[NSString alloc] initWithUTF8String:"starming"];

int rank = eight + six;

NSLog(@"%@ rank %d", site, rank);

}

return 0;

}

這個過程的處理包括宏的替換,頭文件的導入,以及類似#if的處理。預處理完成後就會進行詞法分析,這裡會把代碼切成一個個 Token,比如大小括號,等於號還有字符串等。

clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m

然後是語法分析,驗證語法是否正確,然後將所有節點組成抽象語法樹 AST 。

clang -fmodules -fsyntax-only -Xclang -ast-dump main.m

完成這些步驟後就可以開始IR中間代碼的生成了,CodeGen 會負責將語法樹自頂向下遍歷逐步翻譯成 LLVM IR,IR 是編譯過程的前端的輸出後端的輸入。

clang -S -fobjc-arc -emit-llvm main.m -o main.ll

這裡 LLVM 會去做些優化工作,在 Xcode 的編譯設置裡也可以設置優化級別-01,-03,-0s,還可以寫些自己的 Pass,官方有比較完整的 Pass 教程: Writing an LLVM Pass — LLVM 5 documentation 。

clang -O3 -S -fobjc-arc -emit-llvm main.m -o main.ll

Pass 是 LLVM 優化工作的一個節點,一個節點做些事,一起加起來就構成了 LLVM 完整的優化和轉化。

如果開啟了 bitcode 蘋果會做進一步的優化,有新的後端架構還是可以用這份優化過的 bitcode 去生成。

clang -emit-llvm -c main.m -o main.bc

生成彙編

clang -S -fobjc-arc main.m -o main.s

生成目標文件

clang -fmodules -c main.m -o main.o

生成可執行文件,這樣就能夠執行看到輸出結果

clang main.o -o main

執行

./main

輸出

starming rank 14

下面是完整步驟:

  • 編譯信息寫入輔助文件,創建文件架構 .app 文件
  • 處理文件打包信息
  • 執行 CocoaPod 編譯前腳本,checkPods Manifest.lock
  • 編譯.m文件,使用 CompileC 和 clang 命令
  • 鏈接需要的 Framework
  • 編譯 xib
  • 拷貝 xib ,資源文件
  • 編譯 ImageAssets
  • 處理 info.plist
  • 執行 CocoaPod 腳本
  • 拷貝標準庫
  • 創建 .app 文件和簽名

Clang 編譯 .m 文件

在 Xcode 編譯過後,可以通過 Show the report navigator 裡對應 target 的 build 中查看每個 .m 文件的 clang 參數信息,這些參數都是通過Build Setting。

具體拿編譯 AFSecurityPolicy.m 的信息來看看。首先對任務進行描述。

CompileC DerivedData path/AFSecurityPolicy.o AFNetworking/AFNetworking/AFSecurityPolicy.m normal x86_64 objective-c com.apple.compilers.llvm.clang.1_0.compiler

接下來對會更新工作路徑,同時設置 PATH

cd /Users/didi/Documents/Demo/GitHub/GCDFetchFeed/GCDFetchFeed/Pods

export LANG=en_US.US-ASCII

export PATH="/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/usr/bin:/Applications/Xcode.app/Contents/Developer/usr/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"

接下來就是實際的編譯命令

clang -x objective-c -arch x86_64 -fmessage-length=0 -fobjc-arc... -Wno-missing-field-initializers ... -DDEBUG=1 ... -isysroot iPhoneSimulator10.1.sdk -fasm-blocks ... -I -F -c AFSecurityPolicy.m -o AFSecurityPolicy.o

clang 命令參數

-x 編譯語言比如objective-c

-arch 編譯的架構,比如arm7

-f 以-f開頭的。

-W 以-W開頭的,可以通過這些定製編譯警告

-D 以-D開頭的,指的是預編譯宏,通過這些宏可以實現條件編譯

-iPhoneSimulator10.1.sdk 編譯採用的iOS SDK版本

-I 把編譯信息寫入指定的輔助文件

-F 需要的Framework

-c 標識符指明需要運行預處理器,語法分析,類型檢查,LLVM生成優化以及彙編代碼生成.o文件

-o 編譯結果

構建 Target

編譯工程中的第三方依賴庫後會構建我們程序的 target,會按順序輸出如下的信息:

Create product structure

Process product packaging

Run custom shell>

Compile ... 各個項目中的.m文件

Link /Users/... 路徑

Copy ... 靜態文件

Compile asset catalogs

Compile Storyboard file ...

Process info.plist

Link Storyboards

Run custom shell>

Run custom shell>

...

Touch GCDFetchFeed.app

Sign GCDFetchFeed.app

從這些信息可以看出在這些步驟中會分別調用不同的命令行工具來執行。

Target 在 Build 過程的控制

在 Xcode 的 Project editor 中的 Build Setting,Build Phases 和 Build Rules 能夠控制編譯的過程。

Build Phases

構建可執行文件的規則。指定 target 的依賴項目,在 target build 之前需要先 build 的依賴。在 Compile Source 中指定所有必須編譯的文件,這些文件會根據 Build Setting 和 Build Rules 裡的設置來處理。

在 Link Binary With Libraries 裡會列出所有的靜態庫和動態庫,它們會和編譯生成的目標文件進行鏈接。

build phase 還會把靜態資源拷貝到 bundle 裡。

可以通過在 build phases 裡添加自定義腳本來做些事情,比如像 CocoaPods 所做的那樣。

Build Rules

指定不同文件類型如何編譯。每條 build rule 指定了該類型如何處理以及輸出在哪。可以增加一條新規則對特定文件類型添加處理方法。

Build Settings

在 build 的過程中各個階段的選項的設置。

pbxproj工程文件

build 過程控制的這些設置都會被保存在工程文件 .pbxproj 裡。在這個文件中可以找 rootObject 的 ID 值

rootObject = 3EE311301C4E1F0800103FA3 /* Project object */;

然後根據這個 ID 找到 main 工程的定義。

/* Begin PBXProject section */

3EE311301C4E1F0800103FA3 /* Project object */ = {

isa = PBXProject;

...

/* End PBXProject section */

在 targets 裡會指向各個 taget 的定義

targets = (

3EE311371C4E1F0800103FA3 /* GCDFetchFeed */,

3EE311501C4E1F0800103FA3 /* GCDFetchFeedTests */,

3EE3115B1C4E1F0800103FA3 /* GCDFetchFeedUITests */,

);

順著這些 ID 就能夠找到更詳細的定義地方。比如我們通過 GCDFetchFeed 這個 target 的 ID 找到定義如下:

3EE311371C4E1F0800103FA3 /* GCDFetchFeed */ = {

isa = PBXNativeTarget;

buildConfigurationList = 3EE311651C4E1F0800103FA3 /* configuration list for PBXNativeTarget "GCDFetchFeed"

buildPhases = (

9527AA01F4AAE11E18397E0C /* Check Pods st.lock */,

3EE311341C4E1F0800103FA3 /* Sources */,

3EE311351C4E1F0800103FA3 /* Frameworks */,

3EE311361C4E1F0800103FA3 /* Resources */,

C3DDA7C46C0308459A18B7D9 /* Embed Pods Frameworks

DD33A716222617FAB49F1472 /* Copy Pods Resources

);

buildRules = (

);

dependencies = (

);

name = GCDFetchFeed;

productName = GCDFetchFeed;

productReference = 3EE311381C4E1F0800103FA3 /* chFeed.app */;

productType = "com.apple.product-type.application";

};

這個裡面又有更多的 ID 可以得到更多的定義,其中 buildConfigurationList 指向了可用的配置項,包含 Debug 和 Release。可以看到還有 buildPhases,buildRules 和 dependencies 都能夠通過這裡索引找到更詳細的定義。

接下來詳細的看看 Clang 所做的事情吧。

Clang Static Analyzer靜態代碼分析

可以在 llvm/clang/ Source Tree - Woboq Code Browser 上查看 Clang 的代碼。

Youtube上一個教程:The Clang AST - a Tutorial - YouTube

靜態分析前會對源代碼分詞成 Token,這個過程稱為詞法分析(Lexical Analysis),在 TokensKind.def 裡有 Clang 定義的所有 Token。通過下面的命令可以輸出所有 token 和所在文件具體位置

clang -fmodules -E -Xclang -dump-tokens main.m

結果如下

annot_module_include '#import

int 'int' [StartOfLine] Loc=<5:1/>

identifier 'main' [LeadingSpace] Loc=<5:5/>

l_paren '(' Loc=<5:9/>

r_paren ')' Loc=<5:10/>

l_brace '{' [LeadingSpace] Loc=<5:12/>

at '@' [StartOfLine] [LeadingSpace] Loc=<6:5/>

identifier 'autoreleasepool' Loc=<6:6/>

l_brace '{' [LeadingSpace] Loc=<6:22/>

identifier 'NSString' [StartOfLine] [LeadingSpace] Loc=<7:9/>

star '*' [LeadingSpace] Loc=<7:18/>

identifier 'a' Loc=<7:19/>

equal '=' [LeadingSpace] Loc=<7:21/>

at '@' [LeadingSpace] Loc=<7:23/>

string_literal '"aaa"' Loc=<7:24/>

semi ';' Loc=<7:29/>

identifier 'NSLog' [StartOfLine] [LeadingSpace] Loc=<8:9/>

l_paren '(' Loc=<8:14/>

at '@' Loc=<8:15/>

string_literal '"hi %@"' Loc=<8:16/>

comma ',' Loc=<8:23/>

identifier 'a' Loc=<8:24/>

r_paren ')' Loc=<8:25/>

semi ';' Loc=<8:26/>

r_brace '}' [StartOfLine] [LeadingSpace] Loc=<9:5/>

return 'return' [StartOfLine] [LeadingSpace] Loc=<10:5/>

numeric_constant '0' [LeadingSpace] Loc=<10:12/>

semi ';' Loc=<10:13/>

r_brace '}' [StartOfLine] Loc=<11:1/>

eof '' Loc=<11:2/>

可以獲得每個 token 的類型,值還有類似 StartOfLine 的位置類型和 Loc=<11:1> 這個樣的具體位置。

接著進行語法分析(Semantic Analysis)將 token 先按照語法組合成語義生成 VarDecl 節點,然後將這些節點按照層級關係構成抽象語法樹 Abstract Syntax Tree (AST)。打印語法樹的命令

clang -fmodules -fsyntax-only -Xclang -ast-dump main.m

輸出結果

TranslationUnitDecl 0x7fa80f018ad0 <<invalid>> <invalid>/<invalid>

|-TypedefDecl 0x7fa80f018fc8 <<invalid>> <invalid> implicit __int128_t '__int128'/<invalid>/<invalid>

| `-BuiltinType 0x7fa80f018d20 '__int128'

|-TypedefDecl 0x7fa80f019028 <<invalid>> <invalid> implicit __uint128_t 'unsigned __int128'/<invalid>/<invalid>

| `-BuiltinType 0x7fa80f018d40 'unsigned __int128'

|-TypedefDecl 0x7fa80f0190b8 <<invalid>> <invalid> implicit SEL 'SEL *'/<invalid>/<invalid>

| `-PointerType 0x7fa80f019080 'SEL *'

| `-BuiltinType 0x7fa80f018f30 'SEL'

|-TypedefDecl 0x7fa80f019198 <<invalid>> <invalid> implicit id 'id'/<invalid>/<invalid>

| `-ObjCObjectPointerType 0x7fa80f019140 'id' imported

| `-ObjCObjectType 0x7fa80f019110 'id' imported

|-TypedefDecl 0x7fa80f019278 <<invalid>> <invalid> implicit Class 'Class'/<invalid>/<invalid>

| `-ObjCObjectPointerType 0x7fa80f019220 'Class'

| `-ObjCObjectType 0x7fa80f0191f0 'Class'

|-ObjCInterfaceDecl 0x7fa80f0192c8 <<invalid>> <invalid> implicit Protocol/<invalid>/<invalid>

|-TypedefDecl 0x7fa80f019618 <<invalid>> <invalid> implicit __NSConstantString 'struct __NSConstantString_tag'/<invalid>/<invalid>

| `-RecordType 0x7fa80f019430 'struct __NSConstantString_tag'

| `-Record 0x7fa80f019390 '__NSConstantString_tag'

|-TypedefDecl 0x7fa80f0196a8 <<invalid>> <invalid> implicit __builtin_ms_va_list 'char *'/<invalid>/<invalid>

| `-PointerType 0x7fa80f019670 'char *'

| `-BuiltinType 0x7fa80f018b60 'char'

|-TypedefDecl 0x7fa80f047978 <<invalid>> <invalid> implicit __builtin_va_list 'struct __va_list_tag [1]'/<invalid>/<invalid>

| `-ConstantArrayType 0x7fa80f047920 'struct __va_list_tag [1]' 1

| `-RecordType 0x7fa80f0197a0 'struct __va_list_tag'

| `-Record 0x7fa80f0196f8 '__va_list_tag'

|-ImportDecl 0x7fa80f0486b0

<2:1> col:1 implicit Foundation

|-FunctionDecl 0x7fa80f048738 <5:1 line:11:1=""> line:5:5 main 'int ()'

| `-CompoundStmt 0x7fa80f393998 <12 line:11:1=""/>

| |-ObjCAutoreleasePoolStmt 0x7fa80f393950 <6:5 line:9:5=""/>

| | `-CompoundStmt 0x7fa80f393928 <6:22 line:9:5=""/>

| | |-DeclStmt 0x7fa80f3a3b38 <7:9 col:29=""/>

| | | `-VarDecl 0x7fa80f3a3580 <9 col:24=""> col:19 used a 'NSString *' cinit

| | | `-ObjCStringLiteral 0x7fa80f3a3648 <23 col:24=""> 'NSString *'

| | | `-StringLiteral 0x7fa80f3a3618 <24> 'char [4]' lvalue "aaa"

| | `-CallExpr 0x7fa80f3938c0 <8:9 col:25=""> 'void'

| | |-ImplicitCastExpr 0x7fa80f3938a8 <9> 'void (*)(id, ...)' <functiontopointerdecay>

| | | `-DeclRefExpr 0x7fa80f3a3b50 <9> 'void (id, ...)' Function 0x7fa80f3a3670 'NSLog' 'void (id, ...)'

| | |-ImplicitCastExpr 0x7fa80f3938f8 <15 col:16=""> 'id':'id' <bitcast>

| | | `-ObjCStringLiteral 0x7fa80f393800 <15 col:16=""> 'NSString *'

| | | `-StringLiteral 0x7fa80f3a3bb8 <16> 'char [6]' lvalue "hi %@"

| | `-ImplicitCastExpr 0x7fa80f393910 <24> 'NSString *' <lvaluetorvalue>

| | `-DeclRefExpr 0x7fa80f393820 <24> 'NSString *' lvalue Var 0x7fa80f3a3580 'a' 'NSString *'

| `-ReturnStmt 0x7fa80f393980 <10:5 col:12=""/>

| `-IntegerLiteral 0x7fa80f393960 <12> 'int' 0

`-<undeserialized>

TranslationUnitDecl 是根節點,表示一個源文件。Decl 表示一個聲明,Expr 表示表達式,Literal 表示字面量是特殊的 Expr,Stmt 表示語句。

clang 靜態分析是通過建立分析引擎和 checkers 所組成的架構,這部分功能可以通過 clang —analyze 命令方式調用。clang static analyzer 分為 analyzer core 分析引擎和 checkers 兩部分,所有 checker 都是基於底層分析引擎之上,通過分析引擎提供的功能能夠編寫新的 checker。

可以通過 clang --analyze -Xclang -analyzer-checker-help 來列出當前 clang 版本下所有 checker。如果想編寫自己的 checker,可以在 clang 項目的 lib / StaticAnalyzer / Checkers 目錄下找到實例參考,比如 ObjCUnusedIVarsChecker.cpp 用來檢查未使用定義過的變量。這種方式能夠方便用戶擴展對代碼檢查規則或者對 bug 類型進行擴展,但是這種架構也有不足,每執行完一條語句後,分析引擎會遍歷所有 checker 中的回調函數,所以 checker 越多,速度越慢。通過 clang -cc1 -analyzer-checker-help 可以列出能調用的 checker,下面是常用 checker

debug.ConfigDumper Dump config table

debug.DumpCFG Display Control-Flow Graphs

debug.DumpCallGraph Display Call Graph

debug.DumpCalls Print calls as they are traversed by the engine

debug.DumpDominators Print the dominance tree for a given CFG

debug.DumpLiveVars Print results of live variable analysis

debug.DumpTraversal Print branch conditions as they are traversed by the engine

debug.ExprInspection Check the analyzer's understanding of expressions

debug.Stats Emit warnings with analyzer statistics

debug.TaintTest Mark tainted symbols as such.

debug.ViewCFG View Control-Flow Graphs using GraphViz

debug.ViewCallGraph View Call Graph using GraphViz

debug.ViewExplodedGraph View Exploded Graphs using GraphViz

這些 checker 裡最常用的是 DumpCFG,DumpCallGraph,DumpLiveVars 和 DumpViewExplodedGraph。

clang static analyzer 引擎大致分為 CFG,MemRegion,SValBuilder,ConstraintManager 和 ExplodedGraph 幾個模塊。clang static analyzer 本質上就是 path-sensitive analysis,要很好的理解 clang static analyzer 引擎就需要對 Data Flow Analysis 有所瞭解,包括迭代數據流分析,path-sensitive,path-insensitive ,flow-sensitive等。

編譯的概念(詞法->語法->語義->IR->優化->CodeGen)在 clang static analyzer 裡到處可見,例如 Relaxed Live Variables Analysis 可以減少分析中的內存消耗,使用 mark-sweep 實現 Dead Symbols 的刪除。

clang static analyzer 提供了很多輔助方法,比如 SVal.dump(),MemRegion.getString 以及 Stmt 和 Dcel 提供的 dump 方法。Clang 抽象語法樹 Clang AST 常見的 API 有 Stmt,Decl,Expr 和 QualType。在編寫 checker 時會遇到 AST 的層級檢查,這時有個很好的接口 StmtVisitor,這個接口類似 RecursiveASTVisitor。

整個 clang static analyzer 的入口是 AnalysisConsumer,接著會調 HandleTranslationUnit() 方法進行 AST 層級進行分析或者進行 path-sensitive 分析。默認會按照 inline 的 path-sensitive 分析,構建 CallGraph,從頂層 caller 按照調用的關係來分析,具體是使用的 WorkList 算法,從 EntryBlock 開始一步步的模擬,這個過程叫做 intra-procedural analysis(IPA)。這個模擬過程還需要對內存進行模擬,clang static analyzer 的內存模型是基於《A Memory Model for Static Analysis of C Programs》這篇論文而來,pdf地址:http://lcs.ios.ac.cn/~xuzb/canalyze/memmodel.pdf 在clang裡的具體實現代碼可以查看這兩個文件 MemRegion.h和 RegionStore.cpp 。

下面舉個簡單例子看看 clang static analyzer 是如何對源碼進行模擬的。

int main()

{

int a;

int b = 10;

a = b;

return a;

}

對應的 AST 以及 CFG

#----------------AST-------------------

# clang -cc1 -ast-dump

TranslationUnitDecl 0xc75b450 <<invalid>> <invalid>/<invalid>

|-TypedefDecl 0xc75b740 <<invalid>> <invalid> implicit __builtin_va_list 'char *'/<invalid>/<invalid>

`-FunctionDecl 0xc75b7b0 <1:1 line:7:1=""> line:1:5 main 'int (void)'

`-CompoundStmt 0xc75b978

<2:1 line:7:1=""/>

|-DeclStmt 0xc75b870 <3:2 col:7=""/>

| `-VarDecl 0xc75b840 <2 col:6=""> col:6 used a 'int'

|-DeclStmt 0xc75b8d8 <4:2 col:12=""/>

| `-VarDecl 0xc75b890 <2 col:10=""> col:6 used b 'int' cinit

| `-IntegerLiteral 0xc75b8c0 <10> 'int' 10

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< a = b <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

|-BinaryOperator 0xc75b928 <5:2 col:6=""> 'int' lvalue '='

| |-DeclRefExpr 0xc75b8e8 <2> 'int' lvalue Var 0xc75b840 'a' 'int'

| `-ImplicitCastExpr 0xc75b918 <6> 'int' <lvaluetorvalue>

| `-DeclRefExpr 0xc75b900 <6> 'int' lvalue Var 0xc75b890 'b' 'int'

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

`-ReturnStmt 0xc75b968 <6:2 col:9=""/>

`-ImplicitCastExpr 0xc75b958 <9> 'int' <lvaluetorvalue>

`-DeclRefExpr 0xc75b940 <9> 'int' lvalue Var 0xc75b840 'a' 'int'

#----------------CFG-------------------

# clang -cc1 -analyze -analyzer-checker=debug.DumpCFG

int main()

[B2 (ENTRY)]

Succs (1): B1

[B1]

1: int a;

2: 10

3: int b = 10;

4: b

5: [B1.4] (ImplicitCastExpr, LValueToRValue, int)

6: a

7: [B1.6] = [B1.5]

8: a

9: [B1.8] (ImplicitCastExpr, LValueToRValue, int)

10: return [B1.9];

Preds (1): B2

Succs (1): B0

[B0 (EXIT)]

Preds (1): B1

CFG 將程序拆得更細,能夠將執行的過程表現的更直觀些,為了避免路徑爆炸,函數 inline 的條件會設置的比較嚴格,函數 CFG 塊多時不會進行 inline 分析,模擬棧深度超過一定值不會進行 inline 分析,這個默認是5。

在MRC使用的是CFG這樣的執行路徑模擬,ARC就沒有了,舉個例子,沒有全部條件都返回,CFG就會報錯,而AST就不會。

官方 AST 相關文檔

  • http://clang.llvm.org/docs/Tooling.html
  • http://clang.llvm.org/docs/IntroductionToTheClangAST.html
  • http://clang.llvm.org/docs/RAVFrontendAction.html
  • http://clang.llvm.org/docs/LibTooling.html
  • http://clang.llvm.org/docs/LibASTMatchers.html

CodeGen 生成 IR 代碼

將語法樹翻譯成 LLVM IR 中間代碼,做為 LLVM Backend 輸入的橋接語言。這樣做的好處在前言裡也提到了,方便 LLVM Backend 給多語言做相同的優化,做到語言無關。

這個過程中還會跟 runtime 橋接。

  • 各種類,方法,成員變量等的結構體的生成,並將其放到對應的Mach-O的section中。
  • Non-Fragile ABI 合成 OBJC_IVAR_$_ 偏移值常量。
  • ObjCMessageExpr 翻譯成相應版本的 objc_msgSend,super 翻譯成 objc_msgSendSuper。
  • strong,weak,copy,atomic 合成 @property 自動實現 setter 和 getter。
  • @synthesize 的處理。
  • 生成 block_layout 數據結構
  • __block 和 __weak
  • _block_invoke
  • ARC 處理,插入 objc_storeStrong 和 objc_storeWeak 等 ARC 代碼。ObjCAutoreleasePoolStmt 轉 objc_autorealeasePoolPush / Pop。自動添加 [super dealloc]。給每個 ivar 的類合成 .cxx_destructor 方法自動釋放類的成員變量。

Clang Attributes

attribute(xx) 的語法格式出現,是 Clang 提供的一些能夠讓開發者在編譯過程中參與一些源碼控制的方法。下面列一些會用到的用法:

attribute((format(NSString, F, A))) 格式化字符串

可以查看 NSLog 的用法

FOUNDATION_EXPORT void NSLog(NSString *format, ...) NS_FORMAT_FUNCTION(1,2) NS_NO_TAIL_CALL;

// Marks APIs which format strings by taking a format string and optional varargs as arguments

#if !defined(NS_FORMAT_FUNCTION)

#if (__GNUC__*10+__GNUC_MINOR__ >= 42) && (TARGET_OS_MAC || TARGET_OS_EMBEDDED)

#define NS_FORMAT_FUNCTION(F,A) __attribute__((format(__NSString__, F, A)))

#else

#define NS_FORMAT_FUNCTION(F,A)

#endif

#endif

attribute((deprecated(s))) 版本棄用提示

在編譯過程中能夠提示開發者該方法或者屬性已經被棄用

- (void)preMethod:( NSString *)string __attribute__((deprecated("preMethod已經被棄用,請使用newMethod")));

- (void)deprecatedMethod DEPRECATED_ATTRIBUTE; //也可以直接使用DEPRECATED_ATTRIBUTE這個系統定義的宏

attribute((availability(os,introduced=m,deprecated=n, obsoleted=o,message="" VA_ARGS))) 指明使用版本範圍

os 指系統的版本,m 指明引入的版本,n 指明過時的版本,o 指完全不用的版本,message 可以寫入些描述信息。

- (void)method __attribute__((availability(ios,introduced=3_0,deprecated=6_0,obsoleted=7_0,message="iOS3到iOS7版本可用,iOS7不能用")));

attribute((unavailable(…))) 方法不可用提示

這個會在編譯過程中告知方法不可用,如果使用了還會讓編譯失敗。

attribute((unused))

沒有被使用也不報警告

attribute((warn_unused_result))

不使用方法的返回值就會警告,目前 swift3 已經支持該特性了。oc中也可以通過定義這個attribute來支持。

attribute((availability(swift, unavailable, message=_msg)))

OC 的方法不能在 Swift 中使用。

attribute((cleanup(…))) 作用域結束時自動執行一個指定方法

作用域結束包括大括號結束,return,goto,break,exception 等情況。這個動作是先於這個對象的 dealloc 調用的。

Reactive Cocoa 中有個比較好的使用範例,@onExit 這個宏,定義如下:

#define onExit \\

rac_keywordify \\

__strong rac_cleanupBlock_t metamacro_concat(rac_exitBlock_, __LINE__) __attribute__((cleanup(rac_executeCleanupBlock), unused)) = ^

static inline void rac_executeCleanupBlock (__strong rac_cleanupBlock_t *block) {

(*block)();

}

這樣可以在就可以很方便的把需要成對出現的代碼寫在一起了。同樣可以在 Reactive Cocoa 看到其使用

if (property != NULL) {

rac_propertyAttributes *attributes = rac_copyPropertyAttributes(property);

if (attributes != NULL) {

@onExit {

free(attributes);

};

BOOL isObject = attributes->objectClass != nil || strstr(attributes->type, @encode(id)) == attributes->type;

BOOL isProtocol = attributes->objectClass == NSClassFromString(@"Protocol");

BOOL isBlock = strcmp(attributes->type, @encode(void(^)())) == 0;

BOOL isWeak = attributes->weak;

shouldAddDeallocObserver = isObject && isWeak && !isBlock && !isProtocol;

}

}

可以看出 attributes 的設置和釋放都在一起使得代碼的可讀性得到了提高。

attribute((overloadable)) 方法重載

能夠在 c 的函數上實現方法重載。即同樣的函數名函數能夠對不同參數在編譯時能夠自動根據參數來選擇定義的函數

__attribute__((overloadable)) void printArgument(int number){

NSLog(@"Add Int %i", number);

}

__attribute__((overloadable)) void printArgument(NSString *number){

NSLog(@"Add NSString %@", number);

}

__attribute__((overloadable)) void printArgument(NSNumber *number){

NSLog(@"Add NSNumber %@", number);

}

attribute((objc_designated_initializer)) 指定內部實現的初始化方法

  • 如果是 objc_designated_initializer 初始化的方法必須調用覆蓋實現 super 的 objc_designated_initializer 方法。
  • 如果不是 objc_designated_initializer 的初始化方法,但是該類有 objc_designated_initializer 的初始化方法,那麼必須調用該類的 objc_designated_initializer 方法或者非 objc_designated_initializer 方法,而不能夠調用 super 的任何初始化方法。

attribute((objc_subclassing_restricted)) 指定不能有子類

相當於 Java 裡的 final 關鍵字,如果有子類繼承就會出錯。

attribute((objc_requires_super)) 子類繼承必須調用 super

聲明後子類在繼承這個方法時必須要調用 super,否則會出現編譯警告,這個可以定義一些必要執行的方法在 super 裡提醒使用者這個方法的內容時必要的。

attribute((const)) 重複調用相同數值參數優化返回

用於數值類型參數的函數,多次調用相同的數值型參數,返回是相同的,只在第一次是需要進行運算,後面只返回第一次的結果,這時編譯器的一種優化處理方式。

attribute((constructor(PRIORITY))) 和 attribute((destructor(PRIORITY)))

PRIORITY 是指執行的優先級,main 函數執行之前會執行 constructor,main 函數執行後會執行 destructor,+load 會比 constructor 執行的更早點,因為動態鏈接器加載 Mach-O 文件時會先加載每個類,需要 +load 調用,然後才會調用所有的 constructor 方法。

通過這個特性,可以做些比較好玩的事情,比如說類已經 load 完了,是不是可以在 constructor 中對想替換的類進行替換,而不用加在特定類的 +load 方法裡。

Clang 警告處理

先看看這個

#pragma clang diagnostic push

#pragma clang diagnostic ignored "-Wdeprecated-declarations"

sizeLabel = [self sizeWithFont:font constrainedToSize:size lineBreakMode:NSLineBreakByWordWrapping];

#pragma clang diagnostic pop

如果沒有#pragma clang 這些定義,會報出 sizeWithFont 的方法會被廢棄的警告,這個加上這個方法當然是為了兼容老系統,加上 ignored “-Wdeprecated-declarations” 的作用是忽略這個警告。通過 clang diagnostic push/pop 可以靈活的控制代碼塊的編譯選項。

使用 libclang 來進行語法分析

使用 libclang 裡面提供的方法對源文件進行語法分析,分析語法樹,遍歷語法樹上每個節點。

使用這個庫可以直接使用 C 的 API,官方也提供了 python binding。還有開源的 node-js / ruby binding,還有 Objective-C的開源庫 GitHub - macmade/ClangKit: ClangKit provides an Objective-C frontend to LibClang. Source tokenization, diagnostics and fix-its are actually implemented. 。

寫個 python 腳本來調用 clang

pip install clang

#!/usr/bin/python

# vim: set fileencoding=utf-8

import clang.cindex

import asciitree

import sys

def node_children(node):

return (c for c in node.get_children() if c.location.file == sys.argv[1])

def print_node(node):

text = node.spelling or node.displayname

kind = str(node.kind)[str(node.kind).index('.')+1:]

return '{} {}'.format(kind, text)

if len(sys.argv) != 2:

print("Usage: dump_ast.py [header file name]")

sys.exit()

clang.cindex.Config.set_library_file('/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/libclang.dylib')

index = clang.cindex.Index.create()

translation_unit = index.parse(sys.argv[1], ['-x', 'objective-c'])

print asciitree.draw_tree(translation_unit.cursor,

lambda n: list(n.get_children()),

lambda n: "%s (%s)" % (n.spelling or n.displayname, str(n.kind).split(".")[1]))

基於語法樹的分析還可以針對字符串做加密。

LibTooling 對語法樹完全的控制

因為 LibTooling 能夠完全控制語法樹,那麼可以做的事情就非常多了。

  • 可以改變 clang 生成代碼的方式。
  • 增加更強的類型檢查。
  • 按照自己的定義進行代碼的檢查分析。
  • 對源碼做任意類型分析,甚至重寫程序。
  • 給 clang 添加一些自定義的分析,創建自己的重構器。
  • 基於現有代碼做出大量的修改。
  • 基於工程生成相關圖形或文檔。
  • 檢查命名是否規範,還能夠進行語言的轉換,比如把 OC 語言轉成JS或者 Swift 。

官方有個文檔開發者可以按照這個裡面的說明去構造 LLVM,clang 和其工具: Tutorial for building tools using LibTooling and LibASTMatchers — Clang 4.0 documentation

按照說明編譯完成後進入 LLVM 的目錄 ~/llvm/tools/clang/tools/ 在這了可以創建自己的 clang 工具。這裡有個範例: GitHub - objcio/issue-6-compiler-tool: Example code for a standalone clang/llvm tool. 可以直接 make 成一個二進制文件。

下面是檢查 target 對象中是否有對應的 action 方法存在檢查的一個例子

@interface Observer

+ (instancetype)observerWithTarget:(id)target action:(SEL)selector;

@end

//查找消息表達式,observer 作為接受者,observerWithTarget:action: 作為 selector,檢查 target 中是否存在相應的方法。

virtual bool VisitObjCMessageExpr(ObjCMessageExpr *E) {

if (E->getReceiverKind() == ObjCMessageExpr::Class) {

QualType ReceiverType = E->getClassReceiver();

Selector Sel = E->getSelector();

string TypeName = ReceiverType.getAsString();

string SelName = Sel.getAsString();

if (TypeName == "Observer" && SelName == "observerWithTarget:action:") {

Expr *Receiver = E->getArg(0)->IgnoreParenCasts();

ObjCSelectorExpr* SelExpr = cast<objcselectorexpr>(E->getArg(1)->IgnoreParenCasts());/<objcselectorexpr>

Selector Sel = SelExpr->getSelector();

if (const ObjCObjectPointerType *OT = Receiver->getType()->getAs<objcobjectpointertype>()) {/<objcobjectpointertype>

ObjCInterfaceDecl *decl = OT->getInterfaceDecl();

if (! decl->lookupInstanceMethod(Sel)) {

errs() << "Warning: class " << TypeName << " does not implement selector " << Sel.getAsString() << "\\n";

SourceLocation Loc = E->getExprLoc();

PresumedLoc PLoc = astContext->getSourceManager().getPresumedLoc(Loc);

errs() << "in " << PLoc.getFilename() << " \\n";

}

}

}

}

return true;

}

ClangPlugin

通過自己寫個插件,比如上面寫的 LibTooling 的 clang 工具,可以將這個插件動態的加載到編譯器中,對編譯進行控制,可以在 LLVM 的這個目錄下查看一些範例 llvm/tools/clang/tools

動態化方案 DynamicCocoa 中就是使用了一個將 OC 源碼轉 JS 的插件來進行代碼的轉換,JSPatch 是直接手寫 JS 而沒有轉換的過程,所以也就沒有多出這一步,而鵝廠的OCS更猛,直接在端內寫了個編譯器。在 C 函數的調用上孫源有個 slides 可以看看: Calling Conventions in Cocoa by sunnyxx bang 也有篇文章: 如何動態調用 C 函數 « bang’s blog 。

這三個方案作者都分別寫了文章詳細說明其實現方案。

  • JSPatch實現原理詳解 « bang’s blog
  • DynamicCocoa:滴滴 iOS 動態化方案的誕生與起航
  • OCS——史上最瘋狂的iOS動態化方案 - 簡書

滴滴的王康在做瘦身時也實現了一個自定義的 clang 插件,具體自定義插件的實現可以查看他的這文章 《基於clang插件的一種iOS包大小瘦身方案》

那麼我們要自己動手做應該怎麼入門呢,除了本身帶的範例外還有些教程可以看看。

  • 收集一些如何使用 clang 庫的例子:GitHub - loarabia/Clang-tutorial: A collection of code samples showing usage of clang and llvm as a library
  • 在 Xcode 中添加 clang 靜態分析自定義 checks: Running the analyzer within Xcode
  • 將 LLVM C 的 API 用 swift 來包裝: GitHub - harlanhaskins/LLVMSwift: A Swifty wrapper for the LLVM C API version 3.9.1

編譯後生成的二進制內容 Link Map File

在 Build Settings 裡設置 Write Link Map File 為 Yes 後每次編譯都會在指定目錄生成這樣一個文件。文件內容包含 Object files,Sections,Symbols。下面分別說說這些內容

Object files

這個部分的內容都是 .m 文件編譯後的 .o 和需要 link 的 .a 文件。前面是文件編號,後面是文件路徑。

Sections

這裡描述的是每個 Section 在可執行文件中的位置和大小。每個 Section 的 Segment 的類型分為 __TEXT 代碼段和 __DATA 數據段兩種。

Symbols

Symbols 是對 Sections 進行了再劃分。這裡會描述所有的 methods,ivar 和字符串,及它們對應的地址,大小,文件編號信息。

每次編譯後生成的 dSYM 文件

在每次編譯後都會生成一個 dSYM 文件,程序在執行中通過地址來調用方法函數,而 dSYM 文件裡存儲了函數地址映射,這樣調用棧裡的地址可以通過 dSYM 這個映射表能夠獲得具體函數的位置。一般都會用來處理 crash 時獲取到的調用棧 .crash 文件將其符號化。

可以通過 Xcode 進行符號化,將 .crash 文件,.dSYM 和 .app 文件放到同一個目錄下,打開 Xcode 的 Window 菜單下的 organizer,再點擊 Device tab,最後選中左邊的 Device Logs。選擇 import 將 .crash 文件導入就可以看到 crash 的詳細 log 了。

還可以通過命令行工具 symbolicatecrash 來手動符號化 crash log。同樣先將 .crash 文件,.dSYM 和 .app 文件放到同一個目錄下,然後輸入下面的命令

export DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer

symbolicatecrash appName.crash appName.app > appName.log

Mach-O 文件

記錄編譯後的可執行文件,對象代碼,共享庫,動態加載代碼和內存轉儲的文件格式。不同於 xml 這樣的文件,它只是二進制字節流,裡面有不同的包含元信息的數據塊,比如字節順序,cpu 類型,塊大小等。文件內容是不可以修改的,因為在 .app 目錄中有個 _CodeSignature 的目錄,裡面包含了程序代碼的簽名,這個簽名的作用就是保證簽名後 .app 裡的文件,包括資源文件,Mach-O 文件都不能夠更改。

Mach-O 文件包含三個區域

  • Mach-O Header:包含字節順序,magic,cpu 類型,加載指令的數量等
  • Load Commands:包含很多內容的表,包括區域的位置,符號表,動態符號表等。每個加載指令包含一個元信息,比如指令類型,名稱,在二進制中的位置等。
  • Data:最大的部分,包含了代碼,數據,比如符號表,動態符號表等。

Mach-O 文件的解析

再通過一個例子來分析下:這次用 xcrun 來

xcrun clang -v

先創建一個test.c的文件

touch test.c

vi test.c

#include <stdio.h>

int main(int argc, char *argv[])

{

printf("hi there!\\n");

return 0;

}

編譯運行,沒有起名默認為 a.out

xcrun clang test.c

./a.out

a.out 就是編譯生成的二進制文件,下面看看這個二進制文件時如何生成的把。先看看輸出的彙編代碼

xcrun clang -S -o - test.c | open -f

輸出的結果裡 . 開頭的行是彙編指令不是彙編代碼,其它的都是彙編代碼。先看看前幾行

.section __TEXT,__text,regular,pure_instructions

.macosx_version_min 10, 12

.globl _main

.align 4, 0x90

.section 指令指定接下來執行哪一個段。

.globl 指令說明 _main 是一個外部符號,因為 main() 函數對於系統來說是需要調用它來運行執行文件的。

.align 指出後面代碼的對齊方式,16(2^4) 字節對齊, 0x90 補齊。

看看接下來的 main 函數頭部部分

_main: ## @main

.cfi_startproc

## BB#0:

pushq %rbp

Ltmp0:

.cfi_def_cfa_offset 16

Ltmp1:

.cfi_offset %rbp, -16

movq %rsp, %rbp

Ltmp2:

.cfi_def_cfa_register %rbp

subq $32, %rsp

_main 是函數開始的地址,二進制文件會有這個位置的引用。

.cfi_startproc 這個指令用於函數的開始,CFI 是 Call Frame Infomation 的縮寫是調用幀信息的意思,在用 debugger 時實際上就是 stepping in / out 的一個調用幀。當出現 .cfi_endproc 時表示匹對結束標記出 main() 函數結束。

pushq %rbp 是彙編代碼,## BB#0: 這個 label 裡的。ABI 會讓 rbp 這個寄存器的被保護起來,當函數調用返回時讓 rbp 寄存器的值跟以前一樣。 ABI 是 application binary interface 的縮寫表示應用二進制接口,它指定了函數調用是如何在彙編代碼層面上工作的。pushq %rbp 將 rbp 的值 push 到棧中。

.cfi_def_cfa_offset 16 和 .cfi_offset %rbp, -16 會輸出一些堆棧和調試信息,確保調試器要使用這些信息時能夠找到。

movq %rsp, %rbp 把局部變量放到棧上。

subq $32, %rsp 會將棧指針移動 32 個字節,就是函數調用的位置。舊的棧指針存在 rbp 裡作為局部變量的基址,再更新堆棧指針到會使用的位置。

再看看 printf()

leaq L_.str(%rip), %rax

movl $0, -4(%rbp)

movl %edi, -8(%rbp)

movq %rsi, -16(%rbp)

movq %rax, %rdi

movb $0, %al

callq _printf

leap 會將 L_.str 這個指針加載到 rax 寄存器裡。可以看看 L_.str 的定義

L_.str: ## @.str

.asciz "hi there\\n"

這個就是我們代碼文件裡定義的那個字符串。

這裡可以看到函數的兩個參數分別保存在 edi 和 rsi 寄存器裡,根據函數地址做了不同的偏移。

當然也可以看出在這個彙編代碼還有能夠優化的地方,因為這兩個值並沒有用,卻還是被寄存器存儲了。

printf() 是個可變參數的函數,按照 ABI 調用約定存儲參數的寄存器數量存儲在寄存器 al 中,可變所以數量設置為0,callq 會調用 printf() 函數。

接下來看看返回和函數的結束

xorl %ecx, %ecx

movl %eax, -20(%rbp) ## 4-byte Spill

movl %ecx, %eax

addq $32, %rsp

popq %rbp

retq

.cfi_endproc

xorl %ecx, %ecx 相當於將 ecx 寄存器設置為0。ABI 約定 eax 寄存器用來保存函數返回值,拷貝 ecx 到 eax 中,這樣 main() 返回值就是0。

函數執行完會恢復堆棧指針,前面是 subq 32 是把 rsp 下移32字節,addq 就是上移歸位。然後把 rbp 的值從棧裡 pop 出來。ret 會讀取出棧返回的地址,.cfi_endproc 和 .cfi_startproc 配對標記結束。

接下來是字符串輸出

.section __TEXT,__cstring,cstring_literals

L_.str: ## @.str

.asciz "hi there\\n"

.subsections_via_symbols

同樣 .section 指出進入一個新的段。最後 .subsections_via_symbols 是靜態鏈接器用的。

接下來通過 size 工具來看看 a.out 裡的 section。

xcrun size -x -l -m a.out

Segment __PAGEZERO: 0x100000000 (vmaddr 0x0 fileoff 0)

Segment __TEXT: 0x1000 (vmaddr 0x100000000 fileoff 0)

Section __text: 0x34 (addr 0x100000f50 offset 3920)

Section __stubs: 0x6 (addr 0x100000f84 offset 3972)

Section __stub_helper: 0x1a (addr 0x100000f8c offset 3980)

Section __cstring: 0xa (addr 0x100000fa6 offset 4006)

Section __unwind_info: 0x48 (addr 0x100000fb0 offset 4016)

total 0xa6

Segment __DATA: 0x1000 (vmaddr 0x100001000 fileoff 4096)

Section __nl_symbol_ptr: 0x10 (addr 0x100001000 offset 4096)

Section __la_symbol_ptr: 0x8 (addr 0x100001010 offset 4112)

total 0x18

Segment __LINKEDIT: 0x1000 (vmaddr 0x100002000 fileoff 8192)

total 0x100003000

可以看出有四個 segment 和多個section。

在運行時,虛擬內存會把 segment 映射到進程的地址空間,虛擬內存會避免將全部執行文件全部加載到內存。

__PAGEZERO segment 的大小是 4GB,不是文件真實大小,是規定進程地址空間前 4GB 被映射為不可執行,不可寫和不可讀。

__TEXT segment 包含被執行的代碼以只讀和可執行的方式映射。

  • __text section 包含編譯後的機器碼。
  • __stubs 和 __stub_helper 是給動態鏈接器 dyld 使用,可以允許延遲鏈接。
  • __cstring 可執行文件中的字符串。
  • __const 不可變的常量。

__DATA segment 以可讀寫和不可執行的方式映射,裡面是會被更改的數據。

  • __nl_symbol_ptr 非延遲指針。可執行文件加載同時加載。
  • __la_symbol_ptr 延遲符號指針。延遲用於可執行文件中調用未定義的函數,可執行文件裡沒有包含的函數會延遲加載。
  • __const 需要重定向的常量,例如 char * const c = “foo”; c指針指向可變的數據。
  • __bss 不用初始化的靜態變量,例如 static int i; ANSI C 標準規定靜態變量必須設置為0。運行時靜態變量的值是可修改的。
  • __common 包含外部全局變量。例如在函數外定義 int i;
  • __dyld 是section佔位符,用於動態鏈接器。

更多 section 類型介紹可以查看蘋果文檔: OS X Assembler Reference

接下來用 otool 查看下 section 裡的內容:

xcrun otool -s __TEXT __text a.out

a.out:

Contents of (__TEXT,__text) section

0000000100000f50 55 48 89 e5 48 83 ec 20 48 8d 05 47 00 00 00 c7

0000000100000f60 45 fc 00 00 00 00 89 7d f8 48 89 75 f0 48 89 c7

0000000100000f70 b0 00 e8 0d 00 00 00 31 c9 89 45 ec 89 c8 48 83

0000000100000f80 c4 20 5d c3

這個返回的內容很難讀,加個 - v 就可以查看反彙編代碼了, -s __TEXT __text 有個縮寫 -t

xcrun otool -v -t a.out

a.out:

(__TEXT,__text) section

_main:

0000000100000f50 pushq %rbp

0000000100000f51 movq %rsp, %rbp

0000000100000f54 subq $0x20, %rsp

0000000100000f58 leaq 0x47(%rip), %rax

0000000100000f5f movl $0x0, -0x4(%rbp)

0000000100000f66 movl %edi, -0x8(%rbp)

0000000100000f69 movq %rsi, -0x10(%rbp)

0000000100000f6d movq %rax, %rdi

0000000100000f70 movb $0x0, %al

0000000100000f72 callq 0x100000f84

0000000100000f77 xorl %ecx, %ecx

0000000100000f79 movl %eax, -0x14(%rbp)

0000000100000f7c movl %ecx, %eax

0000000100000f7e addq $0x20, %rsp

0000000100000f82 popq %rbp

0000000100000f83 retq

看起來是不是很熟悉,和前面的編譯時差不多,不同的就是沒有彙編指令。

現在來看看可執行文件。

通過 otool 來看看可執行文件頭部, 通過 -h 可以打印出頭部信息:

otool -v -h a.out

Mach header

magic cputype cpusubtype caps filetype ncmds sizeofcmds flags

MH_MAGIC_64 X86_64 ALL LIB64 EXECUTE 15 1200 NOUNDEFS DYLDLINK TWOLEVEL PIE

mach_header 結構體

struct mach_header {

uint32_t magic;

cpu_type_t cputype;

cpu_subtype_t cpusubtype;

uint32_t filetype;

uint32_t ncmds;

uint32_t sizeofcmds;

uint32_t flags;

};

cputype 和 cpusubtype 規定可執行文件可以在哪些目標架構運行。ncmds 和 sizeofcmds 是加載命令。通過 -l 可以查看加載命令

otool -v -l a.out | open -f

加載命令結構體

struct segment_command {

uint32_t cmd;

uint32_t cmdsize;

char segname[16];

uint32_t vmaddr;

uint32_t vmsize;

uint32_t fileoff;

uint32_t filesize;

vm_prot_t maxprot;

vm_prot_t initprot;

uint32_t nsects;

uint32_t flags;

};

查看 Load command 1 這個部分可以找到 initprot r-x ,表示只讀和可執行。

在加載命令裡還是看看 __TEXT __text 的section的內容

Section

sectname __text

segname __TEXT

addr 0x0000000100000f50

size 0x0000000000000034

offset 3920

align 2^4 (16)

reloff 0

nreloc 0

type S_REGULAR

attributes PURE_INSTRUCTIONS SOME_INSTRUCTIONS

reserved1 0

reserved2 0

addr 的值表示代碼的位置地址,在上面反彙編的代碼裡可以看到地址是一樣的,offset 表示在文件中的偏移量。

單個文件的就這樣了,但是工程都是多個源文件的,那麼多個文件是怎麼合成一個可執行文件的呢?那麼建多個文件來看看先。Foo.h

#import <foundation>

@interface Foo : NSObject

- (void)say;

@end

Foo.m

#import "Foo.h"

@implementation Foo

- (void)say

{

NSLog(@"hi there again!\\n");

}

@end

SayHi.m

#import "Foo.h"

int main(int argc, char *argv[])

{

@autoreleasepool {

Foo *foo = [[Foo alloc] init];

[foo say];

return 0;

}

}

先編譯多個文件

xcrun clang -c Foo.m

xcrun clang -c SayHi.m

再將編譯後的文件鏈接起來,這樣就可以生成 a.out 可執行文件了。

xcrun clang SayHi.o Foo.o -Wl,`xcrun --show-sdk-path`/System/Library/Frameworks/Foundation.framework/Foundation

逆向 Mach-O 文件

需要先安裝 tweak,安裝越獄可以通過 cydia,不越獄直接打包成 ipa 安裝包。越獄的話會安裝一個 mobilesubstrate 的動態庫,使用 theos 開發工具,非越獄的直接把這個庫打包進 ipa 中或者直接修改彙編代碼。

Mobilesubstrate 提供了三個模塊來方便開發。

  • MobileHooker:利用 method swizzling 技術定義一些宏和函數來替換系統或者目標函數。
  • MobileLoader:在程序啟動時將我們寫的破解程序用的第三方庫注入進去。怎麼注入的呢,還記得先前說的 clang attribute 裡的一個 attribute((constructor)) 麼,它會在 main 執行之前執行,所以把我們的 hook 放在這裡就可以了。
  • Safe mode:類似安全模式,會禁用的改動。

先前提到 Mach-O 的結構有 Header,Load commands 和 Data,Mobileloader 會通過修改二進制的 loadCommands 來先把自己注入然後再把我們寫的第三方庫注入進去,這樣破解程序就會放在 Load commands 段裡面了。

當然如果是我們自己的程序我們是知道要替換哪些方法的,既然是逆向肯定是別人的程序了,這個時候就需要去先分析下我們想替換方法是哪個,網絡相關的分析可以用常用那些抓包工具,比如 Charles,WireShark 等,靜態的可以通過砸殼,反彙編,classdump 頭文件來分析 app 的架構,對應的常用工具dumpdecrypted,hopper disassembler 和 class_dump。運行時的分析可用工具有運行時控制檯cycript,遠程斷點調試lldb+debugserver,logify。

  • 這裡有個實例,講解如何通過逆向實現微信搶紅包的插件: 【Dev Club 分享第三期】iOS 黑客技術大揭秘 - DEV CLUB
  • 入門文章可以看看這篇: MyArticles/iOS冰與火之歌 at master · zhengmin1989/MyArticles · GitHub
  • 玩出新花樣: 黑科技:把第三方 iOS 應用轉成動態庫 - Jun’s Blog,作者另一篇文章: iOS符號表恢復&逆向支付寶 - Jun’s Blog

dyld動態鏈接

生成可執行文件後就是在啟動時進行動態鏈接了,進行符號和地址的綁定。首先會加載所依賴的 dylibs,修正地址偏移,因為 iOS 會用 ASLR 來做地址偏移避免攻擊,確定 Non-Lazy Pointer 地址進行符號地址綁定,加載所有類,最後執行 load 方法和 clang attribute 的 constructor 修飾函數。

用先前 Mach-O 章節的例子繼續分析,每個函數,全局變量和類都是通過符號的形式來定義和使用的,當把目標文件鏈接成一個執行文件時,鏈接器在目標文件和動態庫之間對符號做解析處理。

符號表會規定它們的符號,使用 nm 工具看看

xcrun nm -nm SayHi.o

(undefined) external _OBJC_CLASS_$_Foo

(undefined) external _objc_autoreleasePoolPop

(undefined) external _objc_autoreleasePoolPush

(undefined) external _objc_msgSend

0000000000000000 (__TEXT,__text) external _main

  • OBJC_CLASS$_Foo 表示 Foo 的 OC 符號。
  • (undefined) external 表示未實現非私有,如果是私有就是 non-external。
  • external _main 表示 main() 函數,處理 0 地址,將要到 __TEXT,__text section

再看看 Foo

xcrun nm -nm Foo.o

(undefined) external _NSLog

(undefined) external _OBJC_CLASS_$_NSObject

(undefined) external _OBJC_METACLASS_$_NSObject

(undefined) external ___CFConstantStringClassReference

(undefined) external __objc_empty_cache

0000000000000000 (__TEXT,__text) non-external -[Foo say]

0000000000000060 (__DATA,__objc_const) non-external l_OBJC_METACLASS_RO_$_Foo

00000000000000a8 (__DATA,__objc_const) non-external l_OBJC_$_INSTANCE_METHODS_Foo

00000000000000c8 (__DATA,__objc_const) non-external l_OBJC_CLASS_RO_$_Foo

0000000000000110 (__DATA,__objc_data) external _OBJC_METACLASS_$_Foo

0000000000000138 (__DATA,__objc_data) external _OBJC_CLASS_$_Foo

因為 undefined 符號表示該文件類未實現的,所以在目標文件和 Fundation framework 動態庫做鏈接處理時,鏈接器會嘗試解析所有的 undefined 符號。

鏈接器通過動態庫解析成符號會記錄是通過哪個動態庫解析的,路徑也會一起記錄。對比下 a.out 符號表看看是怎麼解析符號的。

xcrun nm -nm a.out

(undefined) external _NSLog (from Foundation)

(undefined) external _OBJC_CLASS_$_NSObject (from CoreFoundation)

(undefined) external _OBJC_METACLASS_$_NSObject (from CoreFoundation)

(undefined) external ___CFConstantStringClassReference (from CoreFoundation)

(undefined) external __objc_empty_cache (from libobjc)

(undefined) external _objc_autoreleasePoolPop (from libobjc)

(undefined) external _objc_autoreleasePoolPush (from libobjc)

(undefined) external _objc_msgSend (from libobjc)

(undefined) external dyld_stub_binder (from libSystem)

0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header

0000000100000e90 (__TEXT,__text) external _main

0000000100000f10 (__TEXT,__text) non-external -[Foo say]

0000000100001130 (__DATA,__objc_data) external _OBJC_METACLASS_$_Foo

0000000100001158 (__DATA,__objc_data) external _OBJC_CLASS_$_Foo

看看哪些 undefined 的符號,有了更多信息,可以知道在哪個動態庫能夠找到。

通過 otool 可以找到所需庫在哪

xcrun otool -L a.out

a.out:

/System/Library/Frameworks/Foundation.framework/Versions/C/Foundation (compatibility version 300.0.0, current version 1349.25.0)

/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1238.0.0)

/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 150.0.0, current version 1348.28.0)

/usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)

libSystem 裡有很多我們熟悉的lib

  • libdispatch:GCD
  • libsystem_c:C語言庫
  • libsystem_blocks:Block
  • libcommonCrypto:加密,比如md5

dylib 這種格式的表示是動態鏈接的,編譯的時候不會被編譯到執行文件中,在程序執行的時候才 link,這樣就不用算到包的大小裡,而且也能夠不更新執行程序就能夠更新庫。

打印什麼庫被加載了

(export DYLD_PRINT_LIBRARIES=; ./a.out )

dyld: loaded: /Users/didi/Downloads/./a.out

dyld: loaded: /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation

dyld: loaded: /usr/lib/libSystem.B.dylib

dyld: loaded: /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation

...

數數還挺多的,因為 Fundation 還會依賴一些其它的動態庫,其它的庫還會再依賴更多的庫,這樣相互依賴的符號會很多,需要處理的時間也會比較長,這裡系統上的動態鏈接器會使用共享緩存,共享緩存在 /var/db/dyld/。當加載 Mach-O 文件時動態鏈接器會先檢查共享內存是否有。每個進程都會在自己地址空間映射這些共享緩存,這樣可以優化啟動速度。

動態鏈接器的作用順序是怎麼樣的呢,可以先看看 Mike Ash 寫的這篇關於 dyld 的博客: Dynamic Linking On OS X

dyld 做了些什麼事

  • kernel 做啟動程序初始準備,開始由dyld負責。
  • 基於非常簡單的原始棧為 kernel 設置進程來啟動自身。
  • 使用共享緩存來處理遞歸依賴帶來的性能問題,ImageLoader 會讀取二進制文件,其中包含了我們的類,方法等各種符號。
  • 立即綁定 non-lazy 的符號並設置用於 lazy bind 的必要表,將這些庫 link 到執行文件裡。
  • 為可執行文件運行靜態初始化。
  • 設置參數到可執行文件的 main 函數並調用它。
  • 在執行期間,通過綁定符號處理對 lazily-bound 符號存根的調用提供 runtime 動態加載服務(通過 dl*() 這個 API ),併為gdb和其它調試器提供鉤子以獲得關鍵信息。runtime 會調用 map_images 做解析和處理,load_images 來調用 call_load_methods 方法遍歷所有加載了的 Class,按照繼承層級依次調用 +load 方法。
  • 在 mian 函數返回後運行 static terminator。
  • 在某些情況下,一旦 main 函數返回,就需要調用 libSystem 的 _exit。

查看運行時的調用 map_images 和 調用 +load 方法的相關 runtime 處理可以通過 RetVal 的可debug 的 objc/runtime RetVal/objc-runtime: objc runtime 706 來進行斷點查看調用的 runtime 方法具體實現。在 debug-objc 下創建一個類,在 +load 方法裡斷點查看走到這裡調用的堆棧如下:

0 +[someclass load]

1 call_class_loads()

2 ::call_load_methods

3 ::load_images(const char *path __unused, const struct mach_header *mh)

4 dyld::notifySingle(dyld_image_states, ImageLoader const*, ImageLoader::InitializerTimingList*)

11 _dyld_start

在 load_images 方法裡斷點 p path 可以打印出所有加載的動態鏈接庫,這個方法的 hasLoadMethods 用於快速判斷是否有 +load 方法。

prepare_load_methods 這個方法會獲取所有類的列表然後收集其中的 +load 方法,在代碼裡可以發現 Class 的 +load 是先執行的,然後執行 Category 的。為什麼這樣做,原因可以通過 prepare_load_methods 這個方法看出,在遍歷 Class 的 +load 方法時會執行 schedule_class_load 這個方法,這個方法會遞歸到根節點來滿足 Class 收集完整關係樹的需求。

最後 call_load_methods 會創建一個 autoreleasePool 使用函數指針來動態調用類和 Category 的 +load 方法。

如果想了解 Cocoa 的 Fundation 庫可以通過 GNUStep 源碼來學習。比如 NSNotificationCenter 發送通知是按什麼順序發送的可以查看 NSNotificationCenter.m 裡的 addObserver 方法和 postNotification 方法,看看觀察者是怎麼添加的和怎麼被遍歷通知到的。

dyld 是開源的: GitHub - opensource-apple/dyld

還可以看看蘋果的 WWDC 視頻 WWDC 2016 Session 406 裡講解對啟動進行優化。

這篇文章也不錯: Dynamic Linking of Imported Functions in Mach-O - CodeProject

附:安裝編譯 LLVM

多種獲取方式

  • 官網:http://releases.llvm.org/download.html
  • svn

先下載 LLVM

svn co http://llvm.org/svn/llvm-project/llvm/trunk llvm

在 LLVM 的 tools 目錄下下載 Clang

cd llvm/tools

svn co http://llvm.org/svn/llvm-project/cfe/trunk clang

在 LLVM 的 projects 目錄下下載 compiler-rt,libcxx,libcxxabi

cd ../projects

svn co http://llvm.org/svn/llvm-project/compiler-rt/trunk compiler-rt

svn co http://llvm.org/svn/llvm-project/libcxx/trunk libcxx

svn co http://llvm.org/svn/llvm-project/libcxxabi/trunk libcxxabi

在 Clang 的 tools 下安裝 extra 工具

cd ../tools/clang/tools

svn co http://llvm.org/svn/llvm-project/clang-tools-extra/trunk extra

  • git

git clone http://llvm.org/git/llvm.git

cd llvm/tools

git clone http://llvm.org/git/clang.git

cd ../projects

git clone http://llvm.org/git/compiler-rt.git

cd ../tools/clang/tools

git clone http://llvm.org/git/clang-tools-extra.git

安裝

brew install gcc

brew install cmake

mkdir build

cd build

cmake /path/to/llvm/source

cmake --build .

如果希望是 xcodeproject 方式 build 可以使用 -GXcode

mkdir xcodeBuild

cd xcodeBuild

cmake cmake -GXcode /path/to/llvm/source

接下來可以看看 LLVM 的官方教程如何實現一個自己的編程語Kaleidoscope。 LLVM Tutorial: Table of Contents — LLVM 5 documentation

這裡有個使用swift的實現的系列教程Building a Compiler in Swift with LLVM

附:其它編譯工具

js寫的C++解釋器JSCPP

適合學生學習時能夠方便的在瀏覽器裡直接編c++程序。項目地址:GitHub - felixhao28/JSCPP: A simple C++ interpreter written in JavaScript

資料網址

  • http://llvm.org
  • http://clang.llvm.org/
  • http://www.aosabook.org/en/llvm.html
  • GitHub - loarabia/Clang-tutorial: A collection of code samples showing usage of clang and llvm as a library
  • Using an external Xcode Clang Static Analyzer binary, with additional checks - Stack Overflow


分享到:


相關文章: