注册
iOS

isa(二)


前一篇介绍过isa的优化方式以及从被优化过的isa中获取真正的struct objc_class指针。然而我们对知识的渴望,并不允许自己仅仅只是知道它、了解它而已,还想进一步分析struct objc_class结构体,以及用它来做点什么。
接下来,从isa指针中到底有哪些信息获取到这些信息可以用来做什么两个方面,来进一步揭开isa的面纱。

isa指针中到底有哪些信息?

既然已经可以从一个实例对象中获取到isa指针,那么我们就直接用这个指针来struct objc_class中的数据。
要解析指针指向的地址中存放的数据,肯定需要知道struct objc_class的定义(中间有很多方法直接忽略掉,只看其数据结构):

struct objc_class : objc_object {
  // Class ISA;
  Class superclass;
  cache_t cache;             // formerly cache pointer and vtable
  class_data_bits_t bits;   // class_rw_t * plus custom rr/alloc flags
...
}

因为指针占用的内存空间是8字节,所以通过看其结构体的构成,可以知道前16个字节里存放的是struct objc_class *类型的指针ISAsuperclass
这两个指针,实际上存储的是类对象的父类以及元类的指针。父类和元类的解析,因为类型一样,所以和自身的解析是一样的。
第17个字节开始,存放的是cache_t类型的数据,但是这个数据并没有使用指针,所以它里面的数据,会有序并字节对齐的存放在第17个字节开始的位置。可以通过cache_t结构体来查看其数据类型:

struct cache_t {
  struct bucket_t *_buckets;
  mask_t _mask;
  mask_t _occupied;
}

#if __LP64__
typedef uint32_t mask_t; // x86_64 & arm64 asm are less efficient with 16-bits

struct cache_t里面有一个指针,占8字节内存;有两个mask_t类型(即uint32_t类型)的数据,每个占4字节的内存,所以cache_t cache一共占16个字节的内存。
这个字段里,存放的是方法缓存相关的信息。
最后一个结构体class_data_bits_t bits

struct class_data_bits_t {
  // Values are the FAST_ flags above.
  uintptr_t bits;
}

里面就一个占8字节的数据。
从这个结构体的名字可以看出,类的主要信息都存储在这个字段里。这个字段里存储的是class_rw_t类型的指针,但是也不是直接将指针的值存进去的,和被优化的isa一样被优化过的,所以不能直接取出来用。那么要怎么获取到这个指针呢?
在源码中往下看struct class_data_bits_t这个结构体,会发现有一个data()方法,返回一个class_rw_t*

// data pointer
#define FAST_DATA_MASK         0x00007ffffffffff8UL

class_rw_t* data() {
      return (class_rw_t *)(bits & FAST_DATA_MASK);
  }

所以其实我们也可以将获取的bits的值,和0x00007ffffffffff8UL进行&运算,得到class_rw_t*指针。
写个简单的代码,可以走一下这个顺序,验证一下:

int 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;
   
   
  return UIApplicationMain(argc, argv, nil, appDelegateClassName);
   
}

虽说是验证,但是取到这个值,也不知道是不是正确的,反正值是取到了!
要想验证取的值是否正确,还得看看class_rw_t *里面的数据,是否是这个类的信息。
所以还是要跟进去看这个结构体:


struct class_rw_t {
  // Be warned that Symbolication knows the layout of this structure.
  uint32_t flags;
  uint32_t version;

  const class_ro_t *ro;

  method_array_t methods;
  property_array_t properties;
  protocol_array_t protocols;

  Class firstSubclass;
  Class nextSiblingClass;

  char *demangledName;

还是按照刚才的思路,挨个字段去解析:

  • flags

  • version

  • ro
    const 修饰的 class_ro_t * 指针,储了当前类在编译期就已经确定的属性、方法以及遵循的协议,不可修改。

  • methods
    实例方法列表(元类中存储的是类方法列表),是一个指向method_t的二级指针。

  • properties
    属性列表。

  • protocols
    协议列表
    这里主要关注romethodsproperties以及protocols字段。
    先看一下ro的类型class_ro_t的结构:


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_tro字段的值,然后获取class_ro_t里面name,来判断我们获取的指针是否正确:

int 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);    
   
}
2019-11-25 00:00:30.956322+0800 IvarDemo[11902:3499502] c:0x1740261c0
(lldb) p/s name
(const char *) $0 = "CustomClass" "CustomClass"
(lldb)

看log,可以看出,取出的值都是正确的。
既然name可以取出,那么其他的字段,也是可以取出的,例如在 8byte~12byte 存储的instanceSize

    
//6、获取 `class_ro_t` 中的 `instanceSize`
uint32_t instanceSize = *(uint32_t *)((unsigned long long)ro + 4 +4);
/*
使用 class_getInstanceSize() 函数验证
*/
(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字段。
还是先看其定义:

class 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这个模板类:

template <typename Element, typename List>
class list_array_tt {
...
private:
union {
List* list;
uintptr_t arrayAndFlag;
};
...
}

从结构中可以看出,method_array_t类只占8字节的内存空间,成员用union联合,说明listarrayAndFlag公用8字节的空间。
我们知道List在类method_array_t中是method_array_t,但是既然定义成联合,那么肯定不是简单的将method_array_t指针存入这8个字节的,所以现在问题就变成了:怎么获取真正的method_array_t指针的值。
观察这个模板类,发现里面有一个List** beginLists()方法:

List** 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**:

int 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));

