libffi探究

5,843 阅读13分钟

一、函数调用约定(Calling Convention)

在介绍libffi库之前,我们先来了解一个概念:函数调用约定,因为libffi库的工作原理就是基于这个条件进行的。

函数调用约定,简而言之就是对函数调用的一些规定,通过遵循这些规定,来确保函数能正常被调用。具体包含以下内容:

  • 参数的传递方式,参数是通过栈传递还是寄存器传递
  • 参数的传递顺序,是从左到右,还是从右到左
  • 栈的维护方式,比如函数调用后参数从栈中弹出是由调用方处理还是被调用方处理

当然函数调用约定并非都是统一的,不同的设备架构体系,对应的规则也是不同的。比如iOS的arm架构和Mac的x86架构,两者的调用约定是不同的。

其实,在日常工作中,通常比较少接触到这个概念。因为编译器已经帮我们完成了这一工作,我们只需要遵循正确的语法规则即可,编译器会根据不同的架构生成对应的汇编代码,从而确保函数调用约定的正确性。

二、libffi的使用

libffi is a foreign function interface library. It provides a C programming language interface for calling natively compiled functions given information about the target function at run time instead of compile time. It also implements the opposite functionality: libffi can produce a pointer to a function that can accept and decode any combination of arguments defined at run time.

引用一段wiki上对libffi的介绍。

简单来说,libffi可以实现在运行时动态调用函数,同时也可以在运行时生成一个指针,绑定到对应的函数,并能接收和解析传递过来的参数。那么它是怎么做到的呢?

我们上面说了函数能正确被调用的前提条件是遵循函数调用约定,而这一工作通常是由编译器负责的,在编译过程中生成对应的汇编代码。如果我们想在运行时中去动态调用函数,意味着这个过程是无法被编译的,那么就无法保证函数函数调用约定。而libffi在运行时帮我们做到做到了这点,它实际上就等同于一个动态的编译器,能够在运行时中完成编辑器在编译时对函数调用约定的处理。

了解完libffi的原理之后,接下来,我们就进入实操过程!

a. libffi的导入

笔者一开始是按照github上的文档进行操作的,结果发现行不通,提示一堆错误,最终在这里找到了一个能编译成功的版本。下载后,进入到libffi-master目录,然后执行以下操作:

  • 运行./autogen.sh脚本,如果提示出错,可能是没下载autoconf, automake , libtool这些库,分别brew install xxx即可
  • 运行libffi.xcodeproj
  • 选择libffi-iOS,然后运行编译
  • 不出意外,就能编译成功,然后在Products中找到生成的库libffi.a
  • libffi.a导入到需要使用的工程中,并把include对应的头文件也添加到工程中。

b. libffi的使用

  • ffi_call调用函数
int func1(int a, int b) {
    return a + b;
}

- (void)libffiTest {
    //1.
    ffi_type **argTypes;
    argTypes = malloc(sizeof(ffi_type *) * 2);
    argTypes[0] = &ffi_type_sint;
    argTypes[1] = &ffi_type_sint;
    //2.
    ffi_type *retType = &ffi_type_sint;
    //3.
    ffi_cif cif;
    ffi_prep_cif(&cif, FFI_DEFAULT_ABI, 2, retType, argTypes);
    //4.
    void **args = malloc(sizeof(void *) * 2);
    int x = 1, y = 2;
    args[0] = &x;
    args[1] = &y;
    int ret;
    //5.
    ffi_call(&cif, (void(*)(void))func1, &ret, args);
    NSLog(@"libffi return value: %d", ret);
}

运行结果:libffi return value: 3

如上所示,通过ffi_call方法实现了函数func1的调用,我们来具体分析下整个流程。

  1. 定义函数的参数类型,func1的参数为两个int类型,这里使用argTypes指针数组,先创建对应的大小,然后分别赋值int对应的ffi_type_sint类型。
  2. 定义函数的返回类型,func1的返回类型为int,所以retType赋值为ffi_type_sint
  3. 定义函数模板ffi_cif, 通过ffi_prep_cif创建对应的函数模板,第一个参数为ffi_cif模板;第二个参数表示不同CPU架构下的ABI,通常选择FFI_DEFAULT_ABI,会根据不同CPU架构选择到对应的ABI;第三个参数为函数参数个数;第四个参数为定义的函数参数类型;最后一个参数为函数返回值类型。
  4. 创建函数对应的参数值和返回值。
  5. 调用ffi_call方法,分别传入函数模板cif,绑定的函数func1,函数返回值ret和函数参数args
  • ffi_prep_closure_loc绑定函数指针
