iOS如何通过内存地址调用方法

924 阅读2分钟

SEL是方法编号,也是方法名,在dyld加载镜像到内存时,通过_read_image方法加载到内存的表中了

IMP是函数实现指针,找IMP就是找函数实现的过程

SELIMP的关系就可以解释为:

  • SEL就相当于书本的⽬录标题
  • IMP就是书本的⻚码
  • 函数就是具体页码对应的内容

之前遇到过一个面试题,总所周知,分类会覆盖,覆盖顺序是按照编译顺序来决定的,如果我在项目中,有4个分类,但是我想调用指定的分类,该如何做呢?

  1. 大多数的朋友都会去查找类的方法列表,然后通过IMP调用
     u_int count;
    Method *methods = class_copyMethodList([KryTestPerson class], &count);
   
    
    for (int i = 0; i < count; i++) {
        SEL name = method_getName(methods[i]);
        NSString *strName = [NSString stringWithCString:sel_getName(name) encoding:NSUTF8StringEncoding];
        IMP imp = method_getImplementation(methods[i]);
        NSLog(@"%@ 的IMP ==== %p",strName,imp);
    }
    
  1. 还有一种办法,参考一下fishhook的原理,首先通过String Table分割字符串拿到地址,然后再去Symbol Table中找到对应的偏移地址,我们在项目中只需要拿到项目的首地址 + 偏移地址,这样就可以直接根据函数地址来调用了

image.png

image.png

//获取基地址
intptr_t get_load_address(void) {
    const struct mach_header *exe_header = NULL;
    for (uint32_t i = 0; i < _dyld_image_count(); i++) {
        const struct mach_header *header = _dyld_get_image_header(i);
        if (header->filetype == MH_EXECUTE) {
            exe_header = header;
            break;
        }
    }
    //返回值即为加载地址
    return (intptr_t)exe_header;
}
//   通过 IMP直接调用函数 
 
    intptr_t origP = 0x3EC0;

    intptr_t loadP =  get_load_address();

    IMP impxx = origP + loadP;

    impxx();
      
   // 带参数正常调用 需要传递实例对象,SEL,参数
   // ((void (*)(id, SEL,NSString*))impxx)(stu,sel,@"123");

打印结果为 KryTestPerson1的hello

如何找到0x3EC0这个地址,可以去看看fishhook的原理,这里就不做过多叙述,可以看到0x3EC0就是上面MachO-[KryTestPerson(KryTestPerson1)hello]中的地址,只不过我们把前面的1抹掉了,只要偏移量,再加上项目首地址(已经ASLR)过了,就是函数的实际IMP地址

此方法写地址的地址,要保证项目的二进制不在改变,如果你又添加了代码,这个偏移地址是会变的。

对于名称相同的方法,他们都有相同的SEL,方法的名称不包括类名称,所以子类和父类中的同名方法拥有相同的SEL,但是他们的实现可以各不相同, 因而在他们各自的Dispatch表中SEL所对应的IMP是不同的,IMP是一个函数指针,而虽然每一个SEL对应的是一个方法的名称,但考虑到效率,SEL本身是一个整型,编译器会另外生成一张SEL和方法名称对应的表。有了这样的结构,objc就可以实现多态了