相关知识点
Objetive-C的运行时
Class的内存结构
Objective-C的Class在内存结构如下
struct objc_class {
Class isa;
Class superclass;
cache_t cache; // 函数缓存
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
}
Class初始化前的内存结构
在初始化前(第一次调用),bits指向一个class_ro_t的数据,这里存储了对象大小、函数列表、属性列表和协议列表等类信息。
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart; // 对象数据的内存偏移
uint32_t instanceSize; // 对象大小
const uint8_t * ivarLayout;
const char * name; // 类名
method_list_t * baseMethodList; // 函数列表
protocol_list_t * baseProtocols; // 属性列表
const ivar_list_t * ivars; // 成员变量列表
const uint8_t * weakIvarLayout;
property_list_t *baseProperties; // 协议列表
}
Class初始化后的内存结构
Class在调用前,需要调用realizeClassWithoutSwift进行初始化。
/***********************************************************************
* realizeClassWithoutSwift
* Performs first-time initialization on class cls,
* including allocating its read-write data.
* Does not perform any Swift-side initialization.
* Returns the real class structure for the class.
* Locking: runtimeLock must be write-locked by the caller
**********************************************************************/
static Class realizeClassWithoutSwift(Class cls, Class previously)
初始化后,Class的内存结构变化如下,bits指向一个class_rw_t的数据,负责存储函数、属性和协议列表,后续通过加载Category或动态添加的函数都会加载这里。
struct class_rw_t {
uint32_t flags;
uint16_t witness;
const class_ro_t *ro;
method_list_t * methods; // 函数列表
protocol_list_t * properties; // 属性列表
property_list_t * protocols; // 协议列表
Class firstSubclass;
Class nextSiblingClass;
}
Category的内存结构
Category在内存中的结构如下
struct category_t {
const char *name; // 类别名
Class cls; // 类
method_list_t *instanceMethods; // 实例函数列表
method_list_t *classMethods; // 类函数列表
protocol_list_t *protocols; // 协议列表
property_list_t *instanceProperties; // 实例属性列表
property_list_t *_classProperties;
};
根据目标Class是否初始化,Category的加载分成以下两种情况
- Class初始化前
Cateogry在被加载时,调用 objc::unattachedCategories.addForClass 存储起来,在Class初始化再加载。
- Class初始化后
Cateogry在被加载时,调用 attachCategories 将数据加载到Class的class_rw_t的数据中。
注:含load函数的Category
在调用load函数前,系统会自动初始化Class。
LLVM
LLVM项目是模块化,可重用的编译器与及工具链技术的技术。
美国计算协会(ACM)将其2012年软件系统奖项颁给了LLVM,之前曾经获得此奖项的软件和技术包括:Java,Apache,Mosaic,the World Wide Web,Smalltalk,UNIX,Eclipse等等。
LLVM架构
- Frontend:前端词法分析、语法分析、语义分析、生成中间代码
- Optimizer:优化器中间代码优化
- Backend:后端生成机器码
IR
LLVM IR(Intermediate Representation,中间表示)连接着编译器前端和编译器后端。IR的设计很大程度体现着LLVM插件化、模块化的设计哲学,LLVM的各种pass其实都是作用在LLVM IR上的。同时IR也是一个编译器组件接口。通常情况下,设计一门新的编程语言只需要完成能够生成LLVM IR的编译器前端即可,然后就可以轻松使用LLVM的各种编译优化、JIT支持、目标代码生成等功能。
IR有三种形式:
- text:便于阅读的文本格式,类似于汇编语言,拓展名.ll
- memory:内存格式
- bitcode:二进制格式,拓展名.bc
IR的基本结构
- Module(模块)是一份LLVM IR的顶层容器,对应于编译前端的每个翻译单元。(TranslationUnit)。每个模块由目标机器信息、全局符号(全局变量和函数)及元信息组成。
- Function(函数)就是编程语言中的函数,包括函数签名和若干个基本块,函数内的第一个基本块叫做入口基本块。
- BasicBlock(基本块)是一组顺序执行的指令集合,只有一个入口和一个出口,非头尾指令执行时不会违背顺序跳转到其他指令上去。每个基本块最后一条指令一般是跳转指令(跳转到其它基本块上去),函数内最后一个基本块的最后条指令是函数返回指令。
- Instruction(指令)是LLVM IR中的最小可执行单位,每一条指令都单占一行。
例子
- 代码
// DDAdder.m
#import <Foundation/Foundation.h>
@interface DDAdder: NSObject
- (NSInteger)addA:(NSInteger)a b:(NSInteger)b;
@end
@implementation DDAdder
- (NSInteger)addA:(NSInteger)a b:(NSInteger)b {
return a + b;
}
@end
- 编译命令
$ clang -S -emit-llvm DDAdder.m # DDAdder.ll
$ clang -c -emit-llvm DDAdder.m # DDAdder.bc
- 格式转换命令
$ llvm-dis DDAdder.bc # DDAdder.ll
$ llvm-as DDAdder.ll # DDAdder.bc
- DDAdder.ll文件
; ModuleID = 'DDAdder.m'
source_filename = "DDAdder.m"
target datalayout = "e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx12.0.0"
%0 = type opaque
%struct._objc_cache = type opaque
%struct._class_t = type { %struct._class_t*, %struct._class_t*, %struct._objc_cache*, i8* (i8*, i8*)**, %struct._class_ro_t* }
%struct._class_ro_t = type { i32, i32, i32, i8*, i8*, %struct.__method_list_t*, %struct._objc_protocol_list*, %struct._ivar_list_t*, i8*, %struct._prop_list_t* }
%struct.__method_list_t = type { i32, i32, [0 x %struct._objc_method] }
%struct._objc_method = type { i8*, i8*, i8* }
%struct._objc_protocol_list = type { i64, [0 x %struct._protocol_t*] }
%struct._protocol_t = type { i8*, i8*, %struct._objc_protocol_list*, %struct.__method_list_t*, %struct.__method_list_t*, %struct.__method_list_t*, %struct.__method_list_t*, %struct._prop_list_t*, i32, i32, i8**, i8*, %struct._prop_list_t* }
%struct._ivar_list_t = type { i32, i32, [0 x %struct._ivar_t] }
%struct._ivar_t = type { i64*, i8*, i8*, i32, i32 }
%struct._prop_list_t = type { i32, i32, [0 x %struct._prop_t] }
%struct._prop_t = type { i8*, i8* }
@_objc_empty_cache = external global %struct._objc_cache
@"OBJC_METACLASS_$_NSObject" = external global %struct._class_t
@OBJC_CLASS_NAME_ = private unnamed_addr constant [8 x i8] c"DDAdder\00", section "__TEXT,__objc_classname,cstring_literals", align 1
@"_OBJC_METACLASS_RO_$_DDAdder" = internal global %struct._class_ro_t { i32 1, i32 40, i32 40, i8* null, i8* getelementptr inbounds ([8 x i8], [8 x i8]* @OBJC_CLASS_NAME_, i32 0, i32 0), %struct.__method_list_t* null, %struct._objc_protocol_list* null, %struct._ivar_list_t* null, i8* null, %struct._prop_list_t* null }, section "__DATA, __objc_const", align 8
@"OBJC_METACLASS_$_DDAdder" = global %struct._class_t { %struct._class_t* @"OBJC_METACLASS_$_NSObject", %struct._class_t* @"OBJC_METACLASS_$_NSObject", %struct._objc_cache* @_objc_empty_cache, i8* (i8*, i8*)** null, %struct._class_ro_t* @"_OBJC_METACLASS_RO_$_DDAdder" }, section "__DATA, __objc_data", align 8
@"OBJC_CLASS_$_NSObject" = external global %struct._class_t
@OBJC_METH_VAR_NAME_ = private unnamed_addr constant [8 x i8] c"addA:b:\00", section "__TEXT,__objc_methname,cstring_literals", align 1
@OBJC_METH_VAR_TYPE_ = private unnamed_addr constant [14 x i8] c"q32@0:8q16q24\00", section "__TEXT,__objc_methtype,cstring_literals", align 1
@"_OBJC_$_INSTANCE_METHODS_DDAdder" = internal global { i32, i32, [1 x %struct._objc_method] } { i32 24, i32 1, [1 x %struct._objc_method] [%struct._objc_method { i8* getelementptr inbounds ([8 x i8], [8 x i8]* @OBJC_METH_VAR_NAME_, i32 0, i32 0), i8* getelementptr inbounds ([14 x i8], [14 x i8]* @OBJC_METH_VAR_TYPE_, i32 0, i32 0), i8* bitcast (i64 (%0*, i8*, i64, i64)* @"\01-[DDAdder addA:b:]" to i8*) }] }, section "__DATA, __objc_const", align 8
@"_OBJC_CLASS_RO_$_DDAdder" = internal global %struct._class_ro_t { i32 0, i32 8, i32 8, i8* null, i8* getelementptr inbounds ([8 x i8], [8 x i8]* @OBJC_CLASS_NAME_, i32 0, i32 0), %struct.__method_list_t* bitcast ({ i32, i32, [1 x %struct._objc_method] }* @"_OBJC_$_INSTANCE_METHODS_DDAdder" to %struct.__method_list_t*), %struct._objc_protocol_list* null, %struct._ivar_list_t* null, i8* null, %struct._prop_list_t* null }, section "__DATA, __objc_const", align 8
@"OBJC_CLASS_$_DDAdder" = global %struct._class_t { %struct._class_t* @"OBJC_METACLASS_$_DDAdder", %struct._class_t* @"OBJC_CLASS_$_NSObject", %struct._objc_cache* @_objc_empty_cache, i8* (i8*, i8*)** null, %struct._class_ro_t* @"_OBJC_CLASS_RO_$_DDAdder" }, section "__DATA, __objc_data", align 8
@"OBJC_LABEL_CLASS_$" = private global [1 x i8*] [i8* bitcast (%struct._class_t* @"OBJC_CLASS_$_DDAdder" to i8*)], section "__DATA,__objc_classlist,regular,no_dead_strip", align 8
@llvm.compiler.used = appending global [5 x i8*] [i8* getelementptr inbounds ([8 x i8], [8 x i8]* @OBJC_CLASS_NAME_, i32 0, i32 0), i8* getelementptr inbounds ([8 x i8], [8 x i8]* @OBJC_METH_VAR_NAME_, i32 0, i32 0), i8* getelementptr inbounds ([14 x i8], [14 x i8]* @OBJC_METH_VAR_TYPE_, i32 0, i32 0), i8* bitcast ({ i32, i32, [1 x %struct._objc_method] }* @"_OBJC_$_INSTANCE_METHODS_DDAdder" to i8*), i8* bitcast ([1 x i8*]* @"OBJC_LABEL_CLASS_$" to i8*)], section "llvm.metadata"
; Function Attrs: noinline optnone ssp uwtable
define internal i64 @"\01-[DDAdder addA:b:]"(%0* %0, i8* %1, i64 %2, i64 %3) #0 {
%5 = alloca %0*, align 8
%6 = alloca i8*, align 8
%7 = alloca i64, align 8
%8 = alloca i64, align 8
store %0* %0, %0** %5, align 8
store i8* %1, i8** %6, align 8
store i64 %2, i64* %7, align 8
store i64 %3, i64* %8, align 8
%9 = load i64, i64* %7, align 8
%10 = load i64, i64* %8, align 8
%11 = add nsw i64 %9, %10
ret i64 %11
}
attributes #0 = { noinline optnone ssp uwtable "disable-tail-calls"="false" "frame-pointer"="all" "less-precise-fpmad"="false" "min-legal-vector-width"="0" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+cx8,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "tune-cpu"="generic" "unsafe-fp-math"="false" "use-soft-float"="false" }
!llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7}
!llvm.ident = !{!8}
!0 = !{i32 2, !"SDK Version", [2 x i32] [i32 12, i32 0]}
!1 = !{i32 1, !"Objective-C Version", i32 2}
!2 = !{i32 1, !"Objective-C Image Info Version", i32 0}
!3 = !{i32 1, !"Objective-C Image Info Section", !"__DATA,__objc_imageinfo,regular,no_dead_strip"}
!4 = !{i32 1, !"Objective-C Garbage Collection", i8 0}
!5 = !{i32 1, !"Objective-C Class Properties", i32 64}
!6 = !{i32 1, !"wchar_size", i32 4}
!7 = !{i32 7, !"PIC Level", i32 2}
!8 = !{!"Apple clang version 13.0.0 (clang-1300.0.29.3)"}
内存结构和IR结构的映射
Class和Category在内存和IR格式中的结构定义,是一一对应,映射关系如下
Class
| 内存 | IR | |
|---|---|---|
| Objc类 | struct objc_class | %struct._class_t |
| 方法缓存 | struct cache_t | %struct._objc_cache |
| ro | struct class_ro_t | %struct._class_ro_t |
| 变量列表 | struct ivar_list_t | %struct._ivar_list_t |
| 变量 | struct ivar_t | %struct._ivar_t |
| 函数列表 | struct method_list_t | %struct.__method_list_t |
| 函数 | struct method_t | %struct._objc_method |
| 协议列表 | struct protocol_list_t | %struct._objc_protocol_list |
| 协议 | struct protocol_t | %struct._protocol_t |
| 属性列表 | struct property_list_t | %struct._prop_list_t |
| 属性 | struct property_t | %struct._prop_t |
Category
| 内存 | IR | |
|---|---|---|
| Objc类 | struct category_t | %struct._category_t |
| 函数列表 | struct method_list_t | %struct.__method_list_t |
| 函数 | struct method_t | %struct._objc_method |
| 协议列表 | struct protocol_list_t | %struct._objc_protocol_list |
| 协议 | struct protocol_t | %struct._protocol_t |
| 属性列表 | struct property_list_t | %struct._prop_list_t |
| 属性 | struct property_t | %struct._prop_t |
方案简介
第一个运行的代码
为了保证在业务代码运行逻辑前完成静态库代码逻辑的切换,需要保证切换代码是第一个运行的逻辑。
首先明确App启动时,函数代码的调用顺序
- 调用顺序: load函数 > init function函数 > main函数
- 主工程和库排序: 主工程的链接顺序 > 静态库的链接顺序
- 链接顺序: 文件排序
从函数的代码的调用顺序可以确定,只要满足以下条件,就能保证切换代码是第一个被调用
- 将切换代码添加在一个Objective-C类的load函数里
- 类文件添加在主工程的第一个文件的位置(在Build Phases调整即可)
控制变量
针对合并不同库代码的逻辑(如合并函数),这里增加了一个控制变量,用以切换激活不目标库的代码。定义如下
struct dd_control {
uint32_t module_id; // 自定静态库ID
uint32_t index; // 激活的静态库(默认为0)
};
Class的切换
Class在初始化之前,内存结构的数据和IR形式下基本一致(注:在静态库外定义的Category,编译过程中,会直接优化到Class的数据中)。另外,Class在初始化后,函数列表的数据还会进行排序操作。因此,在目标Class初始化前做切换,需要进行的操作较少。具体操作如下
- 存储
- 保存新类的相关数据,具体数据如下
struct dd_class_map_t {
uintptr_t *cls; // 切换的目标类
uintptr_t *super_cls; // 新类的父类
uintptr_t *ro; // 新类的ro数据
uintptr_t *meta_ro; // 新类的元类ro数据
uintptr_t *method_header; // 目标类的实例函数起始地址
uintptr_t *property_header; // 目标类的实例属性起始地址
uintptr_t *protocol_header; // 目标类的实例协议起始地址
uintptr_t *meta_method_header; // 目标类的类函数起始地址
uintptr_t *meta_property_header; // 目标类的实例属性起始地址
uintptr_t *meta_protocol_header; // 目标类的实例协议起始地址
}
-
删除新类的objc_class数据,并从Class列表(section __objc_classlist)删除
-
创建dd_class_map_t的列表,存储所有数据项
- 加载
-
从dd_class_map_t的列表,读取所有修改数据
-
从dd_class_map_t数据项读取目标类,并替换新类数据
Category的切换
Category根据目标类在静态库内外,分两种情况处理
静态库内的类
对于静态库内的类,在做数据处理时,将数据都添加到类上,和编译优化后的处理一致。代码的切换处理,依赖Class的切换操作。
静态库外的类
对于静态库外的类,则是将多个Category合并成一个,对于同名数据做如下处理,只需修改控制变量,无需额外的切换代码控制。
-
同名属性
遇到同名属性,只保留默认库的值,所以暂时不支持不同类型的属性定义切换。
-
同名协议
针对同名协议,采用合并协议数据的方法,遇到同名的数据,则保留默认库的值,所以暂时不支持含同名不同类型数据的协议切换。
-
同名函数
针对同名函数,采用合并函数的方案,步骤如下
- 修改原函数的函数名
- 创建一个原名的函数,根据控制变量,调用相应的原函数
例子
define internal void @"\01+[NSObject(Category) categoryStaticTest]"(i8* %0, i8* %1) {
%3 = load { i32, i32 }, { i32, i32 }* @"Control_$_dd_4", align 4 // 读取控制变量
%4 = extractvalue { i32, i32 } %3, 1
switch i32 %4, label %5 [
i32 0, label %6
i32 1, label %7
]
5: ; preds = %2
call void @"\01+[NSObject(3466) categoryStaticTest]"(i8* %0, i8* %1)
ret void
6: ; preds = %2 // 默认库
call void @"\01+[NSObject(3466) categoryStaticTest]"(i8* %0, i8* %1)
ret void
7: ; preds = %2 // 备选库
call void @"\01+[NSObject(B18892) categoryStaticTest]"(i8* %0, i8* %1)
ret void
}
load函数和init function的切换
load函数
-
清空__DATA,__objc_nlcatlist(section)的数据
-
创建类DDLoad,并添加load函数,调用原有的load函数,如下例子
define internal void @"+[DDLoad load]"(i8* %0, i8* %1) {
%3 = load { i32, i32 }, { i32, i32 }* @"Control_$_dd_4", align 4 // 读取控制变量
%4 = extractvalue { i32, i32 } %3, 1
switch i32 %4, label %5 [
i32 0, label %6
i32 1, label %13
]
5: ; preds = %2
ret void
6: ; preds = %2 // 默认库
%7 = load %struct._class_t*, %struct._class_t** @"OBJC_CLASSLIST_REFERENCES_$_.44", align 8
%8 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_.45, align 8
%9 = bitcast %struct._class_t* %7 to i8*
call void @"\01+[DDTestLoadA load]"(i8* %9, i8* %8)
%10 = load %struct._class_t*, %struct._class_t** @"OBJC_CLASSLIST_REFERENCES_$_", align 8
%11 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_.46, align 8
%12 = bitcast %struct._class_t* %10 to i8*
call void @"+[DDTestLoad(DD) load]"(i8* %12, i8* %11)
ret void
13: ; preds = %2 // 备选库
%14 = load %struct._class_t*, %struct._class_t** @"OBJC_CLASSLIST_REFERENCES_$_.47", align 8
%15 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_.48, align 8
%16 = bitcast %struct._class_t* %14 to i8*
call void @"\01+[DDTestLoadB load]"(i8* %16, i8* %15)
%17 = load %struct._class_t*, %struct._class_t** @"OBJC_CLASSLIST_REFERENCES_$_", align 8
%18 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_.49, align 8
%19 = bitcast %struct._class_t* %17 to i8*
call void @"+[DDTestLoad8892(DD) load]"(i8* %19, i8* %18)
ret void
}
- 将类DDLoad添加到__DATA,__objc_nlcatlist(section)
init function
-
清空的入口数据 | | 内存 | IR | | --- | --- | --- | | 入口 | __DATA,__objc_init_func(section) | llvm.global_ctors(全局变量) |
-
创建function函数,用于调用原有的init function,如下例子
define internal void @function() {
%1 = load { i32, i32 }, { i32, i32 }* @"Control_$_dd_4", align 4 // 读取控制变量
%2 = extractvalue { i32, i32 } %1, 1
switch i32 %2, label %3 [
i32 0, label %4
i32 1, label %5
]
3: ; preds = %0
ret void
4: ; preds = %0 // 默认库
call void @initFuncTestA()
call void @initFuncTest()
ret void
5: ; preds = %0 // 备选库
call void @initFuncTest()
call void @initFuncTestB1()
call void @initFuncTestB2()
ret void
}
- 将function函数添加到入口数据中
全局变量的切换
-
修改原变量的变量名
-
创建一个原名的全局变量,赋予默认库的变量值
-
创建一个切换函数,根据控制变量,更新全局变量的值(在启动时调用)
define internal void @dd_static_variable_function() {
%1 = load { i32, i32 }, { i32, i32 }* @"Control_$_dd_4", align 4 // 读取控制变量
%2 = extractvalue { i32, i32 } %1, 1
switch i32 %2, label %3 [
i32 0, label %4
i32 1, label %7
]
3: ; preds = %0
ret void
4: ; preds = %0 // 默认库
ret void
7: ; preds = %0 // 备选库
%8 = load %1*, %1** @DDTestManagerTestString34, align 8
%9 = bitcast %1* %8 to %0*
store %0* %9, %0** @DDTestManagerTestString, align 8
ret void
}
处理流程
1. 提取数据
- 提取静态库中的o文件,命令如下
tar -xf LibraryA.a -C tmp/ - 提取o文件的__bitcode中bitcode数据,命令如下
segedit libraryA.o -extract __LLVM __bitcode ObjectA.bc
如果目标静态库是多架构的库,还需要先提取其中一个架构的静态库,命令如下
lipo -thin arm64 LibraryA.a -output LibraryA_arm64.a
2. 预处理
-
提取Class和Category的数据,并从原文件中删除
-
提取函数的声明
将合并时需要用到的数据提取到同一个bc文件,方便操作。
3. 合并
-
合并Class和Category(指向同个Class)数据
-
将备选库的Class和Category数据转移到自定义数据中
-
重命名Class相关的全局变量(Global Variable)和函数(Function)
-
合并同名函数(Function),除Class相关的
-
处理同名全局变量(Global Variable),主要指代码中的静态变量
4. 打包数据
- 编译o文件,命令如下
xcrun clang -O1 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk -target arm64-apple-ios13.0.0 -fembed-bitcode -c newObjectA.bc -o newObjectA.o - 打包静态库,命令如下
ar -rcs Library.a tmp/*.o
待解决的问题
未覆盖的情况
- Category不同参数类型的同名函数
- Category不同类型的同名属性
- 复杂的协议定义兼容
待扩展
- 支持C/C++
- 支持Swift