- (void)libffiBindTest {
    //1.
    ffi_type **argTypes;
    ffi_type *returnTypes;
    
    argTypes = malloc(sizeof(ffi_type *) * 2);
    argTypes[0] = &ffi_type_sint;
    argTypes[1] = &ffi_type_sint;
    
    returnTypes = malloc(sizeof(ffi_type *));
    returnTypes = &ffi_type_pointer;
    
    ffi_cif *cif = malloc(sizeof(ffi_cif));
    ffi_status status = ffi_prep_cif(cif, FFI_DEFAULT_ABI, 2, returnTypes, argTypes);
    if (status != FFI_OK) {
        NSLog(@"ffi_prep_cif return %u", status);
        return;
    }
    //2.
    char* (*funcInvoke)(int, int);
    //3.
    ffi_closure *closure = ffi_closure_alloc(sizeof(ffi_closure), &funcInvoke);
    //4.
    status = ffi_prep_closure_loc(closure, cif, bind_func, (__bridge void *)self, funcInvoke);
    if (status != FFI_OK) {
        NSLog(@"ffi_prep_closure_loc return %u", status);
        return;
    }
    //5.
    char *result = funcInvoke(2, 3);
    NSLog(@"libffi return func value: %@", [NSString stringWithUTF8String:result]);
    ffi_closure_free(closure);
}

// 6.
void bind_func(ffi_cif *cif, char **ret, int **args, void *userdata) {
    //7.
    LibffiViewController *viewController = (__bridge LibffiViewController *)userdata;
    int value1 = viewController.value;
    int value2 = *args[0];
    int value3 = *args[1];
    const char *result = [[NSString stringWithFormat:@"str-%d", (value1 + value2 + value3)] UTF8String];
    //8.
    *ret = result;
}

输出结果:libffi return func value: str-6
  1. 这一段主要是创建函数模板ffi_cif,具体过程跟上一个例子一样,这里就不重复了。
  2. 定义一个用来绑定的函数指针funcInvoke
  3. 创建一个ffi_closure对象,并将funcInvoke函数指针传递进去。
  4. 通过ffi_prep_closure_loc方法将ffi_clousure对象、函数模板cif、绑定的函数bind_func、绑定函数bind_func中传递的数据、函数指针funcInvoke等绑定在一起。
  5. 调用函数指针,会进入到绑定的函数bind_func中。
  6. 回调函数bind_func的参数类型分别是:函数模板ffi_cif,函数返回类型指针,函数参数类型指针,ffi_prep_closure_loc中传递进来的数据。
  7. 获取到对应的参数值,以及传入的self对象,然后进行相关逻辑处理。
  8. 最后将处理的结果返回给ret指针对象,作为返回值。

三、libffi的应用

上面讲解了libffi中ffi_callffi_prep_closure_loc两个方法的使用,接下来,我们将通过这两个方法来看看libffi在iOS中的两个应用。

a. block hook

在某些场景,可能需要对某个block进行hook,以实现在block调用前后插入相关代码,或替换该block等功能。

我们知道Block实际上为一个struct对象,其对应的结构类型如下:

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

其中结构体中的invoke表示block对应的函数指针,那么如果我们想对block进行hook,就可以考虑从这里下手——替换invoke函数指针。因此,我们可以先定义一个新的函数指针newInvoke,然后使用libffi将该指针绑定到对应的回调函数中,最后将block的invoke指针替换为newInvoke。这样,当block调用时,就会进入到libffi绑定的回调函数里,那么就可以在这里做一些额外的操作了。

清楚整体流程后,我们便逐一来进行,首先第一步是需要将block转换为对应的结构体,这样我们才能拿到其invoke函数指针。

struct JBlockLiteral {
    void *isa;
    int flags;
    int reserved;
    void (*invoke)(void *, ...);
    struct JBlockDecriptor1 *descriptor;
};

struct JBlockLiteral *blockRef = (__bridge struct JBlockLiteral *)self.originalBlock;
self.originalInvoke = blockRef->invoke;

