深入剖析 iOS 編譯 Clang / LLVM

KelElledge 7年前發布 | 13K 次閱讀 LLVM iOS開發 移動開發

前言

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

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

編譯流程

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

#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; }</code></pre>

在命令行編譯

xcrun -sdk iphoneos clang -arch armv7 -F Foundation -fobjc-arc -c main.m -o main.o
xcrun -sdk iphoneos clang main.o -arch armv7 -fobjc-arc -framework Foundation -o main

在手機上就能夠直接執行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; }</code></pre>

這個過程的處理包括宏的替換,頭文件的導入,以及類似#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 是 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 編譯信息。

具體拿編譯 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 script 'Check Pods Manifest.lock'
Compile ... 各個項目中的.m文件
Link /Users/... 路徑
Copy ... 靜態文件
Compile asset catalogs
Compile Storyboard file ...
Process info.plist
Link Storyboards
Run custom shell script 'Embed Pods Frameworks'
Run custom shell script 'Copy Pods Resources'
...
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 /;</code></pre> 
  

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

/ Begin PBXProject section /
        3EE311301C4E1F0800103FA3 / Project object / = {
            isa = PBXProject;
            ...
/ End PBXProject section /</code></pre> 
  

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

targets = (
    3EE311371C4E1F0800103FA3 / GCDFetchFeed /,
    3EE311501C4E1F0800103FA3 / GCDFetchFeedTests /,
    3EE3115B1C4E1F0800103FA3 / GCDFetchFeedUITests /,
);</code></pre> 
  

順著這些 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";
};</code></pre> 
  

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

接下來看看 Clang 所做的事情。

Clang Static Analyzer靜態代碼分析

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

可以通過 clang --analyze -Xclang -analyzer-checker-help 來列出當前 clang 版本下所有 checker。如果想編寫自己的 checker,可以在 clang 項目的 StaticAnalyzer/Checkers 目錄下找到實例參考。這種方式能夠方便用戶擴展對代碼檢查規則或者對 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》這篇論文而來, 在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 sloc>> <invalid sloc> |-TypedefDecl 0xc75b740 <<invalid sloc>> <invalid sloc> implicit __builtin_va_list 'char *' -FunctionDecl 0xc75b7b0 &lt;test.cpp:1:1, line:7:1&gt; line:1:5 main 'int (void)'-CompoundStmt 0xc75b978 <line:2:1, line:7:1> |-DeclStmt 0xc75b870 <line:3:2, col:7> | -VarDecl 0xc75b840 &lt;col:2, col:6&gt; col:6 used a 'int' |-DeclStmt 0xc75b8d8 &lt;line:4:2, col:12&gt; |-VarDecl 0xc75b890 <col:2, col:10> col:6 used b 'int' cinit | `-IntegerLiteral 0xc75b8c0 <col:10> 'int' 10

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< a = b <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< |-BinaryOperator 0xc75b928 <line:5:2, col:6> 'int' lvalue '=' | |-DeclRefExpr 0xc75b8e8 <col:2> 'int' lvalue Var 0xc75b840 'a' 'int' | -ImplicitCastExpr 0xc75b918 &lt;col:6&gt; 'int' &lt;LValueToRValue&gt; |-DeclRefExpr 0xc75b900 <col:6> 'int' lvalue Var 0xc75b890 'b' 'int' <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

`-ReturnStmt 0xc75b968 <line:6:2, col:9>
  `-ImplicitCastExpr 0xc75b958 <col:9> 'int' <LValueToRValue>
    `-DeclRefExpr 0xc75b940 <col: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</code></pre>

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

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</code></pre>

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

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

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

  • (void)deprecatedMethod DEPRECATED_ATTRIBUTE; //也可以直接使用DEPRECATED_ATTRIBUTE這個系統定義的宏</code></pre>

    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(racexitBlock, LINE) attribute__((cleanup(rac_executeCleanupBlock), unused)) = ^

static inline void rac_executeCleanupBlock (__strong rac_cleanupBlock_t block) { (block)(); }</code></pre>

這樣可以在就可以很方便的把需要成對出現的代碼寫在一起了。同樣可以在 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;
    }
}</code></pre> 

可以看出 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 里面提供的方法對源文件進行語法分析,分析語法樹,遍歷語法數上每個節點。寫個 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 能夠完全控制語法樹,那么可以做的事情就非常多了,比如檢查命名是否規范,還能夠進行語言的轉換,比如把 OC 語言轉成JS或者 Swift 。可以用這個 tools 自己寫個工具去遍歷。

