【iOS】Block Hook概念+BlockHook第三方库分析(基本原理已完结,补充libffi方法解释)

block hook就是勾住block进行逻辑注入,且不影响原有block逻辑。

依赖OC的运行时机制,拦截方法比较容易,但是拦截block却没那么简单


前置知识1:Block数据结构

后面的介绍和分析都用到了block的数据结构,这里先整理一下。

Block的定义在Block_private.h中,点击查看源码。

#define BLOCK_DESCRIPTOR_1 1
struct Block_descriptor_1 {
    uintptr_t reserved;
    uintptr_t size;
};

#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
    // requires BLOCK_HAS_COPY_DISPOSE
    void (*copy)(void *dst, const void *src);
    void (*dispose)(const void *);
};

#define BLOCK_DESCRIPTOR_3 1
struct Block_descriptor_3 {
    // requires BLOCK_HAS_SIGNATURE
    const char *signature;
    const char *layout;     // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};

struct Block_layout {
    void *isa;
    volatile int32_t flags; // contains ref count
    int32_t reserved; 
    void (*invoke)(void *, ...);
    struct Block_descriptor_1 *descriptor;
    // imported variables
};
  • isa指针:指向父类结构体,就是_NSConcreteStackBlock_NSConcreteMallocBlock_NSConcreteGlobalBlock这几个,就是对应在分配内存时的对应区域,不深入。
  • flags:按bit位表示一些block的附加信息,比如判断block类型、判断block引用计数、判断block是否需要执行辅助函数等。
  • // Values for Block_layout->flags to describe block objects
    enum {
        BLOCK_DEALLOCATING =      (0x0001),  // runtime
        BLOCK_REFCOUNT_MASK =     (0xfffe),  // runtime
        BLOCK_NEEDS_FREE =        (1 << 24), // runtime
        BLOCK_HAS_COPY_DISPOSE =  (1 << 25), // compiler
        BLOCK_HAS_CTOR =          (1 << 26), // compiler: helpers have C++ code
        BLOCK_IS_GC =             (1 << 27), // runtime
        BLOCK_IS_GLOBAL =         (1 << 28), // compiler
        BLOCK_USE_STRET =         (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
        BLOCK_HAS_SIGNATURE  =    (1 << 30), // compiler
        BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31)  // compiler
    };
  • reserved:保留变量,我的理解是表示block内部的变量数。
  • invoke:函数指针,指向具体的block实现的函数调用地址。
  • descriptor:block附加描述信息,主要保存了内存size以及copy,dispose函数的指针,签名以及layout等信息。通过源码可以发现,Block_layout中只包含了Block_descriptor_1,这是因为在捕获不同类型变量或者没用到外部变量时,编译器会改变结构体的结构,按需添加Block_descriptor_2和Block_descriptor_3,所以需要BLOCK_HAS_COPY_DISPOSE和BLOCK_HAS_SIGNATURE等枚举来判断
  • variables:因为block有闭包性,所以可以访问block外部的局部变量。这些variables就是复制到结构体中的外部局部变量或变量的地址(__block关键词,复制引用地址实现访问)

另外定义了Block_byref,是变量在被__block修饰时由编译器来生成,暂时不关注。

前置知识2:libffi 动态调用C函数

基本流程如下:

1.准备好参数数据以及ffi_type数组、返回值内存指针、函数指针

2.创建与函数特征相匹配的函数原型:ffi_cif对象

3.使用ffi_call来完成函数调用

需要用到的API:

/* 封装函数原型
ffi_prep_cif returns a libffi status code, of type ffi_status. This will be either FFI_OK if everything worked properly; FFI_BAD_TYPEDEF if one of the ffi_type objects is incorrect; or FFI_BAD_ABI if the abi parameter is invalid.
*/
ffi_status ffi_prep_cif(ffi_cif *cif,
            ffi_abi abi,                  //abi is the ABI to use; normally FFI_DEFAULT_ABI is what you want. Multiple ABIs for more information.
            unsigned int nargs,           //nargs is the number of arguments that this function accepts. ‘libffi’ does not yet handle varargs functions; see Missing Features for more information.
            ffi_type *rtype,              //rtype is a pointer to an ffi_type structure that describes the return type of the function. See Types.
            ffi_type **atypes);           //argtypes is a vector of ffi_type pointers. argtypes must have nargs elements. If nargs is 0, this argument is ignored.

/*  调用指定函数
This calls the function fn according to the description given in cif. cif must have already been prepared using ffi_prep_cif.
*/
void ffi_call(ffi_cif *cif,
          void (*fn)(void),
          void *rvalue,                   //rvalue is a pointer to a chunk of memory that will hold the result of the function call. This must be large enough to hold the result and must be suitably aligned; it is the caller's responsibility to ensure this. If cif declares that the function returns void (using ffi_type_void), then rvalue is ignored. If rvalue is ‘NULL’, then the return value is discarded.
          void **avalue);                 //avalues is a vector of void * pointers that point to the memory locations holding the argument values for a call. If cif declares that the function has no arguments (i.e., nargs was 0), then avalues is ignored. Note that argument values may be modified by the callee (for instance, structs passed by value); the burden of copying pass-by-value arguments is placed on the caller.

但是在block hook中不需要使用ffi_call调用函数,而是要为ffi_cif对象绑定一个ffi_closure,也就是把参数和返回值类型的函数指针,绑定到一个函数实体上

用到的API:

ffi_status ffi_prep_closure_loc (ffi_closure *closure,  //闭包,一个ffi_closure对象
       ffi_cif *cif,  //函数原型
       void (*fun) (ffi_cif *cif, void *ret, void **args, void*user_data), //函数实体
       void *user_data, //函数上下文,函数实体实参
       void *codeloc)   //函数指针,指向函数实体

 


关键因素

1.block是对象,支持消息转发机制。block hook在消息转发时机进行操作

2.block支持以NSInvocation的形式调用,保证block hook之后正常响应旧blcok可以调用新block

(一)block支持消息转发

Objective-C的运行时机制中最重要的一个应用场景就是消息转发。在Objective-C中,一个对象调用某个方法,严格意义上来说他不叫调用,叫发消息。Objective-C不像C/C++,在编译器就确定内部函数的地址,而是到运行时的时候才找到函数的调用地址进行调用。任何Objective-C的方法调用,编译器实际上把它转换成objc_msgSend(对象,方法名,...)这样的C函数调用。通过objc_msgSend函数,运行时机制会根据方法名在对象的方法列表里面查找方法实现,如果没有到父类中查找,一直到根类。如果没有查找到方法实现的地址,就会进入消息转发,如果消息转发没有做处理,则会抛出一个doesNotRecognizeSelector的异常。

block的源码结构中有这样一部分:

struct Block_layout {
    void *isa;
    volatile int32_t flags; // contains ref count
    int32_t reserved; 
    void (*invoke)(void *, ...);
    struct Block_descriptor_1 *descriptor;
    // imported variables
};

1.在Objective-C里,如果包含isa指针,说明这个结构类型则属于对象类型,所以block是一个对象

2.block调用的实际是block对象调用了自己的函数实现(invoke指针,是一个函数指针,指向block的实现),block支持消息转发机制

消息转发原理:

当一个对象的某个方法没实现的时候,OC会将该方法实现指向一个特殊的函数指针_objc_msgForward,之后方法会就进入消息动态绑定或消息转发流程。

hook思路:

因此可以将block的函数指针(invoke)强行指向_objc_msgForward,启动它的消息转发。

启动消息转发后需要对block进行后续处理:

// 为了方法指定一个能够响应这个方法的备选对象
- (id)forwardingTargetForSelector:(SEL)aSelector;

// 返回一个方法的签名,签名包含方法入参信息、返回值等信息,对于block来说就是block的签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;

// 会根据上一步返回的签名生成一个NSInvocation对象
- (void)forwardInvocation:(NSInvocation *)anInvocation;

(二)block支持以NSInvocation的形式调用

NSInvocation:

NSInvocation是一条消息的对象包装,这里消息指的是我们的方法调用,同样也适用block。

一个NSInvocation对象包含一个Objective-C消息的所有元素:一个目标,一个选择器,参数和返回值。可以直接设置这些元素中的每一个,并在NSInvocation调度对象时自动设置返回值。

NSInvocation构造的关键是签名:

// 通过 方法签名创建 Invocation 对象
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sign];