//6、获取 `class_ro_t` 中的 `instanceSize`
uint32_t instanceSize = *(uint32_t *)((unsigned long long)ro + 4 +4);

//7、获取 `class_rw_t` 中 `method`

uintptr_t method = *((uintptr_t *)(((unsigned long long)rw_t)+4+4+8));

/*注:因为`method`成员没有用指针修饰,所以`method`的值即为
union
{List *list;
uintptr_t arrayAndFlag;

}
的值。
*/
//8、获取 `arrayAndFlag`的值
uintptr_t arrayAndFlag = method;

//9、//获取 `List **`
uintptr_t *list;
//判断 `arrayAndFlag` 标志位
if (arrayAndFlag & 1) {
//获取 `array_t *`
uintptr_t array_t = arrayAndFlag & ~1;
list = &(*((uintptr_t *)((unsigned long long)array_t+8)));
}else {
//获取 `List **`
list = &arrayAndFlag;
}

/*这里下断点*/ return UIApplicationMain(argc, argv, nil, appDelegateClassName);

}

虽然到这里,已经获取到了List **(也就是method_list_t**),但是我们要找的方法列表,还只是初见端倪,并没有完全浮出水面。所以我们还是要进一步分析一下method_list_t

struct method_list_t : entsize_list_tt<method_t, method_list_t, 0x3> {
bool isFixedUp() const;
void setFixedUp();

uint32_t indexOfMethod(const method_t *meth) const {
uint32_t i =
(uint32_t)(((uintptr_t)meth - (uintptr_t)this) / entsize());
assert(i < count);
return i;
}
};
template <typename Element, typename List, uint32_t FlagMask>
struct entsize_list_tt {
uint32_t entsizeAndFlags;
uint32_t count;
Element first;
...
}

method_list_t结构体继承自模板结构体entsize_list_tt,所以method_list_t的成员变量有3个:

    uint32_t entsizeAndFlags;
uint32_t count;
Element first;

我们要关注的,是countfirstcount中存储的是方法个数,而first则是方法列表的其实地址,也就是方法数组的第一个元素。(至于第一个entsizeAndFlags,存储的是数组中每个元素的大小。)
既然可以看到其内存布局,也就意味着我们可以获取到数据:

int main(int argc, char * argv[]) {
NSString * appDelegateClassName;

appDelegateClassName = NSStringFromClass([AppDelegate class]);

CustomClass *c = [CustomClass new];
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));

//6、获取 `class_ro_t` 中的 `instanceSize`
uint32_t instanceSize = *(uint32_t *)((unsigned long long)ro + 4 +4);

//7、获取 `class_rw_t` 中 `method`

uintptr_t method = *((uintptr_t *)(((unsigned long long)rw_t)+4+4+8));

/*注:因为`method`成员没有用指针修饰,所以`method`的值即为
union
{List *list;
uintptr_t arrayAndFlag;

}
的值。
*/
//8、获取 `arrayAndFlag`的值
uintptr_t arrayAndFlag = method;

//9、//获取 `List **`
uintptr_t *list;
//判断 `arrayAndFlag` 标志位
if (arrayAndFlag & 1) {
//获取 `array_t *`
uintptr_t array_t = arrayAndFlag & ~1;
list = &(*((uintptr_t *)((unsigned long long)array_t+8)));
}else {
//获取 `List **`
list = &arrayAndFlag;
}

//10、获取`method_list_t` 中的 `count`
//获取`method_list_t *`指针
uintptr_t method_list_t = *list;
//获取`count`
uint32_t count = *(uint32_t *)((unsigned long long)method_list_t + 4);

//11、获取 `method_list_t`中的`first`
/* 这里为了更好的获取方法信息,定义结构体struct method_t */
struct sl_method_t {
SEL name;
const char *types;
IMP imp;
};
struct sl_method_t first =*((struct sl_method_t *)((unsigned long long)method_list_t + 4 + 4));

//12、打印所有方法的信息
//获取数组中每个元素的大小
uint32_t entsizeAndFlags = *(uint32_t *)((unsigned long long)method_list_t);
/*
uint32_t entsize() const {
return entsizeAndFlags & ~FlagMask;
}
*/
uint32_t entsize = entsizeAndFlags & ~0x3;

for (int i=0; i<count; i++) {

struct sl_method_t method = *((struct sl_method_t *)((unsigned long long)method_list_t + 4 + 4 + i*entsize));

printf("method_name:%s \t method_types:%s \t method_imp:0x%llx",method.name,method.types,(unsigned long long)(method.imp));

printf("\n");
}