转换的方式也非常容易,先定义一个与block内部相同结构的结构体JBlockLiteral,然后使用__bridge强制转换即可,这样就可以获取到其invoke函数指针了。

获取到block的函数指针后,就可以定义一个新的函数指针,替换掉block的函数指针

void *newInvoke;
blockRef->invoke = newInvoke;

当然直接这么做是会有问题的,因为newInvoke还是个未处理的野指针,我们需要通过libffi对其进行处理,并与回调函数进行绑定。

通过上面libffi的两个例子,我们知道首先需要创建对应的函数模板ffi_cif,而创建模板是需要知道函数参数和返回值类型的,所以得先获取到block对应的参数和返回值类型。通常我们可以将block转换为struct结构体,然后获取到它的函数签名,最后从函数签名中获取到参数和返回值类型。

NSMethodSignature* NSMethodSignatureForBlock(id block) {
    struct JBlockLiteral *blockRef = (__bridge struct JBlockLiteral *)block;
    if (!(blockRef->flags & JBLOCK_HAS_SIGNATURE)) {
        return nil;
    }
    void *desc = blockRef->descriptor;
    desc += sizeof(struct JBlockDecriptor1);
    if (blockRef->flags & JBLOCK_HAS_COPY_DISPOSE) {
        desc += sizeof(struct JBlockDecriptor2);
    }
    struct JBlockDecriptor3 *desc3 = (struct JBlockDecriptor3 *)desc;
    const char *signature = desc3->signature;
    if (signature) {
        return [NSMethodSignature signatureWithObjCTypes:signature];
    }
    return nil;
}

NSMethodSignature *signature = NSMethodSignatureForBlock(self.originalBlock);
NSUInteger arguments = signature.numberOfArguments;

关于block的签名获取这里就不细讲了,之前在对Block的一些理解这篇文章中已经讲解过了,所以直接看到模板创建部分吧。

ffi_type **argTypes = malloc(sizeof(ffi_type *) * arguments);
//1.
argTypes[0] = &ffi_type_pointer; //第一个参数为block本身
for (int i = 1; i < arguments; i ++) {
    const char *argType = [signature getArgumentTypeAtIndex:i];
    argTypes[i] = [self ffi_typeForTypeEncoding:argType];
}
//2.
const char *returnTypeEncoding = signature.methodReturnType;
ffi_type *returnType = [self ffi_typeForTypeEncoding:returnTypeEncoding];
//3.
ffi_cif *cif = malloc(sizeof(ffi_cif));
ffi_status status = ffi_prep_cif(cif, FFI_DEFAULT_ABI, (int)arguments, returnType, argTypes);
  1. block的参数列表中,第一个为block本身,所以第一个位置放置ffi_type_pointer,然后根据参数的type encoding来设置对应的类型。
- (ffi_type *)ffi_typeForTypeEncoding:(const char *)encoding {
    if (!strcmp(encoding, "c")) {
        return &ffi_type_schar;
    } else if (!strcmp(encoding, "i")) {
        return &ffi_type_sint;
    } else if (!strcmp(encoding, "@")) {
        return &ffi_type_pointer;
    }
    // ....
    return &ffi_type_pointer;
}

这里只是罗列了一部分,完整部分可以参考这里

  1. 返回值类型也是类似,先获取到对应的type encoding,然后再设置对应的类型。

  2. 传入对应的参数,创建函数模板cif

void *newInvoke;
ffi_closure *closure = ffi_closure_alloc(sizeof(ffi_closure), &newInvoke);
status = ffi_prep_closure_loc(closure, cif, BlockInvokeFunc, (__bridge void *)self, newInvoke);

创建模板后,通过ffi_prep_closure_loc将指针newInvoke和回调函数BlockInvokeFunc绑定在一起。

struct JBlockLiteral *blockRef = (__bridge struct JBlockLiteral *)self.originalBlock;
self.originalInvoke = blockRef->invoke;
blockRef->invoke = newInvoke;

将block的invoke函数指针保存到originalInvoke中(以便后面的使用),然后使用newInvoke替换为block的函数指针。意味着,当block调用时,newInvoke会被调用,其绑定的回调函数BlockInvokeFunc也会被调用。

通常对于block的hook处理一般为block调用前后插入代码或使用其他的block替换。因此,我们可以定义三种mode来表示不同的场景。