block签名:

方法的签名获取比较简单,通过[NSString instanceMethodSignatureForSelector:@selector(method:)]获取。但是block签名的获取则没有这么直接。

Block_descriptor_3这个结构体包含signature这个字段,也就是block的签名。但是结构中没有看到访问Block_descriptor_3的方法。这是因为编译器是根据flags上的值判断block是何种类型,生成不同Block_layout结构。

flags和BLOCK_HAS_SIGNATURE做一次与操作,值不为0则说明这个block有Block_descriptor_3这个结构,这样就可以取到里面的签名信息。接着通过[NSMethodSignature signatureWithObjCTypes]生成签名对象,再通过[NSInvocation invocationWithMethodSignature]构造NSInvocation对象,给NSInvocation对象指定消息的响应者,block响应者当然是自己本身,再调invoke方法就可以完成block的调用。


基本步骤

1、保存原来block的副本,因为不影响原有的业务逻辑,在hook注入我们自己业务逻辑之后,我们需要回过头响应原有的block逻辑;
2、强制启动block的消息转发机制;
3、在消息转发最后一步,将副本和hook block取出包装成NSInvocation进行调用;

以上内容其实和BlockHook第三方库的实现思路有所出入,后者并不涉及消息转发机制,而是直接替换了invoke指针的调用。因此之前消息转发的内容并不符合下文所述,删掉。

