日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 >

深入剖析 iOS 编译 Clang LLVM(编译流程)

發(fā)布時間:2025/5/22 59 豆豆
生活随笔 收集整理的這篇文章主要介紹了 深入剖析 iOS 编译 Clang LLVM(编译流程) 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

2019獨角獸企業(yè)重金招聘Python工程師標準>>>

前言

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

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

編譯流程

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

#import <Foundation/Foundation.h> #define DEFINEEight 8int 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; }

在命令行編譯

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

在手機上就能夠直接執(zhí)行main了。這樣還沒發(fā)看清clang的全部過程,可以通過-E查看clang在預處理處理這步做了什么。

clang -E main.m

執(zhí)行完后可以看到文件

# 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" 2int 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

然后是語法分析,驗證語法是否正確,然后將所有節(jié)點組成抽象語法樹 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 會去做些優(yōu)化工作,在 Xcode 的編譯設置里也可以設置優(yōu)化級別-01,-03,-0s,還可以寫些自己的 Pass。

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

如果開啟了 bitcode 蘋果會做進一步的優(yōu)化,有新的后端架構還是可以用這份優(yōu)化過的 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

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

clang main.o -o main 執(zhí)行 ./main 輸出 starming rank 14

下面是完整步驟:

  • 編譯信息寫入輔助文件,創(chuàng)建文件架構 .app 文件
  • 處理文件打包信息
  • 執(zhí)行 CocoaPod 編譯前腳本,checkPods Manifest.lock
  • 編譯.m文件,使用 CompileC 和 clang 命令
  • 鏈接需要的 Framework
  • 編譯 xib
  • 拷貝 xib ,資源文件
  • 編譯 ImageAssets
  • 處理 info.plist
  • 執(zhí)行 CocoaPod 腳本
  • 拷貝標準庫
  • 創(chuàng)建 .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/Podsexport LANG=en_US.US-ASCIIexport 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 命令參數(shù)

-x 編譯語言比如objective-c -arch 編譯的架構,比如arm7 -f 以-f開頭的。 -W 以-W開頭的,可以通過這些定制編譯警告 -D 以-D開頭的,指的是預編譯宏,通過這些宏可以實現(xiàn)條件編譯 -iPhoneSimulator10.1.sdk 編譯采用的iOS SDK版本 -I 把編譯信息寫入指定的輔助文件 -F 需要的Framework -c 標識符指明需要運行預處理器,語法分析,類型檢查,LLVM生成優(yōu)化以及匯編代碼生成.o文件 -o 編譯結果

構建 Target

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

Create product structure Process product packaging Run custom shell script 'Check Pods Manifest.lock' Compile ... 各個項目中的.m文件 Link /Users/... 路徑 Copy ... 靜態(tài)文件 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

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

Target 在 Build 過程的控制

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

Build Phases

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

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

build phase 還會把靜態(tài)資源拷貝到 bundle 里。

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

Build Rules

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

Build Settings

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

pbxproj工程文件

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

rootObject = 3EE311301C4E1F0800103FA3 /* Project object */;

然后根據(jù)這個 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靜態(tài)代碼分析

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

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

可以通過 clang --analyze -Xclang -analyzer-checker-help 來列出當前 clang 版本下所有 checker。如果想編寫自己的 checker,可以在 clang 項目的 StaticAnalyzer/Checkers 目錄下找到實例參考。這種方式能夠方便用戶擴展對代碼檢查規(guī)則或者對 bug 類型進行擴展,但是這種架構也有不足,每執(zhí)行完一條語句后,分析引擎會遍歷所有 checker 中的回調函數(shù),所以 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 有所了解,包括迭代數(shù)據(jù)流分析,path-sensitive,path-insensitive ,flow-sensitive等。

編譯的概念(詞法->語法->語義->IR->優(yōu)化->CodeGen)在 clang static analyzer 里到處可見,例如 Relaxed Live Variables Analysis 可以減少分析中的內存消耗,使用 mark-sweep 實現(xiàn) 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里的具體實現(xiàn)代碼可以查看這兩個文件?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 <test.cpp:1:1, line:7:1> line:1:5 main 'int (void)'`-CompoundStmt 0xc75b978 <line:2:1, line:7:1>|-DeclStmt 0xc75b870 <line:3:2, col:7>| `-VarDecl 0xc75b840 <col:2, col:6> col:6 used a 'int'|-DeclStmt 0xc75b8d8 <line:4:2, col:12>| `-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 <col:6> 'int' <LValueToRValue>| `-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: 103: int b = 10;4: b5: [B1.4] (ImplicitCastExpr, LValueToRValue, int)6: a7: [B1.6] = [B1.5]8: a9: [B1.8] (ImplicitCastExpr, LValueToRValue, int)10: return [B1.9];Preds (1): B2Succs (1): B0[B0 (EXIT)]Preds (1): B1

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

Clang Attributes

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

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))) 版本棄用提示

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

