1. 什么是LLVM IR
当我们点击Xcode进行编译时,查看日志可以看到每一个编译单元都有指定大量的编译参数,我们跳过编译前的预处理和语法分析,使用 clang -emit-llvm XXX -S -o XXX.ll
直接导出查看其生成的IR(Intermediate Representation)。
也许你对于IR很陌生,但是Bitcode肯定会知道 。实际上,当我们设置了 Enable Bitcode=YES
,进行Archive时,Bitcode会被嵌入到链接后的Mach-O中,用于提交到App Store。实际上,Bitcode就是二进制格式的IR。
非Archive编译时,Enable Bitcode 将只增加一个编译参数 -fembed-bitcode-marker
, 该参数用于在Mach-O中作为占位。因为本地编译调试时并不需要bitcode,去掉这个步骤可以大大加快编译速度。
对于静态库等打开了Bitcode编译,通过MachOview查看会发现有一个__LLVM, __bitcode
段;而全工程编译出来对应的是 __LLVM, __bundle
段;可以使用 segedit
命令将指定的Section导出:
segedit XXX.o -extract __LLVM __bitcode result.bc
2. IR文件结构
如下,IR的结构可分为3部分。
1.Module
可以理解为一个类文件对应一个Module,作为一个独立的编译单元。其内部包含声明以及定义的函数,全局变量等,以及架构信息等。
2.Function
Function相当于C里面的方法,其必须存在于Module中,内部由参数,返回类型以及多个BasicBlock组成,每个Function的起始block是一个EntryBlock,也是列表的第一个BasicBlock。
3.Basic Block
BasicBlock则是Instruction存放的地方,Instruction对应的就是我们真正的可执行代码。Instruction可分为普通指令以及Terminator指令,并且BasicBlock都是以Terminator Instruction结尾,包括跳转,返回,异常等。
3. 语法格式
以下是一些基础的语法,可以帮助我们大致看懂一些简单的实现。
- 以@开头为全局标识符(函数,全局变量);以%开头为局部变量。
- %a = alloca i32, align 4 ,alloca相当于malloc,用于内存分配且自动释放;i32为占有几位,此为4个字节;align字节对齐。
- label 严格的讲它也是一种数据类型(type),但它可以标识入口,相当于代码标签。
- 函数的声明使用declare,函数的定义使用define。
- 数组类型用[count x ix]表示,其中count表示数组的大小,ix表示数组中每一个元素对应的数据类型,比如字符串”Hello IR”表示为[9 x i8],9表示该字符串包含9个元素(末尾包含一个\0),每个元素大小为i8即c语言中的char类型大小。
接着,我们可以通过 clang -emit-llvm XXX -S -o XXX.ll
导出一个OC类用Sublime或其他文本编辑器打开来看看更深入的结构。
- target datalayout: 该字符串指定如何在内存中布局数据,例如:
target datalayout = "e-m:o-p:32:32-Fi8-f64:32:64-v64:32:64-v128:32:128-a:0:32-n32-S32"
// e表示小端对齐
// m指定在输出中进行名字重整,以混乱的转义字符\01为前缀的符号将直接传递给汇编程序,而不包含转义字符。 m:o Mach-O mangling风格,私有符号添加L前缀,其他符号 _前缀
// p:32:32 32-bit的指针进行32bit对齐
// Fi8 指定函数指针的对齐方式,i表示函数指针的对齐与函数本身是独立的,8则函数指针的对齐方式是函数上指定的显式对齐方式的倍数,即8倍
// f64:32:64 double类型有32bits的ABI对齐但是优先64Bits对齐
// v64:32:64 64-bit vector同上
// v128:32:128 同上
// a:0:32 聚合类型(数组和结构体)32位对齐
// n32 指定目标CPU本地整数宽度为32bits
// S32 未指定的堆栈对齐为32bits
- Opaque Structure Types: 不透明结构类型用于表示没有指定主体的已命名结构类型。
%0 = type opaque
- Attribute groups:IR中对象引用的属性组。它们对于保持
.ll
文件可读性很重要,因为许多功能将使用同一组属性。在与.ll
单个.c
文件对应的文件的退化情况下 ,单个属性组将捕获用于构建该文件的重要命令行标志。
attributes #2 = { nounwind readnone speculatable willreturn }
attributes #3 = { nounwind }
- Module Flags Metadata: 整个模块的信息如果仅仅依靠IR是很难传递给LLVM的子系统的。llvm.module.flags 的元数据就是为了解决这个问题,这些标志以键/值对的形式出现,类似于字典,使得任何关心标志的子系统都可以很容易地进行查找。
// 三元组的第一个元素是行为标志,指定当多个模块合并在一起时的行为,并且元数据是相同的ID
1 表示Error,当两个值不同时发出错误,否则结果值为操作数
2 表示Warning,如果两个值不一致,则发出警告。结果值将是被链接的第一个模块的标志的操作数,或者如果其他模块使用max,则为max(在这种情况下,结果标志将是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个数组元素分别为14和0,行为则是如果出现两个以上的!"SDK Version"并且他们的值不相等,则抛出error
// !1的ID为!"Objective-C Version",值为2,当出现多个!"Objective-C Version"且值不同时则发出warning
!llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7, !8, !9, !10}
- DICompileUnit:表示一个编译单元,enums:,retainedTypes:,globals:,macros: 这些字段是一些内部包含与编译单元相关调试信息的元组,与代码优化无关(有些节点只有在指令引用它们时才会发出)。
!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节点表示文件
!12 = !DIFile(filename: "/Users/XXX/Desktop/TestSpeed/TestSpeed/AppDelegate.m", directory: "/Users/XXX/Desktop/bcTest/vm")
!13 = !{}
!14 = !{!15}
// DICompositeType 表示由其他类型组成的类型,如结构体,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节点表示导入到编译单元的实体
!24 = !DIImportedEntity(tag: DW_TAG_imported_declaration, scope: !11, entity: !22, file: !16, line: 9)
// 编译单元的描述符则由!llvm.dbg.cu收集,用于跟踪全局变量,类型信息 & 导入的实体(声明和namespace)
!llvm.dbg.cu = !{!11, !25, !27, !29}
- Automatic Linker Flags Named Metadata: 一些目标支持在单个对象文件中嵌入标记到链接器,通常,它与语言扩展一起使用,语言扩展允许源文件包含链接器命令行选项,并通过目标文件将这些选项自动传输到链接器。这些标志使用 !llvm.link .options 的命名元数据在IR中编码。每个操作数都应该是一个元数据节点,而元数据节点应该是其他元数据节点的列表,每个元数据节点应该是定义链接器选项的元数据字符串列表。
//如下,指定了几组linker options,链接iOS相关库
!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:表示来自源语言的函数,可以使用!dbg元数据将一个不同的DISubprogram附加到函数定义中,唯一的DISubprogram可以附加到用于call site调试信息的函数声明中。
!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节点表示文件
!17 = !DIFile(filename: "TestSpeed/TestSpeed/AppDelegate.m", directory: "/Users/XXX/Desktop")
//DISubroutineType节点表示子例程类型,types字段引用一个元组,第一个操作数为返回类型,其次依次为形参的类型,即!50。 如果第一个参数为null,则表示函数的返回值为void
!49 = !DISubroutineType(types: !50)
!50 = !{!51, !56, !58, !61, !64}
//DIDerivedType节点表示从其他类型(比如限定类型)派生的类型。DW_TAG_typedef用于为baseType提供一个名称
!51 = !DIDerivedType(tag: DW_TAG_typedef, name: "BOOL", scope: !53, file: !52, line: 81, baseType: !55)
//DIBasicType节点表示基本类型,比如int、bool和float。标签:默认为DW_TAG_base_type。
!55 = !DIBasicType(name: "signed char", size: 8, encoding: DW_ATE_signed_char)
- getelementptr: 用于获取聚合数据结构(数组或结构体)的子元素的地址。它只执行地址计算,不访问内存。该指令也可用于计算vector的地址。例如:
struct RT {
char A;
int B[10][20];
char C;
};
struct ST {
int X;
double Y;
struct RT Z;
};
///定义了RI ST结构体并在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:
//第一个参数i64 1指向struct.ST类型,即%struct.ST*结构体的一个指针
//第二个参数i32 2表示指向ST结构体的第2个元素,即RT
//第三个参数i32 1表示指向RT的第一个元素,array B[10][20]
//最后两个则就是取出数组的对应下标的值
%arrayidx = getelementptr inbounds %struct.ST, %struct.ST* %s, i64 1, i32 2, i32 1, i64 5, i64 13
ret i32* %arrayidx
}
//于是上面的arrayidx拆分下来等价于如下:第一步拿到struct.ST,然后取出ST位于index 2处的struct.RT,随后struct.RT的index 1处为int二维数组,最后对B[5][13]进行设置偏移
%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的消息发送为直接调用
我们知道在OC中方法调用都是通过runtime进行msgSend调用的,那么能否对一些编译期间已经确定了的调用规则改为直接调用的方式来避免被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 导出IR ------
; Function Attrs: nonlazybind //禁止函数的延迟符号绑定。这可能会更快地调用函数,但如果在程序启动期间没有调用函数,则会付出额外的程序启动时间。
//声明外部符号,#4对应上面第3点的属性组
declare i8* @objc_msgSend(i8*, i8*, ...) #4
//noinline:不内联调用 optnone:跳过optimization pass ssp: 开启堆栈保护
; Function Attrs: noinline optnone ssp
//名字重整,以转义字符\01为前缀
//%0则表示上2中的不透明结构类型,也是就msg_send的第一个参数,id self
//#1为类型组
//!dbg !80 将元数据!80使用!dbg附加到方法中,!80则是上面提到的DISubprogram对象
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是通过OBJC_METH_VAR_NAME_获取到方法的字符串
%7 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_, align 8, !invariant.load !9
//将%0*类型的%6 转换为i8*
%8 = bitcast %0* %6 to i8*
// i8* (i8*, i8*, ...) 的objc_msgSend方法, 转成void (i8*, i8*) 再进行传参调用
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*
// 转成 void (i8*, i8*, i32) 即增加一个入参
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*
// 转成 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
}
可以看到,调用OC方法,即内部都是通过objc_msgSend或其他几个衍生方法来实现的,i8* @objc_msgSend(i8*, i8*, ...)
这是一个带变参的C函数,第一个参数表示指向类实例的指针,第二个参数表示方法的选择子,其余则为可变参数列表。换言之,该函数通过向Objective-C运行时传递消息来间接调用,然后通过提供的入参来找到正确调用的真正函数。
尝试直接在IR中修改为直接call真正的调用方法:
//第1处的msgSend调用
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调用
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调用
%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)
修改完成执行 llc -filetype=obj TESTIR.ll
生成目标文件,然后通过gcc生成可执行文件,最终执行如下:
./a.out
a.out[4491:1939294] call TestTwo
a.out[4491:1939294] call TestThree param: 10
a.out[4491:1939294] 1
可以看到,此种直接调用的方案对于明确指定的方法调用是可行的。