基本思路

  1. 根据 block 对象的签名,使用 ffi_prep_cif 构建 block->invoke 函数的模板 cif
  2. 使用 ffi_closure,根据 cif 动态定义函数 replacementInvoke,并指定通用的实现函数为 ClosureFunc
  3. 将 block->invoke 替换为 replacementInvoke,原始的 block->invoke 存放在 originInvoke
  4. 在 ClosureFunc 中动态调用 originInvoke 函数和执行 hook 的逻辑。

原理图

【iOS】Block Hook概念+BlockHook第三方库分析(基本原理已完结,补充libffi方法解释)_第1张图片

 


BlockHook第三方库分析

参考Git: https://github.com/yulingtianxia/BlockHook

参考文档: http://yulingtianxia.com/blog/2018/02/28/Hook-Objective-C-Block-with-Libffi/

需要在工程中引入 libffi.a 和包含头文件的 include 文件夹

代码思路

  • BHToken: 它实现了 hook 的逻辑,存储了相关的上下文。是最主要的类。
  • NSObject (BlockHook): 提供 hook 的接口,每次 hook block 对象都会创建一个 BHToken,并将其返回给用户。
  • BHCenter 管理 BHToken 对象的中心,以后可以拓展更多玩法。

第一步:拿到block的签名signature并解析

如前文所述,获得Block_descriptor_3中的签名(block和Block_descriptor_3都进行了封装)

不过需要注意的是除了判断BLOCK_HAS_SIGNATURE这个字段,还需要判断BLOCK_HAS_COPY_DISPOSE,这个字段决定了Block_descriptor_2存在与否,也就影响了获取Block_descriptor_3时的指针偏移量。

static const char *BHBlockTypeEncodeString(id blockObj)
{
    struct _BHBlock *block = (__bridge void *)blockObj;
    return _bh_Block_descriptor_3(block)->signature;
}

一个 block 的签名格式是:[返回值类型和偏移][@?0][参数0类型和偏移][参数1类型和偏移]…

编码规则可以查看 Type Encodings。

int (^block)(int, int)的签名是 i16@?0i8i12。block 指针占 8 字节,参数和返回值 int 都是 4 字节

void (^testBlock)(int a)的签名是 v12@?0i8

void (^testBlock)(NSString *a)的签名是 v16@?0@"NSString"8

struct TestStruct {
    int64_t a;
    double b;
    float c;
    char d;
    int *e;
    CGRect *f;
    uint64_t g;
};

struct TestStruct (^StructReturnBlock)(int)的签名是 {TestStruct=qdfc^i^{CGRect}Q}12@?0i8

然后就是解析signature字符串,解析为一个ffi_type指针的数组(在 libffi 中使用 ffi_type 表示各种类型),不分析解析过程了。

- (ffi_type **)_typesWithEncodeString:(const char *)str getCount:(int *)outCount 
            startIndex:(int)start nullAtEnd:(BOOL)nullAtEnd

通过上面方法获得参数类型列表、返回值类型、参数个数。

注意如果block的flag含有BLOCK_USE_STRET(对应源码BLOCK_HAS_STRETx86架构下),意味着返回值是一个大于16字节的结构体。

