基于LLVM对MSHookIvar之后无效的问题进行分析


#1

起源是 @laomeihttp://bbs.iosre.com/t/hook/11716 询问为何MSHookIvar之后原值没有更改。原代码:

UIScreen  *uis=%orig;
CGRect cgr = MSHookIvar<CGRect>(uis, "_bounds");
cgr.size.width=1080.0f;
cgr.size.height=1920.0f;
[uis  bounds]=cgr;
NSLog(@"============:0[%f]",cgr.size.height);

CGRect screenBounds = [uis bounds];
NSLog(@"============:1[%f]",screenBounds.size.height); --调试发现,此处值仍然是旧值

然而这篇文章只解释了MSHookIvar的问题,[uis bounds]=cgr;给只读属性赋值我真的很想知道他是怎么编译的

通过在C/C++层的分析我给出了回答如下:

int a=*b形式的Pointer Dereference会复制一份目标地址的值。比如说:

#include <stdio.h>
static void foo(int* A){
  int C=*A;
  C=10;
}
int main(int argc, char const *argv[]) {
  int B=0;
  foo(&B);
  printf("%i\n",B);
  return 0;
}

输出:

λ : >>> ./a.out             
0

看下MSHookIvar的源码

template <typename Type_>
static inline Type_ &MSHookIvar(id self, const char *name) {
    Ivar ivar(class_getInstanceVariable(object_getClass(self), name));
    void *pointer(ivar == NULL ? NULL : reinterpret_cast<char *>(self) + ivar_getOffset(ivar));
    return *reinterpret_cast<Type_ *>(pointer);
}

这里第一第二行通过ObjC运行时获得了这个ivar的地址。最后一步reinterpret_cast将地址转换成正确类型的指针后通过指针解引用创建了一份这个值的引用。 CGRect cgr = XXXXXX里左值是CGRect而不是引用(CGRect&) , 所以按照标准这里就创建了一份副本(题外话: 如果是C++对象的话这里是隐式调用Copy Constructor)。正常情况下因为ivar是objc对象时本来就是对指针进行操作,所以复制一份指针的值指向的还是正确的objc对象,但对于结构体和其他原本不是指针类型的ivar这么干就会在副本上进行操作了。

@interface ViewController () {
      int foo;
}
@end
-(void)test{
    self->foo=0;
    NSLog(@"%i",self->foo);
    int lol=MSHookIvar<int>(self,"foo");
    lol=10;
    NSLog(@"%i",self->foo);
    MSHookIvar<int>(self,"foo")=10;
    NSLog(@"%i",self->foo);
}

输出:

2018-05-04 18:55:34.021 [50888:729029] 0
2018-05-04 18:55:34.021 [50888:729029] 0
2018-05-04 18:55:34.021 [50888:729029] 10

问题看起来是解决了。然而,纯粹是为了好玩,让我们掏出LLVM以更低层的方式来看待这个问题。构造源码如下:

#include <stdio.h>
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
template <typename Type_>
static inline Type_ &MSHookIvar(id self, const char *name) {
    Ivar ivar(class_getInstanceVariable(object_getClass(self), name));
    void *pointer(ivar == NULL ? NULL : reinterpret_cast<char *>(self) + ivar_getOffset(ivar));
    return *reinterpret_cast<Type_ *>(pointer);
}
int main(int argc, char const *argv[]) {
  NSData* item=[NSData new];
  int lol=MSHookIvar<int>(item,"foo");
  lol=10;
  MSHookIvar<int>(item,"foo")=10;
  return 0;
}

使用clang -S -emit-llvm helloworld.mm 生成如下 LLVM中间表示:

define i32 @main(i32, i8**) #0 {
  %3 = alloca i32, align 4
  %4 = alloca i32, align 4
  %5 = alloca i8**, align 8
  %6 = alloca %0*, align 8
  %7 = alloca i32, align 4
  store i32 0, i32* %3, align 4
  store i32 %0, i32* %4, align 4
  store i8** %1, i8*** %5, align 8
  %8 = load %struct._class_t*, %struct._class_t** @"OBJC_CLASSLIST_REFERENCES_$_", align 8
  %9 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_, align 8, !invariant.load !8
  %10 = bitcast %struct._class_t* %8 to i8*
  %11 = call i8* bitcast (i8* (i8*, i8*, ...)* @objc_msgSend to i8* (i8*, i8*)*)(i8* %10, i8* %9)
  %12 = bitcast i8* %11 to %0*
  store %0* %12, %0** %6, align 8
  %13 = load %0*, %0** %6, align 8
  %14 = bitcast %0* %13 to i8*
  %15 = call dereferenceable(4) i32* @_ZL10MSHookIvarIiERT_P11objc_objectPKc(i8* %14, i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str, i32 0, i32 0))
  %16 = load i32, i32* %15, align 4
  store i32 %16, i32* %7, align 4
  store i32 10, i32* %7, align 4
  %17 = load %0*, %0** %6, align 8
  %18 = bitcast %0* %17 to i8*
  %19 = call dereferenceable(4) i32* @_ZL10MSHookIvarIiERT_P11objc_objectPKc(i8* %18, i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str, i32 0, i32 0))
  store i32 10, i32* %19, align 4
  ret i32 0
}

@_ZL10MSHookIvarIiERT_P11objc_objectPKc是MSHookIvar在被C++ Name Mangling之后的函数名称,对应int& MSHookIvar<int>(objc_object*, char const*)
alloca指令在栈上分配内存空间。
这里我们可以看到在第一次调用之后的IR如下:

  %15 = call dereferenceable(4) i32* @_ZL10MSHookIvarIiERT_P11objc_objectPKc(i8* %14, i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str, i32 0, i32 0))
  %16 = load i32, i32* %15, align 4
  store i32 %16, i32* %7, align 4
  store i32 10, i32* %7, align 4

第一条指令%15 = call 省略i32*后省略的i32*告诉我们返回长度32bit的整数的指针。%16的意思是从%15这个地址加载了一个32bit的整数到%16这个值。 后面两条store指令分别将刚加载的值和10这个常数保存到%7, 那么%7 是什么呢? 倒回去看一下函数开头:

 %7 = alloca i32, align 4

说明%7是一个在栈上分配的32bit整数而不是原来的ivar所处的地址。这也就是为什么第一行MSHookIvar之后数值没有改变的原因。

然后我们来看第二次调用:

 %19 = call dereferenceable(4) i32* @_ZL10MSHookIvarIiERT_P11objc_objectPKc(i8* %18, i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str, i32 0, i32 0))
  store i32 10, i32* %19, align 4

这次我们将常数10直接保存到了MSHookIvar返回的指针(即%19) 所指向的内存区域里,所以这次的MSHookIvar成功的修改了对应的值