通过IR合并不同版本的Objective-C静态库

503 阅读11分钟

相关知识点

Objetive-C的运行时

objc的官方源码

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
}

image.png

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;     // 协议列表
}

image.png

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

image.png

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源码

LLVM安装教程

LLVM项目是模块化,可重用的编译器与及工具链技术的技术。

美国计算协会(ACM)将其2012年软件系统奖项颁给了LLVM,之前曾经获得此奖项的软件和技术包括:Java,Apache,Mosaic,the World Wide Web,Smalltalk,UNIX,Eclipse等等。

LLVM架构

  • Frontend:前端词法分析、语法分析、语义分析、生成中间代码
  • Optimizer:优化器中间代码优化
  • Backend:后端生成机器码

image.png

IR

LLVM IR(Intermediate Representation,中间表示)连接着编译器前端和编译器后端。IR的设计很大程度体现着LLVM插件化、模块化的设计哲学,LLVM的各种pass其实都是作用在LLVM IR上的。同时IR也是一个编译器组件接口。通常情况下,设计一门新的编程语言只需要完成能够生成LLVM IR的编译器前端即可,然后就可以轻松使用LLVM的各种编译优化、JIT支持、目标代码生成等功能。

IR有三种形式:

  • text:便于阅读的文本格式,类似于汇编语言,拓展名.ll
  • memory:内存格式
  • bitcode:二进制格式,拓展名.bc

image.png

IR的基本结构

  • Module(模块)是一份LLVM IR的顶层容器,对应于编译前端的每个翻译单元。(TranslationUnit)。每个模块由目标机器信息、全局符号(全局变量和函数)及元信息组成。
  • Function(函数)就是编程语言中的函数,包括函数签名和若干个基本块,函数内的第一个基本块叫做入口基本块。
  • BasicBlock(基本块)是一组顺序执行的指令集合,只有一个入口和一个出口,非头尾指令执行时不会违背顺序跳转到其他指令上去。每个基本块最后一条指令一般是跳转指令(跳转到其它基本块上去),函数内最后一个基本块的最后条指令是函数返回指令。
  • Instruction(指令)是LLVM IR中的最小可执行单位,每一条指令都单占一行。

image.png

例子

  • 代码
// 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
rostruct 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调整即可) image.png

控制变量

针对合并不同库代码的逻辑(如合并函数),这里增加了一个控制变量,用以切换激活不目标库的代码。定义如下

struct dd_control {
    uint32_t module_id;   // 自定静态库ID
    uint32_t index;       // 激活的静态库(默认为0)
};

Class的切换

Class在初始化之前,内存结构的数据和IR形式下基本一致(注:在静态库外定义的Category,编译过程中,会直接优化到Class的数据中)。另外,Class在初始化后,函数列表的数据还会进行排序操作。因此,在目标Class初始化前做切换,需要进行的操作较少。具体操作如下

  • 存储
  1. 保存新类的相关数据,具体数据如下
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;  // 目标类的实例协议起始地址
}
  1. 删除新类的objc_class数据,并从Class列表(section __objc_classlist)删除

  2. 创建dd_class_map_t的列表,存储所有数据项

  • 加载
  1. 从dd_class_map_t的列表,读取所有修改数据

  2. 从dd_class_map_t数据项读取目标类,并替换新类数据

image.png

Category的切换

Category根据目标类在静态库内外,分两种情况处理

静态库内的类

对于静态库内的类,在做数据处理时,将数据都添加到类上,和编译优化后的处理一致。代码的切换处理,依赖Class的切换操作。

静态库外的类

对于静态库外的类,则是将多个Category合并成一个,对于同名数据做如下处理,只需修改控制变量,无需额外的切换代码控制。

  • 同名属性

    遇到同名属性,只保留默认库的值,所以暂时不支持不同类型的属性定义切换。

  • 同名协议

    针对同名协议,采用合并协议数据的方法,遇到同名的数据,则保留默认库的值,所以暂时不支持含同名不同类型数据的协议切换。

  • 同名函数

    针对同名函数,采用合并函数的方案,步骤如下

    1. 修改原函数的函数名
    2. 创建一个原名的函数,根据控制变量,调用相应的原函数

    例子

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函数

  1. 清空__DATA,__objc_nlcatlist(section)的数据

  2. 创建类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
}
  1. 将类DDLoad添加到__DATA,__objc_nlcatlist(section)

init function

  1. 清空的入口数据 | | 内存 | IR | | --- | --- | --- | | 入口 | __DATA,__objc_init_func(section) | llvm.global_ctors(全局变量) |

  2. 创建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
}
  1. 将function函数添加到入口数据中

全局变量的切换

  1. 修改原变量的变量名

  2. 创建一个原名的全局变量,赋予默认库的变量值

  3. 创建一个切换函数,根据控制变量,更新全局变量的值(在启动时调用)

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

image.png

2. 预处理

  • 提取Class和Category的数据,并从原文件中删除

  • 提取函数的声明

将合并时需要用到的数据提取到同一个bc文件,方便操作。

image.png

3. 合并

  • 合并Class和Category(指向同个Class)数据

  • 将备选库的Class和Category数据转移到自定义数据中

  • 重命名Class相关的全局变量(Global Variable)和函数(Function)

  • 合并同名函数(Function),除Class相关的

  • 处理同名全局变量(Global Variable),主要指代码中的静态变量

image.png

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

image.png

待解决的问题

未覆盖的情况

  • Category不同参数类型的同名函数
  • Category不同类型的同名属性
  • 复杂的协议定义兼容

待扩展

  • 支持C/C++
  • 支持Swift