这时候产生的情况是block的invoke指向的参数列表发生了变化,返回值变为空,参数列表第一个变成结构体的指针,原本第一个的参数self被依次往后挪一位,但是此时signature没有变,所以需要手动调整:

argTypes[0] = &ffi_type_pointer;
returnType = &ffi_type_void;

这才是block的invoke函数真正的函数声明,但是需要理解的是,对于block而言他的签名依然是结构体返回值,self作为第一个参数。

(可具体见代码,作者的这个调整有点东西)

BLOCK_HAS_STRET注释 undefined if !BLOCK_HAS_SIGNATURE ,正确的理解应该是如果没有签名则不定义该字段,也就是该字段没有意义,而不是自己原来理解的那种和BLOCK_HAS_SIGNATURE相对的意思。

第二步:创建block->invoke函数的模板cif

有了参数类型列表argTypes,返回值类型returnType,参数个数argCount后,就可以调用 ffi_prep_cif() 函数创建 ffi_cif 了,也就是函数模板

- (int)_prepCIF:(ffi_cif *)cif withEncodeString:(const char *)str
{
    int argCount;
    ...//获得参数类型列表、返回值类型、参数个数
    ffi_status status = ffi_prep_cif(cif, FFI_DEFAULT_ABI, argCount, returnType, argTypes);
    if (status != FFI_OK) {
        NSLog(@"Got result %ld from ffi_prep_cif", (long)status);
        abort();
    }
    return argCount;
}

上面方法实现了这个逻辑。

函数模板其实就是函数原型,在libffi中只要有函数原型cif对象,函数实现指针,返回值内存指针和函数参数数组,我们就可以实现在运行时动态调用任意C函数

第三步:创建闭包closure替换 Block 的invoke

接下来就是要创建闭包,可以理解为函数实现。libffi中闭包是通过ffi_closure结构定义的

void *_replacementInvoke;
// 初始化
ffi_closure *_closure = ffi_closure_alloc(sizeof(ffi_closure), &_replacementInvoke);
// 为闭包绑定函数 第二个参数是函数原型,第三个参数是函数实体,第四个参数传递自定义数据,第五个参数是函数指针
ffi_status status = ffi_prep_closure_loc(_closure, &_cif, BHFFIClosureFunc, 
                (__bridge void *)(self), _replacementInvoke);
//self->BHToken

当_replacementInvoke被调用时,函数BHFFIClosureFunc 会被调用,传给 replacementInvoke() 的参数及其返回值都会被传给 BHFFIClosureFunc()

现在将函数指针_replacementInvoke和模板_cif 绑定函数闭包之后,需要将Block的invoke替换成_replacementInvoke,并保存原始的实现到_originInvoke

struct _BHBlock *block = (__bridge struct _BHBlock *)self.block;
BHLock *lock = [self.block bh_lockForKey:@selector(block_currentInvokeFunction)];    
[lock lock];
self.originInvoke = block->invoke;
block->invoke = _replacementInvoke;
[lock unlock];

由于前面一系列操作,所以现在invoke指向函数的模板和block的签名依然是一致的,这样当 block 调用时,实际上会调用 _replacementInvoke 函数,进而调用 BHFFIClosureFunc 通用函数。在BHFFIClosureFunc里面会实现 hook 的逻辑。

这一段代码比较好懂,用到了NSLock,避免同时替换。

第三步:实现BHFFIClosureFunc

static void BHFFIClosureFunc(ffi_cif *cif, void *ret, void **args, void *userdata);

个人理解这其实是一种处于调用中间状态的函数,正常的流程应该是通过这个函数直接ffi_call函数实现。

函数通过把userData强转回BHToken,根据token.mode进行不同处理:

