1. 什么是LLVM IR
當(dāng)我們點(diǎn)擊Xcode進(jìn)行編譯時(shí),查看日志可以看到每一個(gè)編譯單元都有指定大量的編譯參數(shù),我們跳過(guò)編譯前的預(yù)處理和語(yǔ)法分析,使用 clang -emit-llvm XXX -S -o XXX.ll
直接導(dǎo)出查看其生成的IR(Intermediate Representation)。
也許你對(duì)于IR很陌生,但是Bitcode肯定會(huì)知道 。實(shí)際上,當(dāng)我們?cè)O(shè)置了 Enable Bitcode=YES
,進(jìn)行Archive時(shí),Bitcode會(huì)被嵌入到鏈接后的Mach-O中,用于提交到App Store。實(shí)際上,Bitcode就是二進(jìn)制格式的IR。
非Archive編譯時(shí),Enable Bitcode 將只增加一個(gè)編譯參數(shù) -fembed-bitcode-marker
, 該參數(shù)用于在Mach-O中作為占位。因?yàn)楸镜鼐幾g調(diào)試時(shí)并不需要bitcode,去掉這個(gè)步驟可以大大加快編譯速度。
對(duì)于靜態(tài)庫(kù)等打開了Bitcode編譯,通過(guò)MachOview查看會(huì)發(fā)現(xiàn)有一個(gè)__LLVM, __bitcode
段;而全工程編譯出來(lái)對(duì)應(yīng)的是 __LLVM, __bundle
段;可以使用 segedit
命令將指定的Section導(dǎo)出:
segedit XXX.o -extract __LLVM __bitcode result.bc
2. IR文件結(jié)構(gòu)
如下,IR的結(jié)構(gòu)可分為3部分。
1.Module
可以理解為一個(gè)類文件對(duì)應(yīng)一個(gè)Module,作為一個(gè)獨(dú)立的編譯單元。其內(nèi)部包含聲明以及定義的函數(shù),全局變量等,以及架構(gòu)信息等。
2.Function
Function相當(dāng)于C里面的方法,其必須存在于Module中,內(nèi)部由參數(shù),返回類型以及多個(gè)BasicBlock組成,每個(gè)Function的起始block是一個(gè)EntryBlock,也是列表的第一個(gè)BasicBlock。
3.Basic Block
BasicBlock則是Instruction存放的地方,Instruction對(duì)應(yīng)的就是我們真正的可執(zhí)行代碼。Instruction可分為普通指令以及Terminator指令,并且BasicBlock都是以Terminator Instruction結(jié)尾,包括跳轉(zhuǎn),返回,異常等。
3. 語(yǔ)法格式
以下是一些基礎(chǔ)的語(yǔ)法,可以幫助我們大致看懂一些簡(jiǎn)單的實(shí)現(xiàn)。
- 以@開頭為全局標(biāo)識(shí)符(函數(shù),全局變量);以%開頭為局部變量。
- %a = alloca i32, align 4 ,alloca相當(dāng)于malloc,用于內(nèi)存分配且自動(dòng)釋放;i32為占有幾位,此為4個(gè)字節(jié);align字節(jié)對(duì)齊。
- label 嚴(yán)格的講它也是一種數(shù)據(jù)類型(type),但它可以標(biāo)識(shí)入口,相當(dāng)于代碼標(biāo)簽。
- 函數(shù)的聲明使用declare,函數(shù)的定義使用define。
- 數(shù)組類型用[count x ix]表示,其中count表示數(shù)組的大小,ix表示數(shù)組中每一個(gè)元素對(duì)應(yīng)的數(shù)據(jù)類型,比如字符串”Hello IR”表示為[9 x i8],9表示該字符串包含9個(gè)元素(末尾包含一個(gè)\0),每個(gè)元素大小為i8即c語(yǔ)言中的char類型大小。
接著,我們可以通過(guò) clang -emit-llvm XXX -S -o XXX.ll
導(dǎo)出一個(gè)OC類用Sublime或其他文本編輯器打開來(lái)看看更深入的結(jié)構(gòu)。
- target datalayout: 該字符串指定如何在內(nèi)存中布局?jǐn)?shù)據(jù),例如:
target datalayout = "e-m:o-p:32:32-Fi8-f64:32:64-v64:32:64-v128:32:128-a:0:32-n32-S32"
// e表示小端對(duì)齊
// m指定在輸出中進(jìn)行名字重整,以混亂的轉(zhuǎn)義字符\01為前綴的符號(hào)將直接傳遞給匯編程序,而不包含轉(zhuǎn)義字符。 m:o Mach-O mangling風(fēng)格,私有符號(hào)添加L前綴,其他符號(hào) _前綴
// p:32:32 32-bit的指針進(jìn)行32bit對(duì)齊
// Fi8 指定函數(shù)指針的對(duì)齊方式,i表示函數(shù)指針的對(duì)齊與函數(shù)本身是獨(dú)立的,8則函數(shù)指針的對(duì)齊方式是函數(shù)上指定的顯式對(duì)齊方式的倍數(shù),即8倍
// f64:32:64 double類型有32bits的ABI對(duì)齊但是優(yōu)先64Bits對(duì)齊
// v64:32:64 64-bit vector同上
// v128:32:128 同上
// a:0:32 聚合類型(數(shù)組和結(jié)構(gòu)體)32位對(duì)齊
// n32 指定目標(biāo)CPU本地整數(shù)寬度為32bits
// S32 未指定的堆棧對(duì)齊為32bits
- Opaque Structure Types: 不透明結(jié)構(gòu)類型用于表示沒有指定主體的已命名結(jié)構(gòu)類型。
%0 = type opaque
- Attribute groups:IR中對(duì)象引用的屬性組。它們對(duì)于保持
.ll
文件可讀性很重要,因?yàn)樵S多功能將使用同一組屬性。在與.ll
單個(gè).c
文件對(duì)應(yīng)的文件的退化情況下 ,單個(gè)屬性組將捕獲用于構(gòu)建該文件的重要命令行標(biāo)志。
attributes #2 = { nounwind readnone speculatable willreturn }
attributes #3 = { nounwind }
- Module Flags Metadata: 整個(gè)模塊的信息如果僅僅依靠IR是很難傳遞給LLVM的子系統(tǒng)的。llvm.module.flags 的元數(shù)據(jù)就是為了解決這個(gè)問(wèn)題,這些標(biāo)志以鍵/值對(duì)的形式出現(xiàn),類似于字典,使得任何關(guān)心標(biāo)志的子系統(tǒng)都可以很容易地進(jìn)行查找。
// 三元組的第一個(gè)元素是行為標(biāo)志,指定當(dāng)多個(gè)模塊合并在一起時(shí)的行為,并且元數(shù)據(jù)是相同的ID
1 表示Error,當(dāng)兩個(gè)值不同時(shí)發(fā)出錯(cuò)誤,否則結(jié)果值為操作數(shù)
2 表示W(wǎng)arning,如果兩個(gè)值不一致,則發(fā)出警告。結(jié)果值將是被鏈接的第一個(gè)模塊的標(biāo)志的操作數(shù),或者如果其他模塊使用max,則為max(在這種情況下,結(jié)果標(biāo)志將是max)
。。。
!0 = !{i32 2, !"SDK Version", [2 x i32] [i32 14, i32 0]}
!1 = !{i32 1, !"Objective-C Version", i32 2}
// !0的ID為!"SDK Version",值為2個(gè)數(shù)組元素分別為14和0,行為則是如果出現(xiàn)兩個(gè)以上的!"SDK Version"并且他們的值不相等,則拋出error
// !1的ID為!"Objective-C Version",值為2,當(dāng)出現(xiàn)多個(gè)!"Objective-C Version"且值不同時(shí)則發(fā)出warning
!llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7, !8, !9, !10}
- DICompileUnit:表示一個(gè)編譯單元,enums:,retainedTypes:,globals:,macros: 這些字段是一些內(nèi)部包含與編譯單元相關(guān)調(diào)試信息的元組,與代碼優(yōu)化無(wú)關(guān)(有些節(jié)點(diǎn)只有在指令引用它們時(shí)才會(huì)發(fā)出)。
!11 = distinct !DICompileUnit(language: DW_LANG_ObjC, file: !12, producer: "Apple clang version 12.0.0 (clang-1200.0.32.2)", isOptimized: false, runtimeVersion: 2, emissionKind: FullDebug, enums: !13, retainedTypes: !14, imports: !23, nameTableKind: None, sysroot: "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.0.sdk", sdk: "iPhoneOS14.0.sdk")
// DIFile節(jié)點(diǎn)表示文件
!12 = !DIFile(filename: "/Users/XXX/Desktop/TestSpeed/TestSpeed/AppDelegate.m", directory: "/Users/XXX/Desktop/bcTest/vm")
!13 = !{}
!14 = !{!15}
// DICompositeType 表示由其他類型組成的類型,如結(jié)構(gòu)體,unions
!15 = !DICompositeType(tag: DW_TAG_structure_type, name: "AppDelegate", scope: !17, file: !16, line: 11, size: 32, flags: DIFlagObjcClassComplete, elements: !18, runtimeLang: DW_LANG_ObjC)
// Represents a module in the programming language, for example, a Clang module, or a Fortran module.
!22 = !DIModule(scope: null, name: "UIKit", configMacros: "\22-DNS_BLOCK_ASSERTIONS=1\22 \22-DOBJC_OLD_DISPATCH_PROTOTYPES=0\22", includePath: "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.0.sdk/System/Library/Frameworks/UIKit.framework")
!23 = !{!24}
// DIImportedEntity節(jié)點(diǎn)表示導(dǎo)入到編譯單元的實(shí)體
!24 = !DIImportedEntity(tag: DW_TAG_imported_declaration, scope: !11, entity: !22, file: !16, line: 9)
// 編譯單元的描述符則由!llvm.dbg.cu收集,用于跟蹤全局變量,類型信息 & 導(dǎo)入的實(shí)體(聲明和namespace)
!llvm.dbg.cu = !{!11, !25, !27, !29}
- Automatic Linker Flags Named Metadata: 一些目標(biāo)支持在單個(gè)對(duì)象文件中嵌入標(biāo)記到鏈接器,通常,它與語(yǔ)言擴(kuò)展一起使用,語(yǔ)言擴(kuò)展允許源文件包含鏈接器命令行選項(xiàng),并通過(guò)目標(biāo)文件將這些選項(xiàng)自動(dòng)傳輸?shù)芥溄悠鳌_@些標(biāo)志使用 !llvm.link .options 的命名元數(shù)據(jù)在IR中編碼。每個(gè)操作數(shù)都應(yīng)該是一個(gè)元數(shù)據(jù)節(jié)點(diǎn),而元數(shù)據(jù)節(jié)點(diǎn)應(yīng)該是其他元數(shù)據(jù)節(jié)點(diǎn)的列表,每個(gè)元數(shù)據(jù)節(jié)點(diǎn)應(yīng)該是定義鏈接器選項(xiàng)的元數(shù)據(jù)字符串列表。
//如下,指定了幾組linker options,鏈接iOS相關(guān)庫(kù)
!llvm.linker.options = !{!31, !32, !33, !34, !35, !36}
!31 = !{!"-framework", !"UIKit"}
!32 = !{!"-framework", !"FileProvider"}
!33 = !{!"-framework", !"UserNotifications"}
!34 = !{!"-framework", !"CoreText"}
!35 = !{!"-framework", !"QuartzCore"}
!36 = !{!"-framework", !"CoreImage"}
- DISubprogram:表示來(lái)自源語(yǔ)言的函數(shù),可以使用!dbg元數(shù)據(jù)將一個(gè)不同的DISubprogram附加到函數(shù)定義中,唯一的DISubprogram可以附加到用于call site調(diào)試信息的函數(shù)聲明中。
!48 = distinct !DISubprogram(name: "-[AppDelegate application:didFinishLaunchingWithOptions:]", scope: !17, file: !17, line: 19, type: !49, scopeLine: 19, flags: DIFlagPrototyped, spFlags: DISPFlagLocalToUnit | DISPFlagDefinition, unit: !11, retainedNodes: !13)
// DIFile節(jié)點(diǎn)表示文件
!17 = !DIFile(filename: "TestSpeed/TestSpeed/AppDelegate.m", directory: "/Users/XXX/Desktop")
//DISubroutineType節(jié)點(diǎn)表示子例程類型,types字段引用一個(gè)元組,第一個(gè)操作數(shù)為返回類型,其次依次為形參的類型,即!50。 如果第一個(gè)參數(shù)為null,則表示函數(shù)的返回值為void
!49 = !DISubroutineType(types: !50)
!50 = !{!51, !56, !58, !61, !64}
//DIDerivedType節(jié)點(diǎn)表示從其他類型(比如限定類型)派生的類型。DW_TAG_typedef用于為baseType提供一個(gè)名稱
!51 = !DIDerivedType(tag: DW_TAG_typedef, name: "BOOL", scope: !53, file: !52, line: 81, baseType: !55)
//DIBasicType節(jié)點(diǎn)表示基本類型,比如int、bool和float。標(biāo)簽:默認(rèn)為DW_TAG_base_type。
!55 = !DIBasicType(name: "signed char", size: 8, encoding: DW_ATE_signed_char)
- getelementptr: 用于獲取聚合數(shù)據(jù)結(jié)構(gòu)(數(shù)組或結(jié)構(gòu)體)的子元素的地址。它只執(zhí)行地址計(jì)算,不訪問(wèn)內(nèi)存。該指令也可用于計(jì)算vector的地址。例如:
struct RT {
char A;
int B[10][20];
char C;
};
struct ST {
int X;
double Y;
struct RT Z;
};
///定義了RI ST結(jié)構(gòu)體并在foo中使用
int *foo(struct ST *s) {
return &s[1].Z.B[5][13];
}
///在IR中表示
%struct.RT = type { i8, [10 x [20 x i32]], i8 }
%struct.ST = type { i32, double, %struct.RT }
define i32* @foo(%struct.ST* %s) nounwind uwtable readnone optsize ssp {
entry:
//第一個(gè)參數(shù)i64 1指向struct.ST類型,即%struct.ST*結(jié)構(gòu)體的一個(gè)指針
//第二個(gè)參數(shù)i32 2表示指向ST結(jié)構(gòu)體的第2個(gè)元素,即RT
//第三個(gè)參數(shù)i32 1表示指向RT的第一個(gè)元素,array B[10][20]
//最后兩個(gè)則就是取出數(shù)組的對(duì)應(yīng)下標(biāo)的值
%arrayidx = getelementptr inbounds %struct.ST, %struct.ST* %s, i64 1, i32 2, i32 1, i64 5, i64 13
ret i32* %arrayidx
}
//于是上面的arrayidx拆分下來(lái)等價(jià)于如下:第一步拿到struct.ST,然后取出ST位于index 2處的struct.RT,隨后struct.RT的index 1處為int二維數(shù)組,最后對(duì)B[5][13]進(jìn)行設(shè)置偏移
%t1 = getelementptr %struct.ST, %struct.ST* %s, i32 1
%t2 = getelementptr %struct.ST, %struct.ST* %t1, i32 0, i32 2
%t3 = getelementptr %struct.RT, %struct.RT* %t2, i32 0, i32 1
%t4 = getelementptr [10 x [20 x i32]], [10 x [20 x i32]]* %t3, i32 0, i32 5
%t5 = getelementptr [20 x i32], [20 x i32]* %t4, i32 0, i32 13
ret i32* %t5
4. 修改OC的消息發(fā)送為直接調(diào)用
我們知道在OC中方法調(diào)用都是通過(guò)runtime進(jìn)行msgSend調(diào)用的,那么能否對(duì)一些編譯期間已經(jīng)確定了的調(diào)用規(guī)則改為直接調(diào)用的方式來(lái)避免被hook呢?
//OC代碼如下:
- (void)runTestOne {
[self runTestTwo];
[self runTestThree:10];
int a = [self runTestFour];
NSLog(@"%d", a);
}
- (void)runTestTwo {
NSLog(@"call TestTwo");
}
- (void)runTestThree:(int)value {
NSLog(@"call TestThree %d", value);
}
- (int)runTestFour {
return 1;
}
//------ clang -S -fobjc-arc -emit-llvm TestIR.m -o TESTIR.ll 導(dǎo)出IR ------
; Function Attrs: nonlazybind //禁止函數(shù)的延遲符號(hào)綁定。這可能會(huì)更快地調(diào)用函數(shù),但如果在程序啟動(dòng)期間沒有調(diào)用函數(shù),則會(huì)付出額外的程序啟動(dòng)時(shí)間。
//聲明外部符號(hào),#4對(duì)應(yīng)上面第3點(diǎn)的屬性組
declare i8* @objc_msgSend(i8*, i8*, ...) #4
//noinline:不內(nèi)聯(lián)調(diào)用 optnone:跳過(guò)optimization pass ssp: 開啟堆棧保護(hù)
; Function Attrs: noinline optnone ssp
//名字重整,以轉(zhuǎn)義字符\01為前綴
//%0則表示上2中的不透明結(jié)構(gòu)類型,也是就msg_send的第一個(gè)參數(shù),id self
//#1為類型組
//!dbg !80 將元數(shù)據(jù)!80使用!dbg附加到方法中,!80則是上面提到的DISubprogram對(duì)象
define internal void @"\01-[TestIR runTestOne]"(%0* %0, i8* %1) #1 {
%3 = alloca %0*, align 8
%4 = alloca i8*, align 8
%5 = alloca i32, align 4
store %0* %0, %0** %3, align 8
store i8* %1, i8** %4, align 8
%6 = load %0*, %0** %3, align 8
// OBJC_SELECTOR_REFERENCES_.2 即為sel,sel是通過(guò)OBJC_METH_VAR_NAME_獲取到方法的字符串
%7 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_, align 8, !invariant.load !9
//將%0*類型的%6 轉(zhuǎn)換為i8*
%8 = bitcast %0* %6 to i8*
// i8* (i8*, i8*, ...) 的objc_msgSend方法, 轉(zhuǎn)成void (i8*, i8*) 再進(jìn)行傳參調(diào)用
call void bitcast (i8* (i8*, i8*, ...)* @objc_msgSend to void (i8*, i8*)*)(i8* %8, i8* %7)
%9 = load %0*, %0** %3, align 8
%10 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_.2, align 8, !invariant.load !9
%11 = bitcast %0* %9 to i8*
// 轉(zhuǎn)成 void (i8*, i8*, i32) 即增加一個(gè)入?yún)? call void bitcast (i8* (i8*, i8*, ...)* @objc_msgSend to void (i8*, i8*, i32)*)(i8* %11, i8* %10, i32 10)
%12 = load %0*, %0** %3, align 8
%13 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_.4, align 8, !invariant.load !9
%14 = bitcast %0* %12 to i8*
// 轉(zhuǎn)成 i32 (i8*, i8*) 返回值為i32
%15 = call i32 bitcast (i8* (i8*, i8*, ...)* @objc_msgSend to i32 (i8*, i8*)*)(i8* %14, i8* %13)
store i32 %15, i32* %5, align 4
%16 = load i32, i32* %5, align 4
notail call void (i8*, ...) @NSLog(i8* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_ to i8*), i32 %16)
ret void
}
可以看到,調(diào)用OC方法,即內(nèi)部都是通過(guò)objc_msgSend或其他幾個(gè)衍生方法來(lái)實(shí)現(xiàn)的,i8* @objc_msgSend(i8*, i8*, ...)
這是一個(gè)帶變參的C函數(shù),第一個(gè)參數(shù)表示指向類實(shí)例的指針,第二個(gè)參數(shù)表示方法的選擇子,其余則為可變參數(shù)列表。換言之,該函數(shù)通過(guò)向Objective-C運(yùn)行時(shí)傳遞消息來(lái)間接調(diào)用,然后通過(guò)提供的入?yún)?lái)找到正確調(diào)用的真正函數(shù)。
嘗試直接在IR中修改為直接call真正的調(diào)用方法:
//第1處的msgSend調(diào)用
call void bitcast (i8* (i8*, i8*, ...)* @objc_msgSend to void (i8*, i8*)*)(i8* %8, i8* %7)
//替換為:
call void @"\01-[TestIR runTestTwo]"(%0* %6, i8* %7)
//第2處的msgSend調(diào)用
call void bitcast (i8* (i8*, i8*, ...)* @objc_msgSend to void (i8*, i8*, i32)*)(i8* %11, i8* %10, i32 10)
//替換為:
call void @"\01-[TestIR runTestThree:]"(%0* %9, i8* %10, i32 10)
//第3處的msgSend調(diào)用
%15 = call i32 bitcast (i8* (i8*, i8*, ...)* @objc_msgSend to i32 (i8*, i8*)*)(i8* %14, i8* %13)
//替換為:
%15 = call i32 @"\01-[TestIR runTestFour]"(%0* %12, i8* %13)
修改完成執(zhí)行 llc -filetype=obj TESTIR.ll
生成目標(biāo)文件,然后通過(guò)gcc生成可執(zhí)行文件,最終執(zhí)行如下:
./a.out
a.out[4491:1939294] call TestTwo
a.out[4491:1939294] call TestThree param: 10
a.out[4491:1939294] 1
可以看到,此種直接調(diào)用的方案對(duì)于明確指定的方法調(diào)用是可行的。