- (void)preMethod:( NSString *)string __attribute__((deprecated("preMethod已經被棄用,請使用newMethod"))); - (void)deprecatedMethod DEPRECATED_ATTRIBUTE; //也可以直接使用DEPRECATED_ATTRIBUTE這個系統(tǒng)定義的宏

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

os 指系統(tǒng)的版本,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(…))) 作用域結束時自動執(zhí)行一個指定方法

作用域結束包括大括號結束,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)(); }

這樣可以在就可以很方便的把需要成對出現(xiàn)的代碼寫在一起了。同樣可以在 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 的函數(shù)上實現(xiàn)方法重載。即同樣的函數(shù)名函數(shù)能夠對不同參數(shù)在編譯時能夠自動根據(jù)參數(shù)來選擇定義的函數(shù)

__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)) 指定內部實現(xiàn)的初始化方法

  • 如果是 objc_designated_initializer 初始化的方法必須調用覆蓋實現(xiàn) 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,否則會出現(xiàn)編譯警告,這個可以定義一些必要執(zhí)行的方法在 super 里提醒使用者這個方法的內容時必要的。

attribute((const)) 重復調用相同數(shù)值參數(shù)優(yōu)化返回

用于數(shù)值類型參數(shù)的函數(shù),多次調用相同的數(shù)值型參數(shù),返回是相同的,只在第一次是需要進行運算,后面只返回第一次的結果,這時編譯器的一種優(yōu)化處理方式。

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

PRIORITY 是指執(zhí)行的優(yōu)先級,main 函數(shù)執(zhí)行之前會執(zhí)行 constructor,main 函數(shù)執(zhí)行后會執(zhí)行 destructor,+load 會比 constructor 執(zhí)行的更早點,因為動態(tài)鏈接器加載 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 的方法會被廢棄的警告,這個加上這個方法當然是為了兼容老系統(tǒng),加上 ignored “-Wdeprecated-declarations” 的作用是忽略這個警告。通過 clang diagnostic push/pop 可以靈活的控制代碼塊的編譯選項。

使用 libclang 來進行語法分析

使用 libclang 里面提供的方法對源文件進行語法分析,分析語法樹,遍歷語法數(shù)上每個節(jié)點。寫個 python 腳本來調用 clang

pip install clang#!/usr/bin/python # vim: set fileencoding=utf-8import clang.cindex import asciitree import sysdef 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.displaynamekind = 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 能夠完全控制語法樹,那么可以做的事情就非常多了,比如檢查命名是否規(guī)范,還能夠進行語言的轉換,比如把 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 infopublic: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 << "\n";} return true;}virtual bool VisitStmt(Stmt *st) {if (ReturnStmt *ret = dyn_cast<ReturnStmt>(st)) {rewriter.ReplaceText(ret->getRetValue()->getLocStart(), 6, "val");errs() << "** Rewrote ReturnStmt\n";} if (CallExpr *call = dyn_cast<CallExpr>(st)) {rewriter.ReplaceText(call->getLocStart(), 7, "add5");errs() << "** Rewrote function call\n";}return true;} };class ExampleASTConsumer : public ASTConsumer { private:ExampleVisitor *visitor; // doesn't have to be privatepublic:// override the constructor in order to pass CIexplicit ExampleASTConsumer(CompilerInstance *CI): visitor(new ExampleVisitor(CI)) // initialize the visitor{ }// override this to call our ExampleVisitor on the entire source filevirtual 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 codeCommonOptionsParser 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() << "\nFound " << numFunctions << " functions.\n\n";// 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

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

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

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

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

Object files

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

Sections

這里描述的是每個 Section 在可執(zhí)行文件中的位置和大小。每個 Section 的 Segment 的類型分為 __TEXT 代碼段和 __DATA 數(shù)據(jù)段兩種。

Symbols

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

每次編譯后生成的 dSYM 文件

在每次編譯后都會生成一個 dSYM 文件,程序在執(zhí)行中通過地址來調用方法函數(shù),而 dSYM 文件里存儲了函數(shù)地址映射,這樣調用棧里的地址可以通過 dSYM 這個映射表能夠獲得具體函數(shù)的位置。一般都會用來處理 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 文件

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

Mach-O 文件包含三個區(qū)域

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

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

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

dyld動態(tài)鏈接

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

附:安裝編譯 LLVM

多種獲取方式

  • 官網:http://releases.llvm.org/download.html
  • svn
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 .

?

原文:https://github.com/ming1016/study/wiki/%E6%B7%B1%E5%85%A5%E5%89%96%E6%9E%90-iOS-%E7%BC%96%E8%AF%91-Clang---LLVM#%E7%BC%96%E8%AF%91%E6%B5%81%E7%A8%8B

?

轉載于:https://my.oschina.net/u/2345393/blog/820141

總結

以上是生活随笔為你收集整理的深入剖析 iOS 编译 Clang LLVM(编译流程)的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。