typedef enum : NSUInteger {
    JBlockHookModeBefore,
    JBlockHookModeInstead,
    JBlockHookModeAfter,
} JBlockHookMode;

在回调函数中,根据传入的不同的mode来分别进行处理

void BlockInvokeFunc(ffi_cif *cif, void *ret, void **args, void *userdata) {
    JBlockHook *blockHook = (__bridge JBlockHook *)userdata;
    JBlockHookMode mode = blockHook.mode;
    id handleBlock = blockHook.handleBlock;
    void *invoke = blockHook.originalInvoke;
    
    switch (mode) {
        case JBlockHookModeBefore:{
            invokeHandleBlock(handleBlock, args, YES);
            invokeOriginalBlockOrMethod(cif, ret, args, invoke);
        }
            break;
        case JBlockHookModeInstead: {
            invokeHandleBlock(handleBlock, args, YES);
        }
            break;
        case JBlockHookModeAfter: {
            invokeOriginalBlockOrMethod(cif, ret, args, invoke);
            invokeHandleBlock(handleBlock, args, YES);
        }
            break;
    }
}
  1. JBlockHook为自定义的一个对象,用来封装hook相关信息,分别获取到mode、插入(或替换)的block,以及原本的block。
  2. 根据mode的值,分别进行对应的处理。invokeHandleBlock为调用新添加的block,invokeOriginalBlockOrMethod为调用原来的block。
void invokeHandleBlock(id handleBlock, void **args, BOOL isBlock) {
    NSMethodSignature *signature = NSMethodSignatureForBlock(handleBlock);
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
    int offset = isBlock ? 1 : 2;
    for (int i = 0; i < signature.numberOfArguments-1; i ++) {
        [invocation setArgument:args[i+offset] atIndex:i+1];
    }
    [invocation invokeWithTarget:handleBlock];
}

我们知道NSInvocation可以对某个对象直接发送消息,不过需要获取到方法签名,所以首先获取到block的函数签名,然后将block的参数分别设置到invocation中,最后调用invokeWithTarget方法即可调用。由于block的第一个参数为自身,所以我们从args的第二个位置开始取值。

void invokeOriginalBlockOrMethod(ffi_cif *cif, void *ret, void **args, void *invoke) {
    if (invoke) {
        ffi_call(cif, invoke, ret, args);
    }
}

原本block的调用:我们之前存储了block原本的invoke函数指针,所以这里可以使用ffi_call直接调用原本的函数指针,并传入对应的参数和返回值即可。

至此,block的hook工作就完成,外部调用如下:

- (void)blockHook {
    int (^block)(int, int) = ^int(int x, int y) {
       int result = x + y;
       NSLog(@"%d + %d = %d", x, y, result);
       return result;
    };
    
    [JBlockHook hookBlock:block mode:JBlockHookModeBefore handleBlock:^(int x, int y){
        NSLog(@"hook block call before with %d, %d", x, y);
    }];
    
    [JBlockHook hookBlock:block mode:JBlockHookModeAfter handleBlock:^(int x, int y){
        NSLog(@"hook block call after with %d, %d", x, y);
    }];
    
    block(2, 3);
}

输出结果:
2020-06-01 11:15:49.387353+0800 JOCDemos[6713:99228] hook block call before with 2, 3
2020-06-01 11:15:49.387890+0800 JOCDemos[6713:99228] 2 + 3 = 5
2020-06-01 11:15:49.388672+0800 JOCDemos[6713:99228] hook block call after with 2, 3

小结

hook block的本质就是通过替换block的invoke函数指针,并使用libffi将新的函数指针绑定到对应的回调函数中,在回调函数中根据不同mode来进行不同的处理。

b. hook method

block的hook是通过替换其invoke指针,那么method的hook呢?其实也是类似的,我们知道每个OC方法都会有一个对应的IMP指针,该指针指向的是方法对应的实现。如果想要对方法进行hook,那么可以考虑通过替换方法对应的IMP指针。

话不多说,直接来看代码:

//1.
Method method = class_getInstanceMethod(cls, sel);
const char *methodTypeEncoding = method_getTypeEncoding(method);
NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:methodTypeEncoding];
NSUInteger argumentsNum = signature.numberOfArguments;
//2.
ffi_type **argTypes = malloc(sizeof(ffi_type *) * (argumentsNum));
argTypes[0] = &ffi_type_pointer;
argTypes[1] = &ffi_type_pointer;
for (int i = 2; i < argumentsNum; i ++) {
    const char *argType = [signature getArgumentTypeAtIndex:i];
    argTypes[i] = [self ffi_typeForTypeEncoding:argType];
}

