前一篇介绍过isa
的优化方式以及从被优化过的isa
中获取真正的struct objc_class
指针。然而我们对知识的渴望,并不允许自己仅仅只是知道它、了解它而已,还想进一步分析struct objc_class
结构体,以及用它来做点什么。
接下来,从isa指针中到底有哪些信息
和获取到这些信息可以用来做什么
两个方面,来进一步揭开isa
的面纱。
isa指针中到底有哪些信息?
既然已经可以从一个实例对象中获取到isa
指针,那么我们就直接用这个指针来struct objc_class
中的数据。
要解析指针指向的地址中存放的数据,肯定需要知道struct objc_class
的定义(中间有很多方法直接忽略掉,只看其数据结构):
1 | struct objc_class : objc_object { |
因为指针占用的内存空间是8字节,所以通过看其结构体的构成,可以知道前16个字节里存放的是struct objc_class *
类型的指针ISA
和superclass
。
这两个指针,实际上存储的是类对象的父类以及元类的指针。父类和元类的解析,因为类型一样,所以和自身的解析是一样的。
第17个字节开始,存放的是cache_t
类型的数据,但是这个数据并没有使用指针,所以它里面的数据,会有序并字节对齐的存放在第17个字节开始的位置。可以通过cache_t
结构体来查看其数据类型:
1 | struct cache_t { |
struct cache_t
里面有一个指针,占8字节内存;有两个mask_t
类型(即uint32_t
类型)的数据,每个占4字节的内存,所以cache_t cache
一共占16个字节的内存。
这个字段里,存放的是方法缓存相关的信息。
最后一个结构体class_data_bits_t bits
:
1 | struct class_data_bits_t { |
里面就一个占8字节的数据。
从这个结构体的名字可以看出,类的主要信息都存储在这个字段里。这个字段里存储的是class_rw_t
类型的指针,但是也不是直接将指针的值存进去的,和被优化的isa一样被优化过的,所以不能直接取出来用。那么要怎么获取到这个指针呢?
在源码中往下看struct class_data_bits_t
这个结构体,会发现有一个data()
方法,返回一个class_rw_t*
:
1 | // data pointer |
所以其实我们也可以将获取的bits
的值,和0x00007ffffffffff8UL进行&
运算,得到class_rw_t*
指针。
写个简单的代码,可以走一下这个顺序,验证一下:
1 | int main(int argc, char * argv[]) { |
虽说是验证,但是取到这个值,也不知道是不是正确的,反正值是取到了!
要想验证取的值是否正确,还得看看class_rw_t *
里面的数据,是否是这个类的信息。
所以还是要跟进去看这个结构体:
1 |
|
还是按照刚才的思路,挨个字段去解析:
- flags
- version
- ro
const 修饰的class_ro_t *
指针,储了当前类在编译期就已经确定的属性、方法以及遵循的协议,不可修改。 - methods
实例方法列表(元类中存储的是类方法列表),是一个指向method_t
的二级指针。 - properties
属性列表。 - protocols
协议列表
这里主要关注ro
、methods
、properties
以及protocols
字段。
先看一下ro
的类型class_ro_t
的结构:刚刚的代码中,我们以及获取到了1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
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;
method_list_t *baseMethods() const {
return baseMethodList;
}
};class_rw_t
指针的值,我们可以通过读取class_rw_t
中ro
字段的值,然后获取class_ro_t
里面name
,来判断我们获取的指针是否正确:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
appDelegateClassName = NSStringFromClass([AppDelegate class]);
CustomClass *c = [CustomClass new];
c.i = 10;
c.obj = [NSObject new];
c.str = @"i love code";
NSLog(@"c:%p",c);
//1、通过实例对象`c`,获取 isa 指针
uintptr_t c_isa = (*(uintptr_t *)c) & 0x0000000FFFFFFFF8ULL;
//2、获取 `class_data_bits_t bits` 的值
uintptr_t c_bits = *(uintptr_t *)((unsigned long long)c_isa + 4*8);
//3、获取 `class_rw_t *`指针
uintptr_t rw_t = c_bits & 0x00007ffffffffff8UL;
//4、获取 `class_rw_t` 中 ro 指针
uintptr_t ro = *(uintptr_t *)((unsigned long long)rw_t+4+4);
//5、获取 `class_ro_t` 中的 `name`
const char *name = (const char *)(*(uintptr_t *)((unsigned long long)ro + 4 +4 + 4 +4 + 8));
/*这里下断点*/ return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}看log,可以看出,取出的值都是正确的。1
2
3
42019-11-25 00:00:30.956322+0800 IvarDemo[11902:3499502] c:0x1740261c0
(lldb) p/s name
(const char *) $0 = "CustomClass" "CustomClass"
(lldb)
既然name
可以取出,那么其他的字段,也是可以取出的,例如在8byte~12byte
存储的instanceSize
:1
2
3
4
5
6
//6、获取 `class_ro_t` 中的 `instanceSize`
uint32_t instanceSize = *(uint32_t *)((unsigned long long)ro + 4 +4);
/*
使用 class_getInstanceSize() 函数验证
*/其他字段就不一一获取。1
2
3
4
5
6
7(lldb) p/d instanceSize
(uint32_t) $0 = 32
Printing description of c:
<CustomClass: 0x17002f340>
(lldb) p/d (uint32_t)class_getInstanceSize([c class])
(uint32_t) $3 = 32
(lldb)
回过头去看看class_rw_t
中的methods
字段。
还是先看其定义:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class method_array_t :
public list_array_tt<method_t, method_list_t>
{
typedef list_array_tt<method_t, method_list_t> Super;
public:
method_list_t **beginCategoryMethodLists() {
return beginLists();
}
method_list_t **endCategoryMethodLists(Class cls);
method_array_t duplicate() {
return Super::duplicate<method_array_t>();
}
};method_array_t
是一个C++类,且其继承自模板类list_array_tt
,所以我们还得了解一下list_array_tt
这个模板类:从结构中可以看出,1
2
3
4
5
6
7
8
9
10template <typename Element, typename List>
class list_array_tt {
...
private:
union {
List* list;
uintptr_t arrayAndFlag;
};
...
}method_array_t
类只占8字节的内存空间,成员用union
联合,说明list
和arrayAndFlag
公用8字节的空间。
我们知道List
在类method_array_t
中是method_array_t
,但是既然定义成联合,那么肯定不是简单的将method_array_t
指针存入这8个字节的,所以现在问题就变成了:怎么获取真正的method_array_t
指针的值。
观察这个模板类,发现里面有一个List** beginLists()
方法:以上三个的函数告诉了咱们怎么通过1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16List** beginLists() {
if (hasArray()) {
return array()->lists;
} else {
return &list;
}
}
bool hasArray() const {
return arrayAndFlag & 1;
}
array_t *array() {
return (array_t *)(arrayAndFlag & ~1);
}arrayAndFlag
获取真正的List**
:
1、判断arrayAndFlag
最后一位是否为1;
2、通过判断结果,分别获取List**
; - 判断正确:
array()->lists
; - 否则:
&list
;
接下来,我们也可以书写代码,尝试着获取List**
:
1 | int main(int argc, char * argv[]) { |
虽然到这里,已经获取到了List **
(也就是method_list_t**
),但是我们要找的方法列表,还只是初见端倪,并没有完全浮出水面。所以我们还是要进一步分析一下method_list_t
:
1 | struct method_list_t : entsize_list_tt<method_t, method_list_t, 0x3> { |
1 | template <typename Element, typename List, uint32_t FlagMask> |
method_list_t
结构体继承自模板结构体entsize_list_tt
,所以method_list_t
的成员变量有3个:
1 | uint32_t entsizeAndFlags; |
我们要关注的,是count
和first
;count
中存储的是方法个数,而first
则是方法列表的其实地址,也就是方法数组的第一个元素。(至于第一个entsizeAndFlags
,存储的是数组中每个元素的大小。)
既然可以看到其内存布局,也就意味着我们可以获取到数据:
1 | int main(int argc, char * argv[]) { |
1 | method_name:setStr: method_types:v24@0:8@16 method_imp:0x1000a8120 |
到这里,就意味着struct class_rw_t
中的method_array_t methods
已经完全被解析出来了。
那么同样的,其他几个字段的内容,也是可以获取到的。(例如: property_array_t properties; protocol_array_t protocols
)。这里就不一一读取。
我们接下来的问题是:
获取到这些信息可以用来做什么?
最简单的,我们既然可以获取到类中每个方法的IMP,那么我们是否可以替换掉其指针,到达Hook的效果呢?
简单修改一下代码:
1 | NSString * sl_str(id self,SEL sel){ |
1 | method_name:setStr: method_types:v24@0:8@16 method_imp:0x1000880fc |
看到最后的hooked!!!
说明,hook成功了!
当然,完整的hook,并不是单纯的替换函数指针即可,还需要考虑很多问题,这里只是简单的测试一下可行性。