static void BHFFIClosureFunc(ffi_cif *cif, void *ret, void **args, void *userdata)
{
    BHToken *token = (__bridge BHToken *)(userdata);
    void *userRet = ret;
    void **userArgs = args;
    if (token.hasStret) {
        // The first arg contains address of a pointer of returned struct.
        userRet = *((void **)args[0]);
        // Other args move backwards.
        userArgs = args + 1;
    }
    *(void **)userRet = NULL;
    BHInvocation *invocation = [[BHInvocation alloc] initWithToken:token];
    invocation.args = userArgs;
    invocation.retValue = userRet;
    invocation.realArgs = args;
    invocation.realRetValue = ret;
    if (BlockHookModeContainsMode(token.mode, BlockHookModeBefore)) {
        [token invokeAspectBlockWithArgs:userArgs retValue:userRet mode:BlockHookModeBefore invocation:invocation];
    }
    if (!(BlockHookModeContainsMode(token.mode, BlockHookModeInstead) && [token invokeAspectBlockWithArgs:userArgs retValue:userRet mode:BlockHookModeInstead invocation:invocation])) {
        [token invokeOriginalBlockWithArgs:args retValue:ret];
    }
    if (BlockHookModeContainsMode(token.mode, BlockHookModeAfter)) {
        [token invokeAspectBlockWithArgs:userArgs retValue:userRet mode:BlockHookModeAfter invocation:invocation];
    }
}

realArgs,realRetValue的产生就是因为前文起到的结构体导致参数列表变化进一步导致了另外的处理。

这里的BHInvocation放到后面看。

第四步:组装 NSInvocation 执行 Hook 逻辑

先看下调用原block的实现,并没有使用NSInvocation而是直接使用ffi_call方法,因为NSInvocation调用的是block,最后会执行invoke指向的函数,也就是自己会死循环。

// BHToken
- (void)invokeOriginalBlockWithArgs:(void **)args retValue:(void *)retValue
{
    if (self.originInvoke) {
        ffi_call(&_cif, self.originInvoke, retValue, args);
    }
    else {
        NSLog(@"You had lost your originInvoke! Please check the order of removing tokens!");
    }
}

调用新block并不是调用invoke指向的函数,因此不能用ffi_call。

// BHToken
- (BOOL)invokeAspectBlockWithArgs:(void **)args retValue:(void *)retValue mode:(BlockHookMode)mode invocation:(BHInvocation *)invocation
{
    if (!self.isStackBlock && !self.block) {
        return NO;
    }
    invocation.mode = mode;
    NSInvocation *blockInvocation = [NSInvocation invocationWithMethodSignature:self.aspectBlockSignature];
    if (self.aspectBlockSignature.numberOfArguments > 1) {
        [blockInvocation setArgument:(void *)&invocation atIndex:1];
    }
    
    // origin block invoke func arguments: block(self), ...
    // origin block invoke func arguments (x86 struct return): struct*, block(self), ...
    // hook block signature arguments: block(self), invocation, ...
    NSUInteger numberOfArguments = MIN(self.aspectBlockSignature.numberOfArguments, self.originalBlockSignature.numberOfArguments + 1);
    for (NSUInteger idx = 2; idx < numberOfArguments; idx++) {
        [blockInvocation setArgument:args[idx - 1] atIndex:idx];
    }
    [blockInvocation invokeWithTarget:self.aspectBlock];
    return YES;
}

代码就是构造NSInvocation,参数需要对齐。需要注意对于block的NSInvocation来说,参数是从1开始(一般方法是从2开始,前面会有self和_cmd)

这里可以看出BHInvocation的作用,如果新block传入一个及以上参数(不包括传入自己),第一个参数必须是BHInvocation,这样新block才可以根据BHInvocation获得原block的信息。(这是作者代码的设计而非硬性规则)

基本的原理就是上面这么多,有一些细节之后有机会可以补充:

1.block和private data

2.BHCenter的内容

3.杨萧玉Blog其他内容


参考资料:

1.https://juejin.im/post/5c653921e51d457fa676eafc Block hook正确姿势?作者:欣东?来源:掘金

2.https://juejin.im/post/5be46cfe6fb9a049a5706546 [Runtime]NSInvocation 作者:一人我编程累 来源:掘金

3.https://github.com/yulingtianxia/BlockHook BlockHook(Hook Objective-C Blocks)

4.https://www.cnblogs.com/bao-jie/p/6196765.html Block解析(iOS)

5.https://www.jianshu.com/p/d96d27819679 史上最详细的Block源码剖析 作者:WhiteZero 来源:简书

6.https://www.jianshu.com/p/c9680c5e6773 "Block的实质初探 "等一系列文章 作者:madaoCN 来源:简书

7.http://yulingtianxia.com/blog/ 杨萧玉Blog

8.https://segmentfault.com/a/1190000004141249 作者:canopus4u

9.https://blog.csdn.net/jaimecool/article/details/76332930 【libffi】动态调用&定义C函数 作者:Yaso_GG

你可能感兴趣的:(iOS学习)