ffi_type *returnType = [self ffi_typeForTypeEncoding:signature.methodReturnType];
//3.
ffi_cif *cif = malloc(sizeof(ffi_cif));
ffi_status status = ffi_prep_cif(cif, FFI_DEFAULT_ABI, (int)argumentsNum, returnType, argTypes);
if (status != FFI_OK) {
    NSLog(@"ffi_prep_cif return: %u", status);
    return;
}
void *methodInvoke;
ffi_closure *closure = ffi_closure_alloc(sizeof(ffi_closure), &methodInvoke);
status = ffi_prep_closure_loc(closure, cif, methodInvokeFunc, (__bridge void *)self, methodInvoke);
if (status != FFI_OK) {
    NSLog(@"ffi_prep_closure_loc return: %u", status);
    return;
}
  1. 通过传入的ClassSEL,可以获取到具体的method对象,然后根据methodtypeEncoding获取到方法的函数签名。
  2. 因为函数签名的参数中前两个参数分别为方法本身和_cmd,所以参数解析直接从第三个参数开始。
  3. 获取到对应的参数和返回值后,下面的过程就和block hook的处理一致。
IMP originalIMP = method_getImplementation(method);
self.originalIMP = originalIMP;
IMP replaceIMP = methodInvoke;
if (!class_addMethod(cls, sel, replaceIMP, methodTypeEncoding)) {
    class_replaceMethod(cls, sel, replaceIMP, methodTypeEncoding);
}

获取到methodIMP指针,然后通过addMethodreplaceMethod的方法将上面ffi_prep_closure_loc处理的指针替换方法原来的IMP指针。

这样当方法被调用时,methodInvoke指针就会被触发,其绑定的回调方法methodInvokeFunc就会被调用。

void methodInvokeFunc(ffi_cif *cif, void *ret, void **args, void *userdata) {
    JBlockHook *hook = (__bridge JBlockHook *)userdata;
    JBlockHookMode mode = hook.mode;
    id handleBlock = hook.handleBlock;
    IMP originalIMP = hook.originalIMP;
    
    switch (mode) {
        case JBlockHookModeBefore:{
            invokeHandleBlock(handleBlock, args, NO);
            invokeOriginalBlockOrMethod(cif, ret, args, (void *)originalIMP);
        }
            break;
        case JBlockHookModeInstead: {
            invokeHandleBlock(handleBlock, args, NO);
        }
            break;
        case JBlockHookModeAfter: {
            invokeOriginalBlockOrMethod(cif, ret, args, (void *)originalIMP);
            invokeHandleBlock(handleBlock, args, NO);
        }
            break;
    }
}

这里的处理方式与BlockInvokeFunc类似,根据mode的值,分别进行不同的操作。

void invokeHandleBlock(id handleBlock, void **args, BOOL isBlock) {
    NSMethodSignature *signature = NSMethodSignatureForBlock(handleBlock);
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
    int offset = isBlock ? 1 : 2;
    for (int i = 0; i < signature.numberOfArguments-1; i ++) {
        [invocation setArgument:args[i+offset] atIndex:i+1];
    }
    [invocation invokeWithTarget:handleBlock];
}

这里要注意的是args对于method hook需要从第三个位置取值,因为前两个位置分别放置了self_cmd

至此,method的hook工作就完成了,外部调用如下:

- (void)libffiMethod:(NSString *)value {
    NSLog(@"libffi method call: %@", value);
}

- (void)libffiHookMethod {
    [JBlockHook hookSel:@selector(libffiMethod:) forCls:self.class mode:JBlockHookModeInstead handleBlock:^(NSString *value){
        NSLog(@"hook method call instead with : %@", value);
    }];
}

输出结果:
hook method call instead with : hook-method

小结

通过替换方法的IMP指针即可达到hook method目的,与hook block的原理类似。

四、总结

笔者通过这几天对libffi库的学习,发现libffi的使用简洁,但功能却非常强大,非常适合做一些hook操作。目前,GitHub上也有两个使用libffi来实现hook的开源库BlockHookstinger,值得大家去探究学习。

参考资料