#include "clang/Driver/Options.h"
#include "clang/AST/AST.h"
#include "clang/AST/ASTContext.h"
#include "clang/AST/ASTConsumer.h"
#include "clang/AST/RecursiveASTVisitor.h"
#include "clang/Frontend/ASTConsumers.h"
#include "clang/Frontend/FrontendActions.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Tooling/CommonOptionsParser.h"
#include "clang/Tooling/Tooling.h"
#include "clang/Rewrite/Core/Rewriter.h"

using namespace std;
using namespace clang;
using namespace clang::driver;
using namespace clang::tooling;
using namespace llvm;

Rewriter rewriter;
int numFunctions = 0;

static llvm::cl::OptionCategory StatSampleCategory("Stat Sample");

class ExampleVisitor : public RecursiveASTVisitor<ExampleVisitor> {
private:
    ASTContext *astContext; // used for getting additional AST info

public:
    explicit ExampleVisitor(CompilerInstance *CI) 
      : astContext(&(CI->getASTContext())) // initialize private members
    {
        rewriter.setSourceMgr(astContext->getSourceManager(), astContext->getLangOpts());
    }

    virtual bool VisitFunctionDecl(FunctionDecl *func) {
        numFunctions++;
        string funcName = func->getNameInfo().getName().getAsString();
        if (funcName == "do_math") {
            rewriter.ReplaceText(func->getLocation(), funcName.length(), "add5");
            errs() << "** Rewrote function def: " << funcName << "
";
        }    
        return true;
    }

    virtual bool VisitStmt(Stmt *st) {
        if (ReturnStmt *ret = dyn_cast<ReturnStmt>(st)) {
            rewriter.ReplaceText(ret->getRetValue()->getLocStart(), 6, "val");
            errs() << "** Rewrote ReturnStmt
";
        }        
        if (CallExpr *call = dyn_cast<CallExpr>(st)) {
            rewriter.ReplaceText(call->getLocStart(), 7, "add5");
            errs() << "** Rewrote function call
";
        }
        return true;
    }
};

class ExampleASTConsumer : public ASTConsumer {
private:
    ExampleVisitor *visitor; // doesn't have to be private

public:
    // override the constructor in order to pass CI
    explicit ExampleASTConsumer(CompilerInstance *CI)
        : visitor(new ExampleVisitor(CI)) // initialize the visitor
    { }

    // override this to call our ExampleVisitor on the entire source file
    virtual void HandleTranslationUnit(ASTContext &Context) {
        visitor->TraverseDecl(Context.getTranslationUnitDecl());
    }
};

class ExampleFrontendAction : public ASTFrontendAction {
public:
    virtual std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef file) {
         return llvm::make_unique<ExampleASTConsumer>(&CI); // pass CI pointer to ASTConsumer
    }
};

int main(int argc, const char **argv) {
    // parse the command-line args passed to your code
    CommonOptionsParser op(argc, argv, StatSampleCategory);        
    // create a new Clang Tool instance (a LibTooling environment)
    ClangTool Tool(op.getCompilations(), op.getSourcePathList());

    // run the Clang Tool, creating a new FrontendAction (explained below)
    int result = Tool.run(newFrontendActionFactory<ExampleFrontendAction>().get());

    errs() << "
Found " << numFunctions << " functions.

";
    // print out the rewritten source code ("rewriter" is a global var.)
    rewriter.getEditBuffer(rewriter.getSourceMgr().getMainFileID()).write(errs());
    return result;
}

ClangPlugin

通過自己寫個插件,可以將這個插件添加到編譯的流程中,對編譯進行控制,可以在 LLVM 的這個目錄下查看一些范例 llvm/tools/clang/tools

孫源主導的動態化方案 DynamicCocoa 中就是使用了一個將 OC 源碼轉 JS 的插件來進行代碼的轉換。

滴滴的王康在做瘦身時也實現了一個自定義的 clang 插件

編譯后生成的二進制內容 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 文件的解析

解析前先看看可以描述該文件的結構體

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

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

根據這個結構體,需要先取出 magic,然后根據偏移量取出其它的信息。遍歷 ncmds 能夠獲得所有的 segment。cputype 包含了 CPU_TYPE_I386,CPU_TYPE_X86_64,CPU_TYPE_ARM,CPU_TYPE_ARM64 等多種 CPU 的類型。

dyld動態鏈接

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

附:安裝編譯 LLVM

多種獲取方式

svn co http://llvm.org/svn/llvm-project/llvm/trunk llvm
cd llvm/tools
svn co http://llvm.org/svn/llvm-project/cfe/trunk clang
cd ../projects
svn co http://llvm.org/svn/llvm-project/compiler-rt/trunk compiler-rt
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 cmake
mkdir build
cmake /path/to/llvm/source
cmake --build .

 

來自:http://www.starming.com/index.php?v=index&view=107

 

 本文由用戶 KelElledge 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!