/*这里下断点*/ return UIApplicationMain(argc, argv, nil, appDelegateClassName);

}
method_name:setStr: 	 method_types:v24@0:8@16 	 method_imp:0x1000a8120
method_name:setObj: method_types:v24@0:8@16 method_imp:0x1000a8174
method_name:.cxx_destruct method_types:v16@0:8 method_imp:0x1000a81b4
method_name:str method_types:@16@0:8 method_imp:0x1000a80e8
method_name:setI: method_types:v20@0:8i16 method_imp:0x1000a80c4
method_name:i method_types:i16@0:8 method_imp:0x1000a80a8
method_name:obj method_types:@16@0:8 method_imp:0x1000a8158
(lldb)

到这里,就意味着struct class_rw_t 中的method_array_t methods已经完全被解析出来了。
那么同样的,其他几个字段的内容,也是可以获取到的。(例如:property_array_t properties; protocol_array_t protocols)。这里就不一一读取。
我们接下来的问题是:

获取到这些信息可以用来做什么?

最简单的,我们既然可以获取到类中每个方法的IMP,那么我们是否可以替换掉其指针,到达Hook的效果呢?
简单修改一下代码:

NSString * sl_str(id self,SEL sel){
return @"hooked!!!";
}

int main(int argc, char * argv[]) {
NSString * appDelegateClassName;

appDelegateClassName = NSStringFromClass([AppDelegate class]);

CustomClass *c = [CustomClass new];
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));

//6、获取 `class_ro_t` 中的 `instanceSize`
uint32_t instanceSize = *(uint32_t *)((unsigned long long)ro + 4 +4);

//7、获取 `class_rw_t` 中 `method`

uintptr_t method = *((uintptr_t *)(((unsigned long long)rw_t)+4+4+8));

/*注:因为`method`成员没有用指针修饰,所以`method`的值即为
union
{List *list;
uintptr_t arrayAndFlag;

}
的值。
*/
//8、获取 `arrayAndFlag`的值
uintptr_t arrayAndFlag = method;

//9、//获取 `List **`
uintptr_t *list;
//判断 `arrayAndFlag` 标志位
if (arrayAndFlag & 1) {
//获取 `array_t *`
uintptr_t array_t = arrayAndFlag & ~1;
list = &(*((uintptr_t *)((unsigned long long)array_t+8)));
}else {
//获取 `List **`
list = &arrayAndFlag;
}

//10、获取`method_list_t` 中的 `count`
//获取`method_list_t *`指针
uintptr_t method_list_t = *list;
//获取`count`
uint32_t count = *(uint32_t *)((unsigned long long)method_list_t + 4);

//11、获取 `method_list_t`中的`first`
/* 这里为了更好的获取方法信息,定义结构体struct method_t */
struct sl_method_t {
SEL name;
const char *types;
IMP imp;
};
struct sl_method_t first =*((struct sl_method_t *)((unsigned long long)method_list_t + 4 + 4));

//12、打印所有方法的信息
//获取数组中每个元素的大小
uint32_t entsizeAndFlags = *(uint32_t *)((unsigned long long)method_list_t);
/*
uint32_t entsize() const {
return entsizeAndFlags & ~FlagMask;
}
*/
uint32_t entsize = entsizeAndFlags & ~0x3;

for (int i=0; i<count; i++) {

struct sl_method_t method = *((struct sl_method_t *)((unsigned long long)method_list_t + 4 + 4 + i*entsize));

printf("method_name:%s \t method_types:%s \t method_imp:0x%llx",method.name,method.types,(unsigned long long)(method.imp));

/*Hook `str` 方法*/
if (!strcmp((char *)(method.name), "str")) {
//修改IMP的值
*(uintptr_t *)((unsigned long long)method_list_t + 4 + 4 + i*entsize+8+8) = (uintptr_t)&sl_str;
}

printf("\n");
}

//调用,检查是否被hook
NSString *str = [c str];

/*这里下断点*/ return UIApplicationMain(argc, argv, nil, appDelegateClassName);

}
method_name:setStr: 	 method_types:v24@0:8@16 	 method_imp:0x1000880fc
method_name:setObj: method_types:v24@0:8@16 method_imp:0x100088150
method_name:.cxx_destruct method_types:v16@0:8 method_imp:0x100088190
method_name:str method_types:@16@0:8 method_imp:0x1000880c4
method_name:setI: method_types:v20@0:8i16 method_imp:0x1000880a0
method_name:i method_types:i16@0:8 method_imp:0x100088084
method_name:obj method_types:@16@0:8 method_imp:0x100088134
(lldb) po str
hooked!!!

(lldb)

看到最后的hooked!!!说明,hook成功了!
当然,完整的hook,并不是单纯的替换函数指针即可,还需要考虑很多问题,这里只是简单的测试一下可行性。

0 个评论

要回